Auth legacy
To trigger authentication actions, use:
ref.read(authServiceProvider.notifier).triggerEvent(AuthEvent.eventName, [data])
// 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);
To listen to authentication state changes, use:
// 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);
// 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);
// Watch user service for fetching other users
final userState = ref.watch(userServiceProvider);
Purpose: Manages the currently logged-in user
- User profile updates
- Authentication tokens
- User preferences
- Sign out functionality
Purpose: Fetches other users’ data (profiles, search)
- Fetch user by ID
- Search users
- Get public profiles
- View other users’ information
Purpose: Handles authentication events
- Sign in/up/out
- Anonymous mode
- Verification flows
- Auto-login
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');
}
}
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');
}
}
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'),
);
}
}
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
final userState = ref.watch(userServiceProvider);
userState.fetchedUser // UserModel? - Currently fetched user
userState.isLoading // bool - Fetch operations loading
userState.error // String? - Fetch operation errors
final authState = ref.watch(authServiceProvider);
authState.isAuthenticated // bool
authState.isAnonymous // bool
authState.isLoading // bool
authState.currentUser // UserModel?
authState.error // String?
authState.state // AuthState enum
- 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
Following the same pattern as UserService, we’ve simplified pledge management:
Before (Complex):
PledgeCreator- separate provider for creatingPledgeUpdater- separate provider for updatingPledgeDeleter- separate provider for deletingPledgeNotifier- 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;
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'),
),
],
);
}
}
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.
- ✅ 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
- AuthService - Main authentication provider
- UserServiceProvider - Shared user state management
- AuthEvent - Events that can be triggered
- AuthState - Current authentication states
- AuthEventData - Data passed with events
Anonymous → TrySignIn/SignIn/SignUp → [Verification?] → Authenticated → SignOut → Anonymous
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();
}
}
// 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);
- 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
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;
// Quick checks throughout your app
final isAuth = ref.watch(isUserAuthenticatedProvider);
final isAnon = ref.watch(isUserAnonymousProvider);
final user = ref.watch(currentAuthenticatedUserProvider);
The framework includes a shared user service for managing user details:
// Watch user state
final userState = ref.watch(userServiceProviderProvider);
// Check user status
bool isLoggedIn = userState.isLoggedIn;
String displayName = userState.displayName;
String? email = userState.email;
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();
final attempt = AuthAttempt(
authType: AuthType.email,
email: 'user@example.com',
password: 'password123',
);
final attempt = GoogleAuthAttempt(
authType: AuthType.google,
// Google-specific fields
);
final attempt = FacebookAuthAttempt(
authType: AuthType.facebook,
// Facebook-specific fields
);
final attempt = AuthAttempt(
authType: AuthType.invite,
invitationUid: 'invite_code_123',
);
For authentication methods that require additional verification (like email PIN):
- Initial Authentication: User provides credentials
- Verification Required: AuthState becomes
awaitingVerification - User Completes Verification: Outside the auth service (e.g., enters PIN)
- Complete Verification: Trigger
AuthEvent.verificationCompleted - 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;
}
The framework automatically integrates with StoredSettings:
- Auto Sign-In: On app start, calls
trySignInwith stored credentials - Credential Storage: Successful authentications are automatically saved
- Credential Clearing: Sign out clears stored credentials
// 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
}
},
);
// Trigger events
ref.read(authServiceProvider.notifier).triggerEvent(
AuthEvent.signIn,
AuthEventData(authAttempt: authAttempt),
);
// Watch state
final authState = ref.watch(authServiceProvider);
if (authState.isAuthenticated) {
// Handle authenticated
}
- Cleaner Code: Event-driven API is more intuitive than Bloc events
- Better Performance: Riverpod’s selective rebuilding
- Anonymous Support: Built-in support for anonymous usage
- Self-Contained: No external dependencies for authentication logic
- Type Safety: Strong typing with sealed classes and enums
- Easy Testing: Clear separation of concerns and dependency injection
- Shared State: User service provides app-wide user state management
final authState = ref.watch(authServiceProvider);
if (authState.hasFailed) {
// Show error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(authState.error ?? 'Authentication failed')),
);
}
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.