fix:对齐金值,主要注销,一键登录等方面修改

This commit is contained in:
jingyun committed 2026-09-08 17:42:17 +08:00
1 parent e29ed0a2c8
commit 6997a65c96
37 files changed
+3442 -514

No files matched your search

@@ -1,92 +1,143 @@
import 'package:dio/dio.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../common/network/auth_interceptor.dart';
import '../../../common/network/dio_provider.dart';
import '../../../common/storage/secure_identity_storage.dart';
import '../../../common/storage/storage_keys.dart';
import '../../../common/storage/storage_service.dart';
import '../../../common/network/dio_provider.dart';
import '../data/auth_models.dart';
import '../data/auth_api.dart';
import '../data/one_key_login_api.dart';
import '../data/auth_models.dart';
const _legacyMockLoggedInKey = 'jinzhi_mock_logged_in';
final authProvider =
StateNotifierProvider<AuthController, AsyncValue<AuthState>>(
(ref) => AuthController(ref.read(dioProvider)),
);
StateNotifierProvider<AuthController, AsyncValue<AuthState>>((ref) {
return AuthController(
ref.read(dioProvider),
identityStorage: ref.read(secureIdentityStorageProvider),
preferences: StorageService.to.preferences,
);
});
class AuthController extends StateNotifier<AsyncValue<AuthState>> {
AuthController(this._dio) : super(const AsyncLoading()) {
_bootstrap();
AuthController(
this._dio, {
required SecureIdentityStorage identityStorage,
required SharedPreferences preferences,
bool autoBootstrap = true,
}) : _identityStorage = identityStorage,
_preferences = preferences,
super(const AsyncLoading()) {
if (autoBootstrap) _bootstrap();
}
final Dio _dio;
final SecureIdentityStorage _identityStorage;
final SharedPreferences _preferences;
AuthApi get _api => AuthApi(_dio);
Future<void> retryBootstrap() => _bootstrap();
Future<void> _bootstrap() async {
final token = await loadJwt();
state = const AsyncLoading();
final token = await _identityStorage.readJwt();
if (token == null || token.isEmpty) {
state = const AsyncData(GuestAuthState());
return;
}
final phone = StorageService.to.getString(storageJinzhiMockLoggedIn) ?? '';
state = AsyncData(
LoggedInAuthState(
user: JinzhiUser(
phone: phone.isEmpty ? '13800000000' : phone,
nickname: '金值用户',
createdAt: DateTime(2026, 6, 18),
),
token: token,
),
);
try {
final user = await _api.getProfile();
await _persistProfile(user);
state = AsyncData(LoggedInAuthState(user: user, token: token));
} catch (error, stackTrace) {
if (_isUnauthorized(error)) {
await _clearLocalSession();
state = const AsyncData(GuestAuthState());
return;
}
final cached = _canUseCachedProfile(error) ? _readCachedProfile() : null;
if (cached != null) {
await _persistProfile(cached);
state = AsyncData(LoggedInAuthState(user: cached, token: token));
} else {
state = AsyncError(error, stackTrace);
}
}
}
Future<void> sendCode(String phone) async {
await AuthApi(_dio).sendSmsCode(phone);
}
Future<void> sendCode(String phone) => _api.sendSmsCode(phone);
Future<void> verifyCode(String phone, String code) async {
final reply = await AuthApi(_dio).verifySmsCode(phone, code);
await saveJwt(reply.accessToken);
await StorageService.to.setString(storageJinzhiMockLoggedIn, reply.phone);
state = AsyncData(
LoggedInAuthState(
user: JinzhiUser(
phone: reply.phone,
nickname: '金值用户',
createdAt: DateTime.now(),
),
token: reply.accessToken,
),
);
final reply = await _api.verifySmsCode(phone, code);
await _persistLogin(reply);
}
/// Test environment reuses Yanting's one-key verification endpoint until
/// 金值 has its own account contract.
Future<void> verifyOneKeyLogin(String loginToken) async {
final reply = await OneKeyLoginApi(_dio).verify(loginToken);
await saveJwt(reply.token);
await StorageService.to.setString(storageJinzhiMockLoggedIn, reply.phone);
final reply = await _api.verifyOneKeyLogin(loginToken);
await _persistLogin(reply);
}
Future<void> _persistLogin(AuthLoginResult reply) async {
await _identityStorage.saveJwt(reply.accessToken);
await _persistProfile(reply.user);
state = AsyncData(
LoggedInAuthState(
user: JinzhiUser(
phone: reply.phone,
nickname: '金值用户',
createdAt: DateTime.now(),
),
token: reply.token,
),
LoggedInAuthState(user: reply.user, token: reply.accessToken),
);
}
Future<void> logout() async {
await clearJwt();
await StorageService.to.setString(storageJinzhiMockLoggedIn, '');
await _clearLocalSession();
state = const AsyncData(GuestAuthState());
}
Future<void> deleteAccount([String? code]) async {
await logout();
Future<void> deleteAccount(String code) async {
final normalized = code.trim();
if (normalized.isEmpty) {
throw ArgumentError.value(code, 'code', 'must not be empty');
}
await _api.deleteAccount(normalized);
await _clearLocalSession();
state = const AsyncData(GuestAuthState());
}
Future<void> _persistProfile(JinzhiUser user) async {
await _preferences.setString(storageUserId, user.id);
await _preferences.setString(storageUserPhone, user.phone);
await _preferences.setString(storageUserCreatedAt, user.createdAt);
await _preferences.remove(_legacyMockLoggedInKey);
}
JinzhiUser? _readCachedProfile() {
final id = _preferences.getString(storageUserId)?.trim() ?? '';
final phone =
(_preferences.getString(storageUserPhone) ??
_preferences.getString(_legacyMockLoggedInKey) ??
'')
.trim();
final createdAt =
_preferences.getString(storageUserCreatedAt)?.trim() ?? '';
if (id.isEmpty && phone.isEmpty) return null;
return JinzhiUser(id: id, phone: phone, createdAt: createdAt);
}
Future<void> _clearLocalSession() async {
await _identityStorage.clearJwt();
await _preferences.remove(storageUserId);
await _preferences.remove(storageUserPhone);
await _preferences.remove(storageUserCreatedAt);
await _preferences.remove(_legacyMockLoggedInKey);
}
bool get isLoggedIn => state.valueOrNull is LoggedInAuthState;
}
bool _isUnauthorized(Object error) =>
error is DioException && error.response?.statusCode == 401;
bool _canUseCachedProfile(Object error) {
if (error is! DioException) return false;
final status = error.response?.statusCode;
return status == null || status == 408 || status == 429 || status >= 500;
}
@@ -24,6 +24,9 @@ abstract interface class OneKeyLoginGateway {
void setWidgetCallback(OneKeyLoginCallback? callback);
/// 仅在用户点按一键登录后调用,预热运营商授权页。
Future<Object?> accelerate();
Future<void> requestLogin(UMCustomModel uiConfig);
void closeAuthPage();
@@ -65,6 +68,11 @@ class UmengOneKeyLoginGateway implements OneKeyLoginGateway {
}
}
@override
Future<Object?> accelerate() {
return UmengVerifySdk.accelerateLoginPageWithTimeout(3);
}
@override
Future<void> requestLogin(UMCustomModel uiConfig) async {
await UmengVerifySdk.getLoginTokenWithTimeout(3, uiConfig);
@@ -137,15 +145,25 @@ class OneKeyLoginSdkSession {
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 {
// 与 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);
@@ -186,6 +204,15 @@ class OneKeyLoginSdkSession {
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.