Tapaya
Acceptation des paiements en magasinGuide d’intégrationTags NFCBeta

Tags NFC

AndroidBeta

Lisez 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

AppareilLecteurArtefacts nécessaires
Téléphone ou tablette Android avec NFCNFC Androidaccept-nfc
Terminal Sunmi de la série PLe lecteur sans contact du terminal, via le service de paiement de Sunmiaccept-nfc et accept-nfc-sunmi
Tout autre appareilAucunNon 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 tagExemples
NFC Type 2NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C
MIFARE ClassicMIFARE Classic Mini, 1K, 4K
Cartes à puce ISO-DEPMIFARE DESFire

Fonctionnement

  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.

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

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

Installation

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 :

import com.tapaya.accept.nfc.nfc

Vé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é :

ChampDescription
uidHexL’UID du tag en hexadécimal majuscule, par exemple 04A1B2C3D4E5F6.
familyLa TagFamily, qui vous indique quelles opérations s’appliquent.
sak, atqaHexIdentification de bas niveau renvoyée lors de la détection du tag.
atsHexATS pour les tags ISO_DEP ; sinon null.
memoryBytesTaille de la mémoire utilisateur, si le lecteur peut la déterminer ; sinon null.
versionHexRé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érationCe 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érationCe 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, restorePlacent 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érationCe 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 success

Terminer 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
    }
}
TagSessionEndReasonQuand cela se produit
CLOSEDVotre application a appelé close().
CARD_REMOVEDLe tag a quitté le champ du lecteur et n’y est pas revenu dans le délai reselect.
IDLE_TIMEOUTAucune commande n’est arrivée dans le délai idle (10 secondes par défaut).
PAYMENTUn paiement a démarré via Accept.payments.pay(). Les paiements sont toujours prioritaires.
ACTIVITY_PAUSEDTé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_LOSTTerminaux 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

reasonQuand cela se produitQue faire
TIMEOUTAucune présentation pendant tapWait.Invitez de nouveau le client à présenter le tag, ou abandonnez.
READER_BUSYUn paiement ou une autre session utilise le lecteur.Réessayez plus tard ; il n’y a pas de file d’attente.
SESSION_ENDEDUn 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_DISABLEDTéléphones uniquement. Le NFC est désactivé dans les paramètres système.Demandez à l’utilisateur d’activer le NFC.
VENDOR_ERRORTerminaux 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_SUPPORTEDL’appareil n’a pas de lecteur que le SDK peut utiliser.Masquez la fonctionnalité lorsque isSupported() renvoie false.

Envoi d’une commande

reasonQuand cela se produitQue faire
TIMEOUTLe tag n’a pas répondu à temps ; il a été resélectionné.Réessayez. Si authLost est défini, authentifiez-vous d’abord.
NO_RESPONSELe tag n’a pas répondu.Réessayez si le tag est toujours dans le champ du lecteur.
AUTH_FAILEDClé ou mot de passe erroné.Essayez la clé suivante ; le tag est déjà resélectionné.
NAKLe 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_SUPPORTEDLa 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_ERRORLe 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

reasonQuand cela se produitQue faire
CARD_REMOVEDLe tag a quitté le champ du lecteur pendant la commande.Demandez une nouvelle présentation et ouvrez une nouvelle session.
SESSION_ENDEDUne 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élaiPar défautMaximumS’applique à
tapWait30 s60 sL’attente de la présentation dans openSession()
operation1 s5 sChaque commande sur le tag
reselect1 s5 sLa resélection du tag après une commande en échec
idle10 s60 sLe 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 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

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.
Il manque quelque chose sur cette page ?Demandez une fonctionnalité. Nous joignons cette page pour que l’équipe ait le contexte.