feat: close v1 feature gaps — freemium gating, video/audio infrastructure, EAS build config

- Add access control service with 3 free workouts (IDs 1, 11, 43), paywall gating on workout detail and lock indicators on explore grid
- Wire VideoPlayer into player background and workout detail preview
- Add placeholder HLS video URLs to 5 sample workouts (Apple test streams)
- Add test audio URLs to music service MOCK_TRACKS for all vibes
- Switch RevenueCat API key to env-based with sandbox fallback
- Create eas.json with development/preview/production build profiles
- Update app.json with iOS privacy descriptions (HealthKit, Camera) and non-exempt encryption flag
- Create collection detail screen (route crash fix)
- Replace hardcoded profile stats with real activity store data
- Add unlockWithPremium i18n key in EN/FR/DE/ES
This commit is contained in:
Millian Lamiaux
2026-03-24 12:20:56 +01:00
parent cd065d07c3
commit a042c348c1
16 changed files with 452 additions and 34 deletions

View File

@@ -14,9 +14,12 @@ import { Host, Button, HStack } from '@expo/ui/swift-ui'
import { glassEffect, padding } from '@expo/ui/swift-ui/modifiers'
import { useHaptics } from '@/src/shared/hooks'
import { usePurchases } from '@/src/shared/hooks/usePurchases'
import { track } from '@/src/shared/services/analytics'
import { canAccessWorkout } from '@/src/shared/services/access'
import { getWorkoutById } from '@/src/shared/data'
import { useTranslatedWorkout, useMusicVibeLabel } from '@/src/shared/data/useTranslatedData'
import { VideoPlayer } from '@/src/shared/components/VideoPlayer'
import { useThemeColors, BRAND } from '@/src/shared/theme'
import type { ThemeColors } from '@/src/shared/theme/types'
@@ -35,6 +38,7 @@ export default function WorkoutDetailScreen() {
const { t } = useTranslation()
const { id } = useLocalSearchParams<{ id: string }>()
const [isSaved, setIsSaved] = useState(false)
const { isPremium } = usePurchases()
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
@@ -62,7 +66,18 @@ export default function WorkoutDetailScreen() {
)
}
const isLocked = !canAccessWorkout(workout.id, isPremium)
const handleStartWorkout = () => {
if (isLocked) {
haptics.buttonTap()
track('paywall_triggered', {
source: 'workout_detail',
workout_id: workout.id,
})
router.push('/paywall')
return
}
haptics.phaseChange()
router.push(`/player/${workout.id}`)
}
@@ -110,6 +125,13 @@ export default function WorkoutDetailScreen() {
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
showsVerticalScrollIndicator={false}
>
{/* Video Preview Hero */}
<VideoPlayer
videoUrl={workout.videoUrl}
mode="preview"
isPlaying={true}
style={styles.videoPreview}
/>
{/* Quick stats */}
<View style={styles.quickStats}>
<View style={[styles.statBadge, { backgroundColor: 'rgba(255, 107, 53, 0.15)' }]}>
@@ -184,11 +206,17 @@ export default function WorkoutDetailScreen() {
<Pressable
style={({ pressed }) => [
styles.startButton,
isLocked && styles.lockedButton,
pressed && styles.startButtonPressed,
]}
onPress={handleStartWorkout}
>
<RNText style={styles.startButtonText}>{t('screens:workout.startWorkout')}</RNText>
{isLocked && (
<Ionicons name="lock-closed" size={18} color="#FFFFFF" style={{ marginRight: 8 }} />
)}
<RNText style={styles.startButtonText}>
{isLocked ? t('screens:workout.unlockWithPremium') : t('screens:workout.startWorkout')}
</RNText>
</Pressable>
</View>
</View>
@@ -235,6 +263,14 @@ function createStyles(colors: ThemeColors) {
height: 44,
},
// Video Preview
videoPreview: {
height: 220,
borderRadius: RADIUS.XL,
overflow: 'hidden' as const,
marginBottom: SPACING[4],
},
// Quick Stats
quickStats: {
flexDirection: 'row',
@@ -380,6 +416,12 @@ function createStyles(colors: ThemeColors) {
backgroundColor: BRAND.PRIMARY,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
lockedButton: {
backgroundColor: colors.bg.surface,
borderWidth: 1,
borderColor: BRAND.PRIMARY,
},
startButtonPressed: {
backgroundColor: BRAND.PRIMARY_DARK,