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:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user