Implementare il `messageController`: come la WebApp comanda la shell nativa

messageController

Una WebApp dentro una WebView può occuparsi di interfaccia, form e logica applicativa. Ma non può, da sola, aprire una schermata nativa, avviare una scansione NFC, salvare un token nel Keychain o chiedere un permesso di sistema.

 

Per questo serve un bridge tra i due mondi. Nel progetto myGenius il punto centrale di questo bridge è src/controllers/messageController.js: riceve i messaggi inviati dalla WebApp, li interpreta e delega l’operazione al servizio nativo corretto.

Il controller non disegna schermate e non contiene la logica completa delle singole feature. Fa da orchestratore. Riceve un comando, controlla il suo type e, quando necessario, anche la sua action; poi attiva navigazione, autenticazione, NFC, download, cookie o permessi.

In questo articolo vediamo il flusso completo, il collegamento con WebView e AppStack, il codice attuale dell’handler e il modo più sicuro per aggiungere nuovi messaggi.


Il bridge tra WebApp e shell nativa

Il componente WebView espone due direzioni di comunicazione:

– dalla pagina web verso React Native, tramite window.ReactNativeWebView.postMessage(...);
– da React Native verso la pagina web, tramite webViewRef.current.postMessage(...).

Il pacchetto react-native-webview consegna al callback onMessage un evento il cui payload è disponibile in event.nativeEvent.data. Questo payload è una stringa: quando il messaggio contiene più informazioni, la soluzione più pratica è serializzare un oggetto JSON.

La WebApp può quindi inviare un comando come questo:

window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'screen',
  action: 'attendance',
}));

La WebView non decide direttamente come aprire AttendanceScreen. Invia soltanto un messaggio con un contratto noto. È la shell nativa a tradurlo in una chiamata a React Navigation.

Questa separazione è importante:

– la WebApp conosce il significato funzionale dell’azione;
– la shell conosce le API native e le route disponibili;
– il controller concentra il mapping tra i due ambienti;
– i servizi specializzati continuano a contenere il comportamento operativo.

In questo modo la pagina web non deve conoscere NfcManager, KeychainService, CookieManager o i dettagli di Linking su Android e iOS.


Dove viene collegato handleWebViewMessage

Il collegamento avviene nello stack principale, in src/navigators/AppStack.js.

import { handleWebViewMessage } from '../controllers/messageController';

export default function AppStack() {
  const webViewRef = useRef(null);
  const { showAlert } = useAlert();

  return (
    <Stack.Navigator initialRouteName={initialRoute}>
      <Stack.Screen
        name="WebView"
        children={({ navigation, route }) => (
          <WebViewScreen
            ref={webViewRef}
            onMessage={(event) =>
              handleWebViewMessage(
                event,
                navigation,
                webViewRef,
                showAlert,
              )
            }
            route={route}
          />
        )}
        options={{ headerShown: false }}
      />
    </Stack.Navigator>
  );
}

AppStack possiede le dipendenze che il controller non può recuperare da solo:

– navigation, per cambiare schermata o resettare lo stack;
– webViewRef, per inviare una risposta alla WebApp o modificare il pull-to-refresh;
– showAlert, per mostrare una modale nativa.

WebViewScreen riceve il callback e lo assegna alla proprietà onMessage del componente WebView (l’esempio seguente è un estratto semplificato):

const WebViewScreen = forwardRef(({ onMessage, route }, ref) => {
  const webViewRef = useRef(null);

  return (
    <WebView
      ref={webViewRef}
      source={{ uri: webviewUrl }}
      onMessage={onMessage}
      javaScriptEnabled
      sharedCookiesEnabled
      thirdPartyCookiesEnabled
    />
  );
});

Lo stesso componente espone al genitore un’interfaccia imperativa semplificata:

useImperativeHandle(ref, () => ({
  postMessage: (message) => {
    if (webViewRef.current) {
      webViewRef.current.postMessage(message);
    }
  },
  setPullToRefresh: (value) => {
    setPullToRefreshEnabled(value);
  },
}));

