Tapaya
Прийом платежів у торговій точціПосібник з інтеграціїNFC-міткиBeta

NFC-мітки

AndroidBeta

Зчитуйте й записуйте NFC-мітки зі свого застосунку для Android за допомогою NFC телефона або зчитувача термінала Sunmi серії P.

Зчитуйте й записуйте NFC-мітки, як-от картки лояльності, перепустки, наліпки та смарт-картки, безпосередньо зі свого застосунку для Android. SDK працює із власним зчитувачем пристрою: з Android NFC на телефоні або з безконтактним зчитувачем термінала Sunmi серії P. Жодного плагіна між ними немає.

Сесії з мітками постачаються в необов'язкових артефактах: com.tapaya:accept-nfc і, для терміналів Sunmi, com.tapaya:accept-nfc-sunmi. Додавайте їх, лише якщо вони вам потрібні; платежі карткою працюють однаково з ними і без них.

Підтримувані пристрої

ПристрійЗчитувачПотрібні артефакти
Телефон або планшет на Android з NFCAndroid NFCaccept-nfc
Термінал Sunmi серії PБезконтактний зчитувач термінала через платіжний сервіс Sunmiaccept-nfc і accept-nfc-sunmi
Будь-який інший пристрійНемаєНе підтримується

Термінали Sunmi серії P не мають Android NFC; їхня антена розташована за платіжним модулем. accept-nfc-sunmi звертається до неї через платіжний сервіс Sunmi і замінює Android NFC щоразу, коли цей сервіс установлено.

На пристрої без придатного зчитувача isSupported() повертає false, а openSession() завершується помилкою NOT_SUPPORTED.

Телефони мають кілька додаткових обмежень

Телефони зчитують мітки лише для активності (activity) на передньому плані, тому сесія завершується, коли ця активність залишає передній план. MIFARE Classic працює лише на телефонах з NFC-контролером NXP; на інших його операції завершуються помилкою NOT_SUPPORTED. Для міток ISO-DEP atsHex містить лише історичні байти ATS, оскільки Android не надає решти.

Підтримувані мітки

Тип міткиПриклади
NFC Type 2NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C
MIFARE ClassicMIFARE Classic Mini, 1K, 4K
Смарт-картки ISO-DEPMIFARE DESFire

Як це працює

  1. Ваш застосунок викликає Accept.nfc.openSession(activity).
  2. SDK вмикає зчитувач пристрою і чекає на прикладання.
  3. Клієнт прикладає мітку, і openSession() повертає TagSession з UID і сімейством мітки.
  4. Ваш застосунок зчитує й записує дані через сесію, по одній команді за раз.
  5. Ваш застосунок викликає close(), що звільняє зчитувач для платежів та інших сесій з мітками.

Платежі завжди мають пріоритет

Пристрій має один безконтактний зчитувач, який сесія з міткою та платіж карткою не можуть використовувати спільно. Початок платежу через Accept.payments.pay() завершує вашу сесію, а openSession() завершується помилкою READER_BUSY, доки платіж не закінчиться. Будьте готові до того, що сесія може завершитися будь-якої миті; див. Завершення сесії.

Передумови

Сесії з мітками не потребують мерчанта, який увійшов у застосунок, плагіна Tapaya Terminal чи дозволу на доступ до геолокації. Артефакти самі додають до вашого маніфесту все потрібне:

  • accept-nfc додає дозвіл NFC і позначає функцію android.hardware.nfc як необов'язкову, тож Play залишає ваш застосунок доступним на терміналах Sunmi, які не мають Android NFC.
  • accept-nfc-sunmi додає дозвіл com.sunmi.perm.CONTACTLESS_CARD і запис <queries> для платіжного сервісу Sunmi. Він містить вбудовану бібліотеку Sunmi PayLib.

Інтеграція

Установлення

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

Якщо ви також оголошуєте com.tapaya:accept, тримайте всі артефакти на одній версії.

Accept.nfc є властивістю-розширенням (extension property); імпортуйте її перед використанням:

import com.tapaya.accept.nfc.nfc

Перевірка підтримки

Викличте isSupported(), щоб вирішити, чи взагалі показувати функцію роботи з мітками:

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

isSupported() не потребує прикладання й не використовує зчитувач, тому її безпечно викликати будь-коли. Вона показує, чи має пристрій зчитувач, який може використовувати SDK. На телефоні з вимкненим NFC вона повертає true; тоді openSession() завершується помилкою NFC_DISABLED. Якщо Accept.initialize ще не виконано, вона викидає NotInitialized.

Відкриття сесії

Передайте активність, яка перебуває на передньому плані та в стані 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() призупиняється, доки не буде прикладено мітку або не мине tapWait (за замовчуванням 30 секунд). Поки триває очікування, показуйте підказку «Прикладіть картку». Цей метод викидає IllegalStateException, якщо activity не перебуває в стані resumed.

