import React, { useEffect, useRef } from 'react'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { HeroUINativeProviderRaw } from 'heroui-native/provider-raw'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { ThemeProvider } from './src/state/ThemeContext'; import { ConnectionProvider } from './src/state/ConnectionContext'; import { AuthProvider, useAuth } from './src/state/AuthContext'; import { AlarmProvider } from './src/state/AlarmContext'; import LoginScreen from './src/screens/LoginScreen'; import HomeScreen from './src/screens/HomeScreen'; import AlertDetailScreen from './src/screens/AlertDetailScreen'; import QrDisplayScreen from './src/screens/QrDisplayScreen'; import ScannerScreen from './src/screens/ScannerScreen'; import SettingsScreen from './src/screens/SettingsScreen'; import LiveAlarmScreen from './src/screens/LiveAlarmScreen'; import { LoadingView } from './src/components/ui'; import { navigationRef, openLiveAlarm } from './src/navigationRef'; import { getLaunchAlert } from './src/services/alarmNative'; import type { RootStackParamList } from './src/navigation'; const Stack = createNativeStackNavigator(); function Navigator({ initialRoute }: { initialRoute: 'Home' | 'Login' }) { return ( {/* The takeover is a modal presentation with no gesture dismissal and no back affordance: while an alarm is live it is the only reachable surface (spec §9, Cross-screen rules). */} ); } /** * A native full-screen launch (`AlarmActivity`, started by the armed foreground * service) carries the alert id in its intent. Read it once on boot and open the * takeover, so a trigger that cold-starts the process still lands on the alarm * rather than the home screen. Ordinary launches resolve `null`. * * Runs inside the navigator: the intent read is async, but `navigationRef` is * not ready on the first frame, so a launch that arrives early is retried until * the container is up (bounded — it is a boot path, not a poll). */ function useNativeLaunchTakeover() { useEffect(() => { let cancelled = false; let attempts = 0; let timer: ReturnType | undefined; const open = (params: RootStackParamList['LiveAlarm']) => { if (cancelled) return; if (navigationRef.isReady()) { openLiveAlarm(params); return; } if (attempts++ > 20) return; timer = setTimeout(() => open(params), 100); }; getLaunchAlert() .then(launch => { if (cancelled || !launch) return; open({ alertId: launch.alertId, message: launch.message || undefined, }); }) .catch(() => { // No native module (iOS / JS-only run) — nothing to do. }); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, []); } function Root() { const navigation = useNavigation>(); const { token, booting } = useAuth(); const prevToken = useRef(token); // Single navigator owns every route; auth state is driven by navigation // resets rather than remounting whole navigators. A reset always replaces // the whole stack, so Login/Home is the only route left, with no back stack. useEffect(() => { if (token === prevToken.current) return; prevToken.current = token; if (token) { navigation.reset({ index: 0, routes: [{ name: 'Home' }] }); } else { navigation.reset({ index: 0, routes: [{ name: 'Login' }] }); } }, [token, navigation]); useNativeLaunchTakeover(); if (booting) return ; return ( ); } function App() { return ( ); } export default App;