Deep linking pratico in React Native: configurazione iOS e Android

deepling-schema

12 minuti di lettura

Configurare un deep link in React Native richiede infatti molto più di una riga dentro NavigationContainer. Il percorso deve essere riconosciuto dal sistema operativo, associato all’app, consegnato al runtime JavaScript, e infine tradotto nella route corretta. In una shell ibrida come myGenius, bisogna anche decidere se aprire una screen nativa oppure una pagina della WebView.

Un link come “Apri la richiesta ferie” è utile soltanto se apre davvero la richiesta corretta. Se porta l’utente alla home dell’app, al browser, oppure a una schermata di login che perde ogni traccia del contesto originale, il deep linking non sta funzionando: l’app si è aperta, sì, ma l’esperienza resta comunque incompleta.

In questa guida vediamo il flusso completo: la registrazione di custom URI scheme e link HTTPS, la configurazione di iOS con Info.plist, Associated Domains e file apple-app-site-association, la configurazione Android con AndroidManifest.xml, android:autoVerify e assetlinks.json, la mappatura route → path con React Navigation, la gestione di cold start e warm app, l’uso di useHandleUrlDeepLink e deepLinkHelper.js, l’autenticazione, i parametri e i controlli di sicurezza, e infine i test e la diagnosi degli errori più comuni.



Configurazione iOS

Su iOS esistono due meccanismi distinti, ed è importante non confonderli tra loro: il custom URL scheme, per esempio my-genius://, e gli Universal Links, basati su https://my-genius.it e su un’associazione verificata tra app e dominio. Il custom scheme si registra in Info.plist. Gli Associated Domains sono invece un entitlement iOS, normalmente gestito nel file .entitlements e nella capability omonima di Xcode — due strade separate, con configurazioni separate.

Nel file ios/myGenius/Info.plist la registrazione minima del custom scheme è questa:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>my-genius</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>my-genius</string>
    </array>
  </dict>
</array>

Dopo questa configurazione, iOS può associare all’app URL come my-genius://attendance o my-genius://holiday-detail/4821. Il nome dello scheme deve restare stabile e coerente con il valore inserito nei prefixes di React Navigation: cambiare my-genius in un solo punto, dimenticandosi degli altri, rompe l’intero flusso.

Il custom scheme, va ricordato, non verifica affatto il proprietario del dominio o dello scheme. Un’altra app potrebbe tentare di registrare lo stesso identificatore senza che nulla lo impedisca. Per link destinati agli utenti reali è quindi sempre preferibile usare gli Universal Links.

Per i link HTTPS, bisogna aggiungere la capability Associated Domains al target iOS in Xcode. Il risultato è un entitlement simile a questo, nel file myGenius.entitlements:

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:my-genius.it</string>
</array>

Nel progetto myGenius l’entitlement applinks:my-genius.it è già presente insieme a webcredentials:my-genius.it, che appartiene però a un caso d’uso diverso: le credenziali condivise, non i deep link.

La stringa applinks: deve contenere esattamente l’host usato nei link. applinks:my-genius.it, applinks:www.my-genius.it e applinks:staging.my-genius.it non sono affatto valori automaticamente equivalenti tra loro. Se l’app deve supportare più host, ognuno va dichiarato e configurato separatamente sul server corrispondente.

La dichiarazione, poi, deve essere bidirezionale: l’app dichiara di gestire il dominio tramite l’entitlement, e il dominio dichiara a sua volta quale app è autorizzata a gestire determinati percorsi. Bisogna quindi pubblicare un file senza estensione in una delle due posizioni previste, https://my-genius.it/apple-app-site-association oppure https://my-genius.it/.well-known/apple-app-site-association. Un esempio con la sintassi components è questo:

{
  "applinks": {
    "details": [
      {
        "appIDs": [
          "TEAM_ID.BUNDLE_ID"
        ],
        "components": [
          { "/": "/attendance*" },
          { "/": "/leaves-request*" },
          { "/": "/holiday-detail/*" },
          { "/": "/sickness-archive*" },
          { "/": "/e-badge*" }
        ]
      }
    ]
  }
}

Va sostituito TEAM_ID.BUNDLE_ID con il valore reale, composto da Team ID Apple e Bundle ID dell’app — attenzione, non bisogna copiare il valore Android it.mygenius.app: i due identificativi appartengono a ecosistemi completamente diversi. Il server deve restituire il file senza redirect inattesi e con una risposta HTTPS corretta; il contenuto deve essere JSON valido, e i path dichiarati devono corrispondere esattamente ai link realmente generati dal backend.