Il risultato è una catena lineare:

WebApp
  ↓ window.ReactNativeWebView.postMessage(JSON.stringify(...))
WebView.onMessage
  ↓
AppStack
  ↓ handleWebViewMessage(event, navigation, webViewRef, showAlert)
messageController
  ↓
servizio nativo o React Navigation

Il formato dei messaggi

Il contratto minimo è composto da type e, quando serve, action.

| type | action o dati | Operazione nativa |
|—|—|—|
| jwt_token | token | Salvataggio del token nel Keychain |
| download | id | Download del documento autenticato |
| nfc | nfcOn, nfcOff, timestampSuccess, errorBeep, ebadge | Controllo NFC, scansione, suoni o navigazione |
| login | qualsiasi valore diverso da cookies | Logout locale e reset alla schermata Identifier |
| permissions | camera | Richiesta del permesso fotocamera |
| screen | route funzionale | Navigazione verso una schermata nativa |
| cookies | `create_cookie` + `payload` | Scrittura di un cookie nella cookie store |
| `pullToRefresh` | `true`, `false`, `on` o altro | Abilitazione o disabilitazione del refresh |

Un esempio di messaggio per il download è:

window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'download',
  id: 4821,
}));

Il controller non usa un router generico né esegue codice ricevuto dalla pagina. Confronta valori espliciti. Il vantaggio è che il contratto è leggibile e ogni nuovo comando può essere ricercato facilmente nel codice.

Una precisazione importante: l’implementazione attuale usa una serie di if e di if/else if, non un unico switch. La logica è comunque organizzata prima per type e poi per action. Uno switch può essere introdotto in un refactor successivo, ma non va descritto come se fosse già presente nel file.


Il controller completo

Questa è la versione attuale di src/controllers/messageController.js, con il parsing, il routing dei messaggi e il supporto alla modale NFC.

import { saveAuthToken, getAuthToken } from '../services/KeychainService';
import { performLogout } from '@geniuswebsrl/auth';
import { downloadDocument } from '../utils/downloadDocument';
import { checkNfcEnabled, stopReader, startNfcScan } from '../services/nfcService';
import { playSound } from '../utils/soundPlayer';
import { Platform, Linking } from 'react-native';

import {
  postMessageToWebView,
  sendErrorMessage,
} from '../helpers/messageHelper';
import {
  checkFcmPermissions,
  deleteNotificationsPreference,
  requestCameraPermission,
} from '../utils/permissions';
import NfcManager, { NfcTech } from 'react-native-nfc-manager';
import { getAndSyncFirebaseToken } from '../utils/firebase';
import CookieManager from '@react-native-cookies/cookies';

