Riverpod Authentication

Riverpod Authentication Framework - Quick Reference

🎯 Triggering Events (Actions)

To trigger authentication actions, use:

ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.eventName, [data])

Available Events:

// Sign out current user
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.signOut);

// Sign in with credentials
ref.read(authServiceProvider.notifier).triggerEvent(
  AuthEvent.signIn,
  AuthEventData(authAttempt: myAuthAttempt)
);

// Sign up with credentials
ref.read(authServiceProvider.notifier).triggerEvent(
  AuthEvent.signUp,
  AuthEventData(authAttempt: myAuthAttempt)
);

// Try auto sign-in with stored credentials
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.trySignIn);

// Set user to anonymous
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.setAnonymous);

👀 Listening to State (Watching)

To listen to authentication state changes, use:

Current User (Logged-in User)

// Watch current user state (includes loading/error)
final userState = ref.watch(currentUserServiceProvider);

// Quick access to current user
final user = ref.watch(currentUserProvider);

// Check if authenticated
final isAuth = ref.watch(isAuthenticatedProvider);

Authentication Status

// Watch full auth state
final authState = ref.watch(authServiceProvider);

// Check if authenticated
final isAuth = ref.watch(isAuthenticatedProvider);

// Check if anonymous
final isAnon = ref.watch(isUserAnonymousProvider);

Other Users (UserService for profiles, search, etc.)

// Watch user service for fetching other users
final userState = ref.watch(userServiceProvider);

🏗️ Service Architecture

CurrentUserService

Purpose: Manages the currently logged-in user

  • User profile updates
  • Authentication tokens
  • User preferences
  • Sign out functionality

UserService

Purpose: Fetches other users’ data (profiles, search)

  • Fetch user by ID
  • Search users
  • Get public profiles
  • View other users’ information

AuthService

Purpose: Handles authentication events

  • Sign in/up/out
  • Anonymous mode
  • Verification flows
  • Auto-login

🔄 Common Patterns

Managing Current User:

class ProfileWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(currentUserServiceProvider);
    final user = userState.user;

    if (userState.isLoading) {
      return CircularProgressIndicator();
    }

    if (user != null) {
      return Column(
        children: [
          Text('Welcome ${user.name}!'),
          ElevatedButton(
            onPressed: () {
              // Update user field
              ref.read(currentUserServiceProvider.notifier)
                 .updateUserField('name', 'New Name');
            },
            child: Text('Update Name'),
          ),
        ],
      );
    }

    return Text('Not logged in');
  }
}

Viewing Other Users:

class UserProfileWidget extends ConsumerWidget {
  final String userId;

  const UserProfileWidget({required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userServiceProvider);

    useEffect(() {
      // Fetch user when widget loads
      ref.read(userServiceProvider.notifier).fetchUser(userId);
      return null;
    }, [userId]);

    if (userState.isLoading) {
      return CircularProgressIndicator();
    }

    if (userState.fetchedUser != null) {
      return Text('User: ${userState.fetchedUser!.name}');
    }

    return Text('User not found');
  }
}

Authentication Flow:

class AuthWidget extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authServiceProvider);
    final isLoading = useState(false);

    return ElevatedButton(
      onPressed: () async {
        isLoading.value = true;

        // Trigger sign in
        await ref.read(authServiceProvider.notifier).triggerEvent(
          AuthEvent.signIn,
          AuthEventData(authAttempt: myAttempt)
        );

        isLoading.value = false;
      },
      child: isLoading.value
        ? CircularProgressIndicator()
        : Text('Sign In'),
    );
  }
}

📊 State Properties

CurrentUserService State:

final userState = ref.watch(currentUserServiceProvider);

userState.user           // UserModel? - Current logged-in user
userState.isLoading      // bool - User operations loading
userState.error          // String? - User operation errors
userState.isLoggedIn     // bool - Quick login check
userState.displayName    // String - Display name or 'Anonymous'
userState.email          // String? - User email

UserService State (for other users):

final userState = ref.watch(userServiceProvider);

userState.fetchedUser    // UserModel? - Currently fetched user
userState.isLoading      // bool - Fetch operations loading
userState.error          // String? - Fetch operation errors

AuthService State:

final authState = ref.watch(authServiceProvider);

authState.isAuthenticated  // bool
authState.isAnonymous     // bool
authState.isLoading       // bool
authState.currentUser     // UserModel?
authState.error           // String?
authState.state           // AuthState enum

🎉 Key Benefits

  • Clear Separation: CurrentUser vs OtherUsers vs Authentication
  • Event-driven: Clean separation between actions and state
  • Type-safe: Full type safety with code generation
  • Reactive: Automatic UI updates when state changes
  • Testable: Easy to mock and test individual providers
  • Anonymous support: Built-in support for anonymous browsing
  • Auto-persistence: Automatically saves/loads credentials
  • Simplified Services: Consolidated functionality like PledgeService instead of separate Creator/Updater/Deleter