In un’app React Native con codice nativo esposto, l’URL deve infine essere inoltrato al RCTLinkingManager. Nel progetto myGenius il collegamento è già presente in AppDelegate.swift:

override func application(
  _ app: UIApplication,
  open url: URL,
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
  return RCTLinkingManager.application(app, open: url, options: options)
}

override func application(
  _ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  return RCTLinkingManager.application(
    application,
    continue: userActivity,
    restorationHandler: restorationHandler
  )
}

Il primo metodo riguarda l’apertura di URL, incluso il custom scheme. Il secondo è quello necessario per ricevere un Universal Link consegnato come NSUserActivity. Se uno dei due passaggi manca, la configurazione server può anche essere perfetta, ma l’URL semplicemente non arriverà mai alla parte JavaScript dell’app.


Configurazione Android

Su Android la configurazione avviene principalmente nell’AndroidManifest.xml, all’interno della MainActivity che deve ricevere l’intent in arrivo.

Per un Android App Link verificato, il filtro deve dichiarare l’azione VIEW, le categorie DEFAULT e BROWSABLE, gli schemi supportati e l’host di riferimento:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTask"
  android:exported="true">

  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="http" />
    <data android:scheme="https" />
    <data android:host="my-genius.it" />
  </intent-filter>

</activity>

android:autoVerify="true" chiede al sistema di verificare l’associazione tra app e dominio. Non basta però aggiungere l’attributo e considerare la cosa fatta: il dominio deve pubblicare un assetlinks.json valido, altrimenti la verifica fallisce silenziosamente. Se si vuole limitare il filtro a percorsi specifici, è possibile usare filtri separati con android:pathPrefix; in alternativa, si può ricevere il link del dominio nella sua interezza, e lasciare poi a React Navigation il compito di accettare soltanto i path applicativi effettivamente conosciuti.

Nel manifest di myGenius l’host my-genius.it e android:autoVerify="true" sono già presenti. È presente anche android:launchMode="singleTask", importante quando un nuovo intent deve essere consegnato a un’istanza già esistente della MainActivity, invece di aprirne una seconda.

Il custom scheme richiede invece un filtro separato:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="my-genius" />
</intent-filter>

Per il flusso NFC del progetto è presente anche un filtro specifico dedicato al tag:

<intent-filter>
  <action android:name="android.nfc.action.NDEF_DISCOVERED" />
  <category android:name="android.intent.category.DEFAULT" />
  <data android:scheme="my-genius" android:host="e-badge" />
</intent-filter>

Questo filtro NFC non sostituisce quello generale per gli URL — sono due eventi differenti tra loro, e vanno tenuti concettualmente separati, anche se condividono lo stesso scheme.

Il file assetlinks.json va pubblicato nella posizione esatta, https://my-genius.it/.well-known/assetlinks.json, con una struttura minima come questa:

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "it.mygenius.app",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:...:ZZ"
      ]
    }
  }
]

Il valore package_name deve corrispondere esattamente all’applicationId Android — nel progetto myGenius è it.mygenius.app. Il fingerprint, invece, deve essere quello del certificato con cui è firmata la build effettivamente installata: il certificato debug e quello release sono diversi tra loro, ed è proprio per questo che un link può funzionare tranquillamente durante lo sviluppo, e poi aprire il browser nella build distribuita — o viceversa. Se esistono più varianti legittime dell’app, si possono dichiarare più fingerprint o più target, secondo la strategia di distribuzione adottata; il valore reale non va comunque mai inventato, ma ricavato dalla configurazione di firma e verificato insieme al team che gestisce la release.

Dopo aver installato una build sul dispositivo, è possibile inviare un URL con ADB per verificare rapidamente la configurazione:

adb shell am start 
  -a android.intent.action.VIEW 
  -c android.intent.category.BROWSABLE 
  -d "https://my-genius.it/attendance"

e, per testare il custom scheme:

adb shell am start 
  -a android.intent.action.VIEW 
  -c android.intent.category.BROWSABLE 
  -d "my-genius://attendance"

Se il custom scheme funziona ma il link HTTPS apre il browser, il problema è quasi sempre nell’associazione App Links, nell’host, nel certificato o nel path dichiarato — vale la pena controllare questi quattro punti nell’ordine, prima di cercare altrove.


