# Tags NFC (/fr/in-store/integration/nfc_tags)





<div className="bg-background relative h-auto w-full overflow-hidden rounded-lg border mt-8">
  <FlickeringGrid className="absolute inset-0 z-0 size-full" squareSize="4" gridGap="6" maxOpacity="0.1" flickerChance="0.1" height="600" width="2000" />

  <NfcTagSessionDiagram />
</div>

Lisez et écrivez des tags NFC, tels que des cartes de fidélité, des badges d’accès, des autocollants et des cartes à puce, directement depuis votre
application Android. Le SDK communique avec le lecteur propre à l’appareil : le NFC Android sur un téléphone, ou le lecteur sans contact d’un terminal Sunmi
de la série P. Aucun plugin ne sert d’intermédiaire.

Les sessions de tag sont fournies dans des artefacts facultatifs, `com.tapaya:accept-nfc` et, pour les terminaux Sunmi,
`com.tapaya:accept-nfc-sunmi`. Ajoutez-les uniquement si vous en avez besoin ; les paiements par carte fonctionnent de la même manière avec ou sans eux.

## Appareils pris en charge [#supported-devices]

| Appareil                               | Lecteur                                                                  | Artefacts nécessaires              |
| :------------------------------------- | :----------------------------------------------------------------------- | :--------------------------------- |
| Téléphone ou tablette Android avec NFC | NFC Android                                                              | `accept-nfc`                       |
| Terminal Sunmi de la série P           | Le lecteur sans contact du terminal, via le service de paiement de Sunmi | `accept-nfc` et `accept-nfc-sunmi` |
| Tout autre appareil                    | Aucun                                                                    | Non pris en charge                 |

Les terminaux Sunmi de la série P n’ont pas de NFC Android ; leur antenne se trouve derrière le module de paiement. `accept-nfc-sunmi`
y accède via le service de paiement de Sunmi et prend le relais du NFC Android dès que ce service est installé.

Sur un appareil sans lecteur utilisable, `isSupported()` renvoie `false` et `openSession()` échoue avec
`NOT_SUPPORTED`.

<Callout title="Les téléphones ont quelques limites supplémentaires" type="info">
  Les téléphones ne lisent les tags que pour l’activité au premier plan ; une session se termine donc lorsque cette activité quitte le premier plan.
  MIFARE Classic ne fonctionne que sur les téléphones équipés d’un contrôleur NFC NXP ; ailleurs, ses opérations échouent avec
  `NOT_SUPPORTED`. Pour les tags ISO-DEP, `atsHex` ne contient que les octets historiques de l’ATS, car Android n’expose pas
  le reste.
</Callout>

## Tags pris en charge [#supported-tags]

