import { useEffect } from 'react'; import { AppState, I18nManager } from 'react-native'; I18nManager.allowRTL(true); import { Stack, router } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import * as Notifications from 'expo-notifications'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { KeyboardProvider } from 'react-native-keyboard-controller'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import * as SplashScreen from 'expo-splash-screen'; import { useFonts, Nunito_400Regular, Nunito_600SemiBold, Nunito_700Bold, Nunito_800ExtraBold, } from '@expo-google-fonts/nunito'; import { supabase } from '../lib/supabase'; import { useAuthStore } from '../stores/auth'; import { useThemeStore } from '../stores/theme'; import { useRealtimeDebugStore } from '../stores/realtimeDebug'; import { useColors } from '../lib/theme'; import { useLanguageStore } from '../stores/language'; import { useAppLockStore } from '../stores/appLock'; import { useLyraVoiceStore } from '../stores/lyraVoice'; import { AppLockGate } from '../components/AppLockGate'; import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet'; import { DeviceApprovalIncomingSheet } from '../components/DeviceApprovalIncomingSheet'; import { DeviceApprovalPendingSheet } from '../components/DeviceApprovalPendingSheet'; import { useDeviceApprovalRealtime } from '../hooks/useDeviceApprovalRealtime'; import { useDevicesStore } from '../stores/devices'; import { OnlinePresenceProvider } from '../components/OnlinePresenceProvider'; import { usePushTokenRegistration } from '../hooks/usePushTokenRegistration'; import { useIncomingCalls } from '../hooks/useIncomingCalls'; import { useCallStore } from '../stores/call'; import { useCallKeepEvents } from '../hooks/useCallKeepEvents'; import '../lib/i18n'; // i18next-Init via Side-Effect import '../global.css'; SplashScreen.preventAutoHideAsync(); Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowBanner: true, shouldShowList: true, shouldPlaySound: true, shouldSetBadge: false, }), }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 2, staleTime: 1000 * 60, }, }, }); function RootLayoutInner() { const { loading, init, user } = useAuthStore(); const initTheme = useThemeStore((s) => s.init); const colorScheme = useThemeStore((s) => s.colorScheme); const initLanguage = useLanguageStore((s) => s.init); const initAppLock = useAppLockStore((s) => s.init); const initLyraVoice = useLyraVoiceStore((s) => s.init); const appLockReady = useAppLockStore((s) => s.ready); const initRealtimeDebug = useRealtimeDebugStore((s) => s.init); const colors = useColors(); const [fontsLoaded] = useFonts({ Nunito_400Regular, Nunito_600SemiBold, Nunito_700Bold, Nunito_800ExtraBold, }); // Push-Token-Registration nach Login (idempotent) usePushTokenRegistration(user?.id); // Eingehende Voice-Calls (Ring-Channel) — zeigt den Call-Screen wenn jemand // anruft. Foreground-only (Phase 1). useIncomingCalls(user?.id); // CallKit/ConnectionService Event-Bridge — mappt native UI-Actions // (Accept/Decline/Hangup im Lockscreen-CallKit-UI) auf useCallStore. useCallKeepEvents(); // Apple-Style Device-Approval Realtime — lauscht auf neue Approval-Requests // für diesen User und zeigt das Incoming-Sheet wenn ein anderes Gerät // sich anmelden möchte. useDeviceApprovalRealtime(!!user?.id); // Push-Tap-Deep-Link: User tippt Notification → navigate zu Chat / Call useEffect(() => { const handle = (response: Notifications.NotificationResponse | null | undefined) => { if (!response) return; const data = response.notification.request.content.data as | { type?: 'dm' | 'room' | 'call'; targetId?: string; callId?: string; from?: { id: string; nickname: string; avatar: string | null }; } | undefined; if (!data?.type) return; if (data.type === 'dm' && data.targetId) { router.push({ pathname: '/dm', params: { userId: data.targetId } }); } else if (data.type === 'room' && data.targetId) { router.push({ pathname: '/room', params: { roomId: data.targetId } }); } else if (data.type === 'call' && data.callId && data.from) { // Eingehender Anruf — Realtime hat (vermutlich) keine Subscription // gehabt weil App im Background war. Wir simulieren receiveIncoming // damit der Standard-Accept/Decline-Flow greift. Falls der Caller in // der Zwischenzeit aufgelegt hat: ring-cancel kommt sobald Channel // subscribed, dann teardown. try { useCallStore.getState().receiveIncoming(data.callId, data.from); router.push('/call'); } catch { // ignore — Call evtl. schon beendet } } }; const sub = Notifications.addNotificationResponseReceivedListener(handle); // Cold-Start: App wurde durch Notification-Tap geöffnet Notifications.getLastNotificationResponseAsync().then(handle).catch(() => {}); return () => sub.remove(); }, []); useEffect(() => { init(); initTheme(); initLanguage(); initAppLock(); initLyraVoice(); if (__DEV__) initRealtimeDebug(); }, []); // Supabase-Doku-Pattern für RN: Token-Auto-Refresh nur wenn App aktiv ist. // Plus Foreground-Reconnect via onAuthStateChange (TOKEN_REFRESHED → // realtime.setAuth wird intern getriggert). Fixt den Realtime-Disconnect-Bug // bei lange eingeloggten Usern (siehe `project_session_2026-05-15_push.md`). useEffect(() => { const sub = AppState.addEventListener('change', (state) => { if (state === 'active') { supabase.auth.startAutoRefresh(); } else { supabase.auth.stopAutoRefresh(); } }); return () => sub.remove(); }, []); useEffect(() => { if (fontsLoaded && !loading && appLockReady) { SplashScreen.hideAsync(); } }, [fontsLoaded, loading, appLockReady]); if (!fontsLoaded || loading || !appLockReady) { // Nativer expo-splash-screen bleibt sichtbar bis SplashScreen.hideAsync() // im Effect oben aufgerufen wird → kein Flicker durch zusätzlichen // React-Splash mehr (User-Feedback: "geht sehr schnell vorbei und zuckt") return null; } return ( { // Slot wurde freigegeben — register retry useDevicesStore.getState().ensureRegistered().catch(() => {}); }} /> ); } export default function RootLayout() { return ( ); }