Mappare i link in React Navigation

Una volta configurato il sistema operativo, React Navigation deve sapere quale path corrisponde esattamente a quale route. Il punto centrale è l’oggetto linking, passato a NavigationContainer:

import { Platform } from 'react-native';

const linking = {
  prefixes: [
    'my-genius://',
    'https://my-genius.it',
  ],
  config: {
    screens: {
      ...(Platform.OS === 'android'
        ? {
            WebView: {
              path: 'e-badge',
              parse: {
                jwt: (value) => value,
                t: (value) => value,
              },
            },
          }
        : {
            NfcScreen: {
              path: 'e-badge',
              parse: {
                jwt: (value) => value,
                t: (value) => value,
              },
            },
          }),

      AttendanceScreen: 'attendance',
      SicknessArchiveScreen: 'sickness-archive',

      LeavesFeature: {
        screens: {
          LeavesRequestScreen: 'leaves-request',
          HolidayRequestDetailScreen: 'holiday-detail/:id',
          LeavesRequestFormScreen: 'leaves-request-form',
          PendingApprovalsScreen: 'pending-approvals',
          LeavesArchiveScreen: 'leaves-archive',
        },
      },
    },
  },
};

Il mapping produce risultati come questi: https://my-genius.it/attendance porta ad AttendanceScreen; https://my-genius.it/holiday-detail/4821 porta a LeavesFeature > HolidayRequestDetailScreen, con { id: '4821' } come parametro; e my-genius://e-badge?jwt=...&t=... porta a NfcScreen su iOS, ma a WebView su Android. La differenza tra NfcScreen e WebView nell’esempio è del tutto intenzionale: la stessa feature può avere un’implementazione nativa su una piattaforma, e una destinazione nella WebView sull’altra.

Attenzione a non dichiarare due volte la stessa chiave di route nello stesso oggetto screens. In JavaScript, la proprietà definita più avanti sovrascrive semplicemente quella precedente. Se NfcScreen viene prima configurata con path: 'e-badge' e poi ridefinita con NfcScreen: 'nfc', il mapping e-badge non sarà più quello atteso su iOS, senza che nessun errore lo segnali. Per due destinazioni diverse conviene usare una sola definizione condizionale, oppure affidare il caso speciale direttamente a useHandleUrlDeepLink.

Nel AppStack di myGenius, la route principale è LeavesFeature, che monta WiredLeavesNavigator. Le screen LeavesRequestScreen, HolidayRequestDetailScreen e le altre appartengono quindi al navigatore interno, non a quello esterno. Una configurazione corretta per un navigator annidato deve rispettare questa gerarchia:

// Struttura corretta per un navigator annidato
config: {
  screens: {
    LeavesFeature: {
      screens: {
        HolidayRequestDetailScreen: 'holiday-detail/:id',
      },
    },
  },
}

Se si dichiara una route interna al livello sbagliato, React Navigation può non riconoscere affatto il path, oppure generare uno stato di navigazione diverso da quello effettivamente atteso.

Il container va poi montato così:

<NavigationContainer
  linking={linking}
  fallback={<SplashScreen />}
>
  <AppStack />
</NavigationContainer>

Il fallback evita di mostrare una schermata intermedia incoerente mentre React Navigation risolve l’URL iniziale. L’oggetto linking andrebbe definito fuori dal componente, oppure memorizzato con useMemo, in modo da non ricreare la configurazione a ogni singolo render senza motivo.

Per evitare che backend, notifiche e navigazione finiscano per usare stringhe diverse tra loro, è utile mantenere una mappa applicativa centrale:

export const DEEP_LINK_PATHS = {
  attendance: 'attendance',
  leavesRequest: 'leaves-request',
  holidayDetail: 'holiday-detail/:id',
  leavesRequestForm: 'leaves-request-form',
  pendingApprovals: 'pending-approvals',
  leavesArchive: 'leaves-archive',
  sicknessArchive: 'sickness-archive',
  eBadge: 'e-badge',
};

La configurazione React Navigation può poi consumare direttamente questi valori, invece di ripetere le stesse stringhe in più punti del codice. La costante, va detto, non sostituisce l’associazione nativa, che deve comunque dichiarare il dominio a parte — serve piuttosto a ridurre i refusi tra codice, notifiche e link generati dal backend.


