Commit a2f703b24134c757f205ee1126f856fb149b499c

Authored by 权海
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
@@ -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 }
  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 +}
1 #import "GeneratedPluginRegistrant.h" 1 #import "GeneratedPluginRegistrant.h"
  2 +#import <WechatOpenSDK/WXApi.h>
@@ -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 }
  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.