Tags NFC
AndroidBetaLisez et écrivez des tags NFC depuis votre application Android, à l’aide du NFC du téléphone ou du lecteur d’un terminal Sunmi de la série P.
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
| 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.
Les téléphones ont quelques limites supplémentaires
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.
Tags pris en charge
| Type de tag | Exemples |
|---|---|
| NFC Type 2 | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| MIFARE Classic | MIFARE Classic Mini, 1K, 4K |
| Cartes à puce ISO-DEP | MIFARE DESFire |
Fonctionnement
- Votre application appelle
Accept.nfc.openSession(activity). - Le SDK active le lecteur de l’appareil et attend une présentation.
- Le client approche un tag, et
openSession()renvoie uneTagSessioncontenant l’UID et la famille du tag. - Votre application lit et écrit via la session, une commande à la fois.
- Votre application appelle
close(), ce qui libère le lecteur pour les paiements et les autres sessions de tag.
Les paiements sont toujours prioritaires
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.
Prérequis
- Un appareil pris en charge.
- L’Accept SDK installé et initialisé. Appeler
isSupported()ouopenSession()avantAccept.initializelèveNotInitialized.
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-nfcajoute l’autorisationNFCet déclareandroid.hardware.nfccomme non obligatoire, afin que Play laisse votre application disponible sur les terminaux Sunmi, qui n’ont pas de NFC Android.accept-nfc-sunmiajoute l’autorisationcom.sunmi.perm.CONTACTLESS_CARDet une entrée<queries>pour le service de paiement de Sunmi. Il intègre Sunmi PayLib.
Intégration
Installation
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 :
import com.tapaya.accept.nfc.nfcVérifier la prise en charge
Appelez isSupported() pour décider s’il faut afficher ou non la fonctionnalité de tags :
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
Transmettez l’activité qui est au premier plan et à l’état resumed :
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
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
Selon la valeur de session.info.family, utilisez les opérations propres à ce type de tag :
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
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. |
// 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 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. |
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 :
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
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. |
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 successTerminer une 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 :
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.
Gestion des erreurs
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 :
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
}Authentifiez-vous de nouveau lorsque authLost est défini
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.
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
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 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
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 en indiquant vendorCode. |
Session terminée
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
Transmettez TagTimeouts à openSession() pour modifier les valeurs par défaut :
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
- 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 avecREADER_BUSY. Les paiements démarrés avecAccept.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
writeBlockannulé 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
Sur un téléphone et, si vous les prenez en charge, sur un terminal Sunmi de la série P :
- Vérifiez que
isSupported()renvoietruesur les deux, etfalsesur un appareil sans NFC. - 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écutezwritePageet relisez la page. - 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é. - Annulez
openSession()pendant l’attente de présentation et ouvrez immédiatement une nouvelle session. - Démarrez un paiement pendant une session : la session doit se terminer en
PAYMENTet le paiement doit aboutir. Retirez le tag pendant une autre session : celle-ci doit se terminer enCARD_REMOVED. - 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 enACTIVITY_PAUSED.