Gestire URL, cold start e warm app

React Native espone due situazioni ben diverse tra loro: il cold start, in cui l’app non era in esecuzione e viene avviata direttamente dal link, e la warm app, in cui l’app è già aperta o in background e riceve semplicemente un nuovo URL.

Per il cold start si usa Linking.getInitialURL(). Per la warm app, invece, si ascolta l’evento url. Una gestione esplicita, simile a quella presente in useAppNavigator.js, può essere organizzata così:

useEffect(() => {
  if (!isReady) return undefined;

  let cancelled = false;

  const processInitialUrl = async () => {
    const initialUrl = await Linking.getInitialURL();

    if (!cancelled && initialUrl) {
      await handleUrl(initialUrl);
    }
  };

  processInitialUrl();

  const subscription = Linking.addEventListener('url', async ({ url }) => {
    if (!cancelled) {
      await handleUrl(url);
    }
  });

  return () => {
    cancelled = true;
    subscription.remove();
  };
}, [isReady, handleUrl]);

Il controllo isReady è importante quando la navigazione viene costruita in modo asincrono, per esempio dopo il controllo della sessione: navigare prima che NavigationContainer sia effettivamente pronto può produrre errori, o semplicemente una destinazione ignorata senza spiegazione.

C’è un rischio da tenere sempre presente: React Navigation, quando riceve la prop linking, usa già da solo il modulo Linking per gestire URL iniziali ed eventi successivi. Se nello stesso momento un listener manuale chiama anche handleUrl, lo stesso link può finire per essere processato due volte. Le strategie possibili sono sostanzialmente due: lasciare a React Navigation la gestione standard URL → stato di navigazione, oppure usare un handler applicativo proprietario per autenticazione, WebView, NFC e fallback. Nel secondo caso bisogna definire con chiarezza chi sia il proprietario della navigazione, senza duplicare mai il consumo dello stesso evento. L’architettura attuale di myGenius usa sia linking sia useHandleUrlDeepLink: è una scelta possibile e legittima, ma va verificata con test mirati di cold start e warm app, per evitare doppie navigazioni, reset inattesi o parametri consumati due volte.


Il ruolo di useHandleUrlDeepLink

La configurazione linking è sufficiente per una mappatura semplice. Una shell ibrida, però, deve aggiungere regole applicative più articolate: controllare se l’utente è autenticato, parcheggiare il link quando il login non è ancora completato, scegliere tra screen nativa e WebView, aggiungere parametri alla URL finale della WebApp, ed eseguire navigate o reset a seconda del flusso richiesto.

Nel progetto questa responsabilità è concentrata in src/hooks/useHandleUrlDeepLink.js. Il flusso, semplificato, è il seguente:

const handleUrl = async (incomingUrl) => {
  if (!incomingUrl) return;

  const { feature, subpath, jwt, tagId } = parseDeepLink(incomingUrl);

  if (!feature) return;

  const loggedIn = await checkLoginCookie();

  if (!loggedIn) {
    setPendingDeepLink(incomingUrl);
    navigationRef.current?.navigate('Identifier');
    return;
  }

  if (Platform.OS === 'ios' && feature === 'e-badge') {
    navigationRef.current?.navigate('NfcScreen', { jwt, tagId });
    return;
  }

  if (Platform.OS === 'android') {
    const url = `${BASE_URL}/${feature}${subpath ? `/${subpath}` : ''}`;
    navigationRef.current?.reset({
      index: 0,
      routes: [{
        name: 'WebView',
        params: { url, jwt, tagId },
      }],
    });
  }
};

Il punto davvero fondamentale, qui, è il controllo dell’autenticazione. Un link ricevuto prima del login non deve mai andare perso: se l’URL non è valido, va semplicemente ignorato registrando un errore tecnico senza dati sensibili; se l’utente non è autenticato, va salvato un riferimento pending e va aperto il login; e solo quando l’utente risulta autenticato si può procedere verso la screen nativa o la WebView.

Nel progetto, pendingDeepLink.js conserva il link in memoria con funzioni come setPendingDeepLink() e popPendingDeepLink(). È sufficiente se il login avviene nella stessa esecuzione dell’app. Se il flusso deve invece sopravvivere a una chiusura completa dell’app, bisogna progettare una persistenza controllata, evitando accuratamente di salvare token sensibili in chiaro da qualche parte.

