Navigation de l'application mobile
Cette page décrit l'architecture technique de la navigation inter-applications
de l'application mobile (frontend/apps/mobile, React Native). Pour la vision
fonctionnelle des parcours, voir le module Application mobile.
Contexte
L'application mobile doit donner accès à un catalogue d'applications ENT (Actualités, Agenda, Blog, Cahier de textes, Carnet de liaison, Cours et Wiki, Formulaire, Messagerie, Vote électronique, Cahier multimédia, Espace documentaire), regroupées par besoin utilisateur (notifications, édition, messagerie, consultation) et évolutives dans le temps.
La pile technique est :
- React Native 0.85 / React 19 ;
- React Navigation 7 (
native-stack+bottom-tabs) ; - une notion d'« expérience » (
src/experiences/) qui aiguille l'utilisateur vers un parcours selon son profil annuaire (RootNavigator→resolveExperience).
Principe : un modèle « Registre + Lanceur + AppShell »
La navigation inter-applications repose sur trois briques, posées au-dessus des expériences existantes.
1. Le registre d'applications — src/apps/registry.ts
Unique source de vérité du catalogue. Chaque application est un
AppDescriptor déclaratif :
interface AppDescriptor {
id: AppId;
label: string;
icon: string;
categories: AppCategory[]; // besoins → sections du lanceur
capabilities: Capability[]; // notify | edit | consult | message
roles: RoleExperience[]; // visibilité (repli hors-ligne)
mount: 'native' | 'webview'; // intégration progressive
route?: string; // repli si l'ENT ne fournit pas d'adresse
entMatch: string[]; // jetons de jointure avec le catalogue ENT
comingSoon?: boolean; // tuile « bientôt » tant que non branchée
}
Les métadonnées mobiles (besoins, mount, icon) restent statiques ; la
visibilité et la route réelles viennent de l'ENT (cf. pont ci-dessous).
Le lanceur, le routage des notifications et la visibilité par profil en découlent : rendre une application disponible se résume à ajouter une entrée, sans toucher à la navigation. C'est ce qui satisfait l'exigence « rester évolutive ».
Helpers exposés :
appsForRole(role)— applications visibles pour un profil ;appsByCategory(role)— applications regroupées par besoin, prêtes pour les sections du lanceur (sections vides omises) ;findApp(id)— résolution par identifiant (deep-link, notification).
2. Le lanceur « Mes apps » — src/apps/AppLauncherScreen.tsx
Écran présentant les applications du profil connecté en grille regroupée par
besoin (Notifications, Messagerie, Consultation, Édition & alimentation). Il ne contient aucune liste en dur : tout provient de
appsByCategory(role). Le rôle est dérivé du profil annuaire via
resolveExperience (repli sur enseignant pour un profil non encore dessiné,
plutôt qu'un écran vide).
L'ouverture d'une application passe par la prop onOpenApp(app). Tant qu'une
application est marquée comingSoon, la tuile affiche un badge « Bientôt » et un
message d'attente — ce qui permet de publier le lanceur complet avant que
toutes les applications soient branchées.
3. L'AppShell — src/apps/AppShell.tsx
Enveloppe commune d'une application ouverte : en-tête homogène (retour, titre,
bouton de bascule inter-apps) et corps natif ou WebView selon app.mount.
Elle est montée comme écran de la pile racine (AppShell, paramétré par
appId), au-dessus de la navigation « socle » :
// RootNavigator (extrait)
<Stack.Screen name="Main">{() => <Themed2d />}</Stack.Screen>
<Stack.Screen name="AppShell" component={AppShell} />
Le lanceur y navigue directement : navigation.navigate('AppShell', { appId }).
4. La WebView authentifiée — src/apps/WebAppScreen.tsx
Pour une application non encore portée en natif, l'AppShell rend une WebView
pointant sur getBaseUrl() + app.route (même origine que l'API). La session
est partagée sans manipulation de jeton :
- Android : le networking React Native écrit les cookies via
ForwardingCookieHandlerdans leCookieManagerde la WebView — la session ENT y est donc déjà présente ; - iOS :
sharedCookiesEnabledexpose les cookies du jar système à WKWebView.
L'écran gère l'indicateur de chargement et un état d'erreur avec ré-essai.
5. L'AppSwitcher — src/apps/AppSwitcherScreen.tsx
Feuille (modal transparent) de bascule rapide entre applications, sans repasser
par le lanceur — c'est le « mode de navigation entre applications » du cahier des
charges. Ouverte depuis le bouton de l'en-tête de l'AppShell
(navigate('AppSwitcher', { currentAppId })), elle liste les applications
ouvertes de la session (les récents, cf. ci-dessous), met en évidence l'app
courante et remplace l'AppShell par l'app choisie
(navigation.replace('AppShell', …) → bascule, pas empilement). Tant qu'une
seule application a été ouverte, elle propose le catalogue du profil pour rester
utile.
6. Le suivi des applications ouvertes — src/apps/OpenAppsContext.tsx
L'AppShell étant unique et basculé par remplacement, on ne conserve pas
plusieurs instances vivantes : OpenAppsProvider mémorise en mémoire l'ordre
d'usage des applications (la plus récente en tête, plafonné). L'AppShell
appelle markOpened(appId) à l'ouverture ; l'AppSwitcher lit recents. La liste
se réinitialise à chaque lancement (pas de persistance).
Pont avec le catalogue ENT
Le registre statique ne décrit que des métadonnées mobiles. La liste des
applications réellement accessibles (et leur URL) provient de l'instance, via
/auth/oauth2/userinfo (entcore) — le même apps que le launcher du portail
web, déjà filtré par les droits de l'utilisateur.
src/services/entApps.ts—fetchEntApps()litapps(name/address/icon…) etauthorizedActions(droits workflow). Best-effort :nullen cas d'échec.src/apps/catalog.ts— fusion pure statique ⊕ ENT :- jointure tolérante (
matchEnt) surentMatchvsname/address/prefixENT, en forme normalisée (minuscules, sans accents ni séparateurs) ; - apps
webview: visibilité pilotée par l'ENT,route←address,comingSoonlevé (l'app devient ouvrable) ; - apps
native: visibilité par lesrolesstatiques (parcours dédiés inconnus de l'ENT) ; entApps === null(chargement / hors-ligne) → repli statique.
- jointure tolérante (
src/apps/CatalogContext.tsx— charge le catalogue une fois après connexion et expose les hooksuseCatalogSections(lanceur),useResolvedApps(AppSwitcher) etuseResolvedApp(AppShell).
Conséquence : activer une application sur l'instance la fait apparaître dans le
mobile sans rebuild. La jointure entMatch accepte plusieurs alias car les
noms d'application varient selon les instances (ex. Messagerie =
Conversation).
Intégration progressive : mount
Le champ mount autorise une livraison incrémentale sans rupture :
mount | Usage |
|---|---|
webview | Branchement rapide d'une application web ENT existante (cookie SSO). |
native | Écran React Native dédié (ex. Carnet de liaison côté parent). |
Une application démarre généralement en webview puis est réécrite en native
au fil de l'eau — sans changer le registre ni la navigation.
Notifications → deep-links
La configuration linking (src/navigation/linking.ts) est dérivée du
registre : chaque application est joignable via openent://app/<id>, qui
pousse l'AppShell avec le bon appId. Elle est branchée sur le
NavigationContainer (App.tsx), aux côtés d'une référence de navigation
globale (src/navigation/navigationRef.ts) utilisable hors composants :
<NavigationContainer ref={navigationRef} linking={buildLinking()}>
Le service push (src/services/push.ts) route alors l'ouverture d'une
notification vers l'application ciblée. Convention de charge utile :
data.appId. Les deux points d'entrée sont couverts :
- app en arrière-plan (tap) :
onNotificationOpenedApp→openApp(appId); - démarrage à froid :
getInitialNotification→openApp(appId), avec une brève ré-tentative tant que la navigation n'est pas prête.
Tests
La logique pure est couverte par Jest, sans dépendance native :
__tests__/apps-registry.test.ts— intégrité du catalogue,findApp,appsForRole, regroupement et ordre des sections (appsByCategory) ;__tests__/linking.test.ts— schémaopenent://, mappingapp/:appId, deep-links dérivés du registre ;__tests__/catalog.test.ts— jointurematchEnt, visibilité pilotée par l'ENT, repli statique, route ENT, regroupement par besoin.
yarn workspace @openent/mobile test
Le smoke test __tests__/App.test.tsx (rendu de <App/>) échoue
indépendamment de cette fonctionnalité : il requiert la transformation Jest des
dépendances ESM et le build de @openent/ts-client. Les composants de
navigation sont donc couverts via leurs modules purs plutôt que par le rendu
complet.
Étapes suivantes
- Déclarer le schéma
openent://et les liens universels côté natif (Androidintent-filter, iOSAssociated Domains). - Affiner les jetons
entMatchau vu desname/addressréels renvoyés par l'instance (cf. loguserinfo raw), et exploiterauthorizedActionspour les capacités fines (éditer vs consulter). - Basculer les
AppDescriptordewebviewversnativeapplication par application. - (Optionnel) Persister les récents et/ou conserver des instances WebView vivantes pour une bascule instantanée entre plusieurs apps.