router top-level property

GoRouter router
final

Main application router using GoRouter for navigation.

Defines the app's complete route structure with auth guards:

  • /login and /signup: Authentication screens (unauthenticated only)
  • /scan, /basket, /benefits: Main app screens (authenticated only)

The redirect callback enforces authentication rules:

  • Logged-out users can only access auth routes
  • Logged-in users are redirected from auth routes to /scan

Main screens are wrapped in _MainShell which provides the bottom navigation bar for seamless tab switching between ScanScreen, BasketScreen, and BalancesScreen.

Usage: Configure in MaterialApp.router via routerConfig: router

Implementation

final GoRouter router = GoRouter(
  /// Initial route shown when the app starts.
  ///
  /// Set to `/login` to show authentication screen first. The [redirect]
  /// callback will immediately redirect authenticated users to `/scan`.
  initialLocation: '/login',

  routes: [
    // ---------- Auth routes (unauthenticated only) ----------

    /// Login screen for existing users.
    ///
    /// Route: `/login`
    /// Access: Unauthenticated users only
    GoRoute(path: '/login', builder: (context, state) => const LoginScreen()),

    /// Registration screen for new users.
    ///
    /// Route: `/signup`
    /// Access: Unauthenticated users only
    GoRoute(path: '/signup', builder: (context, state) => const SignupPage()),

    // ---------- Main app shell with bottom navigation ----------

    /// Shell route that wraps all authenticated screens.
    ///
    /// Provides a persistent [NavigationBar] at the bottom for tab switching.
    /// All nested routes share this shell without re-rendering the nav bar.
    ///
    /// Access: Authenticated users only (via [redirect])
    ShellRoute(
      builder: (context, state, child) => _MainShell(child: child),
      routes: [
        /// Barcode scanning and product lookup screen.
        ///
        /// Route: `/scan`
        /// Purpose: Main entry point for adding products to basket
        /// Features:
        /// - Live barcode scanning
        /// - Product eligibility checking
        /// - WIC category limit enforcement
        /// - Substitute product suggestions
        GoRoute(
          path: '/scan',
          pageBuilder: (context, state) =>
              const NoTransitionPage(child: ScanScreen()),
        ),

        /// Shopping basket management screen.
        ///
        /// Route: `/basket`
        /// Purpose: View and modify items in the shopping basket
        /// Features:
        /// - Item quantity adjustment
        /// - Category usage display
        /// - Quick access to basket summary
        GoRoute(
          path: '/basket',
          pageBuilder: (context, state) =>
              const NoTransitionPage(child: BasketScreen()),
        ),

        /// WIC benefit balance display screen.
        ///
        /// Route: `/benefits`
        /// Purpose: View current WIC category balances and limits
        /// Features:
        /// - Category-by-category balance view
        /// - Usage progress bars
        /// - Account management (sign out)
        GoRoute(
          path: '/balances',
          pageBuilder: (context, state) =>
              const NoTransitionPage(child: BalancesScreen()),
        ),
      ],
    ),
  ],

  /// Guards routes based on [FirebaseAuth] state and prevents unauthorized access.
  ///
  /// This callback runs before every route transition and determines if the
  /// user is allowed to navigate to the requested route.
  ///
  /// **IMPORTANT:** This uses [FirebaseAuth.instance.currentUser] which may be
  /// null immediately after sign-in while Firebase completes internal state
  /// updates. The [StreamProvider<User?>] in [main.dart] provides more reliable
  /// real-time updates. This redirect is primarily for initial app startup.
  ///
  /// Rules:
  /// 1. **Logged-out users** (no [FirebaseAuth.currentUser]):
  ///    - Can only access `/login` and `/signup`
  ///    - Are redirected to `/login` if they try protected routes
  ///
  /// 2. **Logged-in users** (has [FirebaseAuth.currentUser]):
  ///    - Cannot access `/login` or `/signup`
  ///    - Are redirected to `/scan` if they try auth routes
  ///
  /// Returns:
  /// - `null`: Navigation is allowed (user can access route)
  /// - `/login`: User is redirected to login screen
  /// - `/scan`: User is redirected to main screen
  /// - Other paths: User is redirected to that path
  ///
  /// Side effects:
  /// - Called on every route change
  /// - Does NOT require [BuildContext], just accesses [FirebaseAuth]
  /// - Safe to call multiple times
  redirect: (context, state) {
    /// Get current user from [FirebaseAuth].
    ///
    /// null = logged out
    /// non-null = logged in with email/UID available
    final user = FirebaseAuth.instance.currentUser;

    /// Extract requested route path.
    ///
    /// Example: state.uri.path returns '/login', '/scan', etc.
    final path = state.uri.path;

    /// Check if the requested route is an authentication route.
    ///
    /// Auth routes are routes that should only be accessed by logged-out users.
    final isAuthRoute = path == '/login' || path == '/signup';

    // Logged out → restrict to auth routes only
    if (user == null) {
      /// If trying to access auth route (login/signup), allow it.
      /// Otherwise, redirect to login.
      return isAuthRoute ? null : '/login';
    }

    // Logged in → prevent auth routes, redirect to main screen
    if (isAuthRoute) return '/scan';

    /// User is authenticated and trying to access a valid route.
    /// Allow navigation.
    return null;
  },

  /// Refreshes the route state when auth changes.
  ///
  /// This [GoRouterRefreshStream] listens to [FirebaseAuth.authStateChanges]
  /// and notifies [GoRouter] whenever the authentication state changes.
  /// This triggers the [redirect] callback to re-evaluate with the new auth state.
  ///
  /// When a user signs in or out, this ensures [GoRouter] immediately checks
  /// if the current route is still valid and redirects if necessary.
  refreshListenable: GoRouterRefreshStream(
    FirebaseAuth.instance.authStateChanges(),
  ),
);