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.
Universal Links e Android App Links
Per i link destinati a utenti reali, la soluzione più solida parte normalmente da un URL HTTPS:
https://my-genius.it/attendance
https://my-genius.it/holiday-detail/4821
Lo stesso indirizzo può aprire l’app, se installata e correttamente associata al dominio, oppure il sito web, se l’app non è installata o se il sistema non riesce ad aprirla per qualche motivo.
Su iOS questo meccanismo si chiama Universal Links. Su Android si parla invece di Android App Links, cioè deep link HTTPS verificati direttamente dal sistema operativo. Non sono due nomi diversi per la stessa identica configurazione: sono due implementazioni distinte, che raggiungono comunque lo stesso obiettivo su piattaforme diverse tra loro.
Su iOS, il sistema verifica l’associazione tra app e sito attraverso due elementi complementari: l’app dichiara il dominio tra gli Associated Domains, e il sito pubblica il file apple-app-site-association, che dichiara a sua volta quale app può gestire determinati percorsi. L’associazione è quindi bidirezionale — l’app dichiara di fidarsi del dominio, e il dominio dichiara di autorizzare l’app. Quando l’utente apre un URL compatibile, iOS può portarlo direttamente nell’app; se l’app non è disponibile, lo stesso URL resta semplicemente un normale indirizzo web, apribile tranquillamente in Safari.
Su Android, invece, si usano gli intent-filter per dichiarare gli URL supportati dall’app. Per ottenere la verifica del dominio si usa android:autoVerify="true", e il sito pubblica il file all’indirizzo https://my-genius.it/.well-known/assetlinks.json. Questo file contiene l’associazione tra dominio, package dell’app e certificato di firma. Se la verifica va a buon fine, Android può aprire direttamente l’app per gli URL dichiarati, senza affidarsi soltanto a uno scheme o a una scelta manuale lasciata all’utente.
La configurazione di Info.plist, degli Associated Domains, di AndroidManifest.xml, di assetlinks.json e di apple-app-site-association richiede parecchia attenzione ai dettagli. Un dominio con www non allineato, un certificato diverso tra debug e produzione, o un percorso non incluso nei file di associazione sono più che sufficienti per far aprire il browser al posto dell’app. Questi dettagli saranno trattati con più profondità nel prossimo articolo della serie.
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 }) => {
handleIncomingUrl(url);
});
return () => 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.
Deep link, autenticazione e WebView
Un link esterno può arrivare prima ancora che l’utente abbia effettuato il login. Questo è del tutto normale, soprattutto per richieste ferie condivise da un responsabile, notifiche relative a una timbratura, inviti o collegamenti ricevuti via email, o pagine della WebApp aperte direttamente dalla shell nativa.
Il comportamento corretto non è mostrare una schermata generica e perdere completamente il contesto originale. È molto più utile modellare il flusso come una piccola macchina a stati: l’URL arriva, si verifica se è valido — in caso negativo si ignora e si registra un errore tecnico senza dati sensibili — e in caso positivo si verifica se l’utente è già autenticato; se non lo è, si conserva il link come pending e si apre il login; se lo è, si naviga verso la destinazione nativa o la WebView, e infine si segna il link come elaborato per evitare duplicazioni successive.
Nel caso di una shell ibrida come questa, lo stesso URL può avere due destinazioni diverse: una screen nativa per una funzione che richiede hardware, come l’NFC, oppure una pagina della WebApp caricata nella WebView per le funzioni gestionali più ordinarie. Per questo il parser dovrebbe occuparsi soltanto del significato del link, mentre un adapter di piattaforma separato decide come raggiungere davvero la destinazione. La schermata, di suo, non dovrebbe mai contenere regex, controllo del dominio e regole di autenticazione tutte mescolate insieme.
Un’altra regola utile da seguire sempre è non passare mai automaticamente token o credenziali nella URL della WebView. Se il backend deve autorizzare un’operazione, è preferibile usare un flusso temporaneo e verificabile, oppure recuperare i dati soltanto dopo che la sessione dell’app è stata effettivamente controllata.
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.