🏗️ Service Examples

Simplified Pledge Service Pattern

Following the same pattern as UserService, we’ve simplified pledge management:

Before (Complex):

  • PledgeCreator - separate provider for creating
  • PledgeUpdater - separate provider for updating
  • PledgeDeleter - separate provider for deleting
  • PledgeNotifier - separate state management
  • Multiple AsyncValue states to manage

After (Simple):

// 🎯 ONE SERVICE: All pledge operations in one place
final pledgeState = ref.watch(pledgeServiceProvider);

// 📝 CREATE
await ref.read(pledgeServiceProvider.notifier).createPledge(pledge);

// 🔄 UPDATE
await ref.read(pledgeServiceProvider.notifier).updatePledge(pledge);

// 🗑️ DELETE
await ref.read(pledgeServiceProvider.notifier).deletePledge(pledgeId);

// 📊 STATE ACCESS
final pledges = pledgeState.pledges;
final isLoading = pledgeState.isLoading;
final error = pledgeState.error;

Complete Pledge Management:

class PledgeWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final pledgeState = ref.watch(pledgeServiceProvider);

    return Column(
      children: [
        // Show loading indicator
        if (pledgeState.isLoading)
          CircularProgressIndicator(),

        // Show error
        if (pledgeState.hasError)
          Text('Error: ${pledgeState.error}'),

        // Show pledges
        for (final pledge in pledgeState.pledges)
          ListTile(
            title: Text('\$${pledge.pledgeCents / 100}'),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () async {
                await ref.read(pledgeServiceProvider.notifier)
                    .deletePledge(pledge.id!);
              },
            ),
          ),

        // Add new pledge
        ElevatedButton(
          onPressed: () async {
            final pledge = PledgeModel.withTimestamp(
              dareType: DareType.user,
              fromUid: 'user123',
              dareId: 'dare456',
              pledgeCents: 1000,
            );

            await ref.read(pledgeServiceProvider.notifier)
                .createPledge(pledge);
          },
          child: Text('Add Pledge'),
        ),
      ],
    );
  }
}

🏆 Riverpod Authentication Framework

Overview

This is an easy-to-use, event-driven authentication framework built with Riverpod that replaces the UserProfileBloc pattern. It supports anonymous usage, various authentication methods, additional verification steps, and automatic credential storage.

Key Features

  • Anonymous Support: Users can browse the app without signing up
  • Multiple Auth Methods: Email, Google, Facebook, TikTok, Invite codes
  • Additional Verification: Email PIN verification for certain auth methods
  • Auto Sign-In: StoredSettings integration for seamless user experience
  • Self-Contained Authenticators: No dependency on GetIt for authentication
  • Clean State Management: Event-driven architecture with Riverpod
  • User Detail Management: Shared Riverpod user service for app-wide user access

Architecture

Core Components

  1. AuthService - Main authentication provider
  2. UserServiceProvider - Shared user state management
  3. AuthEvent - Events that can be triggered
  4. AuthState - Current authentication states
  5. AuthEventData - Data passed with events

Authentication Flow

Anonymous → TrySignIn/SignIn/SignUp → [Verification?] → Authenticated → SignOut → Anonymous

Usage

1. Basic Setup

import 'package:services/providers/auth/auth_service.dart';
import 'package:services/providers/auth/auth_events.dart';

class MyAuthWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 🎯 Watch authentication state
    final authState = ref.watch(authServiceProvider);

    // Build UI based on state
    if (authState.isAuthenticated) {
      return AuthenticatedView();
    } else if (authState.isAnonymous) {
      return AnonymousView();
    } else if (authState.requiresVerification) {
      return VerificationView();
    }

    return LoadingView();
  }
}

2. Triggering Authentication Events

// Stay anonymous
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.setAnonymous);

// Try auto sign-in with stored credentials
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.trySignIn);

// Sign in with credentials
final attempt = AuthAttempt(
  authType: AuthType.email,
  email: 'user@example.com',
  password: 'password123',
);

ref.read(authServiceProvider.notifier).triggerEvent(
  AuthEvent.signIn,
  AuthEventData(authAttempt: attempt),
);

// Sign up
ref.read(authServiceProvider.notifier).triggerEvent(
  AuthEvent.signUp,
  AuthEventData(authAttempt: attempt),
);

// Complete verification (e.g., after email PIN)
ref.read(authServiceProvider.notifier).triggerEvent(
  AuthEvent.verificationCompleted,
);

// Sign out
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.signOut);

