Tapaya
Přijímání plateb v prodejněPrůvodce integracíNFC tagyBeta

NFC tagy

AndroidBeta

Čtěte a zapisujte NFC tagy ze své aplikace pro Android pomocí NFC telefonu nebo čtečky terminálu Sunmi řady P.

Čtěte a zapisujte NFC tagy, například věrnostní karty, přístupové karty, nálepky a čipové karty, přímo ze své aplikace pro Android. SDK komunikuje přímo s čtečkou zařízení: na telefonu s Android NFC, na terminálu Sunmi řady P s jeho bezkontaktní čtečkou. Mezi SDK a čtečkou nestojí žádný plugin.

Relace s tagy jsou součástí volitelných artefaktů com.tapaya:accept-nfc a pro terminály Sunmi com.tapaya:accept-nfc-sunmi. Přidejte je jen tehdy, když je potřebujete; platby kartou fungují s nimi i bez nich stejně.

Podporovaná zařízení

ZařízeníČtečkaPotřebné artefakty
Telefon nebo tablet s Androidem a NFCAndroid NFCaccept-nfc
Terminál Sunmi řady PBezkontaktní čtečka terminálu, přes platební službu Sunmiaccept-nfc a accept-nfc-sunmi
Jakékoli jiné zařízeníŽádnáNepodporováno

Terminály Sunmi řady P nemají Android NFC; jejich anténa je umístěna za platebním modulem. accept-nfc-sunmi k ní přistupuje přes platební službu Sunmi, a kdykoli je tato služba nainstalovaná, má přednost před Android NFC.

Na zařízení bez použitelné čtečky vrací isSupported() hodnotu false a openSession() selže s NOT_SUPPORTED.

Telefony mají několik omezení navíc

Telefony čtou tagy jen pro aktivitu v popředí, takže relace skončí, jakmile tato aktivita popředí opustí. MIFARE Classic funguje jen na telefonech s NFC řadičem NXP; jinde jeho operace selžou s NOT_SUPPORTED. U tagů ISO-DEP obsahuje atsHex jen historické bajty ATS, protože zbytek Android nezpřístupňuje.

Podporované tagy

Typ taguPříklady
NFC Type 2NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C
MIFARE ClassicMIFARE Classic Mini, 1K, 4K
Čipové karty ISO-DEPMIFARE DESFire

Jak to funguje

  1. Vaše aplikace zavolá Accept.nfc.openSession(activity).
  2. SDK zapne čtečku zařízení a čeká na přiložení.
  3. Zákazník přiloží tag a openSession() vrátí TagSession s UID a rodinou tagu.
  4. Vaše aplikace čte a zapisuje prostřednictvím relace, vždy po jednom příkazu.
  5. Vaše aplikace zavolá close(), čímž uvolní čtečku pro platby a další relace s tagy.

Platby mají vždy přednost

Zařízení má jednu bezkontaktní čtečku, kterou relace s tagem a platba kartou nemohou sdílet. Zahájení platby pomocí Accept.payments.pay() vaši relaci ukončí a openSession() selhává s READER_BUSY, dokud platba neskončí. Počítejte s tím, že relace může skončit kdykoli; viz Ukončení relace.

Předpoklady

Relace s tagy nevyžadují přihlášeného obchodníka, plugin Tapaya Terminal ani oprávnění k poloze. Artefakty si do vašeho manifestu přidají, co potřebují:

  • accept-nfc přidá oprávnění NFC a označí android.hardware.nfc jako nepovinné, takže Google Play ponechá vaši aplikaci dostupnou i na terminálech Sunmi, které Android NFC nemají.
  • accept-nfc-sunmi přidá oprávnění com.sunmi.perm.CONTACTLESS_CARD a záznam <queries> pro platební službu Sunmi. Obsahuje knihovnu Sunmi PayLib.

Integrace

Instalace

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")
}

Pokud deklarujete i com.tapaya:accept, udržujte všechny artefakty ve stejné verzi.

Accept.nfc je extension property; před použitím ji naimportujte:

import com.tapaya.accept.nfc.nfc

Ověření podpory

Zavolejte isSupported() a podle výsledku rozhodněte, zda funkci pro tagy vůbec zobrazit:

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

isSupported() nevyžaduje přiložení a čtečku nijak nevyužívá, takže ji můžete bezpečně volat kdykoli. Říká vám, zda má zařízení čtečku, kterou SDK dokáže použít. Na telefonu s vypnutým NFC vrací true; openSession() pak selže s NFC_DISABLED. Pokud Accept.initialize ještě neproběhlo, vyhodí metoda výjimku NotInitialized.

Otevření relace

Předejte aktivitu, která je v popředí a ve stavu 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 pozastaví, dokud není přiložen tag nebo dokud nevyprší tapWait (ve výchozím nastavení 30 sekund). Během čekání zobrazte výzvu „Přiložte kartu“. Pokud activity není ve stavu resumed, vyhodí metoda výjimku IllegalStateException.

