@icedevml/react-native-host-card-emulation 0.3.0 → 0.4.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 CHANGED
@@ -67,7 +67,7 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
67
67
  + <service
68
68
  + android:name="com.itsecrnd.rtnhceandroid.HCEService"
69
69
  + android:exported="true"
70
- + android:enabled="false"
70
+ + android:enabled="true"
71
71
  + android:permission="android.permission.BIND_NFC_SERVICE" >
72
72
  + <intent-filter>
73
73
  + <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
@@ -98,5 +98,65 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
98
98
 
99
99
  ## API Specification & Demo App
100
100
 
101
- * [module API (declarations)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts)
102
- * [demo app's code (example usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx)
101
+ This module provides a uniform low-level HCE API for both mobile platforms.
102
+
103
+ 1. Subscribe to the event stream (there is a single global event stream for the entire module):
104
+ ```typescript
105
+ NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
106
+ switch (event.type) {
107
+ /* ... your handler here ... */
108
+ }
109
+ });
110
+ ```
111
+ 2. When user indicates that he wants to perform the HCE action, call the following function from the button's onClick routine:
112
+ ```typescript
113
+ await NativeHCEModule?.beginSession();
114
+ ```
115
+ This will emit `sessionStarted` event on both platforms. You can start the HCE straight from that event:
116
+ ```typescript
117
+ // inside NativeHCEModule?.onEvent handler's switch
118
+ case 'sessionStarted':
119
+ NativeHCEModule?.setSessionAlertMessage('Tap towards the reader'); // only for iOS, no-op in Android
120
+ await NativeHCEModule?.startHCE();
121
+ break;
122
+ ```
123
+ 3. Calling `startHCE()` causes:
124
+ * iOS: Launch of the NFC scan system UI. The smartphone will start listening for C-APDUs from the readers right after that UI appears on screen. The `readerDetected` event will be emitted as soon as the NFC tag 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.
125
+ * Android: No user interface appears (you have to implement it on your own scanning UI, within your app). 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.
126
+ 4. Optionally, you can handle `readerDetected` and `readerDeselected events to enhance user's experience.
127
+ ```typescript
128
+ // inside NativeHCEModule?.onEvent handler's switch
129
+ case 'readerDetected':
130
+ NativeHCEModule?.setSessionAlertMessage('Reader detected'); // only for iOS, no-op in Android
131
+ break;
132
+
133
+ case 'readerDeselected':
134
+ NativeHCEModule?.setSessionAlertMessage('Lost reader'); // only for iOS, no-op in Android
135
+ break;
136
+ ```
137
+ 5. Handle CAPDU
138
+ ```typescript
139
+ // inside NativeHCEModule?.onEvent handler's switch
140
+ case 'received':
141
+ NativeHCEModule?.setSessionAlertMessage('Keep holding the tag');
142
+
143
+ const capdu = Buffer.from(event.arg!, 'hex');
144
+ console.log('Received C-APDU, capdu.toString('hex'));
145
+
146
+ // let's say we always want to respond with [0x0A] + Status Word 0x9000
147
+ await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
148
+ break;
149
+ ```
150
+ 6. (iOS) Optionally, if you need to utilize `NFCPresentmentIntentAssertion` then at any time you can call:
151
+ ```typescript
152
+ NativeHCEModule?.acquireExclusiveNFC();
153
+ ```
154
+ Which will throw an exception if the presentment intent assertion was already acquired and is still valid
155
+ (usually 15 seconds) or if the cooldown period for acquiring new assertion was not yet expired (usually also 15 seconds).
156
+
157
+ > [!NOTE]
158
+ > 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.
159
+
160
+ > [!NOTE]
161
+ > 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).
162
+ > Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
@@ -1,9 +1,12 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_DETECT;
4
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_LOST;
5
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
6
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
3
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
4
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
5
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
6
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
7
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_AUTH;
8
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_CAPDU;
9
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_RAPDU;
7
10
 
8
11
  import android.content.BroadcastReceiver;
9
12
  import android.content.Context;
@@ -34,10 +37,10 @@ public class HCEService extends HostApduService {
34
37
  String action = intent.getAction();
35
38
 
36
39
  if (action != null) {
37
- AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
40
+ AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra(KEY_AUTH));
38
41
 
39
- if (action.equals(INTENT_SEND_R_APDU)) {
40
- String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("rapdu"));
42
+ if (action.equals(ACTION_SEND_R_APDU)) {
43
+ String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra(KEY_RAPDU));
41
44
  byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
42
45
  sendResponseApdu(dec);
43
46
  }
@@ -52,10 +55,10 @@ public class HCEService extends HostApduService {
52
55
  public byte[] processCommandApdu(byte[] command, Bundle extras) {
53
56
  String capdu = AESGCMUtil.encryptData(encSecretKey, BinaryUtils.ByteArrayToHexString(command));
54
57
 
55
- Intent intent = new Intent(INTENT_RECEIVE_C_APDU);
58
+ Intent intent = new Intent(ACTION_RECEIVE_C_APDU);
56
59
  intent.setPackage(getApplicationContext().getPackageName());
57
- intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
58
- intent.putExtra("capdu", capdu);
60
+ intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
61
+ intent.putExtra(KEY_CAPDU, capdu);
59
62
  getApplicationContext().sendBroadcast(intent);
60
63
 
61
64
  return null;
@@ -81,12 +84,12 @@ public class HCEService extends HostApduService {
81
84
  * See a comment about registerReceiver() in RTNHCEAndroidModule.
82
85
  */
83
86
  IntentFilter filter = new IntentFilter();
84
- filter.addAction(INTENT_SEND_R_APDU);
87
+ filter.addAction(ACTION_SEND_R_APDU);
85
88
  getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
86
89
 
87
- Intent intent = new Intent(INTENT_READER_DETECT);
90
+ Intent intent = new Intent(ACTION_READER_DETECT);
88
91
  intent.setPackage(getApplicationContext().getPackageName());
89
- intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
92
+ intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
90
93
  getApplicationContext().sendBroadcast(intent);
91
94
  }
92
95
 
@@ -96,9 +99,9 @@ public class HCEService extends HostApduService {
96
99
 
97
100
  getApplicationContext().unregisterReceiver(receiver);
98
101
 
99
- Intent intent = new Intent(INTENT_READER_LOST);
102
+ Intent intent = new Intent(ACTION_READER_LOST);
100
103
  intent.setPackage(getApplicationContext().getPackageName());
101
- intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
104
+ intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
102
105
  getApplicationContext().sendBroadcast(intent);
103
106
  }
104
107
  }
@@ -1,8 +1,11 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
3
  public class IntentKeys {
4
- public static final String INTENT_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.receivecapdu";
5
- public static final String INTENT_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.sendrapdu";
6
- public static final String INTENT_READER_DETECT = "com.itsecrnd.rtnhceandroid.readerdetect";
7
- public static final String INTENT_READER_LOST = "com.itsecrnd.rtnhceandroid.readerlost";
4
+ public static final String ACTION_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.ACTION_RECEIVE_C_APDU";
5
+ public static final String ACTION_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.ACTION_SEND_R_APDU";
6
+ public static final String ACTION_READER_DETECT = "com.itsecrnd.rtnhceandroid.ACTION_READER_DETECT";
7
+ public static final String ACTION_READER_LOST = "com.itsecrnd.rtnhceandroid.ACTION_READER_LOST";
8
+ public static final String KEY_AUTH = "auth";
9
+ public static final String KEY_CAPDU = "capdu";
10
+ public static final String KEY_RAPDU = "rapdu";
8
11
  }
@@ -1,10 +1,10 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
3
  import static android.content.Context.RECEIVER_EXPORTED;
4
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_DETECT;
5
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_LOST;
6
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
7
- import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
4
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
5
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
6
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
7
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
8
8
 
9
9
  import android.content.BroadcastReceiver;
10
10
  import android.content.ComponentName;
@@ -30,20 +30,11 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
30
30
  public static final String TAG = "RTNHCEAndroidModule";
31
31
  public static final String NAME = "NativeHCEModule";
32
32
 
33
+ private volatile boolean sessionRunning;
34
+ private volatile boolean hceRunning;
35
+ private volatile boolean hceBreakConnection;
36
+ private volatile boolean hceDeselected;
33
37
  private final SecretKey encSecretKey;
34
- private boolean sessionOpen;
35
-
36
- private void toggleHceService(Boolean enabled) {
37
- getReactApplicationContext()
38
- .getPackageManager()
39
- .setComponentEnabledSetting(
40
- new ComponentName(getReactApplicationContext(), HCEService.class),
41
- enabled
42
- ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
43
- : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
44
- PackageManager.DONT_KILL_APP
45
- );
46
- }
47
38
 
48
39
  private void sendEvent(final String type, final String arg) {
49
40
  WritableMap map = Arguments.createMap();
@@ -61,13 +52,29 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
61
52
  if (action != null) {
62
53
  AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
63
54
 
64
- if (action.equals(INTENT_RECEIVE_C_APDU)) {
55
+ if (action.equals(ACTION_RECEIVE_C_APDU)) {
65
56
  String capdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("capdu"));
66
- sendEvent("received", capdu);
67
- } else if (action.equals(INTENT_READER_DETECT)) {
57
+
58
+ if (!hceBreakConnection && hceRunning) {
59
+ sendEvent("received", capdu);
60
+ } else {
61
+ Intent rintent = new Intent(ACTION_SEND_R_APDU);
62
+ rintent.setPackage(getReactApplicationContext().getPackageName());
63
+ rintent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
64
+ rintent.putExtra("rapdu", AESGCMUtil.encryptData(encSecretKey, "6999"));
65
+ getReactApplicationContext().getApplicationContext().sendBroadcast(rintent);
66
+ }
67
+ } else if (hceRunning && action.equals(ACTION_READER_DETECT)) {
68
+ hceDeselected = false;
68
69
  sendEvent("readerDetected", "");
69
- } else if (action.equals(INTENT_READER_LOST)) {
70
- sendEvent("readerDeselected", "");
70
+ } else if (action.equals(ACTION_READER_LOST)) {
71
+ if (hceRunning && !hceBreakConnection && !hceDeselected) {
72
+ // only if we haven't send a fake disconnect event already
73
+ hceDeselected = true;
74
+ sendEvent("readerDeselected", "");
75
+ }
76
+
77
+ hceBreakConnection = false;
71
78
  }
72
79
  }
73
80
  } catch (Exception e) {
@@ -78,10 +85,11 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
78
85
 
79
86
  RTNHCEAndroidModule(ReactApplicationContext context) {
80
87
  super(context);
81
-
82
- this.sessionOpen = false;
83
-
84
88
  String encKey;
89
+ this.sessionRunning = false;
90
+ this.hceRunning = false;
91
+ this.hceBreakConnection = false;
92
+ this.hceDeselected = true;
85
93
 
86
94
  try {
87
95
  encSecretKey = AESGCMUtil.generateKey();
@@ -98,9 +106,9 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
98
106
  .commit();
99
107
 
100
108
  IntentFilter filter = new IntentFilter();
101
- filter.addAction(INTENT_RECEIVE_C_APDU);
102
- filter.addAction(INTENT_READER_DETECT);
103
- filter.addAction(INTENT_READER_LOST);
109
+ filter.addAction(ACTION_RECEIVE_C_APDU);
110
+ filter.addAction(ACTION_READER_DETECT);
111
+ filter.addAction(ACTION_READER_LOST);
104
112
 
105
113
  /*
106
114
  * FIXME It seems like those intents are not going through across this module and HostApduService.
@@ -115,6 +123,11 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
115
123
  getReactApplicationContext().getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
116
124
  }
117
125
 
126
+ @Override
127
+ public void invalidate() {
128
+ getReactApplicationContext().getApplicationContext().unregisterReceiver(receiver);
129
+ }
130
+
118
131
  @Override
119
132
  @NonNull
120
133
  public String getName() {
@@ -135,18 +148,18 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
135
148
 
136
149
  @Override
137
150
  public boolean isExclusiveNFC() {
138
- // unsupported on Android
151
+ // unsupported on Android, always false
139
152
  return false;
140
153
  }
141
154
 
142
155
  @Override
143
156
  public void beginSession(Promise promise) {
144
- if (this.sessionOpen) {
145
- promise.reject("err_card_session_exists", "Session already exists.");
146
- } else {
147
- this.sessionOpen = true;
148
- promise.resolve(null);
157
+ if (!this.sessionRunning) {
158
+ this.sessionRunning = true;
149
159
  sendEvent("sessionStarted", "");
160
+ promise.resolve(null);
161
+ } else {
162
+ promise.reject("err_card_session_exists", "Session already exists.");
150
163
  }
151
164
  }
152
165
 
@@ -157,37 +170,48 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
157
170
 
158
171
  @Override
159
172
  public void invalidateSession() {
160
- toggleHceService(false);
161
- this.sessionOpen = false;
162
- sendEvent("sessionInvalidated", "");
173
+ if (this.sessionRunning) {
174
+ this.sessionRunning = false;
175
+ sendEvent("sessionInvalidated", "userInvalidated");
176
+ }
163
177
  }
164
178
 
165
179
  @Override
166
180
  public boolean isSessionRunning() {
167
- return this.sessionOpen;
181
+ return this.sessionRunning;
168
182
  }
169
183
 
170
184
  @Override
171
185
  public void startHCE(Promise promise) {
172
- toggleHceService(true);
186
+ if (this.hceRunning) {
187
+ promise.reject("err_start_emulation", "Error trying to start emulation. Emulation already started.");
188
+ return;
189
+ }
190
+
191
+ this.hceRunning = true;
173
192
  promise.resolve(null);
174
193
  }
175
194
 
176
195
  @Override
177
196
  public void stopHCE(String status, Promise promise) {
178
- toggleHceService(false);
197
+ if (this.hceRunning) {
198
+ this.hceRunning = false;
199
+
200
+ if (!this.hceDeselected) {
201
+ this.hceDeselected = true;
202
+ this.hceBreakConnection = true;
203
+ sendEvent("readerDeselected", "");
204
+ }
205
+ }
206
+
179
207
  promise.resolve(null);
180
208
  }
181
209
 
182
210
  @Override
183
211
  public void respondAPDU(String rapdu, Promise promise) {
184
- if (!this.sessionOpen) {
185
- return;
186
- }
187
-
188
212
  String encRapdu = AESGCMUtil.encryptData(encSecretKey, rapdu);
189
213
 
190
- Intent intent = new Intent(INTENT_SEND_R_APDU);
214
+ Intent intent = new Intent(ACTION_SEND_R_APDU);
191
215
  intent.setPackage(getReactApplicationContext().getPackageName());
192
216
  intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
193
217
  intent.putExtra("rapdu", encRapdu);
@@ -198,10 +222,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
198
222
 
199
223
  @Override
200
224
  public void isHCERunning(Promise promise) {
201
- int enabledSetting = getReactApplicationContext()
202
- .getPackageManager()
203
- .getComponentEnabledSetting(new ComponentName(getReactApplicationContext(), HCEService.class));
204
-
205
- promise.resolve(enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
225
+ // unsupported on Android, always true
226
+ promise.resolve(true);
206
227
  }
207
228
  }
@@ -170,7 +170,14 @@ extension Data {
170
170
 
171
171
  @objc public func startHCE(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
172
172
  Task.init {
173
- try await self.cardSession?.startEmulation()
173
+ do {
174
+ try await self.cardSession?.startEmulation()
175
+ } catch(let error) {
176
+ reject("err_start_emulation", "Error trying to start emulation. \(error)", nil)
177
+ return
178
+ }
179
+
180
+ resolve(nil)
174
181
  }
175
182
  }
176
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icedevml/react-native-host-card-emulation",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",