Navigation et écrans
L'application mobile utilise React Navigation 7 avec deux types de navigateurs : des piles natives (createNativeStackNavigator) et une barre d'onglets (createBottomTabNavigator) dessinée par un composant maison. Tous les en-têtes natifs sont masqués (headerShown: false) : chaque écran affiche son propre en-tête avec ScreenHeader et, si besoin, un bouton retour.
Les trois navigateurs sont dans mobile-app/src/navigation. Leurs paramètres sont typés dans mobile-app/src/types/navigation.types.ts, et mobile-app/src/navigation/hooks.ts fournit deux hooks typés :
| Hook | Type renvoyé | Utilisé par |
|---|---|---|
useRootNavigation() |
Composition de la pile racine (RootStackParamList) et des onglets (MainTabsParamList) |
Écrans connectés : un même objet ouvre un écran empilé ou change d'onglet |
useAuthNavigation() |
Pile d'authentification (AuthStackParamList) |
Écrans non connectés |
Les écrans ne reçoivent plus de prop navigation: any : un nom de route ou un paramètre erroné est refusé par npm run typecheck.
Arbre de navigation
flowchart TD ROOT["RootNavigator : pile racine"] -->|"isAuthenticated faux"| AUTH["AuthStack"] ROOT -->|"isAuthenticated vrai"| MAIN["Main : MainTabs"] ROOT -->|"connecté ou non"| LEGAL["Legal : documents légaux"] ROOT --> PUSH["Écrans empilés : BuyTicket, GiftTicket, WaitingRoom, LiveDraw, TransactionHistory, Notifications, HowItWorks, RewardsCatalog, RewardDetail, ScratchCard, MyCodes, VerifyAge, Settings, DisplaySettings, NotificationSettings, PersonalData, Security, ChangeEmail, ChangePassword, Setup2FA, HelpSupport"] AUTH --> A1["Onboarding"] AUTH --> A2["SignIn, SignUp"] AUTH --> A3["Verify2FA"] AUTH --> A4["ForgotPassword, ResetPassword"] AUTH --> A5["Setup2FA"] MAIN --> T1["HomeTab : HomeScreen"] MAIN --> T2["TicketsTab : MyTicketsScreen"] MAIN --> T3["SponsorsTab : SponsorsScreen"] MAIN --> T4["ProfileTab : ProfileScreen"]
RootNavigator
RootNavigator.tsx contient le NavigationContainer et une pile racine dont le contenu dépend de state.auth.isAuthenticated :
| État | Écrans enregistrés |
|---|---|
| Non connecté | Auth (tout l'AuthStack) |
| Connecté | Main (les onglets), puis les 21 écrans empilés de l'arbre ci-dessus |
Ce rendu conditionnel est le mécanisme de garde : quand setCredentials passe isAuthenticated à vrai, React Navigation démonte l'AuthStack et affiche les onglets ; logout fait l'inverse. Aucune navigation explicite n'est nécessaire après la connexion.
Tous les écrans secondaires sont poussés sur cette pile racine : ils s'affichent en plein écran, sans barre d'onglets, et le retour ramène à l'onglet d'origine. Il n'y a plus de pile imbriquée dans un onglet.
Ouverture automatique de la vérification d'âge
Après la connexion, RootNavigator ouvre de lui-même l'écran VerifyAge quand le profil stocké indique ageVerificationStatus égal à NONE ou REJECTED. La fonction promptAgeVerification est appelée 400 ms après le passage à l'état connecté et à onReady du conteneur. Une référence agePromptShown limite l'ouverture à une fois par session ; elle est remise à zéro à la déconnexion. Un statut PENDING ou VERIFIED n'ouvre rien. Le citoyen peut fermer l'écran : l'achat, le cadeau et les échanges lui seront alors refusés par le backend (403 AGE_VERIFICATION_REQUIRED), et chaque écran concerné propose de rouvrir VerifyAge (voir Portefeuille et loterie).
Fenêtres globales
RootNavigator rend aussi, au-dessus de la navigation, un bandeau de notification simulé et quatre ResultDialog pilotés par le slice ui : points crédités, réseau encombré, forte affluence et information réseau (voir État et appels API).
AuthStack
AuthStack.tsx démarre sur Onboarding si hasSeenOnboarding est faux, sinon sur SignIn. hasSeenOnboarding n'est pas persisté : l'introduction réapparaît à chaque démarrage à froid de l'application.
| Route | Paramètres |
|---|---|
Onboarding |
aucun |
SignIn |
aucun |
SignUp |
referralCode facultatif |
Verify2FA |
userId, method (APP ou EMAIL), email, purpose (signin ou signup), devOtpPreview |
ForgotPassword |
email facultatif |
ResetPassword |
email, devOtpPreview |
Setup2FA |
aucun |
Setup2FA est enregistré dans cette pile, mais aucun écran non connecté n'y mène et l'écran exige un utilisateur connecté pour fonctionner.
MainTabs
MainTabs.tsx déclare quatre onglets avec des libellés traduits :
| Route | Libellé | Écran | Icône |
|---|---|---|---|
HomeTab |
Accueil | HomeScreen.tsx |
tabHome |
TicketsTab |
Mes tickets | MyTicketsScreen.tsx |
tabTickets |
SponsorsTab |
Parrains | SponsorsScreen.tsx |
tabSponsors |
ProfileTab |
Profil | ProfileScreen.tsx |
tabProfile |
La barre TabBar remplace celle de React Navigation : fond surface aux coins supérieurs arrondis (24), ombre portée, icône SVG au-dessus du libellé, onglet actif sur fond sunNav avec icône et texte onSun en gras. Sous les onglets, SecurityFooter rappelle « Application sécurisée · Aucun argent réel ». La marge basse suit l'encart de sécurité de l'appareil.
La rangée porte accessibilityRole="tablist" et chaque onglet accessibilityRole="tab", accessibilityState={{ selected }} et son libellé : les lecteurs d'écran annoncent l'onglet sélectionné. TabBar émet l'événement tabPress avant de naviguer, comme la barre standard, mais ne gère pas l'appui long.
Les 32 écrans
Avant connexion
| Écran | Fichier | Rôle | Données et actions |
|---|---|---|---|
| Introduction | OnboardingScreen.tsx |
Trois diapositives (grand tirage, tirage en direct, portefeuille offert et accès réservé aux 18 ans et plus) | Texte codé en dur ; markOnboardingComplete puis replace('SignIn') ou replace('SignUp') |
| Connexion | SignInScreen.tsx |
Email et mot de passe, lien « Mot de passe oublié ? », bouton Google, lien vers l'inscription | signInWithPassword (POST /auth/signin) ; navigate('Verify2FA') si la 2FA est active, avec le loginTicket pour la méthode APP |
| Inscription | SignUpScreen.tsx |
Email, mot de passe et confirmation avec règles affichées, code de parrainage repliable, deux cases obligatoires (CGU et règlement ; 18 ans ou plus), liens vers les CGU, le règlement et la politique de confidentialité ouvrant Legal, bouton Google |
signUpWithPassword (POST /auth/signup, acceptTerms et isAdultConfirmed à true) puis navigate('Verify2FA', { purpose: 'signup' }) |
| Vérification du code | Verify2FAScreen.tsx |
Code à 6 chiffres reçu par email ou donné par l'application TOTP, ou code de secours | verifyOtpCode (POST /auth/2fa/verify, avec loginTicket s'il est fourni) ; la connexion bascule seule vers les onglets, ou l'écran se remplace par un Verify2FA APP si le backend demande encore le code de l'application |
| Mot de passe oublié | ForgotPasswordScreen.tsx |
Saisie de l'email | requestPasswordReset (POST /auth/password/forgot) puis navigate('ResetPassword') |
| Nouveau mot de passe | ResetPasswordScreen.tsx |
Code reçu, nouveau mot de passe et confirmation, renvoi du code | resetPassword (POST /auth/password/reset), puis popTo('SignIn') |
| Configuration 2FA | Setup2FAScreen.tsx |
Choix application TOTP ou email, QR code, clé et codes de secours copiables, confirmation | setup2FA (POST /auth/2fa/setup), verifyOtpCode |
Onglets
| Écran | Rôle | Données et actions |
|---|---|---|
| Accueil | Logo, cloche des notifications et roue des paramètres, carte du grand tirage (cagnotte, phase, échéance, participants, paliers), achat ou accès à la salle, cadeau, tickets à gratter, points et offres, mission suivante, partage du code de parrainage ; tirer pour rafraîchir | GET /lottery/status toutes les 30 s, GET /lottery/wallet, GET /lottery/my-tickets, GET /user/referrals, GET /user/quests, GET /marketplace/catalog, usePoints, usePushNotifications |
| Mes tickets | Cloche et roue des paramètres, ticket du tirage en cours, filtres, résultats des tirages passés, codes obtenus par grattage, accès à l'historique | GET /lottery/status toutes les 30 s, GET /lottery/my-tickets, GET /marketplace/my-codes |
| Parrains | Code de parrainage (copie et partage), étapes du parrainage, classement des citoyens par EtherPoints, missions à valider | GET /user/referrals, GET /lottery/leaderboard, GET /user/quests, POST /user/quests/complete |
| Profil | Carte d'identité du compte avec statut d'âge, résumé (ETH fictifs, EtherPoints, tickets joués), raccourcis (codes, boutique, historique), déconnexion confirmée ; les réglages s'ouvrent par la roue de l'en-tête | GET /user/profile, GET /lottery/wallet, signOut |
Écrans empilés
| Écran | Route | Rôle | Données et actions |
|---|---|---|---|
BuyTicketScreen.tsx |
BuyTicket |
Achat du ticket unique du grand tirage, solde avant et après | GET /lottery/status, GET /lottery/wallet, GET /lottery/my-tickets, purchaseTickets (POST /lottery/buy) |
GiftTicketScreen.tsx |
GiftTicket (ticketId facultatif) |
Cadeau d'un ticket par email avec message | sendGiftTicket (POST /lottery/gift) |
WaitingRoomScreen.tsx |
WaitingRoom |
Salle d'attente : compte à rebours de révélation, ticket du citoyen, présence, discussion ; son chrono.mp3 joué une fois dans les 8 dernières secondes |
GET /lottery/status toutes les 5 s, useSocket, useSound, puis replace('LiveDraw') |
LiveDrawScreen.tsx |
LiveDraw |
Roue animée avec le son roue-tirage.mp3, numéro gagnant, résultat du citoyen, retrait des gains |
GET /lottery/status toutes les 4 s, useSocket, claimPrize (POST /lottery/claim-prize) |
TransactionHistoryScreen.tsx |
TransactionHistory |
Portefeuille, tickets et badges POAP avec liens vers l'explorateur | GET /lottery/wallet, GET /lottery/my-tickets |
HowItWorksScreen.tsx |
HowItWorks |
Règles, calendrier d'un tirage, engagements, charte, lien vers le contrat | GET /lottery/status, GET /lottery/transparency-charter |
NotificationsScreen.tsx |
Notifications |
Liste des notifications, marquées vues à la sortie | GET /user/notifications |
RewardsCatalogScreen.tsx |
RewardsCatalog |
Boutique : solde de points, offres des commerces, accès aux tickets à gratter et aux codes | GET /marketplace/catalog, usePoints |
RewardDetailScreen.tsx |
RewardDetail (rewardId) |
Coût, disponibilité, échange confirmé | GET /marketplace/rewards/:id, POST /marketplace/claim, puis replace('ScratchCard', { claim }) |
ScratchCardScreen.tsx |
ScratchCard (claim ou offer) |
Choix et achat d'un ticket à gratter, ou révélation d'un code déjà obtenu | POST /marketplace/scratch, puis replace('MyCodes') |
MyCodesScreen.tsx |
MyCodes |
Codes obtenus, actifs ou utilisés | GET /marketplace/my-codes |
VerifyAgeScreen.tsx |
VerifyAge |
Photo du recto et du verso de la pièce d'identité, statut de la vérification | GET /user/identity, POST /user/identity |
SettingsScreen.tsx |
Settings |
Accueil des paramètres : carte d'identité (initiales, prénom et nom, email) puis cinq cartes de section | GET /user/profile |
DisplaySettingsScreen.tsx |
DisplaySettings |
Mode clair ou sombre, langue français ou anglais, interrupteur « Sons » | PUT /user/preferences (theme, language) ; le choix des sons reste sur l'appareil (useSoundPreference) |
NotificationSettingsScreen.tsx |
NotificationSettings |
Interrupteur général et quatre catégories de notifications | GET /user/profile, PUT /user/preferences (pushNotificationsEnabled, notifications) |
PersonalDataScreen.tsx |
PersonalData |
Prénom et nom, export JSON partagé par la feuille système, demande puis confirmation d'anonymisation | PATCH /user/profile, GET /user/gdpr/export, POST /user/gdpr/delete-request, POST /user/gdpr/confirm-delete, puis signOut |
SecurityScreen.tsx |
Security |
Adresse email et mot de passe (liens vers les deux écrans suivants), état, activation et désactivation de la 2FA | GET /user/profile, POST /auth/2fa/disable |
ChangeEmailScreen.tsx |
ChangeEmail |
Nouvelle adresse et mot de passe actuel, puis code à 6 chiffres reçu sur la nouvelle adresse | POST /auth/email/change, POST /auth/email/confirm, puis patchUser |
ChangePasswordScreen.tsx |
ChangePassword |
Création d'un premier mot de passe ou modification avec le mot de passe actuel | POST /auth/password/change, puis applyRenewedSession |
Setup2FAScreen.tsx |
Setup2FA |
Comme dans l'AuthStack |
POST /auth/2fa/setup, POST /auth/2fa/verify |
HelpSupportScreen.tsx |
HelpSupport |
Formulaire pour écrire à un administrateur (objet, message), carte du centre d'aide, section « Informations légales » listant les six documents | POST /user/support ; lien EXPO_PUBLIC_HELP_CENTER_URL ; navigate('Legal', { doc }) |
LegalScreen.tsx |
Legal (doc : cgu, reglement, confidentialite, traceurs, mentions ou transparence) |
Titre et version du document, introduction, sections, puis cartes « Autres documents » | Contenu local de content/legal.ts, identique à celui du web ; replace('Legal', { doc }) pour changer de document |
Toutes les routes appelées depuis les écrans connectés exigent un jeton, sauf GET /lottery/status, /leaderboard, /transparency-charter, GET /marketplace/catalog et /rewards/:id, qui sont publiques. Le rôle n'est pas vérifié côté mobile : l'application ne propose aucun écran réservé à ADMIN ou PARTNER. Un compte Mairie ou partenaire qui se connecte voit l'interface citoyenne.
Legal est déclaré dans la pile racine en dehors de la condition isAuthenticated : il s'ouvre aussi bien depuis l'inscription, avant toute connexion, que depuis Aide & Support. Les six documents (CGU, règlement de participation, mentions légales, politique de confidentialité, politique relative aux traceurs, charte de transparence) sont embarqués dans l'application et ne demandent aucun appel réseau.
Boutons d'en-tête et paramètres
HeaderActions.tsx regroupe deux boutons ronds : la cloche NotificationButton, avec sa pastille quand unreadCount est positif, qui ouvre Notifications, et une roue dentée (icône settings, libellé d'accessibilité « Paramètres ») qui ouvre Settings. Le composant est placé dans l'en-tête de l'accueil (HomeScreen.tsx), de « Mes tickets » (MyTicketsScreen.tsx) et dans TabTitle.tsx, le titre des onglets Profil et Parrains. L'onglet Profil n'a plus de liste « Mon compte » : la roue est le seul accès aux réglages.
SettingsScreen.tsx, repris d'une maquette Figma, sert d'accueil des paramètres. Une carte jaune affiche un avatar avec les initiales, le prénom et le nom (ou « Citoyen d'EtherBay » s'ils sont vides) et l'email ; les initiales et le nom complet viennent de displayName.ts, qui retombe sur les initiales de l'email. Suivent cinq cartes SettingsLinkCard, dans cet ordre :
| Carte | Route | Contenu |
|---|---|---|
| Affichage | DisplaySettings |
Puces Clair ou Sombre, puces Français ou English, interrupteur « Sons » (roue et compte à rebours du grand tirage, sur cet appareil) |
| Notifications | NotificationSettings |
Interrupteur « Autoriser les notifications », puis les catégories rappels du tirage (drawReminders), résultats (results), offres et récompenses (rewards), parrainage (referrals) |
| Données personnelles | PersonalData |
Formulaire prénom et nom, téléchargement des données (export RGPD), suppression du compte |
| Sécurité | Security |
Ligne email vers ChangeEmail, ligne mot de passe vers ChangePassword (« en créer un » si le compte n'en a pas), puis la 2FA : activation, reconfiguration, désactivation |
| Aide & Support | HelpSupport |
Formulaire pour écrire à un administrateur, carte du centre d'aide, informations légales |
La maquette prévoyait une entrée Langue séparée : elle n'a pas été reprise, la langue se règle dans Affichage.
Les réglages d'affichage et de notifications sont enregistrés à chaque changement par usePreferenceSaver.ts, qui appelle PUT /user/preferences, remet l'ancienne valeur et affiche un message si l'envoi échoue. Quand l'interrupteur général est coupé, les catégories sont grisées et affichées désactivées, mais leurs valeurs enregistrées sont conservées.
La carte du centre d'aide ouvre l'adresse de EXPO_PUBLIC_HELP_CENTER_URL. Tant que la variable est vide, la carte affiche la pastille « Bientôt » et ne fait rien : le site du centre d'aide reste à créer.
Le bloc « Compte et appareils » de l'écran Sécurité (portefeuille intégré, déconnexion de tous les appareils) a été retiré. signOutEverywhere existe toujours dans useAuth, mais aucun écran ne l'appelle.
Parcours principaux
flowchart LR ON["Onboarding"] --> SI["SignIn"] ON --> SU["SignUp"] SU -->|"code email"| VE["Verify2FA"] SI -->|"2FA active"| VE SI --> AC["Accueil"] VE --> AC AC -->|"âge NONE ou REJECTED"| VA["VerifyAge"] AC --> BT["BuyTicket"] AC --> GT["GiftTicket"] AC -->|"phase WAITING_ROOM ou DRAWING"| WR["WaitingRoom"] WR -->|"replace"| LD["LiveDraw"] AC --> SC["ScratchCard"] SC -->|"replace"| MC["MyCodes"] AC --> RC["RewardsCatalog"] RC --> RD["RewardDetail"] RD -->|"replace"| SC
L'échange d'une récompense remplace l'écran de détail par la carte à gratter, puis la carte par la liste des codes : le retour arrière ne repasse pas par une carte déjà grattée. De même, la salle d'attente est remplacée par l'écran du tirage dès que la phase devient DRAWING ou COMPLETED.
Ce que la navigation ne fait pas
- Pas de liens profonds.
app.jsondéclare désormais"scheme": "blocklucky", utilisé par le retour de la connexion Google (expo-web-browser), mais leNavigationContainern'a pas de configurationlinking. Un lien de parrainage/invite/<code>ouvert sur le téléphone ne mène pas à un écran ; le code doit être saisi à la main dans l'inscription. - Pas de restauration de session. Au démarrage,
isAuthenticatedvaut toujours faux, même si un jeton est stocké (voir Authentification et stockage sécurisé). - Pas de garde par statut d'âge. L'ouverture automatique de
VerifyAgeest une invitation : aucun écran n'est bloqué côté application, c'est le backend qui refuse les opérations.
Pour ajouter un écran, suivre Ajouter un écran mobile.