Na telefonu na aktivitě záleží: Android čte tagy jen pro ni a jakmile opustí popředí, čekání se zastaví nebo relace skončí s ACTIVITY_PAUSED. Terminály Sunmi čtou tagy bez ohledu na to, co je v popředí, aktivitu ale předáváte i tak.

Chcete-li čekání ukončit, například když zákazník klepne na Cancel, zrušte korutinu, která zavolala openSession(). Čtečka přestane čekat a můžete hned otevřít novou relaci.

Čtení informací o relaci

session.info popisuje přiložený tag:

PolePopis
uidHexUID tagu jako hexadecimální řetězec velkými písmeny, např. 04A1B2C3D4E5F6.
familyHodnota TagFamily, ze které poznáte, jaké operace lze použít.
sak, atqaHexNízkoúrovňová identifikace vrácená při detekci tagu.
atsHexATS pro tagy ISO_DEP; jinak null.
memoryBytesVelikost uživatelské paměti, pokud ji čtečka dokáže zjistit; jinak null.
versionHexOdpověď na GET_VERSION u tagů, které ji podporují; jinak null.

Čtení a zápis tagu

Větvěte podle session.info.family a poté použijte operace pro daný typ tagu:

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.")
}

Příkaz, který neodpovídá přiloženému tagu, selže s NOT_SUPPORTED a relace zůstane otevřená.

NFC Type 2

NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C a další tagy Type 2 (TYPE2_OTHER).

OperaceCo dělá
readPages(startPage)Vrátí 16 bajtů: čtyři 4bajtové stránky počínaje stránkou startPage.
writePage(page, data)Zapíše jednu 4bajtovou stránku.
passwordAuth(password)Jen NTAG a Ultralight EV1. Odemkne chráněné stránky 4bajtovým heslem a vrátí 2bajtový PACK, pomocí kterého můžete ověřit, že je tag pravý.
ultralightCAuth(key)Jen Ultralight C. Autentizuje se 16bajtovým klíčem 3DES.
transceiveType2(frame)Odešle surový rámec Type 2 pro příkazy bez vlastní metody a vrátí odpověď tagu.
// 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 a 4K. Než budete číst nebo zapisovat bloky sektoru, sektor autentizujte.

OperaceCo dělá
authenticateClassic(block, keyType, key)Autentizuje 6bajtovým klíčem sektor, který obsahuje block.
readBlock(block)Vrátí 16 bajtů bloku.
writeBlock(block, data)Zapíše do bloku 16 bajtů.
increment, decrement, restoreUloží výsledek operace s hodnotovým blokem do přenosového bufferu tagu.
transfer(block)Zapíše přenosový buffer do bloku.
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)

Autentizace nesprávným klíčem selže s AUTH_FAILED a SDK za vás tag znovu vybere (re-select), takže můžete ihned zkusit další klíč:

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")

Čipové karty ISO-DEP

MIFARE DESFire a další karty ISO 14443-4.

OperaceCo dělá
transmitApdu(apdu)Odešle jedno příkazové APDU a vrátí odpověď: data následovaná 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

Ukončení relace

Jakmile skončíte, zavolejte close(), ideálně z bloku finally. Dokud to neuděláte, žádná platba nemůže čtečku použít. close() je bezpečné volat opakovaně a u relace, která už skončila, nikdy nevyhazuje výjimku.

Relace může skončit i sama. session.ended je StateFlow, který zůstává null, dokud je relace aktivní, a poté vyšle důvod jejího ukončení:

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
    }
}
TagSessionEndReasonKdy nastává
CLOSEDVaše aplikace zavolala close().
CARD_REMOVEDTag opustil pole čtečky a během časového limitu reselect se nevrátil.
IDLE_TIMEOUTBěhem časového limitu idle (ve výchozím nastavení 10 sekund) nepřišel žádný příkaz.
PAYMENTByla zahájena platba přes Accept.payments.pay(). Platby mají vždy přednost.
ACTIVITY_PAUSEDJen telefony. Aktivita, se kterou jste relaci otevřeli, opustila popředí a Android pro ni přestal číst tagy.
READER_LOSTJen terminály Sunmi. Spojení s platební službou Sunmi se přerušilo.

Jakmile relace skončí, každý další příkaz selže s SESSION_ENDED. Pro nové přiložení zavolejte znovu openSession().

Zpracování chyb

Selhání při práci s tagem vyhazují TagSessionException, jejíž reason vám řekne, co dělat dál. Volání API dřív, než je SDK nastavené, vyhodí NotInitialized. TagSessionException není AcceptException, proto je zachytávejte zvlášť:

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
}

Při nastaveném authLost se znovu autentizujte

U jakéhokoli důvodu může být nastaven příznak výjimky authLost. Pokud je nastaven, znovu se autentizujte, než pošlete další příkaz do chráněné paměti; u DESFire navíc znovu vyberte aplikaci.

Pokud má argument klíče, bloku nebo stránky nesprávnou délku nebo znaménko, případně je rámec nebo APDU prázdné, vyhodí se IllegalArgumentException dřív, než se cokoli dostane k tagu.

Otevírání relace

