import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.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 '../data/auth_api.dart'; import '../data/auth_models.dart'; const _legacyMockLoggedInKey = 'jinzhi_mock_logged_in'; final authProvider = StateNotifierProvider>((ref) { return AuthController( ref.read(dioProvider), identityStorage: ref.read(secureIdentityStorageProvider), preferences: StorageService.to.preferences, ); }); class AuthController extends StateNotifier> { 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 retryBootstrap() => _bootstrap(); Future _bootstrap() async { state = const AsyncLoading(); final token = await _identityStorage.readJwt(); if (token == null || token.isEmpty) { state = const AsyncData(GuestAuthState()); return; } 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 sendCode(String phone) => _api.sendSmsCode(phone); Future verifyCode(String phone, String code) async { final reply = await _api.verifySmsCode(phone, code); await _persistLogin(reply); } Future verifyOneKeyLogin(String loginToken) async { final reply = await _api.verifyOneKeyLogin(loginToken); await _persistLogin(reply); } Future _persistLogin(AuthLoginResult reply) async { await _identityStorage.saveJwt(reply.accessToken); await _persistProfile(reply.user); state = AsyncData( LoggedInAuthState(user: reply.user, token: reply.accessToken), ); } Future logout() async { await _clearLocalSession(); state = const AsyncData(GuestAuthState()); } Future 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 _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 _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; }