@tapayadot/accept-react-native 1.19.1 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,36 @@ Each release must have a `## [x.y.z]` section that matches the `version` in
9
9
  `package.json`. The publish workflow refuses to release a version whose section
10
10
  is missing or empty.
11
11
 
12
+ ## [1.20.0] - 2026-10-29
13
+
14
+ Carries Accept SDK for Android 1.20.0 to Expo apps. The core, optional self-installer, Android NFC,
15
+ and Sunmi NFC artifacts are version-locked to 1.20.0.
16
+
17
+ ### Added
18
+
19
+ - **Optional NFC tag sessions through `Accept.nfc`.** Set the config plugin's `nfc` option to
20
+ `true` for phones and other Android-NFC devices, or `{ "sunmi": true }` to also include Sunmi
21
+ P-series support. Sessions read and write Mifare Classic, Type 2 and ISO-DEP tags using
22
+ `Uint8Array`, expose their selected `TagInfo`, and report their one-shot end reason. They need
23
+ `Accept.initialize()`, not an authenticated merchant or the Accept plugin app.
24
+ - **`TagSessionError` preserves NFC failures.** Its `reason`, `authLost`, and optional `vendorCode`
25
+ distinguish retryable reader/tag failures from malformed arguments and integration errors.
26
+ - **`AcceptPluginPackages` exports the production and sandbox plugin package names** for native
27
+ device-policy controllers adding the selected plugin to Android's lock-task allowlist.
28
+
29
+ ### Changed
30
+
31
+ - **Payments take the reader from an open NFC session.** Its `ended` promise resolves with
32
+ `PAYMENT`; a new session reports `READER_BUSY` until the payment releases the reader.
33
+ - **The Android AAR is now minified.** The wrapper uses only the supported public SDK, NFC, domain,
34
+ and exception APIs, so obfuscation of `com.tapaya.accept.internal` does not change its API.
35
+
36
+ ### Fixed
37
+
38
+ - **Forward-compatible backend enum values no longer discard otherwise usable responses.** Fields
39
+ the Android SDK only filters on now skip unknown values; public business, onboarding and payment
40
+ state enums keep their existing failure behavior.
41
+
12
42
  ## [1.19.1] - 2026-10-20
13
43
 
14
44
  Carries Accept SDK for Android 1.19.1 to Expo apps. Both `com.tapaya:accept` and the optional
package/README.md CHANGED
@@ -49,12 +49,16 @@ npx expo run:android
49
49
  | Option | Default | What it does |
50
50
  | --- | --- | --- |
