import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'core/auth/auth_notifier.dart'; import 'features/auth/screens/welcome_screen.dart'; import 'features/auth/screens/display_name_screen.dart'; import 'features/auth/screens/register_screen.dart'; import 'features/auth/screens/otp_screen.dart'; import 'features/auth/screens/force_register_screen.dart'; import 'features/auth/screens/set_display_name_screen.dart'; import 'features/onboarding/onboarding_screen.dart'; import 'features/splash/splash_screen.dart'; import 'features/home/home_screen.dart'; import 'core/constants.dart'; import 'features/chat/screens/searching_screen.dart'; import 'features/chat/screens/bestie_found_screen.dart'; import 'features/chat/screens/no_bestie_screen.dart'; import 'features/chat/screens/chat_screen.dart'; import 'features/chat/screens/chat_history_screen.dart'; import 'features/chat/screens/chat_transcript_screen.dart'; import 'features/payment/screens/payment_screen.dart'; import 'core/theme/_preview.dart'; class RouterNotifier extends ChangeNotifier { final Ref _ref; RouterNotifier(this._ref) { _ref.listen(authProvider, (prev, next) { // Errors are handled locally by screens (toast) — they should never // trigger router/Navigator rebuilds, otherwise the active SnackBar // re-animates and looks like a duplicate toast. if (next is AsyncError) return; // Skip transient AsyncLoading where the data variant didn't change. if (prev?.valueOrNull?.runtimeType == next.valueOrNull?.runtimeType) { return; } notifyListeners(); }); } } /// Cached onboarding status — loaded once at startup, invalidated after onboarding completes final onboardingDoneProvider = FutureProvider((ref) => isOnboardingDone()); final routerProvider = Provider((ref) => buildRouter(ref)); GoRouter buildRouter(Ref ref) { final notifier = RouterNotifier(ref); return GoRouter( initialLocation: kThemePreviewEnabled ? '/_theme_preview' : '/splash', refreshListenable: notifier, redirect: (context, state) { // Theme preview is dev-only and intentionally bypasses auth + onboarding // gates so it can be opened on any device build. if (state.matchedLocation == '/_theme_preview') return null; final authState = ref.read(authProvider); final isSplash = state.matchedLocation == '/splash'; final isOnboarding = state.matchedLocation == '/onboarding'; final isAuthRoute = state.matchedLocation.startsWith('/auth') || state.matchedLocation == '/welcome'; // Show splash only during initial load if (authState is AsyncLoading) { if (isSplash || isAuthRoute || isOnboarding) return null; return '/splash'; } // Check onboarding status — must complete before anything else final onboardingDone = ref.read(onboardingDoneProvider).valueOrNull ?? false; if (!onboardingDone) { return isOnboarding ? null : '/onboarding'; } if (isOnboarding) { return '/welcome'; } final data = authState.valueOrNull; if (data == null) { // Error state — show login if (!isAuthRoute && !isSplash) return '/welcome'; if (isSplash) return '/welcome'; return null; } if (data is AuthAuthenticatedData || data is AuthAnonymousData) { return (isSplash || isAuthRoute) ? '/home' : null; } if (data is AuthNeedsDisplayNameData) return '/auth/set-name'; if (data is AuthForceRegisterData) return '/auth/force-register'; if (!isAuthRoute && !isSplash) return '/welcome'; if (isSplash) return '/welcome'; return null; }, routes: [ if (kThemePreviewEnabled) GoRoute(path: '/_theme_preview', builder: (_, __) => const ThemePreviewScreen()), GoRoute(path: '/splash', builder: (_, __) => const SplashScreen()), GoRoute(path: '/onboarding', builder: (_, __) => const OnboardingScreen()), GoRoute(path: '/welcome', builder: (_, __) => const WelcomeScreen()), GoRoute(path: '/auth/display-name', builder: (_, __) => const DisplayNameScreen()), GoRoute(path: '/auth/register', builder: (_, __) => const RegisterScreen()), GoRoute(path: '/auth/otp', builder: (context, state) => OtpScreen(phone: state.extra as String)), GoRoute(path: '/auth/set-name', builder: (_, __) => const SetDisplayNameScreen()), GoRoute(path: '/auth/force-register', builder: (_, __) => const ForceRegisterScreen()), GoRoute(path: '/home', builder: (_, __) => const HomeScreen()), GoRoute(path: '/payment', builder: (context, state) { // Payment screen reachable from // - Home "Mulai Curhat" CTA → no extras (general blast follows confirm) // - Chat history "Curhat lagi" CTA → extras carry targetedMitraId/mitraName // for the returning-chat flow, plus optional topicSensitivity. final extra = state.extra; if (extra is Map) { final topic = extra['topicSensitivity']; return PaymentScreen( targetedMitraId: extra['targetedMitraId'] as String?, mitraName: extra['mitraName'] as String?, topicSensitivity: topic is TopicSensitivity ? topic : TopicSensitivity.regular, ); } return const PaymentScreen(); }), GoRoute(path: '/chat/searching', builder: (_, __) => const SearchingScreen()), GoRoute(path: '/chat/found', builder: (context, state) { final extra = state.extra as Map; return BestieFoundScreen( sessionId: extra['sessionId'] as String, mitraName: extra['mitraName'] as String, ); }), GoRoute(path: '/chat/no-bestie', builder: (_, __) => const NoBestieScreen()), GoRoute(path: '/chat/session/:sessionId', builder: (context, state) { final extra = state.extra; final mitraName = extra is String ? extra : (extra is Map ? extra['mitraName'] as String? : null); return ChatScreen( sessionId: state.pathParameters['sessionId']!, mitraName: mitraName ?? 'Bestie', ); }), GoRoute(path: '/chat/history', builder: (_, __) => const ChatHistoryScreen()), GoRoute(path: '/chat/history/:sessionId', builder: (context, state) { return ChatTranscriptScreen(sessionId: state.pathParameters['sessionId']!); }), ], ); }