Non tutte le feature, va ricordato, richiedono gli stessi parametri. Un link NFC può richiedere tagId; una richiesta ferie può richiedere solo id; una pagina pubblica potrebbe non richiedere nulla di particolare. È quindi preferibile validare i requisiti feature per feature:

const requiredParams = {
  'e-badge': ['tagId'],
  'holiday-detail': ['id'],
};

Una condizione globale come if (!feature || !tagId) return finirebbe per bloccare correttamente i link NFC, ma anche, del tutto involontariamente, tutti gli altri link che non c’entrano nulla con l’NFC.


Parsing e normalizzazione con deepLinkHelper.js

Il file src/helpers/deepLinkHelper.js centralizza la differenza tra custom scheme e URL HTTPS, che è più insidiosa di quanto sembri a prima vista. https://my-genius.it/e-badge produce un host my-genius.it e un path e-badge; my-genius://e-badge, invece, produce un host applicativo e-badge e un path vuoto — la stessa parola gioca due ruoli completamente diversi a seconda dello schema. Un parser che guarda soltanto pathname può quindi funzionare perfettamente per HTTPS, e fallire clamorosamente con lo scheme personalizzato.

L’helper del progetto analizza lo schema e tratta i due casi separatamente:

const match = urlNoHash.match(
  /^([a-zA-Z][a-zA-Z0-9+.-]*)://(.*)$/
);

const scheme = match ? match[1].toLowerCase() : null;
const rest = match ? match[2] : urlNoHash;

const [hostAndPath = '', queryString = ''] = rest.split('?');

let segments = [];

if (scheme === 'http' || scheme === 'https') {
  const slashIndex = hostAndPath.indexOf('/');
  const pathOnly = slashIndex >= 0
    ? hostAndPath.slice(slashIndex + 1)
    : '';

  segments = pathOnly
    .replace(/^/+|/+$/g, '')
    .split('/')
    .filter(Boolean);
} else {
  const firstSlash = hostAndPath.indexOf('/');
  const host = firstSlash >= 0
    ? hostAndPath.slice(0, firstSlash)
    : hostAndPath;
  const tail = firstSlash >= 0
    ? hostAndPath.slice(firstSlash + 1)
    : '';

  segments = [host, ...tail.split('/')].filter(Boolean);
}

Il risultato normalizzato può avere questa forma:

{
  feature: 'holiday-detail',
  subpath: '4821',
  jwt: null,
  tagId: null,
  fullUrl: 'https://my-genius.it/holiday-detail/4821',
}

Nel parser del progetto, il parametro NFC viene letto dalla chiave breve t: let tagId = params.t ?? null;. Con l’implementazione attuale, quindi, il link NFC deve necessariamente usare ?t=.... In altri punti del flusso, però, viene usato il nome tagId. Sono due rappresentazioni possibili dello stesso concetto, ma devono essere normalizzate in un solo punto, non lasciate a interpretazioni diverse in giro per il codice. Se si decide di supportare entrambi i formati, una scelta ragionevole è const tagId = params.tagId ?? params.t ?? null;.

La regola pratica, in sintesi, resta questa: il link pubblico può avere un formato compatto, se necessario per brevità; il resto dell’app deve ricevere sempre lo stesso nome, per esempio tagId; e il parser deve gestire la compatibilità tra i formati, non le singole screen sparse nel codice. Lo stesso vale per id, requestId, jwt e ogni altro parametro: la URL va interpretata una sola volta, e le screen devono ricevere sempre dati già validati e normalizzati.


Autenticazione e sicurezza

Un deep link non è, di per sé, un’autorizzazione. Il fatto che l’utente conosca una URL non significa affatto che possa leggere o modificare la risorsa indicata — è un fraintendimento comune da evitare fin dall’inizio.

Ogni link deve passare da controlli distinti tra loro: la validazione tecnica di schema, host, path e formato dei parametri; l’autenticazione, che verifica se esiste una sessione valida; l’autorizzazione, che verifica se l’utente autenticato può davvero accedere a quella risorsa specifica; e infine la validazione server, dove il backend conferma la richiesta e gli eventuali token temporanei coinvolti.

Valori come id, jwt, tagId e subpath restano sempre input esterni, per definizione. Devono essere decodificati, validati e passati al backend secondo le regole dell’applicazione — non bisogna mai usare direttamente un id ricevuto nel link per mostrare dati senza un controllo lato server a monte.