51
51
  | `selfInstall` | `false` | Lets the SDK install the plugin app itself on terminals with no app store. See [Installing the plugin app](#installing-the-plugin-app). |
52
+ | `nfc` | `false` | Adds NFC tag sessions. Use `true` for Android NFC or `{ "sunmi": true }` to add both Android and Sunmi readers. See [NFC tag sessions](#nfc-tag-sessions). |
52
53
 
53
54
  ```json
54
55
  {
55
56
  "expo": {
56
57
  "plugins": [
57
- ["@tapayadot/accept-react-native", { "selfInstall": true }]
58
+ [
59
+ "@tapayadot/accept-react-native",
60
+ { "selfInstall": true, "nfc": { "sunmi": true } }
61
+ ]
58
62
  ]
59
63
  }
60
64
  }
@@ -65,6 +69,12 @@ npx expo run:android
65
69
  you to justify it on your listing, so leave the option off unless your terminals are
66
70
  storeless. Changing it takes effect on the next `npx expo prebuild`.
67
71
 
72
+ `nfc: true` adds `com.tapaya:accept-nfc`, Android's NFC permission, and a non-required NFC feature
73
+ declaration, so Play does not hide the app from devices without Android NFC. `nfc: { "sunmi": true }`
74
+ also adds `com.tapaya:accept-nfc-sunmi`, which embeds Sunmi PayLib and carries only
75
+ `com.sunmi.perm.CONTACTLESS_CARD`. The Sunmi option always includes the base NFC artifact; enabling
76
+ base NFC alone never includes PayLib or the Sunmi permission.
77
+
68
78
  Beyond those options the plugin only raises `minSdkVersion` to 30. Everything else the SDK needs — the
69
79
  `INTERNET`, `ACCESS_NETWORK_STATE`, and location permissions, the `<queries>` entries that
70
80
  make the plugin app visible on API 30+, and the activities that run a payment on a
@@ -120,12 +130,16 @@ Everything hangs off the default export.
120
130
  | SDK | `Accept.sdk` | `version`, `isProduction`, `deviceId()`, `minimumAmounts()` |
121
131
  | Merchant | `Accept.merchant` | `info()`, `config()`, `onboardingStatus()`, `availableCurrencies()` |
122
132
  | Payments | `Accept.payments` | `pay()`, `payAsync()`, `status()`, `cancel()`, `refund()`, `setOptions()` |
133
+ | NFC | `Accept.nfc` | `isSupported()`, `openSession()` and tag read/write sessions |
123
134
  | Plugin | `Accept.plugin` | `isInstalled()`, `install()`, `activateTerminal()`, `status()`, `updateInfo()`, `logout()` |
124
135
  | Displays | `Accept.displays` | `all()`, `customerFacing()`, `isDualScreen` |
125
136
 
126
137
  Top-level: `initialize()`, `clear()`, `setTheme()`, `getTheme()`,
127
138
  `createLocalThemeImageUri()`, `setLogLevel()`, `setDebugLoggingEnabled()`, `setLogger()`.
128
139
 
140
+ The named `AcceptPluginPackages` export contains the production and sandbox plugin package names
141
+ for device-policy controllers configuring Android lock-task allowlists.
142
+
129
143
  ### Lifecycle
130
144
 
131
145
  `Accept.state` moves `idle → initialized → authenticated`:
@@ -208,6 +222,71 @@ nfcPosition: {
208
222
  },
209
223
  ```
210
224
 
225
+ ### NFC tag sessions
226
+
227
+ Tag sessions use the device's reader directly, without the Accept plugin. They require
228
+ `Accept.initialize()`, but not `Accept.auth.authenticate()`. Enable the optional artifact first:
229
+
230
+ ```json
231
+ ["@tapayadot/accept-react-native", { "nfc": true }]
232
+ ```
233
+
234
+ Use `{ "nfc": { "sunmi": true } }` for an APK that also runs on Sunmi P-series terminals. The SDK
235
+ selects the available backend at runtime: Android NFC on phones, Sunmi's pay service on P-series.
236
+
237
+ ```ts
238
+ import Accept, { TagSessionError } from '@tapayadot/accept-react-native';
239
+
240
+ Accept.initialize();
241
+
242
+ if (Accept.nfc.isSupported()) {
243
+ const session = await Accept.nfc.openSession({ tapWaitMs: 30_000 });
244
+ session.ended.then((reason) => console.log('session ended:', reason));
245
+
246
+ try {
247
+ if (session.info.family === 'NTAG') {
248
+ const pages = await session.readPages(4); // Uint8Array, 16 bytes
249
+ console.log(Array.from(pages));
250
+ }
251
+ } catch (error) {
252
+ if (TagSessionError.is(error)) {
253
+ console.log(error.reason, error.authLost, error.vendorCode);
254
+ } else {
255
+ throw error;
256
+ }
257
+ } finally {
258
+ await session.close();
259
+ }
260
+ }
261
+ ```
262
+
263
+ `TagSession` supports Mifare Classic authentication, block and value operations; Type 2 page,
264
+ password, Ultralight C and raw-frame operations; and ISO-DEP APDUs. Binary inputs and results are
265
+ `Uint8Array`. A payment takes priority over a tag session: starting `Accept.payments.pay()` ends the
266
+ session with `PAYMENT`, and opening a session during a payment fails with `READER_BUSY`.
267
+
268
+ Writes pass straight through. Lock bits, OTP pages, Classic sector trailers, passwords, and value
269
+ operations can permanently change a tag; the caller owns any confirmation and safety policy.
270
+ When phone NFC is switched off, `isSupported()` remains true because the reader exists and
271
+ `openSession()` rejects with `NFC_DISABLED` so the app can ask the user to enable it.
272
+
273
+ If NFC was not enabled in the config plugin, `isSupported()` returns false and `openSession()`
274
+ rejects with `ERR_NFC_NOT_CONFIGURED` and rebuild instructions.
275
+
276
+ ### Plugin package names and lock task
277
+
278
+ ```ts
279
+ import { AcceptPluginPackages } from '@tapayadot/accept-react-native';
280
+
281
+ AcceptPluginPackages.PRODUCTION; // com.tapaya.accept.plugin
282
+ AcceptPluginPackages.SANDBOX; // com.tapaya.accept.plugin.sandbox
283
+ AcceptPluginPackages.ALL; // readonly array containing both
284
+ ```
285
+
286
+ These names are for a native device-policy controller calling
287
+ `DevicePolicyManager.setLockTaskPackages`. Manifest package queries make the plugin visible but do
288
+ not authorize it for lock task.
289
+
211
290
  ### Dual-screen terminals
212
291
 
213
292
  On dual-sided hardware, run the payment UI on the customer-facing panel while your app keeps
@@ -363,7 +442,8 @@ Expo inlines `EXPO_PUBLIC_*` variables into the bundle at build time.
363
442
 
364
443
  The example needs a real device with the Accept plugin app installed; an emulator has no card
365
444
  reader. Tap **Login** to mint a merchant token and authenticate, then **activateTerminal()** once
366
- before the first payment.
445
+ before the first payment. The NFC tag-session controls need only `initialize()` and a supported
446
+ Android or Sunmi reader; they do not need login, activation, or the plugin app.
367
447
 
368
448
  ## License
369
449
 
@@ -60,14 +60,41 @@ def selfInstall = Boolean.parseBoolean(
60
60
  String.valueOf(safeExtGet(selfInstallProperty, findProperty(selfInstallProperty) ?: false))
61
61
  )
62
62
 
63
+ // NFC tag sessions are optional. The source set is optional as well as the dependency: keeping
64
+ // references to `com.tapaya.accept.nfc` out of the default build is what lets an app omit the AAR
65
+ // entirely, rather than merely omitting it at runtime after Kotlin compiled against it.
66
+ def nfcProperty = "tapaya.accept.nfc"
67
+ def nfcSunmiProperty = "tapaya.accept.nfcSunmi"
68
+ def nfcSunmi = Boolean.parseBoolean(
69
+ String.valueOf(safeExtGet(nfcSunmiProperty, findProperty(nfcSunmiProperty) ?: false))
70
+ )
71
+ // Sunmi's artifact extends the base NFC artifact. Treat a manually-set Sunmi property as an
72
+ // implicit base opt-in too, matching the config plugin and avoiding an invalid bare-RN setup.
73
+ def nfc = nfcSunmi || Boolean.parseBoolean(
74
+ String.valueOf(safeExtGet(nfcProperty, findProperty(nfcProperty) ?: false))
75
+ )
76
+
77
+ android.sourceSets.main.java.srcDir(nfc ? "src/nfc/java" : "src/nfcDisabled/java")
78
+
63
79
  dependencies {
64
80
  // Published to Maven Central; no additional repository declaration is needed.
65
- implementation 'com.tapaya:accept:1.19.1'
81
+ implementation 'com.tapaya:accept:1.20.0'
66
82
 
67
83
  // Optional, and version-locked to the line above: the installer calls into `:accept` internals
68
84
  // that are not part of the public surface, so a skew between the two is a runtime failure
69
85
  // rather than a resolution error.
70
86
  if (selfInstall) {
71
- implementation 'com.tapaya:accept-installer:1.19.1'
87
+ implementation 'com.tapaya:accept-installer:1.20.0'
88
+ }
89
+
90
+ if (nfc) {
91
+ implementation 'com.tapaya:accept-nfc:1.20.0'
92
+ }
93
+
94
+ // Kept separate from the base artifact because it embeds Sunmi PayLib and merges the vendor's
95
+ // CONTACTLESS_CARD permission. `nfcSunmi` implies `nfc`, so both direct pins are present and
96
+ // version-skew checks can keep the extension line locked to the core SDK.
97
+ if (nfcSunmi) {
98
+ implementation 'com.tapaya:accept-nfc-sunmi:1.20.0'
72
99
  }
73
100
  }
@@ -39,6 +39,7 @@ class AcceptSdkReactNativeModule : Module() {
39
39
  private val paymentJobs = ConcurrentHashMap<String, Job>()
40
40
 
41
41
  private var stateJob: Job? = null
42
+ private val nfcBridge by lazy { createNfcBridge(appContext) }
42
43
 
43
44
  override fun definition() = ModuleDefinition {
44
45
  Name("AcceptSdkReactNative")
@@ -52,6 +53,7 @@ class AcceptSdkReactNativeModule : Module() {
52
53
  }
53
54
 
54
55
  OnDestroy {
56
+ nfcBridge.destroy()
55
57
  stateJob = null
56
58
  paymentJobs.clear()
57
59
  scope.cancel()
@@ -186,6 +188,62 @@ class AcceptSdkReactNativeModule : Module() {
186
188
  sdkCall { Accept.payments.refund(paymentToken, parseRefundReason(reason)).name }
187
189
  }
188
190
 
191
+ // ── NFC tag sessions ──────────────────────────────────────────────────
192
+
193
+ Function("isNfcSupported") { nfcBridge.isSupported() }
194
+
195
+ AsyncFunction("openNfcSession") Coroutine { timeouts: Map<String, Any?> ->
196
+ nfcBridge.openSession(timeouts)
197
+ }
198
+
199
+ AsyncFunction("awaitNfcSessionEnd") Coroutine { sessionId: String ->
200
+ nfcBridge.awaitSessionEnd(sessionId)
201
+ }
202
+
203
+ Function("releaseNfcSession") { sessionId: String -> nfcBridge.releaseSession(sessionId) }
204
+
205
+ AsyncFunction("nfcAuthenticateClassic") Coroutine {
206
+ sessionId: String, block: Int, keyType: String, key: List<Int> ->
207
+ nfcBridge.authenticateClassic(sessionId, block, keyType, key)
208
+ }
209
+ AsyncFunction("nfcReadBlock") Coroutine { sessionId: String, block: Int ->
210
+ nfcBridge.readBlock(sessionId, block)
211
+ }
212
+ AsyncFunction("nfcWriteBlock") Coroutine { sessionId: String, block: Int, data: List<Int> ->
213
+ nfcBridge.writeBlock(sessionId, block, data)
214
+ }
215
+ AsyncFunction("nfcIncrement") Coroutine { sessionId: String, block: Int, value: Int ->
216
+ nfcBridge.increment(sessionId, block, value)
217
+ }
218
+ AsyncFunction("nfcDecrement") Coroutine { sessionId: String, block: Int, value: Int ->
219
+ nfcBridge.decrement(sessionId, block, value)
220
+ }
221
+ AsyncFunction("nfcTransfer") Coroutine { sessionId: String, block: Int ->
222
+ nfcBridge.transfer(sessionId, block)
223
+ }
224
+ AsyncFunction("nfcRestore") Coroutine { sessionId: String, block: Int ->
225
+ nfcBridge.restore(sessionId, block)
226
+ }
227
+ AsyncFunction("nfcReadPages") Coroutine { sessionId: String, startPage: Int ->
228
+ nfcBridge.readPages(sessionId, startPage)
229
+ }
230
+ AsyncFunction("nfcWritePage") Coroutine { sessionId: String, page: Int, data: List<Int> ->
231
+ nfcBridge.writePage(sessionId, page, data)
232
+ }
233
+ AsyncFunction("nfcPasswordAuth") Coroutine { sessionId: String, password: List<Int> ->
234
+ nfcBridge.passwordAuth(sessionId, password)
235
+ }
236
+ AsyncFunction("nfcUltralightCAuth") Coroutine { sessionId: String, key: List<Int> ->
237
+ nfcBridge.ultralightCAuth(sessionId, key)
238
+ }
239
+ AsyncFunction("nfcTransceiveType2") Coroutine { sessionId: String, frame: List<Int> ->
240
+ nfcBridge.transceiveType2(sessionId, frame)
241
+ }
242
+ AsyncFunction("nfcTransmitApdu") Coroutine { sessionId: String, apdu: List<Int> ->
243
+ nfcBridge.transmitApdu(sessionId, apdu)
244
+ }
245
+ AsyncFunction("closeNfcSession") Coroutine { sessionId: String -> nfcBridge.close(sessionId) }
246
+
189
247
  // ── Plugin ──────────────────────────────────────────────────────────────
190
248
 
191
249
  Function("isPluginInstalled") { sdkCallSync { Accept.plugin.isInstalled() } }
@@ -0,0 +1,35 @@
1
+ package com.tapaya.acceptsdkreactnative
2
+
3
+ import expo.modules.kotlin.AppContext
4
+
5
+ /**
6
+ * Dependency-free boundary between the Expo module and the optional NFC artifacts.
7
+ *
8
+ * Gradle supplies exactly one implementation: the real bridge when NFC is opted in, or a stub
9
+ * when it is not. Only bridge-safe primitives cross this interface so the default build never
10
+ * loads a class whose signature mentions `com.tapaya.accept.nfc`.
11
+ */
12
+ internal interface NfcBridge {
13
+ fun isSupported(): Boolean
14
+ suspend fun openSession(timeouts: Map<String, Any?>): Map<String, Any?>
15
+ suspend fun awaitSessionEnd(sessionId: String): String
16
+ fun releaseSession(sessionId: String)
17
+ suspend fun authenticateClassic(sessionId: String, block: Int, keyType: String, key: List<Int>)
18
+ suspend fun readBlock(sessionId: String, block: Int): List<Int>
19
+ suspend fun writeBlock(sessionId: String, block: Int, data: List<Int>)
20
+ suspend fun increment(sessionId: String, block: Int, value: Int)
21
+ suspend fun decrement(sessionId: String, block: Int, value: Int)
22
+ suspend fun transfer(sessionId: String, block: Int)
23
+ suspend fun restore(sessionId: String, block: Int)
24
+ suspend fun readPages(sessionId: String, startPage: Int): List<Int>
25
+ suspend fun writePage(sessionId: String, page: Int, data: List<Int>)
26
+ suspend fun passwordAuth(sessionId: String, password: List<Int>): List<Int>
27
+ suspend fun ultralightCAuth(sessionId: String, key: List<Int>)
28
+ suspend fun transceiveType2(sessionId: String, frame: List<Int>): List<Int>
29
+ suspend fun transmitApdu(sessionId: String, apdu: List<Int>): List<Int>
30
+ suspend fun close(sessionId: String)
31
+ fun destroy()
32
+ }
33
+
34
+ /** Supplied by the selected `src/nfc*` source set. */
35
+ internal fun createNfcBridge(appContext: AppContext): NfcBridge = PlatformNfcBridge(appContext)
@@ -0,0 +1,215 @@
1
+ package com.tapaya.acceptsdkreactnative
2
+
3
+ import androidx.activity.ComponentActivity
4
+ import androidx.lifecycle.Lifecycle
5
+ import com.tapaya.accept.Accept
6
+ import com.tapaya.accept.AcceptException
7
+ import com.tapaya.accept.nfc.MifareKeyType
8
+ import com.tapaya.accept.nfc.TagInfo
9
+ import com.tapaya.accept.nfc.TagSession
10
+ import com.tapaya.accept.nfc.TagSessionException
11
+ import com.tapaya.accept.nfc.TagTimeouts
12
+ import com.tapaya.accept.nfc.nfc
13
+ import expo.modules.kotlin.AppContext
14
+ import expo.modules.kotlin.exception.CodedException
15
+ import java.util.concurrent.ConcurrentHashMap
16
+ import java.util.concurrent.atomic.AtomicLong
17
+ import kotlin.time.Duration.Companion.milliseconds
18
+ import kotlinx.coroutines.CancellationException
19
+ import kotlinx.coroutines.CoroutineScope
20
+ import kotlinx.coroutines.Dispatchers
21
+ import kotlinx.coroutines.SupervisorJob
22
+ import kotlinx.coroutines.cancel
23
+ import kotlinx.coroutines.flow.filterNotNull
24
+ import kotlinx.coroutines.flow.first
25
+ import kotlinx.coroutines.launch
26
+ import org.json.JSONObject
27
+
28
+ /** Real NFC bridge, compiled only when `tapaya.accept.nfc=true`. */
29
+ internal class PlatformNfcBridge(
30
+ private val appContext: AppContext,
31
+ ) : NfcBridge {
32
+ private val sessions = ConcurrentHashMap<String, TagSession>()
33
+ private val nextSessionId = AtomicLong()
34
+ private val cleanupScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
35
+
36
+ override fun isSupported(): Boolean = nfcCallSync { Accept.nfc.isSupported() }
37
+
38
+ override suspend fun openSession(timeouts: Map<String, Any?>): Map<String, Any?> = nfcCall {
39
+ val activity = appContext.currentActivity as? ComponentActivity
40
+ ?: throw CodedException(
41
+ "ERR_NFC_ACTIVITY_UNAVAILABLE",
42
+ "NFC tag sessions need the app's foreground Android activity, but none is available.",
43
+ null,
44
+ )
45
+ if (!activity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
46
+ throw CodedException(
47
+ "ERR_NFC_ACTIVITY_UNAVAILABLE",
48
+ "NFC tag sessions need the foreground Android activity to be resumed. " +
49
+ "Wait until the app is active, then call openSession() again.",
50
+ null,
51
+ )
52
+ }
53
+ val session = try {
54
+ Accept.nfc.openSession(activity, parseTimeouts(timeouts))
55
+ } catch (error: IllegalStateException) {
56
+ throw CodedException(
57
+ "ERR_NFC_ACTIVITY_UNAVAILABLE",
58
+ error.message ?: "The Android activity must be resumed before opening an NFC session.",
59
+ error,
60
+ )
61
+ }
62
+ val id = "nfc-${nextSessionId.incrementAndGet()}"
63
+ sessions[id] = session
64
+ mapOf("sessionId" to id, "info" to session.info.serialize())
65
+ }
66
+
67
+ override suspend fun awaitSessionEnd(sessionId: String): String =
68
+ nfcCall { session(sessionId).ended.filterNotNull().first().name }
69
+
70
+ override fun releaseSession(sessionId: String) {
71
+ // `ended` is the authority: never let a stray JavaScript release make a live reader
72
+ // unreachable. The JS wrapper calls this only after awaitSessionEnd settles.
73
+ sessions[sessionId]?.takeIf { it.ended.value != null }?.let { sessions.remove(sessionId, it) }
74
+ }
75
+
76
+ override suspend fun authenticateClassic(sessionId: String, block: Int, keyType: String, key: List<Int>) =
77
+ nfcCall {
78
+ val parsedKeyType = MifareKeyType.entries.firstOrNull { it.name == keyType }
79
+ ?: invalidArgument("unsupported Mifare key type: $keyType")
80
+ session(sessionId).authenticateClassic(block, parsedKeyType, key.toByteArray("key"))
81
+ }
82
+
83
+ override suspend fun readBlock(sessionId: String, block: Int): List<Int> =
84
+ nfcCall { session(sessionId).readBlock(block).toUnsignedList() }
85
+
86
+ override suspend fun writeBlock(sessionId: String, block: Int, data: List<Int>) =
87
+ nfcCall { session(sessionId).writeBlock(block, data.toByteArray("data")) }
88
+
89
+ override suspend fun increment(sessionId: String, block: Int, value: Int) =
90
+ nfcCall { session(sessionId).increment(block, value) }
91
+
92
+ override suspend fun decrement(sessionId: String, block: Int, value: Int) =
93
+ nfcCall { session(sessionId).decrement(block, value) }
94
+
95
+ override suspend fun transfer(sessionId: String, block: Int) =
96
+ nfcCall { session(sessionId).transfer(block) }
97
+
98
+ override suspend fun restore(sessionId: String, block: Int) =
99
+ nfcCall { session(sessionId).restore(block) }
100
+
101
+ override suspend fun readPages(sessionId: String, startPage: Int): List<Int> =
102
+ nfcCall { session(sessionId).readPages(startPage).toUnsignedList() }
103
+
104
+ override suspend fun writePage(sessionId: String, page: Int, data: List<Int>) =
105
+ nfcCall { session(sessionId).writePage(page, data.toByteArray("data")) }
106
+
107
+ override suspend fun passwordAuth(sessionId: String, password: List<Int>): List<Int> =
108
+ nfcCall { session(sessionId).passwordAuth(password.toByteArray("password")).toUnsignedList() }
109
+
110
+ override suspend fun ultralightCAuth(sessionId: String, key: List<Int>) =
111
+ nfcCall { session(sessionId).ultralightCAuth(key.toByteArray("key")) }
112
+
113
+ override suspend fun transceiveType2(sessionId: String, frame: List<Int>): List<Int> =
114
+ nfcCall { session(sessionId).transceiveType2(frame.toByteArray("frame")).toUnsignedList() }
115
+
116
+ override suspend fun transmitApdu(sessionId: String, apdu: List<Int>): List<Int> =
117
+ nfcCall { session(sessionId).transmitApdu(apdu.toByteArray("apdu")).toUnsignedList() }
118
+
119
+ override suspend fun close(sessionId: String) = nfcCall {
120
+ sessions[sessionId]?.close()
121
+ Unit
122
+ }
123
+
124
+ override fun destroy() {
125
+ val active = sessions.values.toList()
126
+ sessions.clear()
127
+ cleanupScope.launch {
128
+ try {
129
+ active.forEach { session -> runCatching { session.close() } }
130
+ } finally {
131
+ cleanupScope.cancel()
132
+ }
133
+ }
134
+ }
135
+
136
+ private fun session(id: String): TagSession = sessions[id]
137
+ ?: throw TagSessionException(
138
+ reason = com.tapaya.accept.nfc.TagErrorReason.SESSION_ENDED,
139
+ message = "the NFC tag session has ended",
140
+ )
141
+ }
142
+
143
+ private fun TagInfo.serialize(): Map<String, Any?> = mapOf(
144
+ "uidHex" to uidHex,
145
+ "family" to family.name,
146
+ "sak" to sak,
147
+ "atqaHex" to atqaHex,
148
+ "atsHex" to atsHex,
149
+ "memoryBytes" to memoryBytes,
150
+ "versionHex" to versionHex,
151
+ )
152
+
153
+ private fun parseTimeouts(values: Map<String, Any?>): TagTimeouts = try {
154
+ TagTimeouts(
155
+ tapWait = values.duration("tapWaitMs", 30_000),
156
+ operation = values.duration("operationMs", 1_000),
157
+ reselect = values.duration("reselectMs", 1_000),
158
+ idle = values.duration("idleMs", 10_000),
159
+ )
160
+ } catch (error: IllegalArgumentException) {
161
+ invalidArgument(error.message ?: "invalid NFC timeout", error)
162
+ }
163
+
164
+ private fun Map<String, Any?>.duration(key: String, defaultMs: Long) = when (val value = this[key]) {
165
+ null -> defaultMs.milliseconds
166
+ is Number -> {
167
+ val number = value.toDouble()
168
+ if (!number.isFinite() || number % 1.0 != 0.0) invalidArgument("$key must be a whole number of milliseconds")
169
+ number.toLong().milliseconds
170
+ }
171
+ else -> invalidArgument("$key must be a number of milliseconds")
172
+ }
173
+
174
+ private fun List<Int>.toByteArray(name: String): ByteArray = ByteArray(size) { index ->
175
+ val value = this[index]
176
+ if (value !in 0..255) invalidArgument("$name[$index] must be between 0 and 255")
177
+ value.toByte()
178
+ }
179
+
180
+ private fun ByteArray.toUnsignedList(): List<Int> = map { it.toInt() and 0xFF }
181
+
182
+ private fun invalidArgument(message: String, cause: Throwable? = null): Nothing =
183
+ throw CodedException("ERR_INVALID_ARGUMENT", message, cause)
184
+
185
+ private suspend fun <T> nfcCall(block: suspend () -> T): T = try {
186
+ block()
187
+ } catch (error: CancellationException) {
188
+ throw error
189
+ } catch (error: AcceptException) {
190
+ throw AcceptCodedException(error)
191
+ } catch (error: TagSessionException) {
192
+ throw TagSessionCodedException(error)
193
+ } catch (error: IllegalArgumentException) {
194
+ invalidArgument(error.message ?: "invalid NFC argument", error)
195
+ }
196
+
197
+ private fun <T> nfcCallSync(block: () -> T): T = try {
198
+ block()
199
+ } catch (error: AcceptException) {
200
+ throw AcceptCodedException(error)
201
+ }
202
+
203
+ private class TagSessionCodedException(error: TagSessionException) : CodedException(
204
+ "TagSessionException",
205
+ JSONObject(
206
+ mapOf(
207
+ "code" to "TagSessionException",
208
+ "message" to (error.message ?: error.reason.name),
209
+ "reason" to error.reason.name,
210
+ "authLost" to error.authLost,
211
+ "vendorCode" to error.vendorCode,
212
+ ),
213
+ ).toString(),
214
+ error,
215
+ )
@@ -0,0 +1,38 @@
1
+ package com.tapaya.acceptsdkreactnative
2
+
3
+ import expo.modules.kotlin.AppContext
4
+ import expo.modules.kotlin.exception.CodedException
5
+
6
+ /** NFC surface used when the host app did not opt into either optional NFC artifact. */
7
+ internal class PlatformNfcBridge(
8
+ @Suppress("UNUSED_PARAMETER") appContext: AppContext,
9
+ ) : NfcBridge {
10
+ override fun isSupported(): Boolean = false
11
+
12
+ override suspend fun openSession(timeouts: Map<String, Any?>): Map<String, Any?> = notConfigured()
13
+ override suspend fun awaitSessionEnd(sessionId: String): String = notConfigured()
14
+ override fun releaseSession(sessionId: String) = Unit
15
+ override suspend fun authenticateClassic(sessionId: String, block: Int, keyType: String, key: List<Int>) = notConfigured()
16
+ override suspend fun readBlock(sessionId: String, block: Int): List<Int> = notConfigured()
17
+ override suspend fun writeBlock(sessionId: String, block: Int, data: List<Int>) = notConfigured()
18
+ override suspend fun increment(sessionId: String, block: Int, value: Int) = notConfigured()
19
+ override suspend fun decrement(sessionId: String, block: Int, value: Int) = notConfigured()
20
+ override suspend fun transfer(sessionId: String, block: Int) = notConfigured()
21
+ override suspend fun restore(sessionId: String, block: Int) = notConfigured()
22
+ override suspend fun readPages(sessionId: String, startPage: Int): List<Int> = notConfigured()
23
+ override suspend fun writePage(sessionId: String, page: Int, data: List<Int>) = notConfigured()
24
+ override suspend fun passwordAuth(sessionId: String, password: List<Int>): List<Int> = notConfigured()
25
+ override suspend fun ultralightCAuth(sessionId: String, key: List<Int>) = notConfigured()
26
+ override suspend fun transceiveType2(sessionId: String, frame: List<Int>): List<Int> = notConfigured()
27
+ override suspend fun transmitApdu(sessionId: String, apdu: List<Int>): List<Int> = notConfigured()
28
+ override suspend fun close(sessionId: String) = notConfigured()
29
+ override fun destroy() = Unit
30
+
31
+ private fun notConfigured(): Nothing = throw CodedException(
32
+ "ERR_NFC_NOT_CONFIGURED",
33
+ "NFC tag sessions are not included in this build. Set `nfc: true` (or " +
34
+ "`nfc: { sunmi: true }`) on the @tapayadot/accept-react-native config plugin, " +
35
+ "then run `npx expo prebuild --clean` and rebuild the Android app.",
36
+ null,
37
+ )
38
+ }
package/build/Accept.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AcceptDisplay, AcceptLogEntry, AcceptLogLevel, AcceptSubscription, AcceptTheme, ActivateTerminalResult, InitializeOptions, MerchantConfig, MerchantInformation, MinimumAmount, OnboardingStatus, PayResult, PaymentEvent, PaymentOptions, PaymentRequest, PaymentState, PaymentStatus, PaymentSubscription, PluginStatus, PluginUpdateInfo, RefundReason, SdkState } from "./Accept.types";
1
+ import type { AcceptDisplay, AcceptLogEntry, AcceptLogLevel, AcceptSubscription, AcceptTheme, ActivateTerminalResult, InitializeOptions, MerchantConfig, MerchantInformation, MinimumAmount, OnboardingStatus, PayResult, PaymentEvent, PaymentOptions, PaymentRequest, PaymentState, PaymentStatus, PaymentSubscription, PluginStatus, PluginUpdateInfo, RefundReason, SdkState, TagSession, TagTimeouts } from "./Accept.types";
2
2
  /**
3
3
  * Entry point to the Accept SDK. Mirrors the `Accept` object in the Accept SDK for Android, so the
4
4
  * Android reference documentation applies unchanged.
@@ -137,6 +137,12 @@ export declare const Accept: {
137
137
  */
138
138
  refund(paymentToken: string, reason: RefundReason): Promise<PaymentState>;
139
139
  };
140
+ nfc: {
141
+ /** Whether this device has a reader the configured NFC backend can use. */
142
+ isSupported(): boolean;
143
+ /** Waits for a tag and opens a session on it using the foreground Android activity. */
144
+ openSession(timeouts?: TagTimeouts): Promise<TagSession>;
145
+ };
140
146
  plugin: {
141
147
  /**
142
148
  * Whether the plugin app is installed on this device.
@@ -1 +1 @@
1
- {"version":3,"file":"Accept.d.ts","sourceRoot":"","sources":["../src/Accept.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,cAAc,EACd,cAAc,EACd,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EACT,MAAM,gBAAgB,CAAC;AA4UxB;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM;;QAxUjB;;;;;WAKG;oCACyB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;;;QASlD,iGAAiG;0BAClF,MAAM;QAIrB;;;;WAIG;+BACiB,OAAO;QAI3B;;;;;;;;WAQG;oBACS,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;QAIlC;;;;WAIG;0BACe,OAAO,CAAC,aAAa,EAAE,CAAC;;;QAS1C;;;;WAIG;gBACK,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;QAI3C;;;;WAIG;kBACO,OAAO,CAAC,cAAc,CAAC;QAIjC;;;;WAIG;4BACiB,OAAO,CAAC,gBAAgB,CAAC;QAI7C;;;;WAIG;+BACoB,OAAO,CAAC,MAAM,EAAE,CAAC;;;QASxC;;;;;;WAMG;4BACiB,cAAc,GAAG,IAAI;QAIzC;;;;;;;;;;;;;;;;WAgBG;qBAEQ,cAAc,YACb,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GACtC,mBAAmB;QAqBtB;;;;;;;WAOG;0BACe,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC;QAUrD;;;;;WAKG;6BACkB,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;QAIpD;;;;;;WAMG;6BACkB,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAInD;;;;;;WAMG;6BACkB,MAAM,UAAU,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;;;QASzE;;;;WAIG;uBACY,OAAO;QAItB;;;;;;;;;;;;;;;;;;;;WAoBG;mBACQ,OAAO,CAAC,IAAI,CAAC;QAIxB;;;;;;;WAOG;4BACiB,OAAO,CAAC,sBAAsB,CAAC;QAInD;;;;WAIG;kBACO,OAAO,CAAC,YAAY,CAAC;QAI/B;;;;;;;;;WASG;sBACW,OAAO,CAAC,gBAAgB,CAAC;QAIvC;;;;WAIG;kBACO,OAAO,CAAC,IAAI,CAAC;;;QAYvB,iEAAiE;eAC1D,aAAa,EAAE;QAItB;;;WAGG;0BACe,aAAa,GAAG,IAAI;QAItC,gEAAgE;+BAC5C,OAAO;;IAgC3B;;;;;OAKG;yBACiB,iBAAiB,GAAQ,IAAI;IAIjD,yCAAyC;oBAC5B,QAAQ;IAIrB;;;OAGG;+BACwB,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,kBAAkB;IAIzE;;;;;OAKG;aACM,OAAO,CAAC,IAAI,CAAC;IAItB;;;OAGG;oCAC6B,OAAO,GAAG,IAAI;IAI9C;;;OAGG;uBACgB,cAAc,GAAG,IAAI;IAIxC;;;OAGG;wBACiB,CAAC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAMnE;;;;;;OAMG;oBACa,WAAW,GAAG,IAAI,GAAG,IAAI;IAIzC;;;OAGG;gBACS,WAAW,GAAG,IAAI;IAI9B;;;;;;;;;;;OAWG;qCAEO,MAAM,kBACC,MAAM,GACpB,MAAM;CAKV,CAAC"}
1
+ {"version":3,"file":"Accept.d.ts","sourceRoot":"","sources":["../src/Accept.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,cAAc,EACd,cAAc,EACd,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EAER,UAAU,EAEV,WAAW,EAEZ,MAAM,gBAAgB,CAAC;AAodxB;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM;;QA/cjB;;;;;WAKG;oCACyB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;;;QASlD,iGAAiG;0BAClF,MAAM;QAIrB;;;;WAIG;+BACiB,OAAO;QAI3B;;;;;;;;WAQG;oBACS,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;QAIlC;;;;WAIG;0BACe,OAAO,CAAC,aAAa,EAAE,CAAC;;;QAS1C;;;;WAIG;gBACK,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;QAI3C;;;;WAIG;kBACO,OAAO,CAAC,cAAc,CAAC;QAIjC;;;;WAIG;4BACiB,OAAO,CAAC,gBAAgB,CAAC;QAI7C;;;;WAIG;+BACoB,OAAO,CAAC,MAAM,EAAE,CAAC;;;QASxC;;;;;;WAMG;4BACiB,cAAc,GAAG,IAAI;QAIzC;;;;;;;;;;;;;;;;WAgBG;qBAEQ,cAAc,YACb,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GACtC,mBAAmB;QAqBtB;;;;;;;WAOG;0BACe,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC;QAUrD;;;;;WAKG;6BACkB,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;QAIpD;;;;;;WAMG;6BACkB,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAInD;;;;;;WAMG;6BACkB,MAAM,UAAU,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;;;QA8HzE,2EAA2E;uBAC5D,OAAO;QAItB,uFAAuF;+BAC3D,WAAW,GAAQ,OAAO,CAAC,UAAU,CAAC;;;QAYlE;;;;WAIG;uBACY,OAAO;QAItB;;;;;;;;;;;;;;;;;;;;WAoBG;mBACQ,OAAO,CAAC,IAAI,CAAC;QAIxB;;;;;;;WAOG;4BACiB,OAAO,CAAC,sBAAsB,CAAC;QAInD;;;;WAIG;kBACO,OAAO,CAAC,YAAY,CAAC;QAI/B;;;;;;;;;WASG;sBACW,OAAO,CAAC,gBAAgB,CAAC;QAIvC;;;;WAIG;kBACO,OAAO,CAAC,IAAI,CAAC;;;QAYvB,iEAAiE;eAC1D,aAAa,EAAE;QAItB;;;WAGG;0BACe,aAAa,GAAG,IAAI;QAItC,gEAAgE;+BAC5C,OAAO;;IAiC3B;;;;;OAKG;yBACiB,iBAAiB,GAAQ,IAAI;IAIjD,yCAAyC;oBAC5B,QAAQ;IAIrB;;;OAGG;+BACwB,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,kBAAkB;IAIzE;;;;;OAKG;aACM,OAAO,CAAC,IAAI,CAAC;IAItB;;;OAGG;oCAC6B,OAAO,GAAG,IAAI;IAI9C;;;OAGG;uBACgB,cAAc,GAAG,IAAI;IAIxC;;;OAGG;wBACiB,CAAC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAMnE;;;;;;OAMG;oBACa,WAAW,GAAG,IAAI,GAAG,IAAI;IAIzC;;;OAGG;gBACS,WAAW,GAAG,IAAI;IAI9B;;;;;;;;;;;OAWG;qCAEO,MAAM,kBACC,MAAM,GACpB,MAAM;CAKV,CAAC"}