/**
* 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,
},
})