Liens profonds pour applications mobiles natives : routes, jetons, ouverture dans l’app
Apprenez à gérer les liens profonds pour applications mobiles natives : planifiez les routes, gérez la fonctionnalité «ouvrir dans l’app» et transmettez des jetons en toute sécurité pour Kotlin et SwiftUI, sans code de routage personnalisé confus.

Ce que doit faire un lien profond, en clair
Quand quelqu'un tape sur un lien sur son téléphone, il s'attend à un seul résultat : être conduit au bon endroit, immédiatement. Pas quelque part d'approximatif. Pas un écran d'accueil avec une barre de recherche. Pas un écran de connexion qui oublie pourquoi il est venu.
Une bonne expérience de lien profond ressemble à ceci :
- Si l'app est installée, elle s'ouvre exactement sur l'écran que le lien indique.
- Si l'app n'est pas installée, le tap reste utile (par exemple il ouvre un fallback web ou la page du store et permet de renvoyer la personne vers la même destination après l'installation).
- Si la personne doit se connecter, elle se connecte une fois et arrive sur l'écran prévu, pas sur le démarrage par défaut de l'app.
- Si le lien contient une action (accepter une invitation, voir une commande, confirmer un email), l'action est claire et sûre.
La plupart des frustrations viennent de liens qui « marchent à peu près » mais cassent le flux. Les gens voient le mauvais écran, perdent leur contexte ou restent coincés dans une boucle : taper le lien, se connecter, atterrir sur le dashboard, taper le lien à nouveau, se reconnecter. Même une étape en plus peut faire abandonner les utilisateurs, surtout pour des actions ponctuelles comme des invitations ou des réinitialisations de mot de passe.
Avant d'écrire du code Kotlin ou SwiftUI, décidez de ce que vous voulez que les liens signifient. Quels écrans peuvent être ouverts depuis l'extérieur ? Que change-t-il si l'app est fermée ou déjà en cours ? Que doit-il se passer quand l'utilisateur est déconnecté ?
La planification évite la plupart des douleurs plus tard : des routes claires, un comportement « ouvrir dans l'app » prévisible, et un moyen sûr de transmettre des codes à usage unique sans mettre de secrets directement dans l'URL.
Types de liens profonds et où l’« ouvrir dans l’app » dérape
Tous les « liens qui ouvrent une app » ne se comportent pas de la même manière. Les traiter comme interchangeables mène aux échecs classiques : le lien ouvre au mauvais endroit, ouvre un navigateur au lieu de l'app, ou ne fonctionne que sur une plateforme.
Trois catégories courantes :
- Schémas personnalisés (par exemple un schéma d'app comme myapp:). Faciles à configurer, mais beaucoup d'apps et de navigateurs les traitent prudemment.
- Universal Links (iOS) et App Links (Android). Ils utilisent des liens web normaux et peuvent ouvrir l'app si elle est installée, ou tomber sur un site web sinon.
- Liens ouverts dans un navigateur intégré. Liens ouverts dans un client email ou le navigateur interne d'un messenger. Ils se comportent souvent différemment de Safari ou Chrome.
« Ouvrir dans l'app » peut vouloir dire différentes choses selon l'endroit où le tap a lieu. Un lien tapé dans Safari peut sauter directement dans l'app. Le même lien tapé dans un email ou un messenger peut ouvrir d'abord une WebView intégrée, et la personne doit appuyer sur un bouton « ouvrir » en plus (ou ne jamais le voir). Sur Android, Chrome peut respecter les App Links tandis que le navigateur intégré d'une app sociale peut les ignorer.
Cold start vs app déjà lancée est le piège suivant.
- Cold start : l'OS lance votre app, votre app s'initialise, et ce n'est qu'ensuite que vous recevez le lien profond. Si votre flux de démarrage affiche un écran de splash, vérifie l'auth ou charge une config distante, le lien peut être perdu à moins que vous ne le stockiez et le rejouiez après que l'app soit prête.
- Déjà lancée : vous recevez le lien pendant que l'utilisateur est en session. La pile de navigation existe, donc la même destination peut nécessiter un traitement différent (pousser un écran vs réinitialiser la pile).
Un exemple simple : un lien d'invitation tapé depuis Telegram ouvre souvent d'abord un navigateur intégré. Si votre app suppose que l'OS remettra toujours la main directement, les utilisateurs verront une page web et penseront que le lien est cassé. Prévoyez ces environnements dès le départ et vous écrirez moins de glue spécifique à la plateforme plus tard.
Planifiez vos routes avant d’implémenter quoi que ce soit
La plupart des bugs liés aux liens profonds ne sont pas des problèmes Kotlin ou SwiftUI. Ce sont des problèmes de planification. Le lien ne correspond pas clairement à un écran unique, ou il transporte trop d'options « peut-être ».
Commencez par un pattern de route cohérent qui correspond à la façon dont les gens pensent votre app : liste, détail, paramètres, checkout, invitation. Gardez-le lisible et stable, car vous le réutiliserez dans des emails, des QR codes et des pages web.
Un jeu simple de routes pourrait inclure :
- Accueil
- Liste des commandes et Détails de commande (orderId)
- Paramètres de compte
- Acceptation d'invitation (inviteId)
- Recherche (query, tab)
Puis définissez vos paramètres :
- Utilisez des IDs pour les objets uniques (orderId).
- Utilisez des paramètres optionnels pour l'état UI (tab, filter).
- Décidez des valeurs par défaut afin que chaque lien ait une destination unique préférée.
Décidez aussi de ce qui arrive quand le lien est incorrect : données manquantes, ID invalide ou contenu inaccessible pour l'utilisateur. Le défaut le plus sûr est d'ouvrir l'écran stable le plus proche (comme la liste) et d'afficher un court message. Évitez d'envoyer les gens sur un écran vide ou une page de connexion sans contexte.
Enfin, planifiez selon la source. Un QR code a généralement besoin d'une route courte qui s'ouvre rapidement et tolère une connectivité limitée. Un lien dans un email peut être plus long et inclure du contexte supplémentaire. Une page web doit se dégrader gracieusement : si l'app n'est pas installée, la personne doit quand même atterrir quelque part qui explique la suite.
Si vous utilisez une approche pilotée par le backend (par exemple en générant des endpoints API et des écrans avec une plateforme comme AppMaster), ce plan de routes devient un contrat partagé : l'app sait où aller et le backend sait quels IDs et états sont valides.
Transfert sécurisé de jetons sans mettre de secrets dans les URLs
Un lien profond est souvent traité comme une enveloppe sûre. Ce n'en est pas une. Tout ce qui est dans l'URL peut finir dans l'historique du navigateur, des captures d'écran, des aperçus partagés, des logs d'analytics ou copié dans un chat.
Évitez de mettre des secrets dans le lien. Cela inclut les tokens d'accès longue durée, les refresh tokens, les mots de passe, les données personnelles ou tout ce qui permettrait à quelqu'un d'agir au nom de l'utilisateur si le lien est transféré.
Un pattern plus sûr est un code à courte durée de vie et à usage unique. Le lien ne contient que ce code, et l'app l'échange contre une session réelle une fois ouverte. Si quelqu'un vole le lien, le code doit devenir inutilisable après une ou deux minutes, ou après le premier échange réussi.
Un flux de transfert simple :
- Le lien contient un code à usage unique, pas un token de session.
- L'app s'ouvre et appelle votre backend pour racheter le code.
- Le backend valide l'expiration, vérifie qu'il n'a pas été utilisé, puis le marque comme utilisé.
- Le backend renvoie une session authentifiée normale à l'app.
- L'app efface le code de la mémoire une fois racheté.
Même après un rachat réussi, confirmez l'identité dans l'app avant d'exécuter une action sensible. Si le lien sert à approuver un paiement, changer un email ou exporter des données, demandez une reconfirmation rapide comme la biométrie ou une nouvelle authentification.
Stockez la session résultante en toute sécurité. Sur iOS, cela signifie typiquement le Keychain. Sur Android, utilisez un stockage protégé par Keystore. Stockez seulement le nécessaire et nettoyez tout à la déconnexion, à la suppression du compte ou lorsque vous détectez une réutilisation suspecte.
Un exemple concret : vous envoyez un lien d'invitation pour rejoindre un workspace. Le lien contient un code à usage unique qui expire au bout de 10 minutes. L'app le rachète, puis affiche un écran qui indique clairement la suite (rejoindre quel workspace). Ce n'est qu'après la confirmation explicite de l'utilisateur que l'app finalise l'ajout.
Si vous construisez avec AppMaster, cela se mappe proprement à un endpoint qui rachète les codes et renvoie une session, tandis que votre UI mobile gère l'étape de confirmation avant toute action à fort impact.
Authentification et « continuer là où vous vous êtes arrêté »
Les liens profonds pointent souvent vers des écrans contenant des données privées. Commencez par décider ce qui peut être ouvert par n'importe qui (public) et ce qui nécessite une session connectée (protégé). Cette décision unique évite la plupart des surprises du type « ça marchait en test ».
Une règle simple : amenez d'abord à un état d'atterrissage sûr, puis naviguez vers l'écran protégé seulement après avoir confirmé que l'utilisateur est authentifié.
Décidez ce qui est public vs protégé
Considérez que les liens profonds peuvent être transférés à la mauvaise personne.
- Public : pages marketing, articles d'aide, début de réinitialisation de mot de passe, début d'acceptation d'invitation (aucune donnée affichée pour l'instant)
- Protégé : détails de commande, messages, paramètres de compte, écrans admin
- Mixte : un écran de prévisualisation peut convenir, mais n'affichez que des espaces réservés non sensibles tant que l'utilisateur n'est pas connecté
« Continuer après connexion » pour revenir au bon endroit
L'approche fiable est : analyser le lien, stocker la destination prévue, puis router selon l'état d'auth.
Exemple : un utilisateur tape un lien « ouvrir dans l'app » vers un ticket de support spécifique alors qu'il est déconnecté. Votre app doit s'ouvrir sur un écran neutre, lui demander de se connecter, puis l'emmener automatiquement vers ce ticket.
Pour que ce soit fiable, stockez localement une petite « cible de retour » (nom de route plus ID du ticket) avec une courte expiration. Après la connexion, lisez-la une fois, naviguez, puis supprimez-la. Si la connexion échoue ou si la cible expire, retournez à un écran d'accueil sûr.
Gérez les cas limites avec respect :
- Session expirée : affichez un court message, reconnectez, puis continuez.
- Accès révoqué : ouvrez le shell de destination, puis affichez « Vous n'avez plus accès » et proposez une étape suivante sûre.
Évitez aussi d'afficher des données privées dans les aperçus de l'écran verrouillé, les captures d'écran du switcher d'apps ou les aperçus de notifications. Laissez les écrans sensibles vides jusqu'à ce que les données soient chargées et la session vérifiée.
Une approche de routage qui évite le spaghetti du navigation personnalisée
Les liens profonds deviennent désordonnés quand chaque écran parse les URLs à sa manière. Cela répartit les petites décisions (ce qui est optionnel, ce qui est requis, ce qui est valide) dans toute l'app, et il devient difficile de changer en toute sécurité.
Traitez le routage comme une plomberie partagée. Gardez une table de routes et un parseur uniques, et faites en sorte que l'UI reçoive des entrées propres.
Utilisez une seule table de routes partagée
Faites en sorte que iOS et Android s'accordent sur une liste unique et lisible de routes. Pensez-y comme un contrat.
Chaque route mappe à :
- un écran, et
- un petit modèle d'entrée.
Par exemple, « Détails de commande » mappe à un écran Order avec une entrée du type OrderRouteInput(id). Si une route a besoin de valeurs supplémentaires (comme une source de référence), elles appartiennent à ce modèle d'entrée, pas dispersées dans le code des vues.
Centralisez le parsing et la validation
Gardez le parsing, le décodage et la validation en un seul endroit. L'UI ne devrait pas demander « ce token est-il présent ? » ou « cet ID est-il valide ? » Elle doit recevoir soit une entrée de route valide soit un état d'erreur clair.
Un flux pratique :
- Recevoir l'URL (tap, scan, share sheet)
- La parser en une route connue
- Valider les champs requis et les formats autorisés
- Produire une cible d'écran plus un modèle d'entrée
- Naviguer via un point d'entrée unique
Ajoutez un écran de fallback « lien inconnu ». Rendez-le utile, pas une impasse : montrez ce qui n'a pas pu être ouvert, expliquez pourquoi en langage clair, et offrez des actions suivantes comme Aller à l'accueil, rechercher ou se connecter.
Étape par étape : concevoir les liens profonds et le comportement « ouvrir dans l'app »
Les bons liens profonds sont ennuyeux dans le meilleur sens : les gens tapent et arrivent au bon écran, que l'app soit installée ou non.
Étape 1 : choisissez les points d'entrée qui comptent
Listez les 10 premiers types de liens que les gens utilisent vraiment : invitations, réinitialisations de mot de passe, reçus de commande, liens « voir ticket », liens promo. Gardez le nombre volontairement petit.
Étape 2 : écrivez les patterns comme un contrat
Pour chaque point d'entrée, définissez un pattern canonique et les données minimales nécessaires pour ouvrir le bon écran. Préférez des IDs stables plutôt que des noms. Décidez ce qui est requis vs optionnel.
Règles utiles :
- Un seul objectif par route (invite, reset, reçu).
- Les paramètres requis sont toujours présents ; les optionnels ont des valeurs par défaut sûres.
- Utilisez les mêmes patterns sur iOS (SwiftUI) et Android (Kotlin).
- Si vous prévoyez des changements, réservez un préfixe de version simple (comme v1).
- Définissez ce qui se passe quand des paramètres manquent (affichez un écran d'erreur plutôt qu'une page vide).
Étape 3 : décidez du comportement de connexion et de la cible post-connexion
Notez, pour chaque type de lien, si la connexion est requise. Si oui, retenez la destination et reprenez après la connexion.
Exemple : un lien de reçu peut afficher une prévisualisation sans connexion, mais l'action « Télécharger la facture » peut exiger une connexion et doit ramener l'utilisateur à ce reçu précis.
Étape 4 : définissez les règles de transfert de jetons (pas de secrets dans les URLs)
Si le lien nécessite un token à usage unique (acceptation d'invitation, reset, connexion magique), définissez sa durée de validité et comment il peut être utilisé.
L'approche pratique : l'URL contient un code à courte durée de vie et à usage unique, et l'app l'échange avec votre backend contre une session réelle.
Étape 5 : testez les trois états réels
Les liens profonds cassent sur les bords. Testez chaque type de lien dans ces situations :
- Cold start (app fermée)
- Warm start (app en mémoire)
- App non installée (le lien mène quand même quelque part de sensé)
Si vous gardez les routes, les contrôles d'auth et les règles d'échange de tokens en un seul endroit, vous évitez de disperser du code de routage personnalisé dans les écrans Kotlin et SwiftUI.
Erreurs courantes qui cassent les liens profonds (et comment les éviter)
Les liens profonds échouent souvent pour des raisons banales : une petite hypothèse, un écran renommé ou un « token temporaire » qui finit partout.
Les échecs que vous voyez (et les corrections)
-
Mettre des tokens d'accès dans l'URL (et les laisser fuiter vers des logs). Les query strings se copient, se partagent, se stockent dans l'historique et sont capturées par analytics et crash logs. Correction : ne mettre qu'un code court et à usage unique dans le lien, le racheter dans l'app et l'expirer rapidement.
-
Supposer que l'app est installée (pas de fallback). Si un lien ouvre une page d'erreur ou ne fait rien, les utilisateurs abandonnent. Correction : fournissez une page web de fallback qui explique ce qui va se passer et offre un chemin d'installation normal. Même une simple page « Ouvrez l'app pour continuer » est mieux que rien.
-
Ne pas gérer plusieurs comptes sur un même appareil. Ouvrir le bon écran pour le mauvais utilisateur est pire qu'un lien cassé. Correction : quand l'app reçoit un lien, vérifiez quel compte est actif, demandez à l'utilisateur de confirmer ou de changer de compte, puis continuez. Si l'action requiert un workspace spécifique, incluez un identifiant de workspace (non secret) et validez-le.
-
Casser les liens quand les écrans ou routes changent. Si votre route dépend des noms UI, les anciens liens meurent dès que vous renommez un onglet. Correction : concevez des routes stables basées sur l'intention (invite, ticket, commande) et gardez les versions antérieures actives.
-
Pas de traçabilité quand quelque chose tourne mal. Sans moyen de rejouer ce qui s'est passé, le support ne peut que deviner. Correction : incluez un ID de requête non sensible dans le lien, journalisez-le côté serveur et dans l'app, et affichez un message d'erreur contenant cet ID.
Un contrôle de réalité rapide : imaginez un lien d'invitation envoyé dans un chat de groupe. Quelqu'un l'ouvre sur un téléphone professionnel avec deux comptes, l'app n'est pas installée sur sa tablette, et le lien est transféré à un collègue. Si le lien contient seulement un code d'invitation, supporte un fallback, demande le bon compte et journalise un request ID, ce même lien peut réussir dans toutes ces situations sans exposer de secrets.
Exemple : une invitation qui ouvre le bon écran à chaque fois
Les invitations sont un cas classique : un collègue envoie un lien dans un messenger, et le destinataire s'attend à un tap pour arriver sur l'écran d'invitation, pas sur la page d'accueil.
Scénario : un manager invite un nouvel agent support à rejoindre l'espace de travail « Support Team ». L'agent tape l'invitation dans Telegram.
Si l'app est installée, le système doit ouvrir l'app et lui passer les détails de l'invitation. Si l'app n'est pas installée, l'utilisateur doit arriver sur une page web simple expliquant l'invitation et proposant un chemin d'installation. Après l'installation et le premier lancement, l'app doit toujours pouvoir terminer le flux d'invitation sans que l'utilisateur cherche de nouveau le lien.
Dans l'app, le flux est identique sur Kotlin et SwiftUI :
- Lire le code d'invitation depuis le lien entrant.
- Vérifier si l'utilisateur est connecté.
- Vérifier l'invitation côté backend, puis router vers l'écran correct.
La vérification est le point clé. Le lien ne doit pas contenir des secrets comme un token de session longue durée. Il doit porter un code d'invitation à usage unique qui n'est utile qu'après validation serveur.
L'expérience utilisateur doit rester prévisible :
- Pas connecté : affichage de l'écran de connexion, puis retour à l'acceptation d'invitation après la connexion.
- Connecté : une confirmation « Rejoindre l'espace de travail » puis accès au bon workspace.
Si l'invitation est expirée ou déjà utilisée, n'envoyez pas l'utilisateur sur une page d'erreur vide. Affichez un message clair et une prochaine étape : demander une nouvelle invitation, changer de compte ou contacter un admin. « Cette invitation a déjà été acceptée » vaut mieux que « Token invalide ».
Checklist rapide et prochaines étapes
Les liens profonds sont « finis » quand ils se comportent de la même façon partout : cold start, warm start et quand l'utilisateur est déjà connecté.
Checklist rapide
Avant la mise en production, testez chaque item sur de vrais appareils et versions d'OS :
- Le lien ouvre le bon écran en cold start et warm start.
- Rien de sensible n'est dans l'URL. Si vous devez passer un token, rendez-le court et idéalement à usage unique.
- Les liens inconnus, expirés ou déjà utilisés retombent sur un écran clair avec un message utile et une action suivante sûre.
- Ça marche depuis les apps email, navigateurs, scanners de QR et aperçus de messengers (certains préouvrent les liens).
- La journalisation vous dit ce qui s'est passé (lien reçu, route parsée, auth requise, succès ou raison d'échec).
Un moyen simple de valider le comportement est de sélectionner un petit ensemble de liens critiques (invitation, reset, détail de commande, ticket de support, promo) et de les passer par le même flux de tests : tap depuis un email, depuis un chat, scan d'un QR, ouverture après réinstallation.
Prochaines étapes (pour maintenir)
Si les liens profonds commencent à se répandre dans les écrans, traitez le routage et l'auth comme de la plomberie partagée, pas du code spécifique par écran. Centralisez le parsing des routes en un seul endroit, et faites en sorte que chaque destination accepte des paramètres propres (pas des URL brutes). Faites de même pour l'auth : une seule porte qui décide « continuer maintenant » vs « se connecter d'abord, puis continuer ».
Si vous voulez réduire le code glue personnalisé, il peut aider de construire le backend, l'auth et les apps mobiles ensemble. AppMaster (appmaster.io) est une plateforme no-code qui génère des backends prêts pour la production et des apps mobiles natives, ce qui facilite l'alignement des noms de routes et des endpoints de rachat de codes à usage unique au fil des évolutions.
Si vous ne faites qu'une chose la semaine prochaine, faites ceci : écrivez vos routes canoniques et le comportement de fallback exact pour chaque cas d'échec, puis implémentez ces règles dans une couche de routage unique.
FAQ
Un lien profond doit ouvrir exactement l'écran que le lien indique, pas un écran d'accueil générique ou un tableau de bord. Si l'app n'est pas installée, le tap doit quand même être utile en menant vers une destination sensée et en guidant l'utilisateur pour revenir au même point après l'installation.
Les Universal Links (iOS) et App Links (Android) utilisent des URL web normales et peuvent ouvrir l'app quand elle est installée, avec un fallback gracieux vers le site web sinon. Les schémas personnalisés sont plus simples à configurer mais peuvent être bloqués ou traités de façon incohérente par certains navigateurs et apps, ils sont donc mieux en option secondaire.
De nombreuses apps de messagerie et certains clients email ouvrent les liens dans leur propre navigateur intégré, qui ne transmet pas toujours la main au système comme Safari ou Chrome. Prévoyez un pas supplémentaire en rendant le fallback web clair et en gérant le cas où l'utilisateur atterrit d'abord sur une page web.
Au cold start, votre app peut afficher un écran de démarrage, faire des contrôles d'initialisation ou charger une config avant d'être prête à naviguer. La solution fiable est d'enregistrer immédiatement la cible du lien, de terminer l'initialisation, puis de rejouer la navigation une fois l'app prête.
N'incluez jamais de tokens d'accès longue durée, tokens de rafraîchissement, mots de passe ou données personnelles dans l'URL : les URLs peuvent être journalisées, partagées et mises en cache. Utilisez plutôt un code à courte durée de vie et à usage unique dans le lien, et échangez-le contre une session côté backend après l'ouverture de l'app.
Analysez le lien, stockez la destination prévue, puis orientez en fonction de l'état d'authentification afin que la connexion se fasse une seule fois et que l'utilisateur arrive à l'écran souhaité après. Conservez la « cible de retour » stockée petite et limitée dans le temps, et supprimez-la après usage.
Considérez les routes comme un contrat partagé et centralisez l'analyse et la validation en un seul endroit, puis transmettez des paramètres propres aux écrans au lieu d'URL brutes. Cela évite que chaque écran invente ses propres règles pour les paramètres optionnels, les IDs manquants et le traitement d'erreur.
Vérifiez d'abord quel compte est actif et s'il correspond à l'espace de travail ou au tenant indiqué par le lien, puis demandez à l'utilisateur de confirmer ou de changer de compte avant d'afficher du contenu privé. Mieux vaut une courte étape de confirmation que d'ouvrir le bon écran sous le mauvais compte.
Par défaut, ouvrez l'écran stable le plus proche, comme une page de liste, et affichez un message court expliquant ce qui n'a pas pu s'ouvrir. Évitez les écrans vides, les échecs silencieux ou de renvoyer l'utilisateur vers une page de connexion sans contexte.
Testez chaque lien important dans trois états : app fermée, app déjà en cours et app non installée, depuis des sources réelles comme email, apps de chat et scanners QR. Si vous construisez avec AppMaster, vous pouvez aligner facilement les noms de routes et les endpoints de rachat de codes à usage unique entre backend et apps natives, ce qui réduit la maintenance de glue personnalisée.


