15 minuti di lettura
Avvicinare un badge allo smartphone sembra un gesto banale. Dal punto di vista dell’app, però, la timbratura NFC attraversa diversi livelli: hardware, API native, libreria React Native, parsing del tag, autenticazione e chiamata al backend.
Se uno di questi passaggi viene gestito male, l’app può mostrare un lettore che non parte, lasciare una sessione NFC aperta, leggere un tag senza contenuto utile oppure inviare alla API un identificativo diverso da quello atteso.
In questa guida vediamo come implementare la lettura NFC in React Native con react-native-nfc-manager, prendendo come riferimento il flusso di una timbratura aziendale su iOS e Android.
L’obiettivo non è soltanto ottenere un oggetto tag in JavaScript. Il flusso completo deve:
- verificare il supporto NFC del dispositivo;
- avviare la sessione di lettura;
- richiedere la tecnologia corretta, in questo caso
Ndef; - leggere il tag;
- distinguere messaggio NDEF e identificativo del tag;
- estrarre la credenziale contenuta nel badge;
- chiudere sempre la sessione NFC;
- inviare la timbratura al backend con un contratto esplicito.
Come funziona una lettura NFC in React Native
NFC significa Near Field Communication. È una tecnologia di comunicazione a corto raggio: il dispositivo e il tag devono essere molto vicini perché lo scambio di dati abbia luogo.
In una soluzione di timbratura basata su badge, il flusso applicativo può essere rappresentato così:
Utente preme Entrata/Uscita
↓
useNfc avvia la scansione
↓
NfcManager.requestTechnology(NfcTech.Ndef)
↓
NfcManager.getTag()
↓
useHandleNfcTag analizza ndefMessage e tag.id
↓
Estrazione di token e identificativo
↓
POST /badge
↓
Feedback di successo o errore
↓
Cancel della richiesta NFC
La libreria react-native-nfc-manager fa da bridge verso le API native. Non sostituisce però la logica applicativa: è il codice dell’app che deve decidere quale tecnologia richiedere, come interpretare il contenuto e quali dati inviare al server.
Un punto importante è il ciclo di vita della scansione. Una richiesta NFC non dovrebbe essere considerata una semplice funzione che restituisce una stringa. È una sessione che deve essere:
- aperta nel momento corretto;
- associata a una sola operazione, se il caso d’uso prevede una singola timbratura;
- annullata anche in caso di eccezione, uscita dalla schermata o cancellazione da parte dell’utente.
NDEF e UID: due dati diversi
Quando si parla di “ID del tag NFC” si rischia di confondere due informazioni differenti.
Il messaggio NDEF
NDEF è il formato usato per memorizzare uno o più record sul tag. Un record può contenere, per esempio:
- testo;
- URL;
- dati MIME;
- un payload applicativo serializzato in JSON.
Nel progetto il badge contiene un record di testo con un JSON simile a questo:
{
"tk": "eyJhbGciOi..."
}
Il nome della proprietà può essere tk oppure token, in base al formato prodotto dal sistema che prepara il badge.
La lettura NDEF risponde alla domanda:
Quali dati applicativi sono stati scritti nel tag?
L’UID o identificativo hardware
Il campo tag.id, quando disponibile, rappresenta invece l’identificativo del tag esposto dalla piattaforma. Su Android è normalmente disponibile quando il tag viene rilevato e può essere usato come identificativo di prossimità.
L’UID risponde a una domanda diversa:
Quale supporto fisico è stato rilevato?
Non va confuso con ndefMessage[0].id: l’ID di un record NDEF è un campo del record e non coincide necessariamente con l’UID del supporto.
Perché la differenza tra iOS e Android è importante
Con NfcTech.Ndef, iOS usa il flusso di lettura NDEF di Core NFC. In questo scenario il contenuto NDEF è il dato più affidabile da usare a livello cross-platform, mentre l’UID hardware non deve essere dato per scontato.
Android espone più facilmente tag.id durante la lettura. iOS, invece, può non esporre l’UID nella stessa sessione NDEF. Per accedere a informazioni a basso livello possono servire tecnologie e reader session differenti, con compatibilità diversa tra famiglie di tag.
La conseguenza pratica è decisiva:
- se il backend richiede solo una credenziale contenuta nel badge, usare un payload NDEF con token breve e verificabile lato server;
- se il backend richiede anche l’identificativo fisico, testare esplicitamente iOS e Android e definire un fallback;
- non progettare un flusso cross-platform assumendo che
tag.idabbia sempre lo stesso significato e sia sempre disponibile; - non usare l’UID come prova assoluta dell’identità della persona: un badge è una credenziale che può essere perso o prestato.
Configurare react-native-nfc-manager
La libreria usa moduli nativi. L’installazione JavaScript è solo una parte del lavoro: dopo aver aggiunto la dipendenza bisogna completare la configurazione delle due piattaforme.
Nel progetto la dipendenza è:
{
"react-native-nfc-manager": "3.16.1"
}
In un progetto nuovo, il comando è:
npm install react-native-nfc-manager
Per iOS bisogna poi installare i pod:
cd ios pod install cd ..
Su React Native moderno il modulo viene normalmente collegato tramite autolinking. È comunque necessario verificare il progetto nativo generato, le capability e i file di configurazione prima di provare la scansione.
La lettura NFC deve essere provata su dispositivi fisici. Un simulatore iOS o un emulatore Android non riproduce il comportamento dell’hardware NFC reale.
Configurazione iOS
La configurazione iOS richiede almeno due passaggi.
1. Abilitare la capability NFC
In Xcode, nella sezione Signing & Capabilities del target dell’app, aggiungere:
Near Field Communication Tag Reading
Questa capability abilita l’entitlement necessario alla lettura dei tag supportati da Core NFC.
2. Aggiungere NFCReaderUsageDescription
Nel file Info.plist bisogna dichiarare il motivo per cui l’app usa il lettore NFC:
NFCReaderUsageDescription Usiamo il lettore NFC per registrare la timbratura con il badge aziendale.
Il testo deve essere comprensibile per l’utente e descrivere la finalità reale. Se l’app tenta di iniziare una lettura senza la chiave richiesta, il comportamento può essere un errore di runtime o la chiusura dell’app.
3. Controllare l’entitlement
Per la lettura NDEF, il target deve avere la configurazione NFC coerente con il formato che si vuole leggere. In una configurazione iOS possono essere presenti, per esempio, i formati:
com.apple.developer.nfc.readersession.formats NDEF
Il contenuto esatto dipende dalla capability e dal tipo di tag gestito. Questa configurazione appartiene agli entitlements del target, non al file Info.plist. Non bisogna aggiungere formati o tecnologie a caso: una lettura NDEF e una lettura a basso livello non sono lo stesso flusso.
4. Considerare l’interfaccia di sistema
Su iOS la scansione apre l’interfaccia di sistema di Core NFC. Il testo mostrato all’utente e la chiusura della sessione fanno parte dell’esperienza. Se l’utente annulla la sessione o allontana il tag, il codice JavaScript deve riportare lo stato dell’interfaccia a idle e permettere una nuova scansione.
Configurazione Android
Nel manifest Android l’app deve dichiarare l’uso dell’hardware NFC:
<uses-permission android:name="android.permission.NFC" />
Si può anche dichiarare la caratteristica hardware:
<uses-feature android:name="android.hardware.nfc" android:required="false" />
Il valore di android:required dipende dal prodotto. Se l’app ha funzioni non NFC e deve rimanere installabile su dispositivi senza NFC, false evita di escludere quei dispositivi dal catalogo. Se l’NFC è indispensabile per l’intera app, la decisione può essere diversa.
Con react-native-nfc-manager conviene controllare il manifest risultante dopo il manifest merger, invece di assumere che la configurazione sia completa.
A differenza di una fotocamera o della posizione, l’NFC non segue normalmente lo stesso modello di permesso runtime richiesto da quelle API. Bisogna comunque gestire due casi distinti:
- il dispositivo non supporta l’NFC;
- il dispositivo supporta l’NFC, ma la funzione è disabilitata nelle impostazioni di sistema.
La libreria espone i metodi utili per entrambi i controlli:
const supported = await NfcManager.isSupported(); const enabled = await NfcManager.isEnabled();
isSupported() e isEnabled() non sono intercambiabili. Un telefono può avere l’hardware NFC ma averlo disabilitato.
La scansione minima con requestTechnology e getTag
Il flusso più semplice per leggere un tag NDEF è questo:
import NfcManager, { NfcTech } from 'react-native-nfc-manager';
export async function readNdefTag() {
try {
await NfcManager.start();
await NfcManager.requestTechnology(NfcTech.Ndef);
const tag = await NfcManager.getTag();
return tag;
} catch (error) {
console.warn('Errore nella lettura NFC:', error);
return null;
} finally {
await NfcManager.cancelTechnologyRequest().catch(() => {});
}
}
I passaggi hanno responsabilità diverse:
NfcManager.start()inizializza il modulo;requestTechnology(NfcTech.Ndef)registra l’interesse per i tag che espongono contenuto NDEF;getTag()restituisce il tag rilevato;cancelTechnologyRequest()chiude la richiesta e libera la sessione.
La chiamata a cancelTechnologyRequest() deve stare nel finally. Se viene eseguita solo quando tutto va bene, una cancellazione dell’utente o un errore di parsing può lasciare la richiesta in uno stato non previsto e impedire scansioni successive.
Quando l’app non deve più usare il modulo, per esempio in un servizio o durante uno shutdown esplicito, si può valutare anche NfcManager.stop(). Non bisogna però confondere stop() con il cleanup della singola richiesta: il metodo essenziale dopo requestTechnology() è il cancel della richiesta.
Il servizio nfcService.js
Nel progetto la logica di integrazione con react-native-nfc-manager è raccolta in src/services/nfcService.js.
Il servizio espone tre responsabilità principali:
- inizializzare e verificare il lettore;
- avviare una scansione NDEF;
- fermare il lettore e rilasciare la richiesta.
Avvio e controllo del lettore
Una versione semplificata del servizio è:
import NfcManager, { NfcTech } from 'react-native-nfc-manager';
export const checkNfcEnabled = async () => {
try {
return await NfcManager.isEnabled();
} catch (error) {
console.warn('Errore nel controllo NFC:', error);
return false;
}
};
export const startReader = async () => {
try {
await NfcManager.start();
await NfcManager.requestTechnology(NfcTech.Ndef);
} catch (error) {
console.warn('Errore nell’attivazione del lettore:', error);
throw error;
}
};
export const stopReader = async () => {
try {
await NfcManager.cancelTechnologyRequest();
await NfcManager.stop();
} catch (error) {
console.warn('Errore nella disattivazione NFC:', error);
}
};
Nel codice attuale startReader() esegue anche NfcManager.start(). Questo rende il metodo riutilizzabile, ma significa che il chiamante deve conoscere bene il ciclo di vita: non bisogna avviare più richieste contemporaneamente e bisogna chiamare stopReader() o almeno cancelTechnologyRequest() alla fine del flusso.
startNfcScan
Il servizio dispone anche di startNfcScan, che legge il tag e inoltra il risultato alla WebView tramite callback:
export const startNfcScan = async (
playSoundCallback,
postMessageCallback,
sendErrorMessage,
) => {
try {
await startReader();
const tag = await NfcManager.getTag();
const tokenObject = tag
? decodeNdefMessage(tag, sendErrorMessage)
: undefined;
if (!tokenObject) {
sendErrorMessage('Token non valido');
return;
}
postMessageCallback(JSON.stringify({
type: 'tag',
message: tokenObject,
}));
} catch (error) {
console.warn('Errore durante la scansione NFC:', error);
}
};
Nel flusso WebView, postMessageCallback traduce il risultato nativo in un messaggio che la pagina web può consumare. Nel codice attuale playSoundCallback è mantenuto nella firma ma non viene utilizzato: va rimosso oppure collegato a un feedback reale, evitando callback che suggeriscono comportamenti inesistenti. Il servizio non dovrebbe però diventare il posto in cui si mescolano hardware, UI, navigazione e regole del backend.
Una versione più robusta dovrebbe rendere esplicito anche il cleanup della singola scansione:
export const startNfcScan = async (
postMessageCallback,
sendErrorMessage,
) => {
try {
await startReader();
const tag = await NfcManager.getTag();
const tokenObject = tag
? decodeNdefMessage(tag, sendErrorMessage)
: undefined;
if (!tokenObject) {
sendErrorMessage('Token non valido');
return;
}
postMessageCallback(JSON.stringify({
type: 'tag',
message: tokenObject,
}));
} catch (error) {
console.warn('Errore durante la scansione NFC:', error);
sendErrorMessage('Errore durante la scansione NFC');
} finally {
await NfcManager.cancelTechnologyRequest().catch(() => {});
}
};
Se il cleanup è gestito dal controller chiamante, bisogna documentarlo e testarlo. Avere un solo proprietario della sessione è preferibile a chiamare start, cancel e stop da più livelli senza una regola chiara.
Parsing del record NDEF
La lettura di tag.ndefMessage non significa ancora che il badge sia valido. Il codice deve verificare:
- che il messaggio esista;
- che il record sia di un tipo supportato;
- che il payload sia decodificabile;
- che il JSON abbia una proprietà
tkotoken; - che il token abbia la struttura attesa;
- che la validità finale venga controllata dal backend.
Decodifica manuale del servizio
Nel servizio del progetto il record viene interpretato in base al codice del tipo NDEF:
const decodeNdefRecord = (record) => {
const typeCode = record.type[0];
if (typeCode === 84) {
const payload = record.payload;
const statusByte = payload[0];
const languageLength = statusByte & 0x3f;
const textBytes = payload.slice(1 + languageLength);
const text = String.fromCharCode(...textBytes);
return { type: 'Text', value: text };
}
if (typeCode === 85) {
const payload = record.payload;
const identifierCode = payload[0];
const uriPrefixes = [
'',
'http://www.',
'https://www.',
'http://',
'https://',
];
const prefix = uriPrefixes[identifierCode] || '';
const uri = prefix + String.fromCharCode(...payload.slice(1));
return { type: 'URI', value: uri };
}
return { type: 'Unknown', value: record };
};
Il progetto considera i record URI non utili all’estrazione del token e continua la scansione dei record successivi. Per un record Text prova invece a fare il parse del JSON:
import { jwtDecode } from 'jwt-decode';
const decodeNdefMessage = (tag, sendErrorMessage) => {
if (!tag?.ndefMessage?.length) {
return undefined;
}
for (const record of tag.ndefMessage) {
const decoded = decodeNdefRecord(record);
if (decoded.type === 'URI') {
continue;
}
if (decoded.type !== 'Text') {
sendErrorMessage('Tag non valido');
return undefined;
}
try {
const jsonData = JSON.parse(decoded.value);
const token = jsonData?.tk || jsonData?.token;
if (!token) {
sendErrorMessage('Token non presente');
return undefined;
}
const payload = jwtDecode(token);
const now = Math.floor(Date.now() / 1000);
if (!payload.exp || now >= payload.exp) {
sendErrorMessage('Token scaduto');
return undefined;
}
return {
token,
tagId: tag.id,
};
} catch (error) {
console.warn('Errore nel parsing del JSON:', error);
sendErrorMessage('Errore durante il parsing JSON');
return undefined;
}
}
return undefined;
};
Una nota sull’encoding
La conversione con String.fromCharCode(...) è sufficiente per payload ASCII semplici, come un JSON contenente un token. Non è una soluzione generale per tutti i testi Unicode.
Quando si lavora direttamente nel componente o nell’hook, la libreria espone anche gli helper Ndef, per esempio Ndef.isType e Ndef.text.decodePayload. Questi helper rendono più esplicita la verifica del tipo di record e riducono la quantità di codice binario scritto a mano.
Usare jwtDecode non significa validare il token
jwtDecode legge il payload di un JWT e permette di controllare localmente la scadenza. Non verifica la firma crittografica e non dimostra che il token sia stato emesso dal backend.
Il controllo autorevole deve quindi rimanere sul server. Il client può usare exp per evitare una chiamata sicuramente inutile, ma non deve trattare il risultato di jwtDecode come una validazione di sicurezza completa.
L’hook useNfc
L’hook useNfc incapsula lo stato della scansione e la gestione dell’inizializzazione del modulo.
Nel flusso originale del progetto gestisce anche un caso particolare: se la schermata viene aperta con jwt e tagId provenienti da un deep link, la timbratura può essere inviata senza una nuova scansione fisica. Questo ramo deve essere mantenuto separato dalla lettura NFC vera e propria, perché i due flussi hanno origini e rischi diversi.
Una versione coerente con l’implementazione è:
import { useEffect, useState } from 'react';
import NfcManager, {
NfcEvents,
NfcTech,
} from 'react-native-nfc-manager';
import { useAlert } from '../context/AlertModalProvider';
export default function useNfc() {
const { showAlert } = useAlert();
const [hasNfc, setHasNfc] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [buttonsDisabled, setButtonsDisabled] = useState(false);
useEffect(() => {
let mounted = true;
async function initNfc() {
try {
const supported = await NfcManager.isSupported();
if (mounted) {
setHasNfc(supported);
}
if (!supported) {
showAlert(
'NFC non supportato',
'Il tuo dispositivo non supporta NFC',
);
return;
}
await NfcManager.start();
NfcManager.setEventListener(
NfcEvents.SessionClosed,
() => {
if (mounted) {
setButtonsDisabled(false);
setIsLoading(false);
}
},
);
} catch (error) {
showAlert('Errore NfcManager', String(error));
}
}
initNfc();
return () => {
mounted = false;
NfcManager.setEventListener(NfcEvents.SessionClosed, null);
NfcManager.cancelTechnologyRequest().catch(() => {});
};
}, [showAlert]);
const readNdefTag = async (
direction,
handleTag,
submitBadge,
deepLinkJwt,
deepLinkTagId,
clearDeepLinkParams,
) => {
if (!hasNfc) {
showAlert(
'Errore',
'NFC non è supportato su questo dispositivo.',
);
return;
}
if (deepLinkJwt && deepLinkTagId) {
setButtonsDisabled(true);
setIsLoading(true);
try {
await submitBadge(
deepLinkJwt,
deepLinkTagId,
direction,
true,
);
} finally {
setIsLoading(false);
setButtonsDisabled(false);
clearDeepLinkParams?.();
}
return;
}
if (deepLinkJwt || deepLinkTagId) {
showAlert(
'Errore',
'Dati deeplink incompleti. JWT e TagID devono essere entrambi presenti.',
);
return;
}
setButtonsDisabled(true);
setIsLoading(true);
try {
await NfcManager.cancelTechnologyRequest().catch(() => {});
await NfcManager.requestTechnology(NfcTech.Ndef);
const tag = await NfcManager.getTag();
await handleTag(tag, direction);
} catch (error) {
console.warn('Errore readNdefTag:', error);
} finally {
await NfcManager.cancelTechnologyRequest().catch(() => {});
setIsLoading(false);
setButtonsDisabled(false);
}
};
return {
hasNfc,
isLoading,
buttonsDisabled,
readNdefTag,
};
}
Perché lo stato è necessario
Senza buttonsDisabled, l’utente può premere più volte Entrata o Uscita e creare richieste concorrenti. Senza isLoading, l’interfaccia non comunica che il sistema sta aspettando il badge o la risposta API.
Uno stato minimo può essere:
null inizializzazione non conclusa false NFC non supportato true pronto alla lettura loading sessione o invio in corso error ultima operazione fallita
Nel codice reale alcuni di questi stati possono essere rappresentati da più booleani, ma una state machine esplicita diventa preferibile quando il flusso cresce.
Un miglioramento possibile: usare un oggetto options
La firma con molti argomenti posizionali è difficile da mantenere:
readNdefTag( direction, handleTag, submitBadge, deepLinkJwt, deepLinkTagId, clearDeepLinkParams, );
Una forma più leggibile è:
readNdefTag({
direction,
handleTag,
submitBadge,
deepLinkJwt,
deepLinkTagId,
clearDeepLinkParams,
});
Il comportamento non cambia, ma il chiamante rende visibile il significato di ogni dipendenza e riduce gli errori quando vengono aggiunti nuovi parametri.
L’hook useHandleNfcTag
useHandleNfcTag riceve il tag già letto e si occupa di due operazioni:
- estrarre il token dal messaggio NDEF;
- inviare la timbratura con direzione, data e identificativo del tag.
Nel progetto la funzione usa Ndef per distinguere i record URI dai record Text. I record URI vengono ignorati perché possono rappresentare un deep link o un dato di navigazione, mentre il token applicativo viene cercato nei record Text.
Il cuore dell’hook è questo:
import { Ndef } from 'react-native-nfc-manager';
import { sendBadge } from '../../../../services/ApiService';
export const useHandleNfcTag = (
setModalConfig,
setModalVisible,
) => {
const showError = (title, subtitle) => {
setModalConfig({
title,
subtitle,
buttonText: 'Ok',
onClose: () => {
setModalConfig(null);
setModalVisible(false);
},
});
setModalVisible(true);
};
const submitBadge = async (
token,
tagId,
direction,
fromDeepLink = false,
) => {
const type = direction === 'entrata' ? 0 : 1;
const timestamp = new Date().toISOString();
try {
const response = await sendBadge(
token,
type,
timestamp,
tagId,
);
if (response?.success) {
return {
success: true,
direction,
timestamp,
fromDeepLink,
};
}
showError(
'Errore nella timbratura',
'Non è stato possibile registrare la timbratura.',
);
return { success: false };
} catch (error) {
console.warn('Errore invio timbratura:', error);
showError(
'Errore nella timbratura',
'Controlla la connessione e riprova.',
);
return { success: false };
}
};
const handleTag = async (tag, direction) => {
try {
if (!tag?.ndefMessage?.length) {
showError(
'Tag errato',
'Il tag non contiene un messaggio NDEF.',
);
return;
}
let token;
for (const record of tag.ndefMessage) {
if (Ndef.isType(record, Ndef.TNF_WELL_KNOWN, Ndef.RTD_URI)) {
continue;
}
if (!Ndef.isType(record, Ndef.TNF_WELL_KNOWN, Ndef.RTD_TEXT)) {
continue;
}
const text = Ndef.text.decodePayload(record.payload);
try {
const parsed = JSON.parse(text);
token = parsed?.tk || parsed?.token;
if (token) {
break;
}
} catch {
// Un record Text non JSON non è il payload cercato.
}
}
if (!token) {
showError(
'Tag non valido',
'Il tag non contiene i dati necessari per timbrare.',
);
return;
}
// Android può restituire un ID con separatori come 04:AB:12.
const tagId = tag.id?.replace(/:/g, '') || tag.id;
await submitBadge(token, tagId, direction);
} catch (error) {
console.warn('Errore nella lettura del tag NFC:', error);
showError(
'Errore NFC',
'Non è stato possibile leggere il badge.',
);
}
};
return { handleTag, submitBadge };
};
Il codice mostra anche un limite da non nascondere: tag.id può essere undefined, soprattutto nel flusso NDEF iOS. Il valore deve quindi essere trattato come opzionale. Se l’API considera tagId obbligatorio, il contratto deve definire cosa accade quando la piattaforma non lo espone.
Una strategia più portabile è usare nel payload NDEF un identificativo applicativo non segreto, oppure un token breve associato lato server a un badge. In questo modo il flusso principale non dipende dall’UID hardware.
Inviare la timbratura via API
Una volta estratti token e tagId, l’hook genera la direzione e il timestamp:
const type = direction === 'entrata' ? 0 : 1; const timestamp = new Date().toISOString(); await sendBadge(token, type, timestamp, tagId);
Nel contratto applicativo di myGenius la richiesta verso il backend è modellata così:
export const appNfcContracts = {
onSendBadge: async (nfcToken, type, timestamp, tagId) => {
const body = {
token: nfcToken,
type,
timestamp,
};
if (tagId) {
body.tagId = tagId;
}
return apiClient.post('/badge', body);
},
};
Il payload risultante è simile a:
{
"token": "eyJhbGciOi...",
"type": 0,
"timestamp": "2026-08-07T08:15:00.000Z",
"tagId": "04AB12CD34EF"
}
Il client deve inviare il timestamp in un formato non ambiguo, preferibilmente UTC tramite toISOString(). La conversione in orario locale è una responsabilità della UI o del backend, non del dato grezzo della timbratura.
Non confondere il risultato HTTP con il risultato applicativo
Una risposta HTTP positiva non significa sempre che la timbratura sia stata registrata. Il backend può restituire un JSON con success: false per un token scaduto, un badge non associato, una direzione non valida o un evento duplicato.
Il client deve quindi controllare entrambi:
const response = await sendBadge(
token,
type,
timestamp,
tagId,
);
if (!response?.success) {
// Mostrare un errore applicativo, non un successo.
}
Il server deve essere l’autorità finale
Il backend dovrebbe verificare almeno:
- firma e scadenza del token;
- associazione tra utente autenticato e operazione;
- validità del badge o del tag;
- tipo di timbratura;
- timestamp e tolleranza temporale;
- eventuali duplicati o replay;
- autorizzazione dell’utente alla timbratura NFC.
Il client può migliorare l’esperienza, ma non può rendere affidabile da solo un evento che il server non valida.
Gestire cleanup, errori e stati intermedi
La parte più importante di una feature NFC non è soltanto leggere il primo tag. È comportarsi correttamente anche quando il flusso viene interrotto.
Usare sempre try/catch/finally
La struttura consigliata è:
let requestStarted = false;
try {
await NfcManager.start();
await NfcManager.requestTechnology(NfcTech.Ndef);
requestStarted = true;
const tag = await NfcManager.getTag();
// Parsing e invio API.
} catch (error) {
// Errore hardware, annullamento o API.
} finally {
if (requestStarted) {
await NfcManager.cancelTechnologyRequest().catch(() => {});
}
}
Nel caso reale si può chiamare il cancel anche quando la richiesta non è stata completata, gestendo l’errore. L’importante è che il cleanup sia idempotente dal punto di vista del flusso applicativo e non blocchi la UI.
Non mostrare lo stesso errore più volte
Il servizio può notificare un errore di parsing, mentre l’hook può aggiungere un secondo errore generico quando riceve undefined. È preferibile definire un solo proprietario del messaggio finale:
servizio → restituisce risultato o errore tecnico hook → traduce il risultato in stato UI screen → mostra modale/banner
In questo modo un tag senza token non produce tre alert consecutivi.
Distinguere gli errori principali
Gli errori hanno cause diverse e richiedono messaggi diversi:
- NFC non supportato: il dispositivo non ha l’hardware necessario;
- NFC disabilitato: l’hardware esiste ma è spento nelle impostazioni;
- sessione annullata: l’utente ha chiuso il lettore;
- tag non NDEF: il tag è stato rilevato ma non espone il formato richiesto;
- record non supportato: il messaggio esiste ma non contiene un record gestibile;
- JSON non valido: il payload Text non rispetta il formato concordato;
- token assente o scaduto: il badge non contiene una credenziale utilizzabile;
- API non raggiungibile: la lettura è riuscita, ma l’invio è fallito;
- errore applicativo: il server ha rifiutato la timbratura.
Disabilitare i pulsanti durante l’operazione
L’app dovrebbe impedire nuove scansioni mentre una richiesta è in corso. Oltre a buttonsDisabled, è utile mantenere un riferimento all’operazione attiva se la schermata può essere smontata durante l’attesa.
Quando la schermata viene chiusa, bisogna:
- annullare la richiesta NFC;
- rimuovere gli event listener;
- evitare di aggiornare lo stato dopo lo smontaggio;
- ripristinare i pulsanti alla successiva apertura.
Differenze pratiche tra iOS e Android
La libreria offre una API JavaScript comune, ma i sistemi operativi non espongono esattamente le stesse tecnologie.
| Aspetto | Android | iOS |
|---|---|---|
| API nativa | Android NFC Framework | Core NFC |
| Lettura NDEF | Supportata con NfcTech.Ndef |
Supportata con NfcTech.Ndef |
tag.id |
Normalmente disponibile nel tag rilevato | Non garantito nella sessione NDEF |
| Dialog di sistema | Dipende dal flusso e dalla versione Android | Sessione Core NFC con interfaccia di sistema |
| Tag non NDEF | Possibile usare tecnologie Android specifiche | Richiede tecnologie Core NFC compatibili |
| MIFARE Classic | Supporto dipendente dal dispositivo/libreria | Non disponibile come stesso flusso Core NFC |
| NFC disabilitato | Controllabile con isEnabled() |
La capability e il dispositivo devono supportare Core NFC |
La tabella è una guida progettuale, non una garanzia per ogni modello di tag. Il supporto dipende dalla tecnologia fisica, dal sistema operativo, dalla versione della libreria e dalle API native disponibili.
Quando usare Ndef
Usare NfcTech.Ndef quando il requisito è leggere un messaggio applicativo standardizzato:
await NfcManager.requestTechnology(NfcTech.Ndef); const tag = await NfcManager.getTag();
È la scelta più semplice per un badge che contiene testo JSON, URL o un altro record NDEF.
Quando serve una tecnologia specifica
Se il requisito è eseguire comandi a basso livello, leggere memoria grezza o ottenere informazioni che non fanno parte del messaggio NDEF, può servire una tecnologia specifica, come NfcA, IsoDep, MifareIOS o Iso15693IOS, in base al dispositivo e al tag.
Questo cambia il contratto del codice. Non è sufficiente sostituire una costante e aspettarsi lo stesso oggetto:
const technology = Platform.OS === 'ios' ? NfcTech.MifareIOS : NfcTech.NfcA; await NfcManager.requestTechnology(technology);
Il flusso va testato separatamente su entrambe le piattaforme. In particolare, una sessione a basso livello può consentire di leggere l’UID ma non restituire automaticamente lo stesso ndefMessage ottenuto dalla sessione NDEF.
Non usare una tecnologia solo per ottenere un UID
Se il requisito nasce dal desiderio di identificare il badge, prima di passare a un reader a basso livello bisogna chiedersi se l’UID sia davvero necessario. Una credenziale applicativa contenuta in NDEF, con validazione server e scadenza breve, è spesso più semplice da rendere compatibile tra iOS e Android.
Sicurezza: il token nel tag non è una firma
Un badge NFC aziendale è una credenziale di prossimità. Non è una prova biometrica e non prova da solo chi ha avvicinato il badge al telefono.
Se il tag contiene un JWT, bisogna considerare alcuni rischi:
- il contenuto NDEF può essere letto da un altro dispositivo compatibile;
- il token può essere copiato se non è protetto da scadenza e controlli server;
- il badge può essere prestato;
- l’UID non rende automaticamente il payload non clonabile;
- un controllo locale con
jwtDecodenon verifica la firma.
Per ridurre i rischi:
- usare token brevi o monouso quando il caso d’uso lo consente;
- validare firma,
exp, audience e associazioni lato server; - rilevare replay e richieste duplicate;
- non inserire dati personali non necessari nel tag;
- non stampare token o payload completi nei log;
- trasmettere la timbratura solo su connessione protetta;
- definire cosa accade se il tag è perso, sostituito o revocato;
- mantenere separati il token di sessione dell’app e la credenziale del badge.
Un’architettura più prudente può scrivere nel tag un codice applicativo a breve durata o un identificativo opaco. Il server usa quel valore per recuperare o validare il badge senza trasformare il contenuto del tag in un token permanente.
La scelta deve essere coerente anche con privacy e gestione delle presenze. Il dato NFC dovrebbe essere limitato alla finalità dichiarata e non diventare un pretesto per raccogliere informazioni ulteriori sulla posizione o sui movimenti del lavoratore.
Come testare una feature NFC
La lettura NFC richiede test hardware e test applicativi. Una suite minima dovrebbe includere i seguenti casi.
Dispositivo e sessione
- dispositivo Android con NFC supportato;
- dispositivo Android con NFC disabilitato;
- dispositivo senza NFC;
- iPhone fisico compatibile con Core NFC;
- utente che annulla la sessione iOS;
- utente che allontana il tag prima della lettura;
- due pressioni consecutive sul pulsante;
- nuova scansione subito dopo un errore;
- uscita dalla schermata durante
requestTechnology().
Contenuto del tag
- tag NDEF con un record Text valido;
- tag con più record, incluso un URI prima del Text;
- tag con solo URI;
- tag senza
ndefMessage; - record Text con JSON malformato;
- JSON senza
tke senzatoken; - token JWT scaduto;
- token con formato non decodificabile;
- tag con
idvalorizzato; - tag con
idassente su iOS.
Backend
- API raggiungibile e risposta
success: true; - API raggiungibile con risposta applicativa negativa;
- timeout di rete;
- token rifiutato dal server;
- badge non associato all’utente;
- richiesta duplicata;
- timestamp inviato in UTC;
- assenza di
tagIdgestita secondo contratto.
Test automatici
Le API native possono essere isolate con un mock di NfcManager. I test dell’hook dovrebbero verificare almeno:
isSupported() = false → alert e nessuna scansione requestTechnology() fallisce → stato ripristinato getTag() restituisce tag valido → handleTag chiamato getTag() restituisce tag senza NDEF → errore UI payload valido → sendBadge chiamata con tipo e timestamp sendBadge fallisce → errore UI e pulsanti riattivati finally → cancelTechnologyRequest sempre chiamato
I test automatici non sostituiscono però una prova su dispositivi reali. Il comportamento della sessione di lettura e la disponibilità dell’UID devono essere verificati con i modelli di telefono e i badge realmente supportati dal prodotto.
Domande frequenti
NfcManager.start() è sufficiente per leggere un tag?
No. start() inizializza il modulo. Per una lettura mirata bisogna anche richiedere la tecnologia, per esempio requestTechnology(NfcTech.Ndef), attendere getTag() e chiudere la richiesta nel finally.
Posso leggere l’UID con NfcTech.Ndef?
Su Android tag.id è normalmente disponibile. Su iOS l’UID non è garantito nella sessione NDEF. La disponibilità dipende dal tipo di reader session e dalla tecnologia usata. Non bisogna progettare il contratto cross-platform assumendo che l’UID sia sempre presente.
ndefMessage[0].id è l’UID del badge?
No. È l’identificativo del record NDEF, quando presente. L’identificativo hardware del tag viene normalmente esposto separatamente, per esempio in tag.id su Android.
Posso usare un tag con un URL invece di un JSON?
Sì, ma l’app deve definire il contratto. Il parser del progetto ignora i record URI quando cerca il token in un record Text. Se il badge deve contenere un URL, bisogna stabilire se l’URL sia solo un deep link, se contenga un codice temporaneo o se debba essere risolto dal backend.
Perché la seconda scansione non parte?
Le cause frequenti sono una richiesta precedente non annullata, un listener rimasto attivo, una sessione iOS non chiusa o una nuova chiamata avviata prima del completamento del cleanup. Verificare che cancelTechnologyRequest() venga eseguito sempre e che i pulsanti siano bloccati durante l’operazione.
Serve un permesso runtime come per la fotocamera?
Il flusso dei permessi NFC non è identico a quello della fotocamera. Android richiede la dichiarazione del permesso NFC nel manifest e il controllo dello stato dell’hardware. iOS richiede capability, entitlement e NFCReaderUsageDescription. Le schermate e le richieste possono variare in base alla piattaforma e alla versione.
Posso considerare valido un JWT dopo jwtDecode()?
No. jwtDecode() decodifica il payload, ma non verifica la firma. Il backend deve validare la credenziale e decidere se accettare o rifiutare la timbratura.
È meglio inviare l’UID o un token nel payload NDEF?
Dipende dal caso d’uso. L’UID può essere utile per associare un supporto fisico, ma non è sempre disponibile allo stesso modo su iOS e Android e non è segreto. Un token applicativo breve e validato dal server è spesso più flessibile. In molti casi è utile usare entrambi, rendendo però tagId opzionale dove la piattaforma non lo espone.
Posso testare l’NFC con un simulatore?
No, non per verificare il comportamento reale della radio, della sessione e del tag. Servono dispositivi fisici e badge reali, preferibilmente con una matrice di test per piattaforma e tecnologia.
Conclusione
Implementare la lettura NFC in React Native significa collegare correttamente quattro livelli:
- configurazione nativa iOS e Android;
- gestione della sessione tramite
react-native-nfc-manager; - parsing controllato del messaggio NDEF e dell’eventuale
tag.id; - invio della timbratura a un backend che resta l’autorità finale.
Il flusso di myGenius rende visibile questa separazione: nfcService.js gestisce il lettore e può comunicare con la WebView, useNfc coordina stato e sessione, useHandleNfcTag interpreta il badge e il contratto onSendBadge invia l’evento a /badge.
La regola più importante è non trattare NDEF e UID come sinonimi. NDEF contiene dati applicativi; l’UID identifica, quando disponibile, il supporto esposto dalla piattaforma. Usare il dato giusto, chiudere sempre la sessione e delegare la validazione di sicurezza al server rende la feature più prevedibile e più facile da mantenere.
Se il progetto prevede anche deep link, WebView o una schermata NFC nativa, conviene leggere insieme a questa guida l’articolo su deep linking pratico in React Native per iOS e Android, perché il passaggio di jwt e tagId attraverso un URL introduce un flusso diverso dalla scansione fisica.




