import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/network/api_exception.dart'; import '../../../common/network/dio_provider.dart'; import '../../../common/storage/preferences_storage.dart'; import '../../../common/storage/storage_keys.dart'; import '../data/auth_api.dart'; import '../data/auth_models.dart'; import 'auth_session_coordinator.dart'; const _legacyMockLoggedInKey = 'jinzhi_mock_logged_in'; final authProvider = StateNotifierProvider>((ref) { return AuthController( ref.read(dioProvider), preferences: ref.read(preferencesStorageProvider), coordinator: ref.read(authSessionCoordinatorProvider), ); }); class AuthController extends StateNotifier> { AuthController( this._dio, { required PreferencesStorage preferences, required AuthSessionCoordinator coordinator, bool autoBootstrap = true, }) : _preferences = preferences, _coordinator = coordinator, super(const AsyncLoading()) { _coordinator.addListener(_onSessionEnded); if (autoBootstrap) _bootstrap(); } final Dio _dio; final PreferencesStorage _preferences; final AuthSessionCoordinator _coordinator; AuthApi get _api => AuthApi(_dio); Future retryBootstrap() => _bootstrap(); Future _bootstrap() async { state = const AsyncLoading(); final token = await _coordinator.restore(); if (token == null || token.isEmpty) { state = const AsyncData(GuestAuthState()); return; } final cached = _readCachedProfile(); if (cached != null) { state = AsyncData( LoggedInAuthState(user: cached, token: token, isVerified: false), ); await _persistProfile(cached); } await _verifyProfile(token, _coordinator.generation); } Future _verifyProfile(String token, int generation) async { try { final user = await _api.getProfile(); if (generation != _coordinator.generation) return; await _persistProfile(user); state = AsyncData( LoggedInAuthState(user: user, token: token, isVerified: true), ); } catch (error, stackTrace) { if (generation != _coordinator.generation) return; final exception = ApiException.from(error, stackTrace: stackTrace); if (await _coordinator.expireIfNeeded(exception)) { return; } // Any non-session error (transport, 403, 429, 5xx, decode) preserves a // stale cached session. With no profile there is nothing safe to render, // but the credential still remains available for retry. if (state.valueOrNull is LoggedInAuthState) return; state = AsyncError(exception, 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 _coordinator.startSession(reply.accessToken); await _persistProfile(reply.user); state = AsyncData( LoggedInAuthState(user: reply.user, token: reply.accessToken), ); } Future logout() async { await _coordinator.logout(); } 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 _coordinator.accountDeleted(); } 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); } void _onSessionEnded(SessionEndReason reason) { state = AsyncData( reason == SessionEndReason.expired ? const SessionExpiredAuthState() : const GuestAuthState(), ); } bool get isLoggedIn => state.valueOrNull is LoggedInAuthState; @override void dispose() { _coordinator.removeListener(_onSessionEnded); super.dispose(); } }