import { useEffect, useRef, useState } from 'react'; import { Alert, AppState, Image, Platform, Text, useWindowDimensions, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../../lib/theme'; import { apiFetch } from '../../../lib/api'; import { invalidateMe } from '../../../hooks/useMe'; import { protection } from '../../../lib/protection'; import RebreakProtection from '../../../modules/rebreak-protection'; import { getPermissionScreenshot } from '../../../lib/onboardingAssets'; import { OnboardingShell } from '../OnboardingShell'; import { LyraBubble } from '../LyraBubble'; import { CTABar } from '../CTABar'; import { ScreenshotPointer } from '../ScreenshotPointer'; import { PermissionDeniedSheet } from '../../PermissionDeniedSheet'; import i18n from '../../../lib/i18n'; /** * Onboarding-Schutz-Step. * * Platform.OS-Dispatch: * iOS → IosProtectionSlide (NEFilter + Family-Controls) * Android → AndroidProtectionSlide (VpnService + Accessibility-Tamper-Lock) * * Beide haben den gleichen Eltern-Vertrag (current/total/onDone) und nutzen * den gleichen Pre-Explainer + Lyra-Bubble + CTA-Pattern — die Innereien * unterscheiden sich nur in (a) welche Permission-Dialoge geöffnet werden * und (b) welche Screenshots gezeigt werden. */ export function ProtectionSlide(props: { onDone: () => void; current: number; total: number; }) { if (Platform.OS === 'android') { return ; } return ; } // ─── iOS ──────────────────────────────────────────────────────────────────── type IosPhase = 'preexplain_url' | 'preexplain_lock' | 'done'; function IosProtectionSlide({ onDone, current, total, }: { onDone: () => void; current: number; total: number; }) { const { t } = useTranslation(); const [phase, setPhase] = useState('preexplain_url'); const [activating, setActivating] = useState(false); const [permissionDeniedOpen, setPermissionDeniedOpen] = useState(false); async function activateUrlFilter() { if (activating) return; setActivating(true); try { const res = await protection.activateUrlFilter(); if (!res.enabled) { const isCodeFive = typeof res.error === 'string' && /NEFilterErrorDomain:\s*5/i.test(res.error); if (isCodeFive) { setPermissionDeniedOpen(true); return; } Alert.alert( t('onboarding.protection.error_title'), res.error ?? t('onboarding.protection.error_unknown'), ); return; } setPhase('preexplain_lock'); } finally { setActivating(false); } } async function activateAppLock() { if (activating) return; setActivating(true); try { const res = await protection.activateFamilyControls(); if (!res.enabled) { Alert.alert( t('onboarding.protection.applock_failed_title'), res.error ?? t('onboarding.protection.applock_failed_msg'), [ { text: t('onboarding.protection.applock_skip'), style: 'cancel', onPress: () => finishProtectionStep(), }, { text: t('common.retry'), onPress: activateAppLock }, ], ); return; } finishProtectionStep(); } finally { setActivating(false); } } async function finishProtectionStep() { await apiFetch('/api/profile/me/onboarding-step', { method: 'PATCH', body: { step: 'done' }, }).catch(() => {}); invalidateMe(); setPhase('done'); onDone(); } if (phase === 'preexplain_url') { return ( setPermissionDeniedOpen(false)} onRetry={async () => { const res = await protection.resetUrlFilter(); if (res.enabled) setPhase('preexplain_lock'); return res; }} /> ); } if (phase === 'preexplain_lock') { return ( ); } return null; } // ─── Android ──────────────────────────────────────────────────────────────── type AndroidPhase = | 'preexplain_vpn' | 'preexplain_a11y' | 'a11y_pending' | 'done'; function AndroidProtectionSlide({ onDone, current, total, }: { onDone: () => void; current: number; total: number; }) { const { t } = useTranslation(); const [phase, setPhase] = useState('preexplain_vpn'); const [activating, setActivating] = useState(false); // True wenn wir auf Settings-Rückkehr warten. AppState-Listener pollt dann // a11y-State + advanced automatisch wenn ReBreak-Schalter live ist. const awaitingReturnRef = useRef(false); const appStateRef = useRef(AppState.currentState); async function finishProtectionStep() { await apiFetch('/api/profile/me/onboarding-step', { method: 'PATCH', body: { step: 'done' }, }).catch(() => {}); invalidateMe(); setPhase('done'); onDone(); } async function activateVpn() { if (activating) return; setActivating(true); try { const res = await protection.activateUrlFilter(); if (!res.enabled) { Alert.alert( t('onboarding.protection.error_title'), res.error ?? t('onboarding.protection.error_unknown'), ); return; } setPhase('preexplain_a11y'); } finally { setActivating(false); } } async function activateA11y() { if (activating) return; setActivating(true); try { const res = await protection.activateFamilyControls(); if (res.enabled) { // Selten: User hatte a11y schon manuell aktiviert → Lock direkt armed. finishProtectionStep(); return; } if (res.error === 'accessibility_pending') { // Native hat Settings geöffnet; warte auf Rückkehr + poll. awaitingReturnRef.current = true; setPhase('a11y_pending'); return; } Alert.alert( t('onboarding.protection.error_title'), res.error ?? t('onboarding.protection.error_unknown'), ); } finally { setActivating(false); } } // Auto-Check beim Foreground-Return: wenn a11y jetzt aktiv → Lock armen + done. useEffect(() => { const sub = AppState.addEventListener('change', async (next) => { const prev = appStateRef.current; appStateRef.current = next; if (!awaitingReturnRef.current) return; if (prev.match(/inactive|background/) && next === 'active') { try { const a11y = await RebreakProtection.isAccessibilityEnabled(); if (a11y.enabled) { // ReBreak-Service ist live → Tamper-Lock armen + finish. const res = await protection.activateFamilyControls(); if (res.enabled) { awaitingReturnRef.current = false; finishProtectionStep(); } } } catch { // Ignorieren — User kann manuell auf "Ich habe ReBreak aktiviert" tippen. } } }); return () => sub.remove(); }, []); if (phase === 'preexplain_vpn') { return ( ); } if (phase === 'preexplain_a11y') { return ( ); } if (phase === 'a11y_pending') { return ( ); } return null; } function A11yPendingView({ current, total, activating, onRetry, }: { current: number; total: number; activating: boolean; onRetry: () => void; }) { const { t } = useTranslation(); const colors = useColors(); return ( } > {t('onboarding.protection.android_a11y_pending_title')} ); } // ─── PreExplainer (shared) ─────────────────────────────────────────────────── function PreExplainer({ dialog, lyraBodyKey, titleKey, ctaKey, buttonLabelKey, markerHintKey, activating, onActivate, current, total, children, }: { dialog: 'url_filter' | 'screen_time' | 'android_vpn' | 'android_a11y'; lyraBodyKey: string; titleKey: string; ctaKey: string; buttonLabelKey: string; markerHintKey: string; activating: boolean; onActivate: () => void; current: number; total: number; children?: React.ReactNode; }) { const { t } = useTranslation(); const colors = useColors(); const { height: screenH } = useWindowDimensions(); const lang = i18n.language || 'de'; const screenshot = getPermissionScreenshot(dialog, lang); // Dynamische Screenshot-Höhe: Auf kleinen Phones (SE/mini ~667-844 pt) // capped damit alles + CTA-Bar ohne Scroll passt. Auf großen Phones/iPad // skaliert es mit. Min 200, Max 320. const screenshotHeight = Math.min(320, Math.max(200, screenH * 0.32)); return ( } > {t(titleKey)} {t(markerHintKey)} {children} ); }