Files
jinzhi/lib/features/auth/application/one_key_login_service.dart
T
2026-09-04 11:41:22 +08:00

557 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> 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<void> 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<OneKeyLoginSdkResult>();
var _started = false;
var _terminal = false;
var _closed = false;
var _authPageOpened = false;
Timer? _resumeCancellation;
Future<OneKeyLoginSdkResult> 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<void> _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<void> Function() initializeSdk =
UmengService.initOneKeyLoginIfAllowed,
}) : _gateway = gateway,
_initializeSdk = initializeSdk;
final OneKeyLoginGateway _gateway;
final Future<void> Function() _initializeSdk;
OneKeyLoginSdkSession? _activeSession;
Future<OneKeyLoginOutcome>? _activeOperation;
_OneKeyLoginPhase _phase = _OneKeyLoginPhase.idle;
int _generation = 0;
Future<OneKeyLoginOutcome> start({
required BuildContext context,
required Future<void> Function(String loginToken) verifyToken,
required bool appIsDark,
required bool nativeIsDark,
VoidCallback? onNativeRequestDispatched,
VoidCallback? onAuthPageOpened,
}) {
if (_phase == _OneKeyLoginPhase.authPageVisible ||
_phase == _OneKeyLoginPhase.verifying) {
return Future<OneKeyLoginOutcome>.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<OneKeyLoginOutcome> _start({
required int generation,
required BuildContext context,
required Future<void> 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 }