220 lines
7.8 KiB
TypeScript
220 lines
7.8 KiB
TypeScript
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 } from "react-native";
|
|
import { useRestoreTheme, NAV_THEME } from "@/lib/theme";
|
|
import { SirouRouterProvider, useSirouRouter } from "@sirou/react-native";
|
|
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 'react-native';
|
|
|
|
import { useSegments } 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;
|
|
|
|
// Intentionally disabled: redirecting here can happen before the root layout
|
|
// navigator is ready and cause "Attempted to navigate before mounting".
|
|
// Sirou guards handle redirects.
|
|
}, [segments, isAuthed, isMounted]);
|
|
|
|
return null;
|
|
}
|
|
|
|
function SirouBridge() {
|
|
const sirou = useSirouRouter();
|
|
const segments = useSegments();
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!isMounted) return;
|
|
|
|
const checkAuth = async () => {
|
|
// Create EXACT name from segments: (tabs), index => (tabs)/index
|
|
// Use "root" if segments are empty (initial layout)
|
|
const routeName = segments.length > 0 ? segments.join("/") : "root";
|
|
|
|
console.log(`[SirouBridge] checking route: "${routeName}"`);
|
|
|
|
try {
|
|
const result = await (sirou as any).checkGuards(routeName);
|
|
if (!result.allowed && result.redirect) {
|
|
console.log(`[SirouBridge] REDIRECT -> ${result.redirect}`);
|
|
// Use Sirou navigation safely
|
|
InteractionManager.runAfterInteractions(() => {
|
|
sirou.go(result.redirect);
|
|
});
|
|
}
|
|
} catch (e: any) {
|
|
console.warn(
|
|
`[SirouBridge] guard crash for "${routeName}":`,
|
|
e.message,
|
|
);
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, [segments, sirou, 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 (
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
backgroundColor: "rgba(255, 255, 255, 1)",
|
|
}}
|
|
>
|
|
<ActivityIndicator size="large" color="#ea580c" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
<SafeAreaProvider>
|
|
<NavigationIndependentTree>
|
|
<NavigationContainer>
|
|
<SirouRouterProvider config={routes} guards={[authGuard, guestGuard]}>
|
|
<ThemeProvider
|
|
value={colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light}
|
|
>
|
|
<View className="flex-1 bg-background">
|
|
<StatusBar style={colorScheme === "dark" ? "light" : "dark"} />
|
|
<Stack
|
|
screenOptions={{
|
|
headerShown: false,
|
|
}}
|
|
>
|
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
<Stack.Screen
|
|
name="sms-scan"
|
|
options={{ headerShown: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="proforma/[id]"
|
|
options={{ title: "Proforma request" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="payments/[id]"
|
|
options={{ title: "Payment" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="notifications/index"
|
|
options={{ title: "Notifications" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="notifications/settings"
|
|
options={{ title: "Notification settings" }}
|
|
/>
|
|
<Stack.Screen name="help" options={{ headerShown: false }} />
|
|
<Stack.Screen name="terms" options={{ headerShown: false }} />
|
|
<Stack.Screen name="privacy" options={{ headerShown: false }} />
|
|
<Stack.Screen name="history" options={{ headerShown: false }} />
|
|
<Stack.Screen name="company" options={{ headerShown: false }} />
|
|
<Stack.Screen
|
|
name="company-details"
|
|
options={{ headerShown: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="login"
|
|
options={{ title: "Sign in", headerShown: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="register"
|
|
options={{ title: "Create account", headerShown: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="invoices/[id]"
|
|
options={{ title: "Invoice" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="reports/index"
|
|
options={{ title: "Reports" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="documents/index"
|
|
options={{ title: "Documents" }}
|
|
/>
|
|
<Stack.Screen name="settings" options={{ title: "Settings" }} />
|
|
<Stack.Screen name="profile" options={{ headerShown: false }} />
|
|
<Stack.Screen
|
|
name="edit-profile"
|
|
options={{ headerShown: false }}
|
|
/>
|
|
</Stack>
|
|
<SirouBridge />
|
|
<BackupGuard />
|
|
<PortalHost />
|
|
<Toast />
|
|
</View>
|
|
</ThemeProvider>
|
|
</SirouRouterProvider>
|
|
</NavigationContainer>
|
|
</NavigationIndependentTree>
|
|
</SafeAreaProvider>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|