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
This commit is contained in:
458
lib/providers/auth_provider.dart
Normal file
458
lib/providers/auth_provider.dart
Normal file
@@ -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<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;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user