diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart new file mode 100644 index 0000000..2b47172 --- /dev/null +++ b/lib/providers/auth_provider.dart @@ -0,0 +1,458 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../features/authentication/domain/repositories/auth_repository.dart'; +import '../features/authentication/data/models/auth_user.dart'; +import '../core/errors/auth_exceptions.dart'; + +/// Authentication state class +/// +/// Represents the current authentication state with loading and error information +class AuthState { + final AuthUser? user; + final bool isLoading; + final String? error; + final bool isAuthenticated; + + const AuthState({ + this.user, + this.isLoading = false, + this.error, + }) : isAuthenticated = user != null; + + /// Creates a copy of AuthState with updated values + AuthState copyWith({ + AuthUser? user, + bool? isLoading, + String? error, + bool clearError = false, + }) { + return AuthState( + user: user ?? this.user, + isLoading: isLoading ?? this.isLoading, + error: clearError ? null : (error ?? this.error), + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is AuthState && + other.user == user && + other.isLoading == isLoading && + other.error == error; + } + + @override + int get hashCode { + return user.hashCode ^ isLoading.hashCode ^ error.hashCode; + } + + @override + String toString() { + return 'AuthState(user: $user, isLoading: $isLoading, error: $error)'; + } +} + +/// Authentication provider that manages global auth state +/// +/// This provider uses Riverpod to manage authentication state throughout the app. +/// It depends on AuthRepository and provides a clean interface for UI components +/// to interact with authentication functionality. +class AuthProvider extends StateNotifier { + final AuthRepository _authRepository; + StreamSubscription? _authSubscription; + + /// Creates a new AuthProvider + /// + /// [authRepository] - The authentication repository to use + AuthProvider(this._authRepository) : super(const AuthState()) { + _initializeAuthState(); + } + + /// Initialize authentication state by listening to auth changes + void _initializeAuthState() { + _authSubscription = _authRepository.authStateChanges().listen( + (user) { + state = state.copyWith( + user: user, + clearError: true, + ); + }, + onError: (error) { + state = state.copyWith( + error: error.toString(), + isLoading: false, + ); + }, + ); + } + + /// Signs up a new user with email and password + /// + /// [email] - User's email address + /// [password] - User's password + /// + /// Throws AuthException if registration fails + Future signUp(String email, String password) async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.signUp(email, password); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Signs in an existing user with email and password + /// + /// [email] - User's email address + /// [password] - User's password + /// + /// Throws AuthException if sign in fails + Future signIn(String email, String password) async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.signIn(email, password); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Signs out the current user + /// + /// Clears authentication state and signs out from repository + Future signOut() async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + await _authRepository.signOut(); + state = state.copyWith( + user: null, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Sends a password reset email + /// + /// [email] - User's email address + /// + /// Throws AuthException if reset fails + Future resetPassword(String email) async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + await _authRepository.resetPassword(email); + state = state.copyWith(isLoading: false); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Gets the current authenticated user + /// + /// Updates state with current user information + Future getCurrentUser() async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.getCurrentUser(); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Refreshes the current authentication session + /// + /// Updates user data and extends session if possible + Future refreshSession() async { + if (state.isLoading || state.user == null) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.refreshSession(); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Updates user profile information + /// + /// [displayName] - Optional new display name + /// [avatarUrl] - Optional new avatar URL + /// + /// Throws AuthException if update fails + Future updateProfile({ + String? displayName, + String? avatarUrl, + }) async { + if (state.isLoading || state.user == null) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.updateProfile( + displayName: displayName, + avatarUrl: avatarUrl, + ); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Sends email verification to current user + /// + /// Throws AuthException if sending fails + Future sendEmailVerification() async { + if (state.isLoading || state.user == null) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + await _authRepository.sendEmailVerification(); + state = state.copyWith(isLoading: false); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Changes user's password + /// + /// [currentPassword] - User's current password + /// [newPassword] - User's new password + /// + /// Throws AuthException if change fails + Future changePassword(String currentPassword, String newPassword) async { + if (state.isLoading || state.user == null) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + await _authRepository.changePassword(currentPassword, newPassword); + state = state.copyWith(isLoading: false); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Deletes user's account + /// + /// This is a destructive operation and cannot be undone + /// + /// Throws AuthException if deletion fails + Future deleteAccount() async { + if (state.isLoading || state.user == null) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + await _authRepository.deleteAccount(); + state = state.copyWith( + user: null, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Checks if user's email is verified + /// + /// Returns true if email is verified, false if not, null if user not authenticated + Future isEmailVerified() async { + if (state.user == null) return null; + + try { + return await _authRepository.isEmailVerified(); + } catch (e) { + state = state.copyWith(error: e.toString()); + return null; + } + } + + /// Signs in with OAuth provider + /// + /// [provider] - The OAuth provider (e.g., 'google', 'github', 'apple') + /// + /// Throws AuthException if sign in fails + Future signInWithOAuth(String provider) async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.signInWithOAuth(provider); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Signs in anonymously + /// + /// Creates an anonymous user session that can be upgraded later + /// + /// Throws AuthException if sign in fails + Future signInAnonymously() async { + if (state.isLoading) return; + + state = state.copyWith(isLoading: true, clearError: true); + + try { + final user = await _authRepository.signInAnonymously(); + state = state.copyWith( + user: user, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + rethrow; + } + } + + /// Clears any authentication error + void clearError() { + state = state.copyWith(clearError: true); + } + + /// Dispose of the provider and cancel subscriptions + @override + void dispose() { + _authSubscription?.cancel(); + super.dispose(); + } +} + +/// Provider for AuthRepository +/// +/// This provider creates and manages the AuthRepository instance +final authRepositoryProvider = Provider((ref) { + return AuthRepositoryImpl(); +}); + +/// Provider for AuthProvider +/// +/// This is the main provider that UI components should use to access +/// authentication state and methods +final authProvider = StateNotifierProvider((ref) { + final authRepository = ref.watch(authRepositoryProvider); + return AuthProvider(authRepository); +}); + +/// Provider for current authentication state +/// +/// Convenience provider for accessing just the state without methods +final authStateProvider = Provider((ref) { + return ref.watch(authProvider); +}); + +/// Provider for current authenticated user +/// +/// Convenience provider for accessing just the user +final currentUserProvider = Provider((ref) { + return ref.watch(authStateProvider).user; +}); + +/// Provider for authentication status +/// +/// Convenience provider for checking if user is authenticated +final isAuthenticatedProvider = Provider((ref) { + return ref.watch(authStateProvider).isAuthenticated; +}); + +/// Provider for loading state +/// +/// Convenience provider for checking if auth operations are loading +final authLoadingProvider = Provider((ref) { + return ref.watch(authStateProvider).isLoading; +}); + +/// Provider for authentication error +/// +/// Convenience provider for accessing any auth error +final authErrorProvider = Provider((ref) { + return ref.watch(authStateProvider).error; +}); \ No newline at end of file