@exxili/capacitor-nfc 0.0.13 → 0.0.14

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
@@ -1,595 +1,597 @@
1
- # Capacitor NFC Plugin (@exxili/capacitor-nfc)
2
-
3
- A Capacitor plugin for reading and writing NFC tags on iOS and Android devices. This plugin allows you to:
4
-
5
- - Read NDEF messages from NFC tags.
6
- - Write NDEF messages to NFC tags.
7
-
8
- **Note**: NFC functionality is only available on compatible iOS devices running iOS 13.0 or later.
9
-
10
- ## Table of Contents
11
-
12
- - [Capacitor NFC Plugin (@exxili/capacitor-nfc)](#capacitor-nfc-plugin-exxilicapacitor-nfc)
13
- - [Table of Contents](#table-of-contents)
14
- - [Installation](#installation)
15
- - [iOS Setup](#ios-setup)
16
- - [1. Enable NFC Capability](#1-enable-nfc-capability)
17
- - [2. Add Usage Description](#2-add-usage-description)
18
- - [Android Setup](#android-setup)
19
- - [Usage](#usage)
20
- - [Reading NFC Tags](#reading-nfc-tags)
21
- - [Writing NFC Tags](#writing-nfc-tags)
22
- - [API](#api)
23
- - [Methods](#methods)
24
- - [`isSupported()`](#issupported)
25
- - [`startScan()`](#startscan)
26
- - [`cancelScan()`](#cancelscan)
27
- - [`writeNDEF(options: NDEFWriteOptions<T extends string | number[] | Uint8Array = string>)`](#writendefoptions-ndefwriteoptionst-extends-string--number--uint8array--string)
28
- - [`cancelWriteAndroid()`](#cancelwriteandroid)
29
- - [Listeners](#listeners)
30
- - [`onRead(listener: (data: NDEFMessagesTransformable) => void)`](#onreadlistener-data-ndefmessagestransformable--void)
31
- - [Interfaces](#interfaces)
32
- - [`NDEFWriteOptions`](#ndefwriteoptions)
33
- - [`NDEFMessagesTransformable`](#ndefmessagestransformable)
34
- - [`NDEFMessages`](#ndefmessages)
35
- - [`NDEFMessage`](#ndefmessage)
36
- - [`NDEFRecord`](#ndefrecord)
37
- - [`NFCError`](#nfcerror)
38
- - [Integration into a Capacitor App](#integration-into-a-capacitor-app)
39
- - [Example](#example)
40
- - [License](#license)
41
-
42
- ## Installation
43
-
44
- Install the plugin using npm:
45
-
46
- ```bash
47
- npm install @exxili/capacitor-nfc
48
- npx cap sync
49
- ```
50
-
51
- ## iOS Setup
52
-
53
- To use NFC functionality on iOS, you need to perform some additional setup steps.
54
-
55
- ### 1. Enable NFC Capability
56
-
57
- In Xcode:
58
-
59
- 1. Open your project (`.xcworkspace` file) in Xcode.
60
- 2. Select your project in the Project Navigator.
61
- 3. Select your app target.
62
- 4. Go to the **Signing & Capabilities** tab.
63
- 5. Click the `+ Capability` button.
64
- 6. Add **Near Field Communication Tag Reading**.
65
-
66
- > **Advanced tag formats:** If you need ISO 7816, ISO 15693, or FeliCa access (to read raw UIDs, system codes, etc.), Apple requires additional entitlements in your provisioning profile and `Info.plist`. The plugin will fall back automatically when they are absent, but to unlock the full feature set add the relevant keys:
67
- >
68
- > ```xml
69
- > <key>com.apple.developer.nfc.readersession.felica.systemcodes</key>
70
- > <array>
71
- > <string>12FC</string>
72
- > <string>0000</string>
73
- > </array>
74
- > <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
75
- > <array>
76
- > <string>D2760000850100</string>
77
- > <string>D2760000850101</string>
78
- > <string>D2760001180101</string>
79
- > <string>00000000000000</string>
80
- > </array>
81
- > ```
82
- >
83
- > Replace the sample identifiers with the values required for your tags. Consult Apple's CoreNFC documentation for the complete list of entitlement keys.
84
-
85
- ### 2. Add Usage Description
86
-
87
- Add the `NFCReaderUsageDescription` key to your `Info.plist` file to explain why your app needs access to NFC.
88
-
89
- In your `Info.plist` file (usually located at `ios/App/App/Info.plist`), add:
90
-
91
- ```xml
92
- <key>NFCReaderUsageDescription</key>
93
- <string>This app requires access to NFC to read and write NFC tags.</string>
94
- ```
95
-
96
- Replace the description with a message that explains why your app needs NFC access.
97
-
98
- ## Android Setup
99
-
100
- Add the following to your `AndroidManifest.xml` file:
101
-
102
- ```xml
103
- <uses-permission android:name="android.permission.NFC" />
104
- <uses-feature android:name="android.hardware.nfc" android:required="true" />
105
- ```
106
-
107
- ## Usage
108
-
109
- Import the plugin into your code:
110
-
111
- ```typescript
112
- import { NFC } from '@exxili/capacitor-nfc';
113
- ```
114
-
115
- ### Reading NFC Tags
116
-
117
- To read NFC tags, you need to listen for `nfcTag` events. On iOS, you must also start the NFC scanning session using `startScan()`.
118
-
119
- ```typescript
120
- import { NFC, NDEFMessagesTransformable, NFCError } from '@exxili/capacitor-nfc';
121
-
122
- // Start NFC scanning (iOS only)
123
- NFC.startScan().catch((error) => {
124
- console.error('Error starting NFC scan:', error);
125
- });
126
-
127
- // Listen for NFC tag detection
128
- NFC.onRead((data: NDEFMessagesTransformable) => {
129
- // Text (T) and URI (U) records decoded; others best-effort UTF-8
130
- const asString = data.string();
131
- console.log('First record text payload:', asString.messages[0]?.records[0]?.payload);
132
-
133
- // Raw bytes
134
- const asUint8 = data.uint8Array();
135
- console.log('First record raw bytes length:', asUint8.messages[0]?.records[0]?.payload.length);
136
-
137
- // Access tag information (UID, tech types, etc.)
138
- const info = asString.tagInfo;
139
- if (info?.fallback) {
140
- console.log('Reader fallback mode:', info.fallbackMode, 'Reason:', info.reason);
141
- } else if (info) {
142
- console.log('Tag UID:', info.uid);
143
- console.log('Tag technologies:', info.techTypes);
144
- console.log('Tag type:', info.type);
145
- if (info.maxSize) {
146
- console.log('Max NDEF size:', info.maxSize);
147
- }
148
- console.log('Is writable:', info.isWritable);
149
- }
150
- });
151
-
152
- // Handle NFC errors
153
- NFC.onError((error: NFCError) => {
154
- console.error('NFC Error:', error);
155
- });
156
- ```
157
-
158
- ### Writing NFC Tags
159
-
160
- To write NDEF messages to NFC tags, use the `writeNDEF` method and listen for `onWrite` events.
161
-
162
- ```typescript
163
- import { NFC, NDEFWriteOptions, NFCError } from '@exxili/capacitor-nfc';
164
-
165
- const message: NDEFWriteOptions = {
166
- records: [
167
- {
168
- type: 'T', // Well Known Text record. String payload will be encoded as: [status][lang='en'][UTF-8 text]
169
- payload: 'Hello, NFC!',
170
- },
171
- {
172
- type: 'U', // Well Known URI record. String payload encoded as: [0x00][URI bytes]
173
- payload: 'https://example.com',
174
- },
175
- {
176
- type: 'T',
177
- payload: new Uint8Array([0x01, 0x65, 0x48, 0x69]), // Raw bytes preserved (DO NOT re-format)
178
- },
179
- ],
180
- };
181
-
182
- // For complete control over binary content, use raw mode:
183
- const rawMessage: NDEFWriteOptions = {
184
- rawMode: true, // Bypasses automatic Text/URI formatting
185
- records: [
186
- {
187
- type: 'T',
188
- payload: 'Hello, NFC!', // Written as UTF-8 bytes without Text record prefix
189
- },
190
- {
191
- type: 'custom',
192
- payload: new Uint8Array([0x01, 0x02, 0x03, 0x04]), // Exact bytes written to tag
193
- },
194
- ],
195
- };
196
-
197
- // Write NDEF message to NFC tag
198
- NFC.writeNDEF(message)
199
- .then(() => {
200
- console.log('Write initiated');
201
- })
202
- .catch((error) => {
203
- console.error('Error writing to NFC tag:', error);
204
- });
205
-
206
- // Listen for write success
207
- NFC.onWrite(() => {
208
- console.log('NDEF message written successfully.');
209
- });
210
-
211
- // Handle NFC errors
212
- NFC.onError((error: NFCError) => {
213
- console.error('NFC Error:', error);
214
- });
215
- ```
216
-
217
- ## API
218
-
219
- ### Methods
220
-
221
- #### `isSupported()`
222
-
223
- Returns if NFC is supported on the scanning device.
224
-
225
- **Returns**: `Promise<{ supported: boolean }>`
226
-
227
- #### `startScan()`
228
-
229
- Starts the NFC scanning session on **_iOS only_**. Android devices are always in reading mode, so setting up the `nfcTag` listener is sufficient to handle tag reads on Android.
230
-
231
- The iOS implementation now adapts automatically if the extended CoreNFC entitlements (ISO 7816, ISO 15693, FeliCa) are missing. The plugin first attempts the advanced tag reader so you can access UID/tech info. When iOS reports `Missing required entitlement`, the plugin downgrades to a compatibility mode (ISO 14443 only) and, if necessary, to the classic NDEF reader. A synthetic `nfcTag` event is emitted with `tagInfo.fallback`, `tagInfo.fallbackMode`, and `tagInfo.reason` so your UI can react immediately.
232
-
233
- You can override the mode explicitly:
234
-
235
- - `mode: 'auto'` (default) advanced reader with automatic downgrade and caching.
236
- - `mode: 'full'` – force a fresh attempt at the advanced reader, resetting cached fallback state.
237
- - `mode: 'compat'` – skip the advanced probe and jump straight to the ISO 14443 compatibility reader.
238
- - `mode: 'ndef'` – bypass tag sessions entirely and revert to the legacy NDEF-only reader.
239
-
240
- Legacy booleans `forceFull`, `forceCompat`, and `forceNDEF` map to the options above for backwards compatibility.
241
-
242
- **Returns**: `Promise<void>`
243
-
244
- ```typescript
245
- NFC.startScan({ mode: 'auto' })
246
- .then(() => {
247
- // Scanning started
248
- })
249
- .catch((error) => {
250
- console.error('Error starting NFC scan:', error);
251
- });
252
- ```
253
-
254
- #### `cancelScan()`
255
-
256
- Immediately invalidates the active iOS CoreNFC reader session (if any). Useful when you want to abort a scan early instead of waiting for the user to cancel or for a tag detection timeout.
257
-
258
- Platform notes:
259
-
260
- - iOS: Actively ends the session. Calling `startScan()` again after the returned promise resolves is safe.
261
- - Android: No-op (Android is always passively listening via foreground dispatch; you generally just ignore future events or control UI state).
262
-
263
- ```typescript
264
- await NFC.cancelScan(); // iOS: ends session; Android: no-op
265
- // Optionally restart
266
- await NFC.startScan();
267
- ```
268
-
269
- **Returns**: `Promise<void>`
270
-
271
- #### `writeNDEF(options: NDEFWriteOptions<T extends string | number[] | Uint8Array = string>)`
272
-
273
- Writes an NDEF message to an NFC tag.
274
-
275
- Payload may be provided as a string, `Uint8Array`, or an array of numbers.
276
-
277
- Automatic formatting rules (to aid interoperability):
278
-
279
- - Text (`type: 'T'` + string payload): encoded per NFC Forum RTD Text spec `[status][lang=en][UTF-8 text]`.
280
- - URI (`type: 'U'` + string payload): encoded as `[0x00][UTF-8 URI bytes]` (prefix compression not yet applied).
281
- - Any other `type` + string payload: UTF-8 bytes only (no extra framing).
282
- - `Uint8Array` or `number[]` payloads are treated as raw bytes and written verbatim (never altered).
283
-
284
- **Raw Mode**: Set `rawMode: true` to bypass automatic Well Known Type formatting entirely. All string payloads will be written as UTF-8 bytes without Text ('T') or URI ('U') prefixes, giving you complete control over the binary content.
285
-
286
- If you need full manual control of a Text or URI record, supply raw bytes (number[] / Uint8Array) and the plugin will not modify them.
287
-
288
- If you attempt to write zero records the promise rejects with `Error("At least one NDEF record is required")`.
289
-
290
- Android use: since Android has no default UI for reading and writing NFC tags, it is recommended that you add a UI indicator to your application when calling `writeNDEF` and remove it in the `nfcWriteSuccess` listener callback and the `nfcError` listener callback. This will prevent accidental writes to tags that your users intended to read from.
291
-
292
- **Parameters**:
293
-
294
- - `options: NDEFWriteOptions<T extends string | number[] | Uint8Array = string>` - The NDEF message to write. Must include at least one record.
295
-
296
- **Returns**: `Promise<void>`
297
-
298
- ```typescript
299
- NFC.writeNDEF(options)
300
- .then(() => {
301
- // Write initiated
302
- })
303
- .catch((error) => {
304
- console.error('Error writing NDEF message:', error);
305
- });
306
- ```
307
-
308
- #### `cancelWriteAndroid()`
309
-
310
- Cancels an Android NFC write operation. Android does not have a native UI for NFC tag writing, so this method allows developers to hook up a custom UI to cancel an in-progress scan.
311
-
312
- ### Listeners
313
-
314
- #### `onRead(listener: (data: NDEFMessagesTransformable) => void)`
315
-
316
- Adds a listener for NFC tag detection events. Returns type `NDEFMessagesTransformable`, which returns the following methods to provide the payload:
317
-
318
- - `string()`: Returns `NDEFMessages<string>`, where all payloads are strings.
319
- - `base64()`: Returns `NDEFMessages<string>`, where all payloads are the base64-encoded payloads read from the NFC tag.
320
- - `uint8Array()`: Returns `NDEFMessages<Uint8Array>`, where all payloads are the `Uint8Array` bytes from the NFC tag.
321
- - `numberArray()`: Returns `NDEFMessages<number[]>`, where all payloads' bytes from the NFC tag are represented as a `number[]`.
322
-
323
- **Parameters**:
324
-
325
- - `listener: (data: NDEFMessagesTransformable) => void` - The function to call when an NFC tag is detected.
326
-
327
- **Returns**: `() => void` Unsubscribe function to remove just this listener.
328
-
329
- ```typescript
330
- const offRead = NFC.onRead((data) => {
331
- const textRecords = data.string(); // Decoded string representation
332
- const base64Records = data.base64(); // Original base64 payloads
333
- const bytesRecords = data.uint8Array(); // Uint8Array payloads
334
- const numArrayRecords = data.numberArray(); // number[] representation
335
- console.log(textRecords);
336
- });
337
-
338
- // Later (component unmount / cleanup)
339
- offRead();
340
- ```
341
-
342
- > Tip: Register listeners once per screen/component and always dispose them when unmounting to avoid duplicate callbacks.
343
-
344
- ````
345
-
346
- #### On Error
347
-
348
- Adds a listener for NFC error events.
349
-
350
- **Parameters**:
351
-
352
- - `listener: (error: NFCError) => void` - The function to call when an NFC error occurs.
353
-
354
- **Returns**: `() => void` Unsubscribe function.
355
-
356
- #### On Error
357
-
358
- Adds a listener for NFC error events.
359
-
360
- **Parameters**:
361
-
362
- - `listener: (error: NFCError) => void` - The function to call when an NFC error occurs.
363
-
364
- **Returns**: `() => void` Unsubscribe function.
365
-
366
- ```typescript
367
- const offError = NFC.onError(err => console.error('NFC Error:', err));
368
- // later
369
- offError();
370
- ````
371
-
372
- ### Interfaces
373
-
374
- #### `NDEFWriteOptions`
375
-
376
- Options for writing an NDEF message.
377
-
378
- ```typescript
379
- interface NDEFWriteOptions<T extends string | number[] | Uint8Array = string> {
380
- records: NDEFRecord<T>[];
381
- /**
382
- * When true, bypasses automatic Well Known Type formatting (Text 'T' and URI 'U' prefixes).
383
- * All payloads are written as raw bytes without additional framing.
384
- */
385
- rawMode?: boolean;
386
- }
387
- ```
388
-
389
- #### `NDEFMessagesTransformable`
390
-
391
- Returned by `onRead` and includes the following methods to provide the payload:
392
-
393
- - `string()`: Returns `NDEFMessages<string>`, where all payloads are strings.
394
- - `base64()`: Returns `NDEFMessages<string>`, where all payloads are the base64-encoded payloads read from the NFC tag.
395
- - `uint8Array()`: Returns `NDEFMessages<Uint8Array>`, where all payloads are the `Uint8Array` bytes from the NFC tag.
396
- - `numberArray()`: Returns `NDEFMessages<number[]>`, where all payloads bytes from the NFC tag represented as a `number[]`.
397
-
398
- ```typescript
399
- interface NDEFMessagesTransformable {
400
- base64: () => NDEFMessages<string>; // Original base64 strings
401
- uint8Array: () => NDEFMessages<Uint8Array>; // Raw bytes
402
- string: () => NDEFMessages<string>; // Decoded (T & U handled, others UTF-8 best-effort)
403
- numberArray: () => NDEFMessages<number[]>; // Raw bytes as number[]
404
- }
405
- ```
406
-
407
- #### `NDEFMessages`
408
-
409
- Data received from an NFC tag.
410
-
411
- ```typescript
412
- interface NDEFMessages {
413
- messages: NDEFMessage[];
414
- tagInfo?: TagInfo;
415
- }
416
- ```
417
-
418
- #### `TagInfo`
419
-
420
- Information about the NFC tag that was read.
421
-
422
- ```typescript
423
- interface TagInfo {
424
- /**
425
- * The unique identifier of the tag (UID) as a hex string
426
- */
427
- uid?: string;
428
-
429
- /**
430
- * The NFC tag technology types supported
431
- */
432
- techTypes?: string[];
433
-
434
- /**
435
- * The maximum size of NDEF message that can be written to this tag (if applicable)
436
- */
437
- maxSize?: number;
438
-
439
- /**
440
- * Whether the tag is writable
441
- */
442
- isWritable?: boolean;
443
-
444
- /**
445
- * The tag type (e.g., "ISO14443-4", "MifareClassic", etc.)
446
- */
447
- type?: string;
448
-
449
- /**
450
- * Present when the plugin downgraded capabilities for compatibility.
451
- */
452
- fallback?: boolean;
453
-
454
- /**
455
- * Which fallback strategy is in use (`compat` or `ndef`).
456
- */
457
- fallbackMode?: 'compat' | 'ndef';
458
-
459
- /**
460
- * Reason metadata (e.g., `missing-entitlement`).
461
- */
462
- reason?: string;
463
- }
464
- ```
465
-
466
- #### `StartScanOptions`
467
-
468
- Optional tweaks for the iOS reader behavior.
469
-
470
- ```typescript
471
- interface StartScanOptions {
472
- mode?: 'auto' | 'full' | 'compat' | 'ndef';
473
- forceFull?: boolean;
474
- forceCompat?: boolean;
475
- forceNDEF?: boolean;
476
- }
477
- ```
478
-
479
- #### `NDEFMessage`
480
-
481
- An NDEF message consisting of one or more records.
482
-
483
- ```typescript
484
- interface NDEFMessage {
485
- records: NDEFRecord[];
486
- }
487
- ```
488
-
489
- #### `NDEFRecord`
490
-
491
- An NDEF record. `payload` is, by default, an array of bytes representing the data; this is how an `NDEFRecord` is read from an NFC tag. You can choose to provide an `NDEFRecord` as a string a `Uint8Array` also.
492
-
493
- ```typescript
494
- interface NDEFRecord<T = number[]> {
495
- /**
496
- * The type of the record.
497
- */
498
- type: string;
499
-
500
- /**
501
- * The payload of the record.
502
- */
503
- payload: T;
504
- }
505
- ```
506
-
507
- #### `NFCError`
508
-
509
- An NFC error.
510
-
511
- ```typescript
512
- interface NFCError {
513
- /**
514
- * The error message.
515
- */
516
- error: string;
517
- }
518
- ```
519
-
520
- ## Integration into a Capacitor App
521
-
522
- To integrate this plugin into your Capacitor app:
523
-
524
- 1. **Install the plugin:**
525
-
526
- ```bash
527
- npm install @exxili/capacitor-nfc
528
- npx cap sync
529
- ```
530
-
531
- 2. **Import the plugin in your code:**
532
-
533
- ```typescript
534
- import { NFC } from '@exxili/capacitor-nfc';
535
- ```
536
-
537
- 3. **Use the plugin methods as described in the [Usage](#usage) section.**
538
-
539
- ## Example
540
-
541
- Here's a complete example of how to read and write NFC tags in your app:
542
-
543
- ```typescript
544
- import { NFC, NDEFWriteOptions, NFCError, NDEFMessagesTransformable } from '@exxili/capacitor-nfc';
545
-
546
- // Check if NFC is supported (optional gating logic)
547
- const { supported } = await NFC.isSupported();
548
- if (!supported) {
549
- console.warn('NFC not supported on this device');
550
- }
551
-
552
- // Start NFC scanning (needed on iOS only)
553
- NFC.startScan().catch((err) => console.error('Failed to start scan', err));
554
-
555
- // Read listener returns a transformable wrapper
556
- const offRead = NFC.onRead((data: NDEFMessagesTransformable) => {
557
- const textView = data.string(); // NDEFMessages<string>
558
- const rawBytesView = data.uint8Array(); // NDEFMessages<Uint8Array>
559
-
560
- const firstText = textView.messages[0]?.records[0]?.payload;
561
- const firstLength = rawBytesView.messages[0]?.records[0]?.payload.length;
562
- console.log('First text record:', firstText);
563
- console.log('First record byte length:', firstLength);
564
- });
565
-
566
- // Error listener (covers read & write errors)
567
- const offError = NFC.onError((error: NFCError) => console.error('NFC Error:', error));
568
-
569
- // Prepare an NDEF message to write (auto-formats Text/URI if payload is string)
570
- const message: NDEFWriteOptions = {
571
- records: [
572
- { type: 'T', payload: 'Hello, NFC!' },
573
- { type: 'U', payload: 'https://example.com' },
574
- ],
575
- };
576
-
577
- await NFC.writeNDEF(message).catch((err) => console.error('Write failed', err));
578
-
579
- const offWrite = NFC.onWrite(() => console.log('Write success'));
580
-
581
- // Later (cleanup)
582
- offRead();
583
- offError();
584
- offWrite();
585
- ```
586
-
587
- ## License
588
-
589
- [MIT License](https://opensource.org/license/mit)
590
-
591
- ---
592
-
593
- **Support**: If you encounter any issues or have questions, feel free to open an issue.
594
-
595
- ---
1
+ # Capacitor NFC Plugin (@exxili/capacitor-nfc)
2
+
3
+ A Capacitor plugin for reading and writing NFC tags on iOS and Android devices. This plugin allows you to:
4
+
5
+ - Read NDEF messages from NFC tags.
6
+ - Write NDEF messages to NFC tags.
7
+
8
+ **Note**: NFC functionality is only available on compatible iOS devices running iOS 13.0 or later.
9
+
10
+ This project is maintained on a best-effort basis. No response, review, or release timeframe is guaranteed
11
+
12
+ ## Table of Contents
13
+
14
+ - [Capacitor NFC Plugin (@exxili/capacitor-nfc)](#capacitor-nfc-plugin-exxilicapacitor-nfc)
15
+ - [Table of Contents](#table-of-contents)
16
+ - [Installation](#installation)
17
+ - [iOS Setup](#ios-setup)
18
+ - [1. Enable NFC Capability](#1-enable-nfc-capability)
19
+ - [2. Add Usage Description](#2-add-usage-description)
20
+ - [Android Setup](#android-setup)
21
+ - [Usage](#usage)
22
+ - [Reading NFC Tags](#reading-nfc-tags)
23
+ - [Writing NFC Tags](#writing-nfc-tags)
24
+ - [API](#api)
25
+ - [Methods](#methods)
26
+ - [`isSupported()`](#issupported)
27
+ - [`startScan()`](#startscan)
28
+ - [`cancelScan()`](#cancelscan)
29
+ - [`writeNDEF(options: NDEFWriteOptions<T extends string | number[] | Uint8Array = string>)`](#writendefoptions-ndefwriteoptionst-extends-string--number--uint8array--string)
30
+ - [`cancelWriteAndroid()`](#cancelwriteandroid)
31
+ - [Listeners](#listeners)
32
+ - [`onRead(listener: (data: NDEFMessagesTransformable) => void)`](#onreadlistener-data-ndefmessagestransformable--void)
33
+ - [Interfaces](#interfaces)
34
+ - [`NDEFWriteOptions`](#ndefwriteoptions)
35
+ - [`NDEFMessagesTransformable`](#ndefmessagestransformable)
36
+ - [`NDEFMessages`](#ndefmessages)
37
+ - [`NDEFMessage`](#ndefmessage)
38
+ - [`NDEFRecord`](#ndefrecord)
39
+ - [`NFCError`](#nfcerror)
40
+ - [Integration into a Capacitor App](#integration-into-a-capacitor-app)
41
+ - [Example](#example)
42
+ - [License](#license)
43
+
44
+ ## Installation
45
+
46
+ Install the plugin using npm:
47
+
48
+ ```bash
49
+ npm install @exxili/capacitor-nfc
50
+ npx cap sync
51
+ ```
52
+
53
+ ## iOS Setup
54
+
55
+ To use NFC functionality on iOS, you need to perform some additional setup steps.
56
+
57
+ ### 1. Enable NFC Capability
58
+
59
+ In Xcode:
60
+
61
+ 1. Open your project (`.xcworkspace` file) in Xcode.
62
+ 2. Select your project in the Project Navigator.
63
+ 3. Select your app target.
64
+ 4. Go to the **Signing & Capabilities** tab.
65
+ 5. Click the `+ Capability` button.
66
+ 6. Add **Near Field Communication Tag Reading**.
67
+
68
+ > **Advanced tag formats:** If you need ISO 7816, ISO 15693, or FeliCa access (to read raw UIDs, system codes, etc.), Apple requires additional entitlements in your provisioning profile and `Info.plist`. The plugin will fall back automatically when they are absent, but to unlock the full feature set add the relevant keys:
69
+ >
70
+ > ```xml
71
+ > <key>com.apple.developer.nfc.readersession.felica.systemcodes</key>
72
+ > <array>
73
+ > <string>12FC</string>
74
+ > <string>0000</string>
75
+ > </array>
76
+ > <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
77
+ > <array>
78
+ > <string>D2760000850100</string>
79
+ > <string>D2760000850101</string>
80
+ > <string>D2760001180101</string>
81
+ > <string>00000000000000</string>
82
+ > </array>
83
+ > ```
84
+ >
85
+ > Replace the sample identifiers with the values required for your tags. Consult Apple's CoreNFC documentation for the complete list of entitlement keys.
86
+
87
+ ### 2. Add Usage Description
88
+
89
+ Add the `NFCReaderUsageDescription` key to your `Info.plist` file to explain why your app needs access to NFC.
90
+
91
+ In your `Info.plist` file (usually located at `ios/App/App/Info.plist`), add:
92
+
93
+ ```xml
94
+ <key>NFCReaderUsageDescription</key>
95
+ <string>This app requires access to NFC to read and write NFC tags.</string>
96
+ ```
97
+
98
+ Replace the description with a message that explains why your app needs NFC access.
99
+
100
+ ## Android Setup
101
+
102
+ Add the following to your `AndroidManifest.xml` file:
103
+
104
+ ```xml
105
+ <uses-permission android:name="android.permission.NFC" />
106
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
107
+ ```
108
+
109
+ ## Usage
110
+
111
+ Import the plugin into your code:
112
+
113
+ ```typescript
114
+ import { NFC } from '@exxili/capacitor-nfc';
115
+ ```
116
+
117
+ ### Reading NFC Tags
118
+
119
+ To read NFC tags, you need to listen for `nfcTag` events. On iOS, you must also start the NFC scanning session using `startScan()`.
120
+
121
+ ```typescript
122
+ import { NFC, NDEFMessagesTransformable, NFCError } from '@exxili/capacitor-nfc';
123
+
124
+ // Start NFC scanning (iOS only)
125
+ NFC.startScan().catch((error) => {
126
+ console.error('Error starting NFC scan:', error);
127
+ });
128
+
129
+ // Listen for NFC tag detection
130
+ NFC.onRead((data: NDEFMessagesTransformable) => {
131
+ // Text (T) and URI (U) records decoded; others best-effort UTF-8
132
+ const asString = data.string();
133
+ console.log('First record text payload:', asString.messages[0]?.records[0]?.payload);
134
+
135
+ // Raw bytes
136
+ const asUint8 = data.uint8Array();
137
+ console.log('First record raw bytes length:', asUint8.messages[0]?.records[0]?.payload.length);
138
+
139
+ // Access tag information (UID, tech types, etc.)
140
+ const info = asString.tagInfo;
141
+ if (info?.fallback) {
142
+ console.log('Reader fallback mode:', info.fallbackMode, 'Reason:', info.reason);
143
+ } else if (info) {
144
+ console.log('Tag UID:', info.uid);
145
+ console.log('Tag technologies:', info.techTypes);
146
+ console.log('Tag type:', info.type);
147
+ if (info.maxSize) {
148
+ console.log('Max NDEF size:', info.maxSize);
149
+ }
150
+ console.log('Is writable:', info.isWritable);
151
+ }
152
+ });
153
+
154
+ // Handle NFC errors
155
+ NFC.onError((error: NFCError) => {
156
+ console.error('NFC Error:', error);
157
+ });
158
+ ```
159
+
160
+ ### Writing NFC Tags
161
+
162
+ To write NDEF messages to NFC tags, use the `writeNDEF` method and listen for `onWrite` events.
163
+
164
+ ```typescript
165
+ import { NFC, NDEFWriteOptions, NFCError } from '@exxili/capacitor-nfc';
166
+
167
+ const message: NDEFWriteOptions = {
168
+ records: [
169
+ {
170
+ type: 'T', // Well Known Text record. String payload will be encoded as: [status][lang='en'][UTF-8 text]
171
+ payload: 'Hello, NFC!',
172
+ },
173
+ {
174
+ type: 'U', // Well Known URI record. String payload encoded as: [0x00][URI bytes]
175
+ payload: 'https://example.com',
176
+ },
177
+ {
178
+ type: 'T',
179
+ payload: new Uint8Array([0x01, 0x65, 0x48, 0x69]), // Raw bytes preserved (DO NOT re-format)
180
+ },
181
+ ],
182
+ };
183
+
184
+ // For complete control over binary content, use raw mode:
185
+ const rawMessage: NDEFWriteOptions = {
186
+ rawMode: true, // Bypasses automatic Text/URI formatting
187
+ records: [
188
+ {
189
+ type: 'T',
190
+ payload: 'Hello, NFC!', // Written as UTF-8 bytes without Text record prefix
191
+ },
192
+ {
193
+ type: 'custom',
194
+ payload: new Uint8Array([0x01, 0x02, 0x03, 0x04]), // Exact bytes written to tag
195
+ },
196
+ ],
197
+ };
198
+
199
+ // Write NDEF message to NFC tag
200
+ NFC.writeNDEF(message)
201
+ .then(() => {
202
+ console.log('Write initiated');
203
+ })
204
+ .catch((error) => {
205
+ console.error('Error writing to NFC tag:', error);
206
+ });
207
+
208
+ // Listen for write success
209
+ NFC.onWrite(() => {
210
+ console.log('NDEF message written successfully.');
211
+ });
212
+
213
+ // Handle NFC errors
214
+ NFC.onError((error: NFCError) => {
215
+ console.error('NFC Error:', error);
216
+ });
217
+ ```
218
+
219
+ ## API
220
+
221
+ ### Methods
222
+
223
+ #### `isSupported()`
224
+
225
+ Returns if NFC is supported on the scanning device.
226
+
227
+ **Returns**: `Promise<{ supported: boolean }>`
228
+
229
+ #### `startScan()`
230
+
231
+ Starts the NFC scanning session on **_iOS only_**. Android devices are always in reading mode, so setting up the `nfcTag` listener is sufficient to handle tag reads on Android.
232
+
233
+ The iOS implementation now adapts automatically if the extended CoreNFC entitlements (ISO 7816, ISO 15693, FeliCa) are missing. The plugin first attempts the advanced tag reader so you can access UID/tech info. When iOS reports `Missing required entitlement`, the plugin downgrades to a compatibility mode (ISO 14443 only) and, if necessary, to the classic NDEF reader. A synthetic `nfcTag` event is emitted with `tagInfo.fallback`, `tagInfo.fallbackMode`, and `tagInfo.reason` so your UI can react immediately.
234
+
235
+ You can override the mode explicitly:
236
+
237
+ - `mode: 'auto'` (default) – advanced reader with automatic downgrade and caching.
238
+ - `mode: 'full'` – force a fresh attempt at the advanced reader, resetting cached fallback state.
239
+ - `mode: 'compat'` – skip the advanced probe and jump straight to the ISO 14443 compatibility reader.
240
+ - `mode: 'ndef'` – bypass tag sessions entirely and revert to the legacy NDEF-only reader.
241
+
242
+ Legacy booleans `forceFull`, `forceCompat`, and `forceNDEF` map to the options above for backwards compatibility.
243
+
244
+ **Returns**: `Promise<void>`
245
+
246
+ ```typescript
247
+ NFC.startScan({ mode: 'auto' })
248
+ .then(() => {
249
+ // Scanning started
250
+ })
251
+ .catch((error) => {
252
+ console.error('Error starting NFC scan:', error);
253
+ });
254
+ ```
255
+
256
+ #### `cancelScan()`
257
+
258
+ Immediately invalidates the active iOS CoreNFC reader session (if any). Useful when you want to abort a scan early instead of waiting for the user to cancel or for a tag detection timeout.
259
+
260
+ Platform notes:
261
+
262
+ - iOS: Actively ends the session. Calling `startScan()` again after the returned promise resolves is safe.
263
+ - Android: No-op (Android is always passively listening via foreground dispatch; you generally just ignore future events or control UI state).
264
+
265
+ ```typescript
266
+ await NFC.cancelScan(); // iOS: ends session; Android: no-op
267
+ // Optionally restart
268
+ await NFC.startScan();
269
+ ```
270
+
271
+ **Returns**: `Promise<void>`
272
+
273
+ #### `writeNDEF(options: NDEFWriteOptions<T extends string | number[] | Uint8Array = string>)`
274
+
275
+ Writes an NDEF message to an NFC tag.
276
+
277
+ Payload may be provided as a string, `Uint8Array`, or an array of numbers.
278
+
279
+ Automatic formatting rules (to aid interoperability):
280
+
281
+ - Text (`type: 'T'` + string payload): encoded per NFC Forum RTD Text spec `[status][lang=en][UTF-8 text]`.
282
+ - URI (`type: 'U'` + string payload): encoded as `[0x00][UTF-8 URI bytes]` (prefix compression not yet applied).
283
+ - Any other `type` + string payload: UTF-8 bytes only (no extra framing).
284
+ - `Uint8Array` or `number[]` payloads are treated as raw bytes and written verbatim (never altered).
285
+
286
+ **Raw Mode**: Set `rawMode: true` to bypass automatic Well Known Type formatting entirely. All string payloads will be written as UTF-8 bytes without Text ('T') or URI ('U') prefixes, giving you complete control over the binary content.
287
+
288
+ If you need full manual control of a Text or URI record, supply raw bytes (number[] / Uint8Array) and the plugin will not modify them.
289
+
290
+ If you attempt to write zero records the promise rejects with `Error("At least one NDEF record is required")`.
291
+
292
+ Android use: since Android has no default UI for reading and writing NFC tags, it is recommended that you add a UI indicator to your application when calling `writeNDEF` and remove it in the `nfcWriteSuccess` listener callback and the `nfcError` listener callback. This will prevent accidental writes to tags that your users intended to read from.
293
+
294
+ **Parameters**:
295
+
296
+ - `options: NDEFWriteOptions<T extends string | number[] | Uint8Array = string>` - The NDEF message to write. Must include at least one record.
297
+
298
+ **Returns**: `Promise<void>`
299
+
300
+ ```typescript
301
+ NFC.writeNDEF(options)
302
+ .then(() => {
303
+ // Write initiated
304
+ })
305
+ .catch((error) => {
306
+ console.error('Error writing NDEF message:', error);
307
+ });
308
+ ```
309
+
310
+ #### `cancelWriteAndroid()`
311
+
312
+ Cancels an Android NFC write operation. Android does not have a native UI for NFC tag writing, so this method allows developers to hook up a custom UI to cancel an in-progress scan.
313
+
314
+ ### Listeners
315
+
316
+ #### `onRead(listener: (data: NDEFMessagesTransformable) => void)`
317
+
318
+ Adds a listener for NFC tag detection events. Returns type `NDEFMessagesTransformable`, which returns the following methods to provide the payload:
319
+
320
+ - `string()`: Returns `NDEFMessages<string>`, where all payloads are strings.
321
+ - `base64()`: Returns `NDEFMessages<string>`, where all payloads are the base64-encoded payloads read from the NFC tag.
322
+ - `uint8Array()`: Returns `NDEFMessages<Uint8Array>`, where all payloads are the `Uint8Array` bytes from the NFC tag.
323
+ - `numberArray()`: Returns `NDEFMessages<number[]>`, where all payloads' bytes from the NFC tag are represented as a `number[]`.
324
+
325
+ **Parameters**:
326
+
327
+ - `listener: (data: NDEFMessagesTransformable) => void` - The function to call when an NFC tag is detected.
328
+
329
+ **Returns**: `() => void` Unsubscribe function to remove just this listener.
330
+
331
+ ```typescript
332
+ const offRead = NFC.onRead((data) => {
333
+ const textRecords = data.string(); // Decoded string representation
334
+ const base64Records = data.base64(); // Original base64 payloads
335
+ const bytesRecords = data.uint8Array(); // Uint8Array payloads
336
+ const numArrayRecords = data.numberArray(); // number[] representation
337
+ console.log(textRecords);
338
+ });
339
+
340
+ // Later (component unmount / cleanup)
341
+ offRead();
342
+ ```
343
+
344
+ > Tip: Register listeners once per screen/component and always dispose them when unmounting to avoid duplicate callbacks.
345
+
346
+ ````
347
+
348
+ #### On Error
349
+
350
+ Adds a listener for NFC error events.
351
+
352
+ **Parameters**:
353
+
354
+ - `listener: (error: NFCError) => void` - The function to call when an NFC error occurs.
355
+
356
+ **Returns**: `() => void` Unsubscribe function.
357
+
358
+ #### On Error
359
+
360
+ Adds a listener for NFC error events.
361
+
362
+ **Parameters**:
363
+
364
+ - `listener: (error: NFCError) => void` - The function to call when an NFC error occurs.
365
+
366
+ **Returns**: `() => void` Unsubscribe function.
367
+
368
+ ```typescript
369
+ const offError = NFC.onError(err => console.error('NFC Error:', err));
370
+ // later
371
+ offError();
372
+ ````
373
+
374
+ ### Interfaces
375
+
376
+ #### `NDEFWriteOptions`
377
+
378
+ Options for writing an NDEF message.
379
+
380
+ ```typescript
381
+ interface NDEFWriteOptions<T extends string | number[] | Uint8Array = string> {
382
+ records: NDEFRecord<T>[];
383
+ /**
384
+ * When true, bypasses automatic Well Known Type formatting (Text 'T' and URI 'U' prefixes).
385
+ * All payloads are written as raw bytes without additional framing.
386
+ */
387
+ rawMode?: boolean;
388
+ }
389
+ ```
390
+
391
+ #### `NDEFMessagesTransformable`
392
+
393
+ Returned by `onRead` and includes the following methods to provide the payload:
394
+
395
+ - `string()`: Returns `NDEFMessages<string>`, where all payloads are strings.
396
+ - `base64()`: Returns `NDEFMessages<string>`, where all payloads are the base64-encoded payloads read from the NFC tag.
397
+ - `uint8Array()`: Returns `NDEFMessages<Uint8Array>`, where all payloads are the `Uint8Array` bytes from the NFC tag.
398
+ - `numberArray()`: Returns `NDEFMessages<number[]>`, where all payloads bytes from the NFC tag represented as a `number[]`.
399
+
400
+ ```typescript
401
+ interface NDEFMessagesTransformable {
402
+ base64: () => NDEFMessages<string>; // Original base64 strings
403
+ uint8Array: () => NDEFMessages<Uint8Array>; // Raw bytes
404
+ string: () => NDEFMessages<string>; // Decoded (T & U handled, others UTF-8 best-effort)
405
+ numberArray: () => NDEFMessages<number[]>; // Raw bytes as number[]
406
+ }
407
+ ```
408
+
409
+ #### `NDEFMessages`
410
+
411
+ Data received from an NFC tag.
412
+
413
+ ```typescript
414
+ interface NDEFMessages {
415
+ messages: NDEFMessage[];
416
+ tagInfo?: TagInfo;
417
+ }
418
+ ```
419
+
420
+ #### `TagInfo`
421
+
422
+ Information about the NFC tag that was read.
423
+
424
+ ```typescript
425
+ interface TagInfo {
426
+ /**
427
+ * The unique identifier of the tag (UID) as a hex string
428
+ */
429
+ uid?: string;
430
+
431
+ /**
432
+ * The NFC tag technology types supported
433
+ */
434
+ techTypes?: string[];
435
+
436
+ /**
437
+ * The maximum size of NDEF message that can be written to this tag (if applicable)
438
+ */
439
+ maxSize?: number;
440
+
441
+ /**
442
+ * Whether the tag is writable
443
+ */
444
+ isWritable?: boolean;
445
+
446
+ /**
447
+ * The tag type (e.g., "ISO14443-4", "MifareClassic", etc.)
448
+ */
449
+ type?: string;
450
+
451
+ /**
452
+ * Present when the plugin downgraded capabilities for compatibility.
453
+ */
454
+ fallback?: boolean;
455
+
456
+ /**
457
+ * Which fallback strategy is in use (`compat` or `ndef`).
458
+ */
459
+ fallbackMode?: 'compat' | 'ndef';
460
+
461
+ /**
462
+ * Reason metadata (e.g., `missing-entitlement`).
463
+ */
464
+ reason?: string;
465
+ }
466
+ ```
467
+
468
+ #### `StartScanOptions`
469
+
470
+ Optional tweaks for the iOS reader behavior.
471
+
472
+ ```typescript
473
+ interface StartScanOptions {
474
+ mode?: 'auto' | 'full' | 'compat' | 'ndef';
475
+ forceFull?: boolean;
476
+ forceCompat?: boolean;
477
+ forceNDEF?: boolean;
478
+ }
479
+ ```
480
+
481
+ #### `NDEFMessage`
482
+
483
+ An NDEF message consisting of one or more records.
484
+
485
+ ```typescript
486
+ interface NDEFMessage {
487
+ records: NDEFRecord[];
488
+ }
489
+ ```
490
+
491
+ #### `NDEFRecord`
492
+
493
+ An NDEF record. `payload` is, by default, an array of bytes representing the data; this is how an `NDEFRecord` is read from an NFC tag. You can choose to provide an `NDEFRecord` as a string a `Uint8Array` also.
494
+
495
+ ```typescript
496
+ interface NDEFRecord<T = number[]> {
497
+ /**
498
+ * The type of the record.
499
+ */
500
+ type: string;
501
+
502
+ /**
503
+ * The payload of the record.
504
+ */
505
+ payload: T;
506
+ }
507
+ ```
508
+
509
+ #### `NFCError`
510
+
511
+ An NFC error.
512
+
513
+ ```typescript
514
+ interface NFCError {
515
+ /**
516
+ * The error message.
517
+ */
518
+ error: string;
519
+ }
520
+ ```
521
+
522
+ ## Integration into a Capacitor App
523
+
524
+ To integrate this plugin into your Capacitor app:
525
+
526
+ 1. **Install the plugin:**
527
+
528
+ ```bash
529
+ npm install @exxili/capacitor-nfc
530
+ npx cap sync
531
+ ```
532
+
533
+ 2. **Import the plugin in your code:**
534
+
535
+ ```typescript
536
+ import { NFC } from '@exxili/capacitor-nfc';
537
+ ```
538
+
539
+ 3. **Use the plugin methods as described in the [Usage](#usage) section.**
540
+
541
+ ## Example
542
+
543
+ Here's a complete example of how to read and write NFC tags in your app:
544
+
545
+ ```typescript
546
+ import { NFC, NDEFWriteOptions, NFCError, NDEFMessagesTransformable } from '@exxili/capacitor-nfc';
547
+
548
+ // Check if NFC is supported (optional gating logic)
549
+ const { supported } = await NFC.isSupported();
550
+ if (!supported) {
551
+ console.warn('NFC not supported on this device');
552
+ }
553
+
554
+ // Start NFC scanning (needed on iOS only)
555
+ NFC.startScan().catch((err) => console.error('Failed to start scan', err));
556
+
557
+ // Read listener returns a transformable wrapper
558
+ const offRead = NFC.onRead((data: NDEFMessagesTransformable) => {
559
+ const textView = data.string(); // NDEFMessages<string>
560
+ const rawBytesView = data.uint8Array(); // NDEFMessages<Uint8Array>
561
+
562
+ const firstText = textView.messages[0]?.records[0]?.payload;
563
+ const firstLength = rawBytesView.messages[0]?.records[0]?.payload.length;
564
+ console.log('First text record:', firstText);
565
+ console.log('First record byte length:', firstLength);
566
+ });
567
+
568
+ // Error listener (covers read & write errors)
569
+ const offError = NFC.onError((error: NFCError) => console.error('NFC Error:', error));
570
+
571
+ // Prepare an NDEF message to write (auto-formats Text/URI if payload is string)
572
+ const message: NDEFWriteOptions = {
573
+ records: [
574
+ { type: 'T', payload: 'Hello, NFC!' },
575
+ { type: 'U', payload: 'https://example.com' },
576
+ ],
577
+ };
578
+
579
+ await NFC.writeNDEF(message).catch((err) => console.error('Write failed', err));
580
+
581
+ const offWrite = NFC.onWrite(() => console.log('Write success'));
582
+
583
+ // Later (cleanup)
584
+ offRead();
585
+ offError();
586
+ offWrite();
587
+ ```
588
+
589
+ ## License
590
+
591
+ [MIT License](https://opensource.org/license/mit)
592
+
593
+ ---
594
+
595
+ **Support**: If you encounter any issues or have questions, feel free to open an issue.
596
+
597
+ ---