Skip to main content

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 (RootNavigatorresolveExperience).

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 ForwardingCookieHandler dans le CookieManager de la WebView — la session ENT y est donc déjà présente ;
  • iOS : sharedCookiesEnabled expose 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.tsfetchEntApps() lit apps (name/address/icon…) et authorizedActions (droits workflow). Best-effort : null en cas d'échec.
  • src/apps/catalog.ts — fusion pure statique ⊕ ENT :
    • jointure tolérante (matchEnt) sur entMatch vs name/address/prefix ENT, en forme normalisée (minuscules, sans accents ni séparateurs) ;
    • apps webview : visibilité pilotée par l'ENT, routeaddress, comingSoon levé (l'app devient ouvrable) ;
    • apps native : visibilité par les roles statiques (parcours dédiés inconnus de l'ENT) ;
    • entApps === null (chargement / hors-ligne) → repli statique.
  • src/apps/CatalogContext.tsx — charge le catalogue une fois après connexion et expose les hooks useCatalogSections (lanceur), useResolvedApps (AppSwitcher) et useResolvedApp (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 :

mountUsage
webviewBranchement 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.

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) : onNotificationOpenedAppopenApp(appId) ;
  • démarrage à froid : getInitialNotificationopenApp(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éma openent://, mapping app/:appId, deep-links dérivés du registre ;
  • __tests__/catalog.test.ts — jointure matchEnt, visibilité pilotée par l'ENT, repli statique, route ENT, regroupement par besoin.
yarn workspace @openent/mobile test
note

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

  1. Déclarer le schéma openent:// et les liens universels côté natif (Android intent-filter, iOS Associated Domains).
  2. Affiner les jetons entMatch au vu des name/address réels renvoyés par l'instance (cf. log userinfo raw), et exploiter authorizedActions pour les capacités fines (éditer vs consulter).
  3. Basculer les AppDescriptor de webview vers native application par application.
  4. (Optionnel) Persister les récents et/ou conserver des instances WebView vivantes pour une bascule instantanée entre plusieurs apps.