diff --git a/android/app/src/main/res/drawable-night-xhdpi/jinzhi_onetap_back_daynight.png b/android/app/src/main/res/drawable-night-xhdpi/jinzhi_onetap_back_daynight.png
new file mode 100644
index 0000000..305ac19
Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/jinzhi_onetap_back_daynight.png differ
diff --git a/android/app/src/main/res/drawable-night/jinzhi_onetap_back_daynight.xml b/android/app/src/main/res/drawable-night/jinzhi_onetap_back_daynight.xml
new file mode 100644
index 0000000..181f897
--- /dev/null
+++ b/android/app/src/main/res/drawable-night/jinzhi_onetap_back_daynight.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_back.png b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_back.png
new file mode 100644
index 0000000..d84cc9e
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_back.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_back_daynight.png b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_back_daynight.png
new file mode 100644
index 0000000..465d06f
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_back_daynight.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_button.png b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_button.png
new file mode 100644
index 0000000..a84ca6f
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_button.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_button_pressed.png b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_button_pressed.png
new file mode 100644
index 0000000..cc64c57
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_button_pressed.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_checked.png b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_checked.png
new file mode 100644
index 0000000..deb090a
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_checked.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_unchecked.png b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_unchecked.png
new file mode 100644
index 0000000..1bfff56
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/jinzhi_onetap_unchecked.png differ
diff --git a/android/app/src/main/res/drawable/jinzhi_onetap_back_daynight.xml b/android/app/src/main/res/drawable/jinzhi_onetap_back_daynight.xml
new file mode 100644
index 0000000..fab349f
--- /dev/null
+++ b/android/app/src/main/res/drawable/jinzhi_onetap_back_daynight.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_back.imageset/Contents.json
new file mode 100644
index 0000000..ecd626a
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_back.imageset/Contents.json
@@ -0,0 +1 @@
+{"images":[{"filename":"jinzhi_onetap_back.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back.imageset/jinzhi_onetap_back.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back.imageset/jinzhi_onetap_back.png
new file mode 100644
index 0000000..d84cc9e
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back.imageset/jinzhi_onetap_back.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/Contents.json
new file mode 100644
index 0000000..adbb168
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "filename" : "jinzhi_onetap_back_day.png", "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "jinzhi_onetap_back_day@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "jinzhi_onetap_back_day@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day.png
new file mode 100644
index 0000000..27ab3be
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day@2x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day@2x.png
new file mode 100644
index 0000000..f0ddd07
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day@3x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day@3x.png
new file mode 100644
index 0000000..3d3ebd4
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_day.imageset/jinzhi_onetap_back_day@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/Contents.json
new file mode 100644
index 0000000..7adef82
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "filename" : "jinzhi_onetap_back_night.png", "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "jinzhi_onetap_back_night@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "jinzhi_onetap_back_night@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night.png
new file mode 100644
index 0000000..7de7bf7
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night@2x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night@2x.png
new file mode 100644
index 0000000..b9a8951
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night@3x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night@3x.png
new file mode 100644
index 0000000..08783ce
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_back_night.imageset/jinzhi_onetap_back_night@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/Contents.json
new file mode 100644
index 0000000..b040bcf
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "filename" : "jinzhi_onetap_button.png", "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "jinzhi_onetap_button@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "jinzhi_onetap_button@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button.png
new file mode 100644
index 0000000..52e624b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button@2x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button@2x.png
new file mode 100644
index 0000000..cd20779
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button@3x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button@3x.png
new file mode 100644
index 0000000..21a2c92
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_button.imageset/jinzhi_onetap_button@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/Contents.json
new file mode 100644
index 0000000..690d0ef
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/Contents.json
@@ -0,0 +1,8 @@
+{
+ "images" : [
+ { "filename" : "jinzhi_onetap_button_pressed.png", "idiom" : "universal", "scale" : "1x" },
+ { "filename" : "jinzhi_onetap_button_pressed@2x.png", "idiom" : "universal", "scale" : "2x" },
+ { "filename" : "jinzhi_onetap_button_pressed@3x.png", "idiom" : "universal", "scale" : "3x" }
+ ],
+ "info" : { "author" : "xcode", "version" : 1 }
+}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed.png
new file mode 100644
index 0000000..1bb4bae
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed@2x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed@2x.png
new file mode 100644
index 0000000..aadbfd8
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed@3x.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed@3x.png
new file mode 100644
index 0000000..ba29135
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_button_pressed.imageset/jinzhi_onetap_button_pressed@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_checked.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_checked.imageset/Contents.json
new file mode 100644
index 0000000..adf9b51
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_checked.imageset/Contents.json
@@ -0,0 +1 @@
+{"images":[{"filename":"jinzhi_onetap_checked.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_checked.imageset/jinzhi_onetap_checked.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_checked.imageset/jinzhi_onetap_checked.png
new file mode 100644
index 0000000..deb090a
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_checked.imageset/jinzhi_onetap_checked.png differ
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_unchecked.imageset/Contents.json b/ios/Runner/Assets.xcassets/jinzhi_onetap_unchecked.imageset/Contents.json
new file mode 100644
index 0000000..92165f0
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/jinzhi_onetap_unchecked.imageset/Contents.json
@@ -0,0 +1 @@
+{"images":[{"filename":"jinzhi_onetap_unchecked.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1}}
diff --git a/ios/Runner/Assets.xcassets/jinzhi_onetap_unchecked.imageset/jinzhi_onetap_unchecked.png b/ios/Runner/Assets.xcassets/jinzhi_onetap_unchecked.imageset/jinzhi_onetap_unchecked.png
new file mode 100644
index 0000000..1bfff56
Binary files /dev/null and b/ios/Runner/Assets.xcassets/jinzhi_onetap_unchecked.imageset/jinzhi_onetap_unchecked.png differ
diff --git a/lib/common/tracking/umeng_service.dart b/lib/common/tracking/umeng_service.dart
index 5126676..4d8a20d 100644
--- a/lib/common/tracking/umeng_service.dart
+++ b/lib/common/tracking/umeng_service.dart
@@ -29,11 +29,17 @@ abstract final class UmengService {
static Future _init() async {
await DeviceHeaderService.to.ensureInitialized();
- await UmengCommonSdk.initCommon(
- UmengConfig.androidAppKey,
- UmengConfig.iosAppKey,
- DeviceHeaderService.to.commonHeaders()['User-Channel'] ?? 'default',
- UmengConfig.androidPushSecret,
+ // Keep the carrier-login launch path independent of UMeng Common's
+ // network initialization. Waiting for this future is what left the login
+ // overlay spinning forever in the base project; U-Verify is configured
+ // immediately below by initOneKeyLoginIfAllowed, as in Yanting.
+ unawaited(
+ UmengCommonSdk.initCommon(
+ UmengConfig.androidAppKey,
+ UmengConfig.iosAppKey,
+ DeviceHeaderService.to.commonHeaders()['User-Channel'] ?? 'default',
+ UmengConfig.androidPushSecret,
+ ).catchError((Object _) {}),
);
_ready = true;
}
diff --git a/lib/features/auth/application/login_launcher.dart b/lib/features/auth/application/login_launcher.dart
index 28fb4a0..3acd315 100644
--- a/lib/features/auth/application/login_launcher.dart
+++ b/lib/features/auth/application/login_launcher.dart
@@ -8,10 +8,8 @@ import '../../../common/router/app_router.dart';
import 'auth_controller.dart';
import 'one_key_login_service.dart';
-/// Shared login entry: launch the carrier-owned authorization page first and
-/// use the Flutter SMS page only as a deliberate fallback.
abstract final class LoginLauncher {
- static final _service = OneKeyLoginService();
+ static final OneKeyLoginService _service = OneKeyLoginService();
static bool _starting = false;
static Future start(
@@ -21,71 +19,95 @@ abstract final class LoginLauncher {
}) async {
if (_starting) return;
_starting = true;
- final dismissLoading = _showLoading(context);
+
+ var loadingVisible = false;
+
+ void showLoading() {
+ if (loadingVisible || !context.mounted) return;
+ loadingVisible = true;
+ unawaited(
+ showDialog(
+ context: context,
+ useRootNavigator: true,
+ barrierDismissible: false,
+ builder: (_) => const PopScope(
+ canPop: false,
+ child: Center(child: CircularProgressIndicator()),
+ ),
+ ),
+ );
+ }
+
+ void dismissLoading() {
+ if (!loadingVisible || !context.mounted) return;
+ loadingVisible = false;
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+
try {
final observer = _OneKeyLoginLifecycleObserver(_service);
- WidgetsBinding.instance.addObserver(observer);
+ var observerAdded = false;
late final OneKeyLoginOutcome outcome;
try {
+ WidgetsBinding.instance.addObserver(observer);
+ observerAdded = true;
+ // Carrier-auth initialization only begins after this explicit login
+ // action. Keep the loading indicator visible while the native SDK
+ // determines whether one-key login can be launched.
+ showLoading();
outcome = await _service.start(
context: context,
verifyToken: ref.read(authProvider.notifier).verifyOneKeyLogin,
+ // The native carrier page follows the device, while linked H5 pages
+ // retain the appearance selected inside the app.
+ appIsDark: Theme.of(context).brightness == Brightness.dark,
+ nativeIsDark:
+ MediaQuery.platformBrightnessOf(context) == Brightness.dark,
onNativeRequestDispatched: dismissLoading,
onAuthPageOpened: dismissLoading,
);
- } catch (_) {
- outcome = OneKeyLoginOutcome.fallback;
} finally {
- WidgetsBinding.instance.removeObserver(observer);
+ dismissLoading();
+ if (observerAdded) WidgetsBinding.instance.removeObserver(observer);
}
+
if (!context.mounted) return;
- if (outcome == OneKeyLoginOutcome.success) {
- context.go(next?.isNotEmpty == true ? next! : AppRoutes.profile);
- return;
- }
- if (outcome == OneKeyLoginOutcome.fallback ||
- outcome == OneKeyLoginOutcome.unsupported) {
- final target = next == null || next.isEmpty
- ? AppRoutes.login
- : '${AppRoutes.login}?next=${Uri.encodeComponent(next)}';
- if (GoRouterState.of(context).matchedLocation != AppRoutes.login) {
- await context.push(target);
- }
+ switch (outcome) {
+ case OneKeyLoginOutcome.success:
+ if (next != null && next.isNotEmpty) GoRouter.of(context).go(next);
+ return;
+ case OneKeyLoginOutcome.cancelled:
+ case OneKeyLoginOutcome.busy:
+ return;
+ case OneKeyLoginOutcome.fallback:
+ case OneKeyLoginOutcome.unsupported:
+ await openSmsLogin(context, next: next);
}
} finally {
- dismissLoading();
_starting = false;
}
}
+}
- static VoidCallback _showLoading(BuildContext context) {
- var visible = true;
- unawaited(
- showDialog(
- context: context,
- useRootNavigator: true,
- barrierDismissible: false,
- builder: (_) => const PopScope(
- canPop: false,
- child: Center(child: CircularProgressIndicator()),
- ),
- ),
- );
- return () {
- if (visible && context.mounted) {
- Navigator.of(context, rootNavigator: true).pop();
- }
- visible = false;
- };
- }
+Future openSmsLogin(
+ BuildContext context, {
+ String? next,
+ Future Function()? onReturn,
+}) async {
+ if (!context.mounted) return;
+ if (GoRouterState.of(context).matchedLocation == AppRoutes.login) return;
+ final location = next == null || next.isEmpty
+ ? AppRoutes.login
+ : '${AppRoutes.login}?next=${Uri.encodeComponent(next)}';
+ await GoRouter.of(context).push(location);
+ if (context.mounted) await onReturn?.call();
}
class _OneKeyLoginLifecycleObserver with WidgetsBindingObserver {
- _OneKeyLoginLifecycleObserver(this._service);
- final OneKeyLoginService _service;
-
+ _OneKeyLoginLifecycleObserver(this.service);
+ final OneKeyLoginService service;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
- if (state == AppLifecycleState.resumed) _service.onAppResumed();
+ if (state == AppLifecycleState.resumed) service.onAppResumed();
}
}
diff --git a/lib/features/auth/application/one_key_login_service.dart b/lib/features/auth/application/one_key_login_service.dart
index 5a43d21..cfc8f03 100644
--- a/lib/features/auth/application/one_key_login_service.dart
+++ b/lib/features/auth/application/one_key_login_service.dart
@@ -7,139 +7,286 @@ 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 }
-/// Runs only after an explicit user login action. Do not add an availability
-/// probe or `accelerateLoginPage` outside this class: Xiaomi rejects that as
-/// background carrier-auth prefetch.
-class OneKeyLoginService {
- Future? _operation;
- bool _authPageOpened = false;
- bool _terminal = false;
- Timer? _resumeCancellation;
- void Function(OneKeyLoginOutcome)? _completeCurrent;
+enum OneKeyLoginPlatform { android, ios, unsupported }
- Future start({
- required BuildContext context,
- required Future Function(String token) verifyToken,
- VoidCallback? onNativeRequestDispatched,
- VoidCallback? onAuthPageOpened,
- }) {
- if (_operation != null) return Future.value(OneKeyLoginOutcome.busy);
- _terminal = false;
- _authPageOpened = false;
- final operation = _start(
- context: context,
- verifyToken: verifyToken,
- onNativeRequestDispatched: onNativeRequestDispatched,
- onAuthPageOpened: onAuthPageOpened,
- );
- _operation = operation;
- return operation.whenComplete(() {
- _operation = null;
- _completeCurrent = null;
- });
+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;
}
- Future _start({
- required BuildContext context,
- required Future Function(String token) verifyToken,
- VoidCallback? onNativeRequestDispatched,
- VoidCallback? onAuthPageOpened,
- }) async {
- if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) {
- return OneKeyLoginOutcome.unsupported;
+ @override
+ void setTokenCallback(OneKeyLoginCallback? callback) {
+ if (platform == OneKeyLoginPlatform.ios) {
+ UmengVerifySdk.getLoginTokenCallback(callback);
+ } else if (platform == OneKeyLoginPlatform.android) {
+ UmengVerifySdk.setTokenResultCallback_android(callback);
}
- final uiConfig = _uiConfig(context);
- await UmengService.initOneKeyLoginIfAllowed();
- final completer = Completer();
+ }
- void complete(OneKeyLoginOutcome outcome) {
- if (_terminal) return;
- _terminal = true;
- _resumeCancellation?.cancel();
- _resumeCancellation = null;
- _clearCallbacks();
- completer.complete(outcome);
+ @override
+ void setUiCallback(OneKeyLoginCallback? callback) {
+ if (platform == OneKeyLoginPlatform.android) {
+ UmengVerifySdk.setUIClickCallback_android(callback);
}
+ }
- _completeCurrent = complete;
-
- void handleUiResult(Object? result) {
- final code = _code(result);
- if (code == '700000') complete(OneKeyLoginOutcome.cancelled);
- if (code == '700001') complete(OneKeyLoginOutcome.fallback);
+ @override
+ void setWidgetCallback(OneKeyLoginCallback? callback) {
+ if (platform == OneKeyLoginPlatform.ios) {
+ UmengVerifySdk.getWidgetEventCallback(callback);
}
+ }
- void handleTokenResult(Object? result) {
- final code = _code(result);
- // iOS 700002 is only the native button click; token follows later.
- if (Platform.isIOS && code == '700002') return;
- // 600024 is Android environment availability, not a login result.
- if (code == '600024') return;
- if (code == '600001') {
- if (!_authPageOpened) {
- _authPageOpened = true;
- onAuthPageOpened?.call();
- }
- return;
- }
- if (code == '700000') {
- complete(OneKeyLoginOutcome.cancelled);
- return;
- }
- if (code == '700001') {
- complete(OneKeyLoginOutcome.fallback);
- return;
- }
- if (code != '600000' || result is! Map) {
- complete(OneKeyLoginOutcome.fallback);
- return;
- }
- final token = result['token']?.toString();
- if (token == null || token.isEmpty) {
- complete(OneKeyLoginOutcome.fallback);
- return;
- }
- unawaited(() async {
- try {
- await verifyToken(token);
- complete(OneKeyLoginOutcome.success);
- } catch (_) {
- complete(OneKeyLoginOutcome.fallback);
- } finally {
- _closeNativePage();
- }
- }());
- }
+ @override
+ Future requestLogin(UMCustomModel uiConfig) async {
+ await UmengVerifySdk.getLoginTokenWithTimeout(3, uiConfig);
+ }
- if (Platform.isIOS) {
- UmengVerifySdk.getLoginTokenCallback(handleTokenResult);
- UmengVerifySdk.getWidgetEventCallback((_) {});
- } else {
- UmengVerifySdk.setTokenResultCallback_android(handleTokenResult);
- UmengVerifySdk.setUIClickCallback_android(handleUiResult);
- }
- try {
- final request = UmengVerifySdk.getLoginTokenWithTimeout(3, uiConfig);
- onNativeRequestDispatched?.call();
+ @override
+ void closeAuthPage() {
+ if (platform == OneKeyLoginPlatform.ios) {
unawaited(
- request.catchError((Object _) {
- complete(OneKeyLoginOutcome.unsupported);
- }),
+ UmengVerifySdk.cancelLoginVCAnimated(
+ true,
+ ).catchError((Object _) => null),
);
- return await completer.future.timeout(
- const Duration(seconds: 20),
- onTimeout: () => OneKeyLoginOutcome.fallback,
- );
- } finally {
- _clearCallbacks();
+ } 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 (!Platform.isAndroid ||
+ if (_gateway.platform != OneKeyLoginPlatform.android ||
!_authPageOpened ||
_terminal ||
_resumeCancellation != null) {
@@ -147,72 +294,263 @@ class OneKeyLoginService {
}
_resumeCancellation = Timer(const Duration(milliseconds: 500), () {
_resumeCancellation = null;
- if (_terminal) return;
- _completeCurrent?.call(OneKeyLoginOutcome.cancelled);
- _closeNativePage();
+ final completed = _complete(
+ const OneKeyLoginSdkResult(
+ outcome: OneKeyLoginOutcome.cancelled,
+ reason: 'auth_page_returned',
+ ),
+ );
+ if (completed) close();
});
}
- UMCustomModel _uiConfig(BuildContext context) {
- const ink = Color(0xFF202124);
- const muted = Color(0xFF8A8F98);
- const background = Color(0xFFFFFFFF);
- final model = UMCustomModel()
- ..logoIsHidden = true
- ..numberColor = ink.toARGB32()
- ..numberFont = 30
- ..navTitle = ['', Colors.transparent.toARGB32(), 0]
- ..navColor = background.toARGB32()
- ..backgroundColor_ios = background.toARGB32()
- ..sloganText = ['', background.toARGB32(), 0]
- ..privacyPreText = '我已阅读并同意'
- ..privacyOne = ['《隐私政策》', AppH5Urls.privacyUrl]
- ..privacyTwo = ['《用户协议》', AppH5Urls.userProtocolUrl]
- ..privacyColors = [muted.toARGB32(), ink.toARGB32()]
- ..changeBtnTitle = ['其他手机号登录', ink.toARGB32(), 16]
- ..loginBtnText = ['本机号码一键登录', Colors.white.toARGB32(), 16]
- ..privacyFont = 12
- ..checkBoxIsChecked = false;
- if (Platform.isAndroid) {
- model.numberFrame = [-1, 156, -1, -1];
- model.loginBtnFrame = [-1, 283, -1, -1, 52];
- model.changeBtnFrame = [368, -1, -1, -1];
- model.privacyFrame = [-1, 427, -1, -1];
- } else {
- final title = UMCustomWidget('title', UMCustomWidgetType.textView)
- ..title = '金值'
- ..titleFont = 26
- ..titleColor = ink.toARGB32()
- ..width = MediaQuery.sizeOf(context).width.toInt()
- ..height = 54
- ..top = 26
- ..textAlignment = UMCustomWidgetTextAlignmentType.center
- ..isClickEnable = false
- ..rootViewId = UMRootViewId.body;
- model.customWidget = [title.toJsonMap()];
- model.privacyFont = 13;
- model.checkBoxWH = 20;
- model.checkBoxIsChecked = true;
- }
- return model;
- }
-
- String _code(Object? result) => result is Map
- ? (result['code'] ?? result['resultCode'] ?? '').toString()
- : '';
-
- void _clearCallbacks() {
- UmengVerifySdk.getLoginTokenCallback(null);
- UmengVerifySdk.getWidgetEventCallback(null);
- UmengVerifySdk.setTokenResultCallback_android(null);
- UmengVerifySdk.setUIClickCallback_android(null);
- }
-
- void _closeNativePage() {
- if (Platform.isIOS) {
- unawaited(UmengVerifySdk.cancelLoginVCAnimated(true));
- } else {
- UmengVerifySdk.quitLoginPage_android();
- }
+ 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 }
diff --git a/lib/features/auth/utils/one_key_login_result.dart b/lib/features/auth/utils/one_key_login_result.dart
new file mode 100644
index 0000000..bf2c395
--- /dev/null
+++ b/lib/features/auth/utils/one_key_login_result.dart
@@ -0,0 +1,40 @@
+const _umengFallbackCodes = {
+ '600002',
+ '600004',
+ '600005',
+ '600007',
+ '600008',
+ '600009',
+ '600010',
+ '600011',
+ '600012',
+ '600013',
+ '600014',
+ '600015',
+ '600017',
+ '600021',
+ '600023',
+ '600025',
+ '600026',
+};
+
+String? extractUmengLoginCode(Object? result) {
+ if (result is! Map) return null;
+ final code = result['code'] ?? result['resultCode'];
+ final value = code?.toString();
+ return value == null || value.isEmpty ? null : value;
+}
+
+bool isUmengTokenSuccess(Object? result) =>
+ extractUmengLoginCode(result) == '600000';
+bool isUmengAuthPage(Object? result) =>
+ extractUmengLoginCode(result) == '600001';
+bool isUmengEnvironmentAvailable(Object? result) =>
+ extractUmengLoginCode(result) == '600024';
+bool isUmengCancel(Object? result) => extractUmengLoginCode(result) == '700000';
+bool isUmengManualSwitch(Object? result) =>
+ extractUmengLoginCode(result) == '700001';
+bool isUmengFallbackLoginCode(Object? result) {
+ final code = extractUmengLoginCode(result);
+ return code != null && _umengFallbackCodes.contains(code);
+}