Deep linking in React Native: URI scheme, Universal Links e App Links

Deep-linking-in-React-Native

10 minuti di lettura

 

Un utente riceve un’email con scritto “Apri la richiesta ferie”, tocca il link e si aspetta di arrivare direttamente alla richiesta corretta. Non alla home dell’app. Non al browser. Non a una schermata di login senza più traccia del contesto originale — quella frustrazione, per quanto piccola, basta a rovinare l’esperienza.

Questo è esattamente il problema che il deep linking deve risolvere: trasformare un link esterno in un ingresso preciso nell’applicazione. Il link può arrivare da un’email, da un SMS, da una notifica push, da un QR code o da una WebApp caricata dentro una WebView — le fonti cambiano, ma l’aspettativa dell’utente resta sempre la stessa.

In React Native, però, non basta “ascoltare un URL” e considerare il problema risolto. Bisogna scegliere il tipo di link giusto, configurare il rapporto tra app e sistema operativo, distinguere il cold start dalla warm app, proteggere le rotte autenticate, e collegare tutto quanto alla navigazione in modo coerente.

In questo articolo vediamo la differenza tra custom URI scheme, Universal Links e Android App Links, con un focus sul modello adottato da myGenius e sull’integrazione con React Navigation. La configurazione nativa dettagliata di iOS e Android sarà approfondita nel prossimo articolo della serie.


Che cos’è il deep linking

Un deep link è un URL che non si limita ad aprire un’app: indica anche quale contenuto o quale funzionalità mostrare esattamente una volta dentro.

Per esempio:

my-genius://attendance
my-genius://holiday-detail/4821
https://my-genius.it/attendance
https://my-genius.it/holiday-detail/4821

In tutti questi casi l’obiettivo resta lo stesso: aprire una destinazione interna precisa, come la schermata delle presenze o il dettaglio di una richiesta ferie specifica.

Un URL è composto, semplificando un po’, da uno scheme — come my-genius, https o mailto —, da un host — per esempio my-genius.it —, da un path — come /attendance o /holiday-detail/4821 —, e da una query string — come ?id=4821.

La forma dell’URL cambia in base al meccanismo scelto. In un custom scheme, per esempio, la parte subito dopo :// può essere interpretata come host oppure come primo segmento applicativo, a seconda del parser usato. Per questo è importante definire un formato canonico fin dall’inizio, e normalizzarlo in un solo punto dell’app, non in dieci posti diversi.

Il deep link non dovrebbe essere considerato soltanto un dettaglio della navigazione. È, a tutti gli effetti, un contratto pubblico tra il sistema operativo, l’app installata, il sito web associato, il servizio che invia il link, il navigatore React Native, e la schermata che infine riceve i parametri. Se anche uno solo di questi elementi usa nomi o percorsi diversi dagli altri, il link può aprire l’app senza portare davvero l’utente nel posto corretto.


Custom URI scheme: come funziona

Il custom URI scheme è il metodo più semplice per associare un URL all’app. Si sceglie uno scheme proprietario, per esempio my-genius, e si registra l’app affinché possa ricevere link con quel prefisso:

my-genius://attendance
my-genius://holiday-detail/4821

Quando il sistema operativo riconosce lo scheme, avvia l’app o la porta in primo piano, e consegna l’URL a React Native tramite il modulo Linking.

È una soluzione utile quando serve un collegamento rapido e controllato: richiede una configurazione relativamente contenuta, funziona senza possedere un dominio web, è adatta a test locali e ambienti di sviluppo, può essere utile per callback OAuth o flussi avviati da un’altra app, e permette di distinguere facilmente l’app da un normale sito web. Un’applicazione interna, per esempio, può usare my-genius://attendance per aprire la schermata delle presenze direttamente da un sistema aziendale già integrato.

Il custom scheme, però, non dimostra affatto che il link appartenga davvero alla nostra app. In generale, un’altra applicazione può tentare di registrare lo stesso scheme; il sistema operativo potrebbe quindi dover scegliere quale app aprire, o comportarsi in modo diverso tra piattaforme e versioni diverse. A questo si aggiungono altri limiti da tenere presenti: se l’app non è installata, non esiste un fallback web naturale; un link my-genius://... non è utile su desktop; alcuni browser e in-app browser possono bloccare o gestire diversamente gli scheme personalizzati; non è adatto come unico formato per link pubblici inviati a clienti o dipendenti; e i parametri nell’URL possono finire nei log, nella cronologia o nei sistemi di tracking senza che l’app se ne accorga.

