import { useEffect, useState } from "react"; import { Pressable, Text, View } from "react-native"; import { router } from "expo-router"; import { api } from "@/lib/api"; import { useAuth } from "@/lib/auth"; import { useAnalytics } from "@/lib/analytics"; import { t } from "@/lib/i18n"; import { onboardingStarted, onboardingStepCompleted, onboardingSkipped } from "@app/analytics"; import { Body, Button, Card, Heading, Input, Row, Screen, Small, Spacer, Title, } from "@/components/ui"; import { colors, spacing, typography } from "@/lib/theme"; /** * Progressive onboarding (FAS 1b): 3-layer flow. * Step A = immediate value (goal + precision mode) – shown right after registration. * Step B = after first value (diet, allergens, household) – shown after first scan/cook. * Step C = contextual (health profile) – shown when user visits nutrition features. * * All steps are skippable (GDPR-friendly). Existing /v1/me/onboarding preserved * for backward compatibility. */ const GOALS = [ "lose_weight", "build_muscle", "maintain_weight", "more_protein", "less_waste", "lower_cost", "cook_more", ] as const; const DIETS = ["omnivore", "flexitarian", "pescatarian", "vegetarian", "vegan"] as const; const RELIGIOUS = ["none", "halal", "kosher", "hindu_no_beef", "buddhist_vegetarian"] as const; const ALLERGENS = [ "gluten", "milk", "eggs", "tree_nuts", "peanuts", "fish", "crustaceans", "soy", "sesame", ] as const; /** Which layer are we showing? */ type OnboardingLayer = "a" | "b" | "c"; export default function OnboardingScreen() { const setOnboardingCompleted = useAuth((s) => s.setOnboardingCompleted); const setOnboardingStep = useAuth((s) => s.setOnboardingStep); const savedStep = useAuth((s) => s.onboardingStep); const { track } = useAnalytics(); const [layer, setLayer] = useState(savedStep ?? "a"); useEffect(() => { track(onboardingStarted({ properties: { step: layer } })); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Step A state const [goals, setGoals] = useState([]); const [mode, setMode] = useState<"simple" | "exact">("simple"); const primaryGoal = goals[0] ?? null; // Step B state const [diet, setDiet] = useState("omnivore"); const [religiousRule, setReligiousRule] = useState("none"); const [allergens, setAllergens] = useState([]); const [householdKind, setHouseholdKind] = useState<"create" | "join" | "skip">("create"); const [householdName, setHouseholdName] = useState(t("onboarding.householdDefaultName")); const [inviteCode, setInviteCode] = useState(""); // Step C state const [weightKg, setWeightKg] = useState(""); const [heightCm, setHeightCm] = useState(""); const [birthYear, setBirthYear] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const toggleAllergen = (id: string) => setAllergens((prev) => (prev.includes(id) ? prev.filter((a) => a !== id) : [...prev, id])); const toggleGoal = (id: string) => setGoals((prev) => { if (prev.includes(id)) return prev.filter((g) => g !== id); // Keep the first-selected goal as primary by appending at the end. return [...prev, id]; }); const finishStepA = async () => { setBusy(true); setError(null); try { const res = await api<{ step: "a" | "b" | "c" }>("/v1/onboarding/quick-start", { method: "POST", body: { ...(goals.length ? { goals } : {}), precisionMode: mode, }, }); track( onboardingStepCompleted({ properties: { step: "a", goals, primaryGoal, precisionMode: mode }, }), ); setOnboardingStep(res.step); setLayer("b"); // Let user into the app – Step B will be shown contextually later router.replace("/(tabs)"); } catch (err) { setError(err instanceof Error ? err.message : t("common.error")); } finally { setBusy(false); } }; const finishStepB = async () => { setBusy(true); setError(null); try { const res = await api<{ step: "a" | "b" | "c" }>("/v1/onboarding/complete-b", { method: "POST", body: { precisionMode: mode, preferences: { dietPattern: diet, allergens, religiousRule, }, householdChoice: householdKind === "create" ? { kind: "create", name: householdName || t("onboarding.householdDefaultName") } : householdKind === "join" ? { kind: "join", inviteCode } : { kind: "skip" }, }, }); track(onboardingStepCompleted({ properties: { step: "b", diet, allergens, householdKind } })); setOnboardingStep(res.step); setLayer("c"); } catch (err) { setError(err instanceof Error ? err.message : t("common.error")); } finally { setBusy(false); } }; const finishStepC = async () => { setBusy(true); setError(null); try { track(onboardingStepCompleted({ properties: { step: "c" } })); await api("/v1/onboarding/complete-c", { method: "POST", body: { precisionMode: mode, healthProfile: { ...(weightKg ? { weightKg: Number(weightKg) } : {}), ...(heightCm ? { heightCm: Number(heightCm) } : {}), ...(birthYear ? { birthYear: Number(birthYear) } : {}), }, }, }); setOnboardingCompleted(true); setOnboardingStep("c"); router.replace("/(tabs)"); } catch (err) { setError(err instanceof Error ? err.message : t("common.error")); } finally { setBusy(false); } }; const skip = async (targetLayer?: OnboardingLayer) => { setBusy(true); try { track(onboardingSkipped({ properties: { step: targetLayer ?? layer } })); await api("/v1/onboarding/skip", { method: "POST", body: { step: targetLayer }, }); setOnboardingCompleted(true); setOnboardingStep("c"); router.replace("/(tabs)"); } catch (err) { setError(err instanceof Error ? err.message : t("common.error")); } finally { setBusy(false); } }; // Render Step A: Immediate value if (layer === "a") { return ( {t("onboarding.title")} {t("onboarding.subtitle")} {t("onboarding.goal")} {GOALS.map((id) => { const index = goals.indexOf(id); const selected = index >= 0; return ( toggleGoal(id)} > {selected && index === 0 && ( {t("onboarding.primaryGoal")} )} ); })} {t("onboarding.mode")} setMode("simple")} style={mode === "simple" ? sel : undefined}> {t("onboarding.modeSimple")} {t("onboarding.modeSimpleDesc")} setMode("exact")} style={mode === "exact" ? sel : undefined}> {t("onboarding.modeExact")} {t("onboarding.modeExactDesc")} {t("onboarding.modesCombine")} {t("onboarding.memoryTransparency")} {error && {error}}