Files
jinzhi/lib/features/auth/application/one_key_login_service.dart
T

584 lines
18 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<Object?> accelerate();
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<Object?> accelerate() {
return UmengVerifySdk.accelerateLoginPageWithTimeout(3);
}
@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;
}
// 预热必须由用户显式点击触发;不在启动或隐私同意阶段调用它。
unawaited(_startNativeLogin(uiConfig));
return _result.future;
}
Future<void> _startNativeLogin(UMCustomModel uiConfig) async {
try {
// 与 jinzhi-flutter-app 保持一致。未预热时 SDK 会在此处隐式预热,
// 在部分 Android 机型上更容易超时或直接回调失败。
final preheat = await _gateway.accelerate();
if (kDebugMode && preheat is Map && preheat['ret'] != null) {
// `ret` 是运营商预热失败码;插件原生层未将它输出到 logcat。
// 预热失败不阻断接下来的 getLoginToken,SDK 会自行再试一次。
debugPrint(
'OneKeyLogin preheat failed: vendor=${preheat['vendor']}, '
'ret=${preheat['ret']}',
);
}
if (_terminal) return;
_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);
if (kDebugMode && !isUmengTokenSuccess(result)) {
final map = result is Map ? result : const <Object?, Object?>{};
// 本地插件原先丢弃了 onTokenFailed 的 ret;只记录诊断字段,绝不记录 token。
debugPrint(
'OneKeyLogin Android/iOS SDK result: code=$code, '
'msg=${map['msg']}, vendor=${map['vendorName']}, '
'carrierError=${map['carrierFailedResultData']}',
);
}
// 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 }