export const handleWebViewMessage = async (
  event,
  navigation,
  webViewRef,
  showAlert,
) => {
  try {
    const message = JSON.parse(event.nativeEvent.data);

    if (message.type === 'jwt_token') {
      await saveAuthToken(message.token);
    }

    if (message.type === 'download') {
      const token = await getAuthToken();
      await downloadDocument(token, message.id, showAlert);
    }

    if (message.type === 'nfc') {
      if (message.action === 'nfcOn') {
        const enabled = await checkNfcEnabled();

        if (!enabled) {
          promptEnableNFC(webViewRef, showAlert);
        } else {
          await stopReader();
          startNfcScan(
            playSound,
            (msg) => postMessageToWebView(webViewRef, msg),
            (msg) => sendErrorMessage(webViewRef, msg),
          );
        }
      } else if (message.action === 'nfcOff') {
        await NfcManager.cancelTechnologyRequest().catch(() => {});
        await NfcManager.requestTechnology(NfcTech.Ndef);
      } else if (message.action === 'timestampSuccess') {
        playSound('nfc_success.mp3');
      } else if (message.action === 'errorBeep') {
        playSound('nfc_error2.mp3');
      } else if (message.action === 'ebadge') {
        navigation.navigate('NfcScreen');
      }
    }

    if (message.type === 'login') {
      if (message.action !== 'cookies') {
        await performLogout();
        await deleteNotificationsPreference();
      }

      navigation.reset({
        index: 0,
        routes: [{ name: 'Identifier' }],
      });
    }

    if (message.type === 'permissions') {
      if (message.action === 'camera') {
        await requestCameraPermission();
      }
    }

    if (message.type === 'screen') {
      if (message.action === 'leaves') {
        navigation.navigate('LeavesFeature', {
          screen: 'LeavesRequestScreen',
        });
      } else if (message.action === 'leaves-archive') {
        navigation.navigate('LeavesFeature', {
          screen: 'LeavesArchiveScreen',
        });
      } else if (message.action === 'pending-approvals') {
        navigation.navigate('LeavesFeature', {
          screen: 'PendingApprovalsScreen',
        });
      } else if (message.action === 'sickness-archive') {
        navigation.navigate('SicknessArchiveScreen');
      } else if (message.action === 'attendance') {
        navigation.navigate('AttendanceScreen');
      }
    }

    if (message.type === 'cookies') {
      if (message.action === 'create_cookie') {
        const payload = message.payload;

        CookieManager.set('https://my-genius.it/', {
          name: payload.name,
          value: payload.value,
          domain: payload.domain,
          path: payload.path,
          version: '1',
          expires: new Date(payload.expires).toISOString(),
        });
      }
    }

    if (message.type === 'pullToRefresh') {
      if (
        webViewRef &&
        webViewRef.current &&
        webViewRef.current.setPullToRefresh
      ) {
        const isEnabled =
          message.action === true || message.action === 'on';
        webViewRef.current.setPullToRefresh(isEnabled);
      }
    }
  } catch (error) {
    console.error(error);
  }
};

const promptEnableNFC = (webViewRef, showAlert) => {
  postMessageToWebView(
    webViewRef,
    JSON.stringify({ type: 'text', message: 'nfcOff' }),
  );

  if (showAlert) {
    showAlert(
      'NFC disattivato',
      'Per favore attiva NFC nelle impostazioni del dispositivo.',
      [
        {
          text: 'Annulla',
          style: 'cancel',
          onPress: () => {},
        },
        {
          text: 'Impostazioni',
          onPress: () => {
            postMessageToWebView(
              webViewRef,
              JSON.stringify({ type: 'text', message: 'internetOff' }),
            );

            if (Platform.OS === 'android') {
              Linking.sendIntent('android.settings.NFC_SETTINGS').catch(() => {
                if (showAlert) {
                  showAlert(
                    'Errore',
                    'Impossibile aprire le impostazioni NFC.',
                  );
                }
              });
            } else {
              Linking.openSettings();
            }
          },
        },
      ],
    );
  }
};

Nel file reale sono presenti anche gli import di `checkFcmPermissions` e `getAndSyncFirebaseToken`, collegati a blocchi commentati per una possibile gestione delle notifiche. Non sono necessari per i messaggi attualmente attivi e possono essere rimossi durante una pulizia successiva, senza modificare il contratto del bridge.


Cosa succede per ogni type

jwt_token: dalla sessione web al Keychain

Quando la WebApp completa l’autenticazione e invia un messaggio jwt_token, la shell chiama saveAuthToken(message.token).

{
  "type": "jwt_token",
  "token": ""
}

Il token non viene scritto direttamente in AsyncStorage o in una variabile globale. Il controller delega a KeychainService, che incapsula la persistenza sicura della credenziale.

Il controller non verifica la firma o la scadenza del JWT. Il suo compito è trasferire il token al servizio previsto dall’architettura. Validazione, refresh e logout devono restare responsabilità del modulo di autenticazione e del backend.

download: recuperare la credenziale prima del file

Per un download la WebApp invia l’identificativo del documento:

{
  "type": "download",
  "id": 4821,
}