reasonKdy nastáváCo dělat
TIMEOUTBěhem tapWait nedošlo k přiložení.Znovu vyzvěte k přiložení, nebo pokus ukončete.
READER_BUSYČtečku používá platba nebo jiná relace.Zkuste to později; žádná fronta neexistuje.
SESSION_ENDEDBěhem čekání na přiložení byla zahájena platba nebo aktivita opustila popředí.Otevřete novou relaci, až platba skončí nebo bude aplikace znovu v popředí.
NFC_DISABLEDJen telefony. NFC je vypnuté v nastavení systému.Požádejte uživatele, aby NFC zapnul.
VENDOR_ERRORTerminály Sunmi: platební služba není dostupná. vendorCode obsahuje její kód, pokud nějaký vrátila.Zkuste to znovu. Pokud problém přetrvává, nahlaste jej podpoře pro vývojáře i s vendorCode.
NOT_SUPPORTEDZařízení nemá čtečku, kterou SDK dokáže použít.Skryjte funkci, když isSupported() vrátí false.

Odeslání příkazu

reasonKdy nastáváCo dělat
TIMEOUTTag neodpověděl včas; byl znovu vybrán.Zkuste to znovu. Pokud je nastaven authLost, nejprve se autentizujte.
NO_RESPONSETag neodpověděl.Zkuste to znovu, pokud je tag stále v poli čtečky.
AUTH_FAILEDNesprávný klíč nebo heslo.Zkuste další klíč; tag už je znovu vybrán.
NAKTag příkaz odmítl: stránka mimo rozsah, chráněná paměť nebo nepodporovaný příkaz.Opravte příkaz. Relace pokračuje.
NOT_SUPPORTEDPříkaz neodpovídá této rodině tagů nebo NFC řadiči tohoto telefonu (MIFARE Classic vyžaduje NXP).Zkontrolujte session.info.family. Relace pokračuje.
VENDOR_ERRORVlastní SDK čtečky ohlásilo poruchu; vendorCode obsahuje její kód.Zkuste to znovu, poté poruchu nahlaste podpoře pro vývojáře i s vendorCode.

Relace skončila

reasonKdy nastáváCo dělat
CARD_REMOVEDTag opustil pole čtečky během příkazu.Požádejte o nové přiložení a otevřete novou relaci.
SESSION_ENDEDPříkaz byl odeslán po skončení relace.Otevřete novou relaci.

Časové limity

Chcete-li změnit výchozí hodnoty, předejte openSession() objekt TagTimeouts:

Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))
Časový limitVýchozíMaximumPlatí pro
tapWait30 s60 sČekání na přiložení v openSession()
operation1 s5 sKaždý příkaz na tagu
reselect1 s5 sOpětovný výběr tagu po neúspěšném příkazu
idle10 s60 sDoba mezi příkazy, po které relace skončí

Každá hodnota musí být větší než nula. Vytvoření TagTimeouts s hodnotou mimo tyto rozsahy vyhodí IllegalArgumentException.

Dobré vědět

  • Jedna čtečka. Zařízení má jednu bezkontaktní čtečku, takže najednou může běžet jen jedna relace s tagem nebo jedno čekání na přiložení. Druhé volání openSession() selže s READER_BUSY. Platby zahájené pomocí Accept.payments.pay() relaci nejprve ukončí; platby zahájené mimo SDK koordinovat nelze, proto relace udržujte krátké.
  • Zrušení příkazu jej nevrátí zpět. V okamžiku, kdy korutinu zrušíte, už příkaz dorazil k tagu a stále se provádí; zahodí se jen jeho výsledek. Zrušený writeBlock tedy může tag přesto změnit.
  • Příkazy se provádějí v pořadí. Metody relace lze volat z libovolné korutiny; k tagu se dostávají jedna po druhé, v pořadí, v jakém je voláte.
  • Data tagů se nedostanou do logů. SDK nikdy neloguje klíče, hesla ani data tagů a UID maskuje (např. 04A1…E5F6). Jak vaše aplikace data tagů ukládá a zobrazuje, je na vás.

Otestování integrace

Na telefonu a, pokud je podporujete, na terminálu Sunmi řady P:

  1. Ověřte, že isSupported() vrací true na obou zařízeních a false na zařízení bez NFC.
  2. Přiložte tag NTAG nebo Ultralight a ověřte, že relace hlásí správné UID a readPages(0) vrací 16 bajtů. Poté proveďte writePage a stránku znovu přečtěte.
  3. Na kartě MIFARE Classic se autentizujte nesprávným klíčem a potvrďte AUTH_FAILED, pak správným klíčem.
  4. Zrušte openSession() během čekání na přiložení a ihned otevřete novou relaci.
  5. Během relace zahajte platbu: relace by měla skončit jako PAYMENT a platba by měla projít. Během jiné relace tag oddalte: relace by měla skončit jako CARD_REMOVED.
  6. Na telefonu vypněte NFC a potvrďte NFC_DISABLED. Během relace přesuňte aplikaci na pozadí a potvrďte, že relace skončí jako ACTIVITY_PAUSED.
Chybí vám něco na této stránce?Navrhněte funkci. Připojíme tuto stránku, aby tým viděl souvislosti.