@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 +30 -0
- package/README.md +82 -2
- package/android/build.gradle +29 -2
- package/android/src/main/java/com/tapaya/acceptsdkreactnative/AcceptSdkReactNativeModule.kt +58 -0
- package/android/src/main/java/com/tapaya/acceptsdkreactnative/NfcBridge.kt +35 -0
- package/android/src/nfc/java/com/tapaya/acceptsdkreactnative/PlatformNfcBridge.kt +215 -0
- package/android/src/nfcDisabled/java/com/tapaya/acceptsdkreactnative/PlatformNfcBridge.kt +38 -0
- package/build/Accept.d.ts +7 -1
- package/build/Accept.d.ts.map +1 -1
- package/build/Accept.js +84 -0
- package/build/Accept.js.map +1 -1
- package/build/Accept.types.d.ts +59 -0
- package/build/Accept.types.d.ts.map +1 -1
- package/build/Accept.types.js.map +1 -1
- package/build/AcceptPluginPackages.d.ts +7 -0
- package/build/AcceptPluginPackages.d.ts.map +1 -0
- package/build/AcceptPluginPackages.js +11 -0
- package/build/AcceptPluginPackages.js.map +1 -0
- package/build/AcceptSdkReactNativeModule.d.ts +23 -1
- package/build/AcceptSdkReactNativeModule.d.ts.map +1 -1
- package/build/AcceptSdkReactNativeModule.js.map +1 -1
- package/build/TagSessionError.d.ts +13 -0
- package/build/TagSessionError.d.ts.map +1 -0
- package/build/TagSessionError.js +53 -0
- package/build/TagSessionError.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/package.json +12 -1
- package/plugin/withAcceptSdk.js +28 -2
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
|
-
[
|
|
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
|
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
package/build/Accept.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|