На телефоні активність має значення: Android зчитує мітки лише для неї, а вихід з переднього плану зупиняє очікування або завершує сесію з причиною ACTIVITY_PAUSED. Термінали Sunmi зчитують мітки незалежно від того, що перебуває на передньому плані, але активність однаково потрібно передати.

Щоб припинити очікування, наприклад коли клієнт натискає Cancel, скасуйте корутину, яка викликала openSession(). Зчитувач припиняє очікування, і ви можете одразу відкрити нову сесію.

Отримання інформації про сесію

session.info описує прикладену мітку:

ПолеОпис
uidHexUID мітки як шістнадцятковий рядок у верхньому регістрі, наприклад 04A1B2C3D4E5F6.
familyЗначення TagFamily, яке показує, які операції застосовні.
sak, atqaHexНизькорівнева ідентифікація, що повертається під час виявлення мітки.
atsHexATS для міток ISO_DEP; інакше null.
memoryBytesРозмір пам'яті користувача, якщо зчитувач може його визначити; інакше null.
versionHexВідповідь на GET_VERSION для міток, які її підтримують; інакше null.

Зчитування й запис мітки

Розгалужте логіку за session.info.family, а потім використовуйте операції для цього типу мітки:

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

Команда, яка не відповідає прикладеній мітці, завершується помилкою NOT_SUPPORTED, а сесія залишається відкритою.

NFC Type 2

NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C та інші мітки Type 2 (TYPE2_OTHER).

ОпераціяЩо робить
readPages(startPage)Повертає 16 байтів: чотири 4-байтові сторінки, починаючи зі сторінки startPage.
writePage(page, data)Записує одну 4-байтову сторінку.
passwordAuth(password)Лише NTAG і Ultralight EV1. Розблоковує захищені сторінки 4-байтовим паролем і повертає 2-байтовий PACK, за допомогою якого можна перевірити, що мітка справжня.
ultralightCAuth(key)Лише Ultralight C. Виконує автентифікацію 16-байтовим ключем 3DES.
transceiveType2(frame)Надсилає необроблений кадр Type 2 для команд, що не мають окремого методу, і повертає відповідь мітки.
// 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 і 4K. Перш ніж зчитувати або записувати блоки сектора, пройдіть автентифікацію для цього сектора.

ОпераціяЩо робить
authenticateClassic(block, keyType, key)Автентифікує 6-байтовим ключем сектор, що містить block.
readBlock(block)Повертає 16 байтів блоку.
writeBlock(block, data)Записує 16 байтів у блок.
increment, decrement, restoreПоміщають результат операції з блоком значення в буфер передавання мітки.
transfer(block)Записує буфер передавання в блок.
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)

Автентифікація неправильним ключем завершується помилкою AUTH_FAILED, і SDK повторно вибирає мітку (re-select) за вас, тож ви можете одразу спробувати наступний ключ:

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

Смарт-картки ISO-DEP

MIFARE DESFire та інші картки ISO 14443-4.

ОпераціяЩо робить
transmitApdu(apdu)Надсилає одну командну APDU і повертає відповідь: дані, за якими йдуть 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

Завершення сесії

Викличте close(), щойно закінчите, бажано з блоку finally. Доки ви цього не зробите, жоден платіж не зможе використати зчитувач. close() безпечно викликати кілька разів, і для сесії, яка вже завершилася, він ніколи не викидає винятків.

Сесія також може завершитися сама. session.ended є StateFlow, який залишається null, поки сесія активна, а потім надсилає причину її завершення:

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Коли виникає
CLOSEDВаш застосунок викликав close().
CARD_REMOVEDМітка вийшла з поля зчитувача й не повернулася протягом тайм-ауту reselect.
IDLE_TIMEOUTПротягом тайм-ауту idle (за замовчуванням 10 секунд) не надійшло жодної команди.
PAYMENTПочався платіж через Accept.payments.pay(). Платежі завжди мають пріоритет.
ACTIVITY_PAUSEDЛише телефони. Активність, з якою ви відкрили сесію, залишила передній план, і Android припинив зчитувати мітки для неї.
READER_LOSTЛише термінали Sunmi. З'єднання з платіжним сервісом Sunmi обірвалося.

Після завершення сесії кожна наступна команда завершується помилкою SESSION_ENDED. Для нового прикладання знову викличте openSession().

Обробка помилок

Збої під час роботи з міткою викидають TagSessionException, чий reason підказує, що робити далі. Виклик API до налаштування SDK викидає NotInitialized. TagSessionException не є AcceptException, тому перехоплюйте ці два винятки окремо:

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
}

Повторіть автентифікацію, якщо встановлено authLost

За будь-якої причини може бути встановлено прапорець винятку authLost. Якщо його встановлено, пройдіть автентифікацію повторно, перш ніж надсилати наступну команду до захищеної пам'яті; на DESFire також повторно виберіть застосунок.

Аргумент ключа, блоку чи сторінки неправильної довжини або знака, а також порожній кадр чи APDU, спричиняють IllegalArgumentException ще до того, як будь-що дійде до мітки.

Відкриття сесії

