feat: onboarding flow (6 screens) + audio engine + design system

Onboarding:
- 6-screen flow: Problem → Empathy → Solution → Wow → Personalization → Paywall
- useOnboarding hook with Zustand + AsyncStorage persistence
- MiniTimerDemo with live 20s timer + haptics
- Auto-redirect for first-time users
- Mock RevenueCat for dev testing

Audio:
- useAudioEngine hook with expo-av
- Phase sounds (count_3/2/1, beep, bell, fanfare)
- Placeholder music tracks

Design System:
- Typography component + constants
- GlassView component
- Spacing, shadows, animations, borderRadius constants
- Extended color palette (phase gradients, glass, surfaces)

Timer:
- Fix: handle 0-duration phases (immediate advance)
- Enhanced TimerDisplay with phase gradients

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-17 21:52:23 +01:00
parent 31bdb1586f
commit fa189fe72e
55 changed files with 3361 additions and 320 deletions

View File

@@ -1,10 +1,73 @@
import { useEffect } from 'react'
import { useRouter } from 'expo-router'
import * as Haptics from 'expo-haptics'
import { useTimerEngine } from '@/src/features/timer'
import { useAudioEngine } from '@/src/features/audio'
import { TimerDisplay } from '@/src/features/timer/components/TimerDisplay'
import type { TimerEvent } from '@/src/features/timer/types'
export default function TimerScreen() {
const router = useRouter()
const timer = useTimerEngine()
const audio = useAudioEngine()
// Preload audio on mount
useEffect(() => {
audio.preloadAll()
return () => {
audio.unloadAll()
}
}, [])
// Subscribe to timer events → trigger audio + haptics
useEffect(() => {
const unsubscribe = timer.addEventListener(async (event: TimerEvent) => {
switch (event.type) {
case 'PHASE_CHANGED':
await handlePhaseChange(event.to)
break
case 'COUNTDOWN_TICK':
await audio.playPhaseSound(
event.secondsLeft === 1 ? 'count_1' : event.secondsLeft === 2 ? 'count_2' : 'count_3'
)
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
break
case 'ROUND_COMPLETED':
await audio.playPhaseSound('bell')
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
break
case 'SESSION_COMPLETE':
await audio.playPhaseSound('fanfare')
await audio.stopMusic(1000)
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
break
}
})
return unsubscribe
}, [audio.isLoaded])
async function handlePhaseChange(to: string) {
switch (to) {
case 'GET_READY':
await audio.startMusic('LOW')
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
break
case 'WORK':
await audio.playPhaseSound('beep_long')
await audio.switchIntensity('HIGH')
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
break
case 'REST':
await audio.playPhaseSound('beep_double')
await audio.switchIntensity('LOW')
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
break
}
}
function handleStart() {
timer.start()
@@ -12,6 +75,7 @@ export default function TimerScreen() {
function handleStop() {
timer.stop()
audio.stopMusic(300)
router.back()
}