# NFC-мітки (/uk/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>

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

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

## Підтримувані пристрої [#supported-devices]

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

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

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

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

## Підтримувані мітки [#supported-tags]

| Тип мітки                                    | Приклади                                              |
| :------------------------------------------- | :---------------------------------------------------- |
| [NFC Type 2](#nfc-type-2)                    | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| [MIFARE Classic](#mifare-classic)            | MIFARE Classic Mini, 1K, 4K                           |
| [Смарт-картки ISO-DEP](#iso-dep-smart-cards) | MIFARE DESFire                                        |

## Як це працює [#how-it-works]

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

<Callout title="Платежі завжди мають пріоритет" type="warn">
  Пристрій має один безконтактний зчитувач, який сесія з міткою та платіж карткою не можуть використовувати спільно.
  Початок платежу через `Accept.payments.pay()` завершує вашу сесію, а `openSession()` завершується помилкою
  `READER_BUSY`, доки платіж не закінчиться. Будьте готові до того, що сесія може завершитися будь-якої миті; див. [Завершення сесії](#end-a-session).
</Callout>

## Передумови [#prerequisites]

* [Підтримуваний пристрій](#supported-devices).
* Accept SDK [установлено й ініціалізовано](../integration/sdk_integration#initialization). Виклик `isSupported()` або
  `openSession()` до `Accept.initialize` викидає `NotInitialized`.

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

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

## Інтеграція [#integrate]

<div className="fd-steps [&_h3]:fd-step">
  ### Установлення [#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")
  }
  ```

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

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

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

  ### Перевірка підтримки [#check-support]

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

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

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

  ### Відкриття сесії [#open-a-session]

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

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

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

  #### Отримання інформації про сесію [#reading-session-info]

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

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

  ### Зчитування й запис мітки [#read-and-write-a-tag]

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

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

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

  #### NFC Type 2 [#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 для команд, що не мають окремого методу, і повертає відповідь мітки.                                                           |

  ```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 і 4K. Перш ніж зчитувати або записувати блоки сектора, пройдіть автентифікацію для цього сектора.

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

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

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

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

  #### Смарт-картки ISO-DEP [#iso-dep-smart-cards]

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

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

  ### Завершення сесії [#end-a-session]

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

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

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

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

## Обробка помилок [#error-handling]

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

```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="Повторіть автентифікацію, якщо встановлено authLost" type="warn">
  За будь-якої причини може бути встановлено прапорець винятку `authLost`. Якщо його встановлено, пройдіть автентифікацію повторно, перш ніж надсилати
  наступну команду до захищеної пам'яті; на DESFire також повторно виберіть застосунок.
</Callout>

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

### Відкриття сесії [#opening-a-session]

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

### Надсилання команди [#sending-a-command]

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

### Сесію завершено [#session-over]

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

## Тайм-аути [#timeouts]

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

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

| Тайм-аут    | За замовчуванням | Максимум | Стосується                                         |
| :---------- | :--------------- | :------- | :------------------------------------------------- |
| `tapWait`   | 30 с             | 60 с     | Очікування прикладання в `openSession()`           |
| `operation` | 1 с              | 5 с      | Кожної команди на мітці                            |
| `reselect`  | 1 с              | 5 с      | Повторного вибору мітки після невдалої команди     |
| `idle`      | 10 с             | 60 с     | Часу між командами, після якого сесія завершується |

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

## Корисно знати [#good-to-know]

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

## Тестування інтеграції [#test-the-integration]

На телефоні та, якщо ви їх підтримуєте, на терміналі 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`.