Quest’ultimo punto merita un’attenzione particolare. Un URL non dovrebbe mai contenere segreti non necessari. Un token JWT, una password o un dato personale inserito nella query string può essere esposto molto più facilmente rispetto a un identificativo breve validato lato server. Se il flusso richiede davvero un token temporaneo, bisogna definire con il backend durata, revoca, uso singolo e modalità di trasmissione, non improvvisare.

Il custom scheme è quindi semplice, questo è certo, ma la semplicità non equivale affatto a sicurezza, né a una buona esperienza utente in ogni contesto possibile.



URI scheme o link HTTPS: quale scegliere

Le due soluzioni, in realtà, non si escludono affatto a vicenda. Nella maggior parte delle app è utile supportare entrambe, assegnando a ciascuna un ruolo preciso e distinto.

Aspetto Custom URI scheme Universal Links / Android App Links
Esempio my-genius://attendance https://my-genius.it/attendance
Verifica del proprietario No, lo scheme non prova il possesso Sì, tramite associazione app-dominio
App non installata Nessun fallback web naturale Il link può aprire il sito
Uso da desktop Limitato Naturale, perché è un URL HTTPS
Link in email e SMS Possibili, ma meno affidabili Preferibili
Sviluppo e test locali Molto pratico Richiede dominio e configurazione
Callback tra app Spesso appropriato Dipende dal provider
Esperienza pubblica Più fragile Più coerente e riconoscibile

Una strategia ragionevole per myGenius, in questo senso, è usare my-genius:// come percorso tecnico, fallback o supporto a integrazioni interne; usare https://my-genius.it/... per email, SMS, notifiche e collegamenti condivisibili con l’esterno; mantenere sempre la stessa semantica di percorso su entrambi i formati; e gestire la conversione in un parser centrale, senza mai duplicare la logica nelle singole schermate. La URL HTTPS dovrebbe essere considerata la forma canonica di riferimento; il custom scheme può restare un semplice alias verso la stessa destinazione applicativa.


Cold start e warm app

Molti problemi di deep linking compaiono perché si prova soltanto il caso più semplice: app già aperta, navigatore già montato, e utente già autenticato. Nella pratica, però, esistono almeno due flussi ben diversi tra loro, e vanno testati entrambi con la stessa cura.

Nel cold start, l’utente tocca il link quando il processo dell’app non è affatto in esecuzione. Il sistema avvia React Native, e l’URL viene reso disponibile come URL iniziale. React Native espone questo valore con const initialUrl = await Linking.getInitialURL();. Il punto delicato è il momento esatto in cui elaborarlo: il bundle JavaScript potrebbe essere ancora in caricamento, il NavigationContainer potrebbe non essere ancora pronto, e il controllo della sessione potrebbe non essere ancora terminato. Un flusso robusto deve quindi acquisire l’URL iniziale, attendere che il navigatore sia effettivamente pronto, validare scheme, dominio, path e parametri, verificare lo stato di autenticazione, navigare verso la destinazione o conservare il link come pending, ed evitare accuratamente di elaborare lo stesso URL una seconda volta. Se l’utente non è autenticato, il link non deve semplicemente sparire nel nulla: deve essere conservato in modo sicuro, l’utente deve completare il login, e il flusso deve poi riprendere esattamente dalla destinazione richiesta in origine. Naturalmente, prima di conservare un link bisogna sempre escludere o proteggere eventuali dati sensibili presenti nei parametri.

Nella warm app, invece, l’app è già in esecuzione, e un nuovo link viene ricevuto tramite l’evento url:

const subscription = Linking.addEventListener('url', ({ url }) =&​gt; {
  handleIncomingUrl(url);
});

return () =&​gt; subscription.remove();

In questo caso il navigatore esiste già, ma può trovarsi su una schermata qualunque in quel momento. La logica deve decidere se eseguire una normale navigazione, fare un reset completo dello stack, aggiornare semplicemente la schermata corrente, accodare il link perché manca ancora l’autenticazione, oppure rifiutare il link perché il percorso non è più valido. Cold start e warm app, insomma, non sono due nomi per la stessa identica operazione — sono due punti diversi del ciclo di vita dell’app, e vanno testati separatamente uno dall’altro.


Il ruolo di React Navigation

React Navigation offre una configurazione linking che collega i prefissi URL alla struttura dei navigatori. La configurazione definisce almeno quali prefissi accettare, quale path corrisponde a quale screen, quali segmenti rappresentano parametri, e come mostrare un fallback mentre viene risolto il link iniziale.