3. Authentication States

  • anonymous - User is not authenticated (default state)
  • tryingSignIn - Attempting to sign in with stored credentials
  • signingIn - In the process of signing in
  • signingUp - In the process of signing up
  • signedIn - User is successfully authenticated
  • signingOut - In the process of signing out
  • failed - Authentication failed
  • awaitingVerification - Waiting for additional verification (e.g., email PIN)
  • processingVerification - Processing verification step

4. Convenience Getters

final authState = ref.watch(authServiceProvider);

// Check states
bool isAnonymous = authState.isAnonymous;
bool isAuthenticated = authState.isAuthenticated;
bool requiresVerification = authState.requiresVerification;
bool hasFailed = authState.hasFailed;
bool isLoading = authState.isLoading;

// Get data
UserModel? currentUser = authState.currentUser;
String? error = authState.error;
AuthAttempt? authAttempt = authState.authAttempt;

5. Quick Access Providers

// Quick checks throughout your app
final isAuth = ref.watch(isUserAuthenticatedProvider);
final isAnon = ref.watch(isUserAnonymousProvider);
final user = ref.watch(currentAuthenticatedUserProvider);

User Service Integration

The framework includes a shared user service for managing user details:

Accessing User Service

// Watch user state
final userState = ref.watch(userServiceProviderProvider);

// Check user status
bool isLoggedIn = userState.isLoggedIn;
String displayName = userState.displayName;
String? email = userState.email;

Updating User Details

final userService = ref.read(userServiceProviderProvider.notifier);

// Update specific field
await userService.updateUserField('name', 'John Doe');
await userService.updateUserField('autoMute', false);

// Update auth tokens after social login
await userService.updateAuthTokens(
  googleIdToken: 'new_token',
  googleAccessToken: 'access_token',
);

// Refresh user from server
await userService.refreshUser();

Authentication Methods

Email Authentication

final attempt = AuthAttempt(
  authType: AuthType.email,
  email: 'user@example.com',
  password: 'password123',
);

Google Authentication

final attempt = GoogleAuthAttempt(
  authType: AuthType.google,
  // Google-specific fields
);

Facebook Authentication

final attempt = FacebookAuthAttempt(
  authType: AuthType.facebook,
  // Facebook-specific fields
);

Invite Code Authentication

final attempt = AuthAttempt(
  authType: AuthType.invite,
  invitationUid: 'invite_code_123',
);

Additional Verification

For authentication methods that require additional verification (like email PIN):

  1. Initial Authentication: User provides credentials
  2. Verification Required: AuthState becomes awaitingVerification
  3. User Completes Verification: Outside the auth service (e.g., enters PIN)
  4. Complete Verification: Trigger AuthEvent.verificationCompleted
  5. Final Authentication: User becomes authenticated
// Override verification logic in your implementation
bool _requiresAdditionalVerification(AuthResult authResult, AuthAttempt attempt) {
  if (attempt.authType == AuthType.email) {
    // Check if email verification is required
    return !authResult.isEmailVerified; // Your logic here
  }
  return false;
}

StoredSettings Integration

The framework automatically integrates with StoredSettings:

  • Auto Sign-In: On app start, calls trySignIn with stored credentials
  • Credential Storage: Successful authentications are automatically saved
  • Credential Clearing: Sign out clears stored credentials

Migration from UserProfileBloc

Before (Bloc)

// Trigger events
bloc.add(UserProfileSignInEvent(authAttempt));
bloc.add(UserProfileSignOutEvent());

// Listen to state
BlocBuilder<UserProfileBloc, UserProfileState>(
  builder: (context, state) {
    if (state is UserProfileSignedInState) {
      // Handle signed in
    }
  },
);

After (Riverpod)

// Trigger events
ref.read(authServiceProvider.notifier).triggerEvent(
  AuthEvent.signIn,
  AuthEventData(authAttempt: authAttempt),
);

// Watch state
final authState = ref.watch(authServiceProvider);
if (authState.isAuthenticated) {
  // Handle authenticated
}

Benefits

  1. Cleaner Code: Event-driven API is more intuitive than Bloc events
  2. Better Performance: Riverpod’s selective rebuilding
  3. Anonymous Support: Built-in support for anonymous usage
  4. Self-Contained: No external dependencies for authentication logic
  5. Type Safety: Strong typing with sealed classes and enums
  6. Easy Testing: Clear separation of concerns and dependency injection
  7. Shared State: User service provides app-wide user state management

Error Handling

final authState = ref.watch(authServiceProvider);

if (authState.hasFailed) {
  // Show error
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(authState.error ?? 'Authentication failed')),
  );
}

Example Implementation

See auth_example_widget.dart for a complete example showing:

  • Anonymous usage
  • Sign in/Sign up forms
  • Verification handling
  • Error states
  • Auto sign-in functionality

This framework provides a clean, maintainable, and feature-rich authentication system that scales with your app’s needs while maintaining simplicity for common use cases.