import { useCallback, useEffect, useRef, useState } from 'react'; import { Alert, Animated, AppState, Image, Text, TouchableOpacity, View } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useRouter } from 'expo-router'; import { useAppLockStore } from '../stores/appLock'; import { useAuthStore } from '../stores/auth'; /** * Vollbild-Overlay, das den App-Inhalt verdeckt solange die App-Sperre aktiv und * `locked` ist (siehe AppLockGate). Beim Mount — und jedes Mal wenn man aus dem * Hintergrund zur noch-gesperrten App zurückkommt — wird automatisch der * Face-ID/Touch-ID/Passcode-Prompt ausgelöst; schlägt er fehl oder bricht der * User ab, bleibt der „Entsperren"-Button stehen (kein Auto-Retry-Loop — die * inactive→active-Transition direkt nach einem abgebrochenen Prompt löst NICHT * neu aus, nur background→active). * * „Abmelden" unten ist die Notausfahrt: clear't die Session → beim nächsten Start * gibt es keine Session → keine Sperre → frischer Login. Verhindert ein echtes * Aussperren falls Biometrie + Passcode versagen. */ export function LockScreen() { const { t } = useTranslation(); const router = useRouter(); const authenticate = useAppLockStore((s) => s.authenticate); const signOut = useAuthStore((s) => s.signOut); const [busy, setBusy] = useState(false); const inFlight = useRef(false); // dezenter Atem-Puls auf dem Icon (matcht den Splash-Vibe, ohne dessen ganze Choreo) const pulse = useRef(new Animated.Value(1)).current; useEffect(() => { Animated.loop( Animated.sequence([ Animated.timing(pulse, { toValue: 1.04, duration: 1300, useNativeDriver: true }), Animated.timing(pulse, { toValue: 1, duration: 1300, useNativeDriver: true }), ]), ).start(); }, [pulse]); const tryUnlock = useCallback(async () => { if (inFlight.current) return; inFlight.current = true; setBusy(true); try { await authenticate(t('applock.prompt')); } finally { inFlight.current = false; setBusy(false); } }, [authenticate, t]); // Auto-Prompt beim ersten Erscheinen — aber NUR wenn die App schon im // Foreground ist. Wird LockScreen während eines `background`/`inactive`-State // gemountet (typisch wenn der Lock durch das background-Event selbst getriggert // wurde), zeigt FaceID keinen sichtbaren Prompt und failed silent — der User // sieht dann nur den Fallback-Button. // Wenn beim Mount nicht active, fängt der background→active-Listener unten // den Foreground-Wechsel und prompted dann. useEffect(() => { if (AppState.currentState === 'active') { tryUnlock(); } }, [tryUnlock]); // Rückkehr aus dem Hintergrund zur noch gesperrten App → erneut prompten useEffect(() => { let prev = AppState.currentState; const sub = AppState.addEventListener('change', (next) => { if (prev === 'background' && next === 'active') tryUnlock(); prev = next; }); return () => sub.remove(); }, [tryUnlock]); function handleSignOut() { Alert.alert(t('applock.signOut_title'), t('applock.signOut_body'), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('auth.signOut'), style: 'destructive', onPress: async () => { await signOut(); router.replace('/'); }, }, ]); } return ( {t('applock.title')} {t('applock.subtitle')} {t('applock.unlock')} {t('auth.signOut')} ); }