Un esempio semplificato, coerente con le route di myGenius, è il seguente:

const linking = {
  prefixes: [
    'my-genius://',
    'https://my-genius.it',
  ],
  config: {
    screens: {
      AttendanceScreen: 'attendance',
      LeavesRequestScreen: 'leaves-request',
      HolidayRequestDetailScreen: 'holiday-detail/:id',
    },
  },
};

La configurazione viene poi passata al container principale:

&​lt;NavigationContainer
  linking={linking}
  fallback={&​lt;SplashScreen /&​gt;}
&​gt;
  &​lt;AppStack /&​gt;
&​lt;/NavigationContainer&​gt;

In questo modo React Navigation può usare Linking per gestire sia l’URL iniziale sia i nuovi URL ricevuti mentre l’app è già aperta. Il config traduce, per esempio, holiday-detail/4821 nella route HolidayRequestDetailScreen con il parametro id già estratto e pronto all’uso.

La documentazione di React Navigation raccomanda esplicitamente di preferire la proprietà linking rispetto a una gestione manuale basata soltanto su navigationRef. La configurazione centralizzata riduce i casi in cui cold start, warm app e parametri annidati finiscono per essere trattati in modo diverso e incoerente tra loro.

Un’app reale, va detto, può avere regole che vanno ben oltre il semplice mapping URL → screen: la rotta richiede autenticazione, il contenuto viene caricato in una WebView, iOS e Android devono aprire destinazioni diverse, il link arriva da una notifica push e non direttamente da Linking, il percorso deve essere trasformato prima della navigazione, o il link deve essere conservato anche dopo il logout. In questi casi si può introdurre un handler applicativo aggiuntivo, ma bisogna stabilire con chiarezza chi sia il proprietario effettivo della navigazione. Se NavigationContainer e un listener manuale ricevono lo stesso URL e navigano entrambi, si possono creare duplicazioni, schermate aperte due volte, o reset del tutto inattesi.

Nel progetto myGenius il linking di useAppNavigator.js definisce i prefissi https://my-genius.it e my-genius://, mentre useHandleUrlDeepLink aggiunge regole di autenticazione e comportamento specifico per WebView e NFC. È un buon esempio di logica applicativa più ricca del semplice mapping, ma richiede un controllo esplicito sui duplicati e sull’ordine di elaborazione, senza il quale i due meccanismi potrebbero pestarsi i piedi a vicenda.



Errori comuni da evitare

Testare solo l’app già aperta è probabilmente l’errore più diffuso in assoluto. Un link che funziona perfettamente nella warm app può fallire clamorosamente al primo avvio, semplicemente perché il navigatore non è ancora pronto, o l’URL iniziale viene letto troppo tardi rispetto al bootstrap.

Usare soltanto il custom scheme è un’altra scorciatoia rischiosa: è comoda nei test rapidi, ma crea problemi seri quando il link viene aperto da desktop o da un dispositivo senza l’app installata. Per i percorsi destinati agli utenti reali è sempre preferibile un URL HTTPS verificato.

Confondere dominio e path è un errore più sottile, ma altrettanto insidioso: my-genius://holiday-detail/4821 e https://my-genius.it/holiday-detail/4821 non hanno affatto la stessa struttura sottostante, e il parser deve gestire esplicitamente questa differenza per produrre comunque lo stesso risultato applicativo finale.

Dimenticare i domini equivalenti è un altro classico: my-genius.it, www.my-genius.it e un eventuale dominio di staging sono, agli occhi del sistema, host completamente diversi. Devono essere trattati intenzionalmente sia nella configurazione React Native sia nell’associazione nativa, senza dare per scontato che vengano riconosciuti allo stesso modo.

Navigare prima del controllo di autenticazione produce una transizione visibilmente instabile: aprire una schermata protetta e poi rimandare l’utente al login un istante dopo è un’esperienza confusa. Meglio risolvere prima la guardia di autenticazione, conservando comunque il contesto del link originale per riprenderlo dopo.

Registrare due gestori per lo stesso URL è un errore che si paga caro nel tempo: la combinazione di linking, Linking.addEventListener e navigationRef va progettata con attenzione fin dall’inizio. Non basta aggiungere listener finché il link “sembra funzionare” nei test manuali — bisogna capire davvero chi possiede la responsabilità della navigazione.

