router top-level property
final
Main application router using GoRouter for navigation.
Defines the app's complete route structure with auth guards:
/loginand/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(),
),
);