| Type de tag                                   | Exemples                                              |
| :-------------------------------------------- | :---------------------------------------------------- |
| [NFC Type 2](#nfc-type-2)                     | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| [MIFARE Classic](#mifare-classic)             | MIFARE Classic Mini, 1K, 4K                           |
| [Cartes à puce ISO-DEP](#iso-dep-smart-cards) | MIFARE DESFire                                        |

## Fonctionnement [#how-it-works]

1. Votre application appelle `Accept.nfc.openSession(activity)`.
2. Le SDK active le lecteur de l’appareil et attend une présentation.
3. Le client approche un tag, et `openSession()` renvoie une `TagSession` contenant l’UID et la famille du tag.
4. Votre application lit et écrit via la session, une commande à la fois.
5. Votre application appelle `close()`, ce qui libère le lecteur pour les paiements et les autres sessions de tag.

<Callout title="Les paiements sont toujours prioritaires" type="warn">
  Un appareil dispose d’un seul lecteur sans contact, qu’une session de tag et un paiement par carte ne peuvent pas partager. Le démarrage d’un paiement
  avec `Accept.payments.pay()` met fin à votre session, et `openSession()` échoue avec `READER_BUSY` jusqu’à la fin du paiement.
  Préparez-vous à ce qu’une session se termine à tout moment ; voir [Terminer une session](#end-a-session).
</Callout>

## Prérequis [#prerequisites]

* Un [appareil pris en charge](#supported-devices).
* L’Accept SDK [installé et initialisé](../integration/sdk_integration#initialization). Appeler `isSupported()` ou
  `openSession()` avant `Accept.initialize` lève `NotInitialized`.

Les sessions de tag ne nécessitent ni commerçant connecté, ni plugin Tapaya Terminal, ni autorisation de localisation. Les
artefacts ajoutent à votre manifeste ce dont ils ont besoin :

* `accept-nfc` ajoute l’autorisation `NFC` et déclare `android.hardware.nfc` comme **non obligatoire**, afin que Play laisse votre application
  disponible sur les terminaux Sunmi, qui n’ont pas de NFC Android.
* `accept-nfc-sunmi` ajoute l’autorisation `com.sunmi.perm.CONTACTLESS_CARD` et une entrée `<queries>` pour le service de paiement
  de Sunmi. Il intègre Sunmi PayLib.

## Intégration [#integrate]

<div className="fd-steps [&_h3]:fd-step">
  ### Installation [#install]

  ```kotlin title="build.gradle.kts"
  dependencies {
      implementation("com.tapaya:accept-nfc:1.20.0")

      // Only if you run on Sunmi P-series terminals
      implementation("com.tapaya:accept-nfc-sunmi:1.20.0")
  }
  ```

  Si vous déclarez aussi `com.tapaya:accept`, gardez tous les artefacts dans la même version.

  `Accept.nfc` est une propriété d’extension ; importez-la avant de l’utiliser :

  ```kotlin
  import com.tapaya.accept.nfc.nfc
  ```

  ### Vérifier la prise en charge [#check-support]

  Appelez `isSupported()` pour décider s’il faut afficher ou non la fonctionnalité de tags :

  ```kotlin
  if (Accept.nfc.isSupported()) {
      showScanTagButton()
  }
  ```

  `isSupported()` ne nécessite aucune présentation et ne touche pas au lecteur ; vous pouvez donc l’appeler sans risque à tout moment. Elle vous indique
  si l’appareil dispose d’un lecteur que le SDK peut utiliser. Elle renvoie `true` sur un téléphone dont le NFC est désactivé ; `openSession()` échoue alors
  avec `NFC_DISABLED`. Elle lève `NotInitialized` si `Accept.initialize` n’a pas encore été exécuté.

  ### Ouvrir une session [#open-a-session]

  Transmettez l’activité qui est au premier plan et à l’état resumed :

  ```kotlin
  val session = try {
      Accept.nfc.openSession(activity)
  } catch (e: TagSessionException) {
      when (e.reason) {
          TagErrorReason.TIMEOUT -> showMessage("No tag detected. Try again.")
          TagErrorReason.READER_BUSY -> showMessage("The reader is busy. Try again soon.")
          TagErrorReason.NFC_DISABLED -> promptToEnableNfc()
          else -> showMessage("Can't read tags on this device.")
      }
      return
  }

  try {
      showTag(session.info.uidHex, session.info.family)
      // read and write...
  } finally {
      session.close()
  }
  ```

  `openSession()` se suspend jusqu’à ce qu’un tag soit présenté ou que `tapWait` expire (30 secondes par défaut). Affichez une invite
  « Approchez votre carte » pendant l’attente. La méthode lève `IllegalStateException` si `activity` n’est pas à l’état resumed.

  Sur un téléphone, l’activité compte : Android ne lit les tags que pour elle, et quitter le premier plan interrompt l’attente ou met fin
  à la session avec `ACTIVITY_PAUSED`. Les terminaux Sunmi lisent les tags quoi qu’il y ait au premier plan, mais vous transmettez tout de même une
  activité.

  Pour arrêter l’attente, par exemple lorsque le client appuie sur **Cancel**, annulez la coroutine qui a appelé `openSession()`.
  Le lecteur cesse d’attendre et vous pouvez ouvrir immédiatement une nouvelle session.

  #### Lire les informations de session [#reading-session-info]

  `session.info` décrit le tag présenté :

  | Champ            | Description                                                                        |
  | :--------------- | :--------------------------------------------------------------------------------- |
  | `uidHex`         | L’UID du tag en hexadécimal majuscule, par exemple `04A1B2C3D4E5F6`.               |
  | `family`         | La `TagFamily`, qui vous indique quelles opérations s’appliquent.                  |
  | `sak`, `atqaHex` | Identification de bas niveau renvoyée lors de la détection du tag.                 |
  | `atsHex`         | ATS pour les tags `ISO_DEP` ; sinon `null`.                                        |
  | `memoryBytes`    | Taille de la mémoire utilisateur, si le lecteur peut la déterminer ; sinon `null`. |
  | `versionHex`     | Réponse à `GET_VERSION`, pour les tags qui la prennent en charge ; sinon `null`.   |

  ### Lire et écrire un tag [#read-and-write-a-tag]

  Selon la valeur de `session.info.family`, utilisez les opérations propres à ce type de tag :

  ```kotlin
  when (session.info.family) {
      TagFamily.NTAG, TagFamily.ULTRALIGHT, TagFamily.ULTRALIGHT_EV1,
      TagFamily.ULTRALIGHT_C, TagFamily.TYPE2_OTHER -> readType2Tag(session)
      TagFamily.MIFARE_CLASSIC_MINI, TagFamily.MIFARE_CLASSIC_1K,
      TagFamily.MIFARE_CLASSIC_4K -> readClassicCard(session)
      TagFamily.ISO_DEP -> readSmartCard(session)
      else -> showMessage("This tag isn't supported.")
  }
  ```

  Une commande qui ne correspond pas au tag présenté échoue avec `NOT_SUPPORTED`, et la session reste ouverte.

  #### NFC Type 2 [#nfc-type-2]

  NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C et autres tags Type 2 (`TYPE2_OTHER`).

  | Opération                | Ce qu’elle fait                                                                                                                                                                              |
  | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `readPages(startPage)`   | Renvoie 16 octets : quatre pages de 4 octets à partir de `startPage`.                                                                                                                        |
  | `writePage(page, data)`  | Écrit une page de 4 octets.                                                                                                                                                                  |
  | `passwordAuth(password)` | NTAG et Ultralight EV1 uniquement. Déverrouille les pages protégées avec un mot de passe de 4 octets et renvoie le PACK de 2 octets, qui vous permet de vérifier que le tag est authentique. |
  | `ultralightCAuth(key)`   | Ultralight C uniquement. S’authentifie avec une clé 3DES de 16 octets.                                                                                                                       |
  | `transceiveType2(frame)` | Envoie une trame Type 2 brute, pour les commandes sans méthode dédiée, et renvoie la réponse du tag.                                                                                         |

  ```kotlin
  // Protected NTAG and Ultralight EV1 tags only
  session.passwordAuth(password = byteArrayOf(0x12, 0x34, 0x56, 0x78))

  val pages = session.readPages(startPage = 4)  // pages 4 to 7
  session.writePage(page = 4, data = byteArrayOf(0x01, 0x02, 0x03, 0x04))

  // Raw GET_VERSION command
  val version = session.transceiveType2(byteArrayOf(0x60))
  ```

  #### MIFARE Classic [#mifare-classic]

  MIFARE Classic Mini, 1K et 4K. Authentifiez un secteur avant de lire ou d’écrire ses blocs.

  | Opération                                  | Ce qu’elle fait                                                                               |
  | :----------------------------------------- | :-------------------------------------------------------------------------------------------- |
  | `authenticateClassic(block, keyType, key)` | Authentifie le secteur qui contient `block` avec une clé de 6 octets.                         |
  | `readBlock(block)`                         | Renvoie les 16 octets du bloc.                                                                |
  | `writeBlock(block, data)`                  | Écrit 16 octets dans le bloc.                                                                 |
  | `increment`, `decrement`, `restore`        | Placent le résultat d’une opération sur un bloc de valeur dans le tampon de transfert du tag. |
  | `transfer(block)`                          | Écrit le tampon de transfert dans un bloc.                                                    |

  ```kotlin
  session.authenticateClassic(block = 4, keyType = MifareKeyType.A, key = sectorKey)
  val block = session.readBlock(4)
  session.writeBlock(4, newContent)

  // Value blocks: change the value, then write it back
  session.decrement(block = 5, value = 1)
  session.transfer(block = 5)
  ```

  Une clé erronée échoue avec `AUTH_FAILED`, et le SDK resélectionne le tag pour vous ; vous pouvez donc essayer la clé suivante
  immédiatement :

  ```kotlin
  val key = candidateKeys.firstOrNull { key ->
      try {
          session.authenticateClassic(block = 4, keyType = MifareKeyType.A, key = key)
          true
      } catch (e: TagSessionException) {
          if (e.reason != TagErrorReason.AUTH_FAILED) throw e
          false
      }
  } ?: error("no key for sector 1")
  ```

  #### Cartes à puce ISO-DEP [#iso-dep-smart-cards]

  MIFARE DESFire et autres cartes ISO 14443-4.

  | Opération            | Ce qu’elle fait                                                                     |
  | :------------------- | :---------------------------------------------------------------------------------- |
  | `transmitApdu(apdu)` | Envoie une APDU de commande et renvoie la réponse : les données suivies de SW1 SW2. |

  ```kotlin
  val select = byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00, 0x07,
      0xA0.toByte(), 0x00, 0x00, 0x00, 0x03, 0x10, 0x10, 0x00)
  val response = session.transmitApdu(select)
  val sw = response.takeLast(2)   // [0x90, 0x00] on success
  ```

  ### Terminer une session [#end-a-session]

  Appelez `close()` dès que vous avez terminé, idéalement depuis un bloc `finally`. Tant que vous ne l’avez pas fait, aucun paiement ne peut utiliser le
  lecteur. `close()` peut être appelé sans risque plusieurs fois et ne lève jamais d’exception pour une session déjà terminée.

  Une session peut aussi se terminer d’elle-même. `session.ended` est un `StateFlow` qui reste `null` tant que la session est active,
  puis émet le motif de sa fin :

  ```kotlin
  session.ended.filterNotNull().first().let { reason ->
      when (reason) {
          TagSessionEndReason.CARD_REMOVED -> showMessage("Card removed")
          TagSessionEndReason.PAYMENT -> showMessage("A payment took over the reader")
          TagSessionEndReason.ACTIVITY_PAUSED -> showMessage("Keep the app in the foreground")
          else -> Unit
      }
  }
  ```

  | `TagSessionEndReason` | Quand cela se produit                                                                                                                                |
  | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `CLOSED`              | Votre application a appelé `close()`.                                                                                                                |
  | `CARD_REMOVED`        | Le tag a quitté le champ du lecteur et n’y est pas revenu dans le délai `reselect`.                                                                  |
  | `IDLE_TIMEOUT`        | Aucune commande n’est arrivée dans le délai `idle` (10 secondes par défaut).                                                                         |
  | `PAYMENT`             | Un paiement a démarré via `Accept.payments.pay()`. Les paiements sont toujours prioritaires.                                                         |
  | `ACTIVITY_PAUSED`     | Téléphones uniquement. L’activité avec laquelle vous avez ouvert la session a quitté le premier plan, et Android a cessé de lire les tags pour elle. |
  | `READER_LOST`         | Terminaux Sunmi uniquement. La connexion au service de paiement de Sunmi a été perdue.                                                               |

  Une fois une session terminée, toute commande ultérieure échoue avec `SESSION_ENDED`. Appelez de nouveau `openSession()` pour une nouvelle
  présentation.
</div>

## Gestion des erreurs [#error-handling]

Les échecs liés au tag lèvent `TagSessionException`, dont le `reason` vous indique quoi faire ensuite. Appeler l’API avant que le
SDK ne soit configuré lève `NotInitialized`. `TagSessionException` **n’est pas** une `AcceptException` ; interceptez donc les deux
séparément :

```kotlin
try {
    val session = Accept.nfc.openSession(activity)
    // ...
} catch (e: TagSessionException) {
    // reason-specific handling, see the tables below
} catch (e: NotInitialized) {
    // Accept.initialize hasn't been called
}
```

<Callout title="Authentifiez-vous de nouveau lorsque authLost est défini" type="warn">
  Quel que soit le motif, l’indicateur `authLost` de l’exception peut être défini. Dans ce cas, authentifiez-vous de nouveau avant d’envoyer
  une autre commande vers la mémoire protégée ; sur DESFire, resélectionnez également l’application.
</Callout>

Un argument de clé, de bloc ou de page dont la longueur ou le signe est incorrect, ou une trame ou une APDU vide, lève
`IllegalArgumentException` avant que quoi que ce soit n’atteigne le tag.

### Ouverture d’une session [#opening-a-session]

| `reason`        | Quand cela se produit                                                                                           | Que faire                                                                                                                                 |
| :-------------- | :-------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| `TIMEOUT`       | Aucune présentation pendant `tapWait`.                                                                          | Invitez de nouveau le client à présenter le tag, ou abandonnez.                                                                           |
| `READER_BUSY`   | Un paiement ou une autre session utilise le lecteur.                                                            | Réessayez plus tard ; il n’y a pas de file d’attente.                                                                                     |
| `SESSION_ENDED` | Un paiement a démarré, ou l’activité a quitté le premier plan, pendant l’attente de la présentation.            | Ouvrez une nouvelle session une fois le paiement terminé ou l’application revenue au premier plan.                                        |
| `NFC_DISABLED`  | Téléphones uniquement. Le NFC est désactivé dans les paramètres système.                                        | Demandez à l’utilisateur d’activer le NFC.                                                                                                |
| `VENDOR_ERROR`  | Terminaux Sunmi : le service de paiement est indisponible. `vendorCode` contient son code, s’il en a fourni un. | Réessayez. Si le problème persiste, signalez-le au [support développeurs](/support/contacts#developer-support) en indiquant `vendorCode`. |
| `NOT_SUPPORTED` | L’appareil n’a pas de lecteur que le SDK peut utiliser.                                                         | Masquez la fonctionnalité lorsque `isSupported()` renvoie `false`.                                                                        |

### Envoi d’une commande [#sending-a-command]

| `reason`        | Quand cela se produit                                                                                                       | Que faire                                                                                                             |
| :-------------- | :-------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------- |
| `TIMEOUT`       | Le tag n’a pas répondu à temps ; il a été resélectionné.                                                                    | Réessayez. Si `authLost` est défini, authentifiez-vous d’abord.                                                       |
| `NO_RESPONSE`   | Le tag n’a pas répondu.                                                                                                     | Réessayez si le tag est toujours dans le champ du lecteur.                                                            |
| `AUTH_FAILED`   | Clé ou mot de passe erroné.                                                                                                 | Essayez la clé suivante ; le tag est déjà resélectionné.                                                              |
| `NAK`           | Le tag a refusé la commande : page hors limites, mémoire protégée ou commande non prise en charge.                          | Corrigez la commande. La session continue.                                                                            |
| `NOT_SUPPORTED` | La commande ne correspond pas à cette famille de tags, ou au contrôleur NFC de ce téléphone (MIFARE Classic nécessite NXP). | Vérifiez `session.info.family`. La session continue.                                                                  |
| `VENDOR_ERROR`  | Le SDK propre au lecteur a signalé une panne ; `vendorCode` contient son code.                                              | Réessayez, puis signalez-la au [support développeurs](/support/contacts#developer-support) en indiquant `vendorCode`. |

### Session terminée [#session-over]

| `reason`        | Quand cela se produit                                    | Que faire                                                          |
| :-------------- | :------------------------------------------------------- | :----------------------------------------------------------------- |
| `CARD_REMOVED`  | Le tag a quitté le champ du lecteur pendant la commande. | Demandez une nouvelle présentation et ouvrez une nouvelle session. |
| `SESSION_ENDED` | Une commande a été envoyée après la fin de la session.   | Ouvrez une nouvelle session.                                       |

## Délais d’expiration [#timeouts]

Transmettez `TagTimeouts` à `openSession()` pour modifier les valeurs par défaut :

```kotlin
Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))
```

| Délai       | Par défaut | Maximum | S’applique à                                                     |
| :---------- | :--------- | :------ | :--------------------------------------------------------------- |
| `tapWait`   | 30 s       | 60 s    | L’attente de la présentation dans `openSession()`                |
| `operation` | 1 s        | 5 s     | Chaque commande sur le tag                                       |
| `reselect`  | 1 s        | 5 s     | La resélection du tag après une commande en échec                |
| `idle`      | 10 s       | 60 s    | Le temps entre deux commandes avant que la session ne se termine |

Chaque valeur doit être supérieure à zéro. Construire `TagTimeouts` avec une valeur hors de ces plages lève
`IllegalArgumentException`.

## Bon à savoir [#good-to-know]

* **Un seul lecteur.** L’appareil dispose d’un seul lecteur sans contact, d’où une seule session de tag ou attente de présentation à la fois. Un second
  `openSession()` échoue avec `READER_BUSY`. Les paiements démarrés avec `Accept.payments.pay()` mettent d’abord fin à la session ;
  les paiements démarrés en dehors du SDK ne peuvent pas être coordonnés, gardez donc les sessions courtes.
* **Annuler une commande ne l’annule pas sur le tag.** Au moment où vous annulez la coroutine, la commande a déjà atteint le tag
  et s’exécute quand même ; seul son résultat est ignoré. Un `writeBlock` annulé peut donc tout de même modifier le tag.
* **Les commandes s’exécutent dans l’ordre.** Les méthodes de session peuvent être appelées depuis n’importe quelle coroutine ; elles atteignent le tag une par une,
  dans l’ordre de vos appels.
* **Les données de tag restent hors des logs.** Le SDK ne journalise jamais les clés, les mots de passe ni les données de tag, et masque les UID (par exemple
  `04A1…E5F6`). La manière dont votre application stocke et affiche les données de tag relève de votre responsabilité.

## Tester l’intégration [#test-the-integration]

Sur un téléphone et, si vous les prenez en charge, sur un terminal Sunmi de la série P :

1. Vérifiez que `isSupported()` renvoie `true` sur les deux, et `false` sur un appareil sans NFC.
2. Présentez un tag NTAG ou Ultralight et vérifiez que la session indique le bon UID et que `readPages(0)` renvoie 16
   octets. Puis exécutez `writePage` et relisez la page.
3. Sur une carte MIFARE Classic, authentifiez-vous avec une clé erronée et vérifiez que vous obtenez `AUTH_FAILED`, puis avec la bonne clé.
4. Annulez `openSession()` pendant l’attente de présentation et ouvrez immédiatement une nouvelle session.
5. Démarrez un paiement pendant une session : la session doit se terminer en `PAYMENT` et le paiement doit aboutir. Retirez le
   tag pendant une autre session : celle-ci doit se terminer en `CARD_REMOVED`.
6. Sur un téléphone, désactivez le NFC et vérifiez que vous obtenez `NFC_DISABLED`. Pendant une session, mettez l’application en arrière-plan et
   vérifiez que la session se termine en `ACTIVITY_PAUSED`.
