import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../../../core/network/supabase_status.dart'; import '../../features/auth/presentation/controllers/auth_controller.dart'; import '../../features/auth/presentation/controllers/dev_auth_bypass.dart'; import '../../features/auth/presentation/screens/login_screen.dart'; import '../../features/medication/presentation/screens/medication_screen.dart'; import '../../features/nutrition/presentation/screens/nutrition_screen.dart'; import '../../features/meal_planner/presentation/screens/meal_planner_screen.dart'; import '../../features/pantry/presentation/screens/pantry_screen.dart'; import '../scaffold/adaptive_home_scaffold.dart'; /// Locations that may be visited without an authenticated session. const Set _publicPaths = {'/login'}; final _rootNavigatorKey = GlobalKey(); final _shellNavigatorKey = GlobalKey(); /// Whether the current session satisfies the authenticated guard. /// /// Reads the live GoTrue session when the SDK is initialized and reports /// `false` otherwise. Extracted into a provider so integration tests can /// simulate an authenticated (or unauthenticated) session without booting /// Supabase — they simply override this value. final isAuthenticatedProvider = Provider( (ref) => isSupabaseAvailable && Supabase.instance.client.auth.currentSession != null, ); /// Application router with a reactive authentication guard and dev bypass hook. final routerProvider = Provider((ref) { final authRefreshNotifier = ValueNotifier(0); ref.onDispose(authRefreshNotifier.dispose); // Trigger re-evaluation on GoTrue auth transitions or dev bypass toggle ref.listen(authStateProvider, (_, __) => authRefreshNotifier.value++); ref.listen(devAuthBypassProvider, (_, __) => authRefreshNotifier.value++); return GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/medication', refreshListenable: authRefreshNotifier, redirect: (context, state) => _authGuard(context, state, ref), routes: [ GoRoute( path: '/login', builder: (context, state) => const LoginScreen(), ), ShellRoute( navigatorKey: _shellNavigatorKey, builder: (context, state, child) { return AdaptiveHomeScaffold(child: child); }, routes: [ GoRoute( path: '/medication', builder: (context, state) => const MedicationScreen(), ), GoRoute( path: '/nutrition', builder: (context, state) => const NutritionScreen(), ), GoRoute( path: '/meal-planner', builder: (context, state) => const MealPlannerScreen(), ), GoRoute( path: '/pantry', builder: (context, state) => const PantryScreen(), ), ], ), ], ); }); /// Redirect guard enforcing the passwordless session lifecycle with dev bypass. /// /// * Development bypass (debug builds only): `/login` -> `/medication`, all /// other routes pass without a session. /// * Unauthenticated sessions on a protected route -> `/login`. /// * Authenticated sessions on `/login` -> `/medication`. String? _authGuard(BuildContext context, GoRouterState state, Ref ref) { final isDevBypassed = ref.read(devAuthBypassProvider); final location = state.matchedLocation; if (isDevBypassed) { if (location == '/login') return '/medication'; return null; } final isAuthenticated = ref.read(isAuthenticatedProvider); if (!isAuthenticated && !_publicPaths.contains(location)) { return '/login'; } if (isAuthenticated && location == '/login') { return '/medication'; } return null; }