@icedevml/react-native-host-card-emulation 0.8.2 → 0.9.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
@@ -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
- *(no prerequisite steps are required for Android)*
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.handle));
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
  }
@@ -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,7 +33,7 @@ import java.util.List;
32
33
  import java.util.Locale;
33
34
  import java.util.UUID;
34
35
 
35
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
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
 
@@ -42,6 +43,8 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
42
43
  private RTNHCEAndroidModule hceModule;
43
44
  private byte[] pendingCAPDU;
44
45
  private volatile boolean needsResponse;
46
+ private volatile Boolean reportedDeviceLocked;
47
+ private Boolean pendingDeviceLocked;
45
48
  private final HashMap<String, Integer> taskSessionIdMap;
46
49
 
47
50
  public HCEService() {
@@ -50,6 +53,31 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
50
53
  taskSessionIdMap = new HashMap<>();
51
54
  }
52
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
+
53
81
  private boolean isAppOnForeground(Context context) {
54
82
  /*
55
83
  We need to check if app is in foreground otherwise the app will crash.
@@ -87,6 +115,12 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
87
115
 
88
116
  if (pendingCAPDU != null) {
89
117
  Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
118
+
119
+ if (pendingDeviceLocked != null) {
120
+ reportDeviceLockState(pendingDeviceLocked);
121
+ pendingDeviceLocked = null;
122
+ }
123
+
90
124
  hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
91
125
  pendingCAPDU = null;
92
126
  }
@@ -156,6 +190,7 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
156
190
  if (isForeground) {
157
191
  if (hceModule._isHCERunning() && hceModule.isHCEActiveConnection()) {
158
192
  Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
193
+ reportDeviceLockState(isDeviceLocked());
159
194
  needsResponse = true;
160
195
  hceModule.sendEvent("received", capdu);
161
196
  } else {
@@ -165,12 +200,14 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
165
200
  } else {
166
201
  if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
167
202
  Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
203
+ reportDeviceLockState(isDeviceLocked());
168
204
  needsResponse = true;
169
205
  hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", capdu);
170
206
  } else {
171
207
  Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
172
208
  needsResponse = true;
173
209
  pendingCAPDU = command;
210
+ pendingDeviceLocked = isDeviceLocked();
174
211
  }
175
212
  }
176
213
 
@@ -190,8 +227,10 @@ public class HCEService extends HostApduService implements HCEServiceCallback {
190
227
  isForeground = isAppOnForeground(getApplicationContext());
191
228
  isDeactivated = false;
192
229
  pendingCAPDU = null;
230
+ pendingDeviceLocked = null;
193
231
  hceModule = null;
194
232
  needsResponse = false;
233
+ reportedDeviceLocked = null;
195
234
 
196
235
  if (isForeground) {
197
236
  Log.d(TAG, "HCEService:onCreate foreground");
@@ -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.TIRAMISU) {
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.TIRAMISU)
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
 
@@ -1,5 +1,4 @@
1
- import type {TurboModule} from 'react-native';
2
- import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes';
1
+ import type {CodegenTypes, TurboModule} from 'react-native';
3
2
  import {TurboModuleRegistry} from 'react-native';
4
3
 
5
4
  export type HCEModuleEventType =
@@ -18,12 +17,32 @@ export type HCEModuleEventType =
18
17
  * Received C-APDU (hex-encoded string in "arg" key) from the reader.
19
18
  * Respond by calling respondAPDU(rapdu).
20
19
  */
21
- | 'received';
20
+ | 'received'
21
+ /**
22
+ * The device lock state, always reported before the 'received' event of the
23
+ * first C-APDU of a session and then again whenever it changes.
24
+ *
25
+ * iOS: Always 'deviceUnlocked' - it's impossible to communicate over HCE while
26
+ * the device is locked, so the state cannot change during a session.
27
+ * Android: The state is sampled whenever a C-APDU arrives, so a change is
28
+ * reported straight before the 'received' event of the C-APDU during which it
29
+ * was noticed.
30
+ */
31
+ | 'deviceLocked'
32
+ | 'deviceUnlocked';
22
33
  export type HCEModuleStopReason = 'success' | 'failure';
23
34
 
24
35
  export type HCEModuleBackgroundEventType =
25
36
  'readerDeselected'
26
- | 'received';
37
+ | 'received'
38
+ /**
39
+ * The device lock state, always reported before the 'received' event of the
40
+ * first C-APDU of a session and then again whenever it changes. The state is
41
+ * sampled whenever a C-APDU arrives, so a change is reported straight before
42
+ * the 'received' event of the C-APDU during which it was noticed.
43
+ */
44
+ | 'deviceLocked'
45
+ | 'deviceUnlocked';
27
46
 
28
47
  export type HCEModuleEvent = {
29
48
  type: HCEModuleEventType
@@ -118,14 +137,14 @@ export interface Spec extends TurboModule {
118
137
  /**
119
138
  * Event handler for foreground HCE interactions.
120
139
  */
121
- readonly onEvent: EventEmitter<HCEModuleEvent>;
140
+ readonly onEvent: CodegenTypes.EventEmitter<HCEModuleEvent>;
122
141
 
123
142
  /**
124
143
  * Event handler for background HCE interactions.
125
144
  * NOTE: Don't subscribe to that handler directly, use wrapper hceBackground.ts:createBackgroundHCE.
126
145
  * See demo app or README.md for example wrapper usage.
127
146
  */
128
- readonly onBackgroundEvent: EventEmitter<HCEModuleBackgroundEvent>;
147
+ readonly onBackgroundEvent: CodegenTypes.EventEmitter<HCEModuleBackgroundEvent>;
129
148
  }
130
149
 
131
150
  export default TurboModuleRegistry.getEnforcing<Spec>(
@@ -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
- export const createBackgroundHCE = (handle: string) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icedevml/react-native-host-card-emulation",
3
- "version": "0.8.2",
3
+ "version": "0.9.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",