@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 +25 -15
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +28 -6
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +2 -2
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +14 -10
- package/ios/RTNHCEModuleIOS.mm +4 -4
- package/ios/RTNHCEModuleIOSImpl.swift +6 -1
- package/js/NativeHCEModule.ts +3 -2
- package/js/hceBackground.ts +43 -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,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
|
|
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
|
|
226
236
|
case 'readerDeselected':
|
|
227
|
-
//
|
|
228
|
-
|
|
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
|
|
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/
|
|
246
|
-
* [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-
|
|
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(
|
|
243
|
+
Arguments.fromBundle(argBundle),
|
|
222
244
|
15000,
|
|
223
245
|
false // not allowed in foreground
|
|
224
246
|
));
|
|
@@ -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
|
|
161
|
-
Log.d(TAG, "RTNHCEAndroidModule:
|
|
161
|
+
public boolean beginBackgroundHCE(String handle) {
|
|
162
|
+
Log.d(TAG, "RTNHCEAndroidModule:beginBackgroundHCE");
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
try {
|
|
165
|
+
this.serviceCb.onBackgroundHCEInit(handle);
|
|
166
|
+
} catch (IllegalStateException e) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
166
169
|
|
|
167
|
-
this.
|
|
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("
|
|
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
|
|
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,7 @@ export interface Spec extends TurboModule {
|
|
|
73
74
|
*/
|
|
74
75
|
isSessionRunning(): boolean;
|
|
75
76
|
|
|
76
|
-
|
|
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