dot_auth
A Flutter package for phone-based Firebase authentication with Riverpod state management and GoRouter navigation — batteries included.
Table of Contents
- Dependencies
- Quick Start
- Providers
- Methods (Notifiers)
- AuthStatus Enum
- Usage Examples
- Firestore Security Rules
- User Roles & Services
- Custom Router (Manual Setup)
- Quick Reference Card
Dependencies
Add the following to your pubspec.yaml:
dependencies:
dot_auth: ^1.0.0
firebase_core: ^3.0.0
firebase_auth: ^5.0.0
flutter_riverpod: ^2.5.0
go_router: ^14.0.0
flutter_screenutil: ^5.9.0
Then run:
flutter pub get
Quick Start
1. Setup Firebase
Follow the Firebase setup guide for your platform. This generates firebase_options.dart.
2. Initialize in main()
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:dot_auth/dot_auth.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize ScreenUtil
await ScreenUtil.ensureScreenSize();
// Initialize Firebase
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const ProviderScope(child: MyApp()));
}
3. Configure Router
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = AuthRouter.createRouter(
ref: ref,
homeRoute: '/home',
homeBuilder: (context, state) => const HomePage(),
);
return ScreenUtilInit(
designSize: const Size(360, 690),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return MaterialApp.router(
title: 'My App',
theme: authTheme(),
routerConfig: router,
debugShowCheckedModeBanner: false,
);
},
);
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(currentUserProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await ref.read(authStateProvider.notifier).signOut();
if (context.mounted) {
context.pushReplacement('/phone');
}
},
),
],
),
body: Center(
child: Text('Welcome ${user?.phoneNumber ?? "User"}'),
),
);
}
}
Providers
authStateProvider
The main provider for authentication state. Watch this to get all auth-related information.
final authState = ref.watch(authStateProvider);
Auth State Properties
| Property | Type | Description |
|---|---|---|
status |
AuthStatus |
Current auth status (initial, loading, authenticated, unauthenticated, error) |
isAuthenticated |
bool |
true if user is logged in |
isLoading |
bool |
true if authentication is in progress |
isUnauthenticated |
bool |
true if user is not logged in |
user |
UserModel? |
Current user data or null |
errorMessage |
String? |
Error message if authentication failed |
User Properties (authState.user)
| Property | Type | Description |
|---|---|---|
uid |
String |
Unique user ID |
phoneNumber |
String? |
User's phone number |
email |
String? |
User's email address |
displayName |
String? |
User's display name |
photoURL |
String? |
User's photo URL |
creationTime |
DateTime? |
Account creation time |
lastSignInTime |
DateTime? |
Last login time |
isEmailVerified |
bool |
Email verification status |
isPhoneVerified |
bool |
Phone verification status |
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
if (authState.isAuthenticated) {
return Text('Welcome ${authState.user?.phoneNumber}');
}
if (authState.isLoading) {
return const CircularProgressIndicator();
}
if (authState.errorMessage != null) {
return Text('Error: ${authState.errorMessage}');
}
return const Text('Please login');
}
currentUserProvider
A simplified provider that returns only the current user.
final user = ref.watch(currentUserProvider);
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(currentUserProvider);
if (user == null) return const Text('Not logged in');
return Column(
children: [
Text('Phone: ${user.phoneNumber}'),
Text('UID: ${user.uid}'),
Text('Verified: ${user.isPhoneVerified}'),
Text('Email: ${user.email ?? "No email"}'),
Text('Name: ${user.displayName ?? "No name"}'),
],
);
}
isAuthenticatedProvider
A simple boolean provider for authentication status.
final isAuthenticated = ref.watch(isAuthenticatedProvider);
// Returns: true if user is logged in, false otherwise
Widget build(BuildContext context, WidgetRef ref) {
final isAuthenticated = ref.watch(isAuthenticatedProvider);
return isAuthenticated
? const Text('Welcome back!')
: const Text('Please login');
}
phoneAuthProvider
Manages phone authentication state during the OTP process.
final phoneAuth = ref.watch(phoneAuthProvider);
OTP / Verification State Properties
| Property | Type | Description |
|---|---|---|
phoneNumber |
String? |
Entered phone number |
verificationId |
String? |
Firebase verification ID |
otpCode |
String? |
Entered OTP code |
isCodeSent |
bool |
Whether the verification code was sent |
isLoading |
bool |
Whether loading is in progress |
error |
String? |
Error message if any |
resendToken |
int? |
Resend token for Firebase |
Widget build(BuildContext context, WidgetRef ref) {
final phoneAuth = ref.watch(phoneAuthProvider);
return Column(
children: [
if (phoneAuth.isLoading) const CircularProgressIndicator(),
if (phoneAuth.error != null)
Text('Error: ${phoneAuth.error}', style: TextStyle(color: Colors.red)),
if (phoneAuth.isCodeSent)
Text('Code sent to ${phoneAuth.phoneNumber}'),
Text('Phone: ${phoneAuth.phoneNumber ?? "Not set"}'),
Text('Code Sent: ${phoneAuth.isCodeSent}'),
],
);
}
Methods (Notifiers)
Auth State Notifier
final authNotifier = ref.read(authStateProvider.notifier);
| Method | Returns | Description |
|---|---|---|
signOut() |
Future<void> |
Signs out the current user |
setLoading() |
void |
Sets loading state |
setError(String msg) |
void |
Sets error message |
// Sign out user
await ref.read(authStateProvider.notifier).signOut();
// Set custom loading state
ref.read(authStateProvider.notifier).setLoading();
// Set error message
ref.read(authStateProvider.notifier).setError('Authentication failed');
// After sign out, navigate to login
if (context.mounted) {
context.go('/login');
}
Phone Auth Notifier
final phoneNotifier = ref.read(phoneAuthProvider.notifier);
| Method | Returns | Description |
|---|---|---|
setPhoneNumber(String number) |
void |
Sets the phone number |
setVerificationId(String id) |
void |
Sets the Firebase verification ID |
setLoading(bool loading) |
void |
Sets the loading state |
setError(String error) |
void |
Sets an error message |
clearError() |
void |
Clears the current error |
reset() |
void |
Resets all phone auth state |
setOtp(String otp) |
void |
Sets the OTP code |
ref.read(phoneAuthProvider.notifier).setPhoneNumber('+1234567890');
ref.read(phoneAuthProvider.notifier).setVerificationId('verification_id_here');
ref.read(phoneAuthProvider.notifier).setLoading(true);
ref.read(phoneAuthProvider.notifier).setError('Invalid phone number');
ref.read(phoneAuthProvider.notifier).clearError();
ref.read(phoneAuthProvider.notifier).reset(); // use after OTP verification
ref.read(phoneAuthProvider.notifier).setOtp('123456');
AuthStatus Enum
| Value | Description |
|---|---|
AuthStatus.initial |
Initial state, not yet checked |
AuthStatus.loading |
Loading — verifying or signing in |
AuthStatus.authenticated |
User is authenticated |
AuthStatus.unauthenticated |
User is not authenticated |
AuthStatus.error |
An error occurred |
final authState = ref.watch(authStateProvider);
switch (authState.status) {
case AuthStatus.initial:
return const Text('Initializing...');
case AuthStatus.loading:
return const CircularProgressIndicator();
case AuthStatus.authenticated:
return Text('Welcome ${authState.user?.phoneNumber}');
case AuthStatus.unauthenticated:
return const Text('Please login');
case AuthStatus.error:
return Text('Error: ${authState.errorMessage}');
}
Usage Examples
1. Check Authentication Status
Widget build(BuildContext context, WidgetRef ref) {
final isAuthenticated = ref.watch(isAuthenticatedProvider);
final authState = ref.watch(authStateProvider);
if (authState.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (isAuthenticated) {
return Center(child: Text('Welcome ${authState.user?.phoneNumber}'));
}
return const Center(child: Text('Please login'));
}
2. Get User Information
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(currentUserProvider);
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('UID: ${user?.uid ?? "N/A"}'),
Text('Phone: ${user?.phoneNumber ?? "N/A"}'),
Text('Email: ${user?.email ?? "N/A"}'),
Text('Name: ${user?.displayName ?? "N/A"}'),
Text('Phone Verified: ${user?.isPhoneVerified}'),
Text('Email Verified: ${user?.isEmailVerified}'),
],
),
),
);
}
3. Handle Loading and Error States
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
switch (authState.status) {
case AuthStatus.loading:
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading...'),
],
),
);
case AuthStatus.authenticated:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, size: 64, color: Colors.green),
const SizedBox(height: 16),
Text('Welcome ${authState.user?.phoneNumber}'),
],
),
);
case AuthStatus.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Error: ${authState.errorMessage}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () { /* retry logic */ },
child: const Text('Retry'),
),
],
),
);
default:
return const Center(child: Text('Please login'));
}
}
4. Listen to Authentication Changes
class MyWidget extends ConsumerStatefulWidget {
const MyWidget({super.key});
@override
ConsumerState<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends ConsumerState<MyWidget> {
@override
void initState() {
super.initState();
ref.listen(authStateProvider, (previous, next) {
if (next.isAuthenticated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Logged in successfully!')),
);
} else if (next.isUnauthenticated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Logged out')),
);
} else if (next.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${next.errorMessage}')),
);
}
});
}
@override
Widget build(BuildContext context) {
final authState = ref.watch(authStateProvider);
return Scaffold(
body: Center(child: Text('Status: ${authState.status}')),
);
}
}
5. Sign Out with Confirmation
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
final shouldSignOut = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Sign Out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Sign Out'),
),
],
),
);
if (shouldSignOut == true) {
await ref.read(authStateProvider.notifier).signOut();
if (context.mounted) context.go('/login');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Sign Out'),
);
}
6. Phone Authentication State
Widget build(BuildContext context, WidgetRef ref) {
final phoneAuth = ref.watch(phoneAuthProvider);
return Column(
children: [
if (phoneAuth.isLoading) const LinearProgressIndicator(),
if (phoneAuth.error != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red),
),
child: Text(phoneAuth.error!, style: const TextStyle(color: Colors.red)),
),
if (phoneAuth.isCodeSent)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green),
),
child: Column(
children: [
const Text('Verification code sent!'),
Text('To: ${phoneAuth.phoneNumber}'),
],
),
),
const SizedBox(height: 16),
Text('Phone: ${phoneAuth.phoneNumber ?? "Not set"}'),
Text('Code Sent: ${phoneAuth.isCodeSent}'),
Text('Loading: ${phoneAuth.isLoading}'),
],
);
}
7. Protected Route
class ProtectedRoute extends ConsumerWidget {
final Widget child;
const ProtectedRoute({super.key, required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAuthenticated = ref.watch(isAuthenticatedProvider);
final authState = ref.watch(authStateProvider);
if (authState.isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (!isAuthenticated) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.pushReplacement('/login');
});
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return child;
}
}
// Usage in router:
GoRoute(
path: '/profile',
builder: (context, state) => const ProtectedRoute(child: ProfilePage()),
),
8. Complete App with Custom Router
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:dot_auth/dot_auth.dart';
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
final router = GoRouter(
initialLocation: '/login',
redirect: (context, state) {
final isAuthenticated = authState.isAuthenticated;
final isLoginRoute = state.matchedLocation == '/login';
final isOtpRoute = state.matchedLocation == '/otp';
if (isAuthenticated && (isLoginRoute || isOtpRoute)) return '/home';
if (!isAuthenticated && state.matchedLocation == '/home') return '/login';
return null;
},
routes: [
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const AuthPhone(),
),
GoRoute(
path: '/otp',
name: 'otp',
builder: (context, state) => const OtpScreen(),
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'profile',
name: 'profile',
builder: (context, state) => const ProfilePage(),
),
],
),
],
);
return MaterialApp.router(
title: 'My App',
theme: authTheme(),
routerConfig: router,
debugShowCheckedModeBanner: false,
);
}
}
Firestore Security Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function getUserRole() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.userType;
}
function isVendor() { return getUserRole() == 'vendor'; }
function isAdmin() { return getUserRole() == 'admin'; }
function isCustomer() { return getUserRole() == 'customer'; }
function isOwner(userId) { return request.auth.uid == userId; }
// Users — authenticated users can read; only owner can write
match /users/{userId} {
allow read: if isAuthenticated();
allow write: if isAuthenticated() && isOwner(userId);
}
// Vendors — any authenticated user can register; only owner or admin can update; only admin can delete
match /vendors/{vendorId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated();
allow update: if isAuthenticated() && (isOwner(vendorId) || isAdmin());
allow delete: if isAuthenticated() && isAdmin();
}
// Products — only vendors can create; vendor who owns it or admin can update/delete
match /products/{productId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated() && isVendor();
allow update: if isAuthenticated() &&
(isVendor() && resource.data.vendorId == request.auth.uid) || isAdmin();
allow delete: if isAuthenticated() &&
(isVendor() && resource.data.vendorId == request.auth.uid) || isAdmin();
}
// Orders — customer, vendor, or admin can read relevant orders; only customers can create
match /orders/{orderId} {
allow read: if isAuthenticated() && (
resource.data.customerId == request.auth.uid ||
isVendor() && resource.data.vendorId == request.auth.uid ||
isAdmin()
);
allow create: if isAuthenticated() && isCustomer();
allow update: if isAuthenticated() && (
(isCustomer() && resource.data.customerId == request.auth.uid) ||
(isVendor() && resource.data.vendorId == request.auth.uid) ||
isAdmin()
);
}
// Reviews — anyone authenticated can read; only customers can create; owner or admin can edit/delete
match /reviews/{reviewId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated() && isCustomer();
allow update, delete: if isAuthenticated() &&
(isOwner(resource.data.userId) || isAdmin());
}
// Categories — authenticated read; only admin can write
match /categories/{categoryId} {
allow read: if isAuthenticated();
allow write: if isAuthenticated() && isAdmin();
}
}
}
User Roles & Services
UserService
// lib/services/user_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class UserService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
/// Create or update a user document with a role type.
/// [userType] accepts: 'customer', 'vendor', 'admin'
Future<void> setUserType(String userId, String userType) async {
await _firestore.collection('users').doc(userId).set({
'userId': userId,
'phoneNumber': _auth.currentUser?.phoneNumber,
'userType': userType,
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
}
/// Returns true if the current user has the 'vendor' role.
Future<bool> isVendor() async {
final user = _auth.currentUser;
if (user == null) return false;
final doc = await _firestore.collection('users').doc(user.uid).get();
return doc.data()?['userType'] == 'vendor';
}
/// Returns the userType string, or 'customer' if unset, or 'none' if not logged in.
Future<String> getUserType() async {
final user = _auth.currentUser;
if (user == null) return 'none';
final doc = await _firestore.collection('users').doc(user.uid).get();
return doc.data()?['userType'] ?? 'customer';
}
}
Riverpod Providers for User Type
// lib/providers/user_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/user_service.dart';
final userTypeProvider = FutureProvider<String>((ref) async {
return await UserService().getUserType();
});
final isVendorProvider = FutureProvider<bool>((ref) async {
return await UserService().isVendor();
});
/// Auto-refreshes when auth state changes
final vendorStatusProvider = Provider<bool>((ref) {
final authState = ref.watch(authStateProvider);
final isVendorAsync = ref.watch(isVendorProvider);
return isVendorAsync.when(
data: (isVendor) => isVendor,
loading: () => false,
error: (_, __) => false,
);
});
After Login — Set User Type
Future<void> handleSuccessfulLogin(UserCredential credential) async {
final user = credential.user;
if (user != null) {
final userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
if (!userDoc.exists) {
// First-time login — default to customer
await UserService().setUserType(user.uid, 'customer');
}
}
}
Vendor Registration
Future<void> registerAsVendor() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
// Update user role to vendor
await UserService().setUserType(user.uid, 'vendor');
// Create vendor document
await FirebaseFirestore.instance.collection('vendors').doc(user.uid).set({
'vendorId': user.uid,
'businessName': businessName,
'phoneNumber': user.phoneNumber,
'status': 'approved', // or 'pending' for manual review
'createdAt': FieldValue.serverTimestamp(),
'totalProducts': 0,
'rating': 0,
});
}
Vendor-Only Actions
class AddProductScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
future: UserService().isVendor(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
if (snapshot.data != true) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.block, size: 64),
const SizedBox(height: 16),
const Text('Only vendors can add products'),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/register-vendor'),
child: const Text('Register as Vendor'),
),
],
),
),
);
}
return _AddProductForm();
},
);
}
}
class _AddProductForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add Product')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(decoration: const InputDecoration(labelText: 'Product Name')),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 3,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () async {
final user = FirebaseAuth.instance.currentUser;
// Security rules will verify this is a vendor
await FirebaseFirestore.instance.collection('products').add({
'name': 'Product Name',
'price': 100,
'description': 'Description',
'vendorId': user!.uid,
'createdAt': FieldValue.serverTimestamp(),
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Product added!')),
);
},
child: const Text('Add Product'),
),
],
),
),
);
}
}
Custom Router (Manual Setup)
If you build your own router instead of using AuthRouter.createRouter(), you must wire up RouterNotifier manually:
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final routerNotifier = RouterNotifier(ref);
final router = GoRouter(
initialLocation: '/phone',
refreshListenable: routerNotifier, // REQUIRED
redirect: routerNotifier.redirect, // REQUIRED
routes: [
// Your routes here
],
);
return MaterialApp.router(routerConfig: router);
}
}
Note: Omitting
refreshListenableandredirectmeans the router will not react to auth state changes automatically.
Quick Reference Card
// 1. Check if user is logged in
final isAuth = ref.watch(isAuthenticatedProvider);
// 2. Get current user
final user = ref.watch(currentUserProvider);
// 3. Get full auth state
final authState = ref.watch(authStateProvider);
// 4. Auth state checks
authState.isAuthenticated // bool
authState.isLoading // bool
authState.status // AuthStatus enum
// 5. User data
user?.uid // String
user?.phoneNumber // String?
user?.email // String?
user?.displayName // String?
user?.photoURL // String?
user?.creationTime // DateTime?
user?.lastSignInTime // DateTime?
user?.isEmailVerified // bool
user?.isPhoneVerified // bool
// 6. Phone auth state
final phoneAuth = ref.watch(phoneAuthProvider);
phoneAuth.phoneNumber // String?
phoneAuth.verificationId // String?
phoneAuth.isCodeSent // bool
phoneAuth.isLoading // bool
phoneAuth.error // String?
// 7. Sign out
await ref.read(authStateProvider.notifier).signOut();
// 8. Navigate after sign out
context.go('/login');
context.pushReplacement('/login');
Full Manual Control with RouterNotifier
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:dot_auth/dot_auth.dart';
final routerProvider = Provider<GoRouter>((ref) {
// Create RouterNotifier to listen to auth changes
final routerNotifier = RouterNotifier(ref);
return GoRouter(
initialLocation: '/login',
refreshListenable: routerNotifier, // This enables auto-redirect
redirect: (context, state) {
// Get auth state
final authState = ref.read(authStateProvider);
final isAuthenticated = authState.isAuthenticated;
// Define routes
final isLoginRoute = state.matchedLocation == '/login';
final isOtpRoute = state.matchedLocation == '/otp';
final isHomeRoute = state.matchedLocation == '/home';
// Redirect logic
if (isAuthenticated && (isLoginRoute || isOtpRoute)) {
return '/home';
}
if (!isAuthenticated && isHomeRoute) {
return '/login';
}
// No redirect needed
return null;
},
routes: [
// Auth routes
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const AuthPhone(),
),
GoRoute(
path: '/otp',
name: 'otp',
builder: (context, state) => const OtpScreen(),
),
// Protected routes
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
// Add more routes as needed
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => const ProfilePage(),
),
],
);
});
Need Help?
License
MIT License — see LICENSE for details.
Made with ❤️ for the Flutter community