@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 +27 -15
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +104 -48
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +3 -2
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +45 -18
- package/ios/RTNHCEModuleIOS.mm +4 -4
- package/ios/RTNHCEModuleIOSImpl.swift +6 -1
- package/js/NativeHCEModule.ts +4 -2
- package/js/hceBackground.ts +45 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
#
|
|
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/
|
|
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-
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
//
|
|
228
|
-
|
|
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
|
|
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/
|
|
246
|
-
* [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-
|
|
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
|
|
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
|
|
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() &&
|
|
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[]
|
|
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(
|
|
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 &&
|
|
247
|
+
if (this.hceModule != null && this.hceModule.isHCEActiveConnection()) {
|
|
175
248
|
this.hceModule.sendEvent("readerDeselected", "");
|
|
176
249
|
}
|
|
177
|
-
} else {
|
|
178
|
-
|
|
179
|
-
|
|
250
|
+
} else if (backgroundSessionUUID != null) {
|
|
251
|
+
String prevBackgroundSessionUUID = backgroundSessionUUID;
|
|
252
|
+
backgroundSessionUUID = null;
|
|
180
253
|
|
|
181
|
-
|
|
182
|
-
|
|
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(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
161
|
-
Log.d(TAG, "RTNHCEAndroidModule:
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
package/ios/RTNHCEModuleIOS.mm
CHANGED
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
- (
|
|
90
|
-
|
|
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
|
|
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
|
package/js/NativeHCEModule.ts
CHANGED
|
@@ -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
|
-
|
|
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