// Version: 1.1.1 | Created: 2026-04-01 // Riverpod notifier that owns the auth state machine. // All login/logout/session-restore transitions go through here. import 'dart:async'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../features/auth/data/auth_repository.dart'; import '../../features/auth/domain/auth_failure.dart'; import '../storage/sync_persistence_service.dart'; import 'auth_state.dart'; import 'secure_storage.dart'; part 'auth_notifier.g.dart'; /// The single source of truth for authentication state. /// /// keepAlive: true — auth state must persist for the entire app lifetime. /// GoRouter watches this provider to decide which route to show. @Riverpod(keepAlive: true) class AuthNotifier extends _$AuthNotifier { @override AuthState build() { // Kick off session restore immediately; start in [AuthInitial]. _restoreSession(); return const AuthState.initial(); } /// Tries to restore a previous session from secure storage. Future _restoreSession() async { state = const AuthState.loading(); final storage = ref.read(secureStorageProvider); final credentials = await storage.loadCredentials(); if (credentials == null) { state = const AuthState.unauthenticated(); return; } try { final repo = ref.read(authRepositoryProvider); // Timeout: client.init() starts the Matrix sync loop and can hang // indefinitely if the token is expired or the server is unreachable. // After 12 seconds we give up and send the user to the login screen. await repo .restoreSession( accessToken: credentials.accessToken, userId: credentials.userId, deviceId: credentials.deviceId, ) .timeout( const Duration(seconds: 12), onTimeout: () => throw Exception('Session restore timed out'), ); state = AuthState.authenticated( userId: credentials.userId, accessToken: credentials.accessToken, deviceId: credentials.deviceId, ); // Resume background persistence — fire-and-forget so it never blocks auth. try { ref.read(syncPersistenceServiceProvider).start(); } catch (_) { // Persistence failure is non-fatal; the app works without it. } } on AuthFailure { // Stored credentials are invalid; force re-login. await storage.clearCredentials(); state = const AuthState.unauthenticated(); } on Exception { // Covers: timeout, network offline, Olm init failure. // Clear stale credentials so the login screen appears cleanly. await storage.clearCredentials(); state = const AuthState.unauthenticated(); } } /// Attempts to log in with [username] and [password]. /// /// Transitions: loading → authenticated | unauthenticated(failure). Future login({ required String username, required String password, }) async { state = const AuthState.loading(); try { final repo = ref.read(authRepositoryProvider); final response = await repo.login(username: username, password: password); final storage = ref.read(secureStorageProvider); await storage.saveCredentials( accessToken: response.accessToken, userId: response.userId, deviceId: response.deviceId, ); state = AuthState.authenticated( userId: response.userId, accessToken: response.accessToken, deviceId: response.deviceId, ); // Start background sync-to-database persistence now that we are logged in. ref.read(syncPersistenceServiceProvider).start(); } on AuthFailure catch (failure) { state = AuthState.unauthenticated(failure: failure.userMessage); } } /// Logs out the current user, clears storage, and resets to unauthenticated. Future logout() async { state = const AuthState.loading(); final repo = ref.read(authRepositoryProvider); await repo.logout(); final storage = ref.read(secureStorageProvider); await storage.clearCredentials(); state = const AuthState.unauthenticated(); } }