@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 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
- Keep `android:requireDeviceUnlock` set to `false` in any scenario, this is the way to avoid having system prompts
118
- displayed by Android whenever the app is interacted with when the phone is locked. On the JavaScript/TypeScript side,
119
- you will receive `deviceLocked`/`deviceUnlocked` events that will let you decide whether you want to allow the NFC
120
- interaction or not.
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 Specification & Demo App
125
-
126
- This module provides a uniform low-level HCE API for both mobile platforms.
127
+ ## API
127
128
 
128
129
  > [!NOTE]
129
- > Raw native module's API specification is available in [packages/host-card-emulation/js](https://github.com/icedevml/react-native-host-card-emulation/tree/master/packages/host-card-emulation/js) subdirectory.
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
- > [!NOTE]
133
- > 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) example for more insights about the library's API.
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. Subscribe to the event stream. There is a single global event stream, so you can subscribe early and remain subscribed to events for the entire application's lifetime.
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
- useEffect(() => {
140
- const subscription = NativeHCEModule.onEvent(async (event: HCEModuleEvent) => {
141
- switch (event.type) {
142
- /* ... implement event handlers here ... */
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
- // cleanup the event listener when the effect is unmounted
147
- return () => {
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
- 2. When the user indicates that he/she wants to perform the HCE action, call the following function from the button's onClick routine:
153
- ```typescript
154
- await NativeHCEModule.beginSession();
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
- // inside NativeHCEModule.onEvent handler's switch
159
- case 'sessionStarted':
160
- NativeHCEModule.setSessionAlertMessage('Tap towards the reader'); // only for iOS, no-op in Android
161
- await NativeHCEModule.startHCE();
162
- break;
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
- Calling `await NativeHCEModule.startHCE()` causes:
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: HCE commands will be forwarded to your application from that point on. No specific user interface is displayed (you have to implement it on your own, within your app).
167
- 3. Optionally, you can handle `readerDetected` and `readerDeselected` events to enhance user's experience.
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
- // inside NativeHCEModule.onEvent handler's switch
170
- case 'readerDetected':
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
- For those events, trigger mechanisms are platform dependent:
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
- The device lock state is reported through the `deviceLocked` and `deviceUnlocked` events, which are the only source of truth about it. One of them is always emitted before the `received` event of the first C-APDU of a session, so the state is known upfront.
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
- case 'deviceLocked':
185
- setDeviceLocked(true);
186
- break;
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
- case 'deviceUnlocked':
189
- setDeviceLocked(false);
190
- break;
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
- // decode incoming C-APDU to bytes
202
- const capdu = Buffer.from(event.arg!, 'hex');
203
- console.log('Received C-APDU, capdu.toString('hex'));
204
-
205
- // for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
206
- await NativeHCEModule.respondAPDU(null, Buffer.from([0x0A, 0x90, 0x00]).toString("hex"));
207
- break;
208
- ```
209
- You don't have to respond to the APDU right away from within the event handler, but please remember that the reader might time out if you will be lingering with the response for too long.
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 NativeHCEModule.acquireExclusiveNFC();
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, you will receive `readerDetected` events even when the HCE emulation is not started at the time (without any explicit UI being displayed by the system). You can then make a call to `startHCE` to initiate the interaction automatically, without needing the user to click anything beforehand.
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 `NativeHCEModule.isExclusiveNFC()` value to see if it's still granted. You might decide to navigate into a different UI view
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
- > In such event, call `NativeHCEModule.releaseExclusiveNFC()` to explicitly release the exclusive NFC assertion.
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
- Exclusively on Android platform it is possible to handle Host Card Emulation even when the app is not running in foreground.
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
- 1. Register `handleBackgroundHCECall` headless task in your app's `index.js`:
257
- ```typescript
258
- import { BackgroundHCETaskData, createBackgroundHCE } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
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
- AppRegistry.registerHeadlessTask('handleBackgroundHCECall', () => {
261
- return async (taskData: BackgroundHCETaskData) => {
262
- return await runBackgroundHCETask(createBackgroundHCE(taskData));
263
- }
264
- });
265
- ```
266
- Pass the whole `taskData` object to `createBackgroundHCE()`, so that it could be handed over to your event handler as its third argument. The device lock state is reported to your handler as `deviceLocked` and `deviceUnlocked` events: always before the `received` event of the first C-APDU of a session, and then again straight before the `received` event of any C-APDU during which a change of that state was noticed. C-APDUs are always forwarded - it is up to you to decide how to treat a locked device.
267
- 2. Implement your background HCE handler by passing an event callback to `processBackgroundHCE()`:
268
- ```typescript
269
- import { Buffer } from 'buffer/';
270
- import {
271
- ProcessBackgroundHCEFunc,
272
- } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
273
-
274
- export default async function runBackgroundHCETask(processBackgroundHCE: ProcessBackgroundHCEFunc) {
275
- // ... initialize any state keeping variables here ...
276
-
277
- processBackgroundHCE(async (event, respondAPDU, taskData) => {
278
- switch (event.type) {
279
- /* ... background HCE event handler here ... */
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
- > [!WARNING]
306
- > Please note that under some circumstances you might find that your `runBackgroundHCETask()` is running in a few instances concurrently.
307
- > An example of such situation could be when you receive a C-APDU and the reader rapidly restarts the NFC field and reselects your application
308
- > before you manage to respond to the original command. Thus, you should not use any global variables to track the state of your app.
309
- >
310
- > The library automatically identifies stale (deselected) background tasks. For such stale tasks, any subsequent calls to `await respondAPDU()`
311
- > will throw an error to prevent communication glitches.
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)
@@ -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 => "11.0" }
12
+ s.platforms = { :ios => "15.1" }
13
13
  s.author = package["author"]
14
14
  s.source = { :git => package["repository"], :tag => "#{s.version}" }
15
15
 
@@ -1,21 +1,13 @@
1
- buildscript {
2
- ext.safeExtGet = {prop, fallback ->
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', 33)
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-native:+'
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
- data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
62
- + Character.digit(s.charAt(i + 1), 16));
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
  }