import { useCallback, useEffect, useRef, useState } from 'react'; import { Animated, ScrollView, Text, View, Alert, ActivityIndicator, TouchableOpacity } from 'react-native'; import { useRouter } from 'expo-router'; import { useBottomTabBarHeight } from 'react-native-bottom-tabs'; import { useTranslation } from 'react-i18next'; import { Ionicons } from '@expo/vector-icons'; import { AppHeader } from '../../components/AppHeader'; import { LayerSwitchCard } from '../../components/blocker/LayerSwitchCard'; import { ProtectionLockedCard } from '../../components/blocker/ProtectionLockedCard'; import { CooldownBanner } from '../../components/blocker/CooldownBanner'; import { DomainGrid } from '../../components/blocker/DomainGrid'; import { AddDomainSheet } from '../../components/blocker/AddDomainSheet'; import { ProtectionDetailsSheet } from '../../components/blocker/ProtectionDetailsSheet'; import { DeactivationExplainerSheet } from '../../components/blocker/DeactivationExplainerSheet'; import { useProtectionState } from '../../hooks/useProtectionState'; import { useCustomDomains } from '../../hooks/useCustomDomains'; import { useBlocklistSync } from '../../hooks/useBlocklistSync'; import { useDomainSubmissionRealtime } from '../../hooks/useDomainSubmissionRealtime'; import { protection, FAMILY_CONTROLS_AVAILABLE } from '../../lib/protection'; import { useColors } from '../../lib/theme'; export default function BlockerScreen() { const router = useRouter(); const { t } = useTranslation(); const colors = useColors(); // react-native-bottom-tabs Tab-Bar ist iOS-nativ + translucent → unsere Content-View // erstreckt sich UNTER den Tab-Bar. Ohne diese Höhe würden FAB + Bottom-Padding // hinterm Tab-Bar verschwinden. const tabBarHeight = useBottomTabBarHeight(); const { state, loading, cooldownRemainingFormatted, refresh, activateUrlFilter, activateFamilyControls, requestDeactivation, cancelDeactivation, } = useProtectionState(); const plan = state?.plan ?? 'free'; const { domains, tier, countsByType, limits, addDomain, submitDomain, refresh: refreshDomains, } = useCustomDomains(plan); const { sync: syncBlocklist } = useBlocklistSync(); // Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen. const onDomainChange = useCallback(async () => { await refreshDomains(); if (urlFilterActiveRef.current) { const sync = await syncBlocklist(); console.log('[blocker] resync after domain change:', sync); await refresh(); } }, [refreshDomains, syncBlocklist, refresh]); useDomainSubmissionRealtime(onDomainChange, true); const [mailOpen, setMailOpen] = useState(false); // AddSheet state: tracks which section opened it const [addSheetOpen, setAddSheetOpen] = useState(false); const [addSheetKind, setAddSheetKind] = useState<'web' | 'mail'>('web'); const [detailsOpen, setDetailsOpen] = useState(false); const [explainerOpen, setExplainerOpen] = useState(false); const urlFilterActive = state?.layers.urlFilter === true; const familyControlsActive = state?.layers.familyControls === true; const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; const lockedIn = appDeletionLockActive; const urlFilterActiveRef = useRef(urlFilterActive); useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]); // Auto-Sync wenn URL-Filter beim Page-Mount/-Resume schon aktiv ist. const syncedOnceRef = useRef(false); useEffect(() => { if (!urlFilterActive) return; if (syncedOnceRef.current) return; syncedOnceRef.current = true; syncBlocklist().then((res) => { console.log('[blocker] auto-sync on mount:', res); if (res.ok) refresh(); }); }, [urlFilterActive, syncBlocklist, refresh]); // ─── Activate-Handler pro Layer ────────────────────────────────────── async function handleActivateUrlFilter() { try { const result = await activateUrlFilter(); console.log('[blocker] activateUrlFilter:', result); if (!result.enabled) { Alert.alert( t('blocker.activate_url_failed_title'), result.error ?? t('blocker.activate_url_failed_msg'), [ { text: t('common.ok') }, { text: t('blocker.activate_settings_btn'), onPress: () => protection.openSystemSettings() }, ], ); } else { const sync = await syncBlocklist(); console.log('[blocker] post-activate sync:', sync); if (sync.ok) { await refresh(); } else { Alert.alert( t('blocker.sync_list_failed_title'), sync.error ?? t('blocker.sync_list_failed_msg'), ); } } return result; } catch (e: any) { console.error('[blocker] activateUrlFilter threw:', e); Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error')); return { enabled: false }; } } async function handleActivateFamilyControls() { try { const result = await activateFamilyControls(); console.log('[blocker] activateFamilyControls:', result); // `accessibility_pending` = a11y-Berechtigung fehlt noch → System-Settings wurden // geöffnet. Kein Fehler-Modal (sonst Modal-Loop bei jedem Tap). if (!result.enabled && result.error !== 'accessibility_pending') { Alert.alert( t('blocker.activate_app_lock_failed_title'), result.error ?? t('blocker.activate_app_lock_failed_msg'), ); } return result; } catch (e: any) { console.error('[blocker] activateFamilyControls threw:', e); Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error')); } return { enabled: false }; } // ─── 3-Click Cooldown-Trigger ──────────────────────────────────────── function openDetails() { setDetailsOpen(true); } function fromDetailsToExplainer() { setDetailsOpen(false); setTimeout(() => setExplainerOpen(true), 250); } function deflectToLyra() { setDetailsOpen(false); setTimeout(() => router.push('/lyra' as any), 250); } function deflectToBreathe() { setExplainerOpen(false); setTimeout(() => router.push('/urge' as any), 250); } async function handleStartCooldown(reason: string) { await requestDeactivation(reason); } async function handleCancelCooldown() { try { await cancelDeactivation(); } catch (e: any) { Alert.alert(t('common.error'), e?.message ?? t('blocker.deactivation_cancel_failed')); } } const bypassAlertShownRef = useRef(false); useEffect(() => { if (state?.phase !== 'recoveringFromBypass') { bypassAlertShownRef.current = false; return; } if (bypassAlertShownRef.current) return; bypassAlertShownRef.current = true; Alert.alert( t('blocker.protection_off_title'), t('blocker.protection_off_message'), [ { text: t('common.ok'), style: 'cancel' }, { text: t('blocker.reactivate_btn'), onPress: () => { void handleActivateUrlFilter(); } }, ], ); }, [state?.phase, t]); function openAddSheet(kind: 'web' | 'mail') { setAddSheetKind(kind); setAddSheetOpen(true); } // ─── Render ────────────────────────────────────────────────────────── return ( {loading && !state ? ( ) : state ? ( <> {/* Locked-In Mode (FC aktiv) → NUR Schutz-Status + Cooldown-Pfad */} {lockedIn ? ( ) : ( {FAMILY_CONTROLS_AVAILABLE ? ( ) : ( {t('blocker.layers_app_lock_title')} {t('blocker.app_lock_coming_soon_badge')} {t('blocker.app_lock_coming_soon_desc')} )} )} {/* CooldownBanner */} {state.cooldown.active && ( )} {/* Free: Erwartungs-Transparenz-Hinweis */} {plan === 'free' && ( {t('plan_limit.blocker_basic_protection')} )} {/* Über-Limit Banner */} {tier.atLimit && tier.usedSlots > tier.domainLimit && ( {t('plan_limit.blocker_domain_over_limit', { used: tier.usedSlots, plan: plan.charAt(0).toUpperCase() + plan.slice(1), max: tier.domainLimit, })} )} {/* Section 1: Eigene Domains */} openAddSheet('web')} atLimit={countsByType.web >= limits.web} > Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))} /> {/* Section 2: Eigene Mails */} setMailOpen((v) => !v)} onAdd={() => openAddSheet('mail')} atLimit={countsByType.mail >= limits.mail} > Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))} /> {/* Sheets */} { setAddSheetOpen(false); refreshDomains(); }} onAdd={async (pattern, kind) => { const result = await addDomain(pattern, kind); if (result.ok) { const sync = await syncBlocklist(); if (sync.ok) refresh(); } return result; }} /> setDetailsOpen(false)} onRequestDeactivation={fromDetailsToExplainer} onTalkToLyra={deflectToLyra} /> setExplainerOpen(false)} onBreathe={deflectToBreathe} onStartCooldown={handleStartCooldown} /> ) : null} ); } // ─── DomainSection ──────────────────────────────────────────────────────────── function DomainSection({ title, count, max, collapsible = false, open = true, onToggle, onAdd, atLimit, children, }: { title: string; count: number; max: number; collapsible?: boolean; open?: boolean; onToggle?: () => void; onAdd: () => void; atLimit: boolean; children: React.ReactNode; }) { const { t } = useTranslation(); const colors = useColors(); // Animated progress bar const fillAnim = useRef(new Animated.Value(0)).current; const ratio = max > 0 ? Math.min(count / max, 1) : 0; useEffect(() => { Animated.timing(fillAnim, { toValue: ratio, duration: 380, useNativeDriver: false, }).start(); }, [ratio]); const pct = ratio * 100; const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a'; const badgeBg = atLimit ? '#fee2e2' : colors.surfaceElevated; const badgeFg = atLimit ? '#dc2626' : colors.textMuted; return ( {/* Section Header */} {title} {t('blocker.count_label', { count, max })} {collapsible && ( )} {(!collapsible || open) && ( {/* Progressbar */} {/* Add-Button */} {t('blocker.add_domain')} {/* Grid */} {children} )} ); }