@icedevml/react-native-host-card-emulation 0.5.0 → 0.6.1

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,44 +208,51 @@ 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
236
+ // inside processBackgroundHCE handler's switch
226
237
  case 'readerDeselected':
227
- // cleanup, remove the event listener
228
- subscription.remove();
238
+ // our application was deselected or the reader field was lost
239
+ // perform a cleanup of any resources here
229
240
  break;
230
241
  ```
231
242
  4. Implement the actual APDU handler that will process the C-APDU and generate a response.
232
243
  ```typescript
244
+ // inside processBackgroundHCE handler's switch
233
245
  case 'received':
234
246
  // decode incoming C-APDU to bytes
235
247
  const capdu = Buffer.from(event.arg!, 'hex');
236
248
  console.log('Received C-APDU, capdu.toString('hex'));
237
249
 
238
250
  // 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"));
251
+ await respondAPDU(Buffer.from([0x0A, 0x90, 0x00]).toString("hex"));
240
252
  break;
241
253
  ```
242
254
 
243
255
  ### More resources
244
256
 
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)
257
+ * [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/host-card-emulation/js/NativeHCEModule.ts)
258
+ * [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-ndef-app/App.tsx)
@@ -20,7 +20,6 @@ import androidx.annotation.RequiresApi;
20
20
 
21
21
  import com.facebook.react.ReactApplication;
22
22
  import com.facebook.react.ReactHost;
23
- import com.facebook.react.ReactInstanceEventListener;
24
23
  import com.facebook.react.bridge.Arguments;
25
24
  import com.facebook.react.bridge.ReactContext;
26
25
  import com.facebook.react.bridge.UiThreadUtil;
@@ -28,17 +27,27 @@ import com.facebook.react.jstasks.HeadlessJsTaskConfig;
28
27
  import com.facebook.react.jstasks.HeadlessJsTaskContext;
29
28
  import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
30
29
 
30
+ import java.util.HashMap;
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
- public class HCEService extends HostApduService implements ReactInstanceEventListener, HCEServiceCallback {
36
+ public class HCEService extends HostApduService implements 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;
44
+ private final HashMap<String, Integer> taskSessionIdMap;
45
+
46
+ public HCEService() {
47
+ super();
48
+
49
+ taskSessionIdMap = new HashMap<>();
50
+ }
42
51
 
43
52
  private boolean isAppOnForeground(Context context) {
44
53
  /*
@@ -63,20 +72,66 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
63
72
  }
64
73
 
65
74
  @Override
66
- public void onBackgroundHCEInit() {
75
+ public void onBackgroundHCEInit(String handle) {
67
76
  Log.d(TAG, "HCEService:onBackgroundHCEInit");
77
+ Log.d(TAG, "HCEService:onBackgroundHCEInit:" + handle + ":" + backgroundSessionUUID);
78
+
79
+ if (handle == null && backgroundSessionUUID != null) {
80
+ Log.d(TAG, "HCEService:onBackgroundHCEInit foreground call to background session");
81
+ throw new IllegalStateException();
82
+ } else if (handle != null && !handle.equals(backgroundSessionUUID)) {
83
+ Log.d(TAG, "HCEService:onBackgroundHCEInit mismatched background session handle");
84
+ throw new IllegalStateException();
85
+ }
68
86
 
69
87
  if (pendingCAPDU != null) {
70
88
  Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
71
- hceModule.sendBackgroundEvent("received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
89
+ hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
72
90
  pendingCAPDU = null;
73
91
  }
74
92
  }
75
93
 
76
94
  @Override
77
- public void onRespondAPDU(String rapdu) {
95
+ public void onBackgroundHCEFinish(String handle) {
96
+ Log.d(TAG, "HCEService:onBackgroundHCEFinish");
97
+ Integer taskId = taskSessionIdMap.get(handle);
98
+
99
+ if (taskId == null) {
100
+ Log.d(TAG, "HCEService:onBackgroundHCEFinish unable to resolve taskId");
101
+ throw new IllegalArgumentException();
102
+ }
103
+
104
+ ReactHost reactHost = ((ReactApplication) getApplication()).getReactHost();
105
+
106
+ if (reactHost == null) {
107
+ Log.d(TAG, "HCEService:onBackgroundHCEFinish getReactHost() returned null");
108
+ throw new IllegalArgumentException();
109
+ }
110
+
111
+ ReactContext reactContext = reactHost.getCurrentReactContext();
112
+
113
+ if (reactContext == null) {
114
+ Log.d(TAG, "HCEService:onBackgroundHCEFinish getCurrentReactContext() returned null");
115
+ throw new IllegalArgumentException();
116
+ }
117
+
118
+ HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
119
+ headlessJsTaskContext.finishTask(taskId);
120
+ taskSessionIdMap.remove(handle);
121
+ }
122
+
123
+ @Override
124
+ public void onRespondAPDU(String handle, String rapdu) {
78
125
  Log.d(TAG, "HCEService:onRespondAPDU");
79
126
 
127
+ if (handle == null && backgroundSessionUUID != null) {
128
+ Log.d(TAG, "HCEService:onRespondAPDU foreground call to background session");
129
+ throw new IllegalStateException();
130
+ } else if (handle != null && !handle.equals(backgroundSessionUUID)) {
131
+ Log.d(TAG, "HCEService:onRespondAPDU mismatched background session handle");
132
+ throw new IllegalStateException();
133
+ }
134
+
80
135
  if (!needsResponse) {
81
136
  Log.d(TAG, "HCEService:onRespondAPDU not waiting for a response");
82
137
  throw new IllegalStateException();
@@ -92,19 +147,19 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
92
147
  String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
93
148
 
94
149
  if (isForeground) {
95
- if (hceModule._isHCERunning() && !hceModule.isHCEBrokenConnection()) {
150
+ if (hceModule._isHCERunning() && hceModule.isHCEActiveConnection()) {
96
151
  Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
97
152
  needsResponse = true;
98
153
  hceModule.sendEvent("received", capdu);
99
154
  } else {
100
155
  Log.d(TAG, "HCEService:processCommandApdu foreground respond 6999");
101
- return new byte[] { (byte) 0x69, (byte) 0x99 };
156
+ return new byte[]{(byte) 0x69, (byte) 0x99};
102
157
  }
103
158
  } else {
104
159
  if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
105
160
  Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
106
161
  needsResponse = true;
107
- hceModule.sendBackgroundEvent("received", capdu);
162
+ hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", capdu);
108
163
  } else {
109
164
  Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
110
165
  needsResponse = true;
@@ -132,6 +187,7 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
132
187
 
133
188
  if (isForeground) {
134
189
  Log.d(TAG, "HCEService:onCreate foreground");
190
+ this.backgroundSessionUUID = null;
135
191
 
136
192
  ReactContext reactContext = reactHost.getCurrentReactContext();
137
193
 
@@ -155,12 +211,29 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
155
211
  } else {
156
212
  ReactContext reactContext = reactHost.getCurrentReactContext();
157
213
  Log.d(TAG, "HCEService:onCreate background");
214
+ final String useSessID = UUID.randomUUID().toString();
215
+ backgroundSessionUUID = useSessID;
158
216
 
159
217
  if (reactContext == null) {
160
- reactHost.addReactInstanceEventListener(this);
218
+ reactHost.addReactInstanceEventListener(evReactContext -> {
219
+ HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(evReactContext);
220
+ headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
221
+ @Override
222
+ public void onHeadlessJsTaskStart(int i) {
223
+ Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskStart: " + i);
224
+ }
225
+
226
+ @Override
227
+ public void onHeadlessJsTaskFinish(int i) {
228
+ Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskFinish: " + i);
229
+ }
230
+ });
231
+
232
+ setupRunJSTask(evReactContext, useSessID);
233
+ });
161
234
  reactHost.start();
162
235
  } else {
163
- setupRunJSTask(reactContext);
236
+ setupRunJSTask(reactContext, useSessID);
164
237
  }
165
238
  }
166
239
  }
@@ -171,37 +244,20 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
171
244
  needsResponse = false;
172
245
 
173
246
  if (isForeground) {
174
- if (this.hceModule != null && !this.hceModule.isHCEBrokenConnection()) {
247
+ if (this.hceModule != null && this.hceModule.isHCEActiveConnection()) {
175
248
  this.hceModule.sendEvent("readerDeselected", "");
176
249
  }
177
- } else {
178
- this.hceModule.sendBackgroundEvent("readerDeselected", "");
179
- }
250
+ } else if (backgroundSessionUUID != null) {
251
+ String prevBackgroundSessionUUID = backgroundSessionUUID;
252
+ backgroundSessionUUID = null;
180
253
 
181
- if (this.hceModule != null) {
182
- this.hceModule.setHCEService(null);
183
- }
184
- }
185
-
186
- @Override
187
- public void onReactContextInitialized(@NonNull ReactContext reactContext) {
188
- HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
189
- headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
190
- @Override
191
- public void onHeadlessJsTaskStart(int i) {
192
- Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskStart: " + i);
193
- }
194
-
195
- @Override
196
- public void onHeadlessJsTaskFinish(int i) {
197
- Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskFinish: " + i);
254
+ if (this.hceModule != null) {
255
+ this.hceModule.sendBackgroundEvent(prevBackgroundSessionUUID, "readerDeselected", "");
198
256
  }
199
- });
200
-
201
- setupRunJSTask(reactContext);
257
+ }
202
258
  }
203
259
 
204
- public void setupRunJSTask(@NonNull ReactContext reactContext) {
260
+ public void setupRunJSTask(final @NonNull ReactContext reactContext, final String useSessUUID) {
205
261
  hceModule = (RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule");
206
262
 
207
263
  if (hceModule == null) {
@@ -210,19 +266,19 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
210
266
 
211
267
  hceModule.setHCEService(this);
212
268
 
213
- UiThreadUtil.runOnUiThread(new Runnable() {
214
- @Override
215
- public void run() {
216
- Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
217
- HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
218
- headlessJsTaskContext.startTask(
219
- new HeadlessJsTaskConfig(
220
- "handleBackgroundHCECall",
221
- Arguments.fromBundle(new Bundle()),
222
- 15000,
223
- false // not allowed in foreground
224
- ));
225
- }
269
+ UiThreadUtil.runOnUiThread(() -> {
270
+ Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
271
+ Bundle argBundle = new Bundle();
272
+ argBundle.putString("handle", useSessUUID);
273
+ HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
274
+ int taskId = headlessJsTaskContext.startTask(
275
+ new HeadlessJsTaskConfig(
276
+ "handleBackgroundHCECall",
277
+ Arguments.fromBundle(argBundle),
278
+ 15000,
279
+ false // not allowed in foreground
280
+ ));
281
+ taskSessionIdMap.put(useSessUUID, taskId);
226
282
  });
227
283
  }
228
284
 
@@ -1,6 +1,7 @@
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 onBackgroundHCEFinish(String handle);
6
+ void onRespondAPDU(String handle, String rapdu);
6
7
  }
@@ -51,18 +51,29 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
51
51
 
52
52
  void sendEvent(final String type, final String arg) {
53
53
  Log.d(TAG, "RTNHCEAndroidModule:sendEvent");
54
- WritableMap map = Arguments.createMap();
55
- map.putString("type", type);
56
- map.putString("arg", arg);
57
- emitOnEvent(map);
54
+
55
+ if (this.mEventEmitterCallback != null) {
56
+ WritableMap map = Arguments.createMap();
57
+ map.putString("type", type);
58
+ map.putString("arg", arg);
59
+ emitOnEvent(map);
60
+ } else {
61
+ Log.d(TAG, "RTNHCEAndroidModule:sendEvent missing event emitter, ignore");
62
+ }
58
63
  }
59
64
 
60
- void sendBackgroundEvent(final String type, final String arg) {
65
+ void sendBackgroundEvent(final String audience, final String type, final String arg) {
61
66
  Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
62
- WritableMap map = Arguments.createMap();
63
- map.putString("type", type);
64
- map.putString("arg", arg);
65
- emitOnBackgroundEvent(map);
67
+
68
+ if (this.mEventEmitterCallback != null) {
69
+ WritableMap map = Arguments.createMap();
70
+ map.putString("audience", audience);
71
+ map.putString("type", type);
72
+ map.putString("arg", arg);
73
+ emitOnBackgroundEvent(map);
74
+ } else {
75
+ Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent missing event emitter, ignore");
76
+ }
66
77
  }
67
78
 
68
79
  boolean _isHCERunning() {
@@ -77,8 +88,8 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
77
88
  return this.hceBackgroundReady;
78
89
  }
79
90
 
80
- boolean isHCEBrokenConnection() {
81
- return this.hceBrokenConnection;
91
+ boolean isHCEActiveConnection() {
92
+ return !this.hceBrokenConnection;
82
93
  }
83
94
 
84
95
  void setHCEService(HCEServiceCallback serviceCallback) {
@@ -157,14 +168,30 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
157
168
  }
158
169
 
159
170
  @Override
160
- public void initBackgroundHCE(Promise promise) {
161
- Log.d(TAG, "RTNHCEAndroidModule:initBackgroundHCE");
171
+ public boolean beginBackgroundHCE(String handle) {
172
+ Log.d(TAG, "RTNHCEAndroidModule:beginBackgroundHCE");
173
+
174
+ try {
175
+ this.serviceCb.onBackgroundHCEInit(handle);
176
+ } catch (IllegalStateException e) {
177
+ return false;
178
+ }
162
179
 
163
180
  this.hceBackgroundReady = true;
181
+ return true;
182
+ }
164
183
 
165
- promise.resolve(null);
184
+ @Override
185
+ public boolean finishBackgroundHCE(String handle) {
186
+ Log.d(TAG, "RTNHCEAndroidModule:finishBackgroundHCE");
187
+
188
+ try {
189
+ this.serviceCb.onBackgroundHCEFinish(handle);
190
+ } catch (IllegalStateException e) {
191
+ return false;
192
+ }
166
193
 
167
- this.serviceCb.onBackgroundHCEInit();
194
+ return true;
168
195
  }
169
196
 
170
197
  @Override
@@ -208,7 +235,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
208
235
 
209
236
  @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
210
237
  @Override
211
- public void respondAPDU(String rapdu, Promise promise) {
238
+ public void respondAPDU(String handle, String rapdu, Promise promise) {
212
239
  Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
213
240
 
214
241
  if (!this.hceBackgroundReady) {
@@ -224,9 +251,9 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
224
251
  Log.d(TAG, "respondAPDU(): Send to service");
225
252
 
226
253
  try {
227
- serviceCb.onRespondAPDU(rapdu);
254
+ serviceCb.onRespondAPDU(handle, rapdu);
228
255
  } catch (IllegalStateException e) {
229
- promise.reject("err_no_capdu", "There is no C-APDU to respond to at the time.");
256
+ promise.reject("err_illegal_state", "There is no C-APDU to respond to at the time or the provided handle is invalid.");
230
257
  return;
231
258
  }
232
259
 
@@ -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,8 @@ export interface Spec extends TurboModule {
73
74
  */
74
75
  isSessionRunning(): boolean;
75
76
 
76
- initBackgroundHCE(): Promise<void>;
77
+ beginBackgroundHCE(handle: string): boolean;
78
+ finishBackgroundHCE(handle: string): boolean;
77
79
 
78
80
  /**
79
81
  * iOS: Start the card emulation.
@@ -88,7 +90,7 @@ export interface Spec extends TurboModule {
88
90
  /**
89
91
  * Send R-APDU (hex-encoded string) as a response to the last received C-APDU event.
90
92
  */
91
- respondAPDU(rapdu: string): Promise<void>;
93
+ respondAPDU(handle: string | null, rapdu: string): Promise<void>;
92
94
  /**
93
95
  * iOS: Checks if the card emulation is running.
94
96
  * Android: Checks if the HCE service is enabled.
@@ -0,0 +1,45 @@
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
+ NativeHCEModule.finishBackgroundHCE(handle);
36
+ }
37
+ }
38
+ });
39
+
40
+ if (!NativeHCEModule.beginBackgroundHCE(handle)) {
41
+ subscription.remove();
42
+ NativeHCEModule.finishBackgroundHCE(handle);
43
+ }
44
+ }
45
+ }
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.1",
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",