/** * TabataFit Onboarding — 6-Screen Conversion Funnel * Problem → Empathy → Solution → Wow Moment → Personalization → Paywall */ import { useState, useRef, useEffect, useCallback } from 'react' import { View, StyleSheet, Pressable, Animated, Dimensions, ScrollView, TextInput, } from 'react-native' import { useRouter } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import Ionicons from '@expo/vector-icons/Ionicons' import { useTranslation } from 'react-i18next' import { useHaptics } from '@/src/shared/hooks' import { useUserStore } from '@/src/shared/stores' import { OnboardingStep } from '@/src/shared/components/OnboardingStep' import { StyledText } from '@/src/shared/components/StyledText' import { BRAND, DARK, TEXT, PHASE, GLASS, BORDER } from '@/src/shared/constants/colors' import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' import { DURATION, EASE, SPRING } from '@/src/shared/constants/animations' import type { FitnessLevel, FitnessGoal, WeeklyFrequency } from '@/src/shared/types' const { width: SCREEN_WIDTH } = Dimensions.get('window') const TOTAL_STEPS = 6 // ═══════════════════════════════════════════════════════════════════════════ // SCREEN 1 — THE PROBLEM // ═══════════════════════════════════════════════════════════════════════════ function ProblemScreen({ onNext }: { onNext: () => void }) { const { t } = useTranslation('screens') const haptics = useHaptics() const clockScale = useRef(new Animated.Value(0.8)).current const clockOpacity = useRef(new Animated.Value(0)).current const textOpacity = useRef(new Animated.Value(0)).current useEffect(() => { // Clock animation Animated.parallel([ Animated.spring(clockScale, { toValue: 1, ...SPRING.BOUNCY, useNativeDriver: true, }), Animated.timing(clockOpacity, { toValue: 1, duration: DURATION.SLOW, easing: EASE.EASE_OUT, useNativeDriver: true, }), ]).start() // Text fade in after clock setTimeout(() => { Animated.timing(textOpacity, { toValue: 1, duration: DURATION.SLOW, easing: EASE.EASE_OUT, useNativeDriver: true, }).start() }, 400) }, []) return ( {t('onboarding.problem.title')} {t('onboarding.problem.subtitle1')} {t('onboarding.problem.subtitle2')} { haptics.buttonTap() onNext() }} > {t('onboarding.problem.cta')} ) } // ═══════════════════════════════════════════════════════════════════════════ // SCREEN 2 — EMPATHY // ═══════════════════════════════════════════════════════════════════════════ const BARRIERS = [ { id: 'no-time', labelKey: 'onboarding.empathy.noTime' as const, icon: 'time-outline' as const }, { id: 'low-motivation', labelKey: 'onboarding.empathy.lowMotivation' as const, icon: 'battery-dead-outline' as const }, { id: 'no-knowledge', labelKey: 'onboarding.empathy.noKnowledge' as const, icon: 'help-circle-outline' as const }, { id: 'no-gym', labelKey: 'onboarding.empathy.noGym' as const, icon: 'home-outline' as const }, ] function EmpathyScreen({ onNext, barriers, setBarriers, }: { onNext: () => void barriers: string[] setBarriers: (b: string[]) => void }) { const { t } = useTranslation('screens') const haptics = useHaptics() const toggleBarrier = (id: string) => { haptics.selection() if (barriers.includes(id)) { setBarriers(barriers.filter((b) => b !== id)) } else if (barriers.length < 2) { setBarriers([...barriers, id]) } } return ( {t('onboarding.empathy.title')} {t('onboarding.empathy.chooseUpTo')} {BARRIERS.map((item) => { const selected = barriers.includes(item.id) return ( toggleBarrier(item.id)} > {t(item.labelKey)} ) })} { if (barriers.length > 0) { haptics.buttonTap() onNext() } }} > 0 ? TEXT.PRIMARY : TEXT.DISABLED} > {t('common:continue')} ) } // ═══════════════════════════════════════════════════════════════════════════ // SCREEN 3 — THE SOLUTION (Scientific Proof) // ═══════════════════════════════════════════════════════════════════════════ function SolutionScreen({ onNext }: { onNext: () => void }) { const { t } = useTranslation('screens') const haptics = useHaptics() const tabataHeight = useRef(new Animated.Value(0)).current const cardioHeight = useRef(new Animated.Value(0)).current const citationOpacity = useRef(new Animated.Value(0)).current useEffect(() => { // Animate bars Animated.sequence([ Animated.delay(300), Animated.parallel([ Animated.spring(tabataHeight, { toValue: 1, ...SPRING.GENTLE, useNativeDriver: false, }), Animated.spring(cardioHeight, { toValue: 1, ...SPRING.GENTLE, useNativeDriver: false, }), ]), Animated.delay(200), Animated.timing(citationOpacity, { toValue: 1, duration: DURATION.SLOW, easing: EASE.EASE_OUT, useNativeDriver: true, }), ]).start() }, []) const MAX_BAR_HEIGHT = 160 return ( {t('onboarding.solution.title')} {/* Comparison bars */} {/* Tabata bar */} {t('onboarding.solution.tabataCalories')} {t('onboarding.solution.tabata')} {t('onboarding.solution.tabataDuration')} {/* VS */} {t('onboarding.solution.vs')} {/* Cardio bar */} {t('onboarding.solution.cardioCalories')} {t('onboarding.solution.cardio')} {t('onboarding.solution.cardioDuration')} {/* Citation */} {t('onboarding.solution.citation')} {t('onboarding.solution.citationAuthor')} { haptics.buttonTap() onNext() }} > {t('onboarding.solution.cta')} ) } // ═══════════════════════════════════════════════════════════════════════════ // SCREEN 4 — WOW MOMENT (Staggered Feature Reveal) // ═══════════════════════════════════════════════════════════════════════════ const WOW_FEATURES = [ { icon: 'timer-outline' as const, iconColor: BRAND.PRIMARY, titleKey: 'onboarding.wow.card1Title', subtitleKey: 'onboarding.wow.card1Subtitle' }, { icon: 'barbell-outline' as const, iconColor: PHASE.REST, titleKey: 'onboarding.wow.card2Title', subtitleKey: 'onboarding.wow.card2Subtitle' }, { icon: 'mic-outline' as const, iconColor: PHASE.PREP, titleKey: 'onboarding.wow.card3Title', subtitleKey: 'onboarding.wow.card3Subtitle' }, { icon: 'trending-up-outline' as const, iconColor: PHASE.COMPLETE, titleKey: 'onboarding.wow.card4Title', subtitleKey: 'onboarding.wow.card4Subtitle' }, ] as const function WowScreen({ onNext }: { onNext: () => void }) { const { t } = useTranslation('screens') const haptics = useHaptics() const rowAnims = useRef(WOW_FEATURES.map(() => ({ opacity: new Animated.Value(0), translateY: new Animated.Value(20), }))).current const ctaOpacity = useRef(new Animated.Value(0)).current const [ctaReady, setCtaReady] = useState(false) useEffect(() => { // Staggered reveal: each row fades in + slides up, 150ms apart, starting at 300ms const STAGGER_DELAY = 150 const ROW_DURATION = DURATION.NORMAL // 300ms const START_DELAY = 300 WOW_FEATURES.forEach((_, i) => { setTimeout(() => { Animated.parallel([ Animated.timing(rowAnims[i].opacity, { toValue: 1, duration: ROW_DURATION, easing: EASE.EASE_OUT, useNativeDriver: true, }), Animated.timing(rowAnims[i].translateY, { toValue: 0, duration: ROW_DURATION, easing: EASE.EASE_OUT, useNativeDriver: true, }), ]).start() }, START_DELAY + i * STAGGER_DELAY) }) // CTA fades in 200ms after last row finishes const ctaDelay = START_DELAY + (WOW_FEATURES.length - 1) * STAGGER_DELAY + ROW_DURATION + 200 setTimeout(() => { setCtaReady(true) Animated.timing(ctaOpacity, { toValue: 1, duration: ROW_DURATION, easing: EASE.EASE_OUT, useNativeDriver: true, }).start() }, ctaDelay) }, []) return ( {t('onboarding.wow.title')} {t('onboarding.wow.subtitle')} {/* Feature list */} {WOW_FEATURES.map((feature, i) => ( {t(feature.titleKey)} {t(feature.subtitleKey)} ))} {/* CTA fades in after all rows */} { if (ctaReady) { haptics.buttonTap() onNext() } }} > {t('common:next')} ) } // ═══════════════════════════════════════════════════════════════════════════ // SCREEN 5 — PERSONALIZATION // ═══════════════════════════════════════════════════════════════════════════ const LEVELS: { value: FitnessLevel; labelKey: string }[] = [ { value: 'beginner', labelKey: 'common:levels.beginner' }, { value: 'intermediate', labelKey: 'common:levels.intermediate' }, { value: 'advanced', labelKey: 'common:levels.advanced' }, ] const GOALS: { value: FitnessGoal; labelKey: string }[] = [ { value: 'weight-loss', labelKey: 'onboarding.personalization.goals.weightLoss' }, { value: 'cardio', labelKey: 'onboarding.personalization.goals.cardio' }, { value: 'strength', labelKey: 'onboarding.personalization.goals.strength' }, { value: 'wellness', labelKey: 'onboarding.personalization.goals.wellness' }, ] const FREQUENCIES: { value: WeeklyFrequency; labelKey: string }[] = [ { value: 2, labelKey: 'onboarding.personalization.frequencies.2x' }, { value: 3, labelKey: 'onboarding.personalization.frequencies.3x' }, { value: 5, labelKey: 'onboarding.personalization.frequencies.5x' }, ] function PersonalizationScreen({ onNext, name, setName, level, setLevel, goal, setGoal, frequency, setFrequency, }: { onNext: () => void name: string setName: (n: string) => void level: FitnessLevel setLevel: (l: FitnessLevel) => void goal: FitnessGoal setGoal: (g: FitnessGoal) => void frequency: WeeklyFrequency setFrequency: (f: WeeklyFrequency) => void }) { const { t } = useTranslation('screens') const haptics = useHaptics() return ( {t('onboarding.personalization.title')} {/* Name input */} {t('onboarding.personalization.yourName')} {/* Fitness Level */} {t('onboarding.personalization.fitnessLevel')} {LEVELS.map((item) => ( { haptics.selection() setLevel(item.value) }} > {t(item.labelKey)} ))} {/* Goal */} {t('onboarding.personalization.yourGoal')} {GOALS.map((item) => ( { haptics.selection() setGoal(item.value) }} > {t(item.labelKey)} ))} {/* Frequency */} {t('onboarding.personalization.weeklyFrequency')} {FREQUENCIES.map((item) => ( { haptics.selection() setFrequency(item.value) }} > {t(item.labelKey)} ))} {name.trim().length > 0 && ( {t('onboarding.personalization.readyMessage')} )} { if (name.trim()) { haptics.buttonTap() onNext() } }} > {t('common:continue')} ) } // ═══════════════════════════════════════════════════════════════════════════ // SCREEN 6 — PAYWALL // ═══════════════════════════════════════════════════════════════════════════ const PREMIUM_FEATURE_KEYS = [ 'onboarding.paywall.features.unlimited', 'onboarding.paywall.features.offline', 'onboarding.paywall.features.stats', 'onboarding.paywall.features.noAds', ] as const function PaywallScreen({ onSubscribe, onSkip, }: { onSubscribe: (plan: 'premium-monthly' | 'premium-yearly') => void onSkip: () => void }) { const { t } = useTranslation('screens') const haptics = useHaptics() const [selectedPlan, setSelectedPlan] = useState<'premium-monthly' | 'premium-yearly'>('premium-yearly') const featureAnims = useRef(PREMIUM_FEATURE_KEYS.map(() => new Animated.Value(0))).current useEffect(() => { // Staggered feature fade-in PREMIUM_FEATURE_KEYS.forEach((_, i) => { setTimeout(() => { Animated.timing(featureAnims[i], { toValue: 1, duration: DURATION.NORMAL, easing: EASE.EASE_OUT, useNativeDriver: true, }).start() }, i * 100) }) }, []) return ( {t('onboarding.paywall.title')} {/* Features */} {PREMIUM_FEATURE_KEYS.map((featureKey, i) => ( {t(featureKey)} ))} {/* Pricing cards */} {/* Annual */} { haptics.selection() setSelectedPlan('premium-yearly') }} > {t('onboarding.paywall.bestValue')} {t('onboarding.paywall.yearlyPrice')} {t('common:units.perYear')} {t('onboarding.paywall.savePercent')} {/* Monthly */} { haptics.selection() setSelectedPlan('premium-monthly') }} > {t('onboarding.paywall.monthlyPrice')} {t('common:units.perMonth')} {/* CTA */} { haptics.buttonTap() onSubscribe(selectedPlan) }} > {t('onboarding.paywall.trialCta')} {/* Guarantees */} {t('onboarding.paywall.guarantees')} {/* Skip */} {t('onboarding.paywall.skipButton')} ) } // ═══════════════════════════════════════════════════════════════════════════ // MAIN ONBOARDING CONTROLLER // ═══════════════════════════════════════════════════════════════════════════ export default function OnboardingScreen() { const router = useRouter() const [step, setStep] = useState(1) // Personalization state const [barriers, setBarriers] = useState([]) const [name, setName] = useState('') const [level, setLevel] = useState('beginner') const [goal, setGoal] = useState('cardio') const [frequency, setFrequency] = useState(3) const completeOnboarding = useUserStore((s) => s.completeOnboarding) const setSubscription = useUserStore((s) => s.setSubscription) const finishOnboarding = useCallback( (plan: 'free' | 'premium-monthly' | 'premium-yearly') => { completeOnboarding({ name: name.trim() || 'Athlete', fitnessLevel: level, goal, weeklyFrequency: frequency, barriers, }) if (plan !== 'free') { setSubscription(plan) } router.replace('/(tabs)') }, [name, level, goal, frequency, barriers] ) const nextStep = useCallback(() => { setStep((s) => Math.min(s + 1, TOTAL_STEPS)) }, []) const renderStep = () => { switch (step) { case 1: return case 2: return ( ) case 3: return case 4: return case 5: return ( ) case 6: return ( finishOnboarding(plan)} onSkip={() => finishOnboarding('free')} /> ) default: return null } } return ( {renderStep()} ) } // ═══════════════════════════════════════════════════════════════════════════ // STYLES // ═══════════════════════════════════════════════════════════════════════════ const styles = StyleSheet.create({ // Layout helpers screenCenter: { flex: 1, justifyContent: 'center', alignItems: 'center', }, screenFull: { flex: 1, }, titleCenter: { textAlign: 'center', }, subtitle: { textAlign: 'center', }, bottomAction: { position: 'absolute', bottom: SPACING[4], left: 0, right: 0, }, // CTA Button ctaButton: { height: LAYOUT.BUTTON_HEIGHT, backgroundColor: BRAND.PRIMARY, borderRadius: RADIUS.GLASS_BUTTON, alignItems: 'center', justifyContent: 'center', }, ctaButtonDisabled: { backgroundColor: DARK.ELEVATED, }, // ── Screen 2: Barriers ── barrierGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING[3], marginTop: SPACING[8], justifyContent: 'center', }, barrierCard: { width: (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2, paddingVertical: SPACING[6], alignItems: 'center', borderRadius: RADIUS.GLASS_CARD, ...GLASS.BASE, }, barrierCardSelected: { borderColor: BRAND.PRIMARY, backgroundColor: 'rgba(255, 107, 53, 0.1)', }, // ── Screen 3: Comparison ── comparisonContainer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'flex-end', marginTop: SPACING[10], paddingHorizontal: SPACING[8], gap: SPACING[4], }, barColumn: { alignItems: 'center', flex: 1, }, barTrack: { width: 60, height: 160, backgroundColor: DARK.OVERLAY_1, borderRadius: RADIUS.SM, overflow: 'hidden', marginVertical: SPACING[3], justifyContent: 'flex-end', }, barFill: { width: '100%', borderRadius: RADIUS.SM, }, barTabata: { backgroundColor: BRAND.PRIMARY, }, barCardio: { backgroundColor: PHASE.REST, }, vsContainer: { paddingBottom: 80, }, citation: { marginTop: SPACING[8], paddingHorizontal: SPACING[4], }, citationText: { textAlign: 'center', fontStyle: 'italic', lineHeight: 20, }, citationAuthor: { textAlign: 'center', marginTop: SPACING[2], }, // ── Screen 5: Personalization ── personalizationContent: { paddingBottom: SPACING[10], }, fieldGroup: { marginTop: SPACING[6], }, fieldLabel: { letterSpacing: 1.5, marginBottom: SPACING[2], }, textInput: { height: LAYOUT.BUTTON_HEIGHT_SM, backgroundColor: DARK.SURFACE, borderRadius: RADIUS.MD, paddingHorizontal: SPACING[4], color: TEXT.PRIMARY, fontSize: 17, borderWidth: 1, borderColor: BORDER.GLASS, }, segmentRow: { flexDirection: 'row', backgroundColor: DARK.SURFACE, borderRadius: RADIUS.MD, padding: 3, gap: 2, }, segmentButton: { flex: 1, height: 36, alignItems: 'center', justifyContent: 'center', borderRadius: RADIUS.SM, }, segmentButtonActive: { backgroundColor: DARK.ELEVATED, }, readyMessage: { textAlign: 'center', marginTop: SPACING[6], }, // ── Screen 6: Paywall ── paywallContent: { paddingBottom: SPACING[10], }, featuresList: { marginTop: SPACING[8], gap: SPACING[4], }, featureRow: { flexDirection: 'row', alignItems: 'center', }, pricingCards: { flexDirection: 'row', gap: SPACING[3], marginTop: SPACING[8], }, pricingCard: { flex: 1, paddingVertical: SPACING[5], alignItems: 'center', borderRadius: RADIUS.GLASS_CARD, ...GLASS.BASE, }, pricingCardSelected: { borderColor: BRAND.PRIMARY, borderWidth: 2, backgroundColor: 'rgba(255, 107, 53, 0.08)', }, bestValueBadge: { backgroundColor: BRAND.PRIMARY, paddingHorizontal: SPACING[3], paddingVertical: SPACING[1], borderRadius: RADIUS.SM, marginBottom: SPACING[2], }, trialButton: { height: LAYOUT.BUTTON_HEIGHT, backgroundColor: BRAND.PRIMARY, borderRadius: RADIUS.GLASS_BUTTON, alignItems: 'center', justifyContent: 'center', marginTop: SPACING[6], }, guarantees: { alignItems: 'center', marginTop: SPACING[4], }, skipButton: { alignItems: 'center', paddingVertical: SPACING[5], marginTop: SPACING[2], }, }) // ── Screen 4: Feature List Styles ── const wowStyles = StyleSheet.create({ list: { gap: SPACING[5], marginTop: SPACING[4], }, row: { flexDirection: 'row', alignItems: 'center', gap: SPACING[4], }, iconCircle: { width: 44, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center', }, textCol: { flex: 1, }, })