import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'expo-router'; import { useMe, invalidateMe, type OnboardingStep } from '../../hooks/useMe'; import { useLyraVoiceStore } from '../../stores/lyraVoice'; import { apiFetch } from '../../lib/api'; import { WelcomeSlide } from '../../components/onboarding/slides/WelcomeSlide'; import { PrivacySlide } from '../../components/onboarding/slides/PrivacySlide'; import { NicknameSlide } from '../../components/onboarding/slides/NicknameSlide'; import { DigaChoiceSlide } from '../../components/onboarding/slides/DigaChoiceSlide'; import { DigaCodeSlide } from '../../components/onboarding/slides/DigaCodeSlide'; import { PlanSlide } from '../../components/onboarding/slides/PlanSlide'; import { PaymentSlide } from '../../components/onboarding/slides/PaymentSlide'; import { ProtectionSlide } from '../../components/onboarding/slides/ProtectionSlide'; import { DoneSlide } from '../../components/onboarding/slides/DoneSlide'; import { OnboardingNavProvider } from '../../components/onboarding/OnboardingNavContext'; /** * Duo-Style Onboarding — single route, state-machine intern. * * Linear-Flow: * [1] welcome Lyra stellt sich vor * [2] privacy DSGVO-Versprechen * [3] nickname Inline-Input + PATCH /me + step='account' * [4] diga_choice "Hast du Rezept-Code?" Ja/Nein * │ * ├─ Ja → [4a] diga_code (Branch) * │ redeem → step='pre_protection' → SKIP to [7] * │ * └─ Nein → * [5] plan Pro/Legend selection → step='plan' * [6] payment DEV-Stub (RevenueCat-Phase pending) → step='pre_protection' * [7] protection activateUrlFilter() → step='done' * [8] done Geschafft-Screen → /(app) * * Resume: routing-gate (app/(app)/_layout.tsx) routet zu /onboarding wenn * step != 'done'. Hier mappt slideFromStep den persistierten step zur Slide. */ type Slide = | 'welcome' | 'privacy' | 'nickname' | 'diga_choice' | 'diga_code' | 'plan' | 'payment' | 'protection' | 'done'; // Linear-Order für Progress-Indicator + Default-Next-Navigation. // diga_code ist NICHT in der Linear-Liste — wird via Branch erreicht. const LINEAR_ORDER: Slide[] = [ 'welcome', 'privacy', 'nickname', 'diga_choice', 'plan', 'payment', 'protection', 'done', ]; function slideFromStep(step: OnboardingStep): Slide { switch (step) { case 'welcome': return 'welcome'; case 'account': return 'diga_choice'; case 'plan': return 'payment'; case 'pre_protection': return 'protection'; case 'done': return 'done'; case 'nickname': // legacy return 'nickname'; case 'block': // legacy return 'protection'; default: return 'welcome'; } } export default function OnboardingScreen() { const router = useRouter(); const { me } = useMe(); const initialSlide = useMemo( () => (me ? slideFromStep(me.onboardingStep) : 'welcome'), // eslint-disable-next-line react-hooks/exhaustive-deps [me?.id], ); const [slide, setSlide] = useState(initialSlide); // Lyra-Voice fürs Onboarding automatisch an — sie begleitet/spricht jede Slide // vor (User kann per Volume-Button stummschalten). Beim Verlassen den vorigen // Wert wiederherstellen, damit der App-weite Default unangetastet bleibt. const voiceReady = useLyraVoiceStore((s) => s.ready); useEffect(() => { if (!voiceReady) return; const prev = useLyraVoiceStore.getState().enabled; void useLyraVoiceStore.getState().setEnabled(true); return () => { void useLyraVoiceStore.getState().setEnabled(prev); }; }, [voiceReady]); function goToLinearNext() { const idx = LINEAR_ORDER.indexOf(slide); if (idx < 0 || idx === LINEAR_ORDER.length - 1) { router.replace('/(app)'); return; } setSlide(LINEAR_ORDER[idx + 1]); } function goToLinearPrevious() { const idx = LINEAR_ORDER.indexOf(slide); if (idx <= 0) return; setSlide(LINEAR_ORDER[idx - 1]); } // Back erlaubt auf Info-/Auswahl-Slides + protection. NICHT auf: // welcome (erste), done (final), diga_code (hat eigenen onBack). // protection: seit dem Card-Flow keine internen Phasen mehr → Back zur vorigen // Slide ist unkritisch (aktivierter Schutz bleibt via Layer-State erhalten). const BACK_ALLOWED: Slide[] = ['privacy', 'nickname', 'diga_choice', 'plan', 'payment', 'protection']; const canGoBack = BACK_ALLOWED.includes(slide); function exitToApp() { router.replace('/(app)'); } // Branch-Handler ───────────────────────────────────────────────────────────── function onDigaYes() { setSlide('diga_code'); } function onDigaNo() { setSlide('plan'); } function onDigaCodeSuccess() { // Backend hat step='pre_protection' gesetzt → skip plan + payment setSlide('protection'); } async function onPlanChosen(_tier: 'pro' | 'legend', _billing: 'monthly' | 'yearly') { // TODO: tier + billing an PaymentSlide weiterreichen sobald RevenueCat // wired ist (Phase 0). Bis dahin nur step persistieren. await apiFetch('/api/profile/me/onboarding-step', { method: 'PATCH', body: { step: 'plan' }, }).catch(() => {}); invalidateMe(); setSlide('payment'); } // Linear-Indizes für Progress (diga_code wird wie diga_choice gezählt) ────── const linearIdx = (() => { if (slide === 'diga_code') return LINEAR_ORDER.indexOf('diga_choice'); return LINEAR_ORDER.indexOf(slide); })(); const current = Math.max(1, linearIdx + 1); const total = LINEAR_ORDER.length; // Slide-Dispatch ──────────────────────────────────────────────────────────── function renderSlide() { switch (slide) { case 'welcome': return ; case 'privacy': return ; case 'nickname': return ; case 'diga_choice': return ( ); case 'diga_code': return ( setSlide('diga_choice')} current={current} total={total} /> ); case 'plan': return ; case 'payment': return ( ); case 'protection': return ( ); case 'done': return ; } } return ( {renderSlide()} ); }