@utapza/expo-mifare-scanner 1.0.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 +177 -0
- package/android/build.gradle +41 -0
- package/android/src/main/java/expo/modules/mifarescanner/MifareScanner.java +404 -0
- package/android/src/main/java/expo/modules/mifarescanner/MifareScannerModule.kt +71 -0
- package/app.plugin.js +44 -0
- package/expo-module.config.json +6 -0
- package/package.json +34 -0
- package/src/index.js +167 -0
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# @utapza/expo-mifare-scanner
|
|
2
|
+
|
|
3
|
+
Expo module for scanning MIFARE Classic NFC cards on Android.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Scan MIFARE Classic cards
|
|
8
|
+
- ✅ Read NDEF records
|
|
9
|
+
- ✅ Read raw data from multiple sectors
|
|
10
|
+
- ✅ Automatic JSON extraction
|
|
11
|
+
- ✅ Simple async API
|
|
12
|
+
- ✅ Automatic start/stop handling
|
|
13
|
+
- ✅ Timeout support
|
|
14
|
+
- ✅ Comprehensive error handling
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @utapza/expo-mifare-scanner
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Note**: This module requires a development build. It does not work in Expo Go.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Simple API (Recommended)
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import { readNfcTag } from '@utapza/expo-mifare-scanner';
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Start scanning, wait for tag, automatically stop
|
|
33
|
+
const cardData = await readNfcTag({ timeout: 10000 });
|
|
34
|
+
|
|
35
|
+
console.log('Card UID:', cardData.uid);
|
|
36
|
+
console.log('Card Data:', cardData.data); // JSON string
|
|
37
|
+
console.log('Raw Data:', cardData.rawData); // Hex string
|
|
38
|
+
console.log('Timestamp:', cardData.timestamp);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Scan failed:', error.message);
|
|
41
|
+
// Possible errors:
|
|
42
|
+
// - "NFC is not enabled on this device"
|
|
43
|
+
// - "NFC scan timed out after 10000ms"
|
|
44
|
+
// - "Failed to start NFC scanning"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Advanced API (Event-based)
|
|
49
|
+
|
|
50
|
+
If you need more control, you can use the event-based API:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import {
|
|
54
|
+
startScanning,
|
|
55
|
+
stopScanning,
|
|
56
|
+
addCardScannedListener,
|
|
57
|
+
removeCardScannedListener,
|
|
58
|
+
isNfcEnabled
|
|
59
|
+
} from '@utapza/expo-mifare-scanner';
|
|
60
|
+
|
|
61
|
+
// Check if NFC is enabled
|
|
62
|
+
const enabled = await isNfcEnabled();
|
|
63
|
+
|
|
64
|
+
if (enabled) {
|
|
65
|
+
// Set up listener
|
|
66
|
+
const subscription = addCardScannedListener((event) => {
|
|
67
|
+
console.log('Card scanned:', event);
|
|
68
|
+
// Stop scanning when done
|
|
69
|
+
stopScanning();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Start scanning
|
|
73
|
+
await startScanning();
|
|
74
|
+
|
|
75
|
+
// Later, clean up
|
|
76
|
+
removeCardScannedListener(subscription);
|
|
77
|
+
await stopScanning();
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
82
|
+
|
|
83
|
+
### `readNfcTag(options?)`
|
|
84
|
+
|
|
85
|
+
Simplified async function that handles the entire scan lifecycle.
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `options.timeout` (number, optional): Timeout in milliseconds. Default: 30000 (30 seconds)
|
|
89
|
+
|
|
90
|
+
**Returns:** `Promise<CardData>`
|
|
91
|
+
|
|
92
|
+
**CardData:**
|
|
93
|
+
```typescript
|
|
94
|
+
{
|
|
95
|
+
uid: string; // Card UID (hex string)
|
|
96
|
+
data: string; // Card data (JSON string or text)
|
|
97
|
+
rawData: string; // Raw hex data
|
|
98
|
+
timestamp: number; // Unix timestamp
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Throws:**
|
|
103
|
+
- `Error` if NFC is not enabled
|
|
104
|
+
- `Error` if scan times out
|
|
105
|
+
- `Error` if scanning fails to start
|
|
106
|
+
- `Error` if module is not available (Expo Go)
|
|
107
|
+
|
|
108
|
+
### `isNfcEnabled()`
|
|
109
|
+
|
|
110
|
+
Check if NFC is enabled on the device.
|
|
111
|
+
|
|
112
|
+
**Returns:** `Promise<boolean>`
|
|
113
|
+
|
|
114
|
+
### `startScanning()`
|
|
115
|
+
|
|
116
|
+
Start NFC scanning (for advanced use cases).
|
|
117
|
+
|
|
118
|
+
**Throws:** `Error` if module is not available
|
|
119
|
+
|
|
120
|
+
### `stopScanning()`
|
|
121
|
+
|
|
122
|
+
Stop NFC scanning (for advanced use cases).
|
|
123
|
+
|
|
124
|
+
### `addCardScannedListener(listener)`
|
|
125
|
+
|
|
126
|
+
Add event listener for card scans (for advanced use cases).
|
|
127
|
+
|
|
128
|
+
**Parameters:**
|
|
129
|
+
- `listener` (function): Callback that receives `{ uid, data, rawData, timestamp }`
|
|
130
|
+
|
|
131
|
+
**Returns:** Subscription object with `remove()` method
|
|
132
|
+
|
|
133
|
+
### `removeCardScannedListener(subscription)`
|
|
134
|
+
|
|
135
|
+
Remove event listener (for advanced use cases).
|
|
136
|
+
|
|
137
|
+
## Requirements
|
|
138
|
+
|
|
139
|
+
- Android device with NFC support
|
|
140
|
+
- Development build (does not work in Expo Go)
|
|
141
|
+
- NFC enabled on device
|
|
142
|
+
- Expo SDK 54+
|
|
143
|
+
|
|
144
|
+
## Development Build
|
|
145
|
+
|
|
146
|
+
Since this uses custom native code, you need a development build:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install dependencies
|
|
150
|
+
npm install
|
|
151
|
+
|
|
152
|
+
# Generate native code
|
|
153
|
+
npx expo prebuild --platform android
|
|
154
|
+
|
|
155
|
+
# Build and run
|
|
156
|
+
npx expo run:android
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## How It Works
|
|
160
|
+
|
|
161
|
+
1. **NDEF Reading**: First attempts to read NDEF records (standard NFC format)
|
|
162
|
+
2. **MIFARE Reading**: If NDEF fails, reads multiple sectors from MIFARE Classic card
|
|
163
|
+
3. **Data Extraction**: Automatically extracts JSON from the data
|
|
164
|
+
4. **Auto-cleanup**: Automatically stops scanning after tag is discovered
|
|
165
|
+
|
|
166
|
+
## Error Handling
|
|
167
|
+
|
|
168
|
+
The module throws descriptive errors:
|
|
169
|
+
|
|
170
|
+
- `"NFC is not enabled on this device"` - User needs to enable NFC
|
|
171
|
+
- `"NFC scan timed out after Xms"` - No tag detected within timeout
|
|
172
|
+
- `"Failed to start NFC scanning"` - System error starting scan
|
|
173
|
+
- `"ExpoMifareScanner requires a development build"` - Running in Expo Go
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
3
|
+
|
|
4
|
+
group = 'expo.modules'
|
|
5
|
+
version = '1.0.0'
|
|
6
|
+
|
|
7
|
+
buildscript {
|
|
8
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir, "ExpoModulesCorePlugin.gradle")
|
|
9
|
+
if (expoModulesCorePlugin.exists()) {
|
|
10
|
+
apply from: expoModulesCorePlugin
|
|
11
|
+
applyKotlinExpoModulesCorePlugin()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
android {
|
|
16
|
+
namespace "expo.modules.mifarescanner"
|
|
17
|
+
compileSdkVersion rootProject.ext.compileSdkVersion
|
|
18
|
+
|
|
19
|
+
defaultConfig {
|
|
20
|
+
minSdkVersion rootProject.ext.minSdkVersion
|
|
21
|
+
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
lintOptions {
|
|
25
|
+
abortOnError false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
compileOptions {
|
|
29
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
30
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
kotlinOptions {
|
|
34
|
+
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dependencies {
|
|
39
|
+
implementation project(':expo-modules-core')
|
|
40
|
+
implementation 'com.facebook.react:react-native'
|
|
41
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
package expo.modules.mifarescanner;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.nfc.NfcAdapter;
|
|
5
|
+
import android.nfc.NdefMessage;
|
|
6
|
+
import android.nfc.NdefRecord;
|
|
7
|
+
import android.nfc.Tag;
|
|
8
|
+
import android.nfc.tech.MifareClassic;
|
|
9
|
+
import android.nfc.tech.Ndef;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
import java.io.IOException;
|
|
12
|
+
import java.nio.charset.StandardCharsets;
|
|
13
|
+
import java.util.ArrayList;
|
|
14
|
+
import java.util.List;
|
|
15
|
+
|
|
16
|
+
public class MifareScanner {
|
|
17
|
+
private static final String TAG = "MifareScanner";
|
|
18
|
+
private NfcAdapter nfcAdapter;
|
|
19
|
+
private Activity currentActivity;
|
|
20
|
+
private boolean isScanning = false;
|
|
21
|
+
private NfcAdapter.ReaderCallback readerCallback;
|
|
22
|
+
private OnCardScannedListener listener;
|
|
23
|
+
|
|
24
|
+
public interface OnCardScannedListener {
|
|
25
|
+
void onCardScanned(String uid, String data, String rawData, long timestamp);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public MifareScanner(NfcAdapter adapter, Activity activity) {
|
|
29
|
+
this.nfcAdapter = adapter;
|
|
30
|
+
this.currentActivity = activity;
|
|
31
|
+
Log.d(TAG, "MifareScanner initialized");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public void setOnCardScannedListener(OnCardScannedListener listener) {
|
|
35
|
+
this.listener = listener;
|
|
36
|
+
Log.d(TAG, "Card scanned listener set");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public boolean isNfcEnabled() {
|
|
40
|
+
boolean enabled = nfcAdapter != null && nfcAdapter.isEnabled();
|
|
41
|
+
Log.d(TAG, "NFC enabled check: " + enabled);
|
|
42
|
+
return enabled;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public void startScanning() throws Exception {
|
|
46
|
+
if (isScanning) {
|
|
47
|
+
String errorMsg = "NFC scanning is already in progress";
|
|
48
|
+
Log.e(TAG, errorMsg);
|
|
49
|
+
throw new IllegalStateException(errorMsg);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (nfcAdapter == null) {
|
|
53
|
+
String errorMsg = "NFC adapter is not available on this device";
|
|
54
|
+
Log.e(TAG, errorMsg);
|
|
55
|
+
throw new IllegalStateException(errorMsg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (currentActivity == null) {
|
|
59
|
+
String errorMsg = "Activity is not available. Make sure the app is in the foreground.";
|
|
60
|
+
Log.e(TAG, errorMsg);
|
|
61
|
+
throw new IllegalStateException(errorMsg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!nfcAdapter.isEnabled()) {
|
|
65
|
+
String errorMsg = "NFC is not enabled on this device. Please enable NFC in settings.";
|
|
66
|
+
Log.e(TAG, errorMsg);
|
|
67
|
+
throw new IllegalStateException(errorMsg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isScanning = true;
|
|
71
|
+
Log.d(TAG, "Starting MIFARE scanning - Discovery");
|
|
72
|
+
|
|
73
|
+
readerCallback = new NfcAdapter.ReaderCallback() {
|
|
74
|
+
@Override
|
|
75
|
+
public void onTagDiscovered(Tag tag) {
|
|
76
|
+
Log.d(TAG, "Tag discovered - Discovery");
|
|
77
|
+
handleTag(tag);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
int flags = NfcAdapter.FLAG_READER_NFC_A |
|
|
82
|
+
NfcAdapter.FLAG_READER_NFC_B;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
nfcAdapter.enableReaderMode(currentActivity, readerCallback, flags, null);
|
|
86
|
+
Log.d(TAG, "Reader mode enabled successfully - Discovery");
|
|
87
|
+
} catch (Exception e) {
|
|
88
|
+
isScanning = false;
|
|
89
|
+
String errorMsg = "Failed to enable NFC reader mode: " + e.getMessage();
|
|
90
|
+
Log.e(TAG, errorMsg, e);
|
|
91
|
+
throw new RuntimeException(errorMsg, e);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public void stopScanning() {
|
|
96
|
+
if (!isScanning) {
|
|
97
|
+
Log.d(TAG, "Not scanning, ignoring stop request");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (nfcAdapter != null && currentActivity != null) {
|
|
102
|
+
try {
|
|
103
|
+
nfcAdapter.disableReaderMode(currentActivity);
|
|
104
|
+
Log.d(TAG, "Reader mode disabled");
|
|
105
|
+
} catch (Exception e) {
|
|
106
|
+
Log.e(TAG, "Error disabling reader mode: " + e.getMessage(), e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isScanning = false;
|
|
111
|
+
readerCallback = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private void handleTag(Tag tag) {
|
|
115
|
+
// Get UID from tag ID (this is the actual UID, not block 0)
|
|
116
|
+
String uid = bytesToHex(tag.getId());
|
|
117
|
+
Log.d(TAG, "Processing tag with UID: " + uid + " - Discovery");
|
|
118
|
+
|
|
119
|
+
String data = "";
|
|
120
|
+
String rawData = "";
|
|
121
|
+
|
|
122
|
+
// First, try to read NDEF records (if card supports NDEF)
|
|
123
|
+
Ndef ndef = Ndef.get(tag);
|
|
124
|
+
if (ndef != null) {
|
|
125
|
+
try {
|
|
126
|
+
ndef.connect();
|
|
127
|
+
Log.d(TAG, "NDEF detected, reading NDEF records - Reading");
|
|
128
|
+
|
|
129
|
+
NdefMessage ndefMessage = ndef.getNdefMessage();
|
|
130
|
+
if (ndefMessage != null) {
|
|
131
|
+
NdefRecord[] records = ndefMessage.getRecords();
|
|
132
|
+
Log.d(TAG, "Found " + records.length + " NDEF records - Reading");
|
|
133
|
+
|
|
134
|
+
List<String> textRecords = new ArrayList<>();
|
|
135
|
+
StringBuilder allData = new StringBuilder();
|
|
136
|
+
|
|
137
|
+
for (NdefRecord record : records) {
|
|
138
|
+
if (record.getTnf() == NdefRecord.TNF_WELL_KNOWN) {
|
|
139
|
+
byte[] type = record.getType();
|
|
140
|
+
if (java.util.Arrays.equals(type, NdefRecord.RTD_TEXT)) {
|
|
141
|
+
String text = parseTextRecord(record);
|
|
142
|
+
if (text != null && !text.isEmpty()) {
|
|
143
|
+
textRecords.add(text);
|
|
144
|
+
allData.append(text);
|
|
145
|
+
Log.d(TAG, "Found text record: " + text.substring(0, Math.min(50, text.length())) + "... - Reading");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Also try to read raw payload as string (might contain JSON)
|
|
151
|
+
byte[] payload = record.getPayload();
|
|
152
|
+
if (payload != null && payload.length > 0) {
|
|
153
|
+
try {
|
|
154
|
+
String payloadStr = new String(payload, StandardCharsets.UTF_8);
|
|
155
|
+
if (payloadStr.trim().length() > 0) {
|
|
156
|
+
allData.append(payloadStr);
|
|
157
|
+
Log.d(TAG, "Found payload data: " + payloadStr.substring(0, Math.min(50, payloadStr.length())) + "... - Reading");
|
|
158
|
+
}
|
|
159
|
+
} catch (Exception e) {
|
|
160
|
+
// Ignore encoding errors
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (allData.length() > 0) {
|
|
166
|
+
data = allData.toString();
|
|
167
|
+
rawData = bytesToHex(ndefMessage.toByteArray());
|
|
168
|
+
Log.d(TAG, "NDEF data extracted: " + data.substring(0, Math.min(100, data.length())) + "... - Success");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ndef.close();
|
|
172
|
+
} else {
|
|
173
|
+
Log.d(TAG, "No NDEF message found - Reading");
|
|
174
|
+
ndef.close();
|
|
175
|
+
}
|
|
176
|
+
} catch (Exception e) {
|
|
177
|
+
Log.e(TAG, "Error reading NDEF: " + e.getMessage(), e);
|
|
178
|
+
try {
|
|
179
|
+
if (ndef.isConnected()) {
|
|
180
|
+
ndef.close();
|
|
181
|
+
}
|
|
182
|
+
} catch (IOException closeException) {
|
|
183
|
+
Log.e(TAG, "Error closing NDEF: " + closeException.getMessage());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If NDEF didn't work or returned empty, try MIFARE Classic direct reading
|
|
189
|
+
if (data.isEmpty()) {
|
|
190
|
+
MifareClassic mifare = null;
|
|
191
|
+
try {
|
|
192
|
+
mifare = MifareClassic.get(tag);
|
|
193
|
+
if (mifare != null) {
|
|
194
|
+
Log.d(TAG, "MIFARE Classic detected, type: " + mifare.getType() + " - Discovery");
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
mifare.connect();
|
|
198
|
+
Log.d(TAG, "Connected to MIFARE card - Discovery");
|
|
199
|
+
|
|
200
|
+
int sectorCount = mifare.getSectorCount();
|
|
201
|
+
Log.d(TAG, "Card has " + sectorCount + " sectors - Reading");
|
|
202
|
+
|
|
203
|
+
StringBuilder allBlocksData = new StringBuilder();
|
|
204
|
+
List<Byte> allBytes = new ArrayList<>();
|
|
205
|
+
|
|
206
|
+
// Try to read multiple sectors
|
|
207
|
+
for (int sectorIndex = 0; sectorIndex < Math.min(sectorCount, 16); sectorIndex++) {
|
|
208
|
+
try {
|
|
209
|
+
Log.d(TAG, "Attempting authentication on sector " + sectorIndex + " - Authentication");
|
|
210
|
+
|
|
211
|
+
// Try default key first
|
|
212
|
+
boolean authenticated = mifare.authenticateSectorWithKeyA(
|
|
213
|
+
sectorIndex,
|
|
214
|
+
MifareClassic.KEY_DEFAULT
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (!authenticated) {
|
|
218
|
+
// Try key B
|
|
219
|
+
authenticated = mifare.authenticateSectorWithKeyB(
|
|
220
|
+
sectorIndex,
|
|
221
|
+
MifareClassic.KEY_DEFAULT
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!authenticated) {
|
|
226
|
+
Log.d(TAG, "Failed to authenticate sector " + sectorIndex + " - Authentication failed");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Log.d(TAG, "Successfully authenticated sector " + sectorIndex + " - Authentication");
|
|
231
|
+
|
|
232
|
+
int firstBlock = mifare.sectorToBlock(sectorIndex);
|
|
233
|
+
int blockCount = mifare.getBlockCountInSector(sectorIndex);
|
|
234
|
+
int lastBlock = firstBlock + blockCount - 1; // Sector trailer
|
|
235
|
+
|
|
236
|
+
Log.d(TAG, "Sector " + sectorIndex + ": blocks " + firstBlock + " to " + lastBlock + " (trailer at " + lastBlock + ") - Reading");
|
|
237
|
+
|
|
238
|
+
// Read all blocks in this sector (except the last one which is the sector trailer)
|
|
239
|
+
for (int blockOffset = 0; blockOffset < blockCount - 1; blockOffset++) {
|
|
240
|
+
int blockNumber = firstBlock + blockOffset;
|
|
241
|
+
try {
|
|
242
|
+
byte[] blockData = mifare.readBlock(blockNumber);
|
|
243
|
+
String blockHex = bytesToHex(blockData);
|
|
244
|
+
|
|
245
|
+
Log.d(TAG, "Block " + blockNumber + " hex: " + blockHex.substring(0, Math.min(32, blockHex.length())) + "... - Reading");
|
|
246
|
+
|
|
247
|
+
// Check if block is all zeros or all same byte (likely empty)
|
|
248
|
+
boolean isEmpty = true;
|
|
249
|
+
byte firstByte = blockData[0];
|
|
250
|
+
for (byte b : blockData) {
|
|
251
|
+
if (b != 0 && b != firstByte) {
|
|
252
|
+
isEmpty = false;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isEmpty) {
|
|
258
|
+
Log.d(TAG, "Block " + blockNumber + " appears empty, skipping - Reading");
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Collect bytes
|
|
263
|
+
for (byte b : blockData) {
|
|
264
|
+
allBytes.add(b);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Try to read as string, but be more careful
|
|
268
|
+
String blockStr = new String(blockData, StandardCharsets.UTF_8);
|
|
269
|
+
// Remove null characters and control characters
|
|
270
|
+
blockStr = blockStr.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
|
271
|
+
|
|
272
|
+
// Check if it looks like readable text (has printable ASCII or JSON-like characters)
|
|
273
|
+
boolean hasReadableText = false;
|
|
274
|
+
int printableCount = 0;
|
|
275
|
+
for (char c : blockStr.toCharArray()) {
|
|
276
|
+
if (c >= 32 && c < 127) { // Printable ASCII
|
|
277
|
+
printableCount++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// If at least 50% is printable, consider it readable
|
|
282
|
+
if (blockStr.length() > 0 && printableCount > blockStr.length() / 2) {
|
|
283
|
+
hasReadableText = true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (hasReadableText) {
|
|
287
|
+
allBlocksData.append(blockStr);
|
|
288
|
+
Log.d(TAG, "Block " + blockNumber + " readable text: " + blockStr.substring(0, Math.min(50, blockStr.length())) + "... - Reading");
|
|
289
|
+
} else {
|
|
290
|
+
Log.d(TAG, "Block " + blockNumber + " doesn't contain readable text (printable: " + printableCount + "/" + blockStr.length() + ") - Reading");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
} catch (IOException e) {
|
|
294
|
+
Log.e(TAG, "Error reading block " + blockNumber + ": " + e.getMessage());
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
} catch (Exception e) {
|
|
299
|
+
Log.d(TAG, "Error processing sector " + sectorIndex + ": " + e.getMessage());
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
mifare.close();
|
|
304
|
+
Log.d(TAG, "MIFARE connection closed");
|
|
305
|
+
|
|
306
|
+
// Convert collected bytes to hex
|
|
307
|
+
byte[] allBytesArray = new byte[allBytes.size()];
|
|
308
|
+
for (int i = 0; i < allBytes.size(); i++) {
|
|
309
|
+
allBytesArray[i] = allBytes.get(i);
|
|
310
|
+
}
|
|
311
|
+
rawData = bytesToHex(allBytesArray);
|
|
312
|
+
|
|
313
|
+
// Use the string data if we found any
|
|
314
|
+
if (allBlocksData.length() > 0) {
|
|
315
|
+
data = allBlocksData.toString().trim();
|
|
316
|
+
// Try to find JSON in the data
|
|
317
|
+
int jsonStart = data.indexOf("{");
|
|
318
|
+
int jsonEnd = data.lastIndexOf("}");
|
|
319
|
+
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
|
320
|
+
data = data.substring(jsonStart, jsonEnd + 1);
|
|
321
|
+
Log.d(TAG, "Found JSON in data: " + data.substring(0, Math.min(100, data.length())) + "... - Success");
|
|
322
|
+
}
|
|
323
|
+
} else if (allBytesArray.length > 0) {
|
|
324
|
+
// Try to parse as JSON or text
|
|
325
|
+
String fullData = new String(allBytesArray, StandardCharsets.UTF_8);
|
|
326
|
+
fullData = fullData.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
|
327
|
+
|
|
328
|
+
// Look for JSON
|
|
329
|
+
int jsonStart = fullData.indexOf("{");
|
|
330
|
+
int jsonEnd = fullData.lastIndexOf("}");
|
|
331
|
+
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
|
332
|
+
data = fullData.substring(jsonStart, jsonEnd + 1);
|
|
333
|
+
Log.d(TAG, "Found JSON in raw bytes: " + data.substring(0, Math.min(100, data.length())) + "... - Success");
|
|
334
|
+
} else {
|
|
335
|
+
// Just use the cleaned string
|
|
336
|
+
data = fullData.trim();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Log.d(TAG, "MIFARE data extracted, length: " + data.length() + ", rawData length: " + rawData.length() + " - Success");
|
|
341
|
+
|
|
342
|
+
} catch (IOException e) {
|
|
343
|
+
Log.e(TAG, "Error reading MIFARE card: " + e.getMessage(), e);
|
|
344
|
+
Log.d(TAG, "Tag handling failed - Failure");
|
|
345
|
+
try {
|
|
346
|
+
if (mifare != null && mifare.isConnected()) {
|
|
347
|
+
mifare.close();
|
|
348
|
+
}
|
|
349
|
+
} catch (IOException closeException) {
|
|
350
|
+
Log.e(TAG, "Error closing MIFARE connection: " + closeException.getMessage());
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch (Exception e) {
|
|
355
|
+
Log.e(TAG, "Error handling tag: " + e.getMessage(), e);
|
|
356
|
+
Log.d(TAG, "Tag handling failed - Failure");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Send event to JavaScript
|
|
361
|
+
long timestamp = System.currentTimeMillis();
|
|
362
|
+
Log.d(TAG, "Sending onCardScanned event - Success");
|
|
363
|
+
if (listener != null) {
|
|
364
|
+
listener.onCardScanned(uid, data, rawData, timestamp);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private String parseTextRecord(NdefRecord record) {
|
|
369
|
+
try {
|
|
370
|
+
byte[] payload = record.getPayload();
|
|
371
|
+
if (payload == null || payload.length == 0) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// First byte contains language code length and encoding
|
|
376
|
+
boolean isUtf16 = (payload[0] & 0x80) != 0;
|
|
377
|
+
int languageCodeLength = payload[0] & 0x3F;
|
|
378
|
+
java.nio.charset.Charset textEncoding = isUtf16 ? StandardCharsets.UTF_16 : StandardCharsets.UTF_8;
|
|
379
|
+
|
|
380
|
+
// Extract text
|
|
381
|
+
if (payload.length > languageCodeLength + 1) {
|
|
382
|
+
byte[] textBytes = new byte[payload.length - languageCodeLength - 1];
|
|
383
|
+
System.arraycopy(payload, languageCodeLength + 1, textBytes, 0, textBytes.length);
|
|
384
|
+
return new String(textBytes, textEncoding);
|
|
385
|
+
}
|
|
386
|
+
} catch (Exception e) {
|
|
387
|
+
Log.e(TAG, "Error parsing text record: " + e.getMessage(), e);
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private String bytesToHex(byte[] bytes) {
|
|
393
|
+
StringBuilder result = new StringBuilder();
|
|
394
|
+
for (byte b : bytes) {
|
|
395
|
+
result.append(String.format("%02x", b));
|
|
396
|
+
}
|
|
397
|
+
return result.toString();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
public void updateActivity(Activity activity) {
|
|
401
|
+
this.currentActivity = activity;
|
|
402
|
+
Log.d(TAG, "Activity updated");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package expo.modules.mifarescanner
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.nfc.NfcAdapter
|
|
5
|
+
import expo.modules.kotlin.modules.Module
|
|
6
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
7
|
+
import expo.modules.kotlin.events.EventsDefinition
|
|
8
|
+
|
|
9
|
+
class MifareScannerModule : Module() {
|
|
10
|
+
private val TAG = "MifareScannerModule"
|
|
11
|
+
private var nfcAdapter: NfcAdapter? = null
|
|
12
|
+
private var currentActivity: Activity? = null
|
|
13
|
+
private var mifareScanner: MifareScanner? = null
|
|
14
|
+
|
|
15
|
+
override fun definition() = ModuleDefinition {
|
|
16
|
+
Name("ExpoMifareScanner")
|
|
17
|
+
|
|
18
|
+
Events("onCardScanned")
|
|
19
|
+
|
|
20
|
+
OnCreate {
|
|
21
|
+
currentActivity = appContext.currentActivity
|
|
22
|
+
nfcAdapter = NfcAdapter.getDefaultAdapter(appContext.reactContext)
|
|
23
|
+
|
|
24
|
+
if (nfcAdapter != null && currentActivity != null) {
|
|
25
|
+
mifareScanner = MifareScanner(nfcAdapter!!, currentActivity!!)
|
|
26
|
+
mifareScanner?.setOnCardScannedListener { uid, data, rawData, timestamp ->
|
|
27
|
+
sendEvent("onCardScanned", mapOf(
|
|
28
|
+
"uid" to uid,
|
|
29
|
+
"data" to data,
|
|
30
|
+
"rawData" to rawData,
|
|
31
|
+
"timestamp" to timestamp
|
|
32
|
+
))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
OnActivityEntersForeground {
|
|
38
|
+
currentActivity = appContext.currentActivity
|
|
39
|
+
mifareScanner?.updateActivity(currentActivity)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Function("startScanning") {
|
|
43
|
+
try {
|
|
44
|
+
if (mifareScanner == null) {
|
|
45
|
+
throw Exception("MIFARE scanner is not initialized. NFC adapter may not be available.")
|
|
46
|
+
}
|
|
47
|
+
mifareScanner.startScanning()
|
|
48
|
+
} catch (e: Exception) {
|
|
49
|
+
throw Exception("Failed to start NFC scanning: ${e.message}", e)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Function("stopScanning") {
|
|
54
|
+
try {
|
|
55
|
+
mifareScanner?.stopScanning()
|
|
56
|
+
} catch (e: Exception) {
|
|
57
|
+
// Log but don't throw - stopping is best effort
|
|
58
|
+
android.util.Log.e(TAG, "Error stopping scan: ${e.message}", e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Function("isNfcEnabled") {
|
|
63
|
+
try {
|
|
64
|
+
mifareScanner?.isNfcEnabled() ?: false
|
|
65
|
+
} catch (e: Exception) {
|
|
66
|
+
android.util.Log.e(TAG, "Error checking NFC status: ${e.message}", e)
|
|
67
|
+
false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { withAndroidManifest } = require('@expo/config-plugins');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expo config plugin for ExpoMifareScanner
|
|
5
|
+
* Adds NFC permissions and features to AndroidManifest.xml
|
|
6
|
+
*/
|
|
7
|
+
const withMifareScanner = (config) => {
|
|
8
|
+
return withAndroidManifest(config, async (config) => {
|
|
9
|
+
const androidManifest = config.modResults;
|
|
10
|
+
const { manifest } = androidManifest;
|
|
11
|
+
|
|
12
|
+
// Ensure permissions array exists
|
|
13
|
+
if (!manifest['uses-permission']) {
|
|
14
|
+
manifest['uses-permission'] = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Add NFC permission if not already present
|
|
18
|
+
const hasNfcPermission = manifest['uses-permission'].some(
|
|
19
|
+
(perm) => perm.$['android:name'] === 'android.permission.NFC'
|
|
20
|
+
);
|
|
21
|
+
if (!hasNfcPermission) {
|
|
22
|
+
manifest['uses-permission'].push({
|
|
23
|
+
$: { 'android:name': 'android.permission.NFC' },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Add NFC feature if not already present
|
|
28
|
+
if (!manifest['uses-feature']) {
|
|
29
|
+
manifest['uses-feature'] = [];
|
|
30
|
+
}
|
|
31
|
+
const hasNfcFeature = manifest['uses-feature'].some(
|
|
32
|
+
(feat) => feat.$['android:name'] === 'android.hardware.nfc'
|
|
33
|
+
);
|
|
34
|
+
if (!hasNfcFeature) {
|
|
35
|
+
manifest['uses-feature'].push({
|
|
36
|
+
$: { 'android:name': 'android.hardware.nfc', 'android:required': 'false' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return config;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
module.exports = withMifareScanner;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@utapza/expo-mifare-scanner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Expo module for scanning MIFARE Classic cards",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"react-native",
|
|
8
|
+
"expo",
|
|
9
|
+
"mifare",
|
|
10
|
+
"nfc",
|
|
11
|
+
"expo-module"
|
|
12
|
+
],
|
|
13
|
+
"author": "Your Name",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/utapza/expo-mifare-scanner.git"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"expo": "*",
|
|
21
|
+
"expo-modules-core": "*"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"android/src",
|
|
26
|
+
"android/build.gradle",
|
|
27
|
+
"expo-module.config.json",
|
|
28
|
+
"app.plugin.js",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { requireNativeModule, EventEmitter } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
// Import the native module
|
|
4
|
+
let ExpoMifareScanner;
|
|
5
|
+
let isExpoGo = false;
|
|
6
|
+
try {
|
|
7
|
+
ExpoMifareScanner = requireNativeModule('ExpoMifareScanner');
|
|
8
|
+
} catch (e) {
|
|
9
|
+
ExpoMifareScanner = null;
|
|
10
|
+
// Check if we're running in Expo Go (which doesn't support custom native modules)
|
|
11
|
+
try {
|
|
12
|
+
const { Constants } = require('expo-constants');
|
|
13
|
+
isExpoGo = Constants.executionEnvironment === 'storeClient';
|
|
14
|
+
} catch (constantsError) {
|
|
15
|
+
// If expo-constants is not available, we can't definitively detect Expo Go
|
|
16
|
+
// But the error message will still be helpful
|
|
17
|
+
isExpoGo = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Log a warning (not an error) - this is expected in Expo Go
|
|
21
|
+
if (isExpoGo) {
|
|
22
|
+
console.warn('ExpoMifareScanner: Custom native modules are not supported in Expo Go. Please use a development build.');
|
|
23
|
+
} else {
|
|
24
|
+
console.warn('ExpoMifareScanner: Native module not found. This feature requires a development build.');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const emitter = new EventEmitter(ExpoMifareScanner || {});
|
|
29
|
+
|
|
30
|
+
export function addCardScannedListener(listener) {
|
|
31
|
+
return emitter.addListener('onCardScanned', listener);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function removeCardScannedListener(subscription) {
|
|
35
|
+
if (subscription && subscription.remove) {
|
|
36
|
+
subscription.remove();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function startScanning() {
|
|
41
|
+
if (!ExpoMifareScanner) {
|
|
42
|
+
const errorMessage = isExpoGo
|
|
43
|
+
? 'ExpoMifareScanner requires a development build. Custom native modules are not supported in Expo Go.'
|
|
44
|
+
: 'ExpoMifareScanner native module not available. Please rebuild the app with a development build.';
|
|
45
|
+
throw new Error(errorMessage);
|
|
46
|
+
}
|
|
47
|
+
return await ExpoMifareScanner.startScanning();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function stopScanning() {
|
|
51
|
+
if (!ExpoMifareScanner) {
|
|
52
|
+
// Silently fail if module is not available (already stopped)
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return await ExpoMifareScanner.stopScanning();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function isNfcEnabled() {
|
|
59
|
+
if (!ExpoMifareScanner) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return await ExpoMifareScanner.isNfcEnabled();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read NFC tag - simplified async API
|
|
67
|
+
* Starts scanning, waits for tag discovery, automatically stops, and returns data
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} options - Optional configuration
|
|
70
|
+
* @param {number} options.timeout - Timeout in milliseconds (default: 30000 = 30 seconds)
|
|
71
|
+
* @returns {Promise<Object>} Promise that resolves with card data or rejects with error
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* try {
|
|
75
|
+
* const cardData = await readNfcTag({ timeout: 10000 });
|
|
76
|
+
* console.log('Card UID:', cardData.uid);
|
|
77
|
+
* console.log('Card Data:', cardData.data);
|
|
78
|
+
* } catch (error) {
|
|
79
|
+
* console.error('Scan failed:', error.message);
|
|
80
|
+
* }
|
|
81
|
+
*/
|
|
82
|
+
export async function readNfcTag(options = {}) {
|
|
83
|
+
const { timeout = 30000 } = options;
|
|
84
|
+
|
|
85
|
+
if (!ExpoMifareScanner) {
|
|
86
|
+
const errorMessage = isExpoGo
|
|
87
|
+
? 'ExpoMifareScanner requires a development build. Custom native modules are not supported in Expo Go.'
|
|
88
|
+
: 'ExpoMifareScanner native module not available. Please rebuild the app with a development build.';
|
|
89
|
+
throw new Error(errorMessage);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if NFC is enabled
|
|
93
|
+
const nfcEnabled = await isNfcEnabled();
|
|
94
|
+
if (!nfcEnabled) {
|
|
95
|
+
throw new Error('NFC is not enabled on this device. Please enable NFC in settings.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
let scanSubscription = null;
|
|
100
|
+
let timeoutId = null;
|
|
101
|
+
let isResolved = false;
|
|
102
|
+
|
|
103
|
+
// Cleanup function
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
if (timeoutId) {
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
timeoutId = null;
|
|
108
|
+
}
|
|
109
|
+
if (scanSubscription) {
|
|
110
|
+
removeCardScannedListener(scanSubscription);
|
|
111
|
+
scanSubscription = null;
|
|
112
|
+
}
|
|
113
|
+
// Stop scanning (best effort, don't wait for it)
|
|
114
|
+
if (ExpoMifareScanner) {
|
|
115
|
+
ExpoMifareScanner.stopScanning().catch(() => {
|
|
116
|
+
// Ignore errors when stopping
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Set up timeout
|
|
122
|
+
timeoutId = setTimeout(() => {
|
|
123
|
+
if (!isResolved) {
|
|
124
|
+
isResolved = true;
|
|
125
|
+
cleanup();
|
|
126
|
+
reject(new Error(`NFC scan timed out after ${timeout}ms. Please try again.`));
|
|
127
|
+
}
|
|
128
|
+
}, timeout);
|
|
129
|
+
|
|
130
|
+
// Set up event listener
|
|
131
|
+
try {
|
|
132
|
+
scanSubscription = addCardScannedListener((event) => {
|
|
133
|
+
if (isResolved) {
|
|
134
|
+
return; // Already resolved/rejected
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isResolved = true;
|
|
138
|
+
cleanup();
|
|
139
|
+
|
|
140
|
+
// Format the response
|
|
141
|
+
const cardData = {
|
|
142
|
+
uid: event.uid,
|
|
143
|
+
data: event.data,
|
|
144
|
+
rawData: event.rawData,
|
|
145
|
+
timestamp: event.timestamp,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
resolve(cardData);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Start scanning
|
|
152
|
+
startScanning().catch((error) => {
|
|
153
|
+
if (!isResolved) {
|
|
154
|
+
isResolved = true;
|
|
155
|
+
cleanup();
|
|
156
|
+
reject(new Error(`Failed to start NFC scanning: ${error.message}`));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!isResolved) {
|
|
161
|
+
isResolved = true;
|
|
162
|
+
cleanup();
|
|
163
|
+
reject(new Error(`Failed to set up NFC scan: ${error.message}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|