@icedevml/react-native-host-card-emulation 0.8.1 → 0.9.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 +30 -6
- package/android/build.gradle +6 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +49 -1
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +2 -2
- package/ios/RTNHCEModuleIOSImpl.swift +8 -0
- package/js/NativeHCEModule.ts +22 -2
- package/js/hceBackground.ts +25 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,8 @@ Make sure that your project has it enabled. Note that the New Architecture is en
|
|
|
25
25
|
5. Wait until the entitlement is approved by Apple.
|
|
26
26
|
|
|
27
27
|
### Android
|
|
28
|
-
|
|
28
|
+
1. Ensure that your app's `minSdkVersion` is at least 24 (Android 7.0). This is the floor imposed by React Native's New Architecture; the Host Card Emulation APIs used by this package are available since Android 5.1.
|
|
29
|
+
2. Note that HCE availability also depends on the device hardware - always check `isPlatformSupported()` before starting a session.
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
31
32
|
Install this package within your React Native project:
|
|
@@ -101,7 +102,8 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
101
102
|
```xml
|
|
102
103
|
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
|
|
103
104
|
android:description="@string/app_name"
|
|
104
|
-
android:requireDeviceUnlock="false"
|
|
105
|
+
android:requireDeviceUnlock="false"
|
|
106
|
+
android:requireDeviceScreenOn="true">
|
|
105
107
|
<aid-group android:category="other"
|
|
106
108
|
android:description="@string/app_name">
|
|
107
109
|
<aid-filter android:name="F001020304" />
|
|
@@ -112,6 +114,13 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
112
114
|
```
|
|
113
115
|
Replace `F001020304`, `F00102030405`, `F0010203040506` with the ISO7816 AIDs that you want to support.
|
|
114
116
|
|
|
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.
|
|
121
|
+
|
|
122
|
+
For `android:requireDeviceScreenOn` - set it according to your use case.
|
|
123
|
+
|
|
115
124
|
## API Specification & Demo App
|
|
116
125
|
|
|
117
126
|
This module provides a uniform low-level HCE API for both mobile platforms.
|
|
@@ -169,6 +178,20 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
169
178
|
For those events, trigger mechanisms are platform dependent:
|
|
170
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.
|
|
171
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.
|
|
181
|
+
|
|
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.
|
|
183
|
+
```typescript
|
|
184
|
+
case 'deviceLocked':
|
|
185
|
+
setDeviceLocked(true);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'deviceUnlocked':
|
|
189
|
+
setDeviceLocked(false);
|
|
190
|
+
break;
|
|
191
|
+
```
|
|
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.
|
|
172
195
|
4. Receive incoming C-APDU and respond to it:
|
|
173
196
|
```typescript
|
|
174
197
|
// inside NativeHCEModule.onEvent handler's switch
|
|
@@ -232,14 +255,15 @@ for instance - your app may emulate an NDEF tag even when it's not launched on t
|
|
|
232
255
|
|
|
233
256
|
1. Register `handleBackgroundHCECall` headless task in your app's `index.js`:
|
|
234
257
|
```typescript
|
|
235
|
-
import { createBackgroundHCE } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
|
|
258
|
+
import { BackgroundHCETaskData, createBackgroundHCE } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
|
|
236
259
|
|
|
237
260
|
AppRegistry.registerHeadlessTask('handleBackgroundHCECall', () => {
|
|
238
|
-
return async (taskData) => {
|
|
239
|
-
return await runBackgroundHCETask(createBackgroundHCE(taskData
|
|
261
|
+
return async (taskData: BackgroundHCETaskData) => {
|
|
262
|
+
return await runBackgroundHCETask(createBackgroundHCE(taskData));
|
|
240
263
|
}
|
|
241
264
|
});
|
|
242
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.
|
|
243
267
|
2. Implement your background HCE handler by passing an event callback to `processBackgroundHCE()`:
|
|
244
268
|
```typescript
|
|
245
269
|
import { Buffer } from 'buffer/';
|
|
@@ -250,7 +274,7 @@ for instance - your app may emulate an NDEF tag even when it's not launched on t
|
|
|
250
274
|
export default async function runBackgroundHCETask(processBackgroundHCE: ProcessBackgroundHCEFunc) {
|
|
251
275
|
// ... initialize any state keeping variables here ...
|
|
252
276
|
|
|
253
|
-
processBackgroundHCE(async (event, respondAPDU) => {
|
|
277
|
+
processBackgroundHCE(async (event, respondAPDU, taskData) => {
|
|
254
278
|
switch (event.type) {
|
|
255
279
|
/* ... background HCE event handler here ... */
|
|
256
280
|
}
|
package/android/build.gradle
CHANGED
|
@@ -17,6 +17,12 @@ apply plugin: 'com.facebook.react'
|
|
|
17
17
|
android {
|
|
18
18
|
compileSdkVersion safeExtGet('compileSdkVersion', 33)
|
|
19
19
|
namespace "com.itsecrnd.rtnhceandroid"
|
|
20
|
+
|
|
21
|
+
defaultConfig {
|
|
22
|
+
// API 24 is the floor imposed by React Native's New Architecture,
|
|
23
|
+
// the HCE APIs used here are available since API 22.
|
|
24
|
+
minSdkVersion safeExtGet('minSdkVersion', 24)
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
repositories {
|
|
@@ -9,6 +9,7 @@ package com.itsecrnd.rtnhceandroid;
|
|
|
9
9
|
import static com.facebook.react.jstasks.HeadlessJsTaskContext.Companion;
|
|
10
10
|
|
|
11
11
|
import android.app.ActivityManager;
|
|
12
|
+
import android.app.KeyguardManager;
|
|
12
13
|
import android.content.Context;
|
|
13
14
|
import android.nfc.cardemulation.HostApduService;
|
|
14
15
|
import android.os.Build;
|
|
@@ -32,15 +33,18 @@ import java.util.List;
|
|
|
32
33
|
import java.util.Locale;
|
|
33
34
|
import java.util.UUID;
|
|
34
35
|
|
|
35
|
-
@RequiresApi(api = Build.VERSION_CODES.
|
|
36
|
+
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
36
37
|
public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
37
38
|
private static final String TAG = "HCEService";
|
|
38
39
|
|
|
39
40
|
private boolean isForeground;
|
|
41
|
+
private volatile boolean isDeactivated;
|
|
40
42
|
private String backgroundSessionUUID;
|
|
41
43
|
private RTNHCEAndroidModule hceModule;
|
|
42
44
|
private byte[] pendingCAPDU;
|
|
43
45
|
private volatile boolean needsResponse;
|
|
46
|
+
private volatile Boolean reportedDeviceLocked;
|
|
47
|
+
private Boolean pendingDeviceLocked;
|
|
44
48
|
private final HashMap<String, Integer> taskSessionIdMap;
|
|
45
49
|
|
|
46
50
|
public HCEService() {
|
|
@@ -49,6 +53,31 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
49
53
|
taskSessionIdMap = new HashMap<>();
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
private boolean isDeviceLocked() {
|
|
57
|
+
/*
|
|
58
|
+
The KeyguardManager has to be obtained lazily, as the service's context
|
|
59
|
+
is not yet attached when the instance is being constructed.
|
|
60
|
+
*/
|
|
61
|
+
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
|
62
|
+
return keyguardManager != null && keyguardManager.isDeviceLocked();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private void reportDeviceLockState(boolean deviceLocked) {
|
|
66
|
+
if (reportedDeviceLocked != null && reportedDeviceLocked == deviceLocked) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
reportedDeviceLocked = deviceLocked;
|
|
71
|
+
final String type = deviceLocked ? "deviceLocked" : "deviceUnlocked";
|
|
72
|
+
Log.d(TAG, "HCEService:reportDeviceLockState " + type);
|
|
73
|
+
|
|
74
|
+
if (isForeground) {
|
|
75
|
+
hceModule.sendEvent(type, "");
|
|
76
|
+
} else {
|
|
77
|
+
hceModule.sendBackgroundEvent(backgroundSessionUUID, type, "");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
private boolean isAppOnForeground(Context context) {
|
|
53
82
|
/*
|
|
54
83
|
We need to check if app is in foreground otherwise the app will crash.
|
|
@@ -86,6 +115,12 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
86
115
|
|
|
87
116
|
if (pendingCAPDU != null) {
|
|
88
117
|
Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
|
|
118
|
+
|
|
119
|
+
if (pendingDeviceLocked != null) {
|
|
120
|
+
reportDeviceLockState(pendingDeviceLocked);
|
|
121
|
+
pendingDeviceLocked = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
|
|
90
125
|
pendingCAPDU = null;
|
|
91
126
|
}
|
|
@@ -118,6 +153,7 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
118
153
|
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
119
154
|
headlessJsTaskContext.finishTask(taskId);
|
|
120
155
|
taskSessionIdMap.remove(handle);
|
|
156
|
+
stopSelf();
|
|
121
157
|
}
|
|
122
158
|
|
|
123
159
|
@Override
|
|
@@ -144,11 +180,17 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
144
180
|
@Override
|
|
145
181
|
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
146
182
|
Log.d(TAG, "HCEService:processCommandApdu");
|
|
183
|
+
|
|
184
|
+
if (isDeactivated) {
|
|
185
|
+
onCreate();
|
|
186
|
+
}
|
|
187
|
+
|
|
147
188
|
String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
|
|
148
189
|
|
|
149
190
|
if (isForeground) {
|
|
150
191
|
if (hceModule._isHCERunning() && hceModule.isHCEActiveConnection()) {
|
|
151
192
|
Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
|
|
193
|
+
reportDeviceLockState(isDeviceLocked());
|
|
152
194
|
needsResponse = true;
|
|
153
195
|
hceModule.sendEvent("received", capdu);
|
|
154
196
|
} else {
|
|
@@ -158,12 +200,14 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
158
200
|
} else {
|
|
159
201
|
if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
|
|
160
202
|
Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
|
|
203
|
+
reportDeviceLockState(isDeviceLocked());
|
|
161
204
|
needsResponse = true;
|
|
162
205
|
hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", capdu);
|
|
163
206
|
} else {
|
|
164
207
|
Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
|
|
165
208
|
needsResponse = true;
|
|
166
209
|
pendingCAPDU = command;
|
|
210
|
+
pendingDeviceLocked = isDeviceLocked();
|
|
167
211
|
}
|
|
168
212
|
}
|
|
169
213
|
|
|
@@ -181,9 +225,12 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
181
225
|
}
|
|
182
226
|
|
|
183
227
|
isForeground = isAppOnForeground(getApplicationContext());
|
|
228
|
+
isDeactivated = false;
|
|
184
229
|
pendingCAPDU = null;
|
|
230
|
+
pendingDeviceLocked = null;
|
|
185
231
|
hceModule = null;
|
|
186
232
|
needsResponse = false;
|
|
233
|
+
reportedDeviceLocked = null;
|
|
187
234
|
|
|
188
235
|
if (isForeground) {
|
|
189
236
|
Log.d(TAG, "HCEService:onCreate foreground");
|
|
@@ -242,6 +289,7 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
|
242
289
|
public void onDeactivated(int reason) {
|
|
243
290
|
Log.d(TAG, "HCEService:onDeactivated: " + reason);
|
|
244
291
|
needsResponse = false;
|
|
292
|
+
isDeactivated = true;
|
|
245
293
|
|
|
246
294
|
if (isForeground) {
|
|
247
295
|
if (this.hceModule != null && this.hceModule.isHCEActiveConnection()) {
|
|
@@ -104,7 +104,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
104
104
|
@Override
|
|
105
105
|
public boolean isPlatformSupported() {
|
|
106
106
|
Log.d(TAG, "RTNHCEAndroidModule:isPlatformSupported");
|
|
107
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.
|
|
107
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -239,7 +239,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
239
239
|
promise.resolve(null);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
@RequiresApi(api = Build.VERSION_CODES.
|
|
242
|
+
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
243
243
|
@Override
|
|
244
244
|
public void respondAPDU(String handle, String rapdu, Promise promise) {
|
|
245
245
|
Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
|
|
@@ -25,6 +25,7 @@ extension Data {
|
|
|
25
25
|
var cardSessionInvalidated: Bool = true
|
|
26
26
|
var presentmentIntent: NFCPresentmentIntentAssertion? = nil
|
|
27
27
|
var receivedCardAPDU: CardSession.APDU? = nil
|
|
28
|
+
var deviceLockStateReported: Bool = false
|
|
28
29
|
|
|
29
30
|
var emitOnEvent: ((NSString, NSString?) -> ())? = nil
|
|
30
31
|
|
|
@@ -105,6 +106,7 @@ extension Data {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
self.cardSessionInvalidated = false
|
|
109
|
+
self.deviceLockStateReported = false
|
|
108
110
|
resolve(nil)
|
|
109
111
|
|
|
110
112
|
do {
|
|
@@ -121,6 +123,12 @@ extension Data {
|
|
|
121
123
|
self.emitOnEvent!("readerDeselected", "")
|
|
122
124
|
|
|
123
125
|
case .received(let cardAPDU):
|
|
126
|
+
if !self.deviceLockStateReported {
|
|
127
|
+
// it's impossible to communicate over HCE while the device is locked
|
|
128
|
+
self.deviceLockStateReported = true
|
|
129
|
+
self.emitOnEvent!("deviceUnlocked", "")
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
self.receivedCardAPDU = cardAPDU
|
|
125
133
|
self.emitOnEvent!("received", NSString(string:cardAPDU.payload.hexEncodedString()))
|
|
126
134
|
|
package/js/NativeHCEModule.ts
CHANGED
|
@@ -18,12 +18,32 @@ export type HCEModuleEventType =
|
|
|
18
18
|
* Received C-APDU (hex-encoded string in "arg" key) from the reader.
|
|
19
19
|
* Respond by calling respondAPDU(rapdu).
|
|
20
20
|
*/
|
|
21
|
-
| 'received'
|
|
21
|
+
| 'received'
|
|
22
|
+
/**
|
|
23
|
+
* The device lock state, always reported before the 'received' event of the
|
|
24
|
+
* first C-APDU of a session and then again whenever it changes.
|
|
25
|
+
*
|
|
26
|
+
* iOS: Always 'deviceUnlocked' - it's impossible to communicate over HCE while
|
|
27
|
+
* the device is locked, so the state cannot change during a session.
|
|
28
|
+
* Android: The state is sampled whenever a C-APDU arrives, so a change is
|
|
29
|
+
* reported straight before the 'received' event of the C-APDU during which it
|
|
30
|
+
* was noticed.
|
|
31
|
+
*/
|
|
32
|
+
| 'deviceLocked'
|
|
33
|
+
| 'deviceUnlocked';
|
|
22
34
|
export type HCEModuleStopReason = 'success' | 'failure';
|
|
23
35
|
|
|
24
36
|
export type HCEModuleBackgroundEventType =
|
|
25
37
|
'readerDeselected'
|
|
26
|
-
| 'received'
|
|
38
|
+
| 'received'
|
|
39
|
+
/**
|
|
40
|
+
* The device lock state, always reported before the 'received' event of the
|
|
41
|
+
* first C-APDU of a session and then again whenever it changes. The state is
|
|
42
|
+
* sampled whenever a C-APDU arrives, so a change is reported straight before
|
|
43
|
+
* the 'received' event of the C-APDU during which it was noticed.
|
|
44
|
+
*/
|
|
45
|
+
| 'deviceLocked'
|
|
46
|
+
| 'deviceUnlocked';
|
|
27
47
|
|
|
28
48
|
export type HCEModuleEvent = {
|
|
29
49
|
type: HCEModuleEventType
|
package/js/hceBackground.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import NativeHCEModule, {HCEModuleBackgroundEvent} from "./NativeHCEModule";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Android: Data passed by the native side to the 'handleBackgroundHCECall' headless task.
|
|
5
|
+
* Pass it as-is to createBackgroundHCE().
|
|
6
|
+
*/
|
|
7
|
+
export interface BackgroundHCETaskData {
|
|
8
|
+
/**
|
|
9
|
+
* Handle of the background HCE session.
|
|
10
|
+
*/
|
|
11
|
+
handle: string,
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export interface BackgroundEventHandler {
|
|
4
15
|
(
|
|
5
16
|
event: HCEModuleBackgroundEvent,
|
|
6
17
|
respondAPDU: (rapdu: string) => Promise<void>,
|
|
18
|
+
taskData: BackgroundHCETaskData,
|
|
7
19
|
): Promise<void>
|
|
8
20
|
}
|
|
9
21
|
|
|
@@ -13,7 +25,18 @@ export interface ProcessBackgroundHCEFunc {
|
|
|
13
25
|
): void
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Set up handling of a background HCE session.
|
|
30
|
+
*
|
|
31
|
+
* @param taskData Data received by the 'handleBackgroundHCECall' headless task. A bare handle
|
|
32
|
+
* string is accepted as well, for backwards compatibility.
|
|
33
|
+
*/
|
|
34
|
+
export const createBackgroundHCE = (taskData: BackgroundHCETaskData | string): ProcessBackgroundHCEFunc => {
|
|
35
|
+
const useTaskData: BackgroundHCETaskData = typeof taskData === "string"
|
|
36
|
+
? {handle: taskData}
|
|
37
|
+
: taskData;
|
|
38
|
+
const handle = useTaskData.handle;
|
|
39
|
+
|
|
17
40
|
const respondAPDU = (rapdu: string) => {
|
|
18
41
|
return NativeHCEModule.respondAPDU(handle, rapdu);
|
|
19
42
|
}
|
|
@@ -26,7 +49,7 @@ export const createBackgroundHCE = (handle: string) => {
|
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
try {
|
|
29
|
-
await handler(event, respondAPDU);
|
|
52
|
+
await handler(event, respondAPDU, useTaskData);
|
|
30
53
|
} catch (e) {
|
|
31
54
|
throw e;
|
|
32
55
|
} finally {
|
package/package.json
CHANGED