@icedevml/react-native-host-card-emulation 0.9.2 → 1.0.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/Package.swift +87 -0
- package/README.md +216 -152
- package/RTNHCEModuleIOS.podspec +1 -1
- package/android/build.gradle +5 -17
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/BinaryUtils.java +11 -5
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEManager.java +558 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +22 -299
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +232 -166
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +5 -4
- package/ios/RTNHCEModuleIOS.mm +69 -48
- package/ios/RTNHCEModuleIOSImpl.swift +429 -100
- package/js/NativeHCEModule.ts +48 -130
- package/js/background.ts +168 -0
- package/js/index.ts +49 -0
- package/js/internal.ts +218 -0
- package/js/session.ts +192 -0
- package/js/types.ts +179 -0
- package/package.json +19 -3
- package/react-native-spm-prefix.h +10 -0
- package/android/src/main/AndroidMainfest.xml +0 -3
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +0 -7
- package/js/hceBackground.ts +0 -68
package/Package.swift
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// swift-tools-version: 6.0
|
|
2
|
+
//
|
|
3
|
+
// Swift Package Manager support for React Native's SwiftPM autolinking (react-native >= 0.87, experimental).
|
|
4
|
+
// CocoaPods users are unaffected, see RTNHCEModuleIOS.podspec.
|
|
5
|
+
//
|
|
6
|
+
// This package mixes Swift and Objective-C(++) sources, which Swift Package Manager can't compile
|
|
7
|
+
// in a single target. The ObjC++ TurboModule (RTNHCEModuleIOS.mm) calls into the Swift implementation
|
|
8
|
+
// (RTNHCEModuleIOSImpl.swift), so the ObjC target depends on the Swift target and never the reverse.
|
|
9
|
+
//
|
|
10
|
+
// The two React Native packages are resolved by the autolinker relative to
|
|
11
|
+
// build/generated/autolinking/libs/<package> in the app.
|
|
12
|
+
import PackageDescription
|
|
13
|
+
|
|
14
|
+
let reactHeaders: [Target.Dependency] = [
|
|
15
|
+
.product(name: "ReactHeaders", package: "ReactNative"),
|
|
16
|
+
.product(name: "ReactNativeHeaders", package: "ReactNative"),
|
|
17
|
+
.product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"),
|
|
18
|
+
.product(name: "ReactAppHeaders", package: "React-GeneratedCode"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
let package = Package(
|
|
22
|
+
name: "ReactNativeHostCardEmulation",
|
|
23
|
+
// React Native's minimum, the generated Autolinked aggregate never targets a lower version.
|
|
24
|
+
// HCE itself requires iOS 17.4, which is checked at runtime.
|
|
25
|
+
platforms: [.iOS("15.1")],
|
|
26
|
+
products: [
|
|
27
|
+
.library(name: "ReactNativeHostCardEmulation", targets: ["ReactNativeHostCardEmulationSwift", "ReactNativeHostCardEmulationObjC"]),
|
|
28
|
+
],
|
|
29
|
+
dependencies: [
|
|
30
|
+
.package(name: "ReactNative", path: "../../../../xcframeworks"),
|
|
31
|
+
.package(name: "React-GeneratedCode", path: "../../../ios"),
|
|
32
|
+
],
|
|
33
|
+
targets: [
|
|
34
|
+
.target(
|
|
35
|
+
name: "ReactNativeHostCardEmulationSwift",
|
|
36
|
+
dependencies: reactHeaders,
|
|
37
|
+
path: ".",
|
|
38
|
+
exclude: [
|
|
39
|
+
"ios/RTNHCEModuleIOS-Bridging-Header.h",
|
|
40
|
+
"ios/RTNHCEModuleIOS.h",
|
|
41
|
+
"ios/RTNHCEModuleIOS.mm",
|
|
42
|
+
],
|
|
43
|
+
sources: [
|
|
44
|
+
"ios/RTNHCEModuleIOSImpl.swift",
|
|
45
|
+
],
|
|
46
|
+
linkerSettings: [
|
|
47
|
+
.linkedFramework("CoreNFC"),
|
|
48
|
+
.linkedFramework("UIKit"),
|
|
49
|
+
.linkedFramework("Foundation"),
|
|
50
|
+
]
|
|
51
|
+
),
|
|
52
|
+
.target(
|
|
53
|
+
name: "ReactNativeHostCardEmulationObjC",
|
|
54
|
+
dependencies: reactHeaders + ["ReactNativeHostCardEmulationSwift"],
|
|
55
|
+
path: ".",
|
|
56
|
+
sources: [
|
|
57
|
+
"ios/RTNHCEModuleIOS-Bridging-Header.h",
|
|
58
|
+
"ios/RTNHCEModuleIOS.h",
|
|
59
|
+
"ios/RTNHCEModuleIOS.mm",
|
|
60
|
+
],
|
|
61
|
+
publicHeadersPath: "ios",
|
|
62
|
+
cSettings: [
|
|
63
|
+
// SwiftPM autolinking is New Architecture only; CocoaPods gets this from install_modules_dependencies.
|
|
64
|
+
.define("RCT_NEW_ARCH_ENABLED", to: "1"),
|
|
65
|
+
.headerSearchPath("ios"),
|
|
66
|
+
.headerSearchPath("."),
|
|
67
|
+
.unsafeFlags(["-include", "react-native-spm-prefix.h"]),
|
|
68
|
+
],
|
|
69
|
+
cxxSettings: [
|
|
70
|
+
.define("RCT_NEW_ARCH_ENABLED", to: "1"),
|
|
71
|
+
.headerSearchPath("ios"),
|
|
72
|
+
.headerSearchPath("."),
|
|
73
|
+
.unsafeFlags(["-include", "react-native-spm-prefix.h"]),
|
|
74
|
+
.define("DEBUG", .when(configuration: .debug)),
|
|
75
|
+
.define("NDEBUG", .when(configuration: .release)),
|
|
76
|
+
],
|
|
77
|
+
linkerSettings: [
|
|
78
|
+
.linkedFramework("CoreNFC"),
|
|
79
|
+
.linkedFramework("UIKit"),
|
|
80
|
+
.linkedFramework("Foundation"),
|
|
81
|
+
.linkedFramework("CoreGraphics"),
|
|
82
|
+
]
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
swiftLanguageModes: [.v5],
|
|
86
|
+
cxxLanguageStandard: .cxx20
|
|
87
|
+
)
|
package/README.md
CHANGED
|
@@ -63,6 +63,9 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
63
63
|
Replace `F001020304`, `F00102030405`, `F0010203040506` with the ISO7816 AIDs that you want to support.
|
|
64
64
|
3. In the `ios/` app's subdirectory, run `bundle install && bundle exec pod install`.
|
|
65
65
|
|
|
66
|
+
Alternatively, with React Native 0.87+ you can use the experimental Swift Package Manager integration instead of
|
|
67
|
+
CocoaPods - the package ships a `Package.swift` manifest for React Native's SwiftPM autolinking.
|
|
68
|
+
|
|
66
69
|
### Android
|
|
67
70
|
1. In `android/app/src/main/AndroidManifest.xml` add:
|
|
68
71
|
```diff
|
|
@@ -114,203 +117,264 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
114
117
|
```
|
|
115
118
|
Replace `F001020304`, `F00102030405`, `F0010203040506` with the ISO7816 AIDs that you want to support.
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
Always keep `android:requireDeviceUnlock` set to `false`. With `true`, Android displays its own native prompts
|
|
121
|
+
whenever a reader is tapped while the phone is locked (e.g. "unlock the phone to complete the payment"), which
|
|
122
|
+
confuses the users, especially if your app isn't related to payments. Control the lock screen behavior with
|
|
123
|
+
the `allowWhenLocked` option instead (see [Interactions while the device is locked](#android-interactions-while-the-device-is-locked)).
|
|
121
124
|
|
|
122
125
|
For `android:requireDeviceScreenOn` - set it according to your use case.
|
|
123
126
|
|
|
124
|
-
## API
|
|
125
|
-
|
|
126
|
-
This module provides a uniform low-level HCE API for both mobile platforms.
|
|
127
|
+
## API
|
|
127
128
|
|
|
128
129
|
> [!NOTE]
|
|
129
|
-
>
|
|
130
|
-
> Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
|
|
130
|
+
> Version 1.0 introduced a new API, which is not compatible with 0.x versions. See [Migrating from 0.x](#migrating-from-0x).
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
```typescript
|
|
133
|
+
import HCE from '@icedevml/react-native-host-card-emulation';
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The library guarantees that **every C-APDU is responded to exactly once**: your handler returns the R-APDU and the library
|
|
137
|
+
sends it. If the handler throws an exception or returns an invalid response, the reader receives the `6F00` status word
|
|
138
|
+
(no precise diagnosis). Responses are bound to the C-APDU they were produced for, so a late response can never be sent
|
|
139
|
+
as a reply to a different command.
|
|
140
|
+
|
|
141
|
+
All the handlers of a given session are called sequentially - the next event is never delivered before the handler
|
|
142
|
+
of the previous one has finished (including the returned promise). A handler that doesn't finish within the handler
|
|
143
|
+
timeout (10 seconds by default) is abandoned: its result is ignored, `onError` receives an `err_handler_timeout` error
|
|
144
|
+
and the next event is delivered (a timed out `onApdu` is responded to with `6F00`). This way, a single stuck promise
|
|
145
|
+
can't block the session or the background interaction forever.
|
|
146
|
+
|
|
147
|
+
> [!WARNING]
|
|
148
|
+
> Don't log the APDUs in production apps. On Android, `console.log()` ends up in the system log (logcat) also in release
|
|
149
|
+
> builds, and the APDUs might carry sensitive data (tokens, credentials, personal data). The demo app does log them,
|
|
150
|
+
> but only for the demo purposes.
|
|
151
|
+
|
|
152
|
+
See [Source Code for Demo Application: NDEF NFC Type 4 Tag Emulator (for iOS/Android)](https://github.com/icedevml/react-native-host-card-emulation/tree/master/packages/demo-ndef-app) for a complete example.
|
|
153
|
+
|
|
154
|
+
### Checking device capabilities
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const caps = await HCE.getCapabilities();
|
|
158
|
+
// {
|
|
159
|
+
// platform: 'android' | 'ios',
|
|
160
|
+
// hceSupported: boolean, // device & OS support HCE at all
|
|
161
|
+
// nfcEnabled: boolean, // NFC is turned on (Android) / available (iOS)
|
|
162
|
+
// eligible: boolean, // iOS: CardSession.isEligible (region, entitlement); Android: same as hceSupported
|
|
163
|
+
// backgroundSupported: boolean, // registerBackgroundHandler() is supported (Android)
|
|
164
|
+
// presentmentSupported: boolean, // acquireExclusiveNFC() is supported (iOS)
|
|
165
|
+
// }
|
|
166
|
+
```
|
|
134
167
|
|
|
135
168
|
### Quick start guide
|
|
136
169
|
|
|
137
|
-
1.
|
|
170
|
+
1. When the user indicates that they want to perform the HCE action, begin the session and start the emulation:
|
|
138
171
|
```typescript
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
const session = await HCE.beginSession({
|
|
173
|
+
// required: return the R-APDU (including the status word) for the given C-APDU
|
|
174
|
+
onApdu: async (apdu) => {
|
|
175
|
+
// apdu.command: Uint8Array, apdu.deviceLocked: boolean
|
|
176
|
+
// for the demo purposes, we always respond with [0x0A] + status code 0x9000 (success)
|
|
177
|
+
return new Uint8Array([0x0A, 0x90, 0x00]);
|
|
178
|
+
},
|
|
179
|
+
onReaderDetected: () => session.setAlertMessage('Reader detected'), // only for iOS, no-op on Android
|
|
180
|
+
onReaderLost: () => session.setAlertMessage('Lost reader'), // only for iOS, no-op on Android
|
|
181
|
+
onInvalidated: (reason) => console.log('session closed', reason),
|
|
182
|
+
onError: (error) => console.warn('HCE handler error', error),
|
|
183
|
+
});
|
|
145
184
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
subscription.remove();
|
|
149
|
-
}
|
|
150
|
-
}, []);
|
|
185
|
+
session.setAlertMessage('Tap towards the reader'); // only for iOS, no-op on Android
|
|
186
|
+
await session.startEmulation();
|
|
151
187
|
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
This will emit `sessionStarted` event right after the function call, on both iOS and Android platforms. After the session is started, you can decide to start HCE emulation right away in the event handler:
|
|
188
|
+
`beginSession()` resolves once the session is started and ready for `startEmulation()`. Only a single session can be
|
|
189
|
+
active at a time, `beginSession()` rejects with `err_session_exists` otherwise.
|
|
190
|
+
|
|
191
|
+
The optional second argument accepts:
|
|
157
192
|
```typescript
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
193
|
+
await HCE.beginSession(handlers, {
|
|
194
|
+
signal, // AbortSignal, aborting it invalidates the session (see below)
|
|
195
|
+
handlerTimeout: 10000, // ms, the default; 0 or Infinity disables the timeout
|
|
196
|
+
allowWhenLocked: true, // Android: the default; see "Interactions while the device is locked"
|
|
197
|
+
});
|
|
163
198
|
```
|
|
164
|
-
|
|
199
|
+
|
|
200
|
+
Calling `await session.startEmulation()` causes:
|
|
165
201
|
* iOS: Your smartphone will start listening for C-APDUs (Command APDUs originating from reader devices) right after that function is called, which will be additionally indicated by the operating system popping out the NFC scanning user interface prompt.
|
|
166
|
-
* Android:
|
|
167
|
-
|
|
202
|
+
* Android: C-APDUs will be forwarded to this session from that point on. No specific user interface is displayed (you have to implement it on your own, within your app).
|
|
203
|
+
|
|
204
|
+
2. Close the session whenever you don't need it anymore:
|
|
168
205
|
```typescript
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
NativeHCEModule.setSessionAlertMessage('Reader detected'); // only for iOS, no-op in Android
|
|
172
|
-
break;
|
|
173
|
-
|
|
174
|
-
case 'readerDeselected':
|
|
175
|
-
NativeHCEModule.setSessionAlertMessage('Lost reader'); // only for iOS, no-op in Android
|
|
176
|
-
break;
|
|
206
|
+
await session.stopEmulation('success'); // iOS: 'success' / 'failure' UI status; Android: status is ignored
|
|
207
|
+
session.invalidate(); // idempotent
|
|
177
208
|
```
|
|
178
|
-
|
|
179
|
-
* iOS: The `readerDetected` event will be emitted as soon as the NFC reader's field presence is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
|
|
180
|
-
* Android: The `readerDetected` event will be emitted as soon as the first matching SELECT AID command is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
|
|
209
|
+
`session.closed` is a promise that resolves with the reason once the session is closed (by you or by the OS).
|
|
181
210
|
|
|
182
|
-
|
|
211
|
+
3. Within React components, pass an `AbortSignal`, so the session is always closed when the component is unmounted -
|
|
212
|
+
even if it's still being started at that time:
|
|
183
213
|
```typescript
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
|
|
217
|
+
(async () => {
|
|
218
|
+
const session = await HCE.beginSession({ onApdu }, { signal: controller.signal });
|
|
219
|
+
await session.startEmulation();
|
|
220
|
+
})().catch((e) => {
|
|
221
|
+
if (!controller.signal.aborted) {
|
|
222
|
+
console.error(e);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
187
225
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
226
|
+
// aborting invalidates the session
|
|
227
|
+
return () => controller.abort();
|
|
228
|
+
}, []);
|
|
191
229
|
```
|
|
192
|
-
For the lock state events, trigger mechanisms are platform dependent:
|
|
193
|
-
* iOS: The event is always `deviceUnlocked` - it's impossible to communicate over HCE while the device is locked, so the state cannot change during a session.
|
|
194
|
-
* Android: The state is re-sampled whenever a C-APDU arrives and, if it differs from the one reported so far, the corresponding event is emitted straight before the `received` event of that C-APDU. C-APDUs are always forwarded - it is up to you to decide how to treat a locked device.
|
|
195
|
-
4. Receive incoming C-APDU and respond to it:
|
|
196
|
-
```typescript
|
|
197
|
-
// inside NativeHCEModule.onEvent handler's switch
|
|
198
|
-
case 'received':
|
|
199
|
-
NativeHCEModule.setSessionAlertMessage('Keep holding the tag'); // only for iOS, no-op on Android
|
|
200
230
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
#### Events
|
|
232
|
+
|
|
233
|
+
For the reader-related handlers, trigger mechanisms are platform dependent:
|
|
234
|
+
* iOS: `onReaderDetected` is called as soon as the NFC reader's field presence is observed. `onReaderLost` is called if the reader is physically disconnected or a non-matching AID is selected by the reader.
|
|
235
|
+
* Android: `onReaderDetected` is called as soon as the first matching SELECT AID command is observed. `onReaderLost` is called if the reader is physically disconnected, a non-matching AID is selected by the reader or the emulation is stopped.
|
|
236
|
+
|
|
237
|
+
The `deviceLocked` flag of each C-APDU:
|
|
238
|
+
* iOS: Always `false` - it's impossible to communicate over HCE while the device is locked.
|
|
239
|
+
* Android: Sampled when the C-APDU arrives. Always `false` on devices without a secure lock screen (PIN, pattern, password, biometrics).
|
|
240
|
+
|
|
241
|
+
#### Android: Interactions while the device is locked
|
|
242
|
+
|
|
243
|
+
By default (`allowWhenLocked: true`), the C-APDUs are delivered to your handlers also while the device is locked
|
|
244
|
+
and it's up to you to decide what to do based on the `deviceLocked` flag.
|
|
245
|
+
|
|
246
|
+
> [!IMPORTANT]
|
|
247
|
+
> If your app isn't meant to be used on the lock screen, pass `allowWhenLocked: false` to both `HCE.beginSession()`
|
|
248
|
+
> and `HCE.registerBackgroundHandler()`. The C-APDUs received while the device is locked are then rejected by the native
|
|
249
|
+
> side (see the status words below) and never reach your JS code - in background, not even the handler factory is called.
|
|
250
|
+
> Remember that with the background handler, anybody who is able to tap a reader to the locked phone can interact with your app.
|
|
251
|
+
|
|
252
|
+
Keep `android:requireDeviceUnlock="false"` in `aid_list.xml` regardless of this option (see [Installation](#installation)).
|
|
253
|
+
|
|
254
|
+
#### Status words sent by the library
|
|
255
|
+
|
|
256
|
+
| Situation | Status word |
|
|
257
|
+
|---|---|
|
|
258
|
+
| The handler has thrown an exception or returned an invalid response | `6F00` (no precise diagnosis) |
|
|
259
|
+
| Android: nobody can handle the interaction (or the device is locked with `allowWhenLocked: false`), response to SELECT | `6A82` (file or application not found) |
|
|
260
|
+
| Android: nobody can handle the interaction (or the device is locked with `allowWhenLocked: false`), response to other commands | `6985` (conditions of use not satisfied) |
|
|
210
261
|
|
|
211
262
|
### iOS: Acquiring exclusive NFC access
|
|
212
263
|
|
|
213
264
|
If you need to utilize `NFCPresentmentIntentAssertion` for enhanced user experience, call:
|
|
214
265
|
```typescript
|
|
215
|
-
await
|
|
266
|
+
await HCE.acquireExclusiveNFC();
|
|
216
267
|
```
|
|
217
268
|
This function will acquire an exclusive NFC access for 15 seconds. No system services or other applications will be able to interfere with NFC during that period. For example, the NFC background tag reading will be disabled so it would not generate any distracting notifications.
|
|
218
269
|
|
|
219
|
-
While both session and exclusive NFC access are active,
|
|
270
|
+
While both session and exclusive NFC access are active, `onReaderDetected` is called even when the emulation is not started at the time (without any explicit UI being displayed by the system). You can then call `session.startEmulation()` from within that handler to initiate the interaction automatically, without needing the user to click anything beforehand.
|
|
220
271
|
|
|
221
272
|
This function will throw an exception if:
|
|
222
273
|
* the presentment intent assertion was already acquired and is still active;
|
|
223
274
|
* you are in the cooldown period where you are not allowed to acquire the presentment intent assertion (cooldown is 15 seconds after the previous assertion had expired);
|
|
224
|
-
* the feature is not supported or the device is not eligible for whatever reason
|
|
275
|
+
* the feature is not supported or the device is not eligible for whatever reason (rejects with `err_platform_unsupported` on Android).
|
|
225
276
|
|
|
226
|
-
You can check if you are holding an exclusive NFC access by calling
|
|
227
|
-
```typescript
|
|
228
|
-
NativeHCEModule.isExclusiveNFC()
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Whenever you don't need the exclusive NFC access anymore, call the following function to release the `NFCPresentmentIntentAssertion` object:
|
|
232
|
-
```typescript
|
|
233
|
-
NativeHCEModule.releaseExclusiveNFC();
|
|
234
|
-
```
|
|
277
|
+
You can check if you are holding an exclusive NFC access by calling `HCE.isExclusiveNFC()`. Whenever you don't need the exclusive NFC access anymore, call `HCE.releaseExclusiveNFC()`.
|
|
235
278
|
|
|
236
279
|
> [!TIP]
|
|
237
280
|
> Your exclusive NFC access might time out after exceeding certain allowance period imposed by iOS. Thus, you should periodically poll
|
|
238
|
-
> the `
|
|
239
|
-
> once your assertion has timed out, or indicate that in some different way.
|
|
281
|
+
> the `HCE.isExclusiveNFC()` value to see if it's still granted.
|
|
240
282
|
|
|
241
283
|
> [!TIP]
|
|
242
|
-
> Use the React's [AppState](https://reactnative.dev/docs/appstate) listener to see when your app goes out to background
|
|
243
|
-
>
|
|
244
|
-
>
|
|
245
|
-
> You must do so in order to avoid running into ambigous states, since the operating system is not going to enforce your
|
|
246
|
-
> exclusive NFC access anyway, as your app has gone to background.
|
|
247
|
-
>
|
|
248
|
-
> You can attempt to acquire the exclusive access assertion again once your app has gone back to foreground (active state).
|
|
249
|
-
|
|
250
|
-
### Android: Handle HCE calls when the app is not running
|
|
284
|
+
> Use the React's [AppState](https://reactnative.dev/docs/appstate) listener to see when your app goes out to background
|
|
285
|
+
> and call `HCE.releaseExclusiveNFC()` in such event. The operating system is not going to enforce your exclusive NFC access
|
|
286
|
+
> anyway, as your app has gone to background.
|
|
251
287
|
|
|
252
|
-
|
|
253
|
-
In such case, no user interface will be available, the handler will run in headless mode. This is still extremely useful in certain cases,
|
|
254
|
-
for instance - your app may emulate an NDEF tag even when it's not launched on the foreground.
|
|
288
|
+
### Android: Handling HCE interactions without an active session
|
|
255
289
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
290
|
+
Exclusively on Android platform it is possible to handle HCE interactions while the app has no session with running
|
|
291
|
+
emulation. The background handler is used when:
|
|
292
|
+
* the app is not running at all (it's started in headless mode, no user interface is available);
|
|
293
|
+
* the app is in background;
|
|
294
|
+
* the app is in foreground, but it has no session with running emulation.
|
|
259
295
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
```
|
|
284
|
-
3. Whenever you receive `readerDeselected` event, cleanup any side effects (deallocate resources, reset the state back to initial) -- this is obligatory because lack of proper cleanup will result in bugs and unexpected behavior.
|
|
285
|
-
```typescript
|
|
286
|
-
// inside processBackgroundHCE handler's switch
|
|
287
|
-
case 'readerDeselected':
|
|
288
|
-
// our application was deselected or the reader field was lost
|
|
289
|
-
// perform a cleanup of any resources here
|
|
290
|
-
break;
|
|
291
|
-
```
|
|
292
|
-
4. Implement the actual APDU handler that will process the C-APDU and generate a response.
|
|
293
|
-
```typescript
|
|
294
|
-
// inside processBackgroundHCE handler's switch
|
|
295
|
-
case 'received':
|
|
296
|
-
// decode incoming C-APDU to bytes
|
|
297
|
-
const capdu = Buffer.from(event.arg!, 'hex');
|
|
298
|
-
console.log('Received C-APDU, capdu.toString('hex'));
|
|
299
|
-
|
|
300
|
-
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
301
|
-
await respondAPDU(Buffer.from([0x0A, 0x90, 0x00]).toString("hex"));
|
|
302
|
-
break;
|
|
303
|
-
```
|
|
296
|
+
Register the handler at the top level of your app's entry file (`index.js`):
|
|
297
|
+
```typescript
|
|
298
|
+
import { AppRegistry } from 'react-native';
|
|
299
|
+
import HCE from '@icedevml/react-native-host-card-emulation';
|
|
300
|
+
|
|
301
|
+
HCE.registerBackgroundHandler(({ sessionId, appState }) => {
|
|
302
|
+
// called once per interaction with the reader - keep the state of the interaction in this closure
|
|
303
|
+
// appState: 'background' or 'foreground' (the app is in foreground, but has no active session)
|
|
304
|
+
return {
|
|
305
|
+
onApdu: async (apdu) => {
|
|
306
|
+
return new Uint8Array([0x0A, 0x90, 0x00]);
|
|
307
|
+
},
|
|
308
|
+
// the interaction ends after this handler has finished
|
|
309
|
+
onReaderLost: () => {
|
|
310
|
+
// cleanup any side effects of the interaction
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
AppRegistry.registerComponent(appName, () => App);
|
|
316
|
+
```
|
|
304
317
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
`registerBackgroundHandler()` accepts the same `handlerTimeout` and `allowWhenLocked` options as `beginSession()`
|
|
319
|
+
as the second argument, e.g. `HCE.registerBackgroundHandler(factory, { allowWhenLocked: false })`.
|
|
320
|
+
|
|
321
|
+
The factory may also return a promise (e.g. to load some state from the storage) - the C-APDUs are held by the library
|
|
322
|
+
until it resolves. The handler timeout applies to the factory as well: if it throws or doesn't finish in time,
|
|
323
|
+
the reader receives an error status word for the rest of the interaction. If no background handler is registered,
|
|
324
|
+
the reader receives an error status word (see the table above).
|
|
325
|
+
|
|
326
|
+
Routing rules on Android:
|
|
327
|
+
1. If there is a session with running emulation, it handles the interaction (also when the app went to background in the meantime - stop the emulation on `AppState` change if you don't want that).
|
|
328
|
+
While the app's activity is resumed, the library also makes `HCEService` the preferred service (`CardEmulation.setPreferredService()`),
|
|
329
|
+
so that the session wins over other apps that have registered the same AIDs. In background, such conflicts are resolved by Android.
|
|
330
|
+
2. Otherwise, the background handler handles the interaction.
|
|
331
|
+
3. If there is no background handler, the reader receives an error status word.
|
|
332
|
+
|
|
333
|
+
Call `HCE.unregisterBackgroundHandler()` to temporarily stop handling interactions in background (e.g. on a screen
|
|
334
|
+
where the HCE should only work after the user explicitly started it) and `HCE.registerBackgroundHandler()` to resume.
|
|
335
|
+
|
|
336
|
+
While React Native is starting (e.g. the app wasn't running), the interaction is held until the entry file
|
|
337
|
+
has been evaluated, so that the background handler registered there is used. If that doesn't happen within 15 seconds
|
|
338
|
+
(e.g. the JS bundle has crashed), the interaction is rejected.
|
|
339
|
+
|
|
340
|
+
The decision is made when the reader selects the application and it doesn't change until the reader is lost. If the interaction
|
|
341
|
+
was rejected, the reader may re-select the application later (e.g. after you have started the emulation) and it will be routed again.
|
|
342
|
+
|
|
343
|
+
### Error codes
|
|
344
|
+
|
|
345
|
+
Promises returned by the library reject with errors having a `code` property:
|
|
346
|
+
|
|
347
|
+
| Code | Meaning |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `err_platform_unsupported` | The device or the OS doesn't support the requested feature. |
|
|
350
|
+
| `err_invalid_argument` | Invalid handlers or options were passed. |
|
|
351
|
+
| `err_handler_timeout` | Passed to `onError`: a handler hasn't finished within the handler timeout. |
|
|
352
|
+
| `err_session_exists` | Another session is already active. |
|
|
353
|
+
| `err_no_session` | The session is not active anymore. |
|
|
354
|
+
| `err_session_invalidated` | The session was invalidated while it was being started. |
|
|
355
|
+
| `err_aborted` | `beginSession()` was aborted through the `AbortSignal`. |
|
|
356
|
+
| `err_start_emulation` | iOS: The OS has refused to start the emulation. |
|
|
357
|
+
| `err_nfc_reader_not_available`, `err_card_session_unsupported`, `err_card_session_not_eligible`, `err_create_card_session` | iOS: Preflight checks or session creation failed. |
|
|
358
|
+
| `err_already_exists`, `err_acquire_presentment` | iOS: `acquireExclusiveNFC()` failed. |
|
|
359
|
+
|
|
360
|
+
### Migrating from 0.x
|
|
361
|
+
|
|
362
|
+
| 0.x | 1.0 |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `import NativeHCEModule from '.../js/NativeHCEModule'` | `import HCE from '@icedevml/react-native-host-card-emulation'` |
|
|
365
|
+
| `isPlatformSupported()` | `await HCE.getCapabilities()` |
|
|
366
|
+
| `NativeHCEModule.onEvent(...)` + `beginSession()` | `const session = await HCE.beginSession(handlers)` |
|
|
367
|
+
| `'received'` event + `respondAPDU(null, hex)` | return the R-APDU from `onApdu` |
|
|
368
|
+
| `'readerDetected'` / `'readerDeselected'` events | `onReaderDetected` / `onReaderLost` handlers |
|
|
369
|
+
| `'deviceLocked'` / `'deviceUnlocked'` events | `apdu.deviceLocked` flag |
|
|
370
|
+
| `'sessionInvalidated'` event | `onInvalidated` handler / `session.closed` |
|
|
371
|
+
| `startHCE()` / `stopHCE(status)` / `isHCERunning()` | `session.startEmulation()` / `session.stopEmulation(status)` / `session.isEmulationRunning()` |
|
|
372
|
+
| `setSessionAlertMessage()` / `invalidateSession()` | `session.setAlertMessage()` / `session.invalidate()` |
|
|
373
|
+
| `registerHeadlessTask('handleBackgroundHCECall', ...)` + `createBackgroundHCE()` | `HCE.registerBackgroundHandler(factory)` |
|
|
374
|
+
|
|
375
|
+
Note that on Android, the background handler is now also used when the app is in foreground without an active session
|
|
376
|
+
(0.x has responded with `6999` in such case).
|
|
312
377
|
|
|
313
378
|
### More resources
|
|
314
379
|
|
|
315
|
-
* [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/tree/master/packages/host-card-emulation/js)
|
|
316
380
|
* [Source Code for Demo Application: NDEF NFC Type 4 Tag Emulator (for iOS/Android)](https://github.com/icedevml/react-native-host-card-emulation/tree/master/packages/demo-ndef-app)
|
package/RTNHCEModuleIOS.podspec
CHANGED
|
@@ -9,7 +9,7 @@ Pod::Spec.new do |s|
|
|
|
9
9
|
s.description = package["description"]
|
|
10
10
|
s.homepage = package["homepage"]
|
|
11
11
|
s.license = package["license"]
|
|
12
|
-
s.platforms = { :ios => "
|
|
12
|
+
s.platforms = { :ios => "15.1" }
|
|
13
13
|
s.author = package["author"]
|
|
14
14
|
s.source = { :git => package["repository"], :tag => "#{s.version}" }
|
|
15
15
|
|
package/android/build.gradle
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
ext.
|
|
3
|
-
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
-
}
|
|
5
|
-
repositories {
|
|
6
|
-
google()
|
|
7
|
-
gradlePluginPortal()
|
|
8
|
-
}
|
|
9
|
-
dependencies {
|
|
10
|
-
classpath("com.android.tools.build:gradle:7.3.1")
|
|
11
|
-
}
|
|
1
|
+
def safeExtGet(prop, fallback) {
|
|
2
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
12
3
|
}
|
|
13
4
|
|
|
5
|
+
// The Android Gradle Plugin and React Native versions are provided by the app's build.
|
|
14
6
|
apply plugin: 'com.android.library'
|
|
15
7
|
apply plugin: 'com.facebook.react'
|
|
16
8
|
|
|
17
9
|
android {
|
|
18
|
-
compileSdkVersion safeExtGet('compileSdkVersion',
|
|
10
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 36)
|
|
19
11
|
namespace "com.itsecrnd.rtnhceandroid"
|
|
20
12
|
|
|
21
13
|
defaultConfig {
|
|
@@ -26,14 +18,10 @@ android {
|
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
repositories {
|
|
29
|
-
maven {
|
|
30
|
-
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
|
31
|
-
url "$projectDir/../node_modules/react-native/android"
|
|
32
|
-
}
|
|
33
21
|
mavenCentral()
|
|
34
22
|
google()
|
|
35
23
|
}
|
|
36
24
|
|
|
37
25
|
dependencies {
|
|
38
|
-
implementation 'com.facebook.react:react-
|
|
26
|
+
implementation 'com.facebook.react:react-android'
|
|
39
27
|
}
|
|
@@ -44,13 +44,15 @@ public class BinaryUtils {
|
|
|
44
44
|
/**
|
|
45
45
|
* Utility method to convert a hexadecimal string to a byte string.
|
|
46
46
|
*
|
|
47
|
-
* <p>Behavior with input strings containing non-hexadecimal characters is undefined.
|
|
48
|
-
*
|
|
49
47
|
* @param s String containing hexadecimal characters to convert
|
|
50
48
|
* @return Byte array generated from input
|
|
51
|
-
* @throws java.lang.IllegalArgumentException if input length is incorrect
|
|
49
|
+
* @throws java.lang.IllegalArgumentException if input is null, its length is incorrect
|
|
50
|
+
* or it contains non-hexadecimal characters
|
|
52
51
|
*/
|
|
53
52
|
public static byte[] HexStringToByteArray(String s) throws IllegalArgumentException {
|
|
53
|
+
if (s == null) {
|
|
54
|
+
throw new IllegalArgumentException("Hex string must not be null");
|
|
55
|
+
}
|
|
54
56
|
int len = s.length();
|
|
55
57
|
if (len % 2 == 1) {
|
|
56
58
|
throw new IllegalArgumentException("Hex string must have even number of characters");
|
|
@@ -58,8 +60,12 @@ public class BinaryUtils {
|
|
|
58
60
|
byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
|
|
59
61
|
for (int i = 0; i < len; i += 2) {
|
|
60
62
|
// Convert each character into a integer (base-16), then bit-shift into place
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
int hi = Character.digit(s.charAt(i), 16);
|
|
64
|
+
int lo = Character.digit(s.charAt(i + 1), 16);
|
|
65
|
+
if (hi < 0 || lo < 0) {
|
|
66
|
+
throw new IllegalArgumentException("Hex string contains non-hexadecimal characters");
|
|
67
|
+
}
|
|
68
|
+
data[i / 2] = (byte) ((hi << 4) + lo);
|
|
63
69
|
}
|
|
64
70
|
return data;
|
|
65
71
|
}
|