Il controller recupera il token tramite `getAuthToken()` e lo passa a `downloadDocument` insieme all’identificativo e alla callback showAlert.

Questa sequenza evita che la WebApp debba ricevere o gestire il token nativo per avviare un download. Lato web circola soltanto l’ID del documento; l’autenticazione della chiamata viene composta nella shell.

login: logout e reset dello stack

Il tipo `login` ha un comportamento particolare. Se action non è cookies, il controller esegue:

1. performLogout();
2. deleteNotificationsPreference();
3. navigation.reset(...) verso `Identifier`.

Il reset è diverso da una normale `navigate`: elimina lo storico dello stack e impedisce di tornare alla WebView autenticata con il tasto indietro.

L’azione cookies è un’eccezione esplicita. In quel caso il controller salta il logout e procede comunque con il reset. È un dettaglio di contratto che va documentato nella WebApp: cambiare il nome dell’action può produrre un comportamento di autenticazione diverso.

permissions: richiesta fotocamera

Il supporto attivo è:

{
  "type": "permissions",
  "action": "camera",
}

Il controller chiama requestCameraPermission(). Non mostra direttamente una UI e non interpreta il risultato nel file: la gestione concreta del permesso è incapsulata in src/utils/permissions.

La stessa sezione contiene codice commentato per una possibile sincronizzazione del token Firebase. Finché non viene riattivato, non deve essere considerato un comportamento effettivamente supportato dal bridge.


La WebApp invia azioni funzionali, non nomi di componenti React Native:

window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'screen',
  action: 'leaves-archive',
}));

Il controller traduce l’azione nella route dello stack:

navigation.navigate('LeavesFeature', {
  screen: 'LeavesArchiveScreen',
});

Questo è necessario perché LeavesFeature è un navigator interno. La shell apre il contenitore esterno e specifica la schermata iniziale interna.

Le route gestite oggi sono:

– leaves → `LeavesFeature / LeavesRequestScreen`;
– leaves-archive → `LeavesFeature / LeavesArchiveScreen`;
– pending-approvals → `LeavesFeature / PendingApprovalsScreen`;
– sickness-archive → `SicknessArchiveScreen`;
– attendance → AttendanceScreen.

Per ebadge, invece, il messaggio appartiene al gruppo nfc ma produce direttamente:

navigation.navigate('NfcScreen');

Il mapping centralizzato rende evidente il contratto tra WebApp e shell. Il suo limite è che route e action sono stringhe: un refuso non viene intercettato dal compilatore e, nello stato attuale, non produce una risposta di errore alla WebApp.


NFC: dal comando WebApp alla scansione

Il gruppo nfc mostra meglio perché serve un controller intermedio. La WebApp decide quando l’utente vuole iniziare o fermare il flusso; React Native controlla l’hardware e orchestra il servizio NFC.

Avvio della scansione

Con nfcOn il controller:

1. verifica che l’NFC sia abilitato con checkNfcEnabled();
2. se è disabilitato, invia un messaggio alla WebApp e mostra una modale;
3. se è abilitato, chiude un eventuale reader precedente;
4. avvia startNfcScan;
5. passa al servizio due callback per restituire risultato o errore alla WebView.

startNfcScan(
  playSound,
  (msg) => postMessageToWebView(webViewRef, msg),
  (msg) => sendErrorMessage(webViewRef, msg),
);

Il controller non deve conoscere il parsing del badge o la chiamata API della timbratura. Riceve dal servizio un messaggio e lo inoltra alla pagina attraverso `postMessageToWebView`.

NFC disabilitato

promptEnableNFC invia prima alla WebApp:

{
  "type": "text",
  "message": "nfcOff",
}

Poi mostra una modale nativa. Su Android prova ad aprire direttamente `android.settings.NFC_SETTINGS`; su iOS usa `Linking.openSettings()`.

