Auth Service

1. Single Source of Truth

// New - ONE state class with everything needed
class AuthState {
  final UserModel? user;        // If user exists, authenticated
  final String? error;          // If error exists, has error
  final bool isLoading;         // Simple loading flag

  // All derived state comes from these 3 fields:
  bool get isAuthenticated => user != null;
  bool get isAnonymous => user == null;
  bool get hasError => error != null;
  bool get requiresVerification => user?.verified == false;
}

2. Direct Method Calls

// New - simple, direct method calls
final authService = ref.read(authServiceSimpleProvider.notifier);

await authService.signIn(emailAttempt);
await authService.signUp(googleAttempt);
await authService.signOut();
await authService.trySignIn();  // Uses stored credentials
authService.setAnonymous();     // Immediate
authService.clearError();       // Immediate

3. Predictable State Changes

// New - state always makes sense
if (state.isAuthenticated) {
  // state.user is guaranteed to be non-null
  print('Welcome ${state.user!.email}');
}

if (state.hasError) {
  // state.error is guaranteed to be non-null
  showError(state.error!);
}

4. Clear Provider Delegation

// New - all user-related providers delegate to AuthService
@riverpod
UserModel? currentUser(Ref ref) {
  return ref.watch(authServiceSimpleProvider).user; // Single source
}

@riverpod
bool currentUserIsAuthenticated(Ref ref) {
  return ref.watch(authServiceSimpleProvider).isAuthenticated; // Single source
}

@riverpod
bool currentUserIsLoading(Ref ref) {
  final authLoading = ref.watch(authServiceSimpleProvider).isLoading;
  final userLoading = ref.watch(currentUserServiceProvider).isLoading;
  return authLoading || userLoading; // Combines both loading states clearly
}

. Clear Separation of Responsibilities

// AuthService: Manages authentication and user state
class AuthService {
  Future<void> signIn(AuthAttempt attempt) { /* handles auth */ }
  Future<void> signOut() { /* handles auth */ }
  void updateUser(UserModel user) { /* updates user state */ }
}

// CurrentUserService: Manages user operations only
class CurrentUserService {
  Future<void> updateUser(UserModel user) { /* API calls, then delegates to AuthService */ }
  Future<void> updateUserField(String field, dynamic value) { /* user operations */ }
  Future<void> refreshUser() { /* refresh from server */ }
}

Test Examples (AuthService and CurrentUserService combined.)

The test suite provides excellent examples of how to use the new unified service:

Migration Path

For Existing Code Using CurrentUserService:

Old Pattern:

// Don't use these anymore
ref.watch(currentUserServiceProvider)
ref.read(currentUserServiceProvider.notifier).updateUser(user)

New Pattern:

// Use these instead
ref.watch(authServiceProvider)  // or specific convenience providers
ref.read(authServiceProvider.notifier).updateUser(user)

// Or use convenience providers for cleaner code
ref.watch(currentUserProvider)  // Just the user
ref.watch(currentUserIsLoadingProvider)  // Just loading state
ref.watch(currentUserDisplayNameProvider)  // Smart display name

Basic Usage:

// Get the service
final service = container.read(authServiceProvider.notifier);

// Set authenticated user
service.state = AuthState(user: testUser);

// Update user
await service.updateUser(updatedUser);

// Update specific field
await service.updateUserField('displayName', 'New Name');

// Access state via providers
final user = container.read(currentUserProvider);
final isLoading = container.read(currentUserIsLoadingProvider);
final displayName = container.read(currentUserDisplayNameProvider);

Error Handling:

// Clear errors
service.clearError();

// Check error state
final hasError = container.read(authServiceProvider).hasError;
final errorMessage = container.read(currentUserErrorProvider);

Usage Examples

// Check auth state
final authState = ref.watch(authServiceSimpleProvider);
if (authState.isAuthenticated) {
  // User is signed in - state.user is guaranteed non-null
  print('Welcome ${authState.user!.email}');
}

// Sign in
final authService = ref.read(authServiceSimpleProvider.notifier);
await authService.signIn(EmailAttempt.signInWithCredentials(
  email: 'user@example.com',
  password: 'password',
));

// Handle result immediately - no events needed
if (authState.hasError) {
  showError(authState.error!);
} else if (authState.isAuthenticated) {
  navigateToHome();
}

Migration Path

  1. Replace event calls with direct methods:

    // Before
    await triggerEvent(AuthEventType.signIn, AuthEventData(...));
    
    // After
    await authService.signIn(attempt);
    
  2. Update state checks:

    // Before
    if (authState.isAuthenticated && authState.currentUser != null)
    
    // After
    if (authState.isAuthenticated) // user is guaranteed non-null
    
  3. Replace provider references:

    // Before
    ref.watch(authServiceProvider)
    
    // After
    ref.watch(authServiceSimpleProvider)
    

The simplified version maintains all the functionality while being much easier to understand, maintain, and extend.