import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/auth_state.dart'; import 'package:qpay/navbar.dart'; import 'package:qpay/screens/confirm/confirm_screen.dart'; import 'package:qpay/screens/gateway/gateway_redirect.dart'; import 'package:qpay/screens/gateway/gateway_screen.dart'; import 'package:qpay/screens/gateway/gateway_web_screen.dart'; import 'package:qpay/screens/groups/batch_create_screen.dart'; import 'package:qpay/screens/groups/batch_detail_screen.dart'; import 'package:qpay/screens/groups/batch_item_screen.dart'; import 'package:qpay/screens/groups/group_create_screen.dart'; import 'package:qpay/screens/groups/group_detail_screen.dart'; import 'package:qpay/screens/groups/groups_screen.dart'; import 'package:qpay/screens/groups/models/group_batch_model.dart'; import 'package:qpay/screens/groups/models/recipient_group_model.dart'; import 'package:qpay/screens/history/history_screen.dart'; import 'package:qpay/screens/home/home_screen.dart'; import 'package:qpay/screens/integration/integration_screen.dart'; import 'package:qpay/screens/onboarding/complete_screen.dart'; import 'package:qpay/screens/onboarding/landing/landing_screen.dart'; import 'package:qpay/screens/onboarding/login/login_screen.dart'; import 'package:qpay/screens/onboarding/phone/phone_screen.dart'; import 'package:qpay/screens/onboarding/verify/verify_screen.dart'; import 'package:qpay/screens/pay/pay_screen.dart'; import 'package:qpay/screens/poll/poll_screen.dart'; import 'package:qpay/screens/profile_screen.dart'; import 'package:qpay/screens/receipt/receipt_screen.dart'; import 'package:qpay/screens/recipient/recipients_screen.dart'; import 'package:qpay/screens/splash_screen.dart'; /// Tracks whether the splash animation is complete so that /// GoRouter's redirect can show the splash first, then release /// navigation to the originally intended URL. class SplashState extends ChangeNotifier { bool _complete = false; /// The route the user originally intended to visit before being /// redirected to /splash. Set by the redirect callback on first /// invocation. String? _intendedRoute; bool get complete => _complete; String? get intendedRoute => _intendedRoute; /// Called by the redirect to store the intended destination and /// return the splash route. String? guard(String attemptedRoute) { if (_complete) return null; // Avoid saving the splash route itself. if (attemptedRoute == '/splash') return null; _intendedRoute = attemptedRoute; return '/splash'; } void finish() { _complete = true; notifyListeners(); } } final SplashState splashState = SplashState(); /// Route prefixes that require the user to be signed in. Any URL /// whose path starts with one of these strings will be redirected /// to the sign-in screen when no auth token is present. const List _authProtectedRoutePrefixes = ['/groups']; bool _isAuthProtected(String path) { for (final prefix in _authProtectedRoutePrefixes) { if (path == prefix || path.startsWith('$prefix/')) { return true; } } return false; } /// Builds the application's [GoRouter] instance. /// /// The router is responsible for: /// - Showing the splash screen on cold start and replaying the /// originally intended route when the animation finishes. /// - Hosting the [ShellRoute] that adds the [ScaffoldWithNavBar] /// to every authenticated screen (the onboarding flow renders /// without the shell so the UI stays full-bleed). /// - Enforcing the auth wall around protected routes (currently the /// groups flow). Unauthenticated users are bounced to the sign-in /// screen, then sent back to their original destination after a /// successful sign-in. GoRouter buildRouter() { return GoRouter( initialLocation: '/', // React to both the splash completion and auth state changes so // protected routes are re-evaluated as the user signs in or out. refreshListenable: Listenable.merge([splashState, authState]), errorBuilder: (context, state) => Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Page not found', style: TextStyle(fontSize: 20)), const SizedBox(height: 16), ElevatedButton( onPressed: () => context.go('/'), child: const Text('Go Home'), ), ], ), ), ), redirect: (context, state) { // Before splash is complete: redirect everything to /splash, // but save the original intended destination. final guardResult = splashState.guard(state.uri.path); if (guardResult != null) return guardResult; // Once splash is complete we can start evaluating auth rules. // We also wait for the initial auth state read to complete so // a persisted token isn't accidentally ignored on cold start. if (splashState.complete && isAuthInitialized) { final path = state.uri.path; // If the user has just signed in and we still have a pending // intended route, take them there instead of leaving them // stuck on the sign-in screen. if (path == '/onboarding/login' && authState.isLoggedIn) { final intended = authState.consumeIntendedRoute(); if (intended != null && intended != '/onboarding/login') { return intended; } } // Auth wall: any protected route requires a valid token. if (_isAuthProtected(path) && !authState.isLoggedIn) { return authState.requireAuth(path); } // Splash → original route or home. if (path == '/splash') { return splashState.intendedRoute ?? '/'; } } return null; }, routes: [ // Splash route sits outside the ShellRoute so it renders // without the bottom nav bar or side rail. GoRoute( path: '/splash', builder: (context, state) => const SplashScreen(), ), ShellRoute( builder: (context, state, child) { final location = GoRouterState.of(context).uri.toString(); if (location.startsWith('/onboarding/')) { return child; } return ScaffoldWithNavBar(child: child); }, routes: [ GoRoute(path: '/', builder: (context, state) => const HomeScreen()), GoRoute( path: '/home', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/make-payment', builder: (context, state) => const PayScreen(), ), GoRoute( path: '/confirm', builder: (context, state) => const ConfirmScreen(), ), GoRoute( path: '/gateway', builder: (context, state) => const GatewayScreen(), ), GoRoute( path: '/gateway-web', builder: (context, state) => const GatewayWebScreen(), ), GoRoute( path: '/gateway-redirect', builder: (context, state) => const GatewayRedirectScreen(), ), GoRoute( path: '/poll/:id', builder: (context, state) => PollScreen(transactionId: state.pathParameters['id']), ), GoRoute( path: '/poll', builder: (context, state) => const PollScreen(), ), GoRoute( path: '/receipt', builder: (context, state) => const ReceiptScreen(), ), GoRoute( path: '/receipt/:transactionId', builder: (context, state) => ReceiptScreen( transactionId: state.pathParameters['transactionId'], ), ), GoRoute( path: '/integration', builder: (context, state) => const IntegrationScreen(), ), GoRoute( path: '/recipients', builder: (context, state) => const RecipientsScreen(), ), GoRoute( path: '/history', builder: (context, state) => const HistoryScreen(), ), GoRoute( path: '/profile', builder: (context, state) => const ProfileScreen(), ), GoRoute( path: '/groups', builder: (context, state) => const GroupsScreen(), ), GoRoute( path: '/groups/create', builder: (context, state) => const GroupCreateScreen(), ), GoRoute( path: '/groups/:groupId', builder: (context, state) { final group = state.extra as RecipientGroup; return GroupDetailScreen(group: group); }, ), GoRoute( path: '/groups/:groupId/batches/create', builder: (context, state) { final group = state.extra as RecipientGroup; return BatchCreateScreen(group: group); }, ), GoRoute( path: '/groups/:groupId/batches/:batchId', builder: (context, state) { final batchId = state.pathParameters['batchId']!; final groupId = state.pathParameters['groupId']!; return BatchDetailScreen(batchId: batchId, groupId: groupId); }, ), GoRoute( path: '/groups/:groupId/batches/:batchId/items/:itemId', builder: (context, state) { final item = state.extra as GroupBatchItem; return BatchItemScreen(item: item); }, ), GoRoute( path: '/onboarding/landing', builder: (context, state) => const LandingScreen(), ), GoRoute( path: '/onboarding/phone', builder: (context, state) => const PhoneScreen(), ), GoRoute( path: '/onboarding/verify', builder: (context, state) => const VerifyScreen(), ), GoRoute( path: '/onboarding/login', builder: (context, state) => const LoginScreen(), ), GoRoute( path: '/onboarding/complete', builder: (context, state) => const CompleteScreen(), ), ], ), ], ); }