Files
Sage/lib/providers/auth_provider.dart
Dani B f3397a9836 feat(01-04): create AuthProvider for state management
- Implements global auth state management with Riverpod
- Manages auth state (user, loading, error) automatically
- Listens to repository authStateChanges stream
- Provides methods for all auth operations
- Handles loading states and errors properly
- Updates UI state automatically on auth changes
- Includes convenience providers for common use cases
- Properly disposes of stream subscriptions
2026-01-28 10:38:54 -05:00

458 lines
12 KiB
Dart

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<AuthState> {
final AuthRepository _authRepository;
StreamSubscription<AuthUser?>? _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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<bool?> 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<void> 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<void> 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<AuthRepository>((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<AuthProvider, AuthState>((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<AuthState>((ref) {
return ref.watch(authProvider);
});
/// Provider for current authenticated user
///
/// Convenience provider for accessing just the user
final currentUserProvider = Provider<AuthUser?>((ref) {
return ref.watch(authStateProvider).user;
});
/// Provider for authentication status
///
/// Convenience provider for checking if user is authenticated
final isAuthenticatedProvider = Provider<bool>((ref) {
return ref.watch(authStateProvider).isAuthenticated;
});
/// Provider for loading state
///
/// Convenience provider for checking if auth operations are loading
final authLoadingProvider = Provider<bool>((ref) {
return ref.watch(authStateProvider).isLoading;
});
/// Provider for authentication error
///
/// Convenience provider for accessing any auth error
final authErrorProvider = Provider<String?>((ref) {
return ref.watch(authStateProvider).error;
});