import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { Stack, router, useRootNavigationState, useSegments } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import { StatusBar } from 'expo-status-bar'; import { useEffect, useRef } from 'react'; import 'react-native-reanimated'; import { StripeProvider } from '@stripe/stripe-react-native'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { ToastProvider } from '@/components/ui/Toast'; import { AuthProvider, useAuth } from '@/contexts/AuthContext'; import { BeneficiaryProvider } from '@/contexts/BeneficiaryContext'; import { BLEProvider, useBLE } from '@/contexts/BLEContext'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { setOnLogoutBLECleanupCallback } from '@/services/api'; import { performanceService, PERFORMANCE_THRESHOLDS } from '@/services/performance'; // Mark app startup as early as possible performanceService.markAppStart(); // Stripe publishable key (test mode) - must match backend STRIPE_PUBLISHABLE_KEY const STRIPE_PUBLISHABLE_KEY = 'pk_test_51P3kdqP0gvUw6M9C7ixPQHqbPcvga4G5kAYx1h6QXQAt1psbrC2rrmOojW0fTeQzaxD1Q9RKS3zZ23MCvjjZpWLi00eCFWRHMk'; // Prevent auto-hide, ignore errors if splash not available SplashScreen.preventAutoHideAsync().catch(() => {}); // Track if splash screen was hidden to prevent double-hiding let splashHidden = false; function RootLayoutNav() { const colorScheme = useColorScheme(); const { isAuthenticated, isInitializing, user } = useAuth(); const { cleanupBLE } = useBLE(); const segments = useSegments(); const navigationState = useRootNavigationState(); // Track if initial redirect was done const hasInitialRedirect = useRef(false); // Set up BLE cleanup callback for logout // Use ref to ensure callback is always current and stable const cleanupBLERef = useRef(cleanupBLE); useEffect(() => { cleanupBLERef.current = cleanupBLE; }, [cleanupBLE]); useEffect(() => { // Set callback that calls the current ref (always up-to-date) setOnLogoutBLECleanupCallback(() => cleanupBLERef.current()); // Cleanup: remove callback on unmount return () => { setOnLogoutBLECleanupCallback(null); }; }, []); // Empty deps - set once, callback uses ref useEffect(() => { // Wait for navigation to be ready if (!navigationState?.key) { return; } // Wait for INITIAL auth check to complete if (isInitializing) { return; } // Hide splash screen safely (only once) if (!splashHidden) { splashHidden = true; SplashScreen.hideAsync().catch(() => {}); // Track app startup performance const startupDuration = performanceService.markAppReady(); if (__DEV__) { const status = startupDuration <= PERFORMANCE_THRESHOLDS.appStartup ? '✓' : '✗'; console.log(`[Performance] App startup: ${startupDuration}ms ${status} (target: ${PERFORMANCE_THRESHOLDS.appStartup}ms)`); } } const inAuthGroup = segments[0] === '(auth)'; const inTabsGroup = segments[0] === '(tabs)'; // INITIAL REDIRECT (only once after app starts): if (!hasInitialRedirect.current) { hasInitialRedirect.current = true; // Not authenticated → redirect to login if (!isAuthenticated && !inAuthGroup) { router.replace('/(auth)/login'); return; } // Authenticated but in auth group → this means user just logged in // Let the auth screens handle navigation via NavigationController // They will call navigateAfterLogin() to determine the correct route if (isAuthenticated && inAuthGroup) { // Don't redirect - let auth screens finish their flow return; } // Authenticated and in tabs group → user opened app while logged in // Check if user should be in onboarding flow if (isAuthenticated && inTabsGroup && user) { // If user has no name, redirect to complete profile if (!user.firstName) { router.replace('/(auth)/enter-name'); return; } // Otherwise, stay on current tab - user is properly authenticated } } // ONGOING PROTECTION: // If user logs out, redirect to login if (!isAuthenticated && inTabsGroup) { router.replace('/(auth)/login'); return; } }, [isAuthenticated, isInitializing, navigationState?.key, user]); // IMPORTANT: segments not in deps - we don't want to react to navigation changes if (isInitializing) { return ; } return ( ); } export default function RootLayout() { return ( ); }