Feedback utente nella timbratura NFC: suoni, conferme e ultime timbrature

nfc-feedback

12 minuti di lettura

Una timbratura NFC non termina davvero quando il backend restituisce una risposta. Termina nel momento in cui la persona capisce, senza alcuna ambiguità, che cosa è appena successo.

Il telefono può aver letto correttamente il badge, ma l’utente deve comunque sapere se la scansione è iniziata, se il tag è stato riconosciuto, se la timbratura è stata effettivamente registrata, se l’operazione è fallita e può essere ripetuta, e se l’evento appena registrato è ora visibile nello storico.

Questo insieme di segnali è ciò che chiamiamo feedback utente. In una funzione usata ogni giorno, spesso in pochi secondi appena, il feedback è parte integrante dell’affidabilità percepita dall’app — non un dettaglio estetico aggiunto alla fine.

In questo articolo vediamo come organizzarlo in React Native prendendo come riferimento il flusso NFC di myGenius: ActionButtons avvia l’azione, useHandleNfcTag coordina la risposta, soundPlayer riproduce i suoni, NfcInfoModal spiega il gesto iniziale, e TimestampsList mostra gli eventi recenti.


Perché il feedback è parte del flusso NFC

La lettura NFC è, per sua natura, un’operazione asincrona. Tra il tap sul pulsante e la risposta del server possono trascorrere alcuni istanti. Se l’interfaccia non cambia in quel momento, l’utente può pensare che il comando non sia stato ricevuto, e premere di nuovo — un gesto comprensibile, ma potenzialmente dannoso.

Il risultato può essere una doppia richiesta, una seconda sessione NFC avviata inutilmente, o semplicemente una perdita di fiducia nell’app da parte di chi la usa ogni giorno.

Un flusso leggibile separa almeno questi stati: pronto, lettura NFC in corso, invio della timbratura, successo oppure errore, e infine aggiornamento delle ultime timbrature. Ogni stato dovrebbe avere una rappresentazione coerente: nello stato pronto servono pulsanti attivi e un’istruzione comprensibile; nella lettura in corso servono pulsanti disabilitati e un indicatore o un messaggio chiaro; nell’invio in corso serve un loader, soprattutto per le operazioni avviate da deep link; nel successo servono un suono, una conferma visiva e l’orario registrato; e nell’errore serve un suono distinto, un messaggio comprensibile, e la possibilità concreta di riprovare.

Il feedback, però, non deve mai diventare rumore. Un suono troppo forte, un’animazione che dura troppo a lungo, o un modale che blocca la schermata per un errore facilmente recuperabile, finiscono per peggiorare l’esperienza invece di migliorarla.


Progettare gli stati della timbratura

Prima di scrivere i componenti è utile definire con chiarezza chi è responsabile di ogni singolo passaggio. La schermata NFC compone data, lista, pulsanti e modali. ActionButtons presenta le azioni, e impedisce il tap quando l’NFC non è disponibile o l’operazione è già in corso. useNfc gestisce il supporto del dispositivo e la scansione vera e propria. useHandleNfcTag interpreta il tag, chiama il backend, e prepara il risultato. soundPlayer incapsula la libreria audio. E TimestampsList visualizza i dati già caricati, senza conoscere in alcun modo la logica di rete sottostante.

Questa separazione è importante da rispettare: un componente visuale non dovrebbe mai decidere se una risposta HTTP è valida, e un player audio non dovrebbe conoscere la navigazione dell’app.

Il progetto di riferimento usa direttamente response.success, e costruisce il messaggio a partire da direction e timestamp. Il principio resta comunque sempre lo stesso: il livello che conosce il risultato prepara un dato chiaro, mentre il livello presentazionale decide semplicemente come mostrarlo.


I pulsanti Entrata e Uscita con ActionButtons

ActionButtons espone due azioni simmetriche tra loro: USCITA e ENTRATA. Entrambe richiamano readNdefTag, passando la direzione e le callback necessarie per gestire il tag.

Una versione ridotta del componente è questa:

import { View, Pressable, Text } from "react-native";
import LinearGradient from "react-native-linear-gradient";
import Entypo from "react-native-vector-icons/Entypo";