Il codice attuale supporta un jwt nella URL per alcuni flussi specifici, ed è un vincolo applicativo da trattare con la massima attenzione. Un approccio più sicuro sarebbe qualcosa come https://my-genius.it/e-badge?code=ONE_TIME_CODE, dove il backend verifica il codice, lo invalida subito dopo l’uso, e associa l’operazione alla sessione dell’utente. Il token di sessione principale resterebbe così sempre nel canale di autenticazione dell’app, senza mai essere esposto in un link condivisibile con altri.

Quando l’utente esegue il logout, un link pending non dovrebbe mai poter riutilizzare una sessione precedente ormai chiusa. Prima di riprendere la navigazione è quindi necessario verificare nuovamente la sessione, validare la scadenza del link, consumarlo una sola volta, e scartare gli eventuali parametri non più validi rimasti in giro.


Checklist di test

La configurazione non è affatto conclusa quando il link funziona soltanto dal simulatore, o con l’app già aperta. Vale la pena eseguire almeno questi test.

Sul piano funzionale: custom scheme con app chiusa; custom scheme con app già aperta; Universal Link iOS con app chiusa; Universal Link iOS con app in background; Android App Link con app chiusa; Android App Link con app già aperta; link con parametro id; link con parametro NFC; path inesistente; URL con query string malformata; app non installata, verificando il fallback web; utente autenticato; utente non autenticato; login completato e ripresa del link pending; e logout prima del consumo del link.

Per iOS, va verificato che il dominio sia presente negli Associated Domains del target corretto; che il Bundle ID nel file AASA sia quello della build installata; che il Team ID sia corretto; che apple-app-site-association sia raggiungibile senza redirect; che i path del file AASA coincidano con quelli prodotti dal backend; che AppDelegate.swift inoltri sia il custom scheme sia gli Universal Links; e che il test venga sempre ripetuto su una build reale, non soltanto in debug.

Per Android, va verificato che android:autoVerify="true" sia presente nel filtro HTTPS; che siano presenti VIEW, DEFAULT e BROWSABLE; che host e schema siano corretti; che assetlinks.json sia raggiungibile da /.well-known/; che package_name coincida con it.mygenius.app; che il fingerprint corrisponda al certificato della build installata; che MainActivity usi il launch mode previsto; e che il dominio venga effettivamente verificato sul dispositivo. È utile testare sempre almeno una build release: una configurazione verificata con il certificato debug non dimostra affatto che funzionerà anche con il certificato di produzione.

Dalla macchina di sviluppo si possono verificare i file direttamente dal server:

curl -i https://my-genius.it/.well-known/assetlinks.json
curl -i https://my-genius.it/.well-known/apple-app-site-association

Va controllato lo status HTTP, l’assenza di redirect inattesi, e la validità del contenuto JSON. Non è necessario che questi file siano visibili da una pagina web: devono soltanto essere pubblicamente raggiungibili agli endpoint previsti.


Errori comuni e diagnosi

Se il link apre il browser invece dell’app, conviene controllare nell’ordine: il dominio dichiarato nell’entitlement o nel manifest; l’host esatto, compreso l’eventuale www; il file AASA o assetlinks.json nella posizione corretta; Bundle ID, package name e certificato; il path effettivamente dichiarato; e infine la cache di verifica del sistema operativo, che a volte trattiene una configurazione precedente più a lungo del previsto.

Se il custom scheme funziona ma HTTPS no, va ricordato che il custom scheme prova soltanto che l’app registra correttamente uno schema — non prova affatto che Universal Links o App Links siano configurati correttamente. In questo caso conviene concentrarsi sull’associazione dominio-app, non sul parser JavaScript, che quasi certamente non è il colpevole.

Se l’app si apre ma resta sulla schermata iniziale, le cause probabili sono che NavigationContainer non è ancora pronto, che getInitialURL() viene letto troppo tardi o non viene letto affatto, che la route non esiste nella configurazione, che la screen è annidata ma il mapping è piatto, che una guardia di autenticazione reindirizza verso Identifier, o che il link viene processato da due handler diversi e uno sovrascrive lo stato dell’altro.

Se la screen viene aperta due volte, è il sintomo tipico di una gestione duplicata: NavigationContainer usa linking e, contemporaneamente, un listener manuale chiama handleUrl. La soluzione è scegliere un proprietario unico dell’evento, oppure fare in modo che il percorso applicativo manuale non esegua una seconda navigazione automatica sopra quella già gestita.

