feat: onboarding flow with staggered-reveal wow screen

6-screen conversion funnel: Problem → Empathy → Solution → Wow → Personalization → Paywall.
Screen 4 uses a staggered-reveal feature list where all 4 features animate in
sequentially (150ms apart), replacing a carousel pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-20 18:52:05 +01:00
parent 2d24831f8e
commit aa75afb1b7
2 changed files with 1235 additions and 0 deletions

1127
app/onboarding.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,108 @@
/**
* TabataFit OnboardingStep
* Reusable wrapper for each onboarding screen — progress bar, animation, layout
*/
import { useRef, useEffect } from 'react'
import { View, StyleSheet, Animated, Dimensions } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { DARK, BRAND, TEXT } from '../constants/colors'
import { SPACING, LAYOUT } from '../constants/spacing'
import { DURATION, EASE } from '../constants/animations'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
interface OnboardingStepProps {
step: number
totalSteps: number
children: React.ReactNode
}
export function OnboardingStep({ step, totalSteps, children }: OnboardingStepProps) {
const insets = useSafeAreaInsets()
const slideAnim = useRef(new Animated.Value(SCREEN_WIDTH)).current
const fadeAnim = useRef(new Animated.Value(0)).current
const progressAnim = useRef(new Animated.Value(0)).current
useEffect(() => {
// Reset position for new step
slideAnim.setValue(SCREEN_WIDTH)
fadeAnim.setValue(0)
// Animate in
Animated.parallel([
Animated.timing(slideAnim, {
toValue: 0,
duration: DURATION.NORMAL,
easing: EASE.EASE_OUT,
useNativeDriver: true,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: DURATION.NORMAL,
easing: EASE.EASE_OUT,
useNativeDriver: true,
}),
]).start()
// Animate progress bar
Animated.timing(progressAnim, {
toValue: step / totalSteps,
duration: DURATION.SLOW,
easing: EASE.EASE_OUT,
useNativeDriver: false,
}).start()
}, [step])
const progressWidth = progressAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
})
return (
<View style={[styles.container, { paddingTop: insets.top + SPACING[3] }]}>
{/* Progress bar */}
<View style={styles.progressTrack}>
<Animated.View style={[styles.progressFill, { width: progressWidth }]} />
</View>
{/* Step content */}
<Animated.View
style={[
styles.content,
{
transform: [{ translateX: slideAnim }],
opacity: fadeAnim,
},
{ paddingBottom: insets.bottom + SPACING[6] },
]}
>
{children}
</Animated.View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: DARK.BASE,
},
progressTrack: {
height: 3,
backgroundColor: DARK.SURFACE,
marginHorizontal: LAYOUT.SCREEN_PADDING,
borderRadius: 2,
overflow: 'hidden',
},
progressFill: {
height: '100%',
backgroundColor: BRAND.PRIMARY,
borderRadius: 2,
},
content: {
flex: 1,
paddingHorizontal: LAYOUT.SCREEN_PADDING,
paddingTop: SPACING[8],
},
})