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

@@ -8,7 +8,9 @@ import Ionicons from '@expo/vector-icons/Ionicons'
import { useTranslation } from 'react-i18next'
import { useHaptics } from '@/src/shared/hooks'
import { usePurchases } from '@/src/shared/hooks/usePurchases'
import { useWorkouts, useCollections, useFeaturedWorkouts, useTrainers } from '@/src/shared/hooks/useSupabaseData'
import { isFreeWorkout } from '@/src/shared/services/access'
import { StyledText } from '@/src/shared/components/StyledText'
import { useThemeColors, BRAND } from '@/src/shared/theme'
@@ -108,9 +110,11 @@ function CollectionCard({
function WorkoutCard({
workout,
onPress,
isLocked,
}: {
workout: Workout
onPress: () => void
isLocked?: boolean
}) {
const colors = useThemeColors()
const categoryColor = CATEGORY_COLORS[workout.category]
@@ -133,9 +137,15 @@ function WorkoutCard({
</StyledText>
</View>
{isLocked && (
<View style={styles.lockBadge}>
<Ionicons name="lock-closed" size={10} color="#FFFFFF" />
</View>
)}
<View style={styles.playArea}>
<View style={[styles.playCircle, { backgroundColor: categoryColor + '20' }]}>
<Ionicons name="play" size={18} color={categoryColor} />
<Ionicons name={isLocked ? 'lock-closed' : 'play'} size={18} color={categoryColor} />
</View>
</View>
@@ -209,6 +219,7 @@ export default function ExploreScreen() {
const router = useRouter()
const haptics = useHaptics()
const colors = useThemeColors()
const { isPremium } = usePurchases()
const [filters, setFilters] = useState<FilterState>({
category: 'all',
@@ -319,6 +330,7 @@ export default function ExploreScreen() {
key={workout.id}
workout={workout}
onPress={() => handleWorkoutPress(workout.id)}
isLocked={!isPremium && !isFreeWorkout(workout.id)}
/>
))}
</View>
@@ -398,6 +410,7 @@ export default function ExploreScreen() {
key={workout.id}
workout={workout}
onPress={() => handleWorkoutPress(workout.id)}
isLocked={!isPremium && !isFreeWorkout(workout.id)}
/>
))}
</View>
@@ -516,6 +529,17 @@ const styles = StyleSheet.create({
paddingVertical: 3,
borderRadius: RADIUS.SM,
},
lockBadge: {
position: 'absolute',
top: SPACING[3],
left: SPACING[3],
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: 'rgba(0,0,0,0.6)',
alignItems: 'center',
justifyContent: 'center',
},
playArea: {
position: 'absolute',
top: 0,

View File

@@ -18,7 +18,7 @@ import * as Linking from 'expo-linking'
import Constants from 'expo-constants'
import { useTranslation } from 'react-i18next'
import { useMemo, useState } from 'react'
import { useUserStore } from '@/src/shared/stores'
import { useUserStore, useActivityStore } from '@/src/shared/stores'
import { requestNotificationPermissions, usePurchases } from '@/src/shared/hooks'
import { useThemeColors, BRAND } from '@/src/shared/theme'
import type { ThemeColors } from '@/src/shared/theme/types'
@@ -78,12 +78,14 @@ export default function ProfileScreen() {
const planLabel = isPremium ? 'TabataFit+' : t('profile.freePlan')
const avatarInitial = profile.name?.[0]?.toUpperCase() || 'U'
// Mock stats (replace with real data from activityStore when available)
const stats = {
workouts: 47,
streak: 12,
calories: 12500,
}
// Real stats from activity store
const history = useActivityStore((s) => s.history)
const streak = useActivityStore((s) => s.streak)
const stats = useMemo(() => ({
workouts: history.length,
streak: streak.current,
calories: history.reduce((sum, r) => sum + (r.calories ?? 0), 0),
}), [history, streak])
const handleSignOut = () => {
updateProfile({