@icedevml/react-native-host-card-emulation 0.5.0 → 0.6.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/README.md CHANGED
@@ -1,12 +1,17 @@
1
- # Package: React Native HCE Module
1
+ # React Native Package: @icedevml/react-native-host-card-emulation
2
2
 
3
3
  Host Card Emulation Module for React Native on iOS and Android platforms.
4
4
 
5
+ > [!NOTE]
6
+ > Pull requests are welcome! Please [report an issue](https://github.com/icedevml/react-native-host-card-emulation/issues) if you are running into any difficulties.
7
+
5
8
  > [!IMPORTANT]
6
9
  > As per Apple's policy, **Host Card Emulation feature is only available for users based in the European
7
10
  > Economic Area (EEA)** with an iPhone running iOS 17.4 or later. Applications require a special HCE entitlement
8
11
  > in order to use this feature, which is managed by Apple. Read more in the official support article:
9
12
  > "[HCE-based contactless NFC transactions for apps in the European Economic Area (EEA)](https://developer.apple.com/support/hce-transactions-in-apps/)".
13
+ >
14
+ > However, you can still use this package without any limitations if you only want to cover for Android implementation.
10
15
 
11
16
  ## Prerequisites
12
17
  This package was designed for React Native [New Architecture](https://reactnative.dev/architecture/landing-page) only.
@@ -112,11 +117,11 @@ Afterwards, follow the subsections below for each platform that you need to supp
112
117
  This module provides a uniform low-level HCE API for both mobile platforms.
113
118
 
114
119
  > [!NOTE]
115
- > Raw native module's API specification is available in [packages/native-hce-module/js/NativeHCEModule.ts](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts).
120
+ > Raw native module's API specification is available in [packages/host-card-emulation/js/NativeHCEModule.ts](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/host-card-emulation/js/NativeHCEModule.ts).
116
121
  > Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
117
122
 
118
123
  > [!NOTE]
119
- > See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx) example for more insignt about the library's API.
124
+ > See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-ndef-app/App.tsx) example for more insignt about the library's API.
120
125
 
121
126
  ### Quick start guide
122
127
 
