fix:对齐金值,主要注销,一键登录等方面修改
This commit is contained in:
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.
|
||||
|
||||
Reference in new issue
Block a user