Mobile·10 min de lecture·1,870 mots

Architecture mobile (Expo)

Architecture — Mobile (mobile-staging/)

Executive summary

App Expo SDK 54 / React Native 0.81.5 ciblant iOS + Android uniquement (jsEngine Hermes, app.json.platforms = ["ios", "android"] — pas de cible web, voir BUG-134). File-based routing via expo-router v6. Architecture 2 espaces in-app : client (5 onglets (tabs)/) + gérant (vitrine-pro/ non-Stack, basculé via toggle Compte).

Source de vérité dev : ~/DEVELOPPEMENT/ sur la machine fabrice (192.168.1.152, user fabrice). Le dossier mobile-staging/ sur le VPS est un miroir. Repo mobile : github.com/ouattaradfabrice/wari-mobile (branche main SSH).

iOS bundle : pro.wari.mobile (Team 9HTKV7262K, ascAppId 6768885815). Slug Expo : wari-mobile. EAS project : 4d8772a8-77f0-4fb1-818e-576a2f755fe1.

Technology stack

CatégorieTechnologieVersionPourquoi ce choix
FrameworkExpo SDK54Outillage natif (EAS, expo-camera, expo-notifications, expo-secure-store) sans toucher iOS/Android natifs
RNReact Native0.81.5Aligné Expo 54 ; jsEngine Hermes obligatoire
Routerexpo-router6.0.23File-based, gère stacks + tabs + modals
LangageTypeScript5.9Strict
StateZustand5.016 stores, simple et performant (vs Redux/MobX)
Data fetching@tanstack/react-query5.62Cache automatique, invalidations, staleTime tuné 60s sur Android mid-range
Auth storageexpo-secure-store15.0Keychain iOS / Keystore Android + cache mémoire (perf Android)
Local storage@react-native-async-storage2.2Favoris, historique, queue offline
Notifexpo-notifications~0.32Push iOS+Android (FCM via google-services.json, APNs Production via Apple Push Cert)
Cameraexpo-camera17.0Scan code-barres + photo prise
Image pickerexpo-image-picker17.0Sélection photos upload
Localizationexpo-localization + i18n-js17.0 / 4.5Détection locale device
i18ni18next + react-i18next26.2 / 17.0Traductions strings (fr default, en/ar/pt préparées)
Iconsphosphor-react-native3.0Lib unique — re-export via lib/icons.ts (DEC-133/134/135)
Animationsreact-native-reanimated4.1 + worklets 0.5TopBar absolute, transitions tabs, gestes
Gesturesreact-native-gesture-handler2.28Drag-to-close modals, swipe stories
Modalsreact-native-screens4.16Modals natives perf
Safe areareact-native-safe-area-context5.6insets.bottom / top (CTA dynamic padding)
SVGreact-native-svg15.12Sparklines DIY (DEC-189), icônes
QRreact-native-qrcode-svg6.3QR tables restau + partage vitrine + iCal
WebViewreact-native-webview13.15Builder vitrine embedded (mobile builder) + paiement PayDunya
Network@react-native-community/netinfo11.4Détection offline (banner)
Monitoring@sentry/react-native7.2Crash + perf
TestsJest 29 + jest-expo 55 + @testing-library/react-native 13 + @testing-library/jest-native 5.4Unit + composants
BuildsEAS BuildCLI ≥ 18development (apk dev client) / preview (apk internal) / production (app-bundle Android + ios store)

Architecture pattern

Pattern : deux espaces in-app fusionnés dans une seule app.

App root (_layout.tsx)

├── Espace CLIENT (par défaut)
│   └── (tabs)/_layout.tsx — 5 onglets : Accueil · Activité · Panier · Explorer · Compte
│       (TopBar absolue glass + SearchOverlay + AnimatedTabBar)

└── Espace GÉRANT (modal Animated.View, zIndex 200)
    └── vitrine-pro/_layout.tsx — guard isTenant + GerantLayout + GerantTabBar dynamique
        Tabs selon modulesActifs : Tableau / Activité (Commandes+RDV) / Catalogue (Produits|Services) /
                                    Clientes / Cuisine (restau) / Carte / Tables / Réglages / Stories

Le toggle Mode Gérant se fait depuis l'onglet Compte → bouton "Ma Vitrine" (visible si isTenant). Animated.View overlay couvre l'écran complet pour passer au-dessus de la TopBar absolue (DEC-153).

