Architecture
Comment l'application mobile Feelbat est organisée, comment les données circulent, et les règles qui la maintiennent maintenable.
Feelbat est une application React Native 0.78 / React 19 en TypeScript. C'est une application IoT B2B de surveillance structurelle (voir onboarding.md pour le produit). Les données proviennent de capteurs physiques via Bluetooth LE (sur site) ou Sigfox/LTE via des gateways (à distance), sont servies par un backend GraphQL/Apollo, et fonctionnent en offline-first.
Séquence de démarrage
Point d'entrée index.js → src/App.tsx → src/apollo.tsx. L'arbre des providers :
App.tsx
└── <Provider store> Redux (redux-persist)
└── <PersistGate> réhydrate l'état persisté avant le rendu
└── ApolloProviderWithReux construit le client Apollo (src/apollo.tsx)
└── <ApolloProvider>
└── <ThemeProvider> thème Paper + Navigation (clair/sombre)
└── <GestureHandlerRootView>
└── <FabVisibilityProvider>
└── <Portal.Host> overlays react-native-paper
└── <NavigationProvider> l'unique NavigationContainer
App.tsx initialise aussi Sentry, Firebase Cloud Messaging (handler d'arrière-plan → notification locale via Notifee), et appelle enableScreens(false) (les native screens sont désactivés). Voir navigation.md pour ce qui vit à l'intérieur de NavigationProvider.
Carte des répertoires (src/)
src/
├── App.tsx Providers racine ; init Sentry / Firebase / Crisp
├── apollo.tsx Client Apollo : chaîne de links, file offline, cache-persist, refresh de token
├── assets/ Images / animations Lottie statiques (alias : assets)
├── components/ UI + logique partagées entre écrans (alias : components)
│ ├── Navigation/ Système de nav : provider, Stack, types (alias : stack, NavigationTypes)
│ ├── Offline/ Bascule online/offline, bannière, dialog de pré-téléchargement
│ ├── HandleSyncMutationOnline/ Watcher NetInfo toujours monté ; rejoue les files offline
│ ├── SyncManagerButton/ Badge / point d'entrée de sync en attente
│ └── Charts/ Payment/ Dialogs/ Inputs/ Layout/ ... widgets partagés
├── features/ Modules de feature verticaux autonomes (le pattern moderne)
│ ├── changeMode/ Change le mode de mesure d'un capteur DeltaV
│ ├── quickLookDeltV/ Panneau « quick look » DeltaV (max/dernier, PPV, inclinomètre)
│ └── unitPreference/ Préférences d'unité d'affichage par projet
├── gql/ SORTIE de graphql-codegen (générée — ne pas éditer) (alias : gql)
├── i18n/ Init i18next + dossiers de namespaces par langue (en/fr/de/it/pt/es)
├── screens/ Destinations de navigation, chacune avec ses propres components/hooks/mutation/fragments (alias : screens)
├── store/ Redux (alias : store → ./ducks)
│ ├── index.ts configureStore + redux-persist (whitelist)
│ ├── ducks/ Ducks écrits à la main (online, token, syncManager, trackedQueries, ...)
│ └── charts-helpers.ts Transformations métrique → graphique
├── theme/ Provider de thème + palettes de couleurs clair/sombre (alias implicite via src)
├── types/ Types de domaine TS partagés
└── utils/ Non-UI : hooks, context (ble/dialog), realm/models, services, mappers, sigfox, payment (alias : utils)
features/ vs screens/ vs components/
C'est la distinction la plus importante à comprendre :
| Couche | Ce que c'est | Règle de frontière |
|---|---|---|
screens/ |
Destinations de navigation. Un écran peut posséder un arbre privé profond (components/, hooks/, mutation/, fragments/). C'est l'unité d'organisation historique / dominante. |
Aucune règle appliquée pour l'instant |
components/ |
UI + logique réutilisées entre écrans. | Aucune règle appliquée pour l'instant |
features/ |
Direction plus récente. Une tranche verticale entièrement encapsulée (domaine + données + UI + état) exposant une API publique restreinte via son index.ts. Les consommateurs n'importent que depuis le barrel. |
no-circular appliquée en erreur (voir plus bas) |
Toute nouvelle fonctionnalité non triviale doit être construite comme un module de feature, pas comme un nouveau sous-arbre de screens/. Voir ADR-001 et les implémentations de référence : changeMode (centré context/hook), quickLookDeltV (centré adapter) et unitPreference (entièrement en couches).
Alias de modules
Définis dans babel.config.js (babel-plugin-module-resolver) et reflétés dans tsconfig.json (paths). Toujours préférer les alias aux chemins relatifs profonds.
| Alias | Pointe vers |
|---|---|
components |
./src/components |
stack |
./src/components/Navigation/Stack |
NavigationTypes |
./src/components/Navigation/types |
store |
./src/store/ducks |
screens |
./src/screens |
utils |
./src/utils |
assets |
./src/assets |
gql |
./src/gql |
src |
./src/ |
Noter que store pointe vers ./src/store/ducks, et non la racine du store. Babel branche aussi @env (react-native-dotenv), le plugin reanimated et export-namespace-from.
Warning
Jest résout ces alias via babel, pas via un moduleNameMapper Jest. Voir testing.md.
Gestion de l'état
Trois mécanismes d'état coexistent par conception — voir ADR-003 :
- Redux (global, persisté) —
src/store/. Redux Toolkit ne fait que configurer le store (configureStore) ; les reducers sont des ducks écrits à la main (constante d'action + action creator + reducerswitch+ selectors par fichier), pascreateSlice. Persisté viaredux-persist+ AsyncStorage (cléroot). La whitelist incluttoken,user,trackedQueries,syncManager,measures,language,themeMode. Les ducks incluentonline,token,syncManager,trackedQueries,user,cart,plans,projects,chart, … - Zustand (local à une feature) — utilisé dans les features récentes, ex.
src/features/unitPreference/hooks/unitPreference.store.ts. - React Context (portée réduite) — providers d'appareil BLE + de dialogs (
src/utils/context/), et contextes de feature (src/features/changeMode/context/).
Règle générale : état global/persisté de l'application → duck Redux. État privé à une feature → zustand ou context à l'intérieur de cette feature. N'ajoutez pas de duck Redux pour des préoccupations locales à une feature.
Couche de données
Apollo GraphQL (src/apollo.tsx)
Le client est construit dans ApolloProviderWithReux et reconstruit quand le token d'accès change. La chaîne de links (ApolloLink.from) :
loggerLink → errorLink → trackerLink → queueLink → serializingLink → retryLink → authLink → httpLink
- errorLink intercepte
jwt expiredet appelleremoveToken. - trackerLink (
src/utils/trackerLink.ts) est la file de mutations offline (ci-dessous). - queueLink + serializingLink retiennent/sérialisent les opérations pendant l'offline.
- retryLink réessaie 3× ; authLink attache le token Bearer.
- Cache :
InMemoryCacheavectypePoliciespersonnalisés, persisté vers AsyncStorage viaapollo3-cache-persist.
Génération de code : les types GraphQL sont générés par graphql-codegen (preset client) dans src/gql/ (graphql.ts, gql.ts, fragment-masking.ts). Ce dossier est généré — ne jamais l'éditer à la main. Régénérez avec yarn generate-interfaces:<env> après avoir modifié des opérations. Les opérations elles-mêmes sont co-localisées près des écrans/features (*.mutation.ts, *.fragment.ts, api/queries/*). codegen collecte les documents depuis documents: "src/**/*/*.ts" (uniquement les .ts imbriqués — pas les .tsx ni les fichiers de premier niveau) et exige que chaque nom d'opération soit globalement unique.
Realm — défini mais NON branché
src/utils/realm/models/ définit des schémas (Metric, SensorMetrics, …) mais realm n'est pas une dépendance et il n'y a aucun Realm.open / RealmProvider nulle part. Ces classes ne sont actuellement utilisées que comme types TypeScript (ex. dans store/charts-helpers.ts). Les données de mesure offline sont en réalité stockées sous forme de fichiers JSON sur disque via react-native-fs (CachesDirectoryPath/${sensorId}.json). Considérez les modèles Realm comme vestigiaux ou « prévus mais non adoptés » — ne supposez pas qu'une base Realm active existe.
Offline-first & synchronisation
Tout repose sur le booléen Redux online (store/ducks/online).
Lectures (queries) : online bascule la fetchPolicy Apollo entre cache-and-network et cache-only, et ouvre/ferme queueLink. Le passage hors ligne est gouverné par usePreloadQueries (src/utils/usePreloadQueries.ts), qui pré-charge les données avant la bascule (déclenché depuis components/Offline/ToggleOnlineOfflineButton).
Écritures (mutations) : deux files distinctes.
- File Apollo générique —
trackerLink(src/utils/trackerLink.ts). Les opérations marquéescontext.trackedsont sérialisées (query + vars + context, viacircular-json, avec un uuid) dans le ducktrackedQuerieset retirées en cas de succès. À la reconnexion,apollo.tsxles rejoue viaclient.mutate. - File de sync métier — duck
syncManager(store/ducks/syncManager.ts). Une file spécifique aux capteurs qui suitsensorsToSync(lots de mesures avec unSyncStatus) etsyncableMutations(typées par capteur/projet :CREATE_MEASURE,UPDATE_SENSOR_ACTIVE, …).
Orchestration :
NetInfo (connectivité)
│
▼
HandleSyncMutationOnline (toujours monté, components/HandleSyncMutationOnline)
├─ perte de connectivité → propose de passer hors ligne
└─ online === true → syncMeasures() (lit les JSON par capteur, appelle SYNC_BLE_METRICS_V3)
→ execMutations() (useSyncableMutations rejoue les mutations en file)
Écran SyncManager tableau de bord de sync côté utilisateur (screens/SyncManager)
SyncManagerButton badge du nombre d'éléments en attente dans les en-têtes
Offline/* bannière + bascule online/offline manuelle + pré-téléchargement
Voir ADR-002 pour la raison des deux files.
Frontières d'architecture (dependency-cruiser)
.dependency-cruiser.js est appliqué en CI (yarn depcruise, contrôlé par DangerJS — voir ci-cd.md) :
| Règle | Sévérité | Portée | Signification |
|---|---|---|---|
no-circular-features |
erreur | src/features/ |
Aucune dépendance circulaire impliquant un module src/features/ |
no-orphans |
avertissement | tout | Signale les modules inatteignables (exclut *.test.*, *.d.ts, index.ts) |
no-circular-features se déclenche sur tout cycle touchant une feature — y compris un cycle qui reboucle via les couches legacy. Une cause fréquente est une couche basse (src/utils, src/components, src/store) qui importe le barrel racine d'une feature pendant que la feature réimporte cette couche ; corrigez-le en important le module feuille de la feature (ex. features/x/config/types) depuis la couche basse, ou en déplaçant le primitif partagé hors de la feature.
Lancez-le localement avant de committer des changements de feature (les hooks husky ne le font pas) :
NODE_OPTIONS=--max-old-space-size=8192 npx depcruise src --config .dependency-cruiser.js --output-type err
dependency-cruiser requiert Node ≥ 18.17 ; si le défaut local est plus ancien (ex. 18.16, il échoue en dur), lancez-le sous Node 20 (nvm use 20).
La config déclare aussi LEGACY_PATH_REGEX (src/(components\|screens\|store\|utils)/) mais aucune règle ne l'utilise encore — elle marque la future frontière prévue entre le code legacy et features/.
Bibliothèques clés
| Domaine | Bibliothèque |
|---|---|
| Navigation | @react-navigation/* v7 (native-stack, bottom-tabs, material-top-tabs) |
| Kit UI | react-native-paper (principal), @gorhom/bottom-sheet, moti + reanimated, lottie |
| Données | @apollo/client + apollo-link-* + apollo3-cache-persist, graphql-codegen |
| État | @reduxjs/toolkit + redux-persist + reselect, zustand |
| BLE (cœur) | react-native-ble-plx, react-native-nordic-dfu (firmware) |
| Cartes | @rnmapbox/maps |
| Graphiques | react-native-charts-wrapper, echarts |
| Paiements | @stripe/stripe-react-native |
| i18n | i18next + react-i18next, dayjs |
| Formulaires | react-hook-form + yup / zod |
| Infra | Firebase messaging, Notifee, Sentry, Crisp chat, AWS SDK, NetInfo |
Pages liées
- navigation.md — arbre des navigateurs & garde d'authentification
- coding-rules.md — conventions à suivre pour écrire du code ici
- testing.md — comment les couches sont testées
- ADR — les décisions derrière cette architecture