import React, { useEffect, useState, useMemo, useCallback, useRef } 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 { ModalToast } from "@/components/ModalToast"; import "@/global.css"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { View as RNView, ActivityIndicator, AppState, AppStateStatus, } from "react-native"; import { NAV_THEME, loadTheme } from "@/lib/theme"; import { SirouRouterProvider, useSirouRouter } from "@sirou/react-native"; import { refreshTokens } from "@/lib/api-middlewares"; import { ThemeProvider, NavigationIndependentTree } from "@react-navigation/native"; import { routes } from "@/lib/routes"; import { authGuard, guestGuard } from "@/lib/auth-guards"; import { useAuthStore } from "@/lib/auth-store"; import { usePinStore } from "@/lib/pin-store"; import { useFonts } from "expo-font"; import { useColorScheme } from "nativewind"; import { useSegments, useLocalSearchParams, useRouter } from "expo-router"; /** * GlobalGuard: Handles all routing security and authentication redirects. */ function GlobalGuard() { const segments = useSegments(); const params = useLocalSearchParams(); const router = useRouter(); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const sirou = useSirouRouter(); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); useEffect(() => { if (!isMounted || !segments) return; const performGuardCheck = async () => { const routeName = segments.length > 0 ? segments.join("/") : "root"; const isAuthPage = segments[0] === "login" || segments[0] === "register" || segments[0] === "otp"; if (!isAuthenticated && !isAuthPage) { console.log(`[GlobalGuard] Unauthorized on "${routeName}". Ejecting...`); router.replace("/login"); return; } if (isAuthenticated && isAuthPage) { console.log(`[GlobalGuard] Authenticated user on auth page.`); const { hasPin } = usePinStore.getState(); router.replace(hasPin ? "/" : "/set-pin"); return; } try { const result = await (sirou as any).checkGuards(routeName, params); if (!result.allowed && result.redirect) { console.log(`[GlobalGuard] Sirou Guard Redirect -> ${result.redirect}`); router.replace(result.redirect as any); } } catch (e: any) { console.warn(`[GlobalGuard] guard crash:`, e.message); } }; performGuardCheck(); }, [segments, params, sirou, isMounted, isAuthenticated]); return null; } /** * PinGuard: Only locks on cold start (app killed & reopened). * No session timeout, no inactivity lock, no background checks. * Also redirects to set-pin if user is authenticated but has no PIN. */ function PinGuard() { const segments = useSegments(); const router = useRouter(); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const initialCheckDone = useRef(false); useEffect(() => { if (!isAuthenticated || segments.length === 0) return; const pinRoutes = ["pin-lock", "set-pin", "forgot-pin", "forgot-pin-verify"]; const checkPinRedirect = () => { const { hasPin: hp, isLocked } = usePinStore.getState(); const route = segments[0]; if (!hp && !pinRoutes.includes(route)) { router.replace("/set-pin"); return; } if (hp && isLocked() && !pinRoutes.includes(route)) { router.replace("/pin-lock"); } }; // Cold start only — defer to next tick so Stack navigator is fully mounted if (!initialCheckDone.current) { initialCheckDone.current = true; setTimeout(() => checkPinRedirect(), 0); } }, [segments, isAuthenticated]); return null; } /** * SessionHeartbeat: Proactively refreshes tokens. */ function SessionHeartbeat() { const isAuthenticated = useAuthStore((s) => s.isAuthenticated); useEffect(() => { if (!isAuthenticated) return; 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); } }; performRefresh("Mount"); const interval = setInterval(() => performRefresh("Interval"), INTERVAL_MS); const subscription = AppState.addEventListener("change", (nextAppState) => { if (nextAppState === "active") { performRefresh("Foreground"); } }); return () => { clearInterval(interval); subscription.remove(); }; }, [isAuthenticated]); return null; } export default function RootLayout() { const { colorScheme, setColorScheme } = useColorScheme(); const [isMounted, setIsMounted] = useState(false); const [authHydrated, setAuthHydrated] = useState(false); const [pinHydrated, setPinHydrated] = useState(false); const [isThemeRestored, setIsThemeRestored] = 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); // Auth Hydration const initializeAuth = () => { if (useAuthStore.persist.hasHydrated()) { setAuthHydrated(true); } else { useAuthStore.persist.onFinishHydration(() => setAuthHydrated(true)); } }; // Pin Hydration const initializePin = () => { if (usePinStore.persist.hasHydrated()) { setPinHydrated(true); } else { usePinStore.persist.onFinishHydration(() => setPinHydrated(true)); } }; // Theme Restoration const initializeTheme = async () => { try { const savedTheme = await loadTheme(); if (savedTheme && savedTheme !== "system") { setColorScheme(savedTheme); } } catch (e) { console.warn("[RootLayout] Theme restore failed:", e); } finally { setIsThemeRestored(true); } }; initializeAuth(); initializePin(); initializeTheme(); }, []); const theme = useMemo(() => { const isDark = colorScheme === "dark"; return isDark ? NAV_THEME.dark : NAV_THEME.light; }, [colorScheme]); if (!isMounted || !authHydrated || !pinHydrated || !fontsLoaded || !isThemeRestored) { return ( ); } return ( ); }