import { useEffect, useRef, useState } from 'react'; import { View, ActivityIndicator, AppState, Platform } from 'react-native'; import { useRouter } from 'expo-router'; import * as Notifications from 'expo-notifications'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../stores/auth'; import { useNotificationStore } from '../../stores/notifications'; import { useColors } from '../../lib/theme'; import { NativeTabs } from '../../components/NativeTabs'; import { protection } from '../../lib/protection'; import { preloadTabIcons, getTabIcon } from '../../lib/tabIcons'; export default function AppLayout() { const router = useRouter(); const { t } = useTranslation(); const { session, loading } = useAuthStore(); const colors = useColors(); const loadNotifications = useNotificationStore((s) => s.load); const startRealtime = useNotificationStore((s) => s.startRealtime); const stopRealtime = useNotificationStore((s) => s.stopRealtime); const resetNotifications = useNotificationStore((s) => s.reset); const rearmInFlightRef = useRef(false); const bypassNotifiedRef = useRef(false); // Android-Tab-Icons müssen async aus Ionicons-Font generiert werden (kein // SF-Symbol-Support). preloadTabIcons() läuft schon beim Modul-Import — hier // nur den ready-State tracken damit wir re-rendern wenn der Cache fertig ist. const [tabIconsReady, setTabIconsReady] = useState(Platform.OS !== 'android'); useEffect(() => { if (Platform.OS === 'android' && !tabIconsReady) { preloadTabIcons().then(() => setTabIconsReady(true)); } }, [tabIconsReady]); useEffect(() => { if (!loading && !session) { router.replace('/signin'); } }, [session, loading]); useEffect(() => { if (!session) { resetNotifications(); return; } loadNotifications(); startRealtime(); return () => { stopRealtime(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [session?.user?.id]); useEffect(() => { if (!session || Platform.OS !== 'ios') return; let cancelled = false; let pollTimer: ReturnType | null = null; async function notifyBypassDetected(): Promise { const perms = await Notifications.getPermissionsAsync(); let granted = perms.granted || perms.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL; if (!granted) { const req = await Notifications.requestPermissionsAsync(); granted = req.granted || req.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL; } if (!granted) return false; await Notifications.scheduleNotificationAsync({ content: { title: 'ReBreak Schutz manipuliert', body: 'Tippe hier, um den Schutz sofort wieder zu aktivieren.', sound: 'default', data: { type: 'protection_bypass_detected' }, }, trigger: null, }); return true; } async function enforceProtection() { if (cancelled || rearmInFlightRef.current) return; try { const state = await protection.getCombinedState(); if (cancelled) return; if (state.phase !== 'recoveringFromBypass') { bypassNotifiedRef.current = false; return; } if (bypassNotifiedRef.current) return; bypassNotifiedRef.current = true; const notified = await notifyBypassDetected(); if (!notified) { // Fallback wenn Notifications nicht erlaubt sind. rearmInFlightRef.current = true; router.replace('/blocker'); await protection.activateFamilyControls().catch(() => ({ enabled: false })); } } finally { rearmInFlightRef.current = false; } } async function onBypassNotificationTap() { if (rearmInFlightRef.current) return; rearmInFlightRef.current = true; try { router.replace('/blocker'); await protection.activateFamilyControls().catch(() => ({ enabled: false })); } finally { rearmInFlightRef.current = false; } } // Initial check + foreground re-check + periodisches Polling als Fallback. enforceProtection(); const notifTapSub = Notifications.addNotificationResponseReceivedListener((response) => { const type = response.notification.request.content.data?.type; if (type === 'protection_bypass_detected') { void onBypassNotificationTap(); } }); Notifications.getLastNotificationResponseAsync().then((response) => { const type = response?.notification.request.content.data?.type; if (type === 'protection_bypass_detected') { void onBypassNotificationTap(); } }); const appStateSub = AppState.addEventListener('change', (s) => { if (s === 'active') { enforceProtection(); } }); pollTimer = setInterval(enforceProtection, 15000); return () => { cancelled = true; notifTapSub.remove(); appStateSub.remove(); if (pollTimer) clearInterval(pollTimer); }; }, [session, router]); if (loading || !session) { return ( ); } return ( Platform.OS === 'ios' ? { sfSymbol: 'house.fill' } : (getTabIcon('home') as any), }} /> Platform.OS === 'ios' ? { sfSymbol: 'bubble.left.and.bubble.right.fill' } : (getTabIcon('chat') as any), }} /> Platform.OS === 'ios' ? { sfSymbol: 'sparkles' } : (getTabIcon('coach') as any), }} /> Platform.OS === 'ios' ? { sfSymbol: 'checkmark.shield.fill' } : (getTabIcon('blocker') as any), }} /> Platform.OS === 'ios' ? { sfSymbol: 'envelope.fill' } : (getTabIcon('mail') as any), }} /> ); }