@icedevml/react-native-host-card-emulation 0.2.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/LICENSE +21 -0
- package/README.md +17 -0
- package/RTNHCEModuleIOS.podspec +19 -0
- package/android/build.gradle +33 -0
- package/android/src/main/AndroidMainfest.xml +3 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/AESGCMUtil.java +89 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/BinaryUtils.java +66 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +92 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/IntentKeys.java +6 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +148 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +47 -0
- package/ios/RTNHCEModuleIOS-Bridging-Header.h +1 -0
- package/ios/RTNHCEModuleIOS.h +12 -0
- package/ios/RTNHCEModuleIOS.mm +125 -0
- package/ios/RTNHCEModuleIOSImpl.swift +190 -0
- package/js/NativeHCEModule.ts +33 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Package: React Native HCE Module
|
|
2
|
+
|
|
3
|
+
Host Card Emulation Module for React Native on iOS and Android platforms.
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> As per Apple's policy, **Host Card Emulation feature is only available for users based in the European
|
|
7
|
+
> Economic Area (EEA)** with an iPhone running iOS 17.4 or later. Applications require a special HCE entitlement
|
|
8
|
+
> in order to use this feature, which is managed by Apple. Read more in the official support article:
|
|
9
|
+
> "[HCE-based contactless NFC transactions for apps in the European Economic Area (EEA)](https://developer.apple.com/support/hce-transactions-in-apps/)".
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> Kindly please reference this project in your README if you are willing to create a derivative library.
|
|
13
|
+
|
|
14
|
+
## API Specification & Demo App
|
|
15
|
+
|
|
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)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "RTNHCEModuleIOS"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.description = package["description"]
|
|
10
|
+
s.homepage = package["homepage"]
|
|
11
|
+
s.license = package["license"]
|
|
12
|
+
s.platforms = { :ios => "11.0" }
|
|
13
|
+
s.author = package["author"]
|
|
14
|
+
s.source = { :git => package["repository"], :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = {prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
gradlePluginPortal()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath("com.android.tools.build:gradle:7.3.1")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
apply plugin: 'com.android.library'
|
|
15
|
+
apply plugin: 'com.facebook.react'
|
|
16
|
+
|
|
17
|
+
android {
|
|
18
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 33)
|
|
19
|
+
namespace "com.itsecrnd.rtnhceandroid"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
repositories {
|
|
23
|
+
maven {
|
|
24
|
+
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
|
25
|
+
url "$projectDir/../node_modules/react-native/android"
|
|
26
|
+
}
|
|
27
|
+
mavenCentral()
|
|
28
|
+
google()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dependencies {
|
|
32
|
+
implementation 'com.facebook.react:react-native:+'
|
|
33
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.itsecrnd.rtnhceandroid;
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint;
|
|
4
|
+
|
|
5
|
+
import javax.crypto.BadPaddingException;
|
|
6
|
+
import javax.crypto.Cipher;
|
|
7
|
+
import javax.crypto.IllegalBlockSizeException;
|
|
8
|
+
import javax.crypto.KeyGenerator;
|
|
9
|
+
import javax.crypto.NoSuchPaddingException;
|
|
10
|
+
import javax.crypto.SecretKey;
|
|
11
|
+
import javax.crypto.spec.GCMParameterSpec;
|
|
12
|
+
|
|
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
|
+
@SuppressLint("NewApi")
|
|
21
|
+
public class AESGCMUtil {
|
|
22
|
+
public static SecretKey generateKey() {
|
|
23
|
+
KeyGenerator keyGen;
|
|
24
|
+
try {
|
|
25
|
+
keyGen = KeyGenerator.getInstance("AES");
|
|
26
|
+
} catch (NoSuchAlgorithmException e) {
|
|
27
|
+
throw new RuntimeException("Failed to get AES algorithm.", e);
|
|
28
|
+
}
|
|
29
|
+
keyGen.init(256);
|
|
30
|
+
return keyGen.generateKey();
|
|
31
|
+
}
|
|
32
|
+
|
|
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
|
+
public static String encryptData(SecretKey key, String plaintext) {
|
|
40
|
+
try {
|
|
41
|
+
byte[] iv = new byte[32];
|
|
42
|
+
new SecureRandom().nextBytes(iv);
|
|
43
|
+
|
|
44
|
+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
45
|
+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
|
|
46
|
+
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
|
|
47
|
+
|
|
48
|
+
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
|
|
49
|
+
byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
|
|
50
|
+
|
|
51
|
+
return Base64.getEncoder().encodeToString(iv) + ';' +
|
|
52
|
+
Base64.getEncoder().encodeToString(ciphertextBytes);
|
|
53
|
+
} catch (NoSuchPaddingException
|
|
54
|
+
| NoSuchAlgorithmException
|
|
55
|
+
| IllegalBlockSizeException
|
|
56
|
+
| InvalidAlgorithmParameterException
|
|
57
|
+
| BadPaddingException
|
|
58
|
+
| InvalidKeyException e) {
|
|
59
|
+
throw new RuntimeException(e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public static String decryptData(SecretKey key, String ciphertext) {
|
|
64
|
+
try {
|
|
65
|
+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
66
|
+
String[] parts = ciphertext.split(";");
|
|
67
|
+
|
|
68
|
+
if (parts.length != 2) {
|
|
69
|
+
throw new IllegalArgumentException("Invalid ciphertext parameter.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
byte[] iv = Base64.getDecoder().decode(parts[0]);
|
|
73
|
+
byte[] ciphertextBytes = Base64.getDecoder().decode(parts[1]);
|
|
74
|
+
|
|
75
|
+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
|
|
76
|
+
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
|
|
77
|
+
|
|
78
|
+
byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
|
|
79
|
+
return new String(plaintextBytes, StandardCharsets.UTF_8);
|
|
80
|
+
} catch (NoSuchPaddingException
|
|
81
|
+
| IllegalBlockSizeException
|
|
82
|
+
| InvalidAlgorithmParameterException
|
|
83
|
+
| BadPaddingException
|
|
84
|
+
| InvalidKeyException
|
|
85
|
+
| NoSuchAlgorithmException e) {
|
|
86
|
+
throw new RuntimeException(e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Content of "BinaryUtils" class:
|
|
3
|
+
* Original source: https://github.com/googlearchive/android-CardEmulation
|
|
4
|
+
*
|
|
5
|
+
* ---
|
|
6
|
+
*
|
|
7
|
+
* Copyright (C) 2013 The Android Open Source Project
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
package com.itsecrnd.rtnhceandroid;
|
|
24
|
+
|
|
25
|
+
public class BinaryUtils {
|
|
26
|
+
/**
|
|
27
|
+
* Utility method to convert a byte array to a hexadecimal string.
|
|
28
|
+
*
|
|
29
|
+
* @param bytes Bytes to convert
|
|
30
|
+
* @return String, containing hexadecimal representation.
|
|
31
|
+
*/
|
|
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'};
|
|
34
|
+
char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles)
|
|
35
|
+
int v;
|
|
36
|
+
for (int j = 0; j < bytes.length; j++) {
|
|
37
|
+
v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned value
|
|
38
|
+
hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from upper nibble
|
|
39
|
+
hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character from lower nibble
|
|
40
|
+
}
|
|
41
|
+
return new String(hexChars);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Utility method to convert a hexadecimal string to a byte string.
|
|
46
|
+
*
|
|
47
|
+
* <p>Behavior with input strings containing non-hexadecimal characters is undefined.
|
|
48
|
+
*
|
|
49
|
+
* @param s String containing hexadecimal characters to convert
|
|
50
|
+
* @return Byte array generated from input
|
|
51
|
+
* @throws java.lang.IllegalArgumentException if input length is incorrect
|
|
52
|
+
*/
|
|
53
|
+
public static byte[] HexStringToByteArray(String s) throws IllegalArgumentException {
|
|
54
|
+
int len = s.length();
|
|
55
|
+
if (len % 2 == 1) {
|
|
56
|
+
throw new IllegalArgumentException("Hex string must have even number of characters");
|
|
57
|
+
}
|
|
58
|
+
byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
|
|
59
|
+
for (int i = 0; i < len; i += 2) {
|
|
60
|
+
// Convert each character into a integer (base-16), then bit-shift into place
|
|
61
|
+
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
|
62
|
+
+ Character.digit(s.charAt(i+1), 16));
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
package com.itsecrnd.rtnhceandroid;
|
|
2
|
+
|
|
3
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
|
|
4
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
|
|
5
|
+
|
|
6
|
+
import android.content.BroadcastReceiver;
|
|
7
|
+
import android.content.Context;
|
|
8
|
+
import android.content.Intent;
|
|
9
|
+
import android.content.IntentFilter;
|
|
10
|
+
import android.nfc.cardemulation.HostApduService;
|
|
11
|
+
import android.nfc.cardemulation.PollingFrame;
|
|
12
|
+
import android.os.Build;
|
|
13
|
+
import android.os.Bundle;
|
|
14
|
+
import android.util.Log;
|
|
15
|
+
|
|
16
|
+
import androidx.annotation.NonNull;
|
|
17
|
+
import androidx.annotation.RequiresApi;
|
|
18
|
+
|
|
19
|
+
import java.util.List;
|
|
20
|
+
import java.util.Locale;
|
|
21
|
+
|
|
22
|
+
import javax.crypto.SecretKey;
|
|
23
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
24
|
+
|
|
25
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
26
|
+
public class HCEService extends HostApduService {
|
|
27
|
+
private static final String TAG = "HCEService";
|
|
28
|
+
|
|
29
|
+
private SecretKey encSecretKey;
|
|
30
|
+
|
|
31
|
+
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
32
|
+
@Override
|
|
33
|
+
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);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
@Override
|
|
45
|
+
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
46
|
+
String capdu = AESGCMUtil.encryptData(encSecretKey, BinaryUtils.ByteArrayToHexString(command));
|
|
47
|
+
|
|
48
|
+
Intent intent = new Intent(INTENT_RECEIVE_C_APDU);
|
|
49
|
+
intent.setPackage(getApplicationContext().getPackageName());
|
|
50
|
+
intent.putExtra("capdu", capdu);
|
|
51
|
+
getApplicationContext().sendBroadcast(intent);
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
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
|
+
@Override
|
|
63
|
+
public void onCreate() {
|
|
64
|
+
Log.d(TAG, "Starting service");
|
|
65
|
+
|
|
66
|
+
IntentFilter filter = new IntentFilter();
|
|
67
|
+
filter.addAction(INTENT_SEND_R_APDU);
|
|
68
|
+
|
|
69
|
+
String encKey = getApplicationContext()
|
|
70
|
+
.getSharedPreferences("RTNHCEAndroidModuleBroadcastEnc", Context.MODE_PRIVATE)
|
|
71
|
+
.getString("encKey", "");
|
|
72
|
+
|
|
73
|
+
if (encKey.isEmpty()) {
|
|
74
|
+
throw new RuntimeException("Failed to load encKey.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
byte[] bArr = BinaryUtils.HexStringToByteArray(encKey);
|
|
78
|
+
encSecretKey = new SecretKeySpec(bArr, 0, bArr.length, "AES");
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* See a comment about registerReceiver() in RTNHCEAndroidModule.
|
|
82
|
+
*/
|
|
83
|
+
getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Override
|
|
87
|
+
public void onDeactivated(int reason) {
|
|
88
|
+
Log.d(TAG, "Finishing service: " + reason);
|
|
89
|
+
|
|
90
|
+
getApplicationContext().unregisterReceiver(receiver);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package com.itsecrnd.rtnhceandroid;
|
|
2
|
+
|
|
3
|
+
import static android.content.Context.RECEIVER_EXPORTED;
|
|
4
|
+
|
|
5
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
|
|
6
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
|
|
7
|
+
|
|
8
|
+
import android.content.BroadcastReceiver;
|
|
9
|
+
import android.content.Context;
|
|
10
|
+
import android.content.Intent;
|
|
11
|
+
import android.content.IntentFilter;
|
|
12
|
+
import android.os.Build;
|
|
13
|
+
import android.util.Log;
|
|
14
|
+
|
|
15
|
+
import androidx.annotation.NonNull;
|
|
16
|
+
import androidx.annotation.RequiresApi;
|
|
17
|
+
|
|
18
|
+
import com.facebook.react.bridge.Arguments;
|
|
19
|
+
import com.facebook.react.bridge.Promise;
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
21
|
+
import com.facebook.react.bridge.WritableMap;
|
|
22
|
+
|
|
23
|
+
import javax.crypto.SecretKey;
|
|
24
|
+
|
|
25
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
26
|
+
public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
27
|
+
public static final String TAG = "RTNHCEAndroidModule";
|
|
28
|
+
public static final String NAME = "NativeHCEModule";
|
|
29
|
+
|
|
30
|
+
private final SecretKey encSecretKey;
|
|
31
|
+
|
|
32
|
+
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
33
|
+
@Override
|
|
34
|
+
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);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
RTNHCEAndroidModule(ReactApplicationContext context) {
|
|
49
|
+
super(context);
|
|
50
|
+
String encKey;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
encSecretKey = AESGCMUtil.generateKey();
|
|
54
|
+
encKey = BinaryUtils.ByteArrayToHexString(encSecretKey.getEncoded());
|
|
55
|
+
} catch (Exception e) {
|
|
56
|
+
throw new RuntimeException("Failed to generate encryption key.", e);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getReactApplicationContext()
|
|
60
|
+
.getSharedPreferences("RTNHCEAndroidModuleBroadcastEnc", Context.MODE_PRIVATE)
|
|
61
|
+
.edit()
|
|
62
|
+
.putString("encKey", encKey)
|
|
63
|
+
.commit();
|
|
64
|
+
|
|
65
|
+
IntentFilter filter = new IntentFilter();
|
|
66
|
+
filter.addAction(INTENT_RECEIVE_C_APDU);
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* FIXME It seems like those intents are not going through across this module and HostApduService.
|
|
70
|
+
* This happens even though those things are within the same application and package.
|
|
71
|
+
* It also seems like there is no way to bind HostApduService (or it's a skill issue).
|
|
72
|
+
*
|
|
73
|
+
* For now we will just encrypt&authenticate all messages using AES/GCM and use exported
|
|
74
|
+
* receivers. This is not a perfect solution but it is going to be secure and sufficient for now.
|
|
75
|
+
*
|
|
76
|
+
* If somebody knows a better way then any pull requests would be greatly appreciated.
|
|
77
|
+
*/
|
|
78
|
+
getReactApplicationContext().getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Override
|
|
82
|
+
@NonNull
|
|
83
|
+
public String getName() {
|
|
84
|
+
return RTNHCEAndroidModule.NAME;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Override
|
|
88
|
+
public boolean isPlatformSupported() {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Override
|
|
93
|
+
public void acquireExclusiveNFC(Promise promise) {
|
|
94
|
+
promise.resolve(null);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Override
|
|
98
|
+
public boolean isExclusiveNFC() {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Override
|
|
103
|
+
public void beginSession(Promise promise) {
|
|
104
|
+
promise.resolve(null);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Override
|
|
108
|
+
public void setSessionAlertMessage(String message) {
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Override
|
|
113
|
+
public void invalidateSession() {
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Override
|
|
118
|
+
public boolean isSessionRunning() {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Override
|
|
123
|
+
public void startHCE(Promise promise) {
|
|
124
|
+
promise.resolve(null);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Override
|
|
128
|
+
public void stopHCE(String status, Promise promise) {
|
|
129
|
+
promise.resolve(null);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Override
|
|
133
|
+
public void respondAPDU(String rapdu, Promise promise) {
|
|
134
|
+
String encRapdu = AESGCMUtil.encryptData(encSecretKey, rapdu);
|
|
135
|
+
|
|
136
|
+
Intent intent = new Intent(INTENT_SEND_R_APDU);
|
|
137
|
+
intent.setPackage(getReactApplicationContext().getPackageName());
|
|
138
|
+
intent.putExtra("rapdu", encRapdu);
|
|
139
|
+
getReactApplicationContext().getApplicationContext().sendBroadcast(intent);
|
|
140
|
+
|
|
141
|
+
promise.resolve(null);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Override
|
|
145
|
+
public void isHCERunning(Promise promise) {
|
|
146
|
+
promise.resolve(true);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.itsecrnd.rtnhceandroid;
|
|
2
|
+
|
|
3
|
+
import android.os.Build;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.Nullable;
|
|
6
|
+
import androidx.annotation.RequiresApi;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.TurboReactPackage;
|
|
9
|
+
import com.facebook.react.bridge.NativeModule;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.module.model.ReactModuleInfo;
|
|
12
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
13
|
+
|
|
14
|
+
import java.util.HashMap;
|
|
15
|
+
import java.util.Map;
|
|
16
|
+
|
|
17
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
18
|
+
public class RTNHCEAndroidPackage extends TurboReactPackage {
|
|
19
|
+
@Nullable
|
|
20
|
+
@Override
|
|
21
|
+
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
|
|
22
|
+
if (name.equals(RTNHCEAndroidModule.NAME)) {
|
|
23
|
+
return new RTNHCEAndroidModule(reactContext);
|
|
24
|
+
} else {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override
|
|
30
|
+
public ReactModuleInfoProvider getReactModuleInfoProvider() {
|
|
31
|
+
return () -> {
|
|
32
|
+
final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
|
|
33
|
+
moduleInfos.put(
|
|
34
|
+
RTNHCEAndroidModule.NAME,
|
|
35
|
+
new ReactModuleInfo(
|
|
36
|
+
RTNHCEAndroidModule.NAME,
|
|
37
|
+
RTNHCEAndroidModule.NAME,
|
|
38
|
+
false, // canOverrideExistingModule
|
|
39
|
+
false, // needsEagerInit
|
|
40
|
+
true, // hasConstants
|
|
41
|
+
false, // isCxxModule
|
|
42
|
+
true // isTurboModule
|
|
43
|
+
));
|
|
44
|
+
return moduleInfos;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#import <React-RCTAppDelegate/RCTDefaultReactNativeFactoryDelegate.h>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <RTNHCEModuleSpec/RTNHCEModuleSpec.h>
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
7
|
+
@interface RTNHCEModuleIOS : NativeHCEModuleSpecBase <NativeHCEModuleSpec>
|
|
8
|
+
|
|
9
|
+
@end
|
|
10
|
+
|
|
11
|
+
NS_ASSUME_NONNULL_END
|
|
12
|
+
#endif
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#import "RTNHCEModuleIOS.h"
|
|
2
|
+
#import "RCTDefaultReactNativeFactoryDelegate.h"
|
|
3
|
+
#import "RTNHCEModuleIOS-Swift.h"
|
|
4
|
+
|
|
5
|
+
@implementation RTNHCEModuleIOS {
|
|
6
|
+
API_AVAILABLE(ios(17.4))
|
|
7
|
+
RTNHCEModuleIOSImpl *hcemoduleios;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
-(id) init {
|
|
11
|
+
self = [super init];
|
|
12
|
+
|
|
13
|
+
if (@available(iOS 17.4, *)) {
|
|
14
|
+
__weak RTNHCEModuleIOS *weakSelf = self;
|
|
15
|
+
|
|
16
|
+
hcemoduleios = [RTNHCEModuleIOSImpl new];
|
|
17
|
+
[hcemoduleios setEmitOnEventWithEmitOnEvent:^(NSString *type, NSString *arg){
|
|
18
|
+
[weakSelf emitOnEvent:@{@"type": type, @"arg": arg}];
|
|
19
|
+
}];
|
|
20
|
+
} else {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return(self);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
- (void)dealloc {
|
|
28
|
+
[hcemoduleios setEmitOnEventWithEmitOnEvent:^(NSString *type, NSString *arg){
|
|
29
|
+
// no-op
|
|
30
|
+
}];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
34
|
+
return std::make_shared<facebook::react::NativeHCEModuleSpecJSI>(params);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
- (nonnull NSNumber *)isPlatformSupported {
|
|
38
|
+
if (@available(iOS 17.4, *)) {
|
|
39
|
+
return @1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return @0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (void)acquireExclusiveNFC:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
46
|
+
if (@available(iOS 17.4, *)) {
|
|
47
|
+
return [hcemoduleios acquireExclusiveNFCWithResolve:resolve reject:reject];
|
|
48
|
+
} else {
|
|
49
|
+
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (nonnull NSNumber *)isExclusiveNFC {
|
|
54
|
+
if (@available(iOS 17.4, *)) {
|
|
55
|
+
return [hcemoduleios isExclusiveNFC] ? @1 : @0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return @0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- (void)beginSession:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
62
|
+
if (@available(iOS 17.4, *)) {
|
|
63
|
+
return [hcemoduleios beginSessionWithResolve:resolve reject:reject];
|
|
64
|
+
} else {
|
|
65
|
+
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (void)setSessionAlertMessage:(nonnull NSString *)message {
|
|
70
|
+
if (@available(iOS 17.4, *)) {
|
|
71
|
+
return [hcemoduleios setSessionAlertMessageWithMessage:message];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (void)invalidateSession {
|
|
76
|
+
if (@available(iOS 17.4, *)) {
|
|
77
|
+
[hcemoduleios invalidateSession];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (nonnull NSNumber *)isSessionRunning {
|
|
82
|
+
if (@available(iOS 17.4, *)) {
|
|
83
|
+
return [hcemoduleios isSessionRunning] ? @1 : @0;
|
|
84
|
+
} else {
|
|
85
|
+
return @0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
- (void)startHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
90
|
+
if (@available(iOS 17.4, *)) {
|
|
91
|
+
return [hcemoduleios startHCEWithResolve:resolve reject:reject];
|
|
92
|
+
} else {
|
|
93
|
+
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
- (void)stopHCE:(nonnull NSString *)status resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
98
|
+
if (@available(iOS 17.4, *)) {
|
|
99
|
+
return [hcemoduleios stopHCEWithStatus:status resolve:resolve reject:reject];
|
|
100
|
+
} else {
|
|
101
|
+
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
- (void)respondAPDU:(nonnull NSString *)rapdu resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
106
|
+
if (@available(iOS 17.4, *)) {
|
|
107
|
+
return [hcemoduleios respondAPDUWithRapdu:rapdu resolve:resolve reject:reject];
|
|
108
|
+
} else {
|
|
109
|
+
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
- (void)isHCERunning:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
114
|
+
if (@available(iOS 17.4, *)) {
|
|
115
|
+
[hcemoduleios isHCERunningWithResolve:resolve reject:reject];
|
|
116
|
+
} else {
|
|
117
|
+
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
+ (NSString *)moduleName {
|
|
122
|
+
return @"NativeHCEModule";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreNFC
|
|
3
|
+
|
|
4
|
+
extension StringProtocol {
|
|
5
|
+
func asHexArray() -> [UInt8] {
|
|
6
|
+
var startIndex = self.startIndex
|
|
7
|
+
return stride(from: 0, to: count, by: 2).compactMap { _ in
|
|
8
|
+
let endIndex = index(startIndex, offsetBy: 2, limitedBy: self.endIndex) ?? self.endIndex
|
|
9
|
+
defer { startIndex = endIndex }
|
|
10
|
+
return UInt8(self[startIndex..<endIndex], radix: 16)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
extension Data {
|
|
16
|
+
struct HexEncodingOptions: OptionSet {
|
|
17
|
+
let rawValue: Int
|
|
18
|
+
static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func hexEncodedString(options: HexEncodingOptions = []) -> String {
|
|
22
|
+
let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
|
|
23
|
+
return map { String(format: format, $0) }.joined()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@available(iOS 17.4, *)
|
|
28
|
+
@objc public class RTNHCEModuleIOSImpl: NSObject {
|
|
29
|
+
var cardSession: CardSession? = nil
|
|
30
|
+
var cardSessionInvalidated: Bool = true
|
|
31
|
+
var presentmentIntent: NFCPresentmentIntentAssertion? = nil
|
|
32
|
+
var receivedCardAPDU: CardSession.APDU? = nil
|
|
33
|
+
|
|
34
|
+
var emitOnEvent: ((NSString, NSString?) -> ())? = nil
|
|
35
|
+
|
|
36
|
+
@objc public func setEmitOnEvent(emitOnEvent: @escaping (NSString, NSString?) -> ()) {
|
|
37
|
+
self.emitOnEvent = emitOnEvent
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc public func acquireExclusiveNFC(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
41
|
+
Task.init {
|
|
42
|
+
if self.presentmentIntent != nil && self.presentmentIntent!.isValid {
|
|
43
|
+
reject("err_already_exists", "NFCPresentmentIntentAssertion already exists and is valid.", nil)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
self.presentmentIntent = nil
|
|
48
|
+
|
|
49
|
+
do {
|
|
50
|
+
self.presentmentIntent = try await NFCPresentmentIntentAssertion.acquire()
|
|
51
|
+
} catch (let error) {
|
|
52
|
+
// TODO exact error details here and in other places as well
|
|
53
|
+
reject("err_create_presentment", "Failed to create NFCPresentmentIntentAssertion: \(error)", nil)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolve(nil)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@objc public func isExclusiveNFC() -> Bool {
|
|
62
|
+
return self.presentmentIntent != nil && self.presentmentIntent!.isValid
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@objc public func beginSession(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
66
|
+
Task.init {
|
|
67
|
+
guard NFCReaderSession.readingAvailable else {
|
|
68
|
+
reject("err_nfc_reader_not_available", "Preflight check failed: NFCReaderSession.readingAvailable is false", nil)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
guard CardSession.isSupported else {
|
|
73
|
+
reject("err_card_session_unsupported", "Preflight check failed: CardSession.isSupported is false", nil)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
guard await CardSession.isEligible else {
|
|
78
|
+
reject("err_card_session_not_eligible", "Preflight check failed: CardSession.isEligible is false", nil)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
guard self.cardSessionInvalidated else {
|
|
83
|
+
reject("err_card_session_exists", "CardSession already exists.", nil)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
do {
|
|
88
|
+
self.cardSession = try await CardSession()
|
|
89
|
+
} catch (let error) {
|
|
90
|
+
reject("err_create_card_session", "Failed to create CardSession(): \(error)", nil)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
resolve(nil)
|
|
95
|
+
|
|
96
|
+
do {
|
|
97
|
+
for try await event in cardSession!.eventStream {
|
|
98
|
+
switch event {
|
|
99
|
+
case .sessionStarted:
|
|
100
|
+
cardSession!.alertMessage = String(localized: "Communicating with card reader.")
|
|
101
|
+
self.emitOnEvent!("sessionStarted", "")
|
|
102
|
+
|
|
103
|
+
case .readerDetected:
|
|
104
|
+
self.emitOnEvent!("readerDetected", "")
|
|
105
|
+
|
|
106
|
+
case .readerDeselected:
|
|
107
|
+
self.emitOnEvent!("readerDeselected", "")
|
|
108
|
+
|
|
109
|
+
case .received(let cardAPDU):
|
|
110
|
+
self.receivedCardAPDU = cardAPDU
|
|
111
|
+
self.emitOnEvent!("received", NSString(string:cardAPDU.payload.hexEncodedString()))
|
|
112
|
+
|
|
113
|
+
case .sessionInvalidated(let reason):
|
|
114
|
+
self.cardSessionInvalidated = true
|
|
115
|
+
self.emitOnEvent!("sessionInvalidated", "\(reason)" as NSString)
|
|
116
|
+
self.cardSession = nil
|
|
117
|
+
|
|
118
|
+
@unknown default:
|
|
119
|
+
fatalError("Received unknown CardSession event.")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (let err) {
|
|
123
|
+
self.emitOnEvent!("internalError", "\(err)" as NSString)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@objc public func setSessionAlertMessage(message: NSString) {
|
|
129
|
+
self.cardSession?.alertMessage = String(message)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@objc public func invalidateSession() {
|
|
133
|
+
self.cardSession?.invalidate()
|
|
134
|
+
self.cardSession = nil
|
|
135
|
+
self.cardSessionInvalidated = true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@objc public func isSessionRunning() -> Bool {
|
|
139
|
+
return self.cardSession != nil && !self.cardSessionInvalidated
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@objc public func isHCERunning(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
143
|
+
Task.init {
|
|
144
|
+
guard self.cardSession != nil else {
|
|
145
|
+
return resolve(false)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let isRunning = await self.cardSession!.isEmulationInProgress
|
|
149
|
+
resolve(isRunning)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@objc public func respondAPDU(rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
154
|
+
Task.init {
|
|
155
|
+
guard let capdu = self.receivedCardAPDU else {
|
|
156
|
+
reject("err_no_apdu_received", "There is no received APDU to respond to.", nil)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
do {
|
|
161
|
+
try await capdu.respond(response: Data.init(String(rapdu).asHexArray()))
|
|
162
|
+
} catch (let error) {
|
|
163
|
+
reject("err_apdu_respond", "Error trying to respond to APDU. \(error)", nil)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
resolve(nil)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@objc public func startHCE(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
172
|
+
Task.init {
|
|
173
|
+
try await self.cardSession?.startEmulation()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@objc public func stopHCE(status: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
178
|
+
Task.init {
|
|
179
|
+
let sStatus = String(status)
|
|
180
|
+
|
|
181
|
+
if (sStatus == "success") {
|
|
182
|
+
await self.cardSession?.stopEmulation(status: .success)
|
|
183
|
+
} else {
|
|
184
|
+
await self.cardSession?.stopEmulation(status: .failure)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resolve(nil)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {TurboModule, CodegenTypes} from 'react-native';
|
|
2
|
+
import {TurboModuleRegistry} from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type HCEModuleIOSEventType = 'sessionStarted' | 'readerDetected' | 'readerDeselected' | 'received' | 'sessionInvalidated';
|
|
5
|
+
export type HCEModuleIOSStopReason = 'success' | 'failure';
|
|
6
|
+
|
|
7
|
+
export type HCEModuleIOSEvent = {
|
|
8
|
+
type: HCEModuleIOSEventType
|
|
9
|
+
arg: string | null,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Spec extends TurboModule {
|
|
13
|
+
isPlatformSupported(): boolean;
|
|
14
|
+
|
|
15
|
+
acquireExclusiveNFC(): Promise<boolean>;
|
|
16
|
+
isExclusiveNFC(): boolean;
|
|
17
|
+
|
|
18
|
+
beginSession(): Promise<void>;
|
|
19
|
+
setSessionAlertMessage(message: string): void;
|
|
20
|
+
invalidateSession(): void;
|
|
21
|
+
isSessionRunning(): boolean;
|
|
22
|
+
|
|
23
|
+
startHCE(): Promise<void>;
|
|
24
|
+
stopHCE(status: HCEModuleIOSStopReason): Promise<void>;
|
|
25
|
+
respondAPDU(rapdu: string): Promise<void>;
|
|
26
|
+
isHCERunning(): Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
readonly onEvent: CodegenTypes.EventEmitter<HCEModuleIOSEvent>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default TurboModuleRegistry.getEnforcing<Spec>(
|
|
32
|
+
'NativeHCEModule',
|
|
33
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icedevml/react-native-host-card-emulation",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Host Card Emulation (HCE) React Native Turbo Module for iOS and Android platforms.",
|
|
5
|
+
"react-native": "js/index",
|
|
6
|
+
"source": "js/index",
|
|
7
|
+
"files": [
|
|
8
|
+
"js",
|
|
9
|
+
"android",
|
|
10
|
+
"ios",
|
|
11
|
+
"RTNHCEModuleIOS.podspec",
|
|
12
|
+
"!android/build",
|
|
13
|
+
"!ios/build",
|
|
14
|
+
"!**/__tests__",
|
|
15
|
+
"!**/__fixtures__",
|
|
16
|
+
"!**/__mocks__"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"ios",
|
|
21
|
+
"nfc",
|
|
22
|
+
"hce"
|
|
23
|
+
],
|
|
24
|
+
"repository": "https://github.com/icedevml/react-native-host-card-emulation",
|
|
25
|
+
"author": "Michal Leszczynski ml@icedev.pl (https://github.com/icedevml)",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/icedevml/react-native-host-card-emulation/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/icedevml/react-native-host-card-emulation#readme",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "*",
|
|
33
|
+
"react-native": "*"
|
|
34
|
+
},
|
|
35
|
+
"codegenConfig": {
|
|
36
|
+
"name": "RTNHCEModuleSpec",
|
|
37
|
+
"type": "modules",
|
|
38
|
+
"jsSrcsDir": "js",
|
|
39
|
+
"android": {
|
|
40
|
+
"javaPackageName": "com.itsecrnd.rtnhceandroid"
|
|
41
|
+
},
|
|
42
|
+
"ios": {
|
|
43
|
+
"modulesProvider": {
|
|
44
|
+
"NativeHCEModule": "RTNHCEModuleIOS"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|