export default function ActionButtons({
  hasNfc,
  buttonsDisabled,
  readNdefTag,
  handleTag,
  submitBadge,
  jwt,
  tagId,
  clearDeepLinkParams,
}) {
  const Button = ({ label, direction, colors, icon }) => (
    <Pressable
      onPress={() => readNdefTag(
        direction,
        handleTag,
        submitBadge,
        jwt,
        tagId,
        clearDeepLinkParams
      )}
      disabled={!hasNfc || buttonsDisabled}
    >
      {({ pressed }) => (
        <LinearGradient
          colors={colors}
          style={{ opacity: pressed ? 0.8 : 1 }}
        >
          {icon === "left" && <Entypo name="arrow-left" size={20} color="#fff" />}
          <Text>{label}</Text>
          {icon === "right" && <Entypo name="arrow-right" size={20} color="#fff" />}
        </LinearGradient>
      )}
    </Pressable>
  );

  return (
    <View>
      <Button label="USCITA" direction="uscita" colors={["#5a4ca7", "#a21fab"]} icon="left" />
      <Button label="ENTRATA" direction="entrata" colors={["#a21fab", "#5a4ca7"]} icon="right" />
    </View>
  );
}

Ci sono tre dettagli che meritano attenzione in questo componente. Il primo è che disabled={!hasNfc || buttonsDisabled} evita di proporre un’azione che il dispositivo semplicemente non può eseguire. Il secondo è che l’opacità comunica visivamente lo stato disabilitato, ma non sostituisce affatto la proprietà disabled: sono due segnali complementari, non intercambiabili. Il terzo è che la direzione è un dato esplicito, passato direttamente: non conviene mai dedurla dal testo mostrato sul pulsante, perché quel testo può cambiare per ragioni di localizzazione o restyling senza che la logica se ne accorga.

La variabile buttonsDisabled dovrebbe restare attiva durante l’intera scansione e l’invio successivo. In questo modo il feedback non si limita a raccontare che cosa è successo dopo: previene anche, in modo concreto, le azioni duplicate.


Riprodurre suoni con soundPlayer

Nel progetto l’audio è incapsulato in soundPlayer.js, basato su react-native-sound:

import Sound from "react-native-sound";

Sound.setCategory("Playback");

export const playSound = (soundFile) => {
  const sound = new Sound(soundFile, Sound.MAIN_BUNDLE, (error) => {
    if (error) return;

    sound.setVolume(1);
    sound.play(() => sound.release());
  });
};

L’helper riceve il nome del file, e lo cerca nel bundle principale. Il callback di completamento libera l’istanza, evitando di mantenere inutilmente risorse audio in memoria dopo la riproduzione.

Il codice di gestione della timbratura usa file distinti per momenti distinti:

playSound("nfc_beep.mp3");

// Dopo una risposta positiva
playSound("nfc_success.mp3");

// Dopo un errore
playSound("nfc_error.mp3");

Su iOS, i file .mp3 devono essere inclusi correttamente nel target e nel bundle dell’app. L’importazione JavaScript dell’helper, da sola, non garantisce affatto che una risorsa nativa sia stata effettivamente aggiunta a Xcode. Prima di pubblicare una funzione audio vale la pena verificare che i file siano presenti nel progetto Xcode e nella sezione Copy Bundle Resources, che il nome del file coincida esattamente — maiuscole e minuscole comprese —, che la categoria audio sia compatibile con il comportamento desiderato, che il suono rispetti modalità silenziosa, volume e impostazioni del dispositivo, e che la riproduzione non blocchi mai il thread dell’interfaccia. Su Android bisogna verificare il percorso del resource bundle e il comportamento sui dispositivi reali: un test in simulatore non è sufficiente per valutare volume, latenza e interazione con altre sorgenti audio.

Non è necessario associare un suono a ogni singolo evento interno dell’app. Una scelta equilibrata prevede un beep breve quando il tag viene rilevato, un suono positivo soltanto dopo la conferma del backend, e un suono di errore quando l’operazione non può essere completata. La conferma finale, in ogni caso, deve arrivare sempre dopo la risposta del server, mai subito dopo la lettura del badge: leggere il tag significa soltanto aver acquisito i dati, non significa affatto che la timbratura sia stata registrata.


Conferma di successo ed errore

L’hook useHandleNfcTag separa nettamente il percorso positivo da quello negativo. Dopo sendBadge, controlla response.success; in caso contrario, prepara un errore leggibile per l’utente.

La conferma positiva include direzione e orario:

