import { useCallback, useEffect, useRef, useState } from 'react'; import { Alert, AppState, type AppStateStatus } from 'react-native'; import { useTranslation } from 'react-i18next'; import { protection, type ProtectionState, type ProtectionPhase, formatCooldownRemaining, } from '../lib/protection'; const POLL_MS_ACTIVE_COOLDOWN = 5_000; const POLL_MS_NORMAL = 30_000; type UseProtectionStateReturn = { state: ProtectionState | null; loading: boolean; error: string | null; /** Live Countdown-String "23:59:42" während Cooldown läuft. */ cooldownRemainingFormatted: string; /** Refetch ohne loading-flicker. */ refresh: () => Promise; /** Aktiviert ALLE Layers (legacy, beide Dialoge nacheinander). */ activate: () => Promise<{ allLayersOn: boolean; missingLayers: string[] }>; /** Aktiviert NUR den URL-Filter (NEFilter). */ activateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>; /** Aktiviert NUR Family Controls (= der Lock — danach nur per Cooldown abschaltbar). */ activateFamilyControls: () => Promise<{ enabled: boolean; error?: string }>; /** Startet 24h Cooldown via Backend. UI muss Friction-Flow vorher durchlaufen. */ requestDeactivation: (reason?: string) => Promise; /** Bricht laufenden Cooldown ab. Schutz bleibt aktiv. */ cancelDeactivation: () => Promise; }; /** * Single-Source-of-Truth-Hook für Protection-State. * * - Initial-Fetch on mount * - Polling: alle 30s normal, 5s während aktivem Cooldown (Live-Countdown) * - Refresh on AppState 'active' (User kommt aus Background zurück) * - Layer-Change-Listener vom Native-Modul (Bypass-Detection) */ export function useProtectionState(): UseProtectionStateReturn { const { t } = useTranslation(); const [state, setState] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tickSeconds, setTickSeconds] = useState(0); const pollTimer = useRef | null>(null); const tickTimer = useRef | null>(null); const prevCooldownActiveRef = useRef(null); // Verhindert Mehrfach-Alert wenn fetchState + AppState-Listener beide kurz // hintereinander applyCooldownDisableIfElapsed → true sehen. const cooldownDisabledNoticeShownRef = useRef(false); // Freundlicher Hinweis nachdem der Cooldown abgelaufen ist und der Schutz // (inkl. Tamper-Lock) abgeschaltet wurde. Android: a11y-Service kann sich // nicht selbst deaktivieren → User zu den Einstellungen leiten. const showCooldownElapsedNotice = useCallback(() => { if (cooldownDisabledNoticeShownRef.current) return; cooldownDisabledNoticeShownRef.current = true; Alert.alert( t('blocker.cooldown_elapsed_title'), t('blocker.cooldown_elapsed_message'), [ { text: t('common.ok'), style: 'cancel' }, { text: t('blocker.cooldown_elapsed_open_settings'), onPress: () => { protection.openSystemSettings('accessibility').catch(() => {}); }, }, ], ); }, [t]); const fetchState = useCallback(async (showLoading = false) => { if (showLoading) setLoading(true); try { const next = await protection.getCombinedState(); const prevActive = prevCooldownActiveRef.current; prevCooldownActiveRef.current = next.cooldown.active; // Cooldown ist gerade von active → inactive gekippt: Auto-Disable prüfen. if (prevActive === true && !next.cooldown.active) { const didDisable = await protection.applyCooldownDisableIfElapsed(); if (didDisable) { showCooldownElapsedNotice(); // Nativer State hat sich geändert → ein weiterer Fetch für konsistenten State. const afterDisable = await protection.getCombinedState(); setState(afterDisable); setTickSeconds(afterDisable.cooldown.remainingSeconds); setError(null); return; } } setState(next); setTickSeconds(next.cooldown.remainingSeconds); setError(null); } catch (e: any) { setError(e?.message ?? 'unknown'); } finally { if (showLoading) setLoading(false); } }, [showCooldownElapsedNotice]); // Initial fetch useEffect(() => { fetchState(true); }, [fetchState]); // Adaptive poll-rate: 5s während Cooldown, 30s sonst useEffect(() => { const interval = state?.cooldown.active ? POLL_MS_ACTIVE_COOLDOWN : POLL_MS_NORMAL; if (pollTimer.current) clearInterval(pollTimer.current); pollTimer.current = setInterval(() => fetchState(false), interval); return () => { if (pollTimer.current) clearInterval(pollTimer.current); }; }, [state?.cooldown.active, fetchState]); // Live-Countdown-Tick (nur während Cooldown — 1s-Decrement client-side) useEffect(() => { if (!state?.cooldown.active) { if (tickTimer.current) { clearInterval(tickTimer.current); tickTimer.current = null; } return; } tickTimer.current = setInterval(() => { setTickSeconds((s) => Math.max(0, s - 1)); }, 1000); return () => { if (tickTimer.current) clearInterval(tickTimer.current); }; }, [state?.cooldown.active]); // AppState-Listener: Refresh + Auto-Disable wenn Cooldown elapsed ist. // Guard in applyCooldownDisableIfElapsed: cooldownEndsAt muss gesetzt sein // (= es lief je ein Cooldown) und remainingSeconds <= 0. Verhindert // False-Positives wenn canDisableProtection im Initial-State true ist. useEffect(() => { const sub = AppState.addEventListener('change', async (status: AppStateStatus) => { if (status !== 'active') return; const didDisable = await protection.applyCooldownDisableIfElapsed(); if (didDisable) showCooldownElapsedNotice(); await fetchState(false); }); return () => sub.remove(); }, [fetchState, showCooldownElapsedNotice]); // Native Layer-Change-Listener (User schaltet VPN extern aus etc.) useEffect(() => { const sub = protection.addLayerChangeListener(() => fetchState(false)); return () => sub?.remove(); }, [fetchState]); // ─── Public Actions ──────────────────────────────────────────────── const activate = useCallback(async () => { const result = await protection.activate(); await fetchState(false); return result; }, [fetchState]); const activateUrlFilter = useCallback(async () => { const result = await protection.activateUrlFilter(); await fetchState(false); return result; }, [fetchState]); const activateFamilyControls = useCallback(async () => { const result = await protection.activateFamilyControls(); await fetchState(false); return result; }, [fetchState]); const requestDeactivation = useCallback( async (reason?: string) => { await protection.requestDeactivation(reason); await fetchState(false); }, [fetchState], ); const cancelDeactivation = useCallback(async () => { await protection.cancelDeactivation(); await fetchState(false); }, [fetchState]); return { state, loading, error, cooldownRemainingFormatted: formatCooldownRemaining(tickSeconds), refresh: () => fetchState(false), activate, activateUrlFilter, activateFamilyControls, requestDeactivation, cancelDeactivation, }; } export type { ProtectionPhase };