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