@icedevml/react-native-host-card-emulation 0.2.1 → 0.3.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
@@ -8,10 +8,95 @@ Host Card Emulation Module for React Native on iOS and Android platforms.
8
8
  > in order to use this feature, which is managed by Apple. Read more in the official support article:
9
9
  > "[HCE-based contactless NFC transactions for apps in the European Economic Area (EEA)](https://developer.apple.com/support/hce-transactions-in-apps/)".
10
10
 
11
- > [!NOTE]
12
- > Kindly please reference this project in your README if you are willing to create a derivative library.
11
+ ## Prerequisites
12
+ This package was designed for React Native [New Architecture](https://reactnative.dev/architecture/landing-page) only.
13
+ Make sure that your project has it enabled. Note that the New Architecture is enabled by default for React Native 0.76+ projects.
14
+
15
+ ### iOS
16
+ 1. Ensure that you are enrolled as organization (not individual) in Apple Developer Program, and that you are established in the European Economic Area (EEA).
17
+ 2. Ensure that you meet other [non-technical requirements imposed by Apple](https://developer.apple.com/support/hce-transactions-in-apps/#requirements-and-availability).
18
+ 3. Find out the Bundle ID of your existing application or [register a new Bundle ID](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle).
19
+ 4. [Request the HCE Entitlement](https://developer.apple.com/contact/request/hce-entitlement/) for your application's bundle ID.
20
+ 5. Wait until the entitlement is approved by Apple.
21
+
22
+ ### Android
23
+ *(no prerequisite steps are required for Android)*
24
+
25
+ ## Installation
26
+ Install `@icedevml/react-native-host-card-emulation` package within your React Native project. Then, follow the subsections below for each platform that you need to support.
27
+
28
+ ### iOS
29
+ 1. Make sure to set up a proper signing team and bundle identifier in "Signing & Capabilities" configuration section of your project.
30
+ 2. Add Host Card Emulation (HCE) entitlement with desired AIDs. This could be done either through XCode (by clicking "+ Capability") or within the entitlements XML file directly. Example:
31
+ ```xml
32
+ <?xml version="1.0" encoding="UTF-8"?>
33
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
34
+ <plist version="1.0">
35
+ <dict>
36
+ <key>com.apple.developer.nfc.hce</key>
37
+ <true/>
38
+ <key>com.apple.developer.nfc.hce.iso7816.select-identifier-prefixes</key>
39
+ <array>
40
+ <string>F001020304</string>
41
+ <string>F00102030405</string>
42
+ <string>F0010203040506</string>
43
+ </array>
44
+ </dict>
45
+ </plist>
46
+ ```
47
+ Replace `F001020304`, `F00102030405`, `F0010203040506` with the ISO7816 AIDs that you want to support.
48
+ 3. In the `ios/` app's subdirectory, run `bundle install && bundle exec pod install`.
49
+
50
+ ### Android
51
+ 1. In `android/app/src/main/AndroidManifest.xml` add:
52
+ ```diff
53
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
54
+ ...
55
+
56
+ + <uses-permission android:name="android.permission.NFC" />
57
+ + <uses-feature android:name="android.hardware.nfc.hce" android:required="true" />
58
+
59
+ ...
60
+
61
+ <application
62
+ ...
63
+ >
64
+
65
+ ...
66
+
67
+ + <service
68
+ + android:name="com.itsecrnd.rtnhceandroid.HCEService"
69
+ + android:exported="true"
70
+ + android:enabled="false"
71
+ + android:permission="android.permission.BIND_NFC_SERVICE" >
72
+ + <intent-filter>
73
+ + <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
74
+ + <category android:name="android.intent.category.DEFAULT"/>
75
+ + </intent-filter>
76
+ +
77
+ + <meta-data
78
+ + android:name="android.nfc.cardemulation.host_apdu_service"
79
+ + android:resource="@xml/aid_list" />
80
+ + </service>
81
+ </application>
82
+ </manifest>
83
+ ```
84
+ 2. Create file `android/app/src/main/res/xml/aid_list.xml`:
85
+ ```xml
86
+ <host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
87
+ android:description="@string/app_name"
88
+ android:requireDeviceUnlock="false">
89
+ <aid-group android:category="other"
90
+ android:description="@string/app_name">
91
+ <aid-filter android:name="F001020304" />
92
+ <aid-filter android:name="F00102030405" />
93
+ <aid-filter android:name="F0010203040506" />
94
+ </aid-group>
95
+ </host-apdu-service>
96
+ ```
97
+ Replace `F001020304`, `F00102030405`, `F0010203040506` with the ISO7816 AIDs that you want to support.
13
98
 
14
99
  ## API Specification & Demo App
15
100
 
16
- * [module API (declarations)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/react-native-hce-module/js/NativeHCEModule.ts)
17
- * [demo app's code (example usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-ios-app/App.tsx)
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)
@@ -2,6 +2,13 @@ package com.itsecrnd.rtnhceandroid;
2
2
 
3
3
  import android.annotation.SuppressLint;
4
4
 
5
+ import java.nio.charset.StandardCharsets;
6
+ import java.security.InvalidAlgorithmParameterException;
7
+ import java.security.InvalidKeyException;
8
+ import java.security.NoSuchAlgorithmException;
9
+ import java.security.SecureRandom;
10
+ import java.util.Base64;
11
+
5
12
  import javax.crypto.BadPaddingException;
6
13
  import javax.crypto.Cipher;
7
14
  import javax.crypto.IllegalBlockSizeException;
@@ -10,15 +17,14 @@ import javax.crypto.NoSuchPaddingException;
10
17
  import javax.crypto.SecretKey;
11
18
  import javax.crypto.spec.GCMParameterSpec;
12
19
 
13
- import java.nio.charset.StandardCharsets;
14
- import java.security.InvalidAlgorithmParameterException;
15
- import java.security.InvalidKeyException;
16
- import java.security.NoSuchAlgorithmException;
17
- import java.security.SecureRandom;
18
- import java.util.Base64;
19
-
20
20
  @SuppressLint("NewApi")
21
21
  public class AESGCMUtil {
22
+ public static String randomString() {
23
+ byte[] rnd = new byte[32];
24
+ new SecureRandom().nextBytes(rnd);
25
+ return BinaryUtils.ByteArrayToHexString(rnd);
26
+ }
27
+
22
28
  public static SecretKey generateKey() {
23
29
  KeyGenerator keyGen;
24
30
  try {
@@ -30,12 +36,6 @@ public class AESGCMUtil {
30
36
  return keyGen.generateKey();
31
37
  }
32
38
 
33
- public static byte[] generateNonce(int size) {
34
- byte[] nonce = new byte[size];
35
- new SecureRandom().nextBytes(nonce); // Fill nonce with random bytes
36
- return nonce;
37
- }
38
-
39
39
  public static String encryptData(SecretKey key, String plaintext) {
40
40
  try {
41
41
  byte[] iv = new byte[32];
@@ -49,14 +49,14 @@ public class AESGCMUtil {
49
49
  byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
50
50
 
51
51
  return Base64.getEncoder().encodeToString(iv) + ';' +
52
- Base64.getEncoder().encodeToString(ciphertextBytes);
52
+ Base64.getEncoder().encodeToString(ciphertextBytes);
53
53
  } catch (NoSuchPaddingException
54
54
  | NoSuchAlgorithmException
55
55
  | IllegalBlockSizeException
56
56
  | InvalidAlgorithmParameterException
57
57
  | BadPaddingException
58
58
  | InvalidKeyException e) {
59
- throw new RuntimeException(e);
59
+ throw new RuntimeException("Crypto error.", e);
60
60
  }
61
61
  }
62
62
 
@@ -83,7 +83,7 @@ public class AESGCMUtil {
83
83
  | BadPaddingException
84
84
  | InvalidKeyException
85
85
  | NoSuchAlgorithmException e) {
86
- throw new RuntimeException(e);
86
+ throw new RuntimeException("Crypto error.", e);
87
87
  }
88
88
  }
89
89
  }
@@ -30,7 +30,7 @@ public class BinaryUtils {
30
30
  * @return String, containing hexadecimal representation.
31
31
  */
32
32
  public static String ByteArrayToHexString(byte[] bytes) {
33
- final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
33
+ final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
34
34
  char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles)
35
35
  int v;
36
36
  for (int j = 0; j < bytes.length; j++) {
@@ -59,7 +59,7 @@ public class BinaryUtils {
59
59
  for (int i = 0; i < len; i += 2) {
60
60
  // Convert each character into a integer (base-16), then bit-shift into place
61
61
  data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
62
- + Character.digit(s.charAt(i+1), 16));
62
+ + Character.digit(s.charAt(i + 1), 16));
63
63
  }
64
64
  return data;
65
65
  }
@@ -1,5 +1,7 @@
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;
3
5
  import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
4
6
  import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
5
7
 
@@ -8,15 +10,12 @@ import android.content.Context;
8
10
  import android.content.Intent;
9
11
  import android.content.IntentFilter;
10
12
  import android.nfc.cardemulation.HostApduService;
11
- import android.nfc.cardemulation.PollingFrame;
12
13
  import android.os.Build;
13
14
  import android.os.Bundle;
14
15
  import android.util.Log;
15
16
 
16
- import androidx.annotation.NonNull;
17
17
  import androidx.annotation.RequiresApi;
18
18
 
19
- import java.util.List;
20
19
  import java.util.Locale;
21
20
 
22
21
  import javax.crypto.SecretKey;
@@ -31,12 +30,20 @@ public class HCEService extends HostApduService {
31
30
  private final BroadcastReceiver receiver = new BroadcastReceiver() {
32
31
  @Override
33
32
  public void onReceive(Context context, Intent intent) {
34
- String action = intent.getAction();
35
-
36
- if (action != null && action.equals(INTENT_SEND_R_APDU)) {
37
- String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("rapdu"));
38
- byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
39
- sendResponseApdu(dec);
33
+ try {
34
+ String action = intent.getAction();
35
+
36
+ if (action != null) {
37
+ AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
38
+
39
+ if (action.equals(INTENT_SEND_R_APDU)) {
40
+ String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("rapdu"));
41
+ byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
42
+ sendResponseApdu(dec);
43
+ }
44
+ }
45
+ } catch (Exception e) {
46
+ Log.e(TAG, "Failed to handle broadcast in HCEService.", e);
40
47
  }
41
48
  }
42
49
  };
@@ -47,27 +54,20 @@ public class HCEService extends HostApduService {
47
54
 
48
55
  Intent intent = new Intent(INTENT_RECEIVE_C_APDU);
49
56
  intent.setPackage(getApplicationContext().getPackageName());
57
+ intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
50
58
  intent.putExtra("capdu", capdu);
51
59
  getApplicationContext().sendBroadcast(intent);
52
60
 
53
61
  return null;
54
62
  }
55
63
 
56
- @Override
57
- public void processPollingFrames(@NonNull List<PollingFrame> frames) {
58
- super.processPollingFrames(frames);
59
- Log.i(TAG, "Polling frames: " + frames.size());
60
- }
61
-
62
64
  @Override
63
65
  public void onCreate() {
64
- Log.d(TAG, "Starting service");
65
-
66
- IntentFilter filter = new IntentFilter();
67
- filter.addAction(INTENT_SEND_R_APDU);
66
+ Log.d(TAG, "HCEService onCreate");
68
67
 
68
+ String prefsName = getApplicationContext().getPackageName() + ".nativehcemodule";
69
69
  String encKey = getApplicationContext()
70
- .getSharedPreferences("RTNHCEAndroidModuleBroadcastEnc", Context.MODE_PRIVATE)
70
+ .getSharedPreferences(prefsName, Context.MODE_PRIVATE)
71
71
  .getString("encKey", "");
72
72
 
73
73
  if (encKey.isEmpty()) {
@@ -80,13 +80,25 @@ public class HCEService extends HostApduService {
80
80
  /*
81
81
  * See a comment about registerReceiver() in RTNHCEAndroidModule.
82
82
  */
83
+ IntentFilter filter = new IntentFilter();
84
+ filter.addAction(INTENT_SEND_R_APDU);
83
85
  getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
86
+
87
+ Intent intent = new Intent(INTENT_READER_DETECT);
88
+ intent.setPackage(getApplicationContext().getPackageName());
89
+ intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
90
+ getApplicationContext().sendBroadcast(intent);
84
91
  }
85
92
 
86
93
  @Override
87
94
  public void onDeactivated(int reason) {
88
- Log.d(TAG, "Finishing service: " + reason);
95
+ Log.d(TAG, "HCEService onDeactivated: " + reason);
89
96
 
90
97
  getApplicationContext().unregisterReceiver(receiver);
98
+
99
+ Intent intent = new Intent(INTENT_READER_LOST);
100
+ intent.setPackage(getApplicationContext().getPackageName());
101
+ intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
102
+ getApplicationContext().sendBroadcast(intent);
91
103
  }
92
104
  }
@@ -3,4 +3,6 @@ package com.itsecrnd.rtnhceandroid;
3
3
  public class IntentKeys {
4
4
  public static final String INTENT_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.receivecapdu";
5
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";
6
8
  }
@@ -1,14 +1,17 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
3
  import static android.content.Context.RECEIVER_EXPORTED;
4
-
4
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_DETECT;
5
+ import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_LOST;
5
6
  import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
6
7
  import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
7
8
 
8
9
  import android.content.BroadcastReceiver;
10
+ import android.content.ComponentName;
9
11
  import android.content.Context;
10
12
  import android.content.Intent;
11
13
  import android.content.IntentFilter;
14
+ import android.content.pm.PackageManager;
12
15
  import android.os.Build;
13
16
  import android.util.Log;
14
17
 
@@ -28,25 +31,56 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
28
31
  public static final String NAME = "NativeHCEModule";
29
32
 
30
33
  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
+
48
+ private void sendEvent(final String type, final String arg) {
49
+ WritableMap map = Arguments.createMap();
50
+ map.putString("type", type);
51
+ map.putString("arg", arg);
52
+ emitOnEvent(map);
53
+ }
31
54
 
32
55
  private final BroadcastReceiver receiver = new BroadcastReceiver() {
33
56
  @Override
34
57
  public void onReceive(Context context, Intent intent) {
35
- String action = intent.getAction();
36
-
37
- if (action != null && action.equals(INTENT_RECEIVE_C_APDU)) {
38
- String capdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("capdu"));
39
-
40
- WritableMap map = Arguments.createMap();
41
- map.putString("type", "received");
42
- map.putString("arg", capdu);
43
- emitOnEvent(map);
58
+ try {
59
+ String action = intent.getAction();
60
+
61
+ if (action != null) {
62
+ AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
63
+
64
+ if (action.equals(INTENT_RECEIVE_C_APDU)) {
65
+ String capdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("capdu"));
66
+ sendEvent("received", capdu);
67
+ } else if (action.equals(INTENT_READER_DETECT)) {
68
+ sendEvent("readerDetected", "");
69
+ } else if (action.equals(INTENT_READER_LOST)) {
70
+ sendEvent("readerDeselected", "");
71
+ }
72
+ }
73
+ } catch (Exception e) {
74
+ Log.e(TAG, "Failed to handle broadcast in RTNHCEAndroidModule.", e);
44
75
  }
45
76
  }
46
77
  };
47
78
 
48
79
  RTNHCEAndroidModule(ReactApplicationContext context) {
49
80
  super(context);
81
+
82
+ this.sessionOpen = false;
83
+
50
84
  String encKey;
51
85
 
52
86
  try {
@@ -56,14 +90,17 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
56
90
  throw new RuntimeException("Failed to generate encryption key.", e);
57
91
  }
58
92
 
93
+ String prefsName = getReactApplicationContext().getPackageName() + ".nativehcemodule";
59
94
  getReactApplicationContext()
60
- .getSharedPreferences("RTNHCEAndroidModuleBroadcastEnc", Context.MODE_PRIVATE)
95
+ .getSharedPreferences(prefsName, Context.MODE_PRIVATE)
61
96
  .edit()
62
97
  .putString("encKey", encKey)
63
98
  .commit();
64
99
 
65
100
  IntentFilter filter = new IntentFilter();
66
101
  filter.addAction(INTENT_RECEIVE_C_APDU);
102
+ filter.addAction(INTENT_READER_DETECT);
103
+ filter.addAction(INTENT_READER_LOST);
67
104
 
68
105
  /*
69
106
  * FIXME It seems like those intents are not going through across this module and HostApduService.
@@ -86,55 +123,73 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
86
123
 
87
124
  @Override
88
125
  public boolean isPlatformSupported() {
89
- return true;
126
+ PackageManager pm = getReactApplicationContext().getPackageManager();
127
+ return pm.hasSystemFeature(PackageManager.FEATURE_NFC)
128
+ && pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
90
129
  }
91
130
 
92
131
  @Override
93
132
  public void acquireExclusiveNFC(Promise promise) {
94
- promise.resolve(null);
133
+ promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
95
134
  }
96
135
 
97
136
  @Override
98
137
  public boolean isExclusiveNFC() {
138
+ // unsupported on Android
99
139
  return false;
100
140
  }
101
141
 
102
142
  @Override
103
143
  public void beginSession(Promise promise) {
104
- promise.resolve(null);
144
+ if (this.sessionOpen) {
145
+ promise.reject("err_card_session_exists", "Session already exists.");
146
+ } else {
147
+ this.sessionOpen = true;
148
+ promise.resolve(null);
149
+ sendEvent("sessionStarted", "");
150
+ }
105
151
  }
106
152
 
107
153
  @Override
108
154
  public void setSessionAlertMessage(String message) {
109
-
155
+ // unsupported on Android, no-op
110
156
  }
111
157
 
112
158
  @Override
113
159
  public void invalidateSession() {
114
-
160
+ toggleHceService(false);
161
+ this.sessionOpen = false;
162
+ sendEvent("sessionInvalidated", "");
115
163
  }
116
164
 
117
165
  @Override
118
166
  public boolean isSessionRunning() {
119
- return true;
167
+ return this.sessionOpen;
120
168
  }
121
169
 
122
170
  @Override
123
171
  public void startHCE(Promise promise) {
172
+ toggleHceService(true);
124
173
  promise.resolve(null);
125
174
  }
126
175
 
127
176
  @Override
128
177
  public void stopHCE(String status, Promise promise) {
178
+ toggleHceService(false);
129
179
  promise.resolve(null);
130
180
  }
131
181
 
132
182
  @Override
133
183
  public void respondAPDU(String rapdu, Promise promise) {
184
+ if (!this.sessionOpen) {
185
+ return;
186
+ }
187
+
134
188
  String encRapdu = AESGCMUtil.encryptData(encSecretKey, rapdu);
135
189
 
136
190
  Intent intent = new Intent(INTENT_SEND_R_APDU);
137
191
  intent.setPackage(getReactApplicationContext().getPackageName());
192
+ intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
138
193
  intent.putExtra("rapdu", encRapdu);
139
194
  getReactApplicationContext().getApplicationContext().sendBroadcast(intent);
140
195
 
@@ -143,6 +198,10 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
143
198
 
144
199
  @Override
145
200
  public void isHCERunning(Promise promise) {
146
- promise.resolve(true);
201
+ int enabledSetting = getReactApplicationContext()
202
+ .getPackageManager()
203
+ .getComponentEnabledSetting(new ComponentName(getReactApplicationContext(), HCEService.class));
204
+
205
+ promise.resolve(enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
147
206
  }
148
207
  }
@@ -40,7 +40,7 @@ public class RTNHCEAndroidPackage extends TurboReactPackage {
40
40
  true, // hasConstants
41
41
  false, // isCxxModule
42
42
  true // isTurboModule
43
- ));
43
+ ));
44
44
  return moduleInfos;
45
45
  };
46
46
  }
@@ -1,31 +1,89 @@
1
1
  import type {TurboModule, CodegenTypes} from 'react-native';
2
2
  import {TurboModuleRegistry} from 'react-native';
3
3
 
4
- export type HCEModuleIOSEventType = 'sessionStarted' | 'readerDetected' | 'readerDeselected' | 'received' | 'sessionInvalidated';
5
- export type HCEModuleIOSStopReason = 'success' | 'failure';
4
+ export type HCEModuleEventType = 'sessionStarted'
5
+ | 'sessionInvalidated'
6
+ /**
7
+ * iOS: Whether the reader field was detected.
8
+ * Android: Whether the reader had sent the select command to us.
9
+ */
10
+ | 'readerDetected'
11
+ /**
12
+ * Whether our HCE app was deselected or the reader was lost.
13
+ */
14
+ | 'readerDeselected'
15
+ /**
16
+ * Received C-APDU (hex-encoded string in "arg" key) from the reader.
17
+ * Respond by calling respondAPDU(rapdu).
18
+ */
19
+ | 'received';
20
+ export type HCEModuleStopReason = 'success' | 'failure';
6
21
 
7
- export type HCEModuleIOSEvent = {
8
- type: HCEModuleIOSEventType
22
+ export type HCEModuleEvent = {
23
+ type: HCEModuleEventType
9
24
  arg: string | null,
10
25
  }
11
26
 
12
27
  export interface Spec extends TurboModule {
28
+ /**
29
+ * Checks if a given smartphone supports required HCE features.
30
+ */
13
31
  isPlatformSupported(): boolean;
14
32
 
15
- acquireExclusiveNFC(): Promise<boolean>;
33
+ /**
34
+ * iOS: Tries to acquire NFCPresentmentIntentAssertion.
35
+ * Android: Not supported (throws an exception).
36
+ */
37
+ acquireExclusiveNFC(): Promise<void>;
38
+ /**
39
+ * iOS: Whether the NFCPresentmentIntentAssertion is acquired and still valid.
40
+ * Android: Always false.
41
+ */
16
42
  isExclusiveNFC(): boolean;
17
43
 
44
+ /**
45
+ * Begin card operation session.
46
+ * iOS: Create CardSession.
47
+ * Android: Sets an internal flag that the session is open.
48
+ */
18
49
  beginSession(): Promise<void>;
50
+ /**
51
+ * iOS: Set alert message on the NFC system UI.
52
+ * Android: NO-OP.
53
+ */
19
54
  setSessionAlertMessage(message: string): void;
55
+ /**
56
+ * iOS: Invalidate CardSession.
57
+ * Android: Set an internal flag that the session is closed and disable HCE service.
58
+ */
20
59
  invalidateSession(): void;
60
+ /**
61
+ * iOS: Checks if CardSession is running.
62
+ * Android: Checks if an internal flag is set.
63
+ */
21
64
  isSessionRunning(): boolean;
22
65
 
66
+ /**
67
+ * iOS: Start the card emulation.
68
+ * Android: Enable HCE service.
69
+ */
23
70
  startHCE(): Promise<void>;
24
- stopHCE(status: HCEModuleIOSStopReason): Promise<void>;
71
+ /**
72
+ * iOS: Stop the card emulation with "success"/"failure" UI status.
73
+ * Android: Disable HCE service.
74
+ */
75
+ stopHCE(status: HCEModuleStopReason): Promise<void>;
76
+ /**
77
+ * Send R-APDU (hex-encoded string) as a response to the last received C-APDU event.
78
+ */
25
79
  respondAPDU(rapdu: string): Promise<void>;
80
+ /**
81
+ * iOS: Checks if the card emulation is running.
82
+ * Android: Checks if the HCE service is enabled.
83
+ */
26
84
  isHCERunning(): Promise<boolean>;
27
85
 
28
- readonly onEvent: CodegenTypes.EventEmitter<HCEModuleIOSEvent>;
86
+ readonly onEvent: CodegenTypes.EventEmitter<HCEModuleEvent>;
29
87
  }
30
88
 
31
89
  export default TurboModuleRegistry.getEnforcing<Spec>(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icedevml/react-native-host-card-emulation",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Michal Leszczynski
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.