Sous-patterns :

  • TopBar position: absolute (zIndex 100) avec effet glass translucide. Tous les ScrollView des écrans tabs doivent lire useUIStore().topbarHeight et appliquer paddingTop: topbarHeight + SPACING.sm (DEC-153).
  • Bottom sheets clavier-safe : règles strictes DEC-170/174 (keyboardShouldPersistTaps, pas de KeyboardAvoidingView sur position absolute, 2 variantes ctaPaddingBottom selon contexte Modal vs GerantLayout).
  • Filtres réutilisables : <FilterChip /> + <FilterSheet /> partout (jamais d'UI ad-hoc, DEC-137/138/139).
  • Icônes unifiées Phosphor via lib/icons.ts central (DEC-133→136).
  • API client lib/api.ts apiFetch() : ne throw jamais, retourne { data, error, status }.
  • Routes constants dans lib/constants.ts : VITRINE_DASHBOARD, VITRINE_CATALOGUE, ...
  • Hooks métier React Query dans hooks/ : 1 fichier par domaine (useAuth, useCart, useVitrine, useRdv, useRestaurant, useStoriesFeed, ...).
  • Type guards côté écrans pour gérer payloads teaser (vitrines INVITATION).

State management — Zustand stores

16 stores Zustand, organisés par domaine :

StoreRôle
authStoreuser, tenant, token, isAuthenticated, selectIsTenant, hydrate via expo-secure-store
cartStorePanier produits multi-tenants (groupé à l'affichage, checkout par vitrine)
panierRepasStorePanier restau séparé (DEC-219, jamais mélangé avec cartStore) — reset auto si tenantId change
favorisStoreFavoris locaux (AsyncStorage). V2 sync vers Wishlist backend
historyStore20 derniers produits consultés (AsyncStorage)
gerantStoreactiveTab, modeGerant, selectedCommande, activeReglagesTab (WP-232)
explorerStoreactiveTab persistant + pendingCategorieId (navigation cross-tabs)
inboxArchiveStoreArchives Inbox gérant
notesClientsStoreNotes locales par cliente
offlineQueueStoreQueue actions hors ligne (V2)
networkStoreNetInfo connecté/déconnecté
themeStoreLight/dark/system (DEC-101)
uiStoretopbarHeight + autres dimensions mesurées dynamiquement
villeStore + geoStoreGéoloc + ville sélectionnée

Pas de Redux, pas de Context API pour le state global métier.

Routing — expo-router v6 (file-based)

app/
├── _layout.tsx              # Root : QueryClient + Sentry + i18n + AuthHydrator + Stack natif
├── (tabs)/                  # Group "tabs" → AnimatedTabBar
│   ├── _layout.tsx          # TopBar absolute + tabs
│   ├── index.tsx            # /(tabs)
│   ├── activite.tsx         # /(tabs)/activite
│   ├── panier.tsx
│   ├── explorer.tsx
│   ├── compte.tsx
│   ├── commandes.tsx
│   └── recherche.tsx
├── (onboarding)/            # Group "onboarding" — flow post-magic link
├── auth/                    # /auth/index, /auth/canal, /auth/otp, /auth/profil, /auth/verify
├── produit/[id].tsx         # /produit/:id (Zalando-like Stack screen)
├── prestation/[id].tsx
├── prestation/[id]/creneaux.tsx
├── vitrine/[slug].tsx       # Vitrine mini-site
├── restaurant/[slug]/...    # carte, panier, suivi/[id], table/[qrToken]
├── commande/checkout.tsx
├── reservation/confirmer.tsx
├── vitrine-pro/             # **Espace gérant — NON expo-router Stack** (in-app overlay)
│   ├── _layout.tsx          # Custom GerantLayout + GerantTabBar
│   ├── dashboard.tsx
│   ├── commandes.tsx
│   └── ...
└── conversation/, evenement/, story/, search/, favoris.tsx, messages.tsx, ...

Tous les Stack.Screen headerShown: false (BUG-116 fix) ; navigation pure custom.

API client

lib/api.ts :

async function apiFetch<T>(url, opts?): Promise<{ data: T|null; error: string|null; status: number }>
// Ne throw jamais. Retry singleton 401 (refresh token). Timeout 10s.

Routes en constants (lib/constants.ts) :

const API_URL = "https://wari.pro";   // prod
// dev override via app.json.expo.extra.apiUrl si besoin
const VITRINE_DASHBOARD = "/api/mobile/vitrine/dashboard";
const VITRINE_CATALOGUE = "/api/mobile/vitrine/catalogue";
// ...60+ endpoints

Authentification : Bearer JWT via header Authorization: Bearer <token> lu depuis expo-secure-store (cache mémoire pour Android perf). Voir lib/auth.ts.

Push notifications

Stack :

  1. Expo dev build (pas Expo Go) — pkg expo-notifications ne fonctionne pas en Expo Go SDK 53+.
  2. Firebase FCM (Android) — google-services.json commité (clé restreinte par fingerprint).
  3. APNs Production (iOS) — cert configuré côté Apple Developer + Expo.
  4. Register flow :
    • Mobile : useNotifications hook s'enregistre au mount auth → récupère ExponentPushToken[...].
    • POST /api/mobile/push-tokens/register avec {token, platform}. Backend stocke selon session.role :
      • TENANT_ADMIN/SUPER_ADMIN → PushToken.userId
      • CLIENT → PushToken.clientAccountId
  5. Envoi : backend lib/push.ts sendPushToTenantAdmin / sendPushToClient via expo-server-sdk. Cleanup auto sur DeviceNotRegistered.
  6. Tap handler : useNotifications enregistre Notifications.addNotificationResponseReceivedListener → router selon data.type (commande / rdv / paiement / acces / story).

⚠️ Expo Go : lib/notifications-import.ts (pattern BUG-118 fix) — import statique remplacé par require() conditionnel Constants.executionEnvironment !== "storeClient" pour éviter ERROR log au boot.

i18n + International Foundation

  • i18next + react-i18next : strings dans locales/<lng>/translation.json.
  • Default fr ; en, ar, pt préparés (Wave 4-7 markets).
  • Markets : table backend Market (24 markets ISO Wave 1-8 BF→UEMOA→CEDEAO→EU→Maghreb→US→Lusophonie).
  • Money pattern : lib/money.ts côté backend ; côté mobile, formatage par devise via Intl.NumberFormat.

Builds & deployment

EAS profiles (eas.json) :

ProfileAndroidiOSChannel
developmentapk + dev client(n/a — dev local)development
previewapk internalnon-simulatorpreview
productionapp-bundle (Play Store)autoIncrement buildNumber + Releaseproduction

Submit :

  • eas submit --platform ios → ascAppId 6768885815 (TestFlight → App Store)
  • Android : pas encore configuré (Play Store pas lancé V1)

OTA :

  • 95% des releases mobile = eas update --branch production (~5 min, pas de rebuild natif)
  • Native build seulement quand : changement permissions, ajout package natif, version Expo bump, icône
  • Voir reference_mobile_commandes_prod.md dans la mémoire

Design system

  • Couleurs : palette WARI 12 + hex libre (DEC-173). Pas de color picker natif.
  • Charte tenant : themeCouleur du Tenant utilisé comme accent dans la vitrine mobile.
  • Icônes : Phosphor uniquement (DEC-133), tailles fixes 16/20/22/24/32-48, poids selon état.
  • Filtres : <FilterChip /> (variants filter/cta) + <FilterSheet /> (sections pliables + sticky CTA).
  • Glass surfaces : useTheme().glass.bg (5-8% opacity) pour conteneurs sticky. withAlpha(color, opacity) helper.
  • Tabs underline : pattern Apple/Spotify pour tabs internes écran (Explorer, VitrineTabs, ReglagesTabs).
  • Touch targets : min 44×44 partout.

Architecture client (5 onglets)

OngletIcôneRôle
AccueilHouseDécouverte curatée (3 horizontales + grille catégories + banner) — pas de filtres
ActivitéBellCommandes en cours + nouveautés vitrines suivies + favoris + mes RDV + mes demandes accès
PanierShoppingCartPanier local Zustand, multi-tenants groupés
ExplorerCompass3 tabs internes (Vitrines/Produits/Prestations) + FilterSheet par tab
CompteUserAuth + profil + paramètres + accès Ma Vitrine (si isTenant)

Architecture gérant (vitrine-pro)

TabBar dynamique selon modulesActifs du tenant :

Module actifOnglets
catalogue seulTableau / Activité / Catalogue / Clientes / Réglages
catalogue+services (MIXTE)Tableau / Activité / Catalogue (Produits
restaurantTableau / Cuisine / Carte / Tables / Réglages
rdv actifActivité avec sous-tabs Commandes/RDV
evenementsOnglet supplémentaire
storiesStories listing + composer

Composants pivots :

  • GerantLayout.tsx — Animated.View overlay + header + slot
  • GerantTabBar.tsx — TabBar custom dynamique + badges (nbAttente, nbDemandes, cuisine count)
  • BoutiqueActivite.tsx — pivot sous-tabs Commandes/RDV
  • BoutiqueCatalogue.tsx — pivot sous-tabs Produits/Services
  • GerantReglages.tsx — 4 tabs WP-232 (Vitrine / Contact / Accès / Avancé)

Testing strategy

  • Jest 29 + jest-expo 55 + @testing-library/react-native 13
  • Configuration : jest.config.js + jest.setup.js
  • Tests existants : composants UI, stores Zustand, hooks (couverture partielle)
  • Commande : npm test ou npm run test:watch
  • Typecheck : npm run typecheck (tsc --noEmit)
  • Lint : npm run lint / lint:fix

Pas de tests E2E (Detox / Maestro pas configurés). QA pratique = TestFlight + sessions test (tests-pratiques/).

Source tree

Voir source-tree-analysis.md section "Part mobile".

Development workflow

Voir development-guide-mobile.md.

Deployment

Voir deployment-guide.md section "EAS".