Se Android non riesce ad aprire le impostazioni, viene mostrato un secondo alert. La WebApp riceve anche il messaggio `internetOff` quando l’utente seleziona il pulsante delle impostazioni: questo valore fa parte del protocollo UI esistente e va mantenuto o modificato insieme al codice web che lo consuma.

Suoni e schermata badge

Le azioni timestampSuccess ed errorBeep non navigano e non rispondono alla WebApp: riproducono rispettivamente `nfc_success.mp3` e `nfc_error2.mp3`.

ebadge è invece un comando di navigazione che porta alla schermata NFC nativa. La distinzione è utile: stesso type, responsabilità diverse a seconda di action.

L’azione nfcOff usa direttamente NfcManager: annulla una richiesta tecnologica in corso e richiede `NfcTech.Ndef`. È una sequenza legata al comportamento corrente del flusso NFC; prima di cambiarla bisogna verificare il comportamento su entrambe le piattaforme e il contratto di `nfcService`.


Creazione del cookie

La WebApp può chiedere alla shell di creare un cookie:

window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'cookies',
  action: 'create_cookie',
  payload: {
    name: 'session',
    value: 'abc123',
    domain: 'my-genius.it',
    path: '/',
    expires: '2026-12-31T23:59:59.000Z',
  },
}));

Il controller usa `CookieManager.set` sulla URL `https://my-genius.it/` e copia nel cookie store nome, valore, dominio, path, versione e scadenza.

Questo passaggio è distinto dal salvataggio del JWT nel Keychain. Il Keychain serve al codice nativo; il cookie serve alla sessione della WebView. Confondere i due canali crea facilmente bug in cui la WebApp appare autenticata ma le chiamate native non hanno il token, oppure il contrario.

La libreria `@react-native-cookies/cookies` risulta oggi archiviata e deprecata nel repository upstream. Nel progetto continua a essere il contratto utilizzato dal controller; questa è quindi una dipendenza da monitorare in una futura attività di manutenzione, non un motivo per cambiare il comportamento durante l’aggiunta di un nuovo messaggio.

Pull-to-refresh

Il messaggio `pullToRefresh` non agisce sul contenuto web. Agisce sul wrapper imperativo esposto da WebViewScreen:

{
  "type": "pullToRefresh",
  "action": "on",
}

Il controller considera attivo il refresh quando action è `true` oppure `’on’`. Tutti gli altri valori lo disabilitano.

const isEnabled =
  message.action === true || message.action === 'on';

webViewRef.current.setPullToRefresh(isEnabled);

La scelta di passare dal ref, invece di manipolare direttamente il componente WebView nel controller, mantiene il controller disaccoppiato dai dettagli interni di WebViewScreen.


Messaggi dalla shell verso la WebApp

La comunicazione non è unidirezionale. `src/helpers/messageHelper.js` definisce due funzioni:

export const postMessageToWebView = (webViewRef, message) => {
  if (webViewRef.current) {
    webViewRef.current.postMessage(message);
  }
};

export const sendErrorMessage = (webViewRef, msg) => {
  if (webViewRef.current) {
    const message = JSON.stringify({
      type: 'error',
      message: msg,
    });

    webViewRef.current.postMessage(message);
  }
};

`postMessageToWebView` inoltra il messaggio già preparato; nel protocollo corrente il payload viene normalmente serializzato come stringa JSON. `sendErrorMessage` costruisce invece un formato comune per gli errori:

{
  "type": "error",
  "message": "Descrizione dell'errore"
}

Questa simmetria rende il protocollo più facile da estendere. Ogni nuova operazione dovrebbe definire sia il comando WebApp → nativo sia, quando necessario, gli eventi nativo → WebApp:

WebApp → { type: "nfc", action: "nfcOn" }
nativo → { type: "nfc", action: "success", data: ... }

Il formato esatto delle risposte deve essere concordato con il codice web. Non basta inviare un oggetto JavaScript se il consumer si aspetta una stringa JSON.


Come aggiungere un nuovo messaggio