const showSuccess = (direction, timestamp) => {
  const formattedTime = new Date(timestamp).toLocaleTimeString();
  const subtitle = direction === "entrata"
    ? `Entrata alle ore ${formattedTime}nBuon lavoro!`
    : `Uscita alle ore ${formattedTime}nA presto!`;

  setModalConfig({
    title: "Timbratura registrata!",
    subtitle,
    imageSource: direction === "entrata"
      ? niceDayAtWorkImage
      : soLongImage,
    buttonText: "Ok",
  });
  setModalVisible(true);
  playSound("nfc_success.mp3");
};

Per l’errore, il messaggio non dovrebbe mai esporre dettagli tecnici inutili all’utente finale:

const showError = (title, subtitle) => {
  setModalConfig({
    title,
    subtitle,
    imageSource: esclamationMarkImage,
    buttonText: "Ok",
  });
  setModalVisible(true);
  playSound("nfc_error.mp3");
};

Messaggi diversi aiutano a distinguere i vari casi possibili: “Tag errato” quando il tag non contiene un record NDEF valido, “Tag non valido” quando il contenuto non include il token richiesto, “Errore nella timbratura” quando il server non ha registrato l’evento, ed “Errore di rete” quando il dispositivo non riesce a completare la richiesta.

Il testo deve sempre dire anche che cosa fare dopo: avvicinare di nuovo il tag, controllare la connessione, o contattare il responsabile. E non bisogna mai dichiarare “timbratura registrata” prima di aver ricevuto una risposta effettivamente positiva dal server.

Nel flusso avviato da deep link, il codice mostra subito un loader, e lo chiude non appena arriva la risposta. Nella scansione manuale, invece, il risultato viene mostrato dopo un breve ritardo. Questo comportamento va valutato con attenzione: un setTimeout può rendere la risposta meno immediata, e lasciare callback attive dopo lo smontaggio della schermata. Se il ritardo non è indispensabile, è preferibile mostrare il risultato non appena il backend risponde, oppure gestire il timer con un cleanup adeguato.


Istruzioni alla prima apertura con NfcInfoModal

Un utente che apre la schermata per la prima volta non conosce necessariamente il gesto corretto da compiere. NfcInfoModal risolve questo problema con una spiegazione breve, un’immagine animata, e un pulsante di conferma.

Il componente riceve tre props:

<NfcInfoModal
  visible={nfcInfoModalVisible}
  setVisible={setNfcInfoModalVisible}
  setCookie={setCookie}
/>

Quando l’utente chiude il modale, il componente nasconde la finestra e salva il cookie:

const handleClose = () => {
  setVisible(false);
  setCookie();
};

Il cookie, o un valore equivalente in uno storage locale, deve servire esclusivamente a ricordare che le istruzioni sono già state mostrate una volta. Non deve mai essere usato per autorizzare la timbratura, né per conservare token sensibili.

Una buona modale introduttiva dovrebbe spiegare dove si trova il tag “Timbra qui”, come attivare Entrata o Uscita, dove avvicinare il telefono, e che il risultato sarà confermato dall’app. L’immagine animata è utile soltanto se resta leggibile e non rallenta l’apertura della schermata. È opportuno fornire comunque un testo equivalente, perché non tutti gli utenti percepiscono un’animazione allo stesso modo.


Mostrare le ultime timbrature

La schermata NFC usa useTimestamps(2) per caricare un intervallo breve, e passa i dati a TimestampSection, che li visualizza dentro uno ScrollView con RefreshControl.

const { timestamps, refreshing, onRefresh } = useTimestamps(2);

<TimestampSection
  timestamps={timestamps}
  refreshing={refreshing}
  onRefresh={onRefresh}
/>

TimestampSection mantiene il comportamento di refresh fuori dalla lista:

<ScrollView
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
  }
>
  <TimestampsList data={timestamps} badge={false} order="olderFirst" />
</ScrollView>

In questo modo la schermata principale mostra le ultime registrazioni, senza confonderle con l’archivio completo dello storico.

Per ogni giorno, TimestampsList ordina gli eventi per orario, separa ingressi e uscite, pareggia le due colonne usando un trattino quando manca una coppia, mostra mese, giorno e orari, distingue giorni lavorativi, sabati e domeniche, e può mostrare l’intestazione mese/anno quando badge è attivo. L’ordinamento è esplicito:

