feat: Apple Watch app + Paywall + Privacy Policy + rebranding

## Major Features
- Apple Watch companion app (6 phases complete)
  - WatchConnectivity iPhone ↔ Watch
  - HealthKit integration (HR, calories)
  - SwiftUI premium UI
  - 9 complication types
  - Always-On Display support

- Paywall screen with RevenueCat integration
- Privacy Policy screen
- App rebranding: tabatago → TabataFit
- Bundle ID: com.millianlmx.tabatafit

## Changes
- New: ios/TabataFit Watch App/ (complete Watch app)
- New: app/paywall.tsx (subscription UI)
- New: app/privacy.tsx (privacy policy)
- New: src/features/watch/ (Watch sync hooks)
- New: admin-web/ (admin dashboard)
- Updated: app.json, package.json (branding)
- Updated: profile.tsx (paywall + privacy links)
- Updated: i18n translations (EN/FR/DE/ES)
- New: app icon 1024x1024

## Watch App Files
- TabataFitWatchApp.swift (entry point)
- ContentView.swift (premium UI)
- HealthKitManager.swift (HR + calories)
- WatchSessionManager.swift (communication)
- Complications/ (WidgetKit)
- UserDefaults+Shared.swift (data sharing)
This commit is contained in:
Millian Lamiaux
2026-03-11 09:43:53 +01:00
parent f80798069b
commit 2ad7ae3a34
86 changed files with 19648 additions and 365 deletions

View File

@@ -29,9 +29,11 @@ import { useTranslation } from 'react-i18next'
import { useTimer } from '@/src/shared/hooks/useTimer'
import { useHaptics } from '@/src/shared/hooks/useHaptics'
import { useAudio } from '@/src/shared/hooks/useAudio'
import { useMusicPlayer } from '@/src/shared/hooks/useMusicPlayer'
import { useActivityStore } from '@/src/shared/stores'
import { getWorkoutById } from '@/src/shared/data'
import { useTranslatedWorkout } from '@/src/shared/data/useTranslatedData'
import { useWatchSync } from '@/src/features/watch'
import { track } from '@/src/shared/services/analytics'
import { BRAND, PHASE_COLORS, GRADIENTS, darkColors } from '@/src/shared/theme'
@@ -280,8 +282,37 @@ export default function PlayerScreen() {
const timer = useTimer(rawWorkout ?? null)
const audio = useAudio()
// Music player - synced with workout timer
useMusicPlayer({
vibe: workout?.musicVibe ?? 'electronic',
isPlaying: timer.isRunning && !timer.isPaused,
})
const [showControls, setShowControls] = useState(true)
// Watch sync integration
const { isAvailable: isWatchAvailable, sendWorkoutState } = useWatchSync({
onPlay: () => {
timer.resume()
track('watch_control_play', { workout_id: workout?.id ?? id })
},
onPause: () => {
timer.pause()
track('watch_control_pause', { workout_id: workout?.id ?? id })
},
onSkip: () => {
timer.skip()
haptics.selection()
track('watch_control_skip', { workout_id: workout?.id ?? id })
},
onStop: () => {
haptics.phaseChange()
timer.stop()
router.back()
track('watch_control_stop', { workout_id: workout?.id ?? id })
},
})
// Animation refs
const timerScaleAnim = useRef(new Animated.Value(0.8)).current
const phaseColor = PHASE_COLORS[timer.phase].fill
@@ -398,6 +429,34 @@ export default function PlayerScreen() {
}
}, [timer.timeRemaining])
// Sync workout state with Apple Watch
useEffect(() => {
if (!isWatchAvailable || !timer.isRunning) return;
sendWorkoutState({
phase: timer.phase,
timeRemaining: timer.timeRemaining,
currentRound: timer.currentRound,
totalRounds: timer.totalRounds,
currentExercise: timer.currentExercise,
nextExercise: timer.nextExercise,
calories: timer.calories,
isPaused: timer.isPaused,
isPlaying: timer.isRunning && !timer.isPaused,
});
}, [
timer.phase,
timer.timeRemaining,
timer.currentRound,
timer.totalRounds,
timer.currentExercise,
timer.nextExercise,
timer.calories,
timer.isPaused,
timer.isRunning,
isWatchAvailable,
]);
return (
<View style={styles.container}>
<StatusBar hidden />