@@ -175,7 +180,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
175
180
  console.log('Received C-APDU, capdu.toString('hex'));
176
181
 
177
182
  // for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
178
- await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
183
+ await NativeHCEModule?.respondAPDU(null, Buffer.from([0x0A, 0x90, 0x00], "hex"));
179
184
  break;
180
185
  ```
181
186
  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.
@@ -203,29 +208,34 @@ for instance - your app may emulate an NDEF tag even when it's not launched on t
203
208
 
204
209
  1. Register `handleBackgroundHCECall` headless task in your app's `index.js`:
205
210
  ```typescript
211
+ import { createBackgroundHCE } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
212
+
206
213
  AppRegistry.registerHeadlessTask('handleBackgroundHCECall', () => {
207
- return async () => {
208
- return await runBackground();
214
+ return async (taskData) => {
215
+ return await runBackgroundHCETask(createBackgroundHCE(taskData.handle));
209
216
  }
210
217
  });
211
218
  ```
212
219
  2. Register onBackgroundEvent listener in your `runBackground()` function and call to `await NativeHCEModule?.initBackgroundHCE()` at the very end, after the event listener is fully set up.
213
220
  ```typescript
214
- async function runBackground() {
215
- let subscription = NativeHCEModule?.onBackgroundEvent(async (event: HCEModuleBackgroundEvent) => {
221
+ import { Buffer } from 'buffer/';
222
+ import {
223
+ ProcessBackgroundHCEFunc,
224
+ } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
225
+
226
+ export default async function runBackgroundHCETask(processBackgroundHCE: ProcessBackgroundHCEFunc) {
227
+ processBackgroundHCE(async (event, respondAPDU) => {
216
228
  switch (event.type) {
217
229
  /* ... background HCE event handler here ... */
218
230
  }
219
231
  });
220
-
221
- await NativeHCEModule?.initBackgroundHCE();
222
232
  }
223
233
  ```
224
234
  3. Cleanup your event listener in `readerDeselected` -- this is obligatory because lack of proper cleanup will result in bugs and unexpected behavior.
225
235
  ```typescript
226
236
  case 'readerDeselected':
227
- // cleanup, remove the event listener
228
- subscription.remove();
237
+ // our application was deselected or the reader field was lost
238
+ // perform a cleanup of any resources here
229
239
  break;
230
240
  ```
231
241
  4. Implement the actual APDU handler that will process the C-APDU and generate a response.
@@ -236,11 +246,11 @@ for instance - your app may emulate an NDEF tag even when it's not launched on t
236
246
  console.log('Received C-APDU, capdu.toString('hex'));
237
247
 
238
248
  // for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
239
- await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
249
+ await respondAPDU(Buffer.from([0x0A, 0x90, 0x00]).toString("hex"));
240
250
  break;
241
251
  ```
242
252
 
243
253
  ### More resources
244
254
 
245
- * [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts)
246
- * [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx)
255
+ * [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/host-card-emulation/js/NativeHCEModule.ts)
256
+ * [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-ndef-app/App.tsx)
@@ -30,12 +30,14 @@ import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
30
30
 
31
31
  import java.util.List;
32
32
  import java.util.Locale;
33
+ import java.util.UUID;
33
34
 
34
35
  @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
35
36
  public class HCEService extends HostApduService implements ReactInstanceEventListener, HCEServiceCallback {
36
37
  private static final String TAG = "HCEService";
37
38
 
38
39
  private boolean isForeground;
40
+ private String backgroundSessionUUID;
39
41
  private RTNHCEAndroidModule hceModule;
40
42
  private byte[] pendingCAPDU;
41
43
  private volatile boolean needsResponse;
@@ -63,20 +65,36 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
63
65
  }
64
66
 
65
67
  @Override
66
- public void onBackgroundHCEInit() {
68
+ public void onBackgroundHCEInit(String handle) {
67
69
  Log.d(TAG, "HCEService:onBackgroundHCEInit");
68
70
 
71
+ if (handle == null && backgroundSessionUUID != null) {
72
+ Log.d(TAG, "HCEService:onBackgroundHCEInit foreground call to background session");
73
+ throw new IllegalStateException();
74
+ } else if (handle != null && !handle.equals(backgroundSessionUUID)) {
75
+ Log.d(TAG, "HCEService:onBackgroundHCEInit mismatched background session handle");
76
+ throw new IllegalStateException();
77
+ }
78
+
69
79
  if (pendingCAPDU != null) {
70
80
  Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
71
- hceModule.sendBackgroundEvent("received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
81
+ hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
72
82
  pendingCAPDU = null;
73
83
  }
74
84
  }
75
85
 
76
86
  @Override
77
- public void onRespondAPDU(String rapdu) {
87
+ public void onRespondAPDU(String handle, String rapdu) {
78
88
  Log.d(TAG, "HCEService:onRespondAPDU");
79
89
 
90
+ if (handle == null && backgroundSessionUUID != null) {
91
+ Log.d(TAG, "HCEService:onRespondAPDU foreground call to background session");
92
+ throw new IllegalStateException();
93
+ } else if (handle != null && !handle.equals(backgroundSessionUUID)) {
94
+ Log.d(TAG, "HCEService:onRespondAPDU mismatched background session handle");
95
+ throw new IllegalStateException();
96
+ }
97
+
80
98
  if (!needsResponse) {
81
99
  Log.d(TAG, "HCEService:onRespondAPDU not waiting for a response");
82
100
  throw new IllegalStateException();
@@ -104,7 +122,7 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
104
122
  if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
105
123
  Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
106
124
  needsResponse = true;
107
- hceModule.sendBackgroundEvent("received", capdu);
125
+ hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", capdu);
108
126
  } else {
109
127
  Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
110
128
  needsResponse = true;
@@ -132,6 +150,7 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
132
150
 
133
151
  if (isForeground) {
134
152
  Log.d(TAG, "HCEService:onCreate foreground");
153
+ this.backgroundSessionUUID = null;
135
154
 
136
155
  ReactContext reactContext = reactHost.getCurrentReactContext();
137
156
 
@@ -155,6 +174,7 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
155
174
  } else {
156
175
  ReactContext reactContext = reactHost.getCurrentReactContext();
157
176
  Log.d(TAG, "HCEService:onCreate background");
177
+ this.backgroundSessionUUID = UUID.randomUUID().toString();
158
178
 
159
179
  if (reactContext == null) {
160
180
  reactHost.addReactInstanceEventListener(this);
@@ -175,7 +195,7 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
175
195
  this.hceModule.sendEvent("readerDeselected", "");
176
196
  }
177
197
  } else {
178
- this.hceModule.sendBackgroundEvent("readerDeselected", "");
198
+ this.hceModule.sendBackgroundEvent(backgroundSessionUUID, "readerDeselected", "");
179
199
  }
180
200
 
181
201
  if (this.hceModule != null) {
@@ -214,11 +234,13 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
214
234
  @Override
215
235
  public void run() {
216
236
  Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
237
+ Bundle argBundle = new Bundle();
238
+ argBundle.putString("handle", backgroundSessionUUID);
217
239
  HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
218
240
  headlessJsTaskContext.startTask(
219
241
  new HeadlessJsTaskConfig(
220
242
  "handleBackgroundHCECall",
221
- Arguments.fromBundle(new Bundle()),
243
+ Arguments.fromBundle(argBundle),
222
244
  15000,
223
245
  false // not allowed in foreground
224
246
  ));
@@ -1,6 +1,6 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
3
  public interface HCEServiceCallback {
4
- void onBackgroundHCEInit();
5
- void onRespondAPDU(String rapdu);
4
+ void onBackgroundHCEInit(String handle);
5
+ void onRespondAPDU(String handle, String rapdu);
6
6
  }
@@ -57,9 +57,10 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
57
57
  emitOnEvent(map);
58
58
  }
59
59
 
60
- void sendBackgroundEvent(final String type, final String arg) {
60
+ void sendBackgroundEvent(final String audience, final String type, final String arg) {
61
61
  Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
62
62
  WritableMap map = Arguments.createMap();
63
+ map.putString("audience", audience);
63
64
  map.putString("type", type);
64
65
  map.putString("arg", arg);
65
66
  emitOnBackgroundEvent(map);
@@ -157,14 +158,17 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
157
158
  }
158
159
 
159
160
  @Override
160
- public void initBackgroundHCE(Promise promise) {
161
- Log.d(TAG, "RTNHCEAndroidModule:initBackgroundHCE");
161
+ public boolean beginBackgroundHCE(String handle) {
162
+ Log.d(TAG, "RTNHCEAndroidModule:beginBackgroundHCE");
162
163
 
163
- this.hceBackgroundReady = true;
164
-
165
- promise.resolve(null);
164
+ try {
165
+ this.serviceCb.onBackgroundHCEInit(handle);
166
+ } catch (IllegalStateException e) {
167
+ return false;
168
+ }
166
169
 
167
- this.serviceCb.onBackgroundHCEInit();
170
+ this.hceBackgroundReady = true;
171
+ return true;
168
172
  }
169
173
 
170
174
  @Override
@@ -208,7 +212,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
208
212
 
209
213
  @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
210
214
  @Override
211
- public void respondAPDU(String rapdu, Promise promise) {
215
+ public void respondAPDU(String handle, String rapdu, Promise promise) {
212
216
  Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
213
217
 
214
218
  if (!this.hceBackgroundReady) {
@@ -224,9 +228,9 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
224
228
  Log.d(TAG, "respondAPDU(): Send to service");
225
229
 
226
230
  try {
227
- serviceCb.onRespondAPDU(rapdu);
231
+ serviceCb.onRespondAPDU(handle, rapdu);
228
232
  } catch (IllegalStateException e) {
229
- promise.reject("err_no_capdu", "There is no C-APDU to respond to at the time.");
233
+ promise.reject("err_illegal_state", "There is no C-APDU to respond to at the time or the provided handle is invalid.");
230
234
  return;
231
235
  }
232
236
 
@@ -86,8 +86,8 @@
86
86
  }
87
87
  }
88
88
 
89
- - (void)initBackgroundHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
90
- reject(@"err_platform_unsupported", @"Background HCE is not supported on iOS.", nil);
89
+ - (nonnull NSNumber *)beginBackgroundHCE:(nonnull NSString *)handle {
90
+ return @0;
91
91
  }
92
92
 
93
93
  - (void)startHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
@@ -106,9 +106,9 @@
106
106
  }
107
107
  }
108
108
 
109
- - (void)respondAPDU:(nonnull NSString *)rapdu resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
109
+ - (void)respondAPDU:(NSString * _Nullable)handle rapdu:(nonnull NSString *)rapdu resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
110
110
  if (@available(iOS 17.4, *)) {
111
- return [hcemoduleios respondAPDUWithRapdu:rapdu resolve:resolve reject:reject];
111
+ return [hcemoduleios respondAPDUWithHandle:handle rapdu:rapdu resolve:resolve reject:reject];
112
112
  } else {
113
113
  reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
114
114
  }
@@ -145,8 +145,13 @@ extension Data {
145
145
  }
146
146
  }
147
147
 
148
- @objc public func respondAPDU(rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
148
+ @objc public func respondAPDU(handle: NSString?, rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
149
149
  Task.init {
150
+ guard handle == nil else {
151
+ reject("err_illegal_state", "The provided handle is invalid.", nil)
152
+ return
153
+ }
154
+
150
155
  guard let capdu = self.receivedCardAPDU else {
151
156
  reject("err_no_apdu_received", "There is no received APDU to respond to.", nil)
152
157
  return
@@ -32,6 +32,7 @@ export type HCEModuleEvent = {
32
32
  export type HCEModuleBackgroundEvent = {
33
33
  type: HCEModuleBackgroundEventType
34
34
  arg: string | null,
35
+ audience: string,
35
36
  }
36
37
 
37
38
  export interface Spec extends TurboModule {
@@ -73,7 +74,7 @@ export interface Spec extends TurboModule {
73
74
  */
74
75
  isSessionRunning(): boolean;
75
76
 
76
- initBackgroundHCE(): Promise<void>;
77
+ beginBackgroundHCE(handle: string): boolean;
77
78
 
78
79
  /**
79
80
  * iOS: Start the card emulation.
@@ -88,7 +89,7 @@ export interface Spec extends TurboModule {
88
89
  /**
89
90
  * Send R-APDU (hex-encoded string) as a response to the last received C-APDU event.
90
91
  */
91
- respondAPDU(rapdu: string): Promise<void>;
92
+ respondAPDU(handle: string | null, rapdu: string): Promise<void>;
92
93
  /**
93
94
  * iOS: Checks if the card emulation is running.
94
95
  * Android: Checks if the HCE service is enabled.
@@ -0,0 +1,43 @@
1
+ import NativeHCEModule, {HCEModuleBackgroundEvent} from "./NativeHCEModule";
2
+
3
+ export interface BackgroundEventHandler {
4
+ (
5
+ event: HCEModuleBackgroundEvent,
6
+ respondAPDU: (rapdu: string) => Promise<void>,
7
+ ): Promise<void>
8
+ }
9
+
10
+ export interface ProcessBackgroundHCEFunc {
11
+ (
12
+ handler: BackgroundEventHandler
13
+ ): void
14
+ }
15
+
16
+ export const createBackgroundHCE = (handle: string) => {
17
+ const respondAPDU = (rapdu: string) => {
18
+ return NativeHCEModule.respondAPDU(handle, rapdu);
19
+ }
20
+
21
+ return (handler: BackgroundEventHandler) => {
22
+ const subscription = NativeHCEModule.onBackgroundEvent(async (event) => {
23
+ if (event.audience !== handle) {
24
+ // ignore
25
+ return;
26
+ }
27
+
28
+ try {
29
+ await handler(event, respondAPDU);
30
+ } catch (e) {
31
+ throw e;
32
+ } finally {
33
+ if (event.type === "readerDeselected") {
34
+ subscription.remove();
35
+ }
36
+ }
37
+ });
38
+
39
+ if (!NativeHCEModule.beginBackgroundHCE(handle)) {
40
+ subscription.remove();
41
+ }
42
+ }
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icedevml/react-native-host-card-emulation",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Host Card Emulation (HCE) React Native Turbo Module for iOS and Android platforms.",
5
5
  "react-native": "js/index",
6
6
  "source": "js/index",