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/exercise/presentation/screens/exercise_screen.dart'; import '../../features/medication/presentation/screens/medication_screen.dart'; import '../../features/nutrition/presentation/screens/nutrition_screen.dart'; import '../../features/nutrition/presentation/screens/food_search_screen.dart'; import '../../features/meal_planner/presentation/screens/meal_planner_screen.dart'; import '../../features/pantry/presentation/screens/pantry_screen.dart'; import '../../features/recipes/presentation/screens/recipes_screen.dart'; import '../../features/settings/presentation/screens/settings_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. Watches [authStateProvider] so the value is re-derived /// on every GoTrue transition — `currentSession` itself is a plain, non /// reactive getter, so without the watch this provider would cache whatever /// was true at first read (e.g. booting already signed-in) and a later /// sign-out could never redirect to `/login`. 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) { // Recompute on every auth transition (signedIn / signedOut / // tokenRefreshed / initialSession). ref.watch(authStateProvider); return 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. // // The auth bump is deferred by one microtask so Riverpod finishes // propagating the transition to dependent providers (notably // isAuthenticatedProvider, which watches authStateProvider) before the // redirect guard re-reads them; ValueNotifier listeners otherwise run // inside the provider-notification cascade and could observe a stale // cached value (symptom: sign-out clears the session but never navigates // to /login). ref.listen(authStateProvider, (_, __) { Future.microtask(() => 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(), ), // Full-screen food search (pushed above the shell so it covers the // navigation rail). `?preset=pantry` reorders the product action // sheet to emphasize adding to pantry stock. GoRoute( path: '/food-search', builder: (context, state) => FoodSearchScreen( preset: foodSearchPresetFrom(state.uri.queryParameters['preset']), ), ), 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: '/exercise', builder: (context, state) => const ExerciseScreen(), ), GoRoute( path: '/meal-planner', builder: (context, state) => const MealPlannerScreen(), ), GoRoute( path: '/recipes', builder: (context, state) => const RecipesScreen(), ), GoRoute( path: '/pantry', builder: (context, state) => const PantryScreen(), ), GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), ), ], ), ], ); }); /// 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; }