import { Spinner } from '@/components/ui/spinner'; import { ToastProvider } from '@/components/ui/toast'; import { View } from '@/components/ui/view'; import { useColorScheme } from '@/hooks/useColorScheme'; import { AuthProvider, useAuth } from '@/providers/auth-provider'; import { ThemeProvider } from '@/providers/theme-provider'; import { Colors } from '@/theme/colors'; import { osName } from 'expo-device'; import { isLiquidGlassAvailable } from 'expo-glass-effect'; import * as NavigationBar from 'expo-navigation-bar'; import { Stack } from 'expo-router'; import * as SecureStore from 'expo-secure-store'; import * as SplashScreen from 'expo-splash-screen'; import { StatusBar } from 'expo-status-bar'; import { setBackgroundColorAsync } from 'expo-system-ui'; import React, { useEffect } from 'react'; import { Platform } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import 'react-native-reanimated'; SplashScreen.setOptions({ duration: 200, fade: true, }); /** * The three route groups and the conditions under which each is reachable. * * `Stack.Protected` unmounts the screens whose guard is false and redirects * away from them, so this is not a cosmetic hide: with no signed-in user there is no * navigation path into `(tabs)` at all, deep link or otherwise. The guards are * mutually exclusive, so exactly one group is mounted at a time. * * Guards are a convenience, not the boundary — firestore.rules says the same * thing server-side, where a modified client cannot argue. A query that is not * scoped to the signed-in uid fails there regardless of what this file does. */ function RootNavigator() { const colorScheme = useColorScheme() || 'light'; const { user, profile, loading } = useAuth(); // `onAuthStateChanged` fires once after the SDK has finished reading // LargeSecureStore, which takes a beat. Rendering the navigator before it // resolves flashes the sign-in screen at a user who is already signed in. if (loading) { return ( ); } const signedIn = !!user; // While the profile document is still being created, `profile` is null. // There is no trigger doing it — the provider writes it from its snapshot // listener — so this window is real. Treating null as "not onboarded" would // flash the onboarding screen at a returning user, so hold them in the app // until it arrives. const needsOnboarding = signedIn && profile?.onboarded === false; return ( ); } /** * Status bar, Android navigation bar and root background, kept in step with the * theme. * * A component rather than inline in `RootLayout` so `useColorScheme()` resolves * *inside* `ThemeProvider`. Read above it and this would see the OS scheme * rather than the user's choice — which on native the global `Appearance` * override happens to paper over, but on web would leave the system chrome * stuck on the system theme. */ function SystemChrome() { const colorScheme = useColorScheme() || 'light'; useEffect(() => { if (Platform.OS === 'android') { // `setButtonStyleAsync` was removed in expo-navigation-bar 57; `setStyle` // is the replacement and is synchronous. NavigationBar.setStyle(colorScheme === 'light' ? 'dark' : 'light'); } }, [colorScheme]); // Keep the root view background color in sync with the current theme useEffect(() => { setBackgroundColorAsync( colorScheme === 'dark' ? Colors.dark.background : Colors.light.background ); }, [colorScheme]); return ( ); } export default function RootLayout() { return ( {/* `storage` makes the light/dark choice survive a restart. SecureStore has no web implementation, so on web this degrades to no persistence rather than erroring — the toggle itself still works there. */} ); }