import React, { useEffect, useState } from "react"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { PortalHost } from "@rn-primitives/portal"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { Toast } from "@/components/Toast"; import "@/global.css"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { View, ActivityIndicator, InteractionManager, AppState, } from "react-native"; import { useRestoreTheme, NAV_THEME } from "@/lib/theme"; import { SirouRouterProvider, useSirouRouter } from "@sirou/react-native"; import { refreshTokens } from "@/lib/api-middlewares"; import { NavigationContainer, NavigationIndependentTree, ThemeProvider, } from "@react-navigation/native"; import { routes } from "@/lib/routes"; import { authGuard, guestGuard } from "@/lib/auth-guards"; import { useAuthStore } from "@/lib/auth-store"; import { useFonts } from "expo-font"; import { api } from "@/lib/api"; import { useColorScheme } from "nativewind"; import { useSegments, useLocalSearchParams, useRouter } from "expo-router"; function BackupGuard() { const segments = useSegments(); const isAuthed = useAuthStore((s) => s.isAuthenticated); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); useEffect(() => { if (!isMounted) return; }, [segments, isAuthed, isMounted]); return null; } /** * SessionHeartbeat: Proactively refreshes tokens every 5 minutes and upon app foregrounding. */ function SessionHeartbeat() { const isAuthenticated = useAuthStore((s) => s.isAuthenticated); useEffect(() => { if (!isAuthenticated) return; // Refresh every 5 minutes const INTERVAL_MS = 5 * 60 * 1000; const performRefresh = async (reason: string) => { try { console.log(`[SessionHeartbeat] Refresh triggered by: ${reason}`); await refreshTokens(); } catch (err) { console.warn(`[SessionHeartbeat] Refresh failed (${reason}):`, err); } }; // 1. Initial/Interval Refresh performRefresh("Mount"); // Refresh immediately on mount const interval = setInterval(() => performRefresh("Interval"), INTERVAL_MS); // 2. Foreground Refresh (AppState listener) const subscription = AppState.addEventListener("change", (nextAppState) => { if (nextAppState === "active") { performRefresh("Foreground"); } }); return () => { clearInterval(interval); subscription.remove(); }; }, [isAuthenticated]); return null; } function SirouBridge() { const sirou = useSirouRouter(); const router = useRouter(); const segments = useSegments(); const params = useLocalSearchParams(); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); useEffect(() => { if (!isMounted) return; const checkAuth = async () => { const routeName = segments.length > 0 ? segments.join("/") : "root"; console.log( `[SirouBridge] checking route: "${routeName}" with params:`, params, ); try { const result = await (sirou as any).checkGuards(routeName, params); if (!result.allowed && result.redirect) { console.log(`[SirouBridge] REDIRECT -> ${result.redirect}`); InteractionManager.runAfterInteractions(() => { // Use Expo Router directly — sirou.go fires NAVIGATE which Expo can't resolve router.replace(result.redirect as any); }); } } catch (e: any) { console.warn( `[SirouBridge] guard crash for "${routeName}":`, e.message, ); } }; checkAuth(); }, [segments, params, sirou, router, isMounted, isAuthenticated]); return null; } export default function RootLayout() { const { colorScheme } = useColorScheme(); useRestoreTheme(); const [isMounted, setIsMounted] = useState(false); const [hasHydrated, setHasHydrated] = useState(false); const [fontsLoaded] = useFonts({ "DMSans-Regular": require("../assets/fonts/DMSans-Regular.ttf"), "DMSans-Bold": require("../assets/fonts/DMSans-Bold.ttf"), "DMSans-Medium": require("../assets/fonts/DMSans-Medium.ttf"), "DMSans-SemiBold": require("../assets/fonts/DMSans-SemiBold.ttf"), "DMSans-Light": require("../assets/fonts/DMSans-Light.ttf"), "DMSans-ExtraLight": require("../assets/fonts/DMSans-ExtraLight.ttf"), "DMSans-Thin": require("../assets/fonts/DMSans-Thin.ttf"), "DMSans-Black": require("../assets/fonts/DMSans-Black.ttf"), "DMSans-ExtraBold": require("../assets/fonts/DMSans-ExtraBold.ttf"), }); useEffect(() => { setIsMounted(true); const initializeAuth = async () => { if (useAuthStore.persist.hasHydrated()) { setHasHydrated(true); } else { const unsub = useAuthStore.persist.onFinishHydration(() => { setHasHydrated(true); }); return unsub; } }; initializeAuth(); }, []); if (!isMounted || !hasHydrated || !fontsLoaded) { return ( ); } return ( ); }