glp-companion/lib/app/router/app_router.dart

105 lines
3.8 KiB
Dart

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<String> _publicPaths = {'/login'};
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
/// 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<bool>(
(ref) => isSupabaseAvailable &&
Supabase.instance.client.auth.currentSession != null,
);
/// Application router with a reactive authentication guard and dev bypass hook.
final routerProvider = Provider<GoRouter>((ref) {
final authRefreshNotifier = ValueNotifier<int>(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;
}