Supponiamo di voler permettere alla WebApp di aprire una nuova schermata `ProfileScreen`.

1. Definire il contratto

Prima si stabilisce il messaggio minimo:

{
  "type": "screen",
  "action": "profile",
}

E si documenta il comportamento atteso: `profile` deve aprire `ProfileScreen`, senza parametri obbligatori.

2. Verificare che la route esista

La route deve essere registrata nello stack nativo. Il controller non può navigare verso una schermata che AppStack non conosce.

3. Aggiungere il mapping nel gruppo corretto

Nel codice attuale si estende il blocco screen:

if (message.type === 'screen') {
  if (message.action === 'profile') {
    navigation.navigate('ProfileScreen');
  } else if (message.action === 'attendance') {
    navigation.navigate('AttendanceScreen');
  }
}

Per un nuovo gruppo si aggiunge invece una nuova sezione per type, mantenendo le responsabilità separate:

if (message.type === 'documents') {
  if (message.action === 'open') {
    navigation.navigate('DocumentsScreen', {
      id: message.id,
    });
  }
}

4. Aggiungere un consumer nella WebApp

window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'screen',
  action: 'profile',
}));

5. Testare entrambi i versi

Il test non deve limitarsi alla navigazione. Occorre verificare:

– messaggio corretto con WebView montata;
– messaggio inviato prima che il ref sia disponibile;
– action sconosciuta;
– JSON non valido;
– route non registrata;
– ritorno dalla schermata nativa alla WebView;
– comportamento su Android e iOS quando l’azione usa API di sistema.


Validazione, errori e sicurezza

Il controller attuale è volutamente semplice, ma presenta alcuni punti da rendere più robusti in una versione successiva.

Il parsing può fallire

Il codice esegue direttamente:

const message = JSON.parse(event.nativeEvent.data);

Se la WebApp invia una stringa non JSON, l’eccezione viene catturata dal `try/catch` esterno e scritta in console. Non viene però inviata una risposta strutturata alla pagina.

Una validazione minima potrebbe essere:

const rawData = event?.nativeEvent?.data;

if (typeof rawData !== 'string') {
  return;
}

const message = JSON.parse(rawData);

if (!message || typeof message.type !== 'string') {
  return;
}

In un protocollo più maturo si possono validare anche action, id, token e `payload` con uno schema condiviso tra WebApp e shell.

Un catch globale non basta per il debugging

Il `catch` impedisce al callback di propagare l’errore, ma appiattisce nello stesso punto problemi molto diversi:

– JSON malformato;
– token mancante;
– route inesistente;
– permesso negato;
– NFC non disponibile;
– cookie con scadenza non valida.

La versione attuale mantiene il comportamento minimo con `console.error`. In produzione sarebbe preferibile aggiungere un codice errore stabile e, quando è sicuro farlo, una risposta alla WebApp.

Non fidarsi implicitamente del payload

Nel componente WebView attuale è configurato `originWhitelist={[‘*’]}`. È una scelta ampia, utile quando la WebView deve gestire più origini, ma non costituisce un’autenticazione del mittente. Se l’applicazione usa un’origine nota, è preferibile restringere la whitelist e mantenere comunque la validazione del protocollo.

Un messaggio proveniente dalla WebView non deve essere considerato automaticamente valido. Prima di eseguire un’azione nativa bisogna verificare almeno:

– che type sia riconosciuto;
– che action sia ammessa per quel type;
– che i dati obbligatori siano presenti;
– che gli identificativi abbiano il formato previsto;
– che l’operazione sia compatibile con lo stato corrente dell’app.

Questo vale soprattutto per token, cookie, URL, ID di documenti e navigazione.

Il protocollo deve avere una sola fonte di verità

Se la WebApp usa leaves-archive e la shell si aspetta `leavesArchive`, il messaggio viene ignorato senza un errore evidente. Per evitare divergenze:

