import { useCallback, useEffect, useRef, useState } from 'react'; import { AppState, type AppStateStatus } from 'react-native'; 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 [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 fetchState = useCallback(async (showLoading = false) => { if (showLoading) setLoading(true); try { const next = await protection.getCombinedState(); setState(next); setTickSeconds(next.cooldown.remainingSeconds); setError(null); } catch (e: any) { setError(e?.message ?? 'unknown'); } finally { if (showLoading) setLoading(false); } }, []); // 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 wenn App aus Background zurückkommt. // KEIN auto-disable hier — Backend's canDisableProtection-Flag ist auch // in initial-state true, würde sonst den Filter killen ohne dass User // jemals einen Cooldown gestartet hat. Auto-Disable nur über expliziten // UI-Pfad nach Cooldown-Ablauf (kommt in Step 5b). useEffect(() => { const sub = AppState.addEventListener('change', (status: AppStateStatus) => { if (status === 'active') { fetchState(false); } }); return () => sub.remove(); }, [fetchState]); // 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 };