Thème et composants
L'interface mobile suit la maquette Figma de l'application : fond gris très clair, cartes blanches très arrondies, jaune « soleil » #FFDA38 pour les mises en avant, encre bleu nuit #1D2637 pour le texte et les boutons sombres, et des teintes pastel (lavande, menthe, pêche) pour distinguer les familles d'information. Le thème est défini en TypeScript dans mobile-app/src/theme et distribué par un contexte React. Les composants de base sont dans mobile-app/src/components/ui.
Le frontend web a repris cette charte : ses variables CSS recopient les valeurs de colors.ts, il charge la même police Inter et utilise une copie du jeu d'icônes (voir Design system du web). Les deux applications ne partagent aucun fichier : un changement de jeton se reporte à la main des deux côtés.
Fichiers du thème
| Fichier | Contenu |
|---|---|
colors.ts |
Interface ColorPalette, famille sunFamily commune, palettes lightColors et darkColors |
spacing.ts |
Espacements adaptés à la largeur d'écran et rayons |
typography.ts |
Familles Inter, tailles et interlignes adaptés à la largeur |
ThemeProvider.tsx |
Contexte : mode, langue, palette, dimensions, t(fr, en) |
useAppTheme.ts |
Hook d'accès au contexte |
Couleurs
Les jetons de la famille « soleil » sont identiques en thème clair et sombre :
| Clé | Valeur | Usage |
|---|---|---|
sun |
#FFDA38 |
Carte du grand tirage, bouton principal, pastilles |
sunSoft, sunPale, sunLight, sunChip |
#FFF4B8, #FFF6CA, #FFE979, #FFF7D3 |
Fonds de cartes et d'étiquettes |
sunTrack |
#E4C64B |
Piste des jauges sur fond jaune |
onSun, onSunMuted |
#1D2637, #4A5263 |
Texte posé sur le jaune |
onInk |
#FFFFFF |
Texte posé sur l'encre |
shadowSun |
rgba(172, 141, 10, 0.12) |
Ombre des cartes jaunes |
Les autres jetons changent avec le thème :
| Clé | Clair | Sombre | Usage |
|---|---|---|---|
background |
#F6F7FA |
#0F1626 |
Fond des écrans |
surface |
#FFFFFF |
#1A2333 |
Cartes, barre d'onglets, fenêtres |
surfaceMuted, surfaceNeutral |
#F5F6F9, #F0F2F6 |
#222C3E, #283246 |
Zones en retrait, champs |
ink |
#1D2637 |
#F3F5F9 |
Texte principal, boutons sombres |
inkMuted |
#657084 |
#A3ACBD |
Texte atténué, icônes inactives |
border, divider, separator |
#E2E5EB, #E8EBEF, #DFE3EB |
#2C3649 |
Bordures et séparateurs |
sunNav |
#FFF3B5 |
#3A3420 |
Onglet actif |
lavender, lavenderStrong, lavenderSoft |
#EEE9FF, #E8E2FF, #F2EEFC |
#2A2546, #312A55, #2A2546 |
Points, ticket à gratter violet |
violet |
#7357B1 |
#B9A6F0 |
Accent des EtherPoints |
mint, green |
#E6EFEA, #356C58 |
#1E3329, #7FC4A6 |
État positif, âge vérifié |
peach, terracotta |
#FFE5D8, #A95837 |
#3D2A22, #F0A07F |
Erreurs, ticket à gratter « lucky » |
coral |
#E87F69 |
#E87F69 |
Accent ponctuel |
overlay, shadowModal |
rgba(15, 23, 38, 0.5), rgba(17, 24, 39, 0.18) |
rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.45) |
Voile et ombre des fenêtres |
Les clés de l'ancienne palette (primary, primaryLight, secondary, accent, text, textPrimary, textSecondary, textMuted, surfaceElevated, success, warning, error, gold) restent dans ColorPalette comme alias des nouveaux jetons, par exemple primary vaut ink et error vaut terracotta. Elles évitent de casser le code qui les lit encore ; un nouveau composant doit utiliser les jetons ci-dessus.
Le splash a un fond #F6F7FA (le background clair) et l'icône adaptative Android un fond #FFDA38, définis dans app.json.
Typographie
App.tsx charge la police Inter avec useFonts de @expo-google-fonts/inter et n'affiche rien tant qu'elle n'est pas prête. typography.ts associe une famille à chaque graisse :
| Graisse | Famille |
|---|---|
regular |
Inter_400Regular |
medium |
Inter_500Medium |
semibold |
Inter_600SemiBold |
bold |
Inter_700Bold |
| italique grasse | Inter_700Bold_Italic |
AppText propose dix variantes, en points avant mise à l'échelle :
| Variante | Taille | Graisse |
|---|---|---|
display |
43 | bold |
hero |
28 | bold |
title |
23 | bold |
section |
21 | bold |
subtitle |
16 | bold |
body |
13 | regular |
small |
12 | regular |
caption |
11 | regular |
overline |
10 | bold, majuscules |
label |
9 | bold, majuscules |
Les écrans passent aussi souvent une taille explicite (size) et une graisse (weight). L'interligne par défaut vaut 1,3 fois la taille.
Espacements et échelle adaptative
spacing.ts et typography.ts calculent un facteur d'échelle à partir de la largeur de la fenêtre, avec 402 points comme référence (la largeur des écrans de la maquette), borné entre 0,9 et 1,06 :
| Fichier | Valeurs à 402 points |
|---|---|
spacing.ts |
xs 4, sm 8, md 14, lg 20, xl 28, xxl 40, screen 20 (marge latérale), screenText 24 |
typography.ts |
tailles xs 11, sm 12, md 13, lg 16, xl 21, xxl 23, hero 29 ; fonction rf d'arrondi au pixel |
Les rayons ne dépendent pas de l'écran : sm 10, md 12, lg 16, xl 20, xxl 24, hero 28, full 9999.
Le facteur est calculé une seule fois, au chargement du module, avec Dimensions.get('window'). Il ne suit pas une rotation, un changement de taille de fenêtre sur tablette ou un pliable. ThemeProvider fournit en revanche screenWidth, screenHeight et isCompactScreen (largeur inférieure à 360 ou hauteur inférieure à 700) à jour grâce à useWindowDimensions.
ThemeProvider
| Valeur du contexte | Rôle |
|---|---|
mode, toggleTheme, setThemeMode |
Thème LIGHT ou DARK |
language, setLanguage, t(fr, en) |
Langue FR ou EN et traduction en ligne |
colors |
darkColors si mode === 'DARK', sinon lightColors |
spacing, borderRadius, typography |
Objets des fichiers du thème |
screenWidth, screenHeight, isCompactScreen |
Dimensions courantes |
Le mode et la langue sont de simples useState initialisés à LIGHT et FR :
- ils ne sont pas mémorisés sur l'appareil et reviennent à leur valeur initiale à chaque démarrage ;
- le thème du système n'est pas lu, alors que
app.jsondéclare"userInterfaceStyle": "automatic": un téléphone en mode sombre affiche quand même l'application en clair ; - l'écran Affichage (
DisplaySettingsScreen) envoie les changements au backend (PUT /user/preferences, parusePreferenceSaver) et revient à l'ancienne valeur si l'envoi échoue, mais le thème et la langue enregistrés ne sont jamais relus. Il ne propose que Clair et Sombre : la valeurSYSTEMacceptée par le backend n'est pas offerte.
App.tsx adapte la barre d'état : texte clair en thème sombre, texte foncé en thème clair.
La fonction t(fr, en) fonctionne comme sur le web (voir Internationalisation). Les messages d'erreur de useCitizenWallet et ceux des fenêtres globales restent en français seulement, et les messages du backend s'affichent en français quand l'interface est en anglais.
Composants de base
| Composant | Fichier | Props principales | Comportement |
|---|---|---|---|
AppText |
AppText.tsx |
variant, size, weight, color, align, lineHeight |
Taille mise à l'échelle par rf, famille Inter selon la graisse, couleur ink par défaut |
AppButton |
AppButton.tsx |
title, onPress, variant (primary, dark, outline, soft, ghost), size (lg 52, md 40, sm 32), leftIcon, rightIcon, loading, disabled, accessibilityHint |
Rôle button, libellé égal au titre, états disabled et busy annoncés ; secondary et danger restent acceptés comme alias de dark et outline |
AppInput |
AppInput.tsx |
label, icon, errorText, helperText, secureToggle et les props de TextInput |
Champ arrondi avec icône ; secureToggle ajoute un bouton accessible pour afficher ou masquer le mot de passe |
Card |
Card.tsx |
tone (surface, sun, lavender, mint, peach…), radius, padding, elevated, onPress, accessibilityLabel |
Carte de la maquette, cliquable si onPress est fourni |
Pill |
Pill.tsx |
label, tone, dot, uppercase |
Étiquette arrondie |
ListRow |
ListRow.tsx |
title, subtitle, icon, iconTone, value, right, onPress, destructive, divider |
Ligne de liste des réglages et de l'historique |
Screen |
Screen.tsx |
scroll, footer, showSecurityFooter, refreshing, onRefresh, edges |
SafeAreaView, KeyboardAvoidingView, défilement et « tirer pour rafraîchir », pied fixe facultatif |
ScreenHeader |
ScreenHeader.tsx |
title, subtitle, onBack, right |
En-tête d'écran avec BackButton si onBack est fourni |
SectionHeader |
SectionHeader.tsx |
title, subtitle, right |
Titre de section |
SecurityFooter |
SecurityFooter.tsx |
aucune | Mention « Application sécurisée · Aucun argent réel » |
StepList |
StepList.tsx |
steps, variant |
Étapes numérotées (parrainage, fonctionnement) |
FilterChips |
FilterChips.tsx |
options, value, onChange |
Filtres à choix unique |
ResultDialog |
ResultDialog.tsx |
visible, status (success, error, info), title, message, note, primaryLabel, secondaryLabel |
Fenêtre centrée des confirmations et erreurs, accessibilityViewIsModal, titre annoncé comme en-tête |
BottomSheet |
BottomSheet.tsx |
visible, title, onClose |
Modal glissant depuis le bas ; utilisée par le choix de la photo d'identité |
NotificationButton |
NotificationButton.tsx |
onPress, hasUnread |
Cloche avec pastille de notifications non lues |
HeaderActions |
HeaderActions.tsx |
aucune | Cloche (vers Notifications) et roue dentée (vers Settings) des en-têtes de l'accueil, de « Mes tickets » et de TabTitle |
BrandLogo |
BrandLogo.tsx |
size |
Symbole et nom BlockLucky de l'en-tête de l'accueil |
ProgressBar |
ProgressBar.tsx |
percentage, markers, tone, height, accessibilityLabel |
Jauge de participation et repères des paliers |
Loader |
Loader.tsx |
message, skeletonRows |
Indicateur de chargement ou lignes squelettes |
BackButton |
BackButton.tsx |
onPress, label |
Bouton rond avec flèche |
Icônes
Les icônes viennent de la maquette. Les fichiers sources sont dans mobile-app/assets/icons ; leur contenu SVG est recopié dans iconData.ts, qui associe un nom (tabHome, bell, google, idCard, volume, volumeOff…) à un balisage, une largeur et une hauteur. Icon.tsx l'affiche avec SvgXml de react-native-svg :
| Prop | Rôle |
|---|---|
name |
Clé de iconData (type IconName) |
size |
Largeur ; la hauteur suit les proportions de l'icône |
color |
Couleur appliquée par currentColor, ink du thème par défaut |
L'icône est masquée aux lecteurs d'écran (importantForAccessibility="no") : le libellé doit être porté par le bouton qui la contient. Ajouter une icône demande de déposer le SVG dans assets/icons, puis d'ajouter son entrée dans iconData.ts : le fichier SVG n'est pas lu à l'exécution. Les icônes volume et volumeOff, ajoutées pour l'interrupteur des sons, n'existent que dans iconData.ts, sans fichier dans assets/icons.
Sons
mobile-app/src/hooks/useSound.ts joue deux sons courts du grand tirage avec expo-audio (plugin déclaré dans app.json). Les fichiers sont dans mobile-app/assets/sounds et ont été fournis par l'équipe.
| Élément | Valeur |
|---|---|
SOUNDS.wheel |
assets/sounds/roue-tirage.mp3 |
SOUNDS.chrono |
assets/sounds/chrono.mp3 |
CHRONO_SECONDS |
8 : durée du compte à rebours sonore |
useSound(source) |
Renvoie play({ loop? }) et stop() ; crée le lecteur par useAudioPlayer, règle une fois le mode audio (playsInSilentMode: true, interruptionMode: 'mixWithOthers') et coupe le son si la préférence passe à faux |
useSoundPreference() |
Renvoie soundEnabled et setSoundEnabled ; valeur partagée par tous les écrans (useSyncExternalStore), vraie par défaut |
| Mémorisation | Clé blocklucky_sound_enabled dans SecureStore ('1' ou '0'), localStorage sur la cible web d'Expo ; la préférence n'est pas envoyée au backend |
| Utilisation | Comportement |
|---|---|
DrawWheel |
roue-tirage.mp3 en boucle tant que la roue tourne sans résultat, puis une lecture sans boucle pendant l'arrêt sur le secteur gagnant ; arrêt à la fin de l'animation et au démontage |
WaitingRoomScreen |
chrono.mp3 joué une seule fois par tour quand il reste 8 secondes ou moins avant la révélation, arrêté si la salle se ferme ou si l'écran est quitté |
DisplaySettingsScreen |
Interrupteur « Sons » (SwitchRow, icône volume ou volumeOff) : « Roue et compte à rebours du grand tirage, sur cet appareil » |
Les erreurs de lecture et de stockage sont ignorées : sans son, l'interface fonctionne de la même façon.
Composants métier
| Dossier | Composants |
|---|---|
components/auth |
AuthHero, SocialAuthButtons, PasswordRules, CheckboxRow, OrDivider, TextLink, FormMessage, DevOtpHint, HighlightCard, AgeNoticeCard, IdentitySideCard, utilitaires authErrors |
components/draw |
DrawWheel (roue à 10 secteurs, avec le son de la roue), CurrentTicketCard, ResultCards, RoomChat, WaitingRoomClosed, CrossFade, RollingValue, PulseDots, hook useNow, formats drawFormat |
components/lottery |
DeadlineCard, MilestonesPanel, DrawStatusBadge, DialogSummary, ScratchOfferCard, hook useCountdown, formats lotteryFormat |
components/marketplace |
ScratchCanvas, ScratchOfferCard, CodeCard, RewardCard, PointsBalanceCard, utilitaires apiErrors et scratchOffers |
components/profile |
IdentityCard, ReferralCodeCard, SummaryTiles, ListGroup, SwitchRow, TabTitle, utilitaire ageStatus |
components/settings |
SettingsLinkCard (carte de section de l'accueil des paramètres), utilitaires displayName (fullName, nameInitials), hook usePreferenceSaver (enregistrement d'une préférence avec retour arrière en cas d'échec) |
components/web3 |
IntegratedWalletCard, IntegratedWalletButton, ExplorerLink, plus utilisés par aucun écran |
ScratchCanvas découpe la zone à gratter en une grille de cellules. Chaque contact du doigt (PanResponder) efface les cellules proches, et le code est révélé quand 60 % de la surface est découverte. Pour les lecteurs d'écran, la zone est annoncée comme réglable avec le pourcentage découvert, et l'action d'accessibilité « Tout révéler » affiche le code sans geste de glissement.
Les composants du tirage et de la boutique sont décrits dans Portefeuille et loterie.
Accessibilité
La refonte a ajouté les props d'accessibilité de React Native aux composants principaux :
| Élément | Props |
|---|---|
AppButton |
accessibilityRole="button", libellé, indication, états disabled et busy |
| Barre d'onglets | Rôles tablist et tab, état selected |
AppInput |
Bouton d'affichage du mot de passe avec rôle et libellé |
ResultDialog |
accessibilityViewIsModal, titre en rôle header |
FormMessage, copie du code de parrainage, résultat du tirage, bandeau de notification |
accessibilityLiveRegion="polite" |
ScratchCanvas |
Rôle adjustable, progression annoncée, action « Tout révéler » |
Restent à traiter : AppText, Pill, StepList, Screen, Loader et BottomSheet n'ont aucune prop d'accessibilité, la BottomSheet ne déplace pas le focus, et accessibilityLiveRegion n'a d'effet que sur Android. La roue du tirage tourne sans tenir compte du réglage « Réduire les animations » du système. La taille du texte suit le réglage d'accessibilité du système, puisque allowFontScaling n'est jamais désactivé ; les mises en page n'ont pas été vérifiées avec de très grandes tailles. Les obligations sont rappelées dans Accessibilité numérique.
Ajouter un composant
- Lire les couleurs et les espacements avec
useAppTheme(), jamais en dur, pour suivre le thème sombre, et préférer les jetons de la maquette (ink,sun,surface…) aux anciens alias. - Utiliser
AppTextpour tout texte, afin de garder la police Inter et l'échelle typographique. - Passer chaque libellé par
t('...', '...'). - Utiliser
Iconpour les pictogrammes et porter le libellé sur l'élément interactif. - Ajouter
accessibilityRoleetaccessibilityLabeldès la création.