Aller au contenu

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.jssrc/App.tsxsrc/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 :

  1. 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 + reducer switch + selectors par fichier), pas createSlice. Persisté via redux-persist + AsyncStorage (clé root). La whitelist inclut token, user, trackedQueries, syncManager, measures, language, themeMode. Les ducks incluent online, token, syncManager, trackedQueries, user, cart, plans, projects, chart, …
  2. Zustand (local à une feature) — utilisé dans les features récentes, ex. src/features/unitPreference/hooks/unitPreference.store.ts.
  3. 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 expired et appelle removeToken.
  • 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 : InMemoryCache avec typePolicies personnalisés, persisté vers AsyncStorage via apollo3-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.

  1. File Apollo générique — trackerLink (src/utils/trackerLink.ts). Les opérations marquées context.tracked sont sérialisées (query + vars + context, via circular-json, avec un uuid) dans le duck trackedQueries et retirées en cas de succès. À la reconnexion, apollo.tsx les rejoue via client.mutate.
  2. File de sync métier — duck syncManager (store/ducks/syncManager.ts). Une file spécifique aux capteurs qui suit sensorsToSync (lots de mesures avec un SyncStatus) et syncableMutations (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