import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:umeng_verify_sdk/umeng_verify_sdk.dart'; import '../../../common/config/app_h5_urls.dart'; import '../../../common/tracking/umeng_service.dart'; import '../utils/one_key_login_result.dart'; enum OneKeyLoginOutcome { success, fallback, cancelled, unsupported, busy } enum OneKeyLoginPlatform { android, ios, unsupported } typedef OneKeyLoginCallback = void Function(Object? result); abstract interface class OneKeyLoginGateway { OneKeyLoginPlatform get platform; void setTokenCallback(OneKeyLoginCallback? callback); void setUiCallback(OneKeyLoginCallback? callback); void setWidgetCallback(OneKeyLoginCallback? callback); Future requestLogin(UMCustomModel uiConfig); void closeAuthPage(); void clearCallbacks(); } class UmengOneKeyLoginGateway implements OneKeyLoginGateway { const UmengOneKeyLoginGateway(); @override OneKeyLoginPlatform get platform { if (kIsWeb) return OneKeyLoginPlatform.unsupported; if (Platform.isAndroid) return OneKeyLoginPlatform.android; if (Platform.isIOS) return OneKeyLoginPlatform.ios; return OneKeyLoginPlatform.unsupported; } @override void setTokenCallback(OneKeyLoginCallback? callback) { if (platform == OneKeyLoginPlatform.ios) { UmengVerifySdk.getLoginTokenCallback(callback); } else if (platform == OneKeyLoginPlatform.android) { UmengVerifySdk.setTokenResultCallback_android(callback); } } @override void setUiCallback(OneKeyLoginCallback? callback) { if (platform == OneKeyLoginPlatform.android) { UmengVerifySdk.setUIClickCallback_android(callback); } } @override void setWidgetCallback(OneKeyLoginCallback? callback) { if (platform == OneKeyLoginPlatform.ios) { UmengVerifySdk.getWidgetEventCallback(callback); } } @override Future requestLogin(UMCustomModel uiConfig) async { await UmengVerifySdk.getLoginTokenWithTimeout(3, uiConfig); } @override void closeAuthPage() { if (platform == OneKeyLoginPlatform.ios) { unawaited( UmengVerifySdk.cancelLoginVCAnimated( true, ).catchError((Object _) => null), ); } else if (platform == OneKeyLoginPlatform.android) { UmengVerifySdk.quitLoginPage_android(); } } @override void clearCallbacks() { UmengVerifySdk.getLoginTokenCallback(null); UmengVerifySdk.getWidgetEventCallback(null); UmengVerifySdk.setTokenResultCallback_android(null); UmengVerifySdk.setUIClickCallback_android(null); } } class OneKeyLoginSdkResult { const OneKeyLoginSdkResult({ required this.outcome, this.token, this.code, this.reason, }); final OneKeyLoginOutcome outcome; final String? token; final String? code; final String? reason; } class OneKeyLoginSdkSession { OneKeyLoginSdkSession( this._gateway, { this.onNativeRequestDispatched, this.onAuthPageOpened, }); final OneKeyLoginGateway _gateway; final VoidCallback? onNativeRequestDispatched; final VoidCallback? onAuthPageOpened; final _result = Completer(); var _started = false; var _terminal = false; var _closed = false; var _authPageOpened = false; Timer? _resumeCancellation; Future start(UMCustomModel uiConfig) { if (_started) return _result.future; _started = true; if (_gateway.platform == OneKeyLoginPlatform.unsupported) { _complete( const OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.unsupported, reason: 'unsupported_platform', ), ); return _result.future; } // Availability checking already pre-accelerates the carrier page. Start // the token request directly here; the SDK performs its own preflight when // needed and reports its timeout/error through the registered callback. unawaited(_startNativeLogin(uiConfig)); return _result.future; } Future _startNativeLogin(UMCustomModel uiConfig) async { try { _gateway.setTokenCallback(_handleTokenResult); _gateway.setWidgetCallback((Object? _) {}); _gateway.setUiCallback(_handleUiResult); final nativeRequest = _gateway.requestLogin(uiConfig); onNativeRequestDispatched?.call(); await nativeRequest; } catch (_) { final completed = _complete( const OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.unsupported, reason: 'launch_error', ), ); if (completed) close(); } } void _handleUiResult(Object? result) { final code = extractUmengLoginCode(result); if (isUmengCancel(result)) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.cancelled, code: code, reason: 'cancel', ), ); } else if (isUmengManualSwitch(result)) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.fallback, code: code, reason: 'manual_switch', ), ); } } void _handleTokenResult(Object? result) { final code = extractUmengLoginCode(result); // On iOS, 700002 only means the user tapped the authorization page's // login button. It is followed by a separate 600000 (or 600xxx) token // callback, so completing the session here discards that callback. if (_gateway.platform == OneKeyLoginPlatform.ios && code == '700002') { return; } // Android `checkEnvAvailable_android` reports 600024 through the same // token callback. It means the terminal supports authentication, not that // this login attempt failed; wait for the actual login callback instead. if (isUmengEnvironmentAvailable(result)) { return; } if (isUmengAuthPage(result)) { if (_authPageOpened) return; _authPageOpened = true; onAuthPageOpened?.call(); return; } if (isUmengCancel(result)) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.cancelled, code: code, reason: 'cancel', ), ); return; } if (isUmengManualSwitch(result)) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.fallback, code: code, reason: 'manual_switch', ), ); return; } if (isUmengFallbackLoginCode(result)) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.fallback, code: code, reason: 'sdk_fallback', ), ); return; } if (!isUmengTokenSuccess(result) || result is! Map) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.fallback, code: code, reason: 'token_failed', ), ); return; } final token = result['token']?.toString(); if (token == null || token.isEmpty) { _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.fallback, code: code, reason: 'token_empty', ), ); return; } _complete( OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.success, token: token, code: code, ), ); } bool _complete(OneKeyLoginSdkResult result) { if (_terminal) return false; _terminal = true; _resumeCancellation?.cancel(); _resumeCancellation = null; _gateway.clearCallbacks(); _result.complete(result); return true; } void cancel() { _complete( const OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.cancelled, reason: 'disposed', ), ); close(); } void onAppResumed() { if (_gateway.platform != OneKeyLoginPlatform.android || !_authPageOpened || _terminal || _resumeCancellation != null) { return; } _resumeCancellation = Timer(const Duration(milliseconds: 500), () { _resumeCancellation = null; final completed = _complete( const OneKeyLoginSdkResult( outcome: OneKeyLoginOutcome.cancelled, reason: 'auth_page_returned', ), ); if (completed) close(); }); } void close() { if (_closed) return; _closed = true; _gateway.closeAuthPage(); } } class OneKeyLoginService { OneKeyLoginService({ OneKeyLoginGateway gateway = const UmengOneKeyLoginGateway(), Future Function() initializeSdk = UmengService.initOneKeyLoginIfAllowed, }) : _gateway = gateway, _initializeSdk = initializeSdk; final OneKeyLoginGateway _gateway; final Future Function() _initializeSdk; OneKeyLoginSdkSession? _activeSession; Future? _activeOperation; _OneKeyLoginPhase _phase = _OneKeyLoginPhase.idle; int _generation = 0; Future start({ required BuildContext context, required Future Function(String loginToken) verifyToken, required bool appIsDark, required bool nativeIsDark, VoidCallback? onNativeRequestDispatched, VoidCallback? onAuthPageOpened, }) { if (_phase == _OneKeyLoginPhase.authPageVisible || _phase == _OneKeyLoginPhase.verifying) { return Future.value(OneKeyLoginOutcome.busy); } if (_phase == _OneKeyLoginPhase.launching) { _activeSession?.cancel(); _activeSession = null; } final generation = ++_generation; _phase = _OneKeyLoginPhase.launching; final operation = _start( generation: generation, context: context, verifyToken: verifyToken, appIsDark: appIsDark, nativeIsDark: nativeIsDark, onNativeRequestDispatched: () { if (generation != _generation) return; onNativeRequestDispatched?.call(); }, onAuthPageOpened: () { if (generation != _generation) return; _phase = _OneKeyLoginPhase.authPageVisible; onAuthPageOpened?.call(); }, ); _activeOperation = operation; return operation.whenComplete(() { if (generation == _generation && identical(_activeOperation, operation)) { _activeOperation = null; _phase = _OneKeyLoginPhase.idle; } }); } Future _start({ required int generation, required BuildContext context, required Future Function(String loginToken) verifyToken, required bool appIsDark, required bool nativeIsDark, VoidCallback? onNativeRequestDispatched, VoidCallback? onAuthPageOpened, }) async { if (_gateway.platform == OneKeyLoginPlatform.unsupported) { return OneKeyLoginOutcome.unsupported; } await _initializeSdk(); if (generation != _generation) return OneKeyLoginOutcome.cancelled; if (!context.mounted) return OneKeyLoginOutcome.cancelled; final uiConfig = _buildUiConfig(context, appIsDark, nativeIsDark); final session = OneKeyLoginSdkSession( _gateway, onNativeRequestDispatched: onNativeRequestDispatched, onAuthPageOpened: onAuthPageOpened, ); _activeSession = session; try { final sdkResult = await session.start(uiConfig); if (!context.mounted) { session.close(); return OneKeyLoginOutcome.cancelled; } if (sdkResult.outcome != OneKeyLoginOutcome.success) { session.close(); return sdkResult.outcome; } final token = sdkResult.token; if (token == null || token.isEmpty) { session.close(); return OneKeyLoginOutcome.fallback; } if (generation == _generation) { _phase = _OneKeyLoginPhase.verifying; } await verifyToken(token); session.close(); return OneKeyLoginOutcome.success; } catch (_) { session.close(); return OneKeyLoginOutcome.fallback; } finally { if (identical(_activeSession, session)) { _activeSession = null; } } } void cancel() { _generation++; _activeSession?.cancel(); _activeSession = null; _activeOperation = null; _phase = _OneKeyLoginPhase.idle; } void onAppResumed() { _activeSession?.onAppResumed(); } UMCustomModel _buildUiConfig( BuildContext context, bool appIsDark, bool nativeIsDark, ) { final width = MediaQuery.of(context).size.width; // UVerify follows Android's system theme, which can differ from Flutter's // active theme. iOS colors are fixed at launch, so it receives a palette // resolved from the device appearance. final useDarkPalette = _gateway.platform == OneKeyLoginPlatform.ios && nativeIsDark; final pageBg = useDarkPalette ? const Color(0xFF0F0F0F) : Colors.white; final titleInk = useDarkPalette ? const Color(0xFFF0F0F0) : const Color(0xFF252529); final hintInk = useDarkPalette ? const Color(0xFFA1A1AA) : const Color(0xFF8E8E93); const buttonInk = Color(0xFF0F1A00); final transparent = Colors.transparent.toARGB32(); final titleColor = titleInk.toARGB32(); final hintColor = hintInk.toARGB32(); final buttonTextColor = buttonInk.toARGB32(); final bgColor = pageBg.toARGB32(); final protocolUrl = AppH5UrlsHelper.buildUrlWithNight( AppH5Urls.userProtocolUrl, appIsDark, ); final privacyUrl = AppH5UrlsHelper.buildUrlWithNight( AppH5Urls.privacyUrl, appIsDark, ); final model = UMCustomModel(); final titleWidget = UMCustomWidget('title', UMCustomWidgetType.textView) ..title = '金值' ..titleFont = 28 ..textAlignment = UMCustomWidgetTextAlignmentType.center ..top = 26 ..width = width.toInt() ..height = 54 ..titleColor = titleColor ..isClickEnable = false ..rootViewId = UMRootViewId.body; model.logoIsHidden = true; model.numberColor = titleColor; model.numberFont = 32; model.sloganText = ['', bgColor, 0]; model.navTitle = ['', transparent, 0]; model.privacyPreText = '我已阅读并同意'; model.privacyOne = ['《隐私政策》', privacyUrl]; model.privacyTwo = ['《用户协议》', protocolUrl]; model.privacyColors = [hintColor, titleColor]; model.changeBtnTitle = ['其他手机号登录', titleColor, 16]; model.privacyOperatorPreText = '《'; model.privacyOperatorSufText = '》'; model.autoHideLoginLoading = false; model.customWidget = [titleWidget.toJsonMap()]; if (_gateway.platform == OneKeyLoginPlatform.ios) { model.loginBtnText = ['本机号码一键登录', buttonTextColor, 20]; model.loginBtnBgImgs_ios = [ 'jinzhi_onetap_button', 'jinzhi_onetap_button', 'jinzhi_onetap_button_pressed', ]; model.backgroundColor_ios = bgColor; model.navColor = bgColor; // Always use our paired assets so both appearances keep the same iOS // chevron geometry and differ only in color. model.navBackImage = useDarkPalette ? 'jinzhi_onetap_back_night' : 'jinzhi_onetap_back_day'; model.privacyConectTexts = ['和', '']; model.privacyFont = 13; model.checkBoxImages = [ 'jinzhi_onetap_unchecked', 'jinzhi_onetap_checked', ]; // 原生 SDK 会按该尺寸直接放大图片;36pt 会让协议勾选显得过重。 model.checkBoxWH = 22; model.checkBoxIsChecked = true; } else { model.numberFrame = [-1, 156, -1, -1]; // Keep the same Android SDK frame as caizhi. The capsule's visible // geometry is determined by the native drawable's intrinsic size. model.loginBtnFrame = [-1, 283, -1, -1, 56]; model.changeBtnFrame = [368, -1, -1, -1]; model.privacyFrame = [-1, 427, -1, -1]; // Android only accepts width/height here. The back drawable itself uses // caizhi's 48×120 transparent canvas to position the arrow correctly. model.navBackButtonFrame = [-1, -1, 72, 120]; model.loginBtnText = ['本机号码一键登录', buttonTextColor, 16]; model.loginBtnBgImg_android = 'jinzhi_onetap_button'; model.navBackImage = 'jinzhi_onetap_back_daynight'; model.navColor = transparent; model.privacyFont = 12; model.privacyConectTexts = ['和', '、']; model.checkBoxWH = 14; model.checkBoxIsChecked = false; model.checkBoxImages = [ 'jinzhi_onetap_unchecked', 'jinzhi_onetap_checked', ]; model.privacyNavColor = bgColor; model.privacyNavTitleColor = titleColor; model.privacyNavTitleFont = 16; } return model; } } enum _OneKeyLoginPhase { idle, launching, authPageVisible, verifying }