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
use_frameworks!
pod 'OBS', :path => './Runner/Library'
pod 'GoogleSignIn'
pod 'WechatOpenSDK', '~> 2.0'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
... ...
... ... @@ -94,6 +94,7 @@ PODS:
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- WechatOpenSDK (2.0.7)
DEPENDENCIES:
- Flutter (from `Flutter`)
... ... @@ -110,6 +111,7 @@ DEPENDENCIES:
- thinking_analytics (from `.symlinks/plugins/thinking_analytics/ios`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
- WechatOpenSDK (~> 2.0)
SPEC REPOS:
trunk:
... ... @@ -127,6 +129,7 @@ SPEC REPOS:
- ThinkingDataCore
- ThinkingSDK
- TOCropViewController
- WechatOpenSDK
EXTERNAL SOURCES:
Flutter:
... ... @@ -184,7 +187,8 @@ SPEC CHECKSUMS:
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK: fbc76decf307d241a750c9e03b687e4d11aacf37
PODFILE CHECKSUM: fe3137a8e1c8e115fcac0a685d21e651b2d2788b
PODFILE CHECKSUM: 186a2af126028b1ccaa73baa82e32ae092583b1a
COCOAPODS: 1.16.2
... ...
... ... @@ -40,6 +40,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
setup()
WeChatLoginBridge.shared.registerApp()
cacheLaunchURLIfNeeded(launchOptions)
return true
}
... ... @@ -49,6 +50,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
if WeChatLoginBridge.shared.handleOpenURL(url) {
return true
}
if GIDSignIn.sharedInstance.handle(url) {
return true
}
... ... @@ -61,6 +65,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
WeChatLoginBridge.shared.handleUniversalLink(userActivity) {
return true
}
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let urlString = userActivity.webpageURL?.absoluteString else {
return false
... ...
... ... @@ -32,6 +32,16 @@
<string>doublefeel</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.luiz.doublefeel.wechat</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx42b603acf3dd68f5</string>
</array>
</dict>
</array>
<key>NSAppTransportSecurity</key>
<dict>
... ... @@ -45,7 +55,12 @@
<string>ymail</string>
<string>readdle-spark</string>
<string>mqqapi</string>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
</array>
<key>WXAppID</key>
<string>wx42b603acf3dd68f5</string>
<key>NSHealthShareUsageDescription</key>
<string>需要读取 Apple Health 中的心率、HRV、睡眠、步数、活动能量等数据,用于展示健康状态并同步到 Apple Watch。</string>
<key>GIDClientID</key>
... ...
... ... @@ -24,6 +24,16 @@
<string>doublefeel</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.luiz.doublefeel.wechat</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx42b603acf3dd68f5</string>
</array>
</dict>
</array>
<key>NSAppTransportSecurity</key>
<dict>
... ... @@ -37,7 +47,12 @@
<string>ymail</string>
<string>readdle-spark</string>
<string>mqqapi</string>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
</array>
<key>WXAppID</key>
<string>wx42b603acf3dd68f5</string>
<key>NSHealthShareUsageDescription</key>
<string>需要读取 Apple Health 中的心率、HRV、睡眠、步数、活动能量等数据,用于展示健康状态并同步到 Apple Watch。</string>
<key>NSHealthUpdateUsageDescription</key>
... ...
... ... @@ -11,6 +11,7 @@ enum NativePigeonRegistrar {
HealthKitRawDataHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: HealthKitRawDataHostApiImpl())
WearEngineHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WearEngineHostApiImpl())
AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub())
WeChatHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: WeChatLoginBridge.shared)
PlatformHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: PlatformHostApiImpl())
}
}
... ...
// // Copyright 2013 The Flutter Authors. All rights reserved.
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
}
private func wrapError(_ error: Any) -> [Any?] {
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
private func isNullish(_ value: Any?) -> Bool {
return value is NSNull || value == nil
}
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}
private class WeChatApiPigeonCodecReader: FlutterStandardReader {
}
private class WeChatApiPigeonCodecWriter: FlutterStandardWriter {
}
private class WeChatApiPigeonCodecReaderWriter: FlutterStandardReaderWriter {
override func reader(with data: Data) -> FlutterStandardReader {
return WeChatApiPigeonCodecReader(data: data)
}
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
return WeChatApiPigeonCodecWriter(data: data)
}
}
class WeChatApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
static let shared = WeChatApiPigeonCodec(readerWriter: WeChatApiPigeonCodecReaderWriter())
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol WeChatHostApi {
/// Opens WeChat and returns its one-time authorization code.
func requestAuthorizationCode(completion: @escaping (Result<String, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class WeChatHostApiSetup {
static var codec: FlutterStandardMessageCodec { WeChatApiPigeonCodec.shared }
/// Sets up an instance of `WeChatHostApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: WeChatHostApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
/// Opens WeChat and returns its one-time authorization code.
let requestAuthorizationCodeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WeChatHostApi.requestAuthorizationCode\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
requestAuthorizationCodeChannel.setMessageHandler { _, reply in
api.requestAuthorizationCode { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
requestAuthorizationCodeChannel.setMessageHandler(nil)
}
}
}
... ...
#import "GeneratedPluginRegistrant.h"
#import <WechatOpenSDK/WXApi.h>
... ...
... ... @@ -58,6 +58,9 @@ private struct RunnerLaunchView: View {
.onAppear {
appDelegate.setup()
}.onOpenURL { url in
if WeChatLoginBridge.shared.handleOpenURL(url) {
return
}
appDelegate.handleFlutterUrl(url.absoluteString)
}
}
... ...
import Foundation
import UIKit
/// Delivers the native WeChat authorization callback to Flutter's login flow.
final class WeChatLoginBridge: NSObject, WeChatHostApi, WXApiDelegate {
static let shared = WeChatLoginBridge()
private let appId = "wx42b603acf3dd68f5"
private var pendingCompletion: ((Result<String, Error>) -> Void)?
private override init() {}
func registerApp() {
WXApi.registerApp(appId, universalLink: "")
}
func requestAuthorizationCode(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.async {
guard self.pendingCompletion == nil else {
completion(.failure(PigeonError(
code: "wechat_authorization_in_progress",
message: "WeChat authorization is already in progress.",
details: nil
)))
return
}
guard WXApi.isWXAppInstalled() else {
completion(.failure(PigeonError(
code: "wechat_not_installed",
message: "WeChat is not installed.",
details: nil
)))
return
}
let request = SendAuthReq()
request.scope = "snsapi_userinfo"
request.state = UUID().uuidString
self.pendingCompletion = completion
WXApi.send(request) { success in
if !success {
self.finish(.failure(PigeonError(
code: "wechat_request_failed",
message: "Unable to open WeChat.",
details: nil
)))
}
}
}
}
func handleOpenURL(_ url: URL) -> Bool {
WXApi.handleOpen(url, delegate: self)
}
func handleUniversalLink(_ userActivity: NSUserActivity) -> Bool {
WXApi.handleOpenUniversalLink(userActivity, delegate: self)
}
func onReq(_ req: BaseReq) {}
func onResp(_ resp: BaseResp) {
guard let authResponse = resp as? SendAuthResp else { return }
if authResponse.errCode == 0,
let code = authResponse.code,
!code.isEmpty {
finish(.success(code))
return
}
let message = authResponse.errStr.isEmpty
? "WeChat authorization was cancelled or failed."
: authResponse.errStr
finish(.failure(PigeonError(
code: "wechat_authorization_failed",
message: message,
details: nil
)))
}
private func finish(_ result: Result<String, Error>) {
let completion = pendingCompletion
pendingCompletion = nil
completion?(result)
}
}
... ...
... ... @@ -44,7 +44,11 @@ abstract final class AppBootstrap {
localStorage: local,
).dependencies();
// `UserPreferencesStorage.init` runs before GetX dependencies exist. Sync
// here so every cold start invokes native `updateLoginInfo` after the
// PlatformHostApi and HealthRawDataCoreService are ready.
await userPreferencesStorage.syncLoginInfoToNative();
NativeHealthDataUpdateBridge.initialize();
LoadingService.init();
... ... @@ -84,6 +88,8 @@ abstract final class AppBootstrap {
localStorage: local,
).dependencies();
await userPreferencesStorage.syncLoginInfoToNative();
NativeHealthDataUpdateBridge.initialize();
// 2. 后台无 UI,不需要初始化全局 Loading 弹窗
LoadingService.init();
... ...
... ... @@ -8,6 +8,17 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/constants/app_const.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/services/app_config_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
... ... @@ -499,7 +510,6 @@ class LoginController extends GetxController {
try {
await LoadingService.instance.run(() async {
final loginRes = await _userApi.loginWithWechat(code);
if (loginRes is AppSuccess<LoginResponse>) {
await _handleLoginSuccess(loginRes.data, wechatCode: code);
await Get.find<LocalStorage>().setLastLoginMethod('wechat');
... ...
... ... @@ -234,7 +234,8 @@ class LoginView extends GetView<LoginController> {
),
),
],
if (isOhos()) ...[
if (region == AppRegion.china) ...[
const SizedBox(height: 12),
_LoginMethodButton(
width: buttonWidth,
label: '通过微信登录',
... ...
... ... @@ -235,6 +235,8 @@ class AppleHealthRawDataCoreService {
if (running != null) {
return running;
}
// Flush rows from prior calculations without delaying a new calculation.
unawaited(_flushNativeUpload(reason: 'before_calculation'));
final task = _runWithLog(
action: 'startCoreCaculate',
body: () => _readCalculateAndStore(
... ... @@ -245,7 +247,11 @@ class AppleHealthRawDataCoreService {
);
_coreCalculation = task;
try {
return await task;
final result = await task;
// `_readCalculateAndStore` has committed all four result tables by now.
// Trigger a second pass without delaying downstream UI or notifications.
unawaited(_flushNativeUpload(reason: 'after_calculation'));
return result;
} finally {
if (identical(_coreCalculation, task)) {
_coreCalculation = null;
... ... @@ -253,6 +259,29 @@ class AppleHealthRawDataCoreService {
}
}
Future<void> _flushNativeUpload({required String reason}) async {
try {
final succeeded = await performHealthDataUpload();
AppLogger.d('Native health upload $reason succeeded=$succeeded');
await _saveLocalNotificationDebugEvent(<String, Object?>{
'event': 'flutterNativeHealthUploadFinished',
'identifier': 'flutter.native_health_upload.finished',
'reason': reason,
'success': succeeded,
});
} catch (error, stackTrace) {
// Upload failures must leave the calculation path alive. SQLite rows stay
// pending and will be retried by the next pre/post-calculation flush.
_logError('native health upload $reason failed', error, stackTrace);
await _saveLocalNotificationDebugEvent(<String, Object?>{
'event': 'flutterNativeHealthUploadFailed',
'identifier': 'flutter.native_health_upload.failed',
'reason': reason,
'error': error.toString(),
});
}
}
Future<HealthRawStressCalculationResult> syncAndStore({
int? startTime,
int? endTime,
... ...
... ... @@ -81,8 +81,9 @@ class UserPreferencesStorage {
.updateLoginInfo(jsonEncode(userInfo), env.serverBaseUrl);
await _syncHealthRawDatabaseToNative();
}
} on Exception {
return;
AppLogger.d('Native login info synced for cold start');
} catch (error, stackTrace) {
AppLogger.e('Native login info sync failed', error, stackTrace);
}
}
... ...
... ... @@ -16,7 +16,6 @@ PlatformException _createConnectionError(String channelName) {
);
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
... ... @@ -42,9 +41,11 @@ class WeChatHostApi {
/// Constructor for [WeChatHostApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
WeChatHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
WeChatHostApi(
{BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
pigeonVar_messageChannelSuffix =
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
... ... @@ -53,8 +54,10 @@ class WeChatHostApi {
/// Opens WeChat and returns its one-time authorization code.
Future<String> requestAuthorizationCode() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WeChatHostApi.requestAuthorizationCode$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
final String pigeonVar_channelName =
'dev.flutter.pigeon.doublefeel_flutter.WeChatHostApi.requestAuthorizationCode$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
... ...
... ... @@ -5,6 +5,8 @@ import 'package:pigeon/pigeon.dart';
dartOut: 'lib/pigeon/wechat_api.g.dart',
dartPackageName: 'doublefeel_flutter',
dartOptions: DartOptions(),
swiftOut: 'ios/Runner/Pigeon/WeChatApi.swift',
swiftOptions: SwiftOptions(),
arkTSOut: 'ohos/entry/src/main/ets/pigeon/WeChatApi.ets',
copyrightHeader: 'pigeon/copyright.txt',
),
... ...
... ... @@ -8,6 +8,7 @@ dart run pigeon --input pigeon/alipay_api.dart
dart run pigeon --input pigeon/wechat_api.dart
dart run pigeon --input pigeon/platform_api.dart
dart run pigeon --input pigeon/health_kit_raw_data_api.dart
dart run pigeon --input pigeon/wechat_api.dart
# Pigeon emits PigeonError in every swiftOut file; Runner needs a single definition.
... ...