Commit a2f703b24134c757f205ee1126f856fb149b499c
1 parent
b6098908
feat(ui):增加微信登录
(cherry picked from commit d6f4e780) # Conflicts: # lib/app/bootstrap/app_bootstrap.dart # lib/app/modules/login/controllers/login_controller.dart # lib/app/modules/login/views/login_view.dart # lib/data/local/user_preferences_storage.dart # lib/pigeon/wechat_api.g.dart # pigeon/wechat_api.dart
Showing
18 changed files
with
298 additions
and
11 deletions
| @@ -30,6 +30,7 @@ target 'Runner' do | @@ -30,6 +30,7 @@ target 'Runner' do | ||
| 30 | use_frameworks! | 30 | use_frameworks! |
| 31 | pod 'OBS', :path => './Runner/Library' | 31 | pod 'OBS', :path => './Runner/Library' |
| 32 | pod 'GoogleSignIn' | 32 | pod 'GoogleSignIn' |
| 33 | + pod 'WechatOpenSDK', '~> 2.0' | ||
| 33 | 34 | ||
| 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) | 35 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) |
| 35 | end | 36 | end |
| @@ -94,6 +94,7 @@ PODS: | @@ -94,6 +94,7 @@ PODS: | ||
| 94 | - webview_flutter_wkwebview (0.0.1): | 94 | - webview_flutter_wkwebview (0.0.1): |
| 95 | - Flutter | 95 | - Flutter |
| 96 | - FlutterMacOS | 96 | - FlutterMacOS |
| 97 | + - WechatOpenSDK (2.0.7) | ||
| 97 | 98 | ||
| 98 | DEPENDENCIES: | 99 | DEPENDENCIES: |
| 99 | - Flutter (from `Flutter`) | 100 | - Flutter (from `Flutter`) |
| @@ -110,6 +111,7 @@ DEPENDENCIES: | @@ -110,6 +111,7 @@ DEPENDENCIES: | ||
| 110 | - thinking_analytics (from `.symlinks/plugins/thinking_analytics/ios`) | 111 | - thinking_analytics (from `.symlinks/plugins/thinking_analytics/ios`) |
| 111 | - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) | 112 | - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) |
| 112 | - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) | 113 | - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) |
| 114 | + - WechatOpenSDK (~> 2.0) | ||
| 113 | 115 | ||
| 114 | SPEC REPOS: | 116 | SPEC REPOS: |
| 115 | trunk: | 117 | trunk: |
| @@ -127,6 +129,7 @@ SPEC REPOS: | @@ -127,6 +129,7 @@ SPEC REPOS: | ||
| 127 | - ThinkingDataCore | 129 | - ThinkingDataCore |
| 128 | - ThinkingSDK | 130 | - ThinkingSDK |
| 129 | - TOCropViewController | 131 | - TOCropViewController |
| 132 | + - WechatOpenSDK | ||
| 130 | 133 | ||
| 131 | EXTERNAL SOURCES: | 134 | EXTERNAL SOURCES: |
| 132 | Flutter: | 135 | Flutter: |
| @@ -184,7 +187,8 @@ SPEC CHECKSUMS: | @@ -184,7 +187,8 @@ SPEC CHECKSUMS: | ||
| 184 | TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 | 187 | TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 |
| 185 | video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 | 188 | video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 |
| 186 | webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 | 189 | webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 |
| 190 | + WechatOpenSDK: fbc76decf307d241a750c9e03b687e4d11aacf37 | ||
| 187 | 191 | ||
| 188 | -PODFILE CHECKSUM: fe3137a8e1c8e115fcac0a685d21e651b2d2788b | 192 | +PODFILE CHECKSUM: 186a2af126028b1ccaa73baa82e32ae092583b1a |
| 189 | 193 | ||
| 190 | COCOAPODS: 1.16.2 | 194 | COCOAPODS: 1.16.2 |
| @@ -40,6 +40,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { | @@ -40,6 +40,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { | ||
| 40 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? | 40 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? |
| 41 | ) -> Bool { | 41 | ) -> Bool { |
| 42 | setup() | 42 | setup() |
| 43 | + WeChatLoginBridge.shared.registerApp() | ||
| 43 | cacheLaunchURLIfNeeded(launchOptions) | 44 | cacheLaunchURLIfNeeded(launchOptions) |
| 44 | return true | 45 | return true |
| 45 | } | 46 | } |
| @@ -49,6 +50,9 @@ class AppDelegate: NSObject, UIApplicationDelegate { | @@ -49,6 +50,9 @@ class AppDelegate: NSObject, UIApplicationDelegate { | ||
| 49 | open url: URL, | 50 | open url: URL, |
| 50 | options: [UIApplication.OpenURLOptionsKey: Any] = [:] | 51 | options: [UIApplication.OpenURLOptionsKey: Any] = [:] |
| 51 | ) -> Bool { | 52 | ) -> Bool { |
| 53 | + if WeChatLoginBridge.shared.handleOpenURL(url) { | ||
| 54 | + return true | ||
| 55 | + } | ||
| 52 | if GIDSignIn.sharedInstance.handle(url) { | 56 | if GIDSignIn.sharedInstance.handle(url) { |
| 53 | return true | 57 | return true |
| 54 | } | 58 | } |
| @@ -61,6 +65,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { | @@ -61,6 +65,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { | ||
| 61 | continue userActivity: NSUserActivity, | 65 | continue userActivity: NSUserActivity, |
| 62 | restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void | 66 | restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void |
| 63 | ) -> Bool { | 67 | ) -> Bool { |
| 68 | + if userActivity.activityType == NSUserActivityTypeBrowsingWeb, | ||
| 69 | + WeChatLoginBridge.shared.handleUniversalLink(userActivity) { | ||
| 70 | + return true | ||
| 71 | + } | ||
| 64 | guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, | 72 | guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, |
| 65 | let urlString = userActivity.webpageURL?.absoluteString else { | 73 | let urlString = userActivity.webpageURL?.absoluteString else { |
| 66 | return false | 74 | return false |
| @@ -32,6 +32,16 @@ | @@ -32,6 +32,16 @@ | ||
| 32 | <string>doublefeel</string> | 32 | <string>doublefeel</string> |
| 33 | </array> | 33 | </array> |
| 34 | </dict> | 34 | </dict> |
| 35 | + <dict> | ||
| 36 | + <key>CFBundleTypeRole</key> | ||
| 37 | + <string>Editor</string> | ||
| 38 | + <key>CFBundleURLName</key> | ||
| 39 | + <string>com.luiz.doublefeel.wechat</string> | ||
| 40 | + <key>CFBundleURLSchemes</key> | ||
| 41 | + <array> | ||
| 42 | + <string>wx42b603acf3dd68f5</string> | ||
| 43 | + </array> | ||
| 44 | + </dict> | ||
| 35 | </array> | 45 | </array> |
| 36 | <key>NSAppTransportSecurity</key> | 46 | <key>NSAppTransportSecurity</key> |
| 37 | <dict> | 47 | <dict> |
| @@ -45,7 +55,12 @@ | @@ -45,7 +55,12 @@ | ||
| 45 | <string>ymail</string> | 55 | <string>ymail</string> |
| 46 | <string>readdle-spark</string> | 56 | <string>readdle-spark</string> |
| 47 | <string>mqqapi</string> | 57 | <string>mqqapi</string> |
| 58 | + <string>weixin</string> | ||
| 59 | + <string>weixinULAPI</string> | ||
| 60 | + <string>weixinURLParamsAPI</string> | ||
| 48 | </array> | 61 | </array> |
| 62 | + <key>WXAppID</key> | ||
| 63 | + <string>wx42b603acf3dd68f5</string> | ||
| 49 | <key>NSHealthShareUsageDescription</key> | 64 | <key>NSHealthShareUsageDescription</key> |
| 50 | <string>需要读取 Apple Health 中的心率、HRV、睡眠、步数、活动能量等数据,用于展示健康状态并同步到 Apple Watch。</string> | 65 | <string>需要读取 Apple Health 中的心率、HRV、睡眠、步数、活动能量等数据,用于展示健康状态并同步到 Apple Watch。</string> |
| 51 | <key>GIDClientID</key> | 66 | <key>GIDClientID</key> |
| @@ -24,6 +24,16 @@ | @@ -24,6 +24,16 @@ | ||
| 24 | <string>doublefeel</string> | 24 | <string>doublefeel</string> |
| 25 | </array> | 25 | </array> |
| 26 | </dict> | 26 | </dict> |
| 27 | + <dict> | ||
| 28 | + <key>CFBundleTypeRole</key> | ||
| 29 | + <string>Editor</string> | ||
| 30 | + <key>CFBundleURLName</key> | ||
| 31 | + <string>com.luiz.doublefeel.wechat</string> | ||
| 32 | + <key>CFBundleURLSchemes</key> | ||
| 33 | + <array> | ||
| 34 | + <string>wx42b603acf3dd68f5</string> | ||
| 35 | + </array> | ||
| 36 | + </dict> | ||
| 27 | </array> | 37 | </array> |
| 28 | <key>NSAppTransportSecurity</key> | 38 | <key>NSAppTransportSecurity</key> |
| 29 | <dict> | 39 | <dict> |
| @@ -37,7 +47,12 @@ | @@ -37,7 +47,12 @@ | ||
| 37 | <string>ymail</string> | 47 | <string>ymail</string> |
| 38 | <string>readdle-spark</string> | 48 | <string>readdle-spark</string> |
| 39 | <string>mqqapi</string> | 49 | <string>mqqapi</string> |
| 50 | + <string>weixin</string> | ||
| 51 | + <string>weixinULAPI</string> | ||
| 52 | + <string>weixinURLParamsAPI</string> | ||
| 40 | </array> | 53 | </array> |
| 54 | + <key>WXAppID</key> | ||
| 55 | + <string>wx42b603acf3dd68f5</string> | ||
| 41 | <key>NSHealthShareUsageDescription</key> | 56 | <key>NSHealthShareUsageDescription</key> |
| 42 | <string>需要读取 Apple Health 中的心率、HRV、睡眠、步数、活动能量等数据,用于展示健康状态并同步到 Apple Watch。</string> | 57 | <string>需要读取 Apple Health 中的心率、HRV、睡眠、步数、活动能量等数据,用于展示健康状态并同步到 Apple Watch。</string> |
| 43 | <key>NSHealthUpdateUsageDescription</key> | 58 | <key>NSHealthUpdateUsageDescription</key> |
| @@ -11,6 +11,7 @@ enum NativePigeonRegistrar { | @@ -11,6 +11,7 @@ enum NativePigeonRegistrar { | ||
| 11 | HealthKitRawDataHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: HealthKitRawDataHostApiImpl()) | 11 | HealthKitRawDataHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: HealthKitRawDataHostApiImpl()) |
| 12 | WearEngineHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WearEngineHostApiImpl()) | 12 | WearEngineHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WearEngineHostApiImpl()) |
| 13 | AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub()) | 13 | AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub()) |
| 14 | + WeChatHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WeChatLoginBridge.shared) | ||
| 14 | PlatformHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: PlatformHostApiImpl()) | 15 | PlatformHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: PlatformHostApiImpl()) |
| 15 | } | 16 | } |
| 16 | } | 17 | } |
ios/Runner/Pigeon/WeChatApi.swift
0 → 100644
| 1 | +// // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| 2 | +// Autogenerated from Pigeon (v25.5.0), do not edit directly. | ||
| 3 | +// See also: https://pub.dev/packages/pigeon | ||
| 4 | + | ||
| 5 | +import Foundation | ||
| 6 | + | ||
| 7 | +#if os(iOS) | ||
| 8 | + import Flutter | ||
| 9 | +#elseif os(macOS) | ||
| 10 | + import FlutterMacOS | ||
| 11 | +#else | ||
| 12 | + #error("Unsupported platform.") | ||
| 13 | +#endif | ||
| 14 | + | ||
| 15 | +private func wrapResult(_ result: Any?) -> [Any?] { | ||
| 16 | + return [result] | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +private func wrapError(_ error: Any) -> [Any?] { | ||
| 20 | + if let pigeonError = error as? PigeonError { | ||
| 21 | + return [ | ||
| 22 | + pigeonError.code, | ||
| 23 | + pigeonError.message, | ||
| 24 | + pigeonError.details, | ||
| 25 | + ] | ||
| 26 | + } | ||
| 27 | + if let flutterError = error as? FlutterError { | ||
| 28 | + return [ | ||
| 29 | + flutterError.code, | ||
| 30 | + flutterError.message, | ||
| 31 | + flutterError.details, | ||
| 32 | + ] | ||
| 33 | + } | ||
| 34 | + return [ | ||
| 35 | + "\(error)", | ||
| 36 | + "\(type(of: error))", | ||
| 37 | + "Stacktrace: \(Thread.callStackSymbols)", | ||
| 38 | + ] | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +private func isNullish(_ value: Any?) -> Bool { | ||
| 42 | + return value is NSNull || value == nil | ||
| 43 | +} | ||
| 44 | + | ||
| 45 | +private func nilOrValue<T>(_ value: Any?) -> T? { | ||
| 46 | + if value is NSNull { return nil } | ||
| 47 | + return value as! T? | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | + | ||
| 51 | +private class WeChatApiPigeonCodecReader: FlutterStandardReader { | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +private class WeChatApiPigeonCodecWriter: FlutterStandardWriter { | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +private class WeChatApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { | ||
| 58 | + override func reader(with data: Data) -> FlutterStandardReader { | ||
| 59 | + return WeChatApiPigeonCodecReader(data: data) | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + override func writer(with data: NSMutableData) -> FlutterStandardWriter { | ||
| 63 | + return WeChatApiPigeonCodecWriter(data: data) | ||
| 64 | + } | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +class WeChatApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { | ||
| 68 | + static let shared = WeChatApiPigeonCodec(readerWriter: WeChatApiPigeonCodecReaderWriter()) | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | + | ||
| 72 | +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. | ||
| 73 | +protocol WeChatHostApi { | ||
| 74 | + /// Opens WeChat and returns its one-time authorization code. | ||
| 75 | + func requestAuthorizationCode(completion: @escaping (Result<String, Error>) -> Void) | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. | ||
| 79 | +class WeChatHostApiSetup { | ||
| 80 | + static var codec: FlutterStandardMessageCodec { WeChatApiPigeonCodec.shared } | ||
| 81 | + /// Sets up an instance of `WeChatHostApi` to handle messages through the `binaryMessenger`. | ||
| 82 | + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: WeChatHostApi?, messageChannelSuffix: String = "") { | ||
| 83 | + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" | ||
| 84 | + /// Opens WeChat and returns its one-time authorization code. | ||
| 85 | + let requestAuthorizationCodeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WeChatHostApi.requestAuthorizationCode\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 86 | + if let api = api { | ||
| 87 | + requestAuthorizationCodeChannel.setMessageHandler { _, reply in | ||
| 88 | + api.requestAuthorizationCode { result in | ||
| 89 | + switch result { | ||
| 90 | + case .success(let res): | ||
| 91 | + reply(wrapResult(res)) | ||
| 92 | + case .failure(let error): | ||
| 93 | + reply(wrapError(error)) | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | + } else { | ||
| 98 | + requestAuthorizationCodeChannel.setMessageHandler(nil) | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | +} |
| @@ -58,6 +58,9 @@ private struct RunnerLaunchView: View { | @@ -58,6 +58,9 @@ private struct RunnerLaunchView: View { | ||
| 58 | .onAppear { | 58 | .onAppear { |
| 59 | appDelegate.setup() | 59 | appDelegate.setup() |
| 60 | }.onOpenURL { url in | 60 | }.onOpenURL { url in |
| 61 | + if WeChatLoginBridge.shared.handleOpenURL(url) { | ||
| 62 | + return | ||
| 63 | + } | ||
| 61 | appDelegate.handleFlutterUrl(url.absoluteString) | 64 | appDelegate.handleFlutterUrl(url.absoluteString) |
| 62 | } | 65 | } |
| 63 | } | 66 | } |
ios/Runner/WeChatLoginBridge.swift
0 → 100644
| 1 | +import Foundation | ||
| 2 | +import UIKit | ||
| 3 | + | ||
| 4 | +/// Delivers the native WeChat authorization callback to Flutter's login flow. | ||
| 5 | +final class WeChatLoginBridge: NSObject, WeChatHostApi, WXApiDelegate { | ||
| 6 | + static let shared = WeChatLoginBridge() | ||
| 7 | + | ||
| 8 | + private let appId = "wx42b603acf3dd68f5" | ||
| 9 | + private var pendingCompletion: ((Result<String, Error>) -> Void)? | ||
| 10 | + | ||
| 11 | + private override init() {} | ||
| 12 | + | ||
| 13 | + func registerApp() { | ||
| 14 | + WXApi.registerApp(appId, universalLink: "") | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + func requestAuthorizationCode(completion: @escaping (Result<String, Error>) -> Void) { | ||
| 18 | + DispatchQueue.main.async { | ||
| 19 | + guard self.pendingCompletion == nil else { | ||
| 20 | + completion(.failure(PigeonError( | ||
| 21 | + code: "wechat_authorization_in_progress", | ||
| 22 | + message: "WeChat authorization is already in progress.", | ||
| 23 | + details: nil | ||
| 24 | + ))) | ||
| 25 | + return | ||
| 26 | + } | ||
| 27 | + guard WXApi.isWXAppInstalled() else { | ||
| 28 | + completion(.failure(PigeonError( | ||
| 29 | + code: "wechat_not_installed", | ||
| 30 | + message: "WeChat is not installed.", | ||
| 31 | + details: nil | ||
| 32 | + ))) | ||
| 33 | + return | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + let request = SendAuthReq() | ||
| 37 | + request.scope = "snsapi_userinfo" | ||
| 38 | + request.state = UUID().uuidString | ||
| 39 | + self.pendingCompletion = completion | ||
| 40 | + WXApi.send(request) { success in | ||
| 41 | + if !success { | ||
| 42 | + self.finish(.failure(PigeonError( | ||
| 43 | + code: "wechat_request_failed", | ||
| 44 | + message: "Unable to open WeChat.", | ||
| 45 | + details: nil | ||
| 46 | + ))) | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + func handleOpenURL(_ url: URL) -> Bool { | ||
| 53 | + WXApi.handleOpen(url, delegate: self) | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + func handleUniversalLink(_ userActivity: NSUserActivity) -> Bool { | ||
| 57 | + WXApi.handleOpenUniversalLink(userActivity, delegate: self) | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + func onReq(_ req: BaseReq) {} | ||
| 61 | + | ||
| 62 | + func onResp(_ resp: BaseResp) { | ||
| 63 | + guard let authResponse = resp as? SendAuthResp else { return } | ||
| 64 | + if authResponse.errCode == 0, | ||
| 65 | + let code = authResponse.code, | ||
| 66 | + !code.isEmpty { | ||
| 67 | + finish(.success(code)) | ||
| 68 | + return | ||
| 69 | + } | ||
| 70 | + let message = authResponse.errStr.isEmpty | ||
| 71 | + ? "WeChat authorization was cancelled or failed." | ||
| 72 | + : authResponse.errStr | ||
| 73 | + finish(.failure(PigeonError( | ||
| 74 | + code: "wechat_authorization_failed", | ||
| 75 | + message: message, | ||
| 76 | + details: nil | ||
| 77 | + ))) | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + private func finish(_ result: Result<String, Error>) { | ||
| 81 | + let completion = pendingCompletion | ||
| 82 | + pendingCompletion = nil | ||
| 83 | + completion?(result) | ||
| 84 | + } | ||
| 85 | +} |
| @@ -44,7 +44,11 @@ abstract final class AppBootstrap { | @@ -44,7 +44,11 @@ abstract final class AppBootstrap { | ||
| 44 | localStorage: local, | 44 | localStorage: local, |
| 45 | ).dependencies(); | 45 | ).dependencies(); |
| 46 | 46 | ||
| 47 | + // `UserPreferencesStorage.init` runs before GetX dependencies exist. Sync | ||
| 48 | + // here so every cold start invokes native `updateLoginInfo` after the | ||
| 49 | + // PlatformHostApi and HealthRawDataCoreService are ready. | ||
| 47 | await userPreferencesStorage.syncLoginInfoToNative(); | 50 | await userPreferencesStorage.syncLoginInfoToNative(); |
| 51 | + | ||
| 48 | NativeHealthDataUpdateBridge.initialize(); | 52 | NativeHealthDataUpdateBridge.initialize(); |
| 49 | 53 | ||
| 50 | LoadingService.init(); | 54 | LoadingService.init(); |
| @@ -84,6 +88,8 @@ abstract final class AppBootstrap { | @@ -84,6 +88,8 @@ abstract final class AppBootstrap { | ||
| 84 | localStorage: local, | 88 | localStorage: local, |
| 85 | ).dependencies(); | 89 | ).dependencies(); |
| 86 | 90 | ||
| 91 | + await userPreferencesStorage.syncLoginInfoToNative(); | ||
| 92 | + | ||
| 87 | NativeHealthDataUpdateBridge.initialize(); | 93 | NativeHealthDataUpdateBridge.initialize(); |
| 88 | // 2. 后台无 UI,不需要初始化全局 Loading 弹窗 | 94 | // 2. 后台无 UI,不需要初始化全局 Loading 弹窗 |
| 89 | LoadingService.init(); | 95 | LoadingService.init(); |
| @@ -8,6 +8,17 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | @@ -8,6 +8,17 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 8 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | 8 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; |
| 9 | import 'package:doublefeel_flutter/core/constants/app_const.dart'; | 9 | import 'package:doublefeel_flutter/core/constants/app_const.dart'; |
| 10 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 10 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 11 | +import 'package:doublefeel_flutter/core/services/app_config_service.dart'; | ||
| 12 | +import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | ||
| 13 | +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | ||
| 14 | +import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart'; | ||
| 15 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 16 | +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | ||
| 17 | +import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart'; | ||
| 18 | +import 'package:flutter/services.dart'; | ||
| 19 | +import 'package:flutter/widgets.dart'; | ||
| 20 | +import 'package:get/get.dart'; | ||
| 21 | +import 'package:doublefeel_flutter/core/util/app_toast.dart'; | ||
| 11 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 22 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 12 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; | 23 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; |
| 13 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | 24 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; |
| @@ -499,7 +510,6 @@ class LoginController extends GetxController { | @@ -499,7 +510,6 @@ class LoginController extends GetxController { | ||
| 499 | try { | 510 | try { |
| 500 | await LoadingService.instance.run(() async { | 511 | await LoadingService.instance.run(() async { |
| 501 | final loginRes = await _userApi.loginWithWechat(code); | 512 | final loginRes = await _userApi.loginWithWechat(code); |
| 502 | - | ||
| 503 | if (loginRes is AppSuccess<LoginResponse>) { | 513 | if (loginRes is AppSuccess<LoginResponse>) { |
| 504 | await _handleLoginSuccess(loginRes.data, wechatCode: code); | 514 | await _handleLoginSuccess(loginRes.data, wechatCode: code); |
| 505 | await Get.find<LocalStorage>().setLastLoginMethod('wechat'); | 515 | await Get.find<LocalStorage>().setLastLoginMethod('wechat'); |
| @@ -234,7 +234,8 @@ class LoginView extends GetView<LoginController> { | @@ -234,7 +234,8 @@ class LoginView extends GetView<LoginController> { | ||
| 234 | ), | 234 | ), |
| 235 | ), | 235 | ), |
| 236 | ], | 236 | ], |
| 237 | - if (isOhos()) ...[ | 237 | + if (region == AppRegion.china) ...[ |
| 238 | + const SizedBox(height: 12), | ||
| 238 | _LoginMethodButton( | 239 | _LoginMethodButton( |
| 239 | width: buttonWidth, | 240 | width: buttonWidth, |
| 240 | label: '通过微信登录', | 241 | label: '通过微信登录', |
| @@ -235,6 +235,8 @@ class AppleHealthRawDataCoreService { | @@ -235,6 +235,8 @@ class AppleHealthRawDataCoreService { | ||
| 235 | if (running != null) { | 235 | if (running != null) { |
| 236 | return running; | 236 | return running; |
| 237 | } | 237 | } |
| 238 | + // Flush rows from prior calculations without delaying a new calculation. | ||
| 239 | + unawaited(_flushNativeUpload(reason: 'before_calculation')); | ||
| 238 | final task = _runWithLog( | 240 | final task = _runWithLog( |
| 239 | action: 'startCoreCaculate', | 241 | action: 'startCoreCaculate', |
| 240 | body: () => _readCalculateAndStore( | 242 | body: () => _readCalculateAndStore( |
| @@ -245,7 +247,11 @@ class AppleHealthRawDataCoreService { | @@ -245,7 +247,11 @@ class AppleHealthRawDataCoreService { | ||
| 245 | ); | 247 | ); |
| 246 | _coreCalculation = task; | 248 | _coreCalculation = task; |
| 247 | try { | 249 | try { |
| 248 | - return await task; | 250 | + final result = await task; |
| 251 | + // `_readCalculateAndStore` has committed all four result tables by now. | ||
| 252 | + // Trigger a second pass without delaying downstream UI or notifications. | ||
| 253 | + unawaited(_flushNativeUpload(reason: 'after_calculation')); | ||
| 254 | + return result; | ||
| 249 | } finally { | 255 | } finally { |
| 250 | if (identical(_coreCalculation, task)) { | 256 | if (identical(_coreCalculation, task)) { |
| 251 | _coreCalculation = null; | 257 | _coreCalculation = null; |
| @@ -253,6 +259,29 @@ class AppleHealthRawDataCoreService { | @@ -253,6 +259,29 @@ class AppleHealthRawDataCoreService { | ||
| 253 | } | 259 | } |
| 254 | } | 260 | } |
| 255 | 261 | ||
| 262 | + Future<void> _flushNativeUpload({required String reason}) async { | ||
| 263 | + try { | ||
| 264 | + final succeeded = await performHealthDataUpload(); | ||
| 265 | + AppLogger.d('Native health upload $reason succeeded=$succeeded'); | ||
| 266 | + await _saveLocalNotificationDebugEvent(<String, Object?>{ | ||
| 267 | + 'event': 'flutterNativeHealthUploadFinished', | ||
| 268 | + 'identifier': 'flutter.native_health_upload.finished', | ||
| 269 | + 'reason': reason, | ||
| 270 | + 'success': succeeded, | ||
| 271 | + }); | ||
| 272 | + } catch (error, stackTrace) { | ||
| 273 | + // Upload failures must leave the calculation path alive. SQLite rows stay | ||
| 274 | + // pending and will be retried by the next pre/post-calculation flush. | ||
| 275 | + _logError('native health upload $reason failed', error, stackTrace); | ||
| 276 | + await _saveLocalNotificationDebugEvent(<String, Object?>{ | ||
| 277 | + 'event': 'flutterNativeHealthUploadFailed', | ||
| 278 | + 'identifier': 'flutter.native_health_upload.failed', | ||
| 279 | + 'reason': reason, | ||
| 280 | + 'error': error.toString(), | ||
| 281 | + }); | ||
| 282 | + } | ||
| 283 | + } | ||
| 284 | + | ||
| 256 | Future<HealthRawStressCalculationResult> syncAndStore({ | 285 | Future<HealthRawStressCalculationResult> syncAndStore({ |
| 257 | int? startTime, | 286 | int? startTime, |
| 258 | int? endTime, | 287 | int? endTime, |
| @@ -81,8 +81,9 @@ class UserPreferencesStorage { | @@ -81,8 +81,9 @@ class UserPreferencesStorage { | ||
| 81 | .updateLoginInfo(jsonEncode(userInfo), env.serverBaseUrl); | 81 | .updateLoginInfo(jsonEncode(userInfo), env.serverBaseUrl); |
| 82 | await _syncHealthRawDatabaseToNative(); | 82 | await _syncHealthRawDatabaseToNative(); |
| 83 | } | 83 | } |
| 84 | - } on Exception { | ||
| 85 | - return; | 84 | + AppLogger.d('Native login info synced for cold start'); |
| 85 | + } catch (error, stackTrace) { | ||
| 86 | + AppLogger.e('Native login info sync failed', error, stackTrace); | ||
| 86 | } | 87 | } |
| 87 | } | 88 | } |
| 88 | 89 |
| @@ -16,7 +16,6 @@ PlatformException _createConnectionError(String channelName) { | @@ -16,7 +16,6 @@ PlatformException _createConnectionError(String channelName) { | ||
| 16 | ); | 16 | ); |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | - | ||
| 20 | class _PigeonCodec extends StandardMessageCodec { | 19 | class _PigeonCodec extends StandardMessageCodec { |
| 21 | const _PigeonCodec(); | 20 | const _PigeonCodec(); |
| 22 | @override | 21 | @override |
| @@ -42,9 +41,11 @@ class WeChatHostApi { | @@ -42,9 +41,11 @@ class WeChatHostApi { | ||
| 42 | /// Constructor for [WeChatHostApi]. The [binaryMessenger] named argument is | 41 | /// Constructor for [WeChatHostApi]. The [binaryMessenger] named argument is |
| 43 | /// available for dependency injection. If it is left null, the default | 42 | /// available for dependency injection. If it is left null, the default |
| 44 | /// BinaryMessenger will be used which routes to the host platform. | 43 | /// BinaryMessenger will be used which routes to the host platform. |
| 45 | - WeChatHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) | 44 | + WeChatHostApi( |
| 45 | + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) | ||
| 46 | : pigeonVar_binaryMessenger = binaryMessenger, | 46 | : pigeonVar_binaryMessenger = binaryMessenger, |
| 47 | - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; | 47 | + pigeonVar_messageChannelSuffix = |
| 48 | + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; | ||
| 48 | final BinaryMessenger? pigeonVar_binaryMessenger; | 49 | final BinaryMessenger? pigeonVar_binaryMessenger; |
| 49 | 50 | ||
| 50 | static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); | 51 | static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); |
| @@ -53,8 +54,10 @@ class WeChatHostApi { | @@ -53,8 +54,10 @@ class WeChatHostApi { | ||
| 53 | 54 | ||
| 54 | /// Opens WeChat and returns its one-time authorization code. | 55 | /// Opens WeChat and returns its one-time authorization code. |
| 55 | Future<String> requestAuthorizationCode() async { | 56 | Future<String> requestAuthorizationCode() async { |
| 56 | - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WeChatHostApi.requestAuthorizationCode$pigeonVar_messageChannelSuffix'; | ||
| 57 | - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | 57 | + final String pigeonVar_channelName = |
| 58 | + 'dev.flutter.pigeon.doublefeel_flutter.WeChatHostApi.requestAuthorizationCode$pigeonVar_messageChannelSuffix'; | ||
| 59 | + final BasicMessageChannel<Object?> pigeonVar_channel = | ||
| 60 | + BasicMessageChannel<Object?>( | ||
| 58 | pigeonVar_channelName, | 61 | pigeonVar_channelName, |
| 59 | pigeonChannelCodec, | 62 | pigeonChannelCodec, |
| 60 | binaryMessenger: pigeonVar_binaryMessenger, | 63 | binaryMessenger: pigeonVar_binaryMessenger, |
| @@ -5,6 +5,8 @@ import 'package:pigeon/pigeon.dart'; | @@ -5,6 +5,8 @@ import 'package:pigeon/pigeon.dart'; | ||
| 5 | dartOut: 'lib/pigeon/wechat_api.g.dart', | 5 | dartOut: 'lib/pigeon/wechat_api.g.dart', |
| 6 | dartPackageName: 'doublefeel_flutter', | 6 | dartPackageName: 'doublefeel_flutter', |
| 7 | dartOptions: DartOptions(), | 7 | dartOptions: DartOptions(), |
| 8 | + swiftOut: 'ios/Runner/Pigeon/WeChatApi.swift', | ||
| 9 | + swiftOptions: SwiftOptions(), | ||
| 8 | arkTSOut: 'ohos/entry/src/main/ets/pigeon/WeChatApi.ets', | 10 | arkTSOut: 'ohos/entry/src/main/ets/pigeon/WeChatApi.ets', |
| 9 | copyrightHeader: 'pigeon/copyright.txt', | 11 | copyrightHeader: 'pigeon/copyright.txt', |
| 10 | ), | 12 | ), |
| @@ -8,6 +8,7 @@ dart run pigeon --input pigeon/alipay_api.dart | @@ -8,6 +8,7 @@ dart run pigeon --input pigeon/alipay_api.dart | ||
| 8 | dart run pigeon --input pigeon/wechat_api.dart | 8 | dart run pigeon --input pigeon/wechat_api.dart |
| 9 | dart run pigeon --input pigeon/platform_api.dart | 9 | dart run pigeon --input pigeon/platform_api.dart |
| 10 | dart run pigeon --input pigeon/health_kit_raw_data_api.dart | 10 | dart run pigeon --input pigeon/health_kit_raw_data_api.dart |
| 11 | +dart run pigeon --input pigeon/wechat_api.dart | ||
| 11 | 12 | ||
| 12 | 13 | ||
| 13 | # Pigeon emits PigeonError in every swiftOut file; Runner needs a single definition. | 14 | # Pigeon emits PigeonError in every swiftOut file; Runner needs a single definition. |
-
Please register or login to post a comment