import { useState } from 'react'; import { Alert, 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 { 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 Step Protection — 2 Phasen, beide mit Pre-Explainer-Modal das * den iOS-Permission-Dialog vorzeigt + Pulse-Marker auf den "Erlauben"-Button. * * ┌──────────────────────────────────────────────────────────────┐ * │ Phase A: preexplain_url │ * │ Lyra: "Gleich kommt iOS-Dialog. Tippe ERLAUBEN." │ * │ Screenshot vom NEFilter-Dialog + roter Pulse auf "Erlauben" │ * │ CTA "Aktivieren" → triggers protection.activateUrlFilter() │ * │ │ * │ Phase B: preexplain_lock │ * │ Lyra: "Jetzt App-Schutz. Tippe FORTFAHREN." │ * │ Screenshot vom Screen-Time-Dialog + roter Pulse │ * │ CTA "Aktivieren" → triggers protection.activateFamilyControls │ * │ │ * │ Phase C: done → onDone() │ * └──────────────────────────────────────────────────────────────┘ * * Wenn URL-Filter fehlschlägt mit code 5 → PermissionDeniedSheet öffnet sich * (Retry-Pfad via resetUrlFilter()). Family-Controls hat keinen analogen * Recovery-Sheet — User muss in Settings → Bildschirmzeit den App-Zugriff * gewähren. */ type Phase = 'preexplain_url' | 'preexplain_lock' | 'done'; export function ProtectionSlide({ onDone, current, total, }: { onDone: () => void; current: number; total: number; }) { const { t } = useTranslation(); const colors = useColors(); 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 = Platform.OS === 'ios' && 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; } // Filter live → weiter zur Phase B (App-Lock) setPhase('preexplain_lock'); } finally { setActivating(false); } } async function activateAppLock() { if (activating) return; setActivating(true); try { const res = await protection.activateFamilyControls(); if (!res.enabled) { // Family Controls fehlgeschlagen → User Info aber Tour-Done (URL-Filter // läuft schon, das ist der Hauptschutz; App-Lock ist optional) 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(); } return phase === 'preexplain_url' ? ( setPermissionDeniedOpen(false)} onRetry={async () => { const res = await protection.resetUrlFilter(); if (res.enabled) setPhase('preexplain_lock'); return res; }} /> ) : phase === 'preexplain_lock' ? ( ) : null; } // ─── PreExplainer (shared) ─────────────────────────────────────────────────── function PreExplainer({ dialog, lyraBodyKey, titleKey, ctaKey, activating, onActivate, current, total, children, }: { dialog: 'url_filter' | 'screen_time'; lyraBodyKey: string; titleKey: string; ctaKey: 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); const buttonLabelKey = dialog === 'url_filter' ? 'onboarding.protection.dialog_button_allow' : 'onboarding.protection.dialog_button_continue'; // 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)} {/* Screenshot — sauber ohne Overlay. Dynamisch dimensioniert. */} {/* Animierter Pointer UNTER dem Screenshot — Dimensions-agnostic. */} {t('onboarding.protection.tap_marker_hint')} {children} ); }