const entries = order === "recentFirst"
  ? Object.entries(data).reverse()
  : Object.entries(data);

Per la schermata NFC si usa olderFirst, mentre nell’archivio si può usare recentFirst. La scelta va sempre comunicata all’utente: nella schermata operativa può essere utile seguire il flusso della giornata dall’inizio, nello storico può essere più utile vedere subito l’evento più recente.

Dopo una timbratura riuscita, il caricamento della lista dovrebbe essere aggiornato. Se il contratto applicativo non restituisce direttamente il nuovo evento, si può richiamare fetchTimestampsData() dopo la conferma, evitando però richieste duplicate mentre il primo caricamento è ancora in corso.



Errori comuni

Confermare troppo presto è probabilmente l’errore più insidioso: la lettura del tag non equivale affatto alla registrazione sul server. Il feedback positivo va attivato soltanto dopo la risposta valida del contratto, mai prima.

Lasciare attivi i pulsanti è un altro errore frequente: due tap ravvicinati possono avviare due letture o due invii distinti. I pulsanti vanno disabilitati durante l’operazione, e riattivati in un punto ben definito del flusso.

Usare lo stesso suono per tutto rende il feedback poco utile: un unico beep non distingue rilevamento, successo ed errore. Pochi suoni, ma semanticamente distinti tra loro, sono decisamente più utili all’utente.

Confondere dati recenti e archivio penalizza le prestazioni senza motivo: la schermata NFC deve restare rapida. Non è necessario caricare tutto lo storico soltanto per mostrare gli ultimi eventi — meglio usare un intervallo limitato, e affidare i filtri temporali a una schermata separata.

Nascondere l’errore senza spiegazione lascia l’utente senza strumenti: “Operazione fallita” non dice affatto come procedere. Il testo deve indicare il prossimo passo concreto, senza mostrare stack trace o dettagli interni all’app.

Dimenticare il cleanup, infine, lascia comportamenti residui in giro: sessioni NFC, timer e player audio devono essere chiusi o rilasciati quando non servono più, così da evitare effetti indesiderati quando l’utente cambia semplicemente schermata.


Domande frequenti

Il suono di successo parte quando il tag viene letto?

No. Il tag è stato letto, ma la timbratura è confermata soltanto dalla risposta positiva del backend. Il suono di successo deve arrivare dopo, mai prima.

Posso usare un banner invece di una modale?

Sì, se il messaggio non richiede un’azione immediata da parte dell’utente. Per un errore che deve essere compreso, o per le istruzioni iniziali, una modale può essere decisamente più chiara.

Perché mostrare le ultime timbrature nella schermata NFC?

Permette di verificare rapidamente che l’evento sia comparso, e riduce l’incertezza subito dopo il tap. Lo storico completo dovrebbe comunque restare in una schermata dedicata a parte.

I file MP3 funzionano automaticamente su iOS?

No. Devono essere inclusi correttamente nel target, e testati su un dispositivo reale, considerando bundle, volume, modalità silenziosa e categoria audio.

Un badge NFC identifica con certezza la persona?

No. Il badge è una credenziale di prossimità. Può essere perso o prestato; controlli applicativi e organizzativi restano comunque necessari, a prescindere dalla tecnologia usata.

Conclusione

Un buon flusso NFC non si limita a leggere un tag e chiamare un endpoint. Deve accompagnare l’utente dall’azione fino alla certezza del risultato, senza lasciare zone d’ombra lungo il percorso.

Nel riferimento analizzato, ActionButtons rende esplicite Entrata e Uscita, useHandleNfcTag separa successo, errore e caricamento, soundPlayer centralizza la riproduzione audio, NfcInfoModal riduce gli errori alla prima apertura, e TimestampsList rende verificabili le ultime timbrature.

La regola più importante resta semplice: ogni segnale deve rappresentare uno stato reale del sistema, non un’impressione approssimativa. Solo così suoni, modali, banner, animazioni e liste diventano davvero strumenti di chiarezza, e non semplici decorazioni.

Se stai progettando una timbratura aziendale, parti da un flusso misurabile: rilevamento, invio, conferma, aggiornamento dello storico e gestione dell’errore. L’esperienza utente sarà più prevedibile, e anche il debug sarà molto più semplice da affrontare.

Leggi come gestire archivio timbrature nel prossimo articolo.