diff --git a/TabataGo_PRD_v1.1.md b/TabataGo_PRD_v1.1.md
deleted file mode 100644
index cebeb1a..0000000
--- a/TabataGo_PRD_v1.1.md
+++ /dev/null
@@ -1,597 +0,0 @@
-
-
-| đ„ TABATAGO Application Mobile de Minuterie Tabata *Product Requirements Document (PRD) v1.1* Framework : Expo (React Native) · Cible : iOS & Android · FĂ©vrier 2026 |
-| :---: |
-
-# **1\. Résumé Exécutif**
-
-TabataGo est une application mobile premium dĂ©diĂ©e Ă l'entraĂźnement par intervalles en mĂ©thode Tabata (20 secondes d'effort / 10 secondes de repos). Elle cible les personnes qui veulent des sĂ©ances courtes, intenses et guidĂ©es â sans abonnement Ă une salle. Le marchĂ© des applications de fitness dĂ©passe 1,5 milliard de dollars en revenus annuels et le Tabata reste un mot-clĂ© Ă forte demande (popularitĂ© \> 70\) avec une difficultĂ© ASO modĂ©rĂ©e (\< 50).
-
-| Indicateur | Valeur cible |
-| :---- | :---- |
-| Marché visé | Fitness enthousiasts 25-45 ans, Home workout, HIIT lovers |
-| Plateformes | iOS (priorité) \+ Android |
-| Framework | Expo (React Native) â SDK 52+ |
-| ModÚle économique | Freemium \+ Abonnement mensuel/annuel via RevenueCat |
-| Prix cible (US) | $4.99/mois ou $29.99/an |
-| Objectif J+30 | 5 000 téléchargements, taux de conversion essai \> 30% |
-| Objectif J+90 | MRR $5 000, note App Store â„ 4.6 |
-
-# **2\. FonctionnalitĂ©s Core â SpĂ©cifications DĂ©taillĂ©es**
-
-TabataGo repose sur trois piliers fonctionnels indissociables : le Timer (moteur de l'expérience), la Musique (moteur émotionnel), et l'Exercice (moteur pédagogique). Ces trois composants doivent fonctionner en parfaite synchronisation.
-
-## **2.1 Le Timer â Moteur Central**
-
-Le timer est la fonctionnalitĂ© principale et vitale de l'application. Son implĂ©mentation doit ĂȘtre irrĂ©prochable : prĂ©cision, fluiditĂ© visuelle, comportement en arriĂšre-plan.
-
-### **2.1.1 Structure d'une Séance Tabata Standard**
-
-| Phase | Durée par défaut | Configurable ? | Couleur écran | Signal |
-| :---- | :---- | :---- | :---- | :---- |
-| PrĂ©paration (Get Ready) | 10 secondes | Oui (5â30s) | Jaune \#EAB308 | Bip court Ă 3 \+ vibration lĂ©gĂšre |
-| Travail (Work) | 20 secondes | Oui (5â60s) | Orange vif \#F97316 | Bip long au dĂ©marrage \+ haptique fort |
-| Repos (Rest) | 10 secondes | Oui (5â60s) | Bleu calme \#3B82F6 | Double bip court \+ haptique lĂ©ger |
-| Fin de round | InstantanĂ© | â | Flash blanc | Son de cloche \+ vibration |
-| Fin de sĂ©ance | Ăcran rĂ©sultat | â | Vert \#22C55E | Fanfare \+ longue vibration |
-
-### **2.1.2 ParamĂštres Configurables du Timer**
-
-| ParamÚtre | Valeur par défaut | Min | Max | AccÚs |
-| :---- | :---- | :---- | :---- | :---- |
-| Durée Work | 20s | 5s | 60s | Tous |
-| Durée Rest | 10s | 5s | 60s | Tous |
-| Nombre de rounds | 8 | 1 | 30 | Tous |
-| Durée Get Ready | 10s | 0s | 30s | Tous |
-| Nombre de cycles | 1 | 1 | 10 | Premium |
-| Pause entre cycles | 60s | 10s | 300s | Premium |
-| Cycles de rĂ©cupĂ©ration | â | â | â | Premium |
-
-### **2.1.3 Affichage Timer â Ăcran SĂ©ance (plein Ă©cran)**
-
-| đ± LAYOUT â Ăcran Timer Plein Ăcran |
-| :---- |
-| âž Zone HAUTE (20%) : Nom de l'exercice en cours \+ numĂ©ro de round (ex: "Burpees â Round 3/8") |
-| âž Zone CENTRALE (50%) : Chiffre du compte Ă rebours â trĂšs grand (96-120px), police monospace, couleur de phase |
-| ➠Zone BASSE HAUTE (15%) : Barre de progression de la séance complÚte (rounds) \+ indicateur phase actuelle |
-| âž Zone BASSE (15%) : Boutons Pause / Stop / Skip â discrets pour ne pas distraire |
-| ➠FOND : Couleur dynamique selon la phase (orange work, bleu rest, jaune prep) avec transition animée |
-| âž ANIMATION : Pulsation subtile du chiffre Ă chaque seconde \+ ring circulaire de progression |
-
-### **2.1.4 Comportement Technique du Timer**
-
-* PrĂ©cision : Utiliser expo-background-fetch \+ Date.now() delta pour compenser les drifts â tolĂ©rance \< 50ms
-
-* Background : Le timer continue en arriĂšre-plan (notification sticky affichant le compte Ă rebours)
-
-* Verrouillage Ă©cran : Ăcran reste allumĂ© pendant la sĂ©ance (expo-keep-awake)
-
-* Interruptions : Pause automatique si appel téléphonique entrant (AppState listener)
-
-* Reprise : Si l'app est tuĂ©e, afficher une notification "SĂ©ance interrompue â Reprendre ?" au retour
-
-* OTA update safe : Le state du timer est isolé du cycle de render React pour éviter les glitches
-
-## **2.2 La Musique â Moteur Ămotionnel**
-
-La musique transforme une simple minuterie en expérience motivante. Elle doit s'adapter dynamiquement à chaque phase de la séance (work vs rest) et ne jamais entrer en conflit avec la musique de l'utilisateur ou les signaux sonores.
-
-### **2.2.1 Architecture Sonore**
-
-| Couche audio | Description | Technologie | ContrĂŽle utilisateur |
-| :---- | :---- | :---- | :---- |
-| Musique d'ambiance | Tracks BPM-synchronisées intégrées à l'app | expo-av (AVAudioSession) | Volume indépendant, on/off |
-| Signaux de phase | Bips, voix, cloche â changement work/rest | expo-av (prioritĂ© haute) | Volume indĂ©pendant, choix du son |
-| Voix coach | Annonces vocales (optionnel) : "Go\!", "Rest", "Last round\!" | expo-av (TTS ou fichiers pré-enregistrés) | On/off, langue |
-| Haptiques | Vibrations synchronisées aux signaux | expo-haptics | On/off |
-
-### **2.2.2 Catalogue Musical Intégré (Offline)**
-
-| đ” TRACKS INTĂGRĂES â 3 ambiances Ă 3 intensitĂ©s \= 9 tracks minimum |
-| :---- |
-| âž Ambiance ELECTRO : Low (rĂ©cup), Medium (standard), High (intense) â BPM 120/140/160 |
-| âž Ambiance HIP-HOP : Low (rĂ©cup), Medium (standard), High (intense) â BPM 85/100/115 |
-| âž Ambiance ROCK/METAL : Low (rĂ©cup), Medium (standard), High (intense) â BPM 130/150/170 |
-| âž Mode SILENCE : Aucune musique, uniquement les signaux sonores de phase |
-| âž Mode SPOTIFY/APPLE MUSIC : L'app n'interfĂšre pas avec la musique de l'utilisateur (coexistence audio) |
-| âž Toutes les tracks sont royalty-free et embarquĂ©es dans le bundle â aucun streaming requis |
-
-### **2.2.3 Synchronisation Musique â Timer**
-
-* Phase WORK : Passer automatiquement à la track haute intensité de l'ambiance sélectionnée
-
-* Phase REST : Transition en fade-out 1s vers la track basse intensité (ambiance calme)
-
-* Phase GET READY : Intro de 10s sur la track principale
-
-* Transition douce : Cross-fade 500ms entre les phases pour éviter les coupures brutales
-
-* BPM adaptatif (Premium) : L'app détecte le rythme de la track et aligne le bip de fin de phase sur le beat
-
-* Pas de conflit : Si l'utilisateur a sa propre musique, les signaux de phase s'y superposent en ducking audio (baisse temporaire du volume)
-
-### **2.2.4 Signaux Sonores de Phase â Options**
-
-| Signal | Options disponibles | Par défaut | Premium uniquement ? |
-| :---- | :---- | :---- | :---- |
-| Début Work | Bip long, Whistle, Voix "Go\!", Air horn, Clap | Bip long | Non |
-| Début Rest | Double bip, Voix "Rest", Bell, Ding | Double bip | Non |
-| Décompte 3-2-1 | Bips courts, Voix "3, 2, 1", Silence | Bips courts | Non |
-| Fin de round | Cloche, Applaudissements, Voix "Round X done\!" | Cloche | Oui |
-| Fin de séance | Fanfare, Applaudissements, Voix "Workout complete\!" | Fanfare | Non |
-| Dernier round | Voix "Last round\!", Alarm, Son spécial | Voix | Oui |
-
-### **2.2.5 Gestion Technique Audio (expo-av)**
-
-* Session audio iOS : AVAudioSessionCategoryPlayback avec MixWithOthers â permet de jouer avec la musique utilisateur
-
-* Focus audio Android : AudioManager.AUDIOFOCUS\_GAIN\_TRANSIENT\_MAY\_DUCK pour les signaux
-
-* Préchargement : Tous les sons de phase sont chargés en mémoire au démarrage de la séance (zéro latence)
-
-* Mode silencieux iOS : Les signaux de phase respectent le switch mute SAUF si l'utilisateur a activé "override" dans les settings
-
-* Headphones détection : Si écouteurs branchés, désactiver les haptiques de phase par défaut
-
-## **2.3 L'Exercice â Moteur PĂ©dagogique**
-
-Chaque round du timer doit ĂȘtre associĂ© Ă un exercice spĂ©cifique, affichĂ© clairement pendant la phase de travail. C'est ce qui diffĂ©rencie TabataGo d'une simple minuterie gĂ©nĂ©rique.
-
-### **2.3.1 BibliothĂšque d'Exercices**
-
-| Catégorie | Exemples d'exercices | Nb exercices V1 | Nb exercices V2 |
-| :---- | :---- | :---- | :---- |
-| Cardio / Full body | Burpees, Jumping Jacks, Mountain Climbers, High Knees | 8 | 20 |
-| Bas du corps | Squats, Fentes, Jump Squats, Glute Bridges, Wall Sit | 8 | 20 |
-| Haut du corps | Push-ups, Pike Push-ups, Tricep Dips, Shoulder Taps | 6 | 15 |
-| Abdos / Core | Crunches, Planche, Russian Twists, Bicycle Crunches | 6 | 15 |
-| Sans saut (low impact) | Slow Squats, Modified Push-ups, Step Touch, March | 6 | 15 |
-| Avec matériel | Kettlebell Swings, Dumbbell Thrusters, Jump Rope | 4 | 10 |
-
-### **2.3.2 Fiche Exercice â DonnĂ©es par Exercice**
-
-| đ MODĂLE DE DONNĂES â Exercice |
-| :---- |
-| âž id : identifiant unique (ex: "burpee\_classic") |
-| ➠name : Nom localisé (FR: "Burpee", EN: "Burpee", ES: "Burpee") |
-| âž category : cardio | lower\_body | upper\_body | core | low\_impact | equipment |
-| âž difficulty : beginner | intermediate | advanced |
-| âž musclesTargeted : string\[\] (ex: \["quadriceps", "pectoraux", "cardio"\]) |
-| âž description : Instruction courte (max 80 car.) â affichĂ©e pendant la phase Rest |
-| âž cues : string\[\] â 2-3 points clĂ©s de forme (ex: "Dos droit", "Genoux alignĂ©s") |
-| âž gifUrl : Animation GIF courte (1-2s, loop) â 200Ă200px max â embarquĂ©e offline |
-| âž thumbnailUrl : Image statique pour la bibliothĂšque |
-| âž hasModification : bool â si une variante plus facile existe |
-| âž modificationId : id de l'exercice de remplacement (ex: "burpee\_modified") |
-| ➠equipmentNeeded : string\[\] (ex: \[\] pour aucun, \["tapis"\] pour matériel simple) |
-
-### **2.3.3 Affichage de l'Exercice Pendant la Séance**
-
-| Phase | Affichage exercice | Taille | Information complémentaire |
-| :---- | :---- | :---- | :---- |
-| GET READY (10s) | Nom \+ GIF animĂ© de dĂ©monstration | Grande â focus total | "Voici l'exercice suivant" â prĂ©pare mentalement |
-| WORK (20s) | Nom en haut \+ compteur central â GIF en petit coin | Nom moyen, timer dominant | 1-2 cues de forme affichĂ©s sous le nom |
-| REST (10s) | Nom de l'EXERCICE SUIVANT \+ vignette | Taille moyenne | "Prochain : \[Nom\]" â anticipation |
-| FIN DE ROUND | Résumé rapide du round (1s) | Plein écran flash | Round X complété \+ prochain exercice |
-
-### **2.3.4 Types de Programmes Tabata**
-
-| đïž PROGRAMMES DISPONIBLES â V1 |
-| :---- |
-| âž MODE 1 â Exercice unique rĂ©pĂ©tĂ© : Le mĂȘme exercice sur les 8 rounds (ex: 8 rounds de Burpees) â Tabata classique |
-| âž MODE 2 â Circuit 2 exercices : Alternance A/B sur 8 rounds (ex: Squats / Push-ups Ă 4 rĂ©pĂ©titions) â Tabata duo |
-| âž MODE 3 â Circuit 4 exercices : 4 exercices Ă 2 rounds chacun â Tabata circuit (Premium) |
-| âž MODE 4 â Programme libre : L'utilisateur assigne manuellement un exercice Ă chaque round (Premium) |
-| âž MODE 5 â Programme IA : SĂ©lection automatique selon niveau, objectif et historique (Premium V2) |
-
-### **2.3.5 Créateur de Programme Personnalisé (Premium)**
-
-* Interface drag & drop pour assigner les exercices Ă chaque round
-
-* Sauvegarde illimitée de programmes personnalisés (nom, description, tags)
-
-* Partage de programme par lien deep link (ex: tabatago://program/abc123)
-
-* Import de programme depuis un lien partagé par un autre utilisateur
-
-* Favoris : marquer des exercices pour les retrouver rapidement
-
-### **2.3.6 Contenu Offline & Performance**
-
-* Tous les GIFs d'exercices V1 (38 exercices Ă 1 GIF â 150KB chacun â \~6MB total) â embarquĂ©s dans le bundle
-
-* Lazy loading pour les exercices V2+ : téléchargement à la demande, mis en cache localement
-
-* Fallback : Si pas de GIF disponible, afficher une icĂŽne \+ description textuelle
-
-* Accessibilité : alt text sur chaque GIF pour VoiceOver/TalkBack
-
-## **2.4 Synchronisation Timer Ă Musique Ă Exercice**
-
-Les trois composants doivent former une expérience unifiée et cohérente. Ce tableau décrit les événements et leurs effets croisés :
-
-| ĂvĂ©nement Timer | Effet sur la Musique | Effet sur l'Exercice | Haptique |
-| :---- | :---- | :---- | :---- |
-| Début GET READY | Fade-in track principale | Afficher GIF exercice Round 1 | Léger |
-| Décompte 3-2-1 | Volume augmente | Animation pulsation sur le GIF | Bip à 3 |
-| Début WORK | Switch vers track high BPM | Afficher nom \+ cues en grand | Fort |
-| Milieu WORK (10s) | Rien | "Halfway\!" en overlay 1s | Aucun |
-| Fin WORK | Switch vers track low BPM | Afficher exercice SUIVANT | Moyen |
-| Début REST | Track calme | "Next: \[Exercice\]" \+ vignette | Léger |
-| Dernier round warning | Effet sonore spécial | Badge "LAST ROUND" sur nom | Vibration longue |
-| Fin de sĂ©ance | Fade-out \+ fanfare | Ăcran rĂ©sultat avec tous les exercices | CĂ©lĂ©bration |
-
-# **3\. Validation du Marché & Stratégie ASO**
-
-## **3.1 Analyse des Mots-Clés Cibles**
-
-Les mots-clés suivants ont été validés via Astro (difficulté \< 55, popularité \> 20\) :
-
-| Mot-clé | Popularité | Difficulté | Marché |
-| :---- | :---- | :---- | :---- |
-| tabata timer | 72 | 48 | EN đșđž |
-| hiit timer app | 68 | 52 | EN đșđž |
-| minuterie tabata | 35 | 28 | FR đ«đ· |
-| temporizador tabata | 41 | 31 | ES đȘđž |
-| tabata training timer | 55 | 44 | EN đŹđ§ |
-| intervall timer workout | 38 | 29 | DE đ©đȘ |
-
-## **3.2 Analyse Concurrentielle**
-
-Principaux concurrents identifiés et opportunités de différenciation :
-
-| Concurrent | Forces | Faiblesses (notre opportunité) |
-| :---- | :---- | :---- |
-| Tabata Timer (App Store Top 1\) | Notoriété, simplicité | UI datée, pas de suivi streak, pas de localisation |
-| Seconds Pro | TrÚs complet, flexible | Trop complexe, prix élevé, courbe d'apprentissage |
-| HIIT Interval Training Timer | Gratuit, fonctionnel | Pub intrusive, pas d'onboarding émotionnel |
-| Tabata Stopwatch Pro | Simple, rapide | Pas de personnalisation, no streak, no widget |
-
-Notre avantage : onboarding émotionnel fort \+ design moderne \+ widgets iOS/Android \+ streaks \+ localisation dans 5 langues.
-
-# **4\. Onboarding â La SĂ©quence Critique**
-
-L'onboarding est la prioritĂ© absolue : 80% des revenus sont gĂ©nĂ©rĂ©s ici. L'objectif est de crĂ©er un investissement Ă©motionnel avant d'afficher le paywall. La sĂ©quence suit le schĂ©ma : ProblĂšme â Empathie â Solution â Moment Wow â Paywall.
-
-## **4.1 Ăcrans d'Onboarding (sĂ©quence de 6 Ă©crans)**
-
-### **Ăcran 1 â Le ProblĂšme (Identifier la douleur)**
-
-| đŻ OBJECTIF : Identification |
-| :---- |
-| âž Titre : "Tu n'as pas 1 heure pour la salle. Personne n'en a." |
-| âž Sous-titre : "Et pourtant tu veux progresser. On a la solution." |
-| âž Visuel : Animation subtile d'une horloge qui se fragmente puis se reconstruit en 20 min |
-| âž CTA : "Montre-moi comment" (bouton orange pleine largeur) |
-
-### **Ăcran 2 â L'Empathie (L'utilisateur se sent compris)**
-
-| đŹ OBJECTIF : Connexion Ă©motionnelle |
-| :---- |
-| âž Titre : "Qu'est-ce qui t'empĂȘche de t'entraĂźner ?" |
-| âž Choix interactifs (tap) : Manque de temps / Motivation en berne / Je ne sais pas quoi faire / Je n'ai pas accĂšs Ă une salle |
-| âž MĂ©canisme : stocker la rĂ©ponse â personnaliser le reste de l'onboarding |
-| ➠Transition : "On a conçu TabataGo exactement pour ça." |
-
-### **Ăcran 3 â La Solution (PrĂ©senter la mĂ©thode)**
-
-| ⥠OBJECTIF : Comprendre la valeur |
-| :---- |
-| âž Titre : "4 minutes. Vraiment transformatrices." |
-| âž Animation interactive : timeline Tabata (20s work / 10s rest Ă 8 rounds) |
-| ➠Stats affichées : Brûle autant de calories qu'un jogging de 30 min / Prouvé scientifiquement depuis 1996 (Dr. Tabata) |
-| âž Visuel : compteur animĂ© qui tourne â preview de l'app |
-
-### **Ăcran 4 â Le Moment "Wow" (DĂ©mo interactive)**
-
-| đ„ OBJECTIF : Engagement actif (ne pas juste regarder) |
-| :---- |
-| âž Titre : "Essaie maintenant. 20 secondes." |
-| ➠Mini-minuterie Tabata LIVE intégrée dans l'écran d'onboarding |
-| âž L'utilisateur tape sur "Go" et vit 20s de compte Ă rebours \+ son \+ vibration |
-| ➠AprÚs : "Tu viens de faire ta premiÚre série Tabata. 7 de plus et c'est une séance complÚte." |
-| âž Note : Ce moment est le plus diffĂ©renciant â crĂ©er une mini-expĂ©rience rĂ©elle |
-
-### **Ăcran 5 â Personnalisation (Engagement supplĂ©mentaire)**
-
-| âïž OBJECTIF : Investissement personnel |
-| :---- |
-| âž Titre : "Configurons ta premiĂšre semaine." |
-| ➠Sélection : Niveau (Débutant / Intermédiaire / Avancé) |
-| âž SĂ©lection : Objectif (Perte de poids / Cardio / Force / Bien-ĂȘtre) |
-| ➠Sélection : Fréquence souhaitée (2x / 3x / 5x par semaine) |
-| âž RĂ©sultat : "Ton programme personnalisĂ© est prĂȘt." (sensation de valeur avant paiement) |
-
-### **Ăcran 6 â Paywall (AprĂšs l'investissement Ă©motionnel)**
-
-| đł OBJECTIF : Conversion |
-| :---- |
-| ➠Titre : "Continue sur ta lancée. Sans limite." |
-| ➠Présenter l'essai gratuit 7 jours en premier (bouton principal orange) |
-| ➠Options : Mensuel $4.99 / Annuel $29.99 (économie 50% mise en évidence) |
-| ➠Garantie visible : "Annule à tout moment" \+ "Satisfait ou remboursé 30j" |
-| âž Lien "Continuer sans abonnement" en petit en bas (ne pas le cacher) |
-| âž IntĂ©gration RevenueCat â avec A/B test activĂ© dĂšs le lancement |
-
-# **5\. Design & Identité Visuelle**
-
-## **5.1 Palette de Couleurs**
-
-| RĂŽle | Couleur | Hex | Usage |
-| :---- | :---- | :---- | :---- |
-| Primaire / Action | Orange Tabata | \#F97316 | Boutons CTA, accents, timer actif |
-| Fond Dark (défaut) | Charcoal Night | \#1E1E2E | Background principal mode sombre |
-| Fond Light | Warm White | \#FFF7ED | Background mode clair |
-| Texte principal Dark | Stone 900 | \#1C1917 | Titres mode clair |
-| Texte secondaire | Stone 600 | \#57534E | Body text, descriptions |
-| SuccÚs / Streak | Green 500 | \#22C55E | Streaks, complétion, feedback positif |
-| Danger / Alerte | Red 500 | \#EF4444 | Erreurs, derniers secondes |
-
-## **5.2 Typographie**
-
-| Usage | Police | Poids | Taille |
-| :---- | :---- | :---- | :---- |
-| Timer principal | Inter (monospace fallback) | Black (900) | 96-120px |
-| Titres H1 | Inter | Bold (700) | 28-32px |
-| Titres H2 | Inter | SemiBold (600) | 22-24px |
-| Body / Labels | Inter | Regular (400) | 14-16px |
-| Micro-labels | Inter | Medium (500) | 11-12px |
-
-## **5.3 Principes Design**
-
-* Mode sombre par défaut (immersif pendant l'entraßnement)
-
-* Timer en plein Ă©cran pendant la sĂ©ance â aucune distraction
-
-* Animations fluides (60fps) pour les transitions et le compteur
-
-* Haptiques natifs : vibration légÚre à chaque changement de phase
-
-* Support Dynamic Type (iOS) et font scaling (Android)
-
-* IcÎne app : fond noir, lettre T stylisée en orange avec une flamme
-
-# **6\. Architecture Technique & Développement (Expo)**
-
-## **6.1 Stack Technique**
-
-| Composant | Solution choisie | Justification |
-| :---- | :---- | :---- |
-| Framework | Expo SDK 52 (React Native) | Cross-platform, OTA updates, accĂšs natif facile |
-| Navigation | Expo Router v3 (file-based) | Standard moderne, deep linking natif |
-| State management | Zustand \+ AsyncStorage | Léger, performant, persistance simple |
-| Timer engine | expo-background-fetch \+ useInterval custom | Précision \+ exécution background |
-| Audio | expo-av | Sons de décompte et alertes phase |
-| Haptiques | expo-haptics | Retour tactile natif iOS/Android |
-| Notifications | expo-notifications | Rappels d'entraĂźnement \+ streaks |
-| Widget | react-native-widget-extension | Widget iOS 14+ (Live Activity) |
-| Paiements | react-native-purchases (RevenueCat) | Abonnements \+ A/B testing \+ analytics |
-| Analytics | PostHog (react-native-posthog) | Funnel tracking, drop-off analysis |
-| Storage | expo-secure-store \+ AsyncStorage | Données utilisateur \+ préférences |
-| In-App Review | expo-store-review | Prompt aprĂšs streak 7 jours |
-
-## **6.2 Features â MoSCoW Priorisation**
-
-### **Must Have (V1 â Lancement)**
-
-| â
MUST HAVE â Indispensables au lancement |
-| :---- |
-| âž Timer Tabata complet â voir Section 2.1 (toutes phases, sons, haptiques, background) |
-| âž Musique intĂ©grĂ©e â voir Section 2.2 (3 ambiances Ă 3 intensitĂ©s, signaux de phase, coexistence audio) |
-| âž Exercices avec GIFs â voir Section 2.3 (38 exercices, modes 1 et 2, affichage GET READY/WORK/REST) |
-| âž Synchronisation Timer Ă Musique Ă Exercice â voir Section 2.4 |
-| ➠Modes : Tabata classique, HIIT personnalisé, Pause active |
-| ➠Affichage plein écran pendant la séance (mode portrait \+ paysage) |
-| ➠Historique des séances (date, durée, rounds complétés) |
-| ➠SystÚme de Streak (consécutivité quotidienne, animation de feu) |
-| âž Notifications de rappel configurables |
-| ➠Onboarding 6 écrans avec mini-démo live |
-| âž Paywall \+ RevenueCat (essai 7j, mensuel, annuel) |
-| âž Dark mode \+ Light mode |
-| âž Localisation : EN, FR, ES, DE, PT |
-
-### **Should Have (V1.1 â Semaine 4-8)**
-
-| đĄ SHOULD HAVE â Valeur ajoutĂ©e forte |
-| :---- |
-| âž Widget iOS (Home Screen) â affiche le streak et le dernier entraĂźnement |
-| âž Widget Android (Glance API) |
-| ➠BibliothÚque de programmes pré-définis (Débutant / Cardio / Force) |
-| âž Statistiques hebdomadaires et mensuelles avec graphiques |
-| ➠Sons personnalisés (voix, bip, musique d'ambiance) |
-| ➠Integration Apple Health / Google Fit (calories, activité) |
-| ➠Partage social (carte récapitulative de la séance) |
-
-### **Could Have (V2 â Mois 3+)**
-
-| đ” COULD HAVE â DiffĂ©renciation long terme |
-| :---- |
-| ➠Mode Coach IA : suggestions de séances basées sur l'historique |
-| âž Challenges communautaires (leaderboard hebdomadaire) |
-| âž Apple Watch companion app |
-| âž Import/Export de programmes (partage entre utilisateurs) |
-| ➠Mode TV / AirPlay pour entraßnement sur grand écran |
-
-# **7\. Structure de Navigation (Expo Router)**
-
-Architecture file-based avec Expo Router v3 :
-
-| Route | Ăcran | AccĂšs |
-| :---- | :---- | :---- |
-| / | Home â Hub central avec raccourcis et streak | Tous |
-| /onboarding | Séquence onboarding 6 étapes | Nouveaux utilisateurs |
-| /timer | Minuterie plein Ă©cran â sĂ©ance active | Tous |
-| /programs | BibliothĂšque de programmes | Premium |
-| /history | Historique des séances \+ stats | Tous (limité free) |
-| /settings | Préférences, sons, notifications, compte | Tous |
-| /paywall | Ăcran d'abonnement RevenueCat | Free users |
-| /(modals)/review | In-App Review prompt | Streak 7j |
-
-# **8\. Paiements, Pricing & RevenueCat**
-
-## **8.1 Structure des Offres**
-
-| Offre | Prix US | Prix FR | Prix BR | Contenu |
-| :---- | :---- | :---- | :---- | :---- |
-| Gratuit | Free | Free | Free | Timer standard, 7j historique, pas de widget |
-| Essai Premium | 7 jours gratuits | 7 jours gratuits | 7 jours gratuits | AccĂšs complet, pas de CB requise si possible |
-| Premium Mensuel | $4.99/mois | âŹ4.99/mois | R$9.99/mois | AccĂšs complet illimitĂ© |
-| Premium Annuel | $29.99/an | âŹ27.99/an | R$59.99/an | Tout Premium â Ă©conomie 50% mise en avant |
-
-Note : Pricing localisé via RevenueCat Purchasing Power Parity (PPP). Adapter automatiquement les prix pour le Brésil, l'Inde, l'Indonésie et l'Europe de l'Est pour maximiser les conversions mondiales.
-
-## **8.2 Configuration RevenueCat**
-
-* Entitlements : "premium" (accĂšs Ă toutes les features payantes)
-
-* Products : tabatago\_monthly, tabatago\_annual, tabatago\_trial\_7d
-
-* A/B Tests dĂšs J+7 : tester $3.99 vs $4.99 vs $6.99 mensuel
-
-* A/B Tests paywall : liste vs comparaison de plans vs "best value" badge
-
-* Webhook RevenueCat â PostHog pour corrĂ©ler revenus et comportement
-
-# **9\. Analytics & Instrumentation (PostHog)**
-
-## **9.1 ĂvĂ©nements Critiques Ă Tracker**
-
-| ĂvĂ©nement | PropriĂ©tĂ©s | Objectif |
-| :---- | :---- | :---- |
-| onboarding\_step\_viewed | step\_number, step\_name | Identifier le drop-off |
-| onboarding\_demo\_started | duration\_seconds | Mesurer l'engagement Wow |
-| onboarding\_completed | persona, goal, frequency | Segmentation |
-| paywall\_viewed | source, variant | Funnel paiement |
-| trial\_started | plan\_selected | Conversion essai |
-| subscription\_purchased | plan, price, currency | Revenue |
-| session\_started | program\_id, rounds, duration | Engagement produit |
-| session\_completed | rounds\_done, streak\_day | Rétention |
-| streak\_milestone | days\_count | Gamification KPIs |
-| widget\_added | widget\_type | Stickiness |
-| notification\_tapped | notification\_type | Rappels efficacité |
-
-## **9.2 Funnels Ă Monitorer**
-
-* Funnel Acquisition : Impression App Store â TĂ©lĂ©chargement â Onboarding Start â Onboarding Complete â Paywall View â Trial Start â Purchase
-
-* Funnel Rétention : J+1 / J+7 / J+30 ouverture de l'app aprÚs téléchargement
-
-* Funnel Engagement : SĂ©ance dĂ©marrĂ©e â SĂ©ance complĂ©tĂ©e â Streak maintenu
-
-RĂšgle d'or : Si impressions mais pas de tĂ©lĂ©chargements â RĂ©parer screenshots/icĂŽne. Si tĂ©lĂ©chargements mais pas d'essais â RĂ©parer onboarding. Si essais mais pas de paiements â RĂ©parer paywall/pricing.
-
-# **10\. Localisation & Growth Hacking**
-
-## **10.1 Stratégie de Localisation (5 Langues)**
-
-| Langue | Marché cible | Mots-clés spécifiques | Priorité |
-| :---- | :---- | :---- | :---- |
-| Anglais (EN) | US, UK, AU, CA | tabata timer, hiit timer app | 1 â Lancement |
-| Français (FR) | France, Belgique, QuĂ©bec | minuterie tabata, chrono hiit | 1 â Lancement |
-| Espagnol (ES) | Espagne, Mexique, Argentine | temporizador tabata, ejercicio hiit | 2 â Semaine 2 |
-| Allemand (DE) | Allemagne, Autriche, CH | tabata timer app, intervall training | 2 â Semaine 2 |
-| Portugais (PT-BR) | BrĂ©sil | timer tabata, treino hiit | 3 â Mois 2 |
-
-## **10.2 ĂlĂ©ments Ă Localiser**
-
-* App content : tous les textes UI, onboarding, notifications, erreurs
-
-* App Store listing : titre, sous-titre, description courte, description longue
-
-* Screenshots : localisés avec texte natif (utiliser Fastlane Frameit)
-
-* Prix : PPP via RevenueCat (automatique par pays)
-
-* Mots-clés App Store : 100 caractÚres spécifiques par marché
-
-Outil recommandé : i18next \+ expo-localization pour la gestion des traductions en développement.
-
-# **11\. App Store Optimization (ASO)**
-
-## **11.1 Fiche App Store (iOS â EN)**
-
-| Champ | Valeur optimisée |
-| :---- | :---- |
-| Nom de l'app (30 car.) | TabataGo â HIIT Timer |
-| Sous-titre (30 car.) | Tabata & Interval Workout |
-| Mots-clés (100 car.) | tabata,hiit,timer,interval,workout,fitness,training,countdown,sport,exercise |
-| Description (1Ăšre ligne) | The most motivating Tabata timer. 20 seconds of effort. Life-changing results. |
-| Screenshots (10 max) | Voir section 10.2 |
-| Preview video | 15-30s montrant la minuterie en action \+ streak \+ widget |
-
-## **11.2 Plan des Screenshots (6 obligatoires)**
-
-| \# | Contenu | Message clé | Background |
-| :---- | :---- | :---- | :---- |
-| 1 | Timer plein écran en action (20s) | "Train smarter, not longer" | Dark \+ orange |
-| 2 | Ăcran d'onboarding moment Wow | "Your first Tabata in 20 seconds" | Gradient sombre |
-| 3 | Widget iOS home screen | "Train from anywhere â even your lock screen" | iPhone mockup |
-| 4 | Historique \+ Streak en feu | "Build the habit. Keep the streak." | Dark \+ vert |
-| 5 | BibliothĂšque de programmes | "100+ ready-to-go programs" | Gradient |
-| 6 | Statistiques mensuelles | "See your progress. Stay motivated." | Fond clair |
-
-# **12\. Plan de Lancement en 72h**
-
-## **12.1 Timeline de Développement (72 heures)**
-
-| Phase | Durée | Livrables | Responsable |
-| :---- | :---- | :---- | :---- |
-| Setup & Architecture | H0 â H8 | Expo project init, navigation, design system, RevenueCat config | Dev Lead |
-| Onboarding | H8 â H20 | 6 Ă©crans onboarding \+ mini-timer dĂ©mo \+ animation | Dev \+ Design |
-| Core Timer | H20 â H36 | Minuterie plein Ă©cran, son, haptiques, background timer | Dev |
-| Streak \+ Historique | H36 â H48 | SystĂšme streak, storage, affichage historique, notifications | Dev |
-| Paywall \+ Analytics | H48 â H58 | RevenueCat paywall, PostHog events, A/B test config | Dev |
-| Polish & Testing | H58 â H68 | Bug fixes, dark/light mode, performance, edge cases | Dev \+ QA |
-| Submission | H68 â H72 | Screenshots, App Store listing (5 langues), soumission Apple | PM \+ Dev |
-
-## **12.2 Stratégie de Validation Post-Lancement**
-
-Profiter du boost de visibilité des 3 premiers jours accordé par l'App Store aux nouvelles applications :
-
-1. Jour 1-3 : Monitoring organique â Analyser impressions, tĂ©lĂ©chargements, et premiers essais
-
-2. Jour 4-7 : Analyse funnel â Identifier le premier point de friction (onboarding? paywall?)
-
-3. Jour 8-14 : PremiĂšre itĂ©ration â Corriger le problĂšme prioritaire et soumettre mise Ă jour OTA (Expo)
-
-4. Jour 15-30 : Activation publicitĂ© â Apple Search Ads seulement si CVR organique \> 15%
-
-5. Jour 30+ : Scale â Augmenter budget pub sur les marchĂ©s qui convertissent le mieux
-
-RÚgle absolue : Ne jamais dépenser en publicité avant d'avoir validé que le funnel organique convertit.
-
-# **13\. KPIs & Métriques de SuccÚs**
-
-| Métrique | Semaine 1 | Mois 1 | Mois 3 | Outil de mesure |
-| :---- | :---- | :---- | :---- | :---- |
-| Téléchargements | 500+ | 5 000+ | 20 000+ | App Store Connect |
-| Taux conversion ImpressionâDL | \> 5% | \> 8% | \> 10% | App Store Connect |
-| Taux essai démarré | \> 25% | \> 30% | \> 35% | RevenueCat \+ PostHog |
-| Taux conversion essaiâpayant | \> 30% | \> 35% | \> 40% | RevenueCat |
-| Rétention J+7 | \> 30% | \> 35% | \> 40% | PostHog |
-| RĂ©tention J+30 | â | \> 20% | \> 25% | PostHog |
-| MRR | â | $2 000+ | $8 000+ | RevenueCat |
-| Note App Store | â | â„ 4.5 | â„ 4.6 | App Store Connect |
-| Streak moyen (actifs) | â | \> 5 jours | \> 10 jours | PostHog custom |
-
-# **14\. Risques & Mitigations**
-
-| Risque | Probabilité | Impact | Mitigation |
-| :---- | :---- | :---- | :---- |
-| Refus App Store (guidelines) | Moyen | ĂlevĂ© | Respecter HIG Apple, tester paywall sur TestFlight, pas de dark patterns |
-| Mauvais taux de conversion paywall | Moyen | ĂlevĂ© | A/B test RevenueCat dĂšs J+7, 3 variantes de prix/layout |
-| PrĂ©cision du timer en background | Faible | ĂlevĂ© | expo-background-fetch \+ notification locale comme fallback |
-| Faible rétention sans streaks | Moyen | Moyen | Streak J+1 core feature, reminder push personnalisé par heure d'entraßnement |
-| Concurrence sur mots-clĂ©s EN | ĂlevĂ© | Moyen | AgressivitĂ© sur marchĂ©s FR/ES/DE dĂšs J+14 |
-| Revue Apple lente (\> 48h) | Faible | Faible | Soumettre J-5 avant objectif lancement, utiliser Expo OTA pour hotfixes |
-
-# **15\. Approbations & Versions**
-
-| RĂŽle | Nom | Date | Signature |
-| :---- | :---- | :---- | :---- |
-| Product Owner | | Février 2026 | |
-| Tech Lead | | Février 2026 | |
-| Design Lead | | Février 2026 | |
-| Marketing | | Février 2026 | |
-
-*Document créé le 15 fĂ©vrier 2026 â TabataGo PRD v1.1*
\ No newline at end of file
diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx
deleted file mode 100644
index 71518f9..0000000
--- a/app/(tabs)/explore.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { Image } from 'expo-image';
-import { Platform, StyleSheet } from 'react-native';
-
-import { Collapsible } from '@/components/ui/collapsible';
-import { ExternalLink } from '@/components/external-link';
-import ParallaxScrollView from '@/components/parallax-scroll-view';
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-import { IconSymbol } from '@/components/ui/icon-symbol';
-import { Fonts } from '@/constants/theme';
-
-export default function TabTwoScreen() {
- return (
-
- }>
-
-
- Explore
-
-
- This app includes example code to help you get started.
-
-
- This app has two screens:{' '}
- app/(tabs)/index.tsx and{' '}
- app/(tabs)/explore.tsx
-
-
- The layout file in app/(tabs)/_layout.tsx{' '}
- sets up the tab navigator.
-
-
- Learn more
-
-
-
-
- You can open this project on Android, iOS, and the web. To open the web version, press{' '}
- w in the terminal running this project.
-
-
-
-
- For static images, you can use the @2x and{' '}
- @3x suffixes to provide files for
- different screen densities
-
-
-
- Learn more
-
-
-
-
- This template has light and dark mode support. The{' '}
- useColorScheme() hook lets you inspect
- what the user's current color scheme is, and so you can adjust UI colors accordingly.
-
-
- Learn more
-
-
-
-
- This template includes an example of an animated component. The{' '}
- components/HelloWave.tsx component uses
- the powerful{' '}
-
- react-native-reanimated
- {' '}
- library to create a waving hand animation.
-
- {Platform.select({
- ios: (
-
- The components/ParallaxScrollView.tsx{' '}
- component provides a parallax effect for the header image.
-
- ),
- })}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- headerImage: {
- color: '#808080',
- bottom: -90,
- left: -35,
- position: 'absolute',
- },
- titleContainer: {
- flexDirection: 'row',
- gap: 8,
- },
-});
diff --git a/app/modal.tsx b/app/modal.tsx
deleted file mode 100644
index 6dfbc1a..0000000
--- a/app/modal.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Link } from 'expo-router';
-import { StyleSheet } from 'react-native';
-
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-
-export default function ModalScreen() {
- return (
-
- This is a modal
-
- Go to home screen
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- padding: 20,
- },
- link: {
- marginTop: 15,
- paddingVertical: 15,
- },
-});
diff --git a/app/onboarding/index.tsx b/app/onboarding/index.tsx
deleted file mode 100644
index 7cee91c..0000000
--- a/app/onboarding/index.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useRouter, Redirect } from 'expo-router'
-import { Screen1Problem } from '@/src/features/onboarding/screens/Screen1Problem'
-import { Screen2Empathy } from '@/src/features/onboarding/screens/Screen2Empathy'
-import { Screen3Solution } from '@/src/features/onboarding/screens/Screen3Solution'
-import { Screen4WowMoment } from '@/src/features/onboarding/screens/Screen4WowMoment'
-import { Screen5Personalization } from '@/src/features/onboarding/screens/Screen5Personalization'
-import { Screen6Paywall } from '@/src/features/onboarding/screens/Screen6Paywall'
-import { useOnboarding } from '@/src/features/onboarding/hooks/useOnboarding'
-
-export default function OnboardingRouter() {
- const router = useRouter()
- const currentStep = useOnboarding((state) => state.currentStep)
- const isOnboardingComplete = useOnboarding((state) => state.isOnboardingComplete)
- const nextStep = useOnboarding((state) => state.nextStep)
- const completeOnboarding = useOnboarding((state) => state.completeOnboarding)
-
- const handleNext = () => {
- nextStep()
- }
-
- const handleComplete = () => {
- completeOnboarding()
- router.replace('/(tabs)')
- }
-
- // Redirect to tabs if onboarding is already complete
- if (isOnboardingComplete) {
- return
- }
-
- // Render the correct screen based on current step
- switch (currentStep) {
- case 0:
- return
- case 1:
- return
- case 2:
- return
- case 3:
- return
- case 4:
- return
- case 5:
- return
- default:
- // Fallback to first screen if step is out of bounds
- return
- }
-}
diff --git a/app/timer.tsx b/app/timer.tsx
deleted file mode 100644
index 771d2e1..0000000
--- a/app/timer.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-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()
- }
-
- function handleStop() {
- timer.stop()
- audio.stopMusic(300)
- router.back()
- }
-
- return (
-
- )
-}
diff --git a/components/external-link.tsx b/components/external-link.tsx
deleted file mode 100644
index 883e515..0000000
--- a/components/external-link.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Href, Link } from 'expo-router';
-import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
-import { type ComponentProps } from 'react';
-
-type Props = Omit, 'href'> & { href: Href & string };
-
-export function ExternalLink({ href, ...rest }: Props) {
- return (
- {
- if (process.env.EXPO_OS !== 'web') {
- // Prevent the default behavior of linking to the default browser on native.
- event.preventDefault();
- // Open the link in an in-app browser.
- await openBrowserAsync(href, {
- presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
- });
- }
- }}
- />
- );
-}
diff --git a/components/haptic-tab.tsx b/components/haptic-tab.tsx
deleted file mode 100644
index 7f3981c..0000000
--- a/components/haptic-tab.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
-import { PlatformPressable } from '@react-navigation/elements';
-import * as Haptics from 'expo-haptics';
-
-export function HapticTab(props: BottomTabBarButtonProps) {
- return (
- {
- if (process.env.EXPO_OS === 'ios') {
- // Add a soft haptic feedback when pressing down on the tabs.
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- }
- props.onPressIn?.(ev);
- }}
- />
- );
-}
diff --git a/components/hello-wave.tsx b/components/hello-wave.tsx
deleted file mode 100644
index 5def547..0000000
--- a/components/hello-wave.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import Animated from 'react-native-reanimated';
-
-export function HelloWave() {
- return (
-
- đ
-
- );
-}
diff --git a/components/parallax-scroll-view.tsx b/components/parallax-scroll-view.tsx
deleted file mode 100644
index 6f674a7..0000000
--- a/components/parallax-scroll-view.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { PropsWithChildren, ReactElement } from 'react';
-import { StyleSheet } from 'react-native';
-import Animated, {
- interpolate,
- useAnimatedRef,
- useAnimatedStyle,
- useScrollOffset,
-} from 'react-native-reanimated';
-
-import { ThemedView } from '@/components/themed-view';
-import { useColorScheme } from '@/hooks/use-color-scheme';
-import { useThemeColor } from '@/hooks/use-theme-color';
-
-const HEADER_HEIGHT = 250;
-
-type Props = PropsWithChildren<{
- headerImage: ReactElement;
- headerBackgroundColor: { dark: string; light: string };
-}>;
-
-export default function ParallaxScrollView({
- children,
- headerImage,
- headerBackgroundColor,
-}: Props) {
- const backgroundColor = useThemeColor({}, 'background');
- const colorScheme = useColorScheme() ?? 'light';
- const scrollRef = useAnimatedRef();
- const scrollOffset = useScrollOffset(scrollRef);
- const headerAnimatedStyle = useAnimatedStyle(() => {
- return {
- transform: [
- {
- translateY: interpolate(
- scrollOffset.value,
- [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
- [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
- ),
- },
- {
- scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
- },
- ],
- };
- });
-
- return (
-
-
- {headerImage}
-
- {children}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- header: {
- height: HEADER_HEIGHT,
- overflow: 'hidden',
- },
- content: {
- flex: 1,
- padding: 32,
- gap: 16,
- overflow: 'hidden',
- },
-});
diff --git a/components/themed-text.tsx b/components/themed-text.tsx
deleted file mode 100644
index d79d0a1..0000000
--- a/components/themed-text.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { StyleSheet, Text, type TextProps } from 'react-native';
-
-import { useThemeColor } from '@/hooks/use-theme-color';
-
-export type ThemedTextProps = TextProps & {
- lightColor?: string;
- darkColor?: string;
- type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
-};
-
-export function ThemedText({
- style,
- lightColor,
- darkColor,
- type = 'default',
- ...rest
-}: ThemedTextProps) {
- const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
-
- return (
-
- );
-}
-
-const styles = StyleSheet.create({
- default: {
- fontSize: 16,
- lineHeight: 24,
- },
- defaultSemiBold: {
- fontSize: 16,
- lineHeight: 24,
- fontWeight: '600',
- },
- title: {
- fontSize: 32,
- fontWeight: 'bold',
- lineHeight: 32,
- },
- subtitle: {
- fontSize: 20,
- fontWeight: 'bold',
- },
- link: {
- lineHeight: 30,
- fontSize: 16,
- color: '#0a7ea4',
- },
-});
diff --git a/components/themed-view.tsx b/components/themed-view.tsx
deleted file mode 100644
index 6f181d8..0000000
--- a/components/themed-view.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { View, type ViewProps } from 'react-native';
-
-import { useThemeColor } from '@/hooks/use-theme-color';
-
-export type ThemedViewProps = ViewProps & {
- lightColor?: string;
- darkColor?: string;
-};
-
-export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
- const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
-
- return ;
-}
diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx
deleted file mode 100644
index 6345fde..0000000
--- a/components/ui/collapsible.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { PropsWithChildren, useState } from 'react';
-import { StyleSheet, TouchableOpacity } from 'react-native';
-
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-import { IconSymbol } from '@/components/ui/icon-symbol';
-import { Colors } from '@/constants/theme';
-import { useColorScheme } from '@/hooks/use-color-scheme';
-
-export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
- const [isOpen, setIsOpen] = useState(false);
- const theme = useColorScheme() ?? 'light';
-
- return (
-
- setIsOpen((value) => !value)}
- activeOpacity={0.8}>
-
-
- {title}
-
- {isOpen && {children}}
-
- );
-}
-
-const styles = StyleSheet.create({
- heading: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- },
- content: {
- marginTop: 6,
- marginLeft: 24,
- },
-});
diff --git a/components/ui/icon-symbol.ios.tsx b/components/ui/icon-symbol.ios.tsx
deleted file mode 100644
index 9177f4d..0000000
--- a/components/ui/icon-symbol.ios.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
-import { StyleProp, ViewStyle } from 'react-native';
-
-export function IconSymbol({
- name,
- size = 24,
- color,
- style,
- weight = 'regular',
-}: {
- name: SymbolViewProps['name'];
- size?: number;
- color: string;
- style?: StyleProp;
- weight?: SymbolWeight;
-}) {
- return (
-
- );
-}
diff --git a/components/ui/icon-symbol.tsx b/components/ui/icon-symbol.tsx
deleted file mode 100644
index b7ece6b..0000000
--- a/components/ui/icon-symbol.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// Fallback for using MaterialIcons on Android and web.
-
-import MaterialIcons from '@expo/vector-icons/MaterialIcons';
-import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
-import { ComponentProps } from 'react';
-import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
-
-type IconMapping = Record['name']>;
-type IconSymbolName = keyof typeof MAPPING;
-
-/**
- * Add your SF Symbols to Material Icons mappings here.
- * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
- * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
- */
-const MAPPING = {
- 'house.fill': 'home',
- 'paperplane.fill': 'send',
- 'chevron.left.forwardslash.chevron.right': 'code',
- 'chevron.right': 'chevron-right',
-} as IconMapping;
-
-/**
- * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
- * This ensures a consistent look across platforms, and optimal resource usage.
- * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
- */
-export function IconSymbol({
- name,
- size = 24,
- color,
- style,
-}: {
- name: IconSymbolName;
- size?: number;
- color: string | OpaqueColorValue;
- style?: StyleProp;
- weight?: SymbolWeight;
-}) {
- return ;
-}
diff --git a/constants/theme.ts b/constants/theme.ts
deleted file mode 100644
index f06facd..0000000
--- a/constants/theme.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
- * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
- */
-
-import { Platform } from 'react-native';
-
-const tintColorLight = '#0a7ea4';
-const tintColorDark = '#fff';
-
-export const Colors = {
- light: {
- text: '#11181C',
- background: '#fff',
- tint: tintColorLight,
- icon: '#687076',
- tabIconDefault: '#687076',
- tabIconSelected: tintColorLight,
- },
- dark: {
- text: '#ECEDEE',
- background: '#151718',
- tint: tintColorDark,
- icon: '#9BA1A6',
- tabIconDefault: '#9BA1A6',
- tabIconSelected: tintColorDark,
- },
-};
-
-export const Fonts = Platform.select({
- ios: {
- /** iOS `UIFontDescriptorSystemDesignDefault` */
- sans: 'system-ui',
- /** iOS `UIFontDescriptorSystemDesignSerif` */
- serif: 'ui-serif',
- /** iOS `UIFontDescriptorSystemDesignRounded` */
- rounded: 'ui-rounded',
- /** iOS `UIFontDescriptorSystemDesignMonospaced` */
- mono: 'ui-monospace',
- },
- default: {
- sans: 'normal',
- serif: 'serif',
- rounded: 'normal',
- mono: 'monospace',
- },
- web: {
- sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
- serif: "Georgia, 'Times New Roman', serif",
- rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
- mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
- },
-});
diff --git a/hooks/use-color-scheme.ts b/hooks/use-color-scheme.ts
deleted file mode 100644
index 17e3c63..0000000
--- a/hooks/use-color-scheme.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { useColorScheme } from 'react-native';
diff --git a/hooks/use-color-scheme.web.ts b/hooks/use-color-scheme.web.ts
deleted file mode 100644
index 7eb1c1b..0000000
--- a/hooks/use-color-scheme.web.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useColorScheme as useRNColorScheme } from 'react-native';
-
-/**
- * To support static rendering, this value needs to be re-calculated on the client side for web
- */
-export function useColorScheme() {
- const [hasHydrated, setHasHydrated] = useState(false);
-
- useEffect(() => {
- setHasHydrated(true);
- }, []);
-
- const colorScheme = useRNColorScheme();
-
- if (hasHydrated) {
- return colorScheme;
- }
-
- return 'light';
-}
diff --git a/hooks/use-theme-color.ts b/hooks/use-theme-color.ts
deleted file mode 100644
index 0cbc3a6..0000000
--- a/hooks/use-theme-color.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Learn more about light and dark modes:
- * https://docs.expo.dev/guides/color-schemes/
- */
-
-import { Colors } from '@/constants/theme';
-import { useColorScheme } from '@/hooks/use-color-scheme';
-
-export function useThemeColor(
- props: { light?: string; dark?: string },
- colorName: keyof typeof Colors.light & keyof typeof Colors.dark
-) {
- const theme = useColorScheme() ?? 'light';
- const colorFromProps = props[theme];
-
- if (colorFromProps) {
- return colorFromProps;
- } else {
- return Colors[theme][colorName];
- }
-}
diff --git a/scripts/reset-project.js b/scripts/reset-project.js
deleted file mode 100755
index 51dff15..0000000
--- a/scripts/reset-project.js
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * This script is used to reset the project to a blank state.
- * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
- * You can remove the `reset-project` script from package.json and safely delete this file after running it.
- */
-
-const fs = require("fs");
-const path = require("path");
-const readline = require("readline");
-
-const root = process.cwd();
-const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
-const exampleDir = "app-example";
-const newAppDir = "app";
-const exampleDirPath = path.join(root, exampleDir);
-
-const indexContent = `import { Text, View } from "react-native";
-
-export default function Index() {
- return (
-
- Edit app/index.tsx to edit this screen.
-
- );
-}
-`;
-
-const layoutContent = `import { Stack } from "expo-router";
-
-export default function RootLayout() {
- return ;
-}
-`;
-
-const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
-});
-
-const moveDirectories = async (userInput) => {
- try {
- if (userInput === "y") {
- // Create the app-example directory
- await fs.promises.mkdir(exampleDirPath, { recursive: true });
- console.log(`đ /${exampleDir} directory created.`);
- }
-
- // Move old directories to new app-example directory or delete them
- for (const dir of oldDirs) {
- const oldDirPath = path.join(root, dir);
- if (fs.existsSync(oldDirPath)) {
- if (userInput === "y") {
- const newDirPath = path.join(root, exampleDir, dir);
- await fs.promises.rename(oldDirPath, newDirPath);
- console.log(`âĄïž /${dir} moved to /${exampleDir}/${dir}.`);
- } else {
- await fs.promises.rm(oldDirPath, { recursive: true, force: true });
- console.log(`â /${dir} deleted.`);
- }
- } else {
- console.log(`âĄïž /${dir} does not exist, skipping.`);
- }
- }
-
- // Create new /app directory
- const newAppDirPath = path.join(root, newAppDir);
- await fs.promises.mkdir(newAppDirPath, { recursive: true });
- console.log("\nđ New /app directory created.");
-
- // Create index.tsx
- const indexPath = path.join(newAppDirPath, "index.tsx");
- await fs.promises.writeFile(indexPath, indexContent);
- console.log("đ app/index.tsx created.");
-
- // Create _layout.tsx
- const layoutPath = path.join(newAppDirPath, "_layout.tsx");
- await fs.promises.writeFile(layoutPath, layoutContent);
- console.log("đ app/_layout.tsx created.");
-
- console.log("\nâ
Project reset complete. Next steps:");
- console.log(
- `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
- userInput === "y"
- ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
- : ""
- }`
- );
- } catch (error) {
- console.error(`â Error during script execution: ${error.message}`);
- }
-};
-
-rl.question(
- "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
- (answer) => {
- const userInput = answer.trim().toLowerCase() || "y";
- if (userInput === "y" || userInput === "n") {
- moveDirectories(userInput).finally(() => rl.close());
- } else {
- console.log("â Invalid input. Please enter 'Y' or 'N'.");
- rl.close();
- }
- }
-);
diff --git a/src/features/audio/data/sounds.ts b/src/features/audio/data/sounds.ts
deleted file mode 100644
index df5fced..0000000
--- a/src/features/audio/data/sounds.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { PhaseSound } from '../types'
-
-/* eslint-disable @typescript-eslint/no-require-imports */
-export const PHASE_SOUNDS: Record = {
- beep_long: require('@/assets/audio/sounds/beep_long.mp3'),
- beep_double: require('@/assets/audio/sounds/beep_double.mp3'),
- beep_short: require('@/assets/audio/sounds/beep_short.mp3'),
- bell: require('@/assets/audio/sounds/bell.mp3'),
- fanfare: require('@/assets/audio/sounds/fanfare.mp3'),
- count_3: require('@/assets/audio/sounds/count_3.mp3'),
- count_2: require('@/assets/audio/sounds/count_2.mp3'),
- count_1: require('@/assets/audio/sounds/count_1.mp3'),
-}
diff --git a/src/features/audio/data/tracks.ts b/src/features/audio/data/tracks.ts
deleted file mode 100644
index 2431840..0000000
--- a/src/features/audio/data/tracks.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { MusicIntensity } from '../types'
-
-interface MusicTrack {
- id: string
- intensity: MusicIntensity
- asset: number
-}
-
-/* eslint-disable @typescript-eslint/no-require-imports */
-export const TRACKS: MusicTrack[] = [
- {
- id: 'electro_high',
- intensity: 'HIGH',
- asset: require('@/assets/audio/music/electro_high.mp3'),
- },
- {
- id: 'electro_low',
- intensity: 'LOW',
- asset: require('@/assets/audio/music/electro_low.mp3'),
- },
-]
-
-export function getTrack(intensity: MusicIntensity): MusicTrack {
- const track = TRACKS.find((t) => t.intensity === intensity)
- if (!track) throw new Error(`Track not found: ${intensity}`)
- return track
-}
diff --git a/src/features/audio/hooks/useAudioEngine.ts b/src/features/audio/hooks/useAudioEngine.ts
deleted file mode 100644
index e0f29c3..0000000
--- a/src/features/audio/hooks/useAudioEngine.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
-import { Audio } from 'expo-av'
-import type { AudioEngine, MusicIntensity, PhaseSound } from '../types'
-import { PHASE_SOUNDS } from '../data/sounds'
-import { getTrack } from '../data/tracks'
-
-const FADE_STEPS = 10
-
-async function delay(ms: number): Promise {
- return new Promise((resolve) => setTimeout(resolve, ms))
-}
-
-export function useAudioEngine(): AudioEngine {
- const [isLoaded, setIsLoaded] = useState(false)
- const soundsRef = useRef>({})
- const currentIntensityRef = useRef('LOW')
- const musicVolumeRef = useRef(0.5)
-
- // Configure audio session once
- useEffect(() => {
- Audio.setAudioModeAsync({
- playsInSilentModeIOS: true,
- allowsRecordingIOS: false,
- staysActiveInBackground: true,
- shouldDuckAndroid: true,
- playThroughEarpieceAndroid: false,
- }).catch((e) => {
- if (__DEV__) console.warn('[AudioEngine] Failed to configure audio session:', e)
- })
- }, [])
-
- const preloadAll = useCallback(async () => {
- try {
- // Preload phase sounds
- for (const [key, asset] of Object.entries(PHASE_SOUNDS)) {
- const { sound } = await Audio.Sound.createAsync(asset, {
- shouldPlay: false,
- volume: 1.0,
- })
- soundsRef.current[key] = sound
- }
-
- // Preload music tracks
- const highTrack = getTrack('HIGH')
- const lowTrack = getTrack('LOW')
-
- const { sound: musicHigh } = await Audio.Sound.createAsync(highTrack.asset, {
- shouldPlay: false,
- volume: 0,
- isLooping: true,
- })
- soundsRef.current['music_high'] = musicHigh
-
- const { sound: musicLow } = await Audio.Sound.createAsync(lowTrack.asset, {
- shouldPlay: false,
- volume: 0,
- isLooping: true,
- })
- soundsRef.current['music_low'] = musicLow
-
- setIsLoaded(true)
- if (__DEV__) console.log('[AudioEngine] All sounds preloaded')
- } catch (e) {
- if (__DEV__) console.warn('[AudioEngine] Preload error:', e)
- }
- }, [])
-
- const playPhaseSound = useCallback(async (sound: PhaseSound) => {
- const s = soundsRef.current[sound]
- if (!s) return
- try {
- await s.setPositionAsync(0)
- await s.playAsync()
- } catch (e) {
- if (__DEV__) console.warn('[AudioEngine] Play error:', sound, e)
- }
- }, [])
-
- const startMusic = useCallback(async (intensity: MusicIntensity) => {
- const key = intensity === 'HIGH' ? 'music_high' : 'music_low'
- const s = soundsRef.current[key]
- if (!s) return
- try {
- currentIntensityRef.current = intensity
- await s.setPositionAsync(0)
- await s.setVolumeAsync(musicVolumeRef.current)
- await s.playAsync()
- } catch (e) {
- if (__DEV__) console.warn('[AudioEngine] startMusic error:', e)
- }
- }, [])
-
- const switchIntensity = useCallback(async (to: MusicIntensity) => {
- const from = currentIntensityRef.current
- if (from === to) return
-
- const outKey = from === 'HIGH' ? 'music_high' : 'music_low'
- const inKey = to === 'HIGH' ? 'music_high' : 'music_low'
- const outSound = soundsRef.current[outKey]
- const inSound = soundsRef.current[inKey]
-
- if (!outSound || !inSound) return
-
- try {
- const vol = musicVolumeRef.current
- await inSound.setPositionAsync(0)
- await inSound.setVolumeAsync(0)
- await inSound.playAsync()
-
- // Crossfade
- const stepMs = 500 / FADE_STEPS
- for (let i = 1; i <= FADE_STEPS; i++) {
- const progress = i / FADE_STEPS
- await Promise.all([
- outSound.setVolumeAsync(vol * (1 - progress)),
- inSound.setVolumeAsync(vol * progress),
- ])
- await delay(stepMs)
- }
-
- await outSound.stopAsync()
- currentIntensityRef.current = to
- } catch (e) {
- if (__DEV__) console.warn('[AudioEngine] switchIntensity error:', e)
- }
- }, [])
-
- const stopMusic = useCallback(async (fadeMs: number = 500) => {
- const key = currentIntensityRef.current === 'HIGH' ? 'music_high' : 'music_low'
- const s = soundsRef.current[key]
- if (!s) return
-
- try {
- const stepMs = fadeMs / FADE_STEPS
- for (let i = FADE_STEPS; i >= 0; i--) {
- await s.setVolumeAsync(musicVolumeRef.current * (i / FADE_STEPS))
- await delay(stepMs)
- }
- await s.stopAsync()
- } catch (e) {
- if (__DEV__) console.warn('[AudioEngine] stopMusic error:', e)
- }
- }, [])
-
- const unloadAll = useCallback(async () => {
- await Promise.all(
- Object.values(soundsRef.current).map((s) =>
- s.unloadAsync().catch(() => {})
- )
- )
- soundsRef.current = {}
- setIsLoaded(false)
- }, [])
-
- // Cleanup on unmount
- useEffect(() => {
- return () => {
- Object.values(soundsRef.current).forEach((s) => {
- s.unloadAsync().catch(() => {})
- })
- }
- }, [])
-
- return {
- isLoaded,
- preloadAll,
- playPhaseSound,
- startMusic,
- switchIntensity,
- stopMusic,
- unloadAll,
- }
-}
diff --git a/src/features/audio/index.ts b/src/features/audio/index.ts
deleted file mode 100644
index 5d596ce..0000000
--- a/src/features/audio/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export { useAudioEngine } from './hooks/useAudioEngine'
-export type {
- MusicAmbiance,
- MusicIntensity,
- PhaseSound,
- AudioSettings,
- AudioEngine,
-} from './types'
diff --git a/src/features/audio/types.ts b/src/features/audio/types.ts
deleted file mode 100644
index 677df8b..0000000
--- a/src/features/audio/types.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export type MusicAmbiance = 'ELECTRO' | 'SILENCE'
-export type MusicIntensity = 'LOW' | 'HIGH'
-
-export type PhaseSound =
- | 'beep_long'
- | 'beep_double'
- | 'beep_short'
- | 'bell'
- | 'fanfare'
- | 'count_3'
- | 'count_2'
- | 'count_1'
-
-export interface AudioSettings {
- musicEnabled: boolean
- ambiance: MusicAmbiance
- musicVolume: number
- soundsEnabled: boolean
- soundsVolume: number
- hapticsEnabled: boolean
-}
-
-export interface AudioEngine {
- isLoaded: boolean
- preloadAll: () => Promise
- playPhaseSound: (sound: PhaseSound) => Promise
- startMusic: (intensity: MusicIntensity) => Promise
- switchIntensity: (intensity: MusicIntensity) => Promise
- stopMusic: (fadeMs?: number) => Promise
- unloadAll: () => Promise
-}
diff --git a/src/features/onboarding/components/ChoiceButton.tsx b/src/features/onboarding/components/ChoiceButton.tsx
deleted file mode 100644
index 1502e2e..0000000
--- a/src/features/onboarding/components/ChoiceButton.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { StyleSheet, View, Text, Pressable } from 'react-native'
-import { Ionicons } from '@expo/vector-icons'
-import { BRAND, GLASS, TEXT, BORDER, SURFACE } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface ChoiceButtonProps {
- label: string
- description?: string
- icon: keyof typeof Ionicons.glyphMap
- selected: boolean
- onPress: () => void
-}
-
-export function ChoiceButton({
- label,
- description,
- icon,
- selected,
- onPress,
-}: ChoiceButtonProps) {
- return (
-
-
-
-
-
-
-
- {label}
-
- {description && (
- {description}
- )}
-
- {selected && (
-
-
-
- )}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- pressable: {
- width: '100%',
- },
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: GLASS.FILL,
- borderWidth: 1,
- borderColor: GLASS.BORDER,
- borderRadius: RADIUS.LG,
- paddingVertical: SPACING[4],
- paddingHorizontal: SPACING[4],
- gap: SPACING[3],
- },
- containerSelected: {
- backgroundColor: SURFACE.OVERLAY_LIGHT,
- borderColor: BRAND.PRIMARY,
- borderWidth: 2,
- },
- iconContainer: {
- width: 48,
- height: 48,
- borderRadius: RADIUS.MD,
- backgroundColor: SURFACE.OVERLAY_LIGHT,
- alignItems: 'center',
- justifyContent: 'center',
- },
- iconContainerSelected: {
- backgroundColor: SURFACE.OVERLAY_MEDIUM,
- },
- textContainer: {
- flex: 1,
- },
- label: {
- ...TYPOGRAPHY.body,
- color: TEXT.PRIMARY,
- },
- labelSelected: {
- color: BRAND.PRIMARY,
- },
- description: {
- ...TYPOGRAPHY.caption,
- color: TEXT.MUTED,
- marginTop: 4,
- },
- checkmark: {
- marginLeft: SPACING[2],
- },
-})
diff --git a/src/features/onboarding/components/MiniTimerDemo.tsx b/src/features/onboarding/components/MiniTimerDemo.tsx
deleted file mode 100644
index 7649ebf..0000000
--- a/src/features/onboarding/components/MiniTimerDemo.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-import { StyleSheet, View, Text, Animated } from 'react-native'
-import { useTimerEngine } from '@/src/features/timer/hooks/useTimerEngine'
-import { PHASE_COLORS, TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface MiniTimerDemoProps {
- onComplete?: () => void
- onPhaseChange?: (phase: string) => void
- onCountdownTick?: (seconds: number) => void
- autoStartDelay?: number
-}
-
-export function MiniTimerDemo({
- onComplete,
- onPhaseChange,
- onCountdownTick,
- autoStartDelay = 500,
-}: MiniTimerDemoProps) {
- const timer = useTimerEngine()
- const [hasCompleted, setHasCompleted] = useState(false)
- const pulseAnim = useRef(new Animated.Value(1)).current
- const glowAnim = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- // Auto-start after a short delay
- const startTimeout = setTimeout(() => {
- timer.start({
- workDuration: 20,
- restDuration: 0,
- rounds: 1,
- getReadyDuration: 3,
- cycles: 1,
- })
- }, autoStartDelay)
-
- return () => clearTimeout(startTimeout)
- }, [timer, autoStartDelay])
-
- useEffect(() => {
- // Listen for all timer events
- const unsubscribe = timer.addEventListener((event) => {
- switch (event.type) {
- case 'SESSION_COMPLETE':
- setHasCompleted(true)
- onComplete?.()
- break
- case 'PHASE_CHANGED':
- onPhaseChange?.(event.to)
- break
- case 'COUNTDOWN_TICK':
- onCountdownTick?.(event.secondsLeft)
- break
- }
- })
-
- return unsubscribe
- }, [timer, onComplete, onPhaseChange, onCountdownTick])
-
- useEffect(() => {
- // Pulse animation when running
- if (timer.isRunning) {
- Animated.loop(
- Animated.sequence([
- Animated.timing(pulseAnim, {
- toValue: 1.05,
- duration: 800,
- useNativeDriver: true,
- }),
- Animated.timing(pulseAnim, {
- toValue: 1,
- duration: 800,
- useNativeDriver: true,
- }),
- ])
- ).start()
- } else {
- pulseAnim.setValue(1)
- }
- }, [timer.isRunning, pulseAnim])
-
- useEffect(() => {
- // Glow animation
- Animated.loop(
- Animated.sequence([
- Animated.timing(glowAnim, {
- toValue: 1,
- duration: 1500,
- useNativeDriver: true,
- }),
- Animated.timing(glowAnim, {
- toValue: 0,
- duration: 1500,
- useNativeDriver: true,
- }),
- ])
- ).start()
- }, [glowAnim])
-
- const getPhaseText = (): string => {
- if (hasCompleted) return 'DONE!'
- if (timer.phase === 'GET_READY') return 'GET READY'
- if (timer.phase === 'WORK') return 'GO!'
- if (timer.phase === 'COMPLETE') return 'DONE!'
- return ''
- }
-
- const phaseColor = PHASE_COLORS[timer.phase] || PHASE_COLORS.IDLE
- const displaySeconds = timer.secondsLeft > 0 ? timer.secondsLeft : 0
-
- return (
-
-
-
-
- {displaySeconds}
-
-
- {getPhaseText()}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: SPACING[6],
- },
- timerCircle: {
- width: 200,
- height: 200,
- borderRadius: 100,
- borderWidth: 4,
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
- overflow: 'hidden',
- },
- glowOverlay: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- borderRadius: 100,
- },
- countdown: {
- ...TYPOGRAPHY.timeDisplay,
- fontVariant: ['tabular-nums'],
- },
- phaseText: {
- ...TYPOGRAPHY.label,
- marginTop: SPACING[2],
- letterSpacing: 3,
- },
-})
diff --git a/src/features/onboarding/components/OnboardingScreen.tsx b/src/features/onboarding/components/OnboardingScreen.tsx
deleted file mode 100644
index dc1e2c5..0000000
--- a/src/features/onboarding/components/OnboardingScreen.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { ReactNode } from 'react'
-import { StyleSheet, View, SafeAreaView } from 'react-native'
-import { LinearGradient } from 'expo-linear-gradient'
-import { APP_GRADIENTS } from '@/src/shared/constants/colors'
-import { LAYOUT, SPACING } from '@/src/shared/constants/spacing'
-import { ProgressBar } from './ProgressBar'
-
-interface OnboardingScreenProps {
- children: ReactNode
- currentStep: number
- totalSteps?: number
-}
-
-export function OnboardingScreen({
- children,
- currentStep,
- totalSteps = 6,
-}: OnboardingScreenProps) {
- return (
-
-
-
- {children}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- gradient: {
- flex: 1,
- },
- container: {
- flex: 1,
- },
- content: {
- flex: 1,
- paddingHorizontal: LAYOUT.PAGE_HORIZONTAL,
- paddingBottom: SPACING[6],
- },
-})
diff --git a/src/features/onboarding/components/PaywallCard.tsx b/src/features/onboarding/components/PaywallCard.tsx
deleted file mode 100644
index 6553604..0000000
--- a/src/features/onboarding/components/PaywallCard.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { StyleSheet, View, Text, Pressable } from 'react-native'
-import { BRAND, GLASS, TEXT, SURFACE, BORDER } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface PaywallCardProps {
- title: string
- price: string
- period: string
- features: string[]
- selected: boolean
- onPress: () => void
- badge?: string
-}
-
-export function PaywallCard({
- title,
- price,
- period,
- features,
- selected,
- onPress,
- badge,
-}: PaywallCardProps) {
- return (
-
-
- {badge && (
-
- {badge}
-
- )}
-
-
- {title}
-
-
- {price}
- /{period}
-
-
-
-
- {features.map((feature, index) => (
-
-
- {feature}
-
- ))}
-
- {selected && (
-
-
-
- )}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- pressable: {
- width: '100%',
- },
- container: {
- backgroundColor: GLASS.FILL_MEDIUM,
- borderWidth: 1,
- borderColor: GLASS.BORDER,
- borderRadius: RADIUS.XL,
- padding: SPACING[4],
- position: 'relative',
- overflow: 'hidden',
- },
- containerSelected: {
- backgroundColor: SURFACE.OVERLAY_MEDIUM,
- borderColor: BRAND.PRIMARY,
- borderWidth: 2,
- },
- badge: {
- position: 'absolute',
- top: 0,
- right: 0,
- backgroundColor: BRAND.PRIMARY,
- paddingHorizontal: SPACING[3],
- paddingVertical: SPACING[1.5],
- borderBottomLeftRadius: RADIUS.MD,
- },
- badgeText: {
- ...TYPOGRAPHY.overline,
- color: TEXT.PRIMARY,
- fontWeight: '700',
- },
- header: {
- marginBottom: SPACING[3],
- },
- title: {
- ...TYPOGRAPHY.heading,
- color: TEXT.PRIMARY,
- marginBottom: SPACING[2],
- },
- titleSelected: {
- color: BRAND.PRIMARY,
- },
- priceRow: {
- flexDirection: 'row',
- alignItems: 'baseline',
- },
- price: {
- ...TYPOGRAPHY.displaySmall,
- color: TEXT.PRIMARY,
- fontWeight: '900',
- },
- period: {
- ...TYPOGRAPHY.caption,
- color: TEXT.MUTED,
- marginLeft: SPACING[1],
- },
- divider: {
- height: 1,
- backgroundColor: BORDER.LIGHT,
- marginBottom: SPACING[3],
- },
- features: {
- gap: SPACING[2],
- },
- featureRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[2],
- },
- featureDot: {
- width: 6,
- height: 6,
- borderRadius: 3,
- backgroundColor: BRAND.PRIMARY,
- },
- featureText: {
- ...TYPOGRAPHY.caption,
- color: TEXT.SECONDARY,
- },
- selectedIndicator: {
- position: 'absolute',
- top: SPACING[3],
- left: SPACING[3],
- },
- selectedDot: {
- width: 12,
- height: 12,
- borderRadius: 6,
- backgroundColor: BRAND.PRIMARY,
- shadowColor: BRAND.PRIMARY,
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0.6,
- shadowRadius: 6,
- elevation: 3,
- },
-})
diff --git a/src/features/onboarding/components/PrimaryButton.tsx b/src/features/onboarding/components/PrimaryButton.tsx
deleted file mode 100644
index 4353078..0000000
--- a/src/features/onboarding/components/PrimaryButton.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { StyleSheet, Text, Pressable, Animated } from 'react-native'
-import { BRAND, TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-
-interface PrimaryButtonProps {
- title: string
- onPress: () => void
- disabled?: boolean
-}
-
-export function PrimaryButton({
- title,
- onPress,
- disabled = false,
-}: PrimaryButtonProps) {
- const animatedValue = new Animated.Value(1)
-
- const handlePressIn = () => {
- Animated.spring(animatedValue, {
- toValue: 0.96,
- useNativeDriver: true,
- }).start()
- }
-
- const handlePressOut = () => {
- Animated.spring(animatedValue, {
- toValue: 1,
- friction: 3,
- useNativeDriver: true,
- }).start()
- }
-
- return (
-
-
-
- {title}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- button: {
- backgroundColor: BRAND.PRIMARY,
- borderRadius: RADIUS['2XL'],
- paddingVertical: 18,
- paddingHorizontal: 32,
- alignItems: 'center',
- justifyContent: 'center',
- shadowColor: BRAND.PRIMARY,
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.4,
- shadowRadius: 12,
- elevation: 8,
- },
- buttonDisabled: {
- backgroundColor: BRAND.PRIMARY,
- opacity: 0.5,
- shadowOpacity: 0,
- elevation: 0,
- },
- text: {
- ...TYPOGRAPHY.buttonMedium,
- color: TEXT.PRIMARY,
- },
- textDisabled: {
- color: TEXT.PRIMARY,
- opacity: 0.7,
- },
-})
diff --git a/src/features/onboarding/components/ProgressBar.tsx b/src/features/onboarding/components/ProgressBar.tsx
deleted file mode 100644
index 4d3e0f7..0000000
--- a/src/features/onboarding/components/ProgressBar.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useEffect, useRef } from 'react'
-import { StyleSheet, View, Animated } from 'react-native'
-import { BRAND, SURFACE } from '@/src/shared/constants/colors'
-
-interface ProgressBarProps {
- currentStep: number
- totalSteps?: number
-}
-
-export function ProgressBar({
- currentStep,
- totalSteps = 6,
-}: ProgressBarProps) {
- const scaleAnims = useRef(
- Array.from({ length: totalSteps }, () => new Animated.Value(1))
- ).current
-
- useEffect(() => {
- // Animate the newly active dot
- if (currentStep >= 0 && currentStep < totalSteps) {
- Animated.sequence([
- Animated.timing(scaleAnims[currentStep], {
- toValue: 1.3,
- duration: 150,
- useNativeDriver: true,
- }),
- Animated.timing(scaleAnims[currentStep], {
- toValue: 1,
- duration: 150,
- useNativeDriver: true,
- }),
- ]).start()
- }
- }, [currentStep, totalSteps, scaleAnims])
-
- return (
-
- {Array.from({ length: totalSteps }).map((_, index) => {
- const isActive = index === currentStep
- const isCompleted = index < currentStep
-
- return (
-
- )
- })}
-
- )
-}
-
-const DOT_SIZE = 10
-const DOT_SIZE_ACTIVE = 12
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'center',
- gap: 12,
- paddingVertical: 16,
- },
- dot: {
- width: DOT_SIZE,
- height: DOT_SIZE,
- borderRadius: DOT_SIZE / 2,
- backgroundColor: SURFACE.OVERLAY_LIGHT,
- },
- dotActive: {
- width: DOT_SIZE_ACTIVE,
- height: DOT_SIZE_ACTIVE,
- borderRadius: DOT_SIZE_ACTIVE / 2,
- backgroundColor: BRAND.PRIMARY,
- shadowColor: BRAND.PRIMARY,
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0.8,
- shadowRadius: 8,
- elevation: 4,
- },
- dotCompleted: {
- backgroundColor: BRAND.PRIMARY,
- },
-})
diff --git a/src/features/onboarding/components/index.ts b/src/features/onboarding/components/index.ts
deleted file mode 100644
index e5fae39..0000000
--- a/src/features/onboarding/components/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export { OnboardingScreen } from './OnboardingScreen'
-export { ProgressBar } from './ProgressBar'
-export { PrimaryButton } from './PrimaryButton'
-export { ChoiceButton } from './ChoiceButton'
-export { MiniTimerDemo } from './MiniTimerDemo'
-export { PaywallCard } from './PaywallCard'
diff --git a/src/features/onboarding/data/barriers.ts b/src/features/onboarding/data/barriers.ts
deleted file mode 100644
index 8bc33c4..0000000
--- a/src/features/onboarding/data/barriers.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Barrier } from '../types'
-
-export interface BarrierOption {
- id: Barrier
- label: string
- description: string
- icon: string // Ionicons name
-}
-
-export const BARRIERS: BarrierOption[] = [
- { id: 'time', label: 'Le temps', description: 'Je n\'ai pas assez de temps', icon: 'time-outline' },
- { id: 'motivation', label: 'La motivation', description: 'Je n\'arrive pas à rester motivé(e)', icon: 'flash-outline' },
- { id: 'knowledge', label: 'Le savoir', description: 'Je ne sais pas quoi faire', icon: 'book-outline' },
- { id: 'gym', label: 'La salle', description: 'Je n\'ai pas accĂšs Ă une salle', icon: 'barbell-outline' },
-]
diff --git a/src/features/onboarding/data/goals.ts b/src/features/onboarding/data/goals.ts
deleted file mode 100644
index 8664f27..0000000
--- a/src/features/onboarding/data/goals.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Goal } from '../types'
-
-export interface GoalOption {
- id: Goal
- label: string
- description: string
- icon: string
-}
-
-export const GOALS: GoalOption[] = [
- { id: 'weight_loss', label: 'Perte de poids', description: 'Brûler des graisses efficacement', icon: 'flame-outline' },
- { id: 'cardio', label: 'Cardio', description: 'Améliorer mon endurance', icon: 'heart-outline' },
- { id: 'strength', label: 'Force', description: 'Développer ma musculature', icon: 'barbell-outline' },
- { id: 'wellness', label: 'Bien-ĂȘtre', description: 'Me sentir mieux dans mon corps', icon: 'happy-outline' },
-]
diff --git a/src/features/onboarding/data/index.ts b/src/features/onboarding/data/index.ts
deleted file mode 100644
index 6612251..0000000
--- a/src/features/onboarding/data/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// Barrel export for onboarding data
-
-export { BARRIERS, type BarrierOption } from './barriers'
-export { GOALS, type GoalOption } from './goals'
-export { LEVELS, type LevelOption } from './levels'
diff --git a/src/features/onboarding/data/levels.ts b/src/features/onboarding/data/levels.ts
deleted file mode 100644
index 8a3e687..0000000
--- a/src/features/onboarding/data/levels.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { Level } from '../types'
-
-export interface LevelOption {
- id: Level
- label: string
- description: string
- icon: string
-}
-
-export const LEVELS: LevelOption[] = [
- { id: 'beginner', label: 'Débutant', description: 'Je commence le sport', icon: 'leaf-outline' },
- { id: 'intermediate', label: 'Intermédiaire', description: 'Je fais du sport réguliÚrement', icon: 'fitness-outline' },
- { id: 'advanced', label: 'Avancé', description: 'Je suis trÚs actif(ve)', icon: 'trophy-outline' },
-]
diff --git a/src/features/onboarding/hooks/useOnboarding.ts b/src/features/onboarding/hooks/useOnboarding.ts
deleted file mode 100644
index b5a8b57..0000000
--- a/src/features/onboarding/hooks/useOnboarding.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { create } from 'zustand'
-import {
- createJSONStorage,
- persist,
- type StateStorage,
-} from 'zustand/middleware'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import type {
- Barrier,
- Frequency,
- Goal,
- Level,
- OnboardingData,
- OnboardingState,
-} from '../types'
-
-const STORAGE_KEY_COMPLETE = 'tabatago_onboarding_complete'
-const STORAGE_KEY_DATA = 'tabatago_onboarding_data'
-
-// Custom storage that uses AsyncStorage
-const onboardingStorage: StateStorage = {
- getItem: async (name: string): Promise => {
- return await AsyncStorage.getItem(name)
- },
- setItem: async (name: string, value: string): Promise => {
- await AsyncStorage.setItem(name, value)
- },
- removeItem: async (name: string): Promise => {
- await AsyncStorage.removeItem(name)
- },
-}
-
-const initialData: OnboardingData = {
- barrier: null,
- level: null,
- goal: null,
- frequency: null,
-}
-
-interface OnboardingActions {
- setStep: (step: number) => void
- nextStep: () => void
- prevStep: () => void
- setData: (data: Partial) => void
- setBarrier: (barrier: Barrier) => void
- setLevel: (level: Level) => void
- setGoal: (goal: Goal) => void
- setFrequency: (frequency: Frequency) => void
- completeOnboarding: () => void
- resetOnboarding: () => void
-}
-
-type OnboardingStore = OnboardingState & OnboardingActions
-
-export const useOnboarding = create()(
- persist(
- (set, get) => ({
- // Initial state
- currentStep: 0,
- isOnboardingComplete: false,
- data: initialData,
-
- // Actions
- setStep: (step: number) => {
- set({ currentStep: step })
- },
-
- nextStep: () => {
- const { currentStep } = get()
- set({ currentStep: currentStep + 1 })
- },
-
- prevStep: () => {
- const { currentStep } = get()
- if (currentStep > 0) {
- set({ currentStep: currentStep - 1 })
- }
- },
-
- setData: (data: Partial) => {
- set((state) => ({
- data: { ...state.data, ...data },
- }))
- },
-
- setBarrier: (barrier: Barrier) => {
- set((state) => ({
- data: { ...state.data, barrier },
- }))
- },
-
- setLevel: (level: Level) => {
- set((state) => ({
- data: { ...state.data, level },
- }))
- },
-
- setGoal: (goal: Goal) => {
- set((state) => ({
- data: { ...state.data, goal },
- }))
- },
-
- setFrequency: (frequency: Frequency) => {
- set((state) => ({
- data: { ...state.data, frequency },
- }))
- },
-
- completeOnboarding: () => {
- set({ isOnboardingComplete: true })
- },
-
- resetOnboarding: () => {
- set({
- currentStep: 0,
- isOnboardingComplete: false,
- data: initialData,
- })
- },
- }),
- {
- name: STORAGE_KEY_DATA,
- storage: createJSONStorage(() => onboardingStorage),
- partialize: (state) => ({
- isOnboardingComplete: state.isOnboardingComplete,
- data: state.data,
- }),
- }
- )
-)
-
-// Selector hooks for better performance
-export const useOnboardingStep = () => useOnboarding((state) => state.currentStep)
-export const useIsOnboardingComplete = () =>
- useOnboarding((state) => state.isOnboardingComplete)
-export const useOnboardingData = () => useOnboarding((state) => state.data)
diff --git a/src/features/onboarding/index.ts b/src/features/onboarding/index.ts
deleted file mode 100644
index 5feec6b..0000000
--- a/src/features/onboarding/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-// Types
-export * from './types'
-
-// Hooks
-export { useOnboarding, useOnboardingStep, useIsOnboardingComplete, useOnboardingData } from './hooks/useOnboarding'
-
-// Components
-export { OnboardingScreen } from './components/OnboardingScreen'
-export { PrimaryButton } from './components/PrimaryButton'
-export { ChoiceButton } from './components/ChoiceButton'
-export { PaywallCard } from './components/PaywallCard'
-export { MiniTimerDemo } from './components/MiniTimerDemo'
-export { ProgressBar } from './components/ProgressBar'
-
-// Screens
-export { Screen1Problem } from './screens/Screen1Problem'
-export { Screen2Empathy } from './screens/Screen2Empathy'
-export { Screen3Solution } from './screens/Screen3Solution'
-export { Screen4WowMoment } from './screens/Screen4WowMoment'
-export { Screen5Personalization } from './screens/Screen5Personalization'
-export { Screen6Paywall } from './screens/Screen6Paywall'
-
-// Data
-export { BARRIERS } from './data/barriers'
-export { LEVELS } from './data/levels'
-export { GOALS } from './data/goals'
diff --git a/src/features/onboarding/screens/Screen1Problem.tsx b/src/features/onboarding/screens/Screen1Problem.tsx
deleted file mode 100644
index 530c458..0000000
--- a/src/features/onboarding/screens/Screen1Problem.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { useEffect, useRef } from 'react'
-import { StyleSheet, View, Text, Animated } from 'react-native'
-import { Ionicons } from '@expo/vector-icons'
-import { OnboardingScreen } from '../components/OnboardingScreen'
-import { PrimaryButton } from '../components/PrimaryButton'
-import { BRAND, TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface Screen1ProblemProps {
- onNext: () => void
-}
-
-export function Screen1Problem({ onNext }: Screen1ProblemProps) {
- const clockScale = useRef(new Animated.Value(1)).current
- const clockRotation = useRef(new Animated.Value(0)).current
- const opacityAnim = useRef(new Animated.Value(0)).current
- const translateYAnim = useRef(new Animated.Value(20)).current
-
- useEffect(() => {
- // Entrance animation
- Animated.parallel([
- Animated.timing(opacityAnim, {
- toValue: 1,
- duration: 600,
- useNativeDriver: true,
- }),
- Animated.timing(translateYAnim, {
- toValue: 0,
- duration: 600,
- useNativeDriver: true,
- }),
- ]).start()
-
- // Clock pulse animation
- const pulseAnimation = Animated.loop(
- Animated.sequence([
- Animated.timing(clockScale, {
- toValue: 1.1,
- duration: 800,
- useNativeDriver: true,
- }),
- Animated.timing(clockScale, {
- toValue: 1,
- duration: 800,
- useNativeDriver: true,
- }),
- ])
- )
-
- // Clock rotation animation
- const rotationAnimation = Animated.loop(
- Animated.timing(clockRotation, {
- toValue: 1,
- duration: 4000,
- useNativeDriver: true,
- })
- )
-
- pulseAnimation.start()
- rotationAnimation.start()
-
- return () => {
- pulseAnimation.stop()
- rotationAnimation.stop()
- }
- }, [clockScale, clockRotation, opacityAnim, translateYAnim])
-
- const rotationInterpolate = clockRotation.interpolate({
- inputRange: [0, 1],
- outputRange: ['0deg', '360deg'],
- })
-
- return (
-
-
- {/* Clock Icon Animation */}
-
-
-
-
-
-
-
-
-
-
-
- {/* Title and Subtitle */}
-
- Tu n'as pas 1 heure pour t'entrainer ?
- Ni 30 minutes ? Ni meme 10 ?
-
-
- {/* Spacer */}
-
-
- {/* Continue Button */}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- paddingHorizontal: SPACING[4],
- },
- iconContainer: {
- position: 'relative',
- marginBottom: SPACING[10],
- },
- clockCircle: {
- width: 140,
- height: 140,
- borderRadius: 70,
- backgroundColor: 'rgba(249, 115, 22, 0.1)',
- alignItems: 'center',
- justifyContent: 'center',
- borderWidth: 2,
- borderColor: 'rgba(249, 115, 22, 0.3)',
- },
- crossMark: {
- position: 'absolute',
- bottom: -5,
- right: -5,
- backgroundColor: 'rgba(239, 68, 68, 0.2)',
- borderRadius: 20,
- padding: SPACING[1],
- },
- textContainer: {
- alignItems: 'center',
- marginBottom: SPACING[8],
- },
- title: {
- ...TYPOGRAPHY.displayLarge,
- color: TEXT.PRIMARY,
- textAlign: 'center',
- marginBottom: SPACING[4],
- },
- subtitle: {
- ...TYPOGRAPHY.heading,
- color: TEXT.MUTED,
- textAlign: 'center',
- },
- spacer: {
- flex: 1,
- },
-})
diff --git a/src/features/onboarding/screens/Screen2Empathy.tsx b/src/features/onboarding/screens/Screen2Empathy.tsx
deleted file mode 100644
index 755125b..0000000
--- a/src/features/onboarding/screens/Screen2Empathy.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { useState, useEffect, useRef } from 'react'
-import { StyleSheet, View, Text, Animated } from 'react-native'
-import { OnboardingScreen } from '../components/OnboardingScreen'
-import { ChoiceButton } from '../components/ChoiceButton'
-import { useOnboarding } from '../hooks/useOnboarding'
-import { BARRIERS } from '../data/barriers'
-import { TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import type { Barrier } from '../types'
-
-interface Screen2EmpathyProps {
- onNext: () => void
-}
-
-export function Screen2Empathy({ onNext }: Screen2EmpathyProps) {
- const [selectedBarrier, setSelectedBarrier] = useState(null)
- const { setBarrier } = useOnboarding()
- const opacityAnim = useRef(new Animated.Value(0)).current
- const translateYAnim = useRef(new Animated.Value(20)).current
-
- useEffect(() => {
- // Entrance animation
- Animated.parallel([
- Animated.timing(opacityAnim, {
- toValue: 1,
- duration: 600,
- useNativeDriver: true,
- }),
- Animated.timing(translateYAnim, {
- toValue: 0,
- duration: 600,
- useNativeDriver: true,
- }),
- ]).start()
- }, [opacityAnim, translateYAnim])
-
- const handleBarrierSelect = (barrier: Barrier) => {
- setSelectedBarrier(barrier)
- setBarrier(barrier)
- // Auto-advance after selection
- setTimeout(() => {
- onNext()
- }, 300)
- }
-
- return (
-
-
- {/* Title */}
-
-
- Qu'est-ce qui t'empeche de t'entrainer ?
-
-
-
- {/* Choice Buttons */}
-
- {BARRIERS.map((barrier, index) => (
-
- handleBarrierSelect(barrier.id)}
- />
-
- ))}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- paddingTop: SPACING[8],
- },
- titleContainer: {
- marginBottom: SPACING[8],
- paddingHorizontal: SPACING[2],
- },
- title: {
- ...TYPOGRAPHY.heading,
- color: TEXT.PRIMARY,
- textAlign: 'center',
- },
- choicesContainer: {
- gap: SPACING[3],
- },
- choiceWrapper: {
- width: '100%',
- },
-})
diff --git a/src/features/onboarding/screens/Screen3Solution.tsx b/src/features/onboarding/screens/Screen3Solution.tsx
deleted file mode 100644
index 49b8c1e..0000000
--- a/src/features/onboarding/screens/Screen3Solution.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-import { useEffect, useRef } from 'react'
-import { StyleSheet, View, Text, Animated } from 'react-native'
-import { OnboardingScreen } from '../components/OnboardingScreen'
-import { PrimaryButton } from '../components/PrimaryButton'
-import { BRAND, TEXT, SURFACE, PHASE_COLORS } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-
-interface Screen3SolutionProps {
- onNext: () => void
-}
-
-const TABATA_ROUNDS = 8
-const WORK_DURATION = 20
-const REST_DURATION = 10
-
-export function Screen3Solution({ onNext }: Screen3SolutionProps) {
- const opacityAnim = useRef(new Animated.Value(0)).current
- const translateYAnim = useRef(new Animated.Value(20)).current
- const activeRound = useRef(new Animated.Value(0)).current
- const pulseAnim = useRef(new Animated.Value(1)).current
-
- useEffect(() => {
- // Entrance animation
- Animated.parallel([
- Animated.timing(opacityAnim, {
- toValue: 1,
- duration: 600,
- useNativeDriver: true,
- }),
- Animated.timing(translateYAnim, {
- toValue: 0,
- duration: 600,
- useNativeDriver: true,
- }),
- ]).start()
-
- // Round cycling animation
- const roundAnimation = Animated.loop(
- Animated.sequence([
- ...Array.from({ length: TABATA_ROUNDS }, (_, i) =>
- Animated.timing(activeRound, {
- toValue: i + 1,
- duration: 800,
- useNativeDriver: false,
- })
- ),
- Animated.timing(activeRound, {
- toValue: 0,
- duration: 500,
- useNativeDriver: false,
- }),
- ])
- )
-
- // Pulse animation for the "4 minutes" text
- const pulseAnimation = Animated.loop(
- Animated.sequence([
- Animated.timing(pulseAnim, {
- toValue: 1.05,
- duration: 600,
- useNativeDriver: true,
- }),
- Animated.timing(pulseAnim, {
- toValue: 1,
- duration: 600,
- useNativeDriver: true,
- }),
- ])
- )
-
- roundAnimation.start()
- pulseAnimation.start()
-
- return () => {
- roundAnimation.stop()
- pulseAnimation.stop()
- }
- }, [activeRound, opacityAnim, translateYAnim, pulseAnim])
-
- const renderTabataTimeline = () => {
- return (
-
- {Array.from({ length: TABATA_ROUNDS }, (_, index) => {
- const isWork = index % 2 === 0
- const baseColor = isWork ? PHASE_COLORS.WORK : PHASE_COLORS.REST
-
- return (
-
-
- {isWork ? WORK_DURATION : REST_DURATION}s
-
-
- )
- })}
-
- )
- }
-
- return (
-
-
- {/* Animated Title */}
-
- 4 minutes
-
-
- {/* Subtitle */}
- Vraiment transformatrices.
-
- {/* Tabata Timeline Animation */}
-
- {renderTabataTimeline()}
-
- {/* Legend */}
-
-
-
- 20s travail
-
-
-
- 10s repos
-
- x 8 rounds
-
-
-
- {/* Scientific Explanation */}
-
-
- Protocole scientifique HIIT = resultats max en temps min
-
-
-
- {/* Spacer */}
-
-
- {/* Continue Button */}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: 'center',
- paddingTop: SPACING[10],
- },
- titleContainer: {
- marginBottom: SPACING[2],
- },
- title: {
- ...TYPOGRAPHY.brandTitle,
- color: BRAND.PRIMARY,
- textAlign: 'center',
- },
- subtitle: {
- ...TYPOGRAPHY.heading,
- color: TEXT.SECONDARY,
- textAlign: 'center',
- marginBottom: SPACING[8],
- },
- animationContainer: {
- width: '100%',
- paddingVertical: SPACING[6],
- paddingHorizontal: SPACING[4],
- },
- timelineContainer: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- justifyContent: 'center',
- gap: SPACING[2],
- marginBottom: SPACING[4],
- },
- timelineBlock: {
- width: 40,
- height: 40,
- borderRadius: RADIUS.MD,
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: SURFACE.OVERLAY_LIGHT,
- },
- timelineText: {
- ...TYPOGRAPHY.overline,
- color: TEXT.PRIMARY,
- fontWeight: '700',
- },
- legendContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: SPACING[4],
- flexWrap: 'wrap',
- },
- legendItem: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[1],
- },
- legendDot: {
- width: 10,
- height: 10,
- borderRadius: 5,
- },
- legendText: {
- ...TYPOGRAPHY.caption,
- color: TEXT.MUTED,
- },
- explanationContainer: {
- paddingHorizontal: SPACING[4],
- marginTop: SPACING[4],
- },
- explanationText: {
- ...TYPOGRAPHY.body,
- color: TEXT.TERTIARY,
- textAlign: 'center',
- lineHeight: 26,
- },
- spacer: {
- flex: 1,
- },
-})
diff --git a/src/features/onboarding/screens/Screen4WowMoment.tsx b/src/features/onboarding/screens/Screen4WowMoment.tsx
deleted file mode 100644
index 0e20ca0..0000000
--- a/src/features/onboarding/screens/Screen4WowMoment.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import { useState, useEffect, useCallback } from 'react'
-import { StyleSheet, View, Text, Animated } from 'react-native'
-import * as Haptics from 'expo-haptics'
-import { OnboardingScreen } from '../components/OnboardingScreen'
-import { PrimaryButton } from '../components/PrimaryButton'
-import { MiniTimerDemo } from '../components/MiniTimerDemo'
-import { useOnboarding } from '../hooks/useOnboarding'
-import { TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-export function Screen4WowMoment() {
- const nextStep = useOnboarding((state) => state.nextStep)
- const [isComplete, setIsComplete] = useState(false)
- const [currentPhase, setCurrentPhase] = useState('IDLE')
- const fadeAnim = useState(new Animated.Value(0))[0]
-
- // Fade in animation for the button when complete
- useEffect(() => {
- if (isComplete) {
- Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
- Animated.timing(fadeAnim, {
- toValue: 1,
- duration: 400,
- useNativeDriver: true,
- }).start()
- }
- }, [isComplete, fadeAnim])
-
- const getInstructionText = (): string => {
- if (isComplete) {
- return 'Bravo ! Tu viens de completer ton premier mini-Tabata.'
- }
- if (currentPhase === 'GET_READY') {
- return 'Prepare-toi... Le timer va bientot commencer !'
- }
- if (currentPhase === 'WORK') {
- return 'Donne tout ! 20 secondes, c est parti !'
- }
- return 'Un mini-Tabata de 20 secondes. Juste pour sentir.'
- }
-
- const handlePhaseChange = useCallback((phase: string) => {
- setCurrentPhase(phase)
- if (phase === 'WORK') {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
- } else if (phase === 'GET_READY') {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
- }
- }, [])
-
- const handleCountdownTick = useCallback((seconds: number) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
- }, [])
-
- const handleComplete = useCallback(() => {
- setIsComplete(true)
- }, [])
-
- const handleContinue = () => {
- nextStep()
- }
-
- return (
-
-
- {/* Title Section */}
-
- Essaie maintenant
- 20 secondes. Juste pour sentir.
-
-
- {/* Timer Demo - The "Wow" Moment */}
-
-
-
-
- {/* Instruction Text */}
-
- {getInstructionText()}
-
-
- {/* Continue Button - Only visible after completion */}
-
- {isComplete ? (
-
-
-
- ) : (
-
- )}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: 'space-between',
- paddingTop: SPACING[10],
- },
- header: {
- alignItems: 'center',
- paddingHorizontal: SPACING[4],
- },
- title: {
- ...TYPOGRAPHY.displayLarge,
- color: TEXT.PRIMARY,
- textAlign: 'center',
- marginBottom: SPACING[3],
- },
- subtitle: {
- ...TYPOGRAPHY.body,
- color: TEXT.SECONDARY,
- textAlign: 'center',
- },
- timerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- instructionContainer: {
- paddingHorizontal: SPACING[6],
- paddingVertical: SPACING[4],
- alignItems: 'center',
- },
- instructionText: {
- ...TYPOGRAPHY.caption,
- color: TEXT.TERTIARY,
- textAlign: 'center',
- lineHeight: 24,
- },
- buttonContainer: {
- paddingHorizontal: SPACING[4],
- paddingBottom: SPACING[4],
- minHeight: 70,
- alignItems: 'center',
- justifyContent: 'center',
- },
- placeholderButton: {
- height: 54,
- },
-})
diff --git a/src/features/onboarding/screens/Screen5Personalization.tsx b/src/features/onboarding/screens/Screen5Personalization.tsx
deleted file mode 100644
index 1fb7ff1..0000000
--- a/src/features/onboarding/screens/Screen5Personalization.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { useState } from 'react'
-import { StyleSheet, View, Text, ScrollView } from 'react-native'
-import { Ionicons } from '@expo/vector-icons'
-import { useOnboarding } from '../hooks/useOnboarding'
-import { LEVELS } from '../data/levels'
-import { GOALS } from '../data/goals'
-import { OnboardingScreen } from '../components/OnboardingScreen'
-import { ChoiceButton } from '../components/ChoiceButton'
-import { PrimaryButton } from '../components/PrimaryButton'
-import { BRAND, TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-import type { Level, Goal, Frequency } from '../types'
-
-interface Screen5PersonalizationProps {
- onNext: () => void
-}
-
-interface FrequencyOption {
- id: Frequency
- label: string
- description: string
-}
-
-const FREQUENCIES: FrequencyOption[] = [
- { id: 2, label: '2x/semaine', description: 'Démarrage en douceur' },
- { id: 3, label: '3x/semaine', description: 'Rythme equilibre' },
- { id: 5, label: '5x/semaine', description: 'Entrainement intensif' },
-]
-
-export function Screen5Personalization({ onNext }: Screen5PersonalizationProps) {
- const { data, setLevel, setGoal, setFrequency } = useOnboarding()
-
- // Local state for selections
- const [selectedLevel, setSelectedLevel] = useState(data.level)
- const [selectedGoal, setSelectedGoal] = useState(data.goal)
- const [selectedFrequency, setSelectedFrequency] = useState(data.frequency)
-
- const handleLevelSelect = (level: Level) => {
- setSelectedLevel(level)
- setLevel(level)
- }
-
- const handleGoalSelect = (goal: Goal) => {
- setSelectedGoal(goal)
- setGoal(goal)
- }
-
- const handleFrequencySelect = (frequency: Frequency) => {
- setSelectedFrequency(frequency)
- setFrequency(frequency)
- }
-
- const handleContinue = () => {
- onNext()
- }
-
- const isFormComplete = selectedLevel && selectedGoal && selectedFrequency
-
- return (
-
-
-
- Personnalise ton experience
-
- Dis-nous en plus sur toi pour un programme sur mesure
-
-
-
- {/* Level Section */}
-
-
-
- Niveau
-
-
- {LEVELS.map((level) => (
- handleLevelSelect(level.id)}
- />
- ))}
-
-
-
- {/* Goal Section */}
-
-
-
- Objectif
-
-
- {GOALS.map((goal) => (
- handleGoalSelect(goal.id)}
- />
- ))}
-
-
-
- {/* Frequency Section */}
-
-
-
- Frequence
-
-
- {FREQUENCIES.map((freq) => (
- handleFrequencySelect(freq.id)}
- />
- ))}
-
-
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- scrollView: {
- flex: 1,
- },
- scrollContent: {
- paddingBottom: SPACING[6],
- },
- header: {
- marginBottom: SPACING[6],
- },
- title: {
- ...TYPOGRAPHY.heading,
- color: TEXT.PRIMARY,
- marginBottom: SPACING[2],
- },
- subtitle: {
- ...TYPOGRAPHY.caption,
- color: TEXT.MUTED,
- },
- section: {
- marginBottom: SPACING[6],
- },
- sectionHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING[2],
- marginBottom: SPACING[3],
- },
- sectionTitle: {
- ...TYPOGRAPHY.label,
- color: TEXT.SECONDARY,
- textTransform: 'uppercase',
- letterSpacing: 1,
- },
- optionsContainer: {
- gap: SPACING[3],
- },
- footer: {
- paddingTop: SPACING[4],
- },
-})
diff --git a/src/features/onboarding/screens/Screen6Paywall.tsx b/src/features/onboarding/screens/Screen6Paywall.tsx
deleted file mode 100644
index a2b5bc9..0000000
--- a/src/features/onboarding/screens/Screen6Paywall.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import { useState } from 'react'
-import { StyleSheet, View, Text, ScrollView, Pressable } from 'react-native'
-import { useOnboarding } from '../hooks/useOnboarding'
-import { OnboardingScreen } from '../components/OnboardingScreen'
-import { PaywallCard } from '../components/PaywallCard'
-import { BRAND, TEXT } from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SPACING } from '@/src/shared/constants/spacing'
-
-interface Screen6PaywallProps {
- onComplete: () => void
-}
-
-interface PlanOption {
- id: 'trial' | 'monthly' | 'annual'
- title: string
- price: string
- period: string
- features: string[]
- badge?: string
-}
-
-const PLANS: PlanOption[] = [
- {
- id: 'trial',
- title: 'Essai gratuit',
- price: '0',
- period: '7 jours',
- features: [
- 'Acces complet pendant 7 jours',
- 'Tous les programmes debloques',
- 'Annule a tout moment',
- ],
- },
- {
- id: 'monthly',
- title: 'Mensuel',
- price: '4.99',
- period: 'mois',
- features: [
- 'Acces illimite',
- 'Tous les programmes',
- 'Support prioritaire',
- 'Nouvelles fonctionnalites',
- ],
- },
- {
- id: 'annual',
- title: 'Annuel',
- price: '29.99',
- period: 'an',
- badge: 'Economise 50%',
- features: [
- 'Acces illimite',
- 'Tous les programmes',
- 'Support prioritaire',
- 'Nouvelles fonctionnalites',
- 'Entrainements exclusifs',
- ],
- },
-]
-
-export function Screen6Paywall({ onComplete }: Screen6PaywallProps) {
- const { completeOnboarding } = useOnboarding()
- const [selectedPlan, setSelectedPlan] = useState<'trial' | 'monthly' | 'annual'>('annual')
- const [isLoading, setIsLoading] = useState(false)
-
- const handlePlanSelect = (planId: 'trial' | 'monthly' | 'annual') => {
- setSelectedPlan(planId)
- }
-
- const handlePurchase = async () => {
- setIsLoading(true)
-
- try {
- // Mock RevenueCat purchase in dev mode
- // In production, this would call the actual RevenueCat SDK
- await mockPurchase(selectedPlan)
-
- // Complete onboarding and navigate to home
- completeOnboarding()
- onComplete()
- } catch (error) {
- console.error('Purchase failed:', error)
- // In production, show error to user
- } finally {
- setIsLoading(false)
- }
- }
-
- const handleSkip = () => {
- // Skip paywall and go to home
- completeOnboarding()
- onComplete()
- }
-
- return (
-
-
-
- Debloque ton potentiel
-
- 7 jours gratuits, annule quand tu veux
-
-
-
-
- {PLANS.map((plan) => (
- handlePlanSelect(plan.id)}
- />
- ))}
-
-
-
-
- Garantie satisfait ou rembourse sous 30 jours
-
-
-
-
-
-
-
- {isLoading ? 'Traitement...' : selectedPlan === 'trial' ? 'Essayer gratuitement' : "S'abonner"}
-
-
-
-
-
- Continuer sans abonnement
-
-
-
-
- )
-}
-
-// Mock RevenueCat purchase function for dev mode
-async function mockPurchase(planId: string): Promise {
- // Simulate network delay
- await new Promise((resolve) => setTimeout(resolve, 1500))
-
- // Simulate successful purchase
- console.log(`[MOCK] Purchase successful for plan: ${planId}`)
-
- // In production with RevenueCat:
- // const { customerInfo } = await Purchases.purchasePackage(package)
- // if (customerInfo.entitlements.active['pro']) { ... }
-}
-
-const styles = StyleSheet.create({
- scrollView: {
- flex: 1,
- },
- scrollContent: {
- paddingBottom: SPACING[6],
- },
- header: {
- marginBottom: SPACING[6],
- alignItems: 'center',
- },
- title: {
- ...TYPOGRAPHY.heading,
- color: TEXT.PRIMARY,
- marginBottom: SPACING[2],
- textAlign: 'center',
- },
- subtitle: {
- ...TYPOGRAPHY.caption,
- color: TEXT.MUTED,
- textAlign: 'center',
- },
- plansContainer: {
- gap: SPACING[4],
- },
- guarantee: {
- marginTop: SPACING[6],
- alignItems: 'center',
- },
- guaranteeText: {
- ...TYPOGRAPHY.caption,
- color: TEXT.HINT,
- textAlign: 'center',
- },
- footer: {
- paddingTop: SPACING[4],
- },
- continueButton: {
- backgroundColor: BRAND.PRIMARY,
- borderRadius: 28,
- paddingVertical: 18,
- paddingHorizontal: 32,
- alignItems: 'center',
- justifyContent: 'center',
- shadowColor: BRAND.PRIMARY,
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.4,
- shadowRadius: 12,
- elevation: 8,
- },
- buttonLoading: {
- opacity: 0.7,
- },
- continueButtonText: {
- ...TYPOGRAPHY.buttonMedium,
- color: TEXT.PRIMARY,
- },
- skipButton: {
- marginTop: SPACING[4],
- paddingVertical: SPACING[2],
- alignItems: 'center',
- },
- skipButtonText: {
- ...TYPOGRAPHY.caption,
- color: TEXT.HINT,
- textDecorationLine: 'underline',
- },
-})
diff --git a/src/features/onboarding/screens/index.ts b/src/features/onboarding/screens/index.ts
deleted file mode 100644
index cdb24e2..0000000
--- a/src/features/onboarding/screens/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { Screen5Personalization } from './Screen5Personalization'
-export { Screen6Paywall } from './Screen6Paywall'
diff --git a/src/features/onboarding/types.ts b/src/features/onboarding/types.ts
deleted file mode 100644
index 80c73a2..0000000
--- a/src/features/onboarding/types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export type Barrier = 'time' | 'motivation' | 'knowledge' | 'gym'
-
-export type Level = 'beginner' | 'intermediate' | 'advanced'
-
-export type Goal = 'weight_loss' | 'cardio' | 'strength' | 'wellness'
-
-export type Frequency = 2 | 3 | 5
-
-export interface OnboardingData {
- barrier: Barrier | null
- level: Level | null
- goal: Goal | null
- frequency: Frequency | null
-}
-
-export interface OnboardingState {
- currentStep: number
- isOnboardingComplete: boolean
- data: OnboardingData
-}
diff --git a/src/features/timer/components/TimerControls.tsx b/src/features/timer/components/TimerControls.tsx
deleted file mode 100644
index 46448a2..0000000
--- a/src/features/timer/components/TimerControls.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { Pressable, StyleSheet, View } from 'react-native'
-import Ionicons from '@expo/vector-icons/Ionicons'
-
-import { SURFACE, TEXT } from '@/src/shared/constants/colors'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { LAYOUT } from '@/src/shared/constants/spacing'
-
-interface TimerControlsProps {
- isRunning: boolean
- isPaused: boolean
- onPause: () => void
- onResume: () => void
- onStop: () => void
- onSkip: () => void
-}
-
-export function TimerControls({
- isRunning,
- isPaused,
- onPause,
- onResume,
- onStop,
- onSkip,
-}: TimerControlsProps) {
- return (
-
- [styles.button, pressed && styles.pressed]}
- onPress={onStop}
- >
-
-
-
- [
- styles.button,
- styles.mainButton,
- pressed && styles.pressed,
- ]}
- onPress={isPaused ? onResume : onPause}
- >
-
-
-
- [styles.button, pressed && styles.pressed]}
- onPress={onSkip}
- >
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: LAYOUT.CONTROLS_GAP,
- },
- button: {
- width: 56,
- height: 56,
- borderRadius: RADIUS['2XL'],
- backgroundColor: SURFACE.OVERLAY_MEDIUM,
- alignItems: 'center',
- justifyContent: 'center',
- },
- mainButton: {
- width: 72,
- height: 72,
- borderRadius: RADIUS['4XL'],
- backgroundColor: SURFACE.OVERLAY_STRONG,
- },
- pressed: {
- transform: [{ scale: 0.92 }],
- },
-})
diff --git a/src/features/timer/components/TimerDisplay.tsx b/src/features/timer/components/TimerDisplay.tsx
deleted file mode 100644
index 09a6b12..0000000
--- a/src/features/timer/components/TimerDisplay.tsx
+++ /dev/null
@@ -1,615 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-import {
- Animated,
- Pressable,
- StyleSheet,
- Text,
- View,
-} from 'react-native'
-import { LinearGradient } from 'expo-linear-gradient'
-import { BlurView } from 'expo-blur'
-import { StatusBar } from 'expo-status-bar'
-import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import {
- PHASE_GRADIENTS,
- ACCENT,
- BRAND,
- SURFACE,
- TEXT,
- BORDER,
- APP_GRADIENTS,
- GLASS,
-} from '@/src/shared/constants/colors'
-import { TYPOGRAPHY } from '@/src/shared/constants/typography'
-import { SHADOW, TEXT_SHADOW } from '@/src/shared/constants/shadows'
-import { RADIUS } from '@/src/shared/constants/borderRadius'
-import { LAYOUT, SPACING } from '@/src/shared/constants/spacing'
-import { SPRING, ANIMATION } from '@/src/shared/constants/animations'
-import { formatTime } from '@/src/shared/utils/formatTime'
-import type { TimerConfig, TimerPhase, TimerState } from '../types'
-import { TimerControls } from './TimerControls'
-
-const PHASE_LABELS: Record = {
- IDLE: '',
- GET_READY: 'PRĂPARE-TOI',
- WORK: 'GO !',
- REST: 'REPOS',
- COMPLETE: 'TERMINĂ !',
-}
-
-interface TimerDisplayProps {
- state: TimerState
- config: TimerConfig
- exerciseName: string
- nextExerciseName: string
- onStart: () => void
- onPause: () => void
- onResume: () => void
- onStop: () => void
- onSkip: () => void
-}
-
-export function TimerDisplay({
- state,
- config,
- exerciseName,
- nextExerciseName,
- onStart,
- onPause,
- onResume,
- onStop,
- onSkip,
-}: TimerDisplayProps) {
- const insets = useSafeAreaInsets()
- const top = insets.top || 20
- const bottom = insets.bottom || 20
-
- if (state.phase === 'IDLE') {
- return
- }
-
- if (state.phase === 'COMPLETE') {
- return (
-
- )
- }
-
- return (
-
- )
-}
-
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-// IDLE â Start screen
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-
-function IdleView({
- config,
- onStart,
- top,
- bottom,
-}: {
- config: TimerConfig
- onStart: () => void
- top: number
- bottom: number
-}) {
- const glowAnim = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- Animated.loop(
- Animated.sequence([
- Animated.timing(glowAnim, {
- toValue: 1,
- ...ANIMATION.BREATH_HALF,
- }),
- Animated.timing(glowAnim, {
- toValue: 0,
- ...ANIMATION.BREATH_HALF,
- }),
- ])
- ).start()
- }, [])
-
- const glowOpacity = glowAnim.interpolate({
- inputRange: [0, 1],
- outputRange: [0.15, 0.4],
- })
-
- const glowScale = glowAnim.interpolate({
- inputRange: [0, 1],
- outputRange: [1, 1.12],
- })
-
- return (
-
-
-
-
- TABATA
- GO
-
-
-
-
-
-
-
-
-
-
-
- [
- styles.startButton,
- pressed && styles.buttonPressed,
- ]}
- onPress={onStart}
- >
- START
-
-
-
-
- )
-}
-
-function ConfigBadge({ label, sub }: { label: string; sub: string }) {
- return (
-
- {label}
- {sub}
-
- )
-}
-
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-// COMPLETE â Victory screen
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-
-function CompleteView({
- totalElapsedSeconds,
- totalRounds,
- onStop,
- top,
- bottom,
-}: {
- totalElapsedSeconds: number
- totalRounds: number
- onStop: () => void
- top: number
- bottom: number
-}) {
- const scaleAnim = useRef(new Animated.Value(0.5)).current
- const opacityAnim = useRef(new Animated.Value(0)).current
-
- useEffect(() => {
- Animated.parallel([
- Animated.spring(scaleAnim, {
- toValue: 1,
- ...SPRING.BOUNCY,
- useNativeDriver: true,
- }),
- Animated.timing(opacityAnim, ANIMATION.FADE_IN),
- ]).start()
- }, [])
-
- return (
-
-
-
-
- 🔥
- TERMINĂ !
- {formatTime(totalElapsedSeconds)}
-
- {totalRounds} rounds complétés
-
-
- [
- styles.doneButton,
- pressed && styles.buttonPressed,
- ]}
- onPress={onStop}
- >
- Terminer
-
-
-
- )
-}
-
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-// ACTIVE â Countdown with native blur toolbar
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-
-function ActiveView({
- state,
- exerciseName,
- nextExerciseName,
- onPause,
- onResume,
- onStop,
- onSkip,
- top,
- bottom,
-}: {
- state: TimerState
- exerciseName: string
- nextExerciseName: string
- onPause: () => void
- onResume: () => void
- onStop: () => void
- onSkip: () => void
- top: number
- bottom: number
-}) {
- const gradient = PHASE_GRADIENTS[state.phase] ?? PHASE_GRADIENTS.IDLE
-
- // --- Gradient crossfade ---
- const [prevGradient, setPrevGradient] = useState(PHASE_GRADIENTS.IDLE)
- const fadeAnim = useRef(new Animated.Value(1)).current
-
- useEffect(() => {
- fadeAnim.setValue(0)
- Animated.timing(fadeAnim, ANIMATION.GRADIENT_CROSSFADE).start(() => {
- setPrevGradient(gradient)
- })
- }, [state.phase])
-
- // --- Countdown pulse ---
- const pulseAnim = useRef(new Animated.Value(1)).current
-
- useEffect(() => {
- if (state.isRunning) {
- Animated.sequence([
- Animated.timing(pulseAnim, ANIMATION.PULSE_UP),
- Animated.timing(pulseAnim, ANIMATION.PULSE_DOWN),
- ]).start()
- }
- }, [state.secondsLeft])
-
- const isLastSeconds = state.secondsLeft <= 3 && state.secondsLeft > 0
-
- const topLabel =
- state.phase === 'GET_READY'
- ? exerciseName
- : state.phase === 'REST'
- ? `Prochain : ${nextExerciseName}`
- : exerciseName
-
- return (
-
-
-
- {/* Gradient layers */}
-
-
-
-
-
- {/* Content */}
-
- {/* Top bar */}
-
-
- {topLabel}
-
- {state.currentRound > 0 && (
-
-
- {state.currentRound}/{state.totalRounds}
-
-
- )}
-
-
- {/* Center â countdown */}
-
-
- {PHASE_LABELS[state.phase]}
-
-
-
- {state.secondsLeft}
-
-
- {state.isPaused && (
-
- EN PAUSE
-
- )}
-
-
- {/* Bottom toolbar â native blur like iOS UIToolbar */}
-
-
- {Array.from({ length: state.totalRounds }, (_, i) => (
-
- ))}
-
-
-
-
-
-
- )
-}
-
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-// Styles
-// ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
-
-const styles = StyleSheet.create({
- screen: {
- flex: 1,
- },
-
- // --- IDLE ---
- idleContent: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- },
- idleTitle: {
- ...TYPOGRAPHY.brandTitle,
- color: ACCENT.ORANGE,
- ...TEXT_SHADOW.BRAND,
- },
- idleSubtitle: {
- ...TYPOGRAPHY.displaySmall,
- color: ACCENT.WHITE,
- marginTop: -6,
- },
- configSummary: {
- marginTop: SPACING[10],
- },
- configRow: {
- flexDirection: 'row',
- gap: SPACING[3],
- },
- configBadge: {
- backgroundColor: SURFACE.OVERLAY_LIGHT,
- borderRadius: RADIUS.LG,
- paddingVertical: SPACING[3],
- paddingHorizontal: SPACING[5],
- alignItems: 'center',
- borderWidth: 1,
- borderColor: BORDER.LIGHT,
- },
- configBadgeLabel: {
- ...TYPOGRAPHY.heading,
- color: ACCENT.WHITE,
- },
- configBadgeSub: {
- ...TYPOGRAPHY.overline,
- color: TEXT.MUTED,
- marginTop: 2,
- },
- startButtonContainer: {
- marginTop: SPACING[14],
- alignItems: 'center',
- justifyContent: 'center',
- },
- startButtonGlow: {
- position: 'absolute',
- width: 172,
- height: 172,
- borderRadius: 86,
- backgroundColor: ACCENT.ORANGE,
- },
- startButton: {
- width: 150,
- height: 150,
- borderRadius: 75,
- backgroundColor: ACCENT.ORANGE,
- alignItems: 'center',
- justifyContent: 'center',
- ...SHADOW.BRAND_GLOW,
- },
- buttonPressed: {
- transform: [{ scale: 0.95 }],
- },
- startButtonText: {
- ...TYPOGRAPHY.buttonHero,
- color: ACCENT.WHITE,
- },
-
- // --- COMPLETE ---
- completeContent: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- gap: SPACING[2],
- },
- completeEmoji: {
- fontSize: 72,
- marginBottom: SPACING[2],
- },
- completeTitle: {
- ...TYPOGRAPHY.displayLarge,
- color: ACCENT.WHITE,
- },
- completeTime: {
- ...TYPOGRAPHY.timeDisplay,
- color: ACCENT.WHITE,
- ...TEXT_SHADOW.WHITE_MEDIUM,
- },
- completeRounds: {
- ...TYPOGRAPHY.body,
- color: TEXT.TERTIARY,
- fontWeight: '600',
- },
- doneButton: {
- marginTop: SPACING[10],
- paddingHorizontal: SPACING[12],
- paddingVertical: SPACING[4],
- borderRadius: RADIUS['3XL'],
- backgroundColor: SURFACE.OVERLAY_MEDIUM,
- borderWidth: 1,
- borderColor: BORDER.STRONG,
- },
- doneButtonText: {
- ...TYPOGRAPHY.buttonMedium,
- color: ACCENT.WHITE,
- },
-
- // --- ACTIVE ---
- activeContent: {
- flex: 1,
- },
- topZone: {
- paddingHorizontal: LAYOUT.PAGE_HORIZONTAL,
- paddingTop: SPACING[3],
- paddingBottom: SPACING[2],
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- exerciseName: {
- ...TYPOGRAPHY.body,
- color: TEXT.SECONDARY,
- flex: 1,
- },
- roundBadge: {
- backgroundColor: SURFACE.OVERLAY_MEDIUM,
- borderRadius: RADIUS.MD,
- paddingVertical: SPACING[1.5],
- paddingHorizontal: SPACING[3.5],
- marginLeft: SPACING[3],
- },
- roundBadgeText: {
- ...TYPOGRAPHY.label,
- color: ACCENT.WHITE,
- },
-
- centerZone: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- },
- phaseLabel: {
- ...TYPOGRAPHY.heading,
- color: TEXT.TERTIARY,
- letterSpacing: 6,
- marginBottom: SPACING[1],
- },
- countdown: {
- ...TYPOGRAPHY.countdown,
- color: ACCENT.WHITE,
- ...TEXT_SHADOW.WHITE_SOFT,
- },
- countdownFlash: {
- color: ACCENT.RED_HOT,
- ...TEXT_SHADOW.DANGER,
- },
- pausedContainer: {
- marginTop: SPACING[4],
- paddingVertical: SPACING[2],
- paddingHorizontal: SPACING[6],
- borderRadius: RADIUS.XL,
- backgroundColor: SURFACE.SCRIM,
- },
- pausedLabel: {
- ...TYPOGRAPHY.caption,
- fontWeight: '800',
- color: TEXT.MUTED,
- letterSpacing: 6,
- },
-
- // --- Bottom toolbar (native blur) ---
- bottomBar: {
- borderTopWidth: StyleSheet.hairlineWidth,
- borderTopColor: BORDER.MEDIUM,
- paddingTop: SPACING[4],
- gap: SPACING[5],
- },
- roundDots: {
- flexDirection: 'row',
- justifyContent: 'center',
- gap: SPACING[2.5],
- },
- dot: {
- width: 8,
- height: 8,
- borderRadius: RADIUS.XS,
- backgroundColor: SURFACE.OVERLAY_MEDIUM,
- },
- dotFilled: {
- backgroundColor: TEXT.SECONDARY,
- },
- dotActive: {
- backgroundColor: ACCENT.WHITE,
- width: 12,
- height: 12,
- borderRadius: SPACING[1.5],
- ...SHADOW.WHITE_GLOW,
- },
-})
diff --git a/src/features/timer/hooks/useTimerEngine.ts b/src/features/timer/hooks/useTimerEngine.ts
deleted file mode 100644
index 933d135..0000000
--- a/src/features/timer/hooks/useTimerEngine.ts
+++ /dev/null
@@ -1,380 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
-import { AppState, type AppStateStatus } from 'react-native'
-import {
- activateKeepAwakeAsync,
- deactivateKeepAwake,
-} from 'expo-keep-awake'
-import { TIMER_DEFAULTS, TICK_INTERVAL_MS } from '@/src/shared/constants/timer'
-import type {
- TimerConfig,
- TimerEngine,
- TimerEvent,
- TimerEventListener,
- TimerPhase,
-} from '../types'
-
-const VALID_TRANSITIONS: Record = {
- IDLE: ['GET_READY'],
- GET_READY: ['WORK', 'IDLE'],
- WORK: ['REST', 'IDLE'],
- REST: ['WORK', 'COMPLETE', 'IDLE'],
- COMPLETE: ['IDLE'],
-}
-
-function canTransition(from: TimerPhase, to: TimerPhase): boolean {
- return VALID_TRANSITIONS[from].includes(to)
-}
-
-function buildDefaultConfig(): TimerConfig {
- return {
- workDuration: TIMER_DEFAULTS.WORK_DURATION,
- restDuration: TIMER_DEFAULTS.REST_DURATION,
- rounds: TIMER_DEFAULTS.ROUNDS,
- getReadyDuration: TIMER_DEFAULTS.GET_READY_DURATION,
- cycles: TIMER_DEFAULTS.CYCLES,
- cyclePauseDuration: TIMER_DEFAULTS.CYCLE_PAUSE_DURATION,
- }
-}
-
-function getDurationForPhase(phase: TimerPhase, config: TimerConfig): number {
- switch (phase) {
- case 'GET_READY':
- return config.getReadyDuration
- case 'WORK':
- return config.workDuration
- case 'REST':
- return config.restDuration
- default:
- return 0
- }
-}
-
-export function useTimerEngine(): TimerEngine {
- // --- UI state (triggers re-renders) ---
- const [phase, setPhase] = useState('IDLE')
- const [secondsLeft, setSecondsLeft] = useState(0)
- const [currentRound, setCurrentRound] = useState(0)
- const [currentCycle, setCurrentCycle] = useState(0)
- const [isRunning, setIsRunning] = useState(false)
- const [isPaused, setIsPaused] = useState(false)
- const [totalElapsedSeconds, setTotalElapsedSeconds] = useState(0)
- const [config, setConfig] = useState(buildDefaultConfig)
-
- // --- Refs for time-critical values (no stale closures) ---
- const targetEndTimeRef = useRef(0)
- const remainingMsRef = useRef(0)
- const tickRef = useRef | null>(null)
- const phaseRef = useRef('IDLE')
- const currentRoundRef = useRef(0)
- const currentCycleRef = useRef(0)
- const configRef = useRef(buildDefaultConfig())
- const isRunningRef = useRef(false)
- const isPausedRef = useRef(false)
- const elapsedRef = useRef(0)
- const lastTickTimeRef = useRef(0)
- const lastEmittedSecondRef = useRef(-1)
- const listenersRef = useRef>(new Set())
-
- // --- Helpers ---
-
- function emit(event: TimerEvent): void {
- listenersRef.current.forEach((listener) => {
- try {
- listener(event)
- } catch (e) {
- if (__DEV__) console.warn('[TimerEngine] Listener error:', e)
- }
- })
- }
-
- function clearTick(): void {
- if (tickRef.current !== null) {
- clearTimeout(tickRef.current)
- tickRef.current = null
- }
- }
-
- function transitionTo(nextPhase: TimerPhase): void {
- const prevPhase = phaseRef.current
- if (!canTransition(prevPhase, nextPhase)) {
- if (__DEV__) {
- console.warn(
- `[TimerEngine] Invalid transition: ${prevPhase} â ${nextPhase}`
- )
- }
- return
- }
-
- if (__DEV__) {
- console.log(`[TimerEngine] ${prevPhase} â ${nextPhase}`)
- }
-
- phaseRef.current = nextPhase
- setPhase(nextPhase)
-
- emit({ type: 'PHASE_CHANGED', from: prevPhase, to: nextPhase })
-
- const duration = getDurationForPhase(nextPhase, configRef.current)
- if (duration > 0) {
- startCountdown(duration)
- } else {
- // Phase with 0 duration - immediately advance to next phase
- advancePhase()
- }
- }
-
- function startCountdown(durationSeconds: number): void {
- clearTick()
- targetEndTimeRef.current = Date.now() + durationSeconds * 1000
- lastEmittedSecondRef.current = -1
- scheduleTick()
- }
-
- function scheduleTick(): void {
- tickRef.current = setTimeout(tick, TICK_INTERVAL_MS)
- }
-
- function tick(): void {
- const now = Date.now()
-
- // Accumulate elapsed time
- if (lastTickTimeRef.current > 0) {
- const delta = (now - lastTickTimeRef.current) / 1000
- elapsedRef.current += delta
- setTotalElapsedSeconds(Math.floor(elapsedRef.current))
- }
- lastTickTimeRef.current = now
-
- const remainingMs = Math.max(0, targetEndTimeRef.current - now)
- const seconds = Math.ceil(remainingMs / 1000)
-
- setSecondsLeft(seconds)
-
- // Emit COUNTDOWN_TICK for the last 3 seconds (once per second)
- if (seconds <= 3 && seconds > 0 && seconds !== lastEmittedSecondRef.current) {
- lastEmittedSecondRef.current = seconds
- emit({ type: 'COUNTDOWN_TICK', secondsLeft: seconds })
- }
-
- if (remainingMs <= 0) {
- advancePhase()
- } else {
- scheduleTick()
- }
- }
-
- function advancePhase(): void {
- clearTick()
- const current = phaseRef.current
- const cfg = configRef.current
-
- switch (current) {
- case 'GET_READY':
- currentRoundRef.current = 1
- setCurrentRound(1)
- transitionTo('WORK')
- break
-
- case 'WORK':
- transitionTo('REST')
- break
-
- case 'REST': {
- const round = currentRoundRef.current
- emit({ type: 'ROUND_COMPLETED', round })
-
- if (round < cfg.rounds) {
- // Next round
- currentRoundRef.current = round + 1
- setCurrentRound(round + 1)
- transitionTo('WORK')
- } else if (currentCycleRef.current < cfg.cycles) {
- // Next cycle (Premium, future) â for V1 cycles=1, so this is dead code
- currentCycleRef.current += 1
- setCurrentCycle(currentCycleRef.current)
- currentRoundRef.current = 1
- setCurrentRound(1)
- transitionTo('WORK')
- } else {
- // Session complete
- const totalSeconds = Math.floor(elapsedRef.current)
- setTotalElapsedSeconds(totalSeconds)
- emit({ type: 'SESSION_COMPLETE', totalSeconds })
- transitionTo('COMPLETE')
- setIsRunning(false)
- isRunningRef.current = false
- }
- break
- }
-
- default:
- break
- }
- }
-
- // --- Actions ---
-
- const start = useCallback((overrides?: Partial) => {
- if (phaseRef.current !== 'IDLE') return
-
- const cfg: TimerConfig = { ...buildDefaultConfig(), ...overrides }
- configRef.current = cfg
- setConfig(cfg)
-
- // Reset all state
- currentRoundRef.current = 0
- currentCycleRef.current = 1
- elapsedRef.current = 0
- lastTickTimeRef.current = Date.now()
- lastEmittedSecondRef.current = -1
-
- setCurrentRound(0)
- setCurrentCycle(1)
- setTotalElapsedSeconds(0)
- setIsRunning(true)
- setIsPaused(false)
- isRunningRef.current = true
- isPausedRef.current = false
-
- transitionTo('GET_READY')
- }, [])
-
- const pause = useCallback(() => {
- if (!isRunningRef.current || isPausedRef.current) return
-
- clearTick()
- remainingMsRef.current = Math.max(0, targetEndTimeRef.current - Date.now())
-
- isPausedRef.current = true
- setIsPaused(true)
- setIsRunning(false)
- isRunningRef.current = false
-
- if (__DEV__) {
- console.log('[TimerEngine] Paused, remaining:', remainingMsRef.current, 'ms')
- }
- }, [])
-
- const resume = useCallback(() => {
- if (!isPausedRef.current) return
-
- targetEndTimeRef.current = Date.now() + remainingMsRef.current
- lastTickTimeRef.current = Date.now()
-
- isPausedRef.current = false
- isRunningRef.current = true
- setIsPaused(false)
- setIsRunning(true)
-
- scheduleTick()
-
- if (__DEV__) {
- console.log('[TimerEngine] Resumed')
- }
- }, [])
-
- const stop = useCallback(() => {
- if (phaseRef.current === 'IDLE') return
-
- clearTick()
-
- phaseRef.current = 'IDLE'
- isRunningRef.current = false
- isPausedRef.current = false
- lastTickTimeRef.current = 0
-
- setPhase('IDLE')
- setSecondsLeft(0)
- setCurrentRound(0)
- setCurrentCycle(0)
- setIsRunning(false)
- setIsPaused(false)
-
- if (__DEV__) {
- console.log('[TimerEngine] Stopped')
- }
- }, [])
-
- const skip = useCallback(() => {
- if (phaseRef.current === 'IDLE' || phaseRef.current === 'COMPLETE') return
- if (!isRunningRef.current && !isPausedRef.current) return
-
- // If paused, un-pause first
- if (isPausedRef.current) {
- isPausedRef.current = false
- isRunningRef.current = true
- setIsPaused(false)
- setIsRunning(true)
- }
-
- clearTick()
- advancePhase()
- }, [])
-
- const addEventListener = useCallback((listener: TimerEventListener) => {
- listenersRef.current.add(listener)
- return () => {
- listenersRef.current.delete(listener)
- }
- }, [])
-
- // --- AppState: auto-pause on interruption ---
- // iOS: active â inactive (phone call, control center) â background
- // Android: active â background (directly)
- // With auto-pause, we pause on any departure from 'active'.
- // User must manually resume â no reconcile needed.
-
- useEffect(() => {
- const handleAppState = (nextState: AppStateStatus) => {
- if (nextState !== 'active') {
- if (isRunningRef.current && !isPausedRef.current) {
- pause()
- if (__DEV__) {
- console.log(`[TimerEngine] Auto-paused (${nextState})`)
- }
- }
- }
- }
-
- const subscription = AppState.addEventListener('change', handleAppState)
- return () => subscription.remove()
- }, [pause])
-
- // --- Keep-awake ---
-
- useEffect(() => {
- if (isRunning || isPaused) {
- activateKeepAwakeAsync('tabata-session')
- } else {
- deactivateKeepAwake('tabata-session')
- }
- }, [isRunning, isPaused])
-
- // --- Cleanup on unmount ---
-
- useEffect(() => {
- return () => {
- clearTick()
- deactivateKeepAwake('tabata-session')
- }
- }, [])
-
- return {
- phase,
- secondsLeft,
- currentRound,
- totalRounds: config.rounds,
- currentCycle,
- totalCycles: config.cycles,
- isRunning,
- isPaused,
- totalElapsedSeconds,
- start,
- pause,
- resume,
- stop,
- skip,
- addEventListener,
- config,
- }
-}
diff --git a/src/features/timer/index.ts b/src/features/timer/index.ts
deleted file mode 100644
index 138f7db..0000000
--- a/src/features/timer/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export { useTimerEngine } from './hooks/useTimerEngine'
-export { TimerDisplay } from './components/TimerDisplay'
-export { TimerControls } from './components/TimerControls'
-export type {
- TimerPhase,
- TimerConfig,
- TimerState,
- TimerActions,
- TimerEvent,
- TimerEventListener,
- TimerEngine,
-} from './types'
diff --git a/src/features/timer/types.ts b/src/features/timer/types.ts
deleted file mode 100644
index 1cfa0ae..0000000
--- a/src/features/timer/types.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-export type TimerPhase = 'IDLE' | 'GET_READY' | 'WORK' | 'REST' | 'COMPLETE'
-
-export interface TimerConfig {
- workDuration: number
- restDuration: number
- rounds: number
- getReadyDuration: number
- cycles: number
- cyclePauseDuration: number
-}
-
-export interface TimerState {
- phase: TimerPhase
- secondsLeft: number
- currentRound: number
- totalRounds: number
- currentCycle: number
- totalCycles: number
- isRunning: boolean
- isPaused: boolean
- totalElapsedSeconds: number
-}
-
-export interface TimerActions {
- start: (config?: Partial) => void
- pause: () => void
- resume: () => void
- stop: () => void
- skip: () => void
-}
-
-export type TimerEvent =
- | { type: 'PHASE_CHANGED'; from: TimerPhase; to: TimerPhase }
- | { type: 'ROUND_COMPLETED'; round: number }
- | { type: 'SESSION_COMPLETE'; totalSeconds: number }
- | { type: 'COUNTDOWN_TICK'; secondsLeft: number }
-
-export type TimerEventListener = (event: TimerEvent) => void
-
-export interface TimerEngine extends TimerState, TimerActions {
- addEventListener: (listener: TimerEventListener) => () => void
- config: TimerConfig
-}
diff --git a/src/shared/components/GlassView.tsx b/src/shared/components/GlassView.tsx
deleted file mode 100644
index 5341336..0000000
--- a/src/shared/components/GlassView.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { StyleSheet, View, type ViewProps } from 'react-native'
-import { BlurView } from 'expo-blur'
-import { GLASS } from '../constants/colors'
-
-interface GlassViewProps extends ViewProps {
- intensity?: number
- children: React.ReactNode
-}
-
-export function GlassView({
- intensity = GLASS.BLUR_MEDIUM,
- style,
- children,
- ...rest
-}: GlassViewProps) {
- return (
-
-
- {children}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- overflow: 'hidden',
- backgroundColor: GLASS.FILL,
- borderWidth: 0.5,
- borderColor: GLASS.BORDER,
- borderTopColor: GLASS.BORDER_TOP,
- },
-})
diff --git a/src/shared/components/Typography.tsx b/src/shared/components/Typography.tsx
deleted file mode 100644
index 4f5d1b6..0000000
--- a/src/shared/components/Typography.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Text, type TextProps } from 'react-native'
-import { TYPOGRAPHY } from '../constants/typography'
-import { TEXT } from '../constants/colors'
-
-type TypographyVariant = keyof typeof TYPOGRAPHY
-
-interface TypographyProps extends TextProps {
- variant: TypographyVariant
- color?: string
- children: React.ReactNode
-}
-
-export function Typography({
- variant,
- color = TEXT.PRIMARY,
- style,
- children,
- ...rest
-}: TypographyProps) {
- return (
-
- {children}
-
- )
-}
diff --git a/src/shared/constants/shadows.ts b/src/shared/constants/shadows.ts
deleted file mode 100644
index f9fd811..0000000
--- a/src/shared/constants/shadows.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { TextStyle, ViewStyle } from 'react-native'
-
-export const SHADOW = {
- BRAND_GLOW: {
- shadowColor: '#F97316',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.5,
- shadowRadius: 20,
- elevation: 12,
- } as ViewStyle,
-
- WHITE_GLOW: {
- shadowColor: '#FFFFFF',
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0.8,
- shadowRadius: 6,
- } as ViewStyle,
-} as const
-
-export const TEXT_SHADOW = {
- BRAND: {
- textShadowColor: 'rgba(249, 115, 22, 0.5)',
- textShadowOffset: { width: 0, height: 0 },
- textShadowRadius: 30,
- } as TextStyle,
-
- WHITE_SOFT: {
- textShadowColor: 'rgba(255, 255, 255, 0.25)',
- textShadowOffset: { width: 0, height: 0 },
- textShadowRadius: 30,
- } as TextStyle,
-
- WHITE_MEDIUM: {
- textShadowColor: 'rgba(255, 255, 255, 0.3)',
- textShadowOffset: { width: 0, height: 0 },
- textShadowRadius: 20,
- } as TextStyle,
-
- DANGER: {
- textShadowColor: 'rgba(239, 68, 68, 0.6)',
- textShadowOffset: { width: 0, height: 0 },
- textShadowRadius: 40,
- } as TextStyle,
-} as const
diff --git a/src/shared/constants/timer.ts b/src/shared/constants/timer.ts
deleted file mode 100644
index cb8658b..0000000
--- a/src/shared/constants/timer.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const TIMER_DEFAULTS = {
- WORK_DURATION: 20,
- REST_DURATION: 10,
- ROUNDS: 8,
- GET_READY_DURATION: 10,
- CYCLES: 1,
- CYCLE_PAUSE_DURATION: 60,
-} as const
-
-export const TICK_INTERVAL_MS = 100
diff --git a/src/shared/utils/formatTime.ts b/src/shared/utils/formatTime.ts
deleted file mode 100644
index 58ae655..0000000
--- a/src/shared/utils/formatTime.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export function formatTime(totalSeconds: number): string {
- const mins = Math.floor(totalSeconds / 60)
- const secs = totalSeconds % 60
- if (mins > 0) {
- return `${mins}:${secs.toString().padStart(2, '0')}`
- }
- return `${secs}`
-}