import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../features/admin/presentation/admin_shell.dart'; import '../../features/admin/presentation/brackets/admin_bracket_form_screen.dart'; import '../../features/admin/presentation/brackets/admin_brackets_screen.dart'; import '../../features/admin/presentation/events/admin_event_form_screen.dart'; import '../../features/admin/presentation/events/admin_events_screen.dart'; import '../../features/admin/presentation/pending/admin_pending_screen.dart'; import '../../features/admin/presentation/suggestions/admin_suggestions_screen.dart'; import '../../features/admin/presentation/teams/admin_team_form_screen.dart'; import '../../features/admin/presentation/teams/admin_teams_screen.dart'; import '../../features/auth/application/auth_notifier.dart'; import '../../features/auth/presentation/login_screen.dart'; import '../../features/auth/presentation/register_screen.dart'; import '../../features/brackets/presentation/bracket_detail_screen.dart'; import '../../features/brackets/presentation/brackets_screen.dart'; import '../../features/events/presentation/event_detail_screen.dart'; import '../../features/events/presentation/events_screen.dart'; import '../../features/media/presentation/media_screen.dart'; import '../../features/profile/application/profile_notifier.dart'; import '../../features/profile/domain/user_profile.dart'; import '../../features/profile/presentation/manager_dashboard_screen.dart'; import '../../features/profile/presentation/my_profile_screen.dart'; import '../../features/profile/presentation/player_profile_screen.dart'; import '../../features/stats/presentation/stats_screen.dart'; import '../../features/suggestions/presentation/suggestions_screen.dart'; import '../../features/teams/presentation/create_team_screen.dart'; import '../../features/teams/presentation/team_detail_screen.dart'; import '../../features/teams/presentation/teams_screen.dart'; import '../admin/admin_guard.dart'; import '../shell/main_shell.dart'; /// Routes that an unauthenticated user is allowed to visit. Anything else /// triggers a redirect to `/login`. Player profile pages stay reachable to /// signed-out viewers so shared links work. const _publicRoutes = {'/login', '/register'}; /// Path prefixes that anonymous viewers may visit even without a session. /// Player profile pages are intentionally readable so a roster link shared /// outside the app still works. const _viewerPrefixes = ['/players/']; final appRouterProvider = Provider((ref) { // GoRouter listens to this notifier and re-evaluates `redirect` whenever // it fires — we ping it on every auth state change. final refresh = _AuthRouterRefresh(ref); ref.onDispose(refresh.dispose); return GoRouter( initialLocation: '/events', refreshListenable: refresh, redirect: (context, state) { final auth = ref.read(authNotifierProvider); // Don't redirect while the initial auth check is still loading — // GoRouter will re-run this once the notifier has data. if (auth.isLoading || !auth.hasValue) return null; final user = auth.value; final location = state.matchedLocation; final isPublic = _publicRoutes.contains(location); final isViewerOk = _viewerPrefixes.any(location.startsWith); final isAdminRoute = location.startsWith('/admin'); final isManagerRoute = location == '/manager'; if (user == null && !isPublic && !isViewerOk) { return '/login'; } if (user != null && isPublic) { return '/events'; } if (isAdminRoute && !isAdmin(user)) { return '/events'; } if (isManagerRoute) { // Manager dashboard is reserved for users with the manager role // (admins also fall through — they have their own panel). final role = ref.read(currentUserRoleProvider); if (role != UserRole.manager) { return '/events'; } } return null; }, routes: [ GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), GoRoute( path: '/register', builder: (context, state) => const RegisterScreen(), ), ShellRoute( builder: (context, state, child) => MainShell(child: child), routes: [ GoRoute( path: '/events', builder: (context, state) => const EventsScreen(), routes: [ GoRoute( path: ':id', builder: (context, state) => EventDetailScreen(eventId: state.pathParameters['id']!), ), ], ), GoRoute( path: '/brackets', builder: (context, state) => const BracketsScreen(), routes: [ GoRoute( path: ':id', builder: (context, state) => BracketDetailScreen(bracketId: state.pathParameters['id']!), ), ], ), GoRoute( path: '/teams', builder: (context, state) => const TeamsScreen(), routes: [ GoRoute( path: 'new', builder: (context, state) => const CreateTeamScreen(), ), GoRoute( path: ':id', builder: (context, state) => TeamDetailScreen(teamId: state.pathParameters['id']!), ), ], ), GoRoute( path: '/stats', builder: (context, state) => const StatsScreen(), ), GoRoute( path: '/media', builder: (context, state) => const MediaScreen(), ), GoRoute( path: '/suggestions', builder: (context, state) => const SuggestionsScreen(), ), GoRoute( path: '/profile', builder: (context, state) => const MyProfileScreen(), ), GoRoute( path: '/players/:uid', builder: (context, state) => PlayerProfileScreen(uid: state.pathParameters['uid']!), ), GoRoute( path: '/manager', builder: (context, state) => const ManagerDashboardScreen(), ), ], ), ShellRoute( builder: (context, state, child) => AdminShell(child: child), routes: [ GoRoute( path: '/admin/events', builder: (context, state) => const AdminEventsScreen(), ), GoRoute( path: '/admin/events/new', builder: (context, state) => const AdminEventFormScreen(), ), GoRoute( path: '/admin/events/:id/edit', builder: (context, state) => AdminEventFormScreen(eventId: state.pathParameters['id']), ), GoRoute( path: '/admin/teams', builder: (context, state) => const AdminTeamsScreen(), ), GoRoute( path: '/admin/teams/new', builder: (context, state) => const AdminTeamFormScreen(), ), GoRoute( path: '/admin/teams/:id/edit', builder: (context, state) => AdminTeamFormScreen(teamId: state.pathParameters['id']), ), GoRoute( path: '/admin/brackets', builder: (context, state) => const AdminBracketsScreen(), ), GoRoute( path: '/admin/brackets/new', builder: (context, state) => const AdminBracketFormScreen(), ), GoRoute( path: '/admin/brackets/:id/edit', builder: (context, state) => AdminBracketFormScreen(bracketId: state.pathParameters['id']), ), GoRoute( path: '/admin/suggestions', builder: (context, state) => const AdminSuggestionsScreen(), ), GoRoute( path: '/admin/pending', builder: (context, state) => const AdminPendingScreen(), ), ], ), ], ); }); /// Bridges the Riverpod auth notifier (and the derived role provider) to a /// [ChangeNotifier] that GoRouter can subscribe to via `refreshListenable`. /// Sign-in/out and role changes both ping GoRouter to re-run `redirect`. class _AuthRouterRefresh extends ChangeNotifier { _AuthRouterRefresh(this._ref) { _authSub = _ref.listen( authNotifierProvider, (prev, next) => notifyListeners(), fireImmediately: false, ); _roleSub = _ref.listen( currentUserRoleProvider, (prev, next) { if (prev != next) notifyListeners(); }, fireImmediately: false, ); } final Ref _ref; late final ProviderSubscription _authSub; late final ProviderSubscription _roleSub; @override void dispose() { _authSub.close(); _roleSub.close(); super.dispose(); } }