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 (
);
}