Se il dettaglio riceve id come undefined, va controllato che il path abbia effettivamente il parametro dinamico — HolidayRequestDetailScreen: 'holiday-detail/:id' — e che il link lo contenga davvero, per esempio https://my-genius.it/holiday-detail/4821. Se il navigator è annidato, va verificato anche il livello esatto in cui è dichiarata la route.

Se il deep link NFC viene ignorato, va verificata la coerenza tra il nome del parametro nel link (t, tagId o altro), il nome letto da deepLinkHelper.js, il nome passato a navigate o reset, la validazione dei parametri obbligatori, e il comportamento specifico della piattaforma. Una guardia globale che richiede sempre tagId può bloccare correttamente i link NFC, ma anche, del tutto involontariamente, tutti gli altri link che non c’entrano nulla.


Domande frequenti

Qual è la differenza tra custom scheme, Universal Links e App Links?

my-genius:// è un custom scheme: semplice e utile per integrazioni controllate, ma non verifica il dominio e non offre un fallback web naturale. https://my-genius.it/... è invece un link web: su iOS diventa Universal Link quando app e dominio sono associati tramite entitlement e file AASA; su Android diventa App Link quando il dominio è dichiarato negli intent filter e verificato tramite assetlinks.json.

Devo configurare sia Info.plist sia Associated Domains?

Sì, ma per responsabilità diverse tra loro. Info.plist contiene la registrazione del custom scheme. Associated Domains è un entitlement iOS, gestito nel file .entitlements e nella capability Xcode. Per gli Universal Links servono inoltre il file apple-app-site-association sul dominio, e il collegamento nativo a React Native.

Posso usare solo NavigationContainer senza useHandleUrlDeepLink?

Sì, se la navigazione è una semplice corrispondenza URL → screen, e tutte le route sono accessibili con la stessa identica logica. Un handler aggiuntivo diventa utile quando servono login guard, WebView, destinazioni diverse per piattaforma, link pending o trasformazioni dei parametri.

Posso usare sia linking sia un listener manuale?

È possibile, ma è rischioso farlo senza pensarci. Entrambi possono ricevere lo stesso URL contemporaneamente. Se si usano entrambi, va definito con chiarezza chi valida, chi naviga e chi consuma l’evento — altrimenti il link rischia di essere elaborato due volte senza che nessuno se ne accorga.

Un Universal Link funziona se l’app non è installata?

Il link resta un normale URL HTTPS, e può aprire il sito, a condizione che il server gestisca quella rotta correttamente. Il custom scheme, invece, non ha alcun fallback web automatico in questo scenario.

Perché il link funziona in debug ma non in release?

Su Android il fingerprint del certificato debug è diverso da quello release. Su iOS possono cambiare Bundle ID, provisioning profile, entitlement o Team ID. I test vanno sempre eseguiti con gli stessi identificativi e certificati della build che verrà effettivamente distribuita, non con quelli di sviluppo.

Conclusione

Un deep link affidabile nasce dall’allineamento di quattro livelli distinti: il sistema operativo, con Info.plist, Associated Domains e AndroidManifest.xml; il dominio, con apple-app-site-association e assetlinks.json; la navigazione, con prefixes, config e il mapping delle route; e la logica applicativa, con parsing, autenticazione, WebView, NFC e gestione dei link pending.

Se anche uno solo di questi livelli usa un host, un path o un parametro diverso dagli altri, l’app può aprirsi senza mai arrivare alla destinazione corretta.

La strategia più solida per myGenius è mantenere https://my-genius.it/... come formato canonico, supportare my-genius:// per i flussi tecnici, e centralizzare la trasformazione URL → destinazione in deepLinkHelper.js e nell’handler applicativo. In questo modo notifiche, email, WebApp e codice nativo finiscono per parlare tutti lo stesso linguaggio.

Il prossimo passo non è aggiungere altri path a caso, ma verificare l’intero percorso su device reali: app chiusa, app aperta, utente autenticato, utente non autenticato, build debug e build release. Un deep link è davvero pronto quando tutti questi casi conducono alla stessa destinazione prevista, senza duplicazioni e senza esporre dati sensibili lungo il percorso.

 

 

 

Vuoi sapere gli obblighi di legge della timbratura NFC?
Scoprilo nel prossimo articolo!