dot_auth 1.0.14 copy "dot_auth: ^1.0.14" to clipboard
dot_auth: ^1.0.14 copied to clipboard

A powerful Flutter authentication package with phone number OTP verification using Firebase.

dot_auth #

A Flutter package for phone-based Firebase authentication with Riverpod state management and GoRouter navigation — batteries included.

pub version license Flutter


Table of Contents #


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 refreshListenable and redirect means 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

1
likes
145
points
400
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A powerful Flutter authentication package with phone number OTP verification using Firebase.

Repository (GitHub)

Topics

#authentication #firebase #phone-auth #riverpod #otp

License

MIT (license)

Dependencies

animations, firebase_auth, firebase_core, flutter, flutter_riverpod, flutter_screenutil, go_router, google_fonts, phone_form_field, pinput, state_notifier

More

Packages that depend on dot_auth