# NFC tagy (/cs/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>

Č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í [#supported-devices]

| Zařízení                              | Čtečka                                                    | Potřebné artefakty                |
| :------------------------------------ | :-------------------------------------------------------- | :-------------------------------- |
| Telefon nebo tablet s Androidem a NFC | Android NFC                                               | `accept-nfc`                      |
| Terminál Sunmi řady P                 | Bezkontaktní čtečka terminálu, přes platební službu Sunmi | `accept-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`.

<Callout title="Telefony mají několik omezení navíc" type="info">
  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.
</Callout>

## Podporované tagy [#supported-tags]

| Typ tagu                                     | Příklady                                              |
| :------------------------------------------- | :---------------------------------------------------- |
| [NFC Type 2](#nfc-type-2)                    | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| [MIFARE Classic](#mifare-classic)            | MIFARE Classic Mini, 1K, 4K                           |
| [Čipové karty ISO-DEP](#iso-dep-smart-cards) | MIFARE DESFire                                        |

## Jak to funguje [#how-it-works]

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.

<Callout title="Platby mají vždy přednost" type="warn">
  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](#end-a-session).
</Callout>

## Předpoklady [#prerequisites]

* [Podporované zařízení](#supported-devices).
* Accept SDK [nainstalované a inicializované](../integration/sdk_integration#initialization). Volání `isSupported()` nebo
  `openSession()` před `Accept.initialize` vyhodí `NotInitialized`.

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 [#integrate]

<div className="fd-steps [&_h3]:fd-step">
  ### Instalace [#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")
  }
  ```

  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:

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

  ### Ověření podpory [#check-support]

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

  ```kotlin
  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 [#open-a-session]

  Předejte aktivitu, která je v popředí a ve stavu 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()` 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 [#reading-session-info]

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

  | Pole             | Popis                                                                        |
  | :--------------- | :--------------------------------------------------------------------------- |
  | `uidHex`         | UID tagu jako hexadecimální řetězec velkými písmeny, např. `04A1B2C3D4E5F6`. |
  | `family`         | Hodnota `TagFamily`, ze které poznáte, jaké operace lze použít.              |
  | `sak`, `atqaHex` | Nízkoúrovňová identifikace vrácená při detekci tagu.                         |
  | `atsHex`         | ATS pro tagy `ISO_DEP`; jinak `null`.                                        |
  | `memoryBytes`    | Velikost uživatelské paměti, pokud ji čtečka dokáže zjistit; jinak `null`.   |
  | `versionHex`     | Odpověď na `GET_VERSION` u tagů, které ji podporují; jinak `null`.           |

  ### Čtení a zápis tagu [#read-and-write-a-tag]

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

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

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

  #### NFC Type 2 [#nfc-type-2]

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

  | Operace                  | Co 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.                                                            |

  ```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 a 4K. Než budete číst nebo zapisovat bloky sektoru, sektor autentizujte.

  | Operace                                    | Co 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`, `restore`        | Uloží výsledek operace s hodnotovým blokem do přenosového bufferu tagu. |
  | `transfer(block)`                          | Zapíše přenosový buffer do bloku.                                       |

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

  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íč:

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

  #### Čipové karty ISO-DEP [#iso-dep-smart-cards]

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

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

  ### Ukončení relace [#end-a-session]

  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í:

  ```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` | Kdy nastává                                                                                                  |
  | :-------------------- | :----------------------------------------------------------------------------------------------------------- |
  | `CLOSED`              | Vaše aplikace zavolala `close()`.                                                                            |
  | `CARD_REMOVED`        | Tag opustil pole čtečky a během časového limitu `reselect` se nevrátil.                                      |
  | `IDLE_TIMEOUT`        | Během časového limitu `idle` (ve výchozím nastavení 10 sekund) nepřišel žádný příkaz.                        |
  | `PAYMENT`             | Byla zahájena platba přes `Accept.payments.pay()`. Platby mají vždy přednost.                                |
  | `ACTIVITY_PAUSED`     | Jen telefony. Aktivita, se kterou jste relaci otevřeli, opustila popředí a Android pro ni přestal číst tagy. |
  | `READER_LOST`         | Jen 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()`.
</div>

## Zpracování chyb [#error-handling]

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ášť:

```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="Při nastaveném authLost se znovu autentizujte" type="warn">
  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.
</Callout>

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 [#opening-a-session]

| `reason`        | Kdy nastává                                                                                           | Co dělat                                                                                                                             |
| :-------------- | :---------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
| `TIMEOUT`       | Bě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_ENDED` | Bě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_DISABLED`  | Jen telefony. NFC je vypnuté v nastavení systému.                                                     | Požádejte uživatele, aby NFC zapnul.                                                                                                 |
| `VENDOR_ERROR`  | Terminá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](/support/contacts#developer-support) i s `vendorCode`. |
| `NOT_SUPPORTED` | Zařízení nemá čtečku, kterou SDK dokáže použít.                                                       | Skryjte funkci, když `isSupported()` vrátí `false`.                                                                                  |

### Odeslání příkazu [#sending-a-command]

| `reason`        | Kdy nastává                                                                                       | Co dělat                                                                                                             |
| :-------------- | :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------- |
| `TIMEOUT`       | Tag neodpověděl včas; byl znovu vybrán.                                                           | Zkuste to znovu. Pokud je nastaven `authLost`, nejprve se autentizujte.                                              |
| `NO_RESPONSE`   | Tag neodpověděl.                                                                                  | Zkuste to znovu, pokud je tag stále v poli čtečky.                                                                   |
| `AUTH_FAILED`   | Nesprávný klíč nebo heslo.                                                                        | Zkuste další klíč; tag už je znovu vybrán.                                                                           |
| `NAK`           | Tag příkaz odmítl: stránka mimo rozsah, chráněná paměť nebo nepodporovaný příkaz.                 | Opravte příkaz. Relace pokračuje.                                                                                    |
| `NOT_SUPPORTED` | Pří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_ERROR`  | Vlastní SDK čtečky ohlásilo poruchu; `vendorCode` obsahuje její kód.                              | Zkuste to znovu, poté poruchu nahlaste [podpoře pro vývojáře](/support/contacts#developer-support) i s `vendorCode`. |

### Relace skončila [#session-over]

| `reason`        | Kdy nastává                            | Co dělat                                            |
| :-------------- | :------------------------------------- | :-------------------------------------------------- |
| `CARD_REMOVED`  | Tag opustil pole čtečky během příkazu. | Požádejte o nové přiložení a otevřete novou relaci. |
| `SESSION_ENDED` | Příkaz byl odeslán po skončení relace. | Otevřete novou relaci.                              |

## Časové limity [#timeouts]

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

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

| Časový limit | Výchozí | Maximum | Platí pro                                 |
| :----------- | :------ | :------ | :---------------------------------------- |
| `tapWait`    | 30 s    | 60 s    | Čekání na přiložení v `openSession()`     |
| `operation`  | 1 s     | 5 s     | Každý příkaz na tagu                      |
| `reselect`   | 1 s     | 5 s     | Opětovný výběr tagu po neúspěšném příkazu |
| `idle`       | 10 s    | 60 s    | Doba 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 [#good-to-know]

* **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 [#test-the-integration]

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