reasonКоли виникаєЩо робити
TIMEOUTПротягом tapWait не було прикладання.Знову попросіть прикласти мітку або припиніть спробу.
READER_BUSYЗчитувачем користується платіж або інша сесія.Повторіть спробу пізніше; черги немає.
SESSION_ENDEDПід час очікування прикладання почався платіж або активність залишила передній план.Відкрийте нову сесію, коли платіж завершиться або застосунок повернеться на передній план.
NFC_DISABLEDЛише телефони. NFC вимкнено в системних налаштуваннях.Попросіть користувача ввімкнути NFC.
VENDOR_ERRORТермінали Sunmi: платіжний сервіс недоступний. vendorCode містить його код, якщо сервіс його надав.Повторіть спробу. Якщо помилка не зникає, повідомте про неї підтримці для розробників, указавши vendorCode.
NOT_SUPPORTEDПристрій не має зчитувача, який може використовувати SDK.Приховайте функцію, коли isSupported() повертає false.

Надсилання команди

reasonКоли виникаєЩо робити
TIMEOUTМітка не відповіла вчасно; її повторно вибрано.Повторіть спробу. Якщо встановлено authLost, спершу пройдіть автентифікацію.
NO_RESPONSEМітка не відповіла.Повторіть спробу, якщо мітка досі в полі зчитувача.
AUTH_FAILEDНеправильний ключ або пароль.Спробуйте наступний ключ; мітку вже повторно вибрано.
NAKМітка відхилила команду: сторінка поза діапазоном, захищена пам'ять або непідтримувана команда.Виправте команду. Сесія триває.
NOT_SUPPORTEDКоманда не відповідає цьому сімейству міток або NFC-контролеру цього телефона (MIFARE Classic потребує NXP).Перевірте session.info.family. Сесія триває.
VENDOR_ERRORВласний SDK зчитувача повідомив про несправність; vendorCode містить її код.Повторіть спробу, а потім повідомте про це підтримці для розробників, указавши vendorCode.

Сесію завершено

reasonКоли виникаєЩо робити
CARD_REMOVEDМітка вийшла з поля зчитувача під час виконання команди.Попросіть прикласти мітку знову й відкрийте нову сесію.
SESSION_ENDEDКоманду надіслано після завершення сесії.Відкрийте нову сесію.

Тайм-аути

Щоб змінити значення за замовчуванням, передайте TagTimeouts в openSession():

Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))
Тайм-аутЗа замовчуваннямМаксимумСтосується
tapWait30 с60 сОчікування прикладання в openSession()
operation1 с5 сКожної команди на мітці
reselect1 с5 сПовторного вибору мітки після невдалої команди
idle10 с60 сЧасу між командами, після якого сесія завершується

Кожне значення має бути більшим за нуль. Створення TagTimeouts зі значенням поза цими діапазонами викидає IllegalArgumentException.

Корисно знати

  • Один зчитувач. Пристрій має один безконтактний зчитувач, тому одночасно можлива лише одна сесія з міткою або одне очікування прикладання. Другий виклик openSession() завершується помилкою READER_BUSY. Платежі, розпочаті через Accept.payments.pay(), спершу завершують сесію; платежі, розпочаті поза SDK, узгодити неможливо, тому робіть сесії короткими.
  • Скасування команди не відміняє її. На момент, коли ви скасовуєте корутину, команда вже дійшла до мітки й усе одно виконується; відкидається лише її результат. Скасований writeBlock усе ще може змінити мітку.
  • Команди виконуються по черзі. Методи сесії можна викликати з будь-якої корутини; вони доходять до мітки по одній, у тому порядку, у якому ви їх викликаєте.
  • Дані міток не потрапляють у логи. SDK ніколи не записує в логи ключі, паролі чи дані міток і маскує UID (наприклад, 04A1…E5F6). Як ваш застосунок зберігає й показує дані міток, вирішуєте ви.

Тестування інтеграції

На телефоні та, якщо ви їх підтримуєте, на терміналі Sunmi серії P:

  1. Перевірте, що isSupported() повертає true на обох пристроях і false на пристрої без NFC.
  2. Прикладіть мітку NTAG або Ultralight і перевірте, що сесія повідомляє правильний UID, а readPages(0) повертає 16 байтів. Потім виконайте writePage і зчитайте сторінку знову.
  3. На MIFARE Classic пройдіть автентифікацію неправильним ключем і переконайтеся, що отримано AUTH_FAILED, а потім правильним.
  4. Скасуйте openSession() під час очікування прикладання й одразу відкрийте нову сесію.
  5. Почніть платіж під час сесії: сесія має завершитися як PAYMENT, а платіж має пройти. Заберіть мітку під час іншої сесії: сесія має завершитися як CARD_REMOVED.
  6. На телефоні вимкніть NFC і переконайтеся, що отримано NFC_DISABLED. Під час сесії переведіть застосунок у фоновий режим і переконайтеся, що сесія завершується як ACTIVITY_PAUSED.
Чогось бракує на цій сторінці?Надішліть запит на функцію. Ми додамо цю сторінку, щоб команда бачила контекст.