– raccogliere i tipi e le action in una specifica;
– riutilizzare costanti quando WebApp e shell vivono nello stesso repository;
– aggiungere test per ogni comando supportato;
– prevedere una risposta per le action sconosciute;
– versionare il contratto quando cambia il significato di un messaggio.


Errori comuni da evitare

Inserire logica di business nel controller

`messageController` dovrebbe coordinare. Non dovrebbe contenere il parsing del badge, la costruzione delle richieste API o i dettagli della persistenza del token. Queste responsabilità appartengono ai servizi e ai package di feature.

Navigare direttamente dalla WebApp

La WebApp deve inviare un’intenzione funzionale. Non dovrebbe conoscere la struttura completa dei navigator nativi, soprattutto quando esistono navigator interni come LeavesFeature.

Usare stringhe senza documentare il contratto

Le stringhe sono semplici, ma non autodescrittive. Un elenco centralizzato o una tabella di messaggi riduce i refusi e rende più facile capire quali payload sono obbligatori.

Aggiungere un comando senza definire la risposta

Per un’operazione asincrona, come NFC o download, il contratto deve chiarire come la WebApp saprà se l’operazione è partita, riuscita o fallita.

Ignorare lo stato della WebView

`webViewRef.current` può essere `null` durante il mount o dopo lo smontaggio. Le helper attuali verificano il ref prima di inviare messaggi; ogni nuovo codice che usa la WebView deve mantenere la stessa protezione.


Domande frequenti

onMessage riceve già un oggetto JavaScript?

No. Con react-native-webview, il dato inviato da `window.ReactNativeWebView.postMessage` è disponibile come event.nativeEvent.data e deve essere trattato come stringa. Se il protocollo usa JSON, occorre eseguire `JSON.parse`.

Perché non chiamare direttamente i servizi nativi dalla WebApp?

Perché la WebApp gira in un contesto web e non ha accesso diretto alle API native. Il bridge delimita il confine: la pagina invia un comando, la shell decide se e come eseguirlo.

Il messageController deve gestire anche le risposte?

Non necessariamente. Può delegare l’invio a helper come `postMessageToWebView` e `sendErrorMessage`, lasciando ai servizi il compito di produrre gli eventi NFC o di errore.

Perché il controller riceve navigation come argomento?

Perché è una funzione di orchestrazione riutilizzabile e non un componente React. Ricevere navigation esplicitamente rende visibile la dipendenza e permette ad AppStack di collegarla alla route corrente.

Posso sostituire gli if con uno switch?

Sì, soprattutto se il numero di tipi cresce. Il passaggio deve però mantenere invariato il contratto dei messaggi e i side effect. Prima di cambiare struttura è utile aggiungere test sul comportamento attuale.

Come distinguere il token nel Keychain dal cookie della WebView?

Sono due canali diversi. jwt_token salva una credenziale tramite KeychainService; `cookies/create_cookie` scrive nella cookie store usata dalla WebView. La sincronizzazione tra i due deve essere esplicita e non va data per scontata.

Conclusione

Il `messageController` è il punto in cui una WebApp smette di essere soltanto una pagina dentro una WebView e inizia a comandare una vera shell mobile. Il suo lavoro non è sostituire i servizi nativi, ma tradurre un protocollo web in azioni controllate: salvare una sessione, scaricare un documento, avviare NFC, aprire una route, creare un cookie o chiedere un permesso.

L’implementazione attuale è lineare: `JSON.parse`, controllo di type, controllo di action e delega alla dipendenza corretta. Questa semplicità è un vantaggio finché il contratto resta documentato e validato.

Quando i messaggi aumenteranno, il passo naturale sarà estrarre un protocollo condiviso, validare i payload, uniformare le risposte e sostituire la catena di condizioni con una mappa o uno switch testabile. Il principio però rimane lo stesso: la WebApp esprime un’intenzione; la shell nativa decide come realizzarla.

 

Nel prossimo articolo ti spiegamo come gestire i cookies cross-origin in modo professionale.

ARTICOLI COLLEGATI