Inserire dati sensibili nella query string resta un rischio concreto: gli URL possono essere tracciati, copiati o visualizzati nei log senza che nessuno se ne accorga. Meglio usare sempre identificativi temporanei, e minimizzare i dati effettivamente trasmessi.

Considerare il link sempre valido, infine, è forse l’errore più facile da commettere per distrazione: una richiesta può essere stata eliminata nel frattempo, un invito può essere scaduto, e una risorsa può non appartenere più all’utente autenticato. Il deep link porta alla destinazione, questo sì, ma l’autorizzazione deve restare sempre e comunque responsabilità del server.


Checklist di test

Prima di considerare conclusa l’integrazione, conviene verificare almeno questa matrice:

Scenario App installata Stato app Link Risultato atteso
1 Sì Chiusa HTTPS App avviata e route corretta
2 Sì In background HTTPS App riattivata e route corretta
3 Sì Aperta Custom scheme Evento ricevuto una sola volta
4 No Browser HTTPS Sito web o pagina di fallback
5 No Browser Custom scheme Fallback esplicito o errore gestito
6 Sì Chiusa Link a rotta protetta Login e ripresa del link
7 Sì Aperta Parametro non valido Errore controllato, senza crash
8 Sì Chiusa Notifica push Destinazione corretta dopo il bootstrap

I test vanno ripetuti su iOS e Android, sia in build di sviluppo sia in build di produzione. In particolare, la verifica del dominio può comportarsi in modo diverso tra simulatore, emulatore e dispositivo reale — vale sempre la pena controllare su hardware fisico prima del rilascio.


Domande frequenti

Deep link e Universal Link sono la stessa cosa?

No. Deep link è il termine generale per indicare un collegamento a un contenuto interno dell’app. Un custom URI scheme è un tipo di deep link tra i tanti possibili. Universal Links è il meccanismo verificato di iOS basato su HTTPS; Android usa il meccanismo equivalente, chiamato App Links.

Posso usare soltanto my-genius://?

Sì, per integrazioni interne, callback o test controllati funziona bene. Non è però la scelta migliore come unico formato per email, SMS e link condivisibili con l’esterno, perché non offre un fallback web naturale, e non verifica in alcun modo il proprietario dello scheme.

Cosa cambia tra cold start e warm app?

Nel cold start il link è l’URL con cui il sistema avvia l’app, e va letto con Linking.getInitialURL(). Nella warm app l’app è già attiva o in memoria, e il nuovo link arriva tramite l’evento url di Linking. I due flussi richiedono test e sincronizzazione distinti tra loro, non intercambiabili.

Devo implementare sia Universal Links sia Android App Links?

Se l’app supporta iOS e Android, e vuoi usare lo stesso URL HTTPS su entrambe, sì. Sono associazioni specifiche di piattaforma: iOS usa Associated Domains e apple-app-site-association; Android usa intent filter verificati e assetlinks.json.

React Navigation gestisce già i deep link?

Sì, tramite la proprietà linking del NavigationContainer o della configurazione di navigazione adottata. La configurazione deve dichiarare prefissi e mapping delle route. La logica aggiuntiva serve soltanto per casi come autenticazione, WebView, notifiche o routing specifico per piattaforma.

Un link può aprire una pagina della WebView?

Sì. Il deep link può essere interpretato dalla shell, e trasformato nell’URL della WebApp da caricare. È importante mantenere separati parsing, autenticazione e caricamento della WebView, così il comportamento resta testabile in ogni sua parte.

Conclusione

Il deep linking in React Native non consiste semplicemente nell’aprire una schermata a partire da una stringa. È il disegno di un percorso completo: dal link ricevuto all’autorizzazione, dal bootstrap dell’app alla navigazione vera e propria, fino al fallback web quando l’app non è installata sul dispositivo.

Il custom URI scheme è semplice e utile per scenari tecnici controllati. Universal Links e Android App Links sono più adatti ai collegamenti pubblici, perché usano HTTPS, verificano l’associazione con il dominio, e mantengono un’esperienza coerente anche fuori dall’app.

La scelta più pratica, nella maggior parte dei casi, è spesso supportare entrambi — ma con una regola chiara da rispettare sempre: un solo contratto di percorso, un parser centrale, e un proprietario esplicito della navigazione.

Nel prossimo articolo passeremo dalla progettazione alla configurazione pratica: Associated Domains e Info.plist su iOS, intent filter e assetlinks.json su Android, configurazione linking di React Navigation, e gestione del mapping route → deep link.

Leggi la guida pratica al deep linking