community-cordova-plugin-nfc 1.8.0 → 1.9.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/CHANGELOG.md CHANGED
@@ -3,7 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [1.8.0](https://github.com/EYALIN/community-cordova-plugin-nfc/compare/v1.7.1...v1.8.0) (unreleased)
6
+ ## [1.9.0](https://github.com/EYALIN/community-cordova-plugin-nfc/compare/v1.8.0...v1.9.0) (2026-10-09)
7
+
8
+ ### Features
9
+
10
+ * **iOS: `nfc.erase` and `nfc.makeReadOnly`** (PLU-257). Both were Android-only; on iOS 13+, call
11
+ them after `scanNdef`/`scanTag({keepSessionOpen: true})` resolves. `erase` writes an empty NDEF
12
+ record, failing with `READ_ONLY` or `NOT_SUPPORTED` as appropriate; `makeReadOnly` locks the tag
13
+ via `writeLockWithCompletionHandler:`, resolving `{alreadyLocked: true}` if the tag is already
14
+ locked and failing with `NOT_SUPPORTED` if the tag can't be locked. Both accept an optional
15
+ `{alertMessage}` on iOS. Below iOS 13, both fail with `NOT_SUPPORTED`.
16
+ * **iOS: `nfc.transceive` reaches MIFARE tags** (PLU-258), via `NFCMiFareTag.sendMiFareCommand` -
17
+ the response has no `sw1`/`sw2` appended, unlike the existing ISO 7816 path. As an experiment,
18
+ the same MIFARE path is also tried on a tag from an NDEF-session scan when it happens to conform
19
+ to the MIFARE protocol; that fallback is unverified on real hardware.
20
+ * **`nfc.capabilities()`** on every platform: `{erase, makeReadOnly, ntagPassword, platform}`.
21
+ `true`/`true` on Android; on iOS, `erase`/`makeReadOnly` are `true` from 13 onward. `ntagPassword`
22
+ is `true` on Android and **`false` on iOS** even on 13+ - the MIFARE `transceive` path those
23
+ helpers would need (PLU-255/PLU-258) is not yet confirmed against a real NTAG213/215.
24
+
25
+ ## [1.8.0](https://github.com/EYALIN/community-cordova-plugin-nfc/compare/v1.7.1...v1.8.0) (2026-09-27)
7
26
 
8
27
  ### Fixes
9
28
 
package/README.md CHANGED
@@ -20,6 +20,13 @@ or if you're asking for new features or priority bug fixes. Thank you!
20
20
  - **Advanced Tag Analysis** (v1.5.0+) - Read raw memory, get NTAG version, counter, signature
21
21
  - **TypeScript Support** - Full TypeScript definitions included
22
22
 
23
+ ## What's New in 1.9.0
24
+
25
+ iOS: `nfc.erase` and `nfc.makeReadOnly` now work (13+, PLU-257). `nfc.transceive` can also reach a
26
+ MIFARE tag (PLU-258), the basis for the Android NTAG password helpers, though that path is not yet
27
+ verified on real hardware. New `nfc.capabilities()` on every platform reports which of these are
28
+ actually supported, instead of trying an operation and parsing the error.
29
+
23
30
  ## What's New in 1.8.0
24
31
 
25
32
  Crash and connection fixes on Android, coded errors on both platforms, correct NTAG helpers, iOS
@@ -247,7 +254,10 @@ await nfc.close();
247
254
 
248
255
  ## Password protection, originality, ISO 15693, MIFARE Classic (1.8.0)
249
256
 
250
- All Android-only (iOS can only transceive ISO 7816 APDUs), after `nfc.connect(tech)` on a scanned tag.
257
+ All Android-only, after `nfc.connect(tech)` on a scanned tag. On iOS, `nfc.transceive` can reach
258
+ a MIFARE tag via `NFCMiFareTag` (v1.9.0, PLU-258), which is what these helpers would need, but that
259
+ path is unverified on real NTAG213/215 hardware - `nfc.capabilities().ntagPassword` reports `false`
260
+ on iOS until it is proven, and these higher-level helpers themselves are not wired to iOS yet.
251
261
  Bytes can be given as a hex string, a byte array or a `Uint8Array`.
252
262
 
253
263
  ```js
@@ -342,6 +352,10 @@ or a hook):
342
352
  | `NFCReadOnlyTag` | Tag is read only. | writing to a locked tag |
343
353
  | `NFCDataWriteFailed` | Write failed. | the write failed |
344
354
  | `NFCUnknownNdefTag` | Unknown NDEF tag status. | unexpected NDEF status |
355
+ | `NFCTagAlreadyReadOnly` | Tag is already read only. | `makeReadOnly` on an already-locked tag (v1.9.0) |
356
+ | `NFCLockNotSupported` | This tag does not support being locked. | `makeReadOnly` on a tag whose hardware can't lock (v1.9.0) |
357
+ | `NFCLockFailed` | Failed to lock tag. | the lock write failed (v1.9.0) |
358
+ | `NFCTagLocked` | Tag is now read only. | a lock succeeded (v1.9.0) |
345
359
 
346
360
  Example `he.lproj/Localizable.strings`:
347
361
 
@@ -377,6 +391,7 @@ English message.
377
391
  - [nfc.scanNdef](#nfcscanndef)
378
392
  - [nfc.scanTag](#nfcscanTag)
379
393
  - [nfc.cancelScan](#nfccancelscan)
394
+ - [nfc.capabilities](#nfccapabilities)
380
395
 
381
396
  ## ReaderMode
382
397
 
@@ -642,12 +657,14 @@ On iOS you can optionally write to NFC tag using the read session
642
657
 
643
658
  Makes a NFC tag read only. **Warning this is permanent.**
644
659
 
645
- nfc.makeReadOnly([onSuccess], [onFailure]);
660
+ nfc.makeReadOnly([onSuccess], [onFailure], [options]);
646
661
 
647
662
  ### Parameters
648
663
 
649
664
  - __onSuccess__: (Optional) The callback that is called when the tag is locked.
650
665
  - __onFailure__: (Optional) The callback that is called if there was an error.
666
+ - __options__: (Optional, **iOS only**, v1.9.0) `{ alertMessage: "..." }` overrides the text shown
667
+ on the system NFC sheet while locking. Ignored on Android.
651
668
 
652
669
  ### Description
653
670
 
@@ -679,9 +696,22 @@ Example usage
679
696
 
680
697
  },
681
698
 
699
+ On **iOS** (13+), call it after `nfc.scanNdef({keepSessionOpen: true})` or
700
+ `nfc.scanTag({keepSessionOpen: true})` resolves - there is no NDEF event handler, so lock the tag
701
+ from that resolved handler instead:
702
+
703
+ const tag = await nfc.scanNdef({ keepSessionOpen: true });
704
+ await nfc.makeReadOnly(null, null, { alertMessage: "Locking tag…" });
705
+
706
+ A tag that is already read only resolves successfully (`{alreadyLocked: true}`) rather than
707
+ failing. A tag whose hardware doesn't support locking fails with `NOT_SUPPORTED`. Below iOS 13,
708
+ `makeReadOnly` always fails with `NOT_SUPPORTED`. Check `nfc.capabilities().makeReadOnly` to
709
+ detect support ahead of time.
710
+
682
711
  ### Supported Platforms
683
712
 
684
713
  - Android
714
+ - iOS 13+ (v1.9.0, PLU-257)
685
715
 
686
716
  ## nfc.share
687
717
 
@@ -734,22 +764,31 @@ Function `nfc.unshare` stops sharing data via peer-to-peer.
734
764
 
735
765
  Erase a NDEF tag
736
766
 
737
- nfc.erase([onSuccess], [onFailure]);
767
+ nfc.erase([onSuccess], [onFailure], [options]);
738
768
 
739
769
  ### Parameters
740
770
 
741
771
  - __onSuccess__: (Optional) The callback that is called when sharing stops.
742
772
  - __onFailure__: (Optional) The callback that is called if there was an error.
773
+ - __options__: (Optional, **iOS only**, v1.9.0) `{ alertMessage: "..." }` overrides the text shown
774
+ on the system NFC sheet while erasing. Ignored on Android.
743
775
 
744
776
  ### Description
745
777
 
746
778
  Function `nfc.erase` erases a tag by writing an empty message. Will format unformatted tags before writing.
747
779
 
748
- This method *must* be called from within an NDEF Event Handler.
780
+ On **Android** this method *must* be called from within an NDEF Event Handler.
781
+
782
+ On **iOS** (13+), call it after `nfc.scanNdef({keepSessionOpen: true})` or
783
+ `nfc.scanTag({keepSessionOpen: true})` resolves, the same way as `nfc.makeReadOnly` above. A
784
+ read-only tag fails with `READ_ONLY`; a tag whose hardware doesn't support writing fails with
785
+ `NOT_SUPPORTED`. Below iOS 13, `erase` always fails with `NOT_SUPPORTED`. Check
786
+ `nfc.capabilities().erase` to detect support ahead of time.
749
787
 
750
788
  ### Supported Platforms
751
789
 
752
790
  - Android
791
+ - iOS 13+ (v1.9.0, PLU-257)
753
792
  ## nfc.handover
754
793
 
755
794
  Send a file to another device via NFC handover.
@@ -1009,6 +1048,47 @@ Function `cancelScan` stops the [NFCReaderSession](https://developer.apple.com/d
1009
1048
 
1010
1049
  - iOS
1011
1050
 
1051
+ ## nfc.capabilities
1052
+
1053
+ Reports which optional NFC features this platform actually implements, so the app can decide
1054
+ what to offer instead of trying an operation and parsing the error. Added v1.9.0 (PLU-258).
1055
+
1056
+ nfc.capabilities([onSuccess], [onFailure]);
1057
+
1058
+ ### Parameters
1059
+
1060
+ - __onSuccess__: (Optional) Called with `{erase, makeReadOnly, ntagPassword, platform}`.
1061
+ - __onFailure__: (Optional) The callback that is called if there was an error.
1062
+
1063
+ ### Returns
1064
+
1065
+ - Promise<{erase: boolean, makeReadOnly: boolean, ntagPassword: boolean, platform: string}> when
1066
+ called without callbacks.
1067
+
1068
+ ### Description
1069
+
1070
+ - `erase` / `makeReadOnly`: `true` on Android always, and on iOS from 13 onward; `false` on iOS
1071
+ below 13 and on any other platform.
1072
+ - `ntagPassword`: whether `ntagAuthenticate` / `ntagSetPassword` / `ntagRemovePassword` (and the
1073
+ underlying raw `transceive` they use) are expected to work. `true` on Android. **`false` on
1074
+ iOS**, even on 13+ - the `NFCMiFareTag` raw-command path those helpers need (PLU-255/PLU-258)
1075
+ hasn't been confirmed against a real NTAG213/215 yet. Re-check this value after that is proven
1076
+ on device; it may become conditional on iOS version at that point.
1077
+ - `platform`: `cordova.platformId`.
1078
+
1079
+ ### Quick Example
1080
+
1081
+ const caps = await nfc.capabilities();
1082
+ if (caps.makeReadOnly) {
1083
+ showLockButton();
1084
+ }
1085
+
1086
+ ### Supported Platforms
1087
+
1088
+ - Android
1089
+ - iOS
1090
+ - any other platform (always reports every feature as unsupported)
1091
+
1012
1092
 
1013
1093
  # Reader Mode Functions
1014
1094
 
@@ -1183,10 +1263,23 @@ Send raw command to the tag and receive the response.
1183
1263
 
1184
1264
  ### Description
1185
1265
 
1186
- Function `transceive` sends raw commands to the tag and receives the response. `nfc.connect` must be called before calling `transceive`. Data passed to transceive can be a hex string representation of bytes or an ArrayBuffer. The response is returned as an ArrayBuffer in the promise.
1266
+ Function `transceive` sends raw commands to the tag and receives the response. On **Android**, `nfc.connect` must be called before calling `transceive`. Data passed to transceive can be a hex string representation of bytes or an ArrayBuffer. The response is returned as an ArrayBuffer in the promise.
1187
1267
 
1188
1268
  See Android's documentation [IsoDep.transceive()](https://developer.android.com/reference/android/nfc/tech/IsoDep.html#transceive(byte[])), [NfcV.transceive()](https://developer.android.com/reference/android/nfc/tech/NfcV.html#transceive(byte[])), [MifareUltralight.transceive()](https://developer.android.com/reference/android/nfc/tech/MifareUltralight.html#transceive(byte[])) for more info.
1189
1269
 
1270
+ On **iOS** (13+), there is no `connect` - call `transceive` after `nfc.scanTag({keepSessionOpen: true})`
1271
+ resolves. The tag is dispatched automatically:
1272
+
1273
+ - An **ISO 7816** tag (since 1.7.0): the response has `sw1`/`sw2` appended, matching Android's
1274
+ `IsoDep.transceive`.
1275
+ - A **MIFARE** tag (v1.9.0, PLU-258), via `NFCMiFareTag.sendMiFareCommand` - this is what
1276
+ `ntagAuthenticate` / `ntagSetPassword` / `ntagRemovePassword` need on iOS. The response is the
1277
+ raw bytes the tag sent back, with **no** `sw1`/`sw2` appended. As an experiment, this path is
1278
+ also tried on the tag from an NDEF-session scan when it happens to conform to the MIFARE
1279
+ protocol, since CoreNFC doesn't document whether that can occur; this fallback is **unverified
1280
+ on real hardware**. Check `nfc.capabilities().ntagPassword` before relying on it - it is `false`
1281
+ by default on iOS until proven.
1282
+
1190
1283
  ### Parameters
1191
1284
 
1192
1285
  - __data__: a string of hex data or an ArrayBuffer
@@ -1210,6 +1303,7 @@ See Android's documentation [IsoDep.transceive()](https://developer.android.com/
1210
1303
  ### Supported Platforms
1211
1304
 
1212
1305
  - Android
1306
+ - iOS 13+ (ISO 7816 since 1.7.0; MIFARE since v1.9.0, PLU-258)
1213
1307
 
1214
1308
  ## nfc.close
1215
1309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "community-cordova-plugin-nfc",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Community Cordova NFC Plugin - Read and write NDEF messages, advanced tag analysis, raw memory access",
5
5
  "main": "./types/index.d.ts",
6
6
  "types": "./types/index.d.ts",
package/plugin.xml CHANGED
@@ -7,7 +7,7 @@
7
7
  xmlns="http://apache.org/cordova/ns/plugins/1.0"
8
8
  xmlns:android="http://schemas.android.com/apk/res/android"
9
9
  id="community-cordova-plugin-nfc"
10
- version="1.8.0">
10
+ version="1.9.0">
11
11
 
12
12
  <name>NFC</name>
13
13
  <description>Community Cordova NFC Plugin - Read and write NDEF messages, advanced tag analysis, raw memory access</description>
@@ -32,9 +32,17 @@
32
32
  - (void)enabled:(CDVInvokedUrlCommand *)command;
33
33
  - (void)writeTag:(CDVInvokedUrlCommand *)command;
34
34
 
35
- // ISO 7816 APDU exchange (added v1.7.0) - mirrors Android nfc.transceive
35
+ // erase / makeReadOnly (added v1.9.0, PLU-257) - mirror Android nfc.erase / nfc.makeReadOnly
36
+ - (void)eraseTag:(CDVInvokedUrlCommand *)command;
37
+ - (void)makeReadOnly:(CDVInvokedUrlCommand *)command;
38
+
39
+ // ISO 7816 APDU / MIFARE raw command exchange (v1.7.0, extended v1.9.0 for MIFARE - PLU-258) -
40
+ // mirrors Android nfc.transceive
36
41
  - (void)transceive:(CDVInvokedUrlCommand *)command;
37
42
 
43
+ // Feature detection (added v1.9.0, PLU-258)
44
+ - (void)capabilities:(CDVInvokedUrlCommand *)command;
45
+
38
46
  // Internal implementation
39
47
  - (void)channel:(CDVInvokedUrlCommand *)command;
40
48
 
@@ -7,6 +7,15 @@
7
7
  #import "NfcPlugin.h"
8
8
  #import <CoreNFC/CoreNFC.h>
9
9
 
10
+ // What the current/next NDEF session is for. Replaces overloading self.messageToWrite (e.g. using
11
+ // its presence/content as a signal) to tell a read apart from a write/erase/lock - PLU-257.
12
+ typedef NS_ENUM(NSInteger, NfcSessionMode) {
13
+ NfcSessionModeRead = 0,
14
+ NfcSessionModeWrite,
15
+ NfcSessionModeErase,
16
+ NfcSessionModeLock
17
+ };
18
+
10
19
  @interface NfcPlugin() {
11
20
  NSString* sessionCallbackId;
12
21
  NSString* channelCallbackId;
@@ -15,8 +24,11 @@
15
24
  // Held when an ISO 7816 tag is connected and keepSessionOpen=YES so that
16
25
  // subsequent JS calls to nfc.transceive() can exchange APDUs. (v1.7.0)
17
26
  id<NFCISO7816Tag> connectedISO7816Tag API_AVAILABLE(ios(13.0));
27
+ // Held when a MIFARE/NTAG tag is connected and keepSessionOpen=YES so that nfc.transceive()
28
+ // can send raw MIFARE commands (NTAG password helpers, GET_VERSION, ...) - PLU-258.
29
+ id<NFCMiFareTag> connectedMiFareTag API_AVAILABLE(ios(13.0));
18
30
  }
19
- @property (nonatomic, assign) BOOL writeMode;
31
+ @property (nonatomic, assign) NfcSessionMode sessionMode;
20
32
  @property (nonatomic, assign) BOOL shouldUseTagReaderSession;
21
33
  @property (nonatomic, assign) BOOL sendCallbackOnSessionStart;
22
34
  @property (nonatomic, assign) BOOL returnTagInCallback;
@@ -106,8 +118,8 @@
106
118
 
107
119
  - (void)writeTag:(CDVInvokedUrlCommand*)command API_AVAILABLE(ios(13.0)){
108
120
  NSLog(@"writeTag");
109
-
110
- self.writeMode = YES;
121
+
122
+ self.sessionMode = NfcSessionModeWrite;
111
123
  self.shouldUseTagReaderSession = NO;
112
124
  BOOL reusingSession = NO;
113
125
 
@@ -168,6 +180,88 @@
168
180
  }
169
181
  }
170
182
 
183
+ // nfc.erase() on iOS (PLU-257): writes a single TNF_EMPTY record, same as Android's eraseTag -
184
+ // see com.chariotsolutions.nfc.plugin.NfcPlugin#eraseTag. Mirrors writeTag:'s session reuse.
185
+ - (void)eraseTag:(CDVInvokedUrlCommand*)command {
186
+ NSLog(@"eraseTag");
187
+
188
+ if (@available(iOS 13.0, *)) {
189
+ // continues below
190
+ } else {
191
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"NOT_SUPPORTED" message:@"erase requires iOS 13 or later."]];
192
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
193
+ return;
194
+ }
195
+
196
+ self.sessionMode = NfcSessionModeErase;
197
+ self.shouldUseTagReaderSession = NO;
198
+ BOOL reusingSession = NO;
199
+
200
+ NFCNDEFPayload *emptyRecord = [[NFCNDEFPayload alloc] initWithFormat:NFCTypeNameFormatEmpty type:[NSData data] identifier:[NSData data] payload:[NSData data]];
201
+ self.messageToWrite = [[NFCNDEFMessage alloc] initWithNDEFRecords:@[emptyRecord]];
202
+
203
+ NSDictionary *options = [command argumentAtIndex:0];
204
+
205
+ if (self.nfcSession && self.nfcSession.isReady) {
206
+ reusingSession = YES;
207
+ } else {
208
+ self.nfcSession = [[NFCNDEFReaderSession alloc]initWithDelegate:self queue:dispatch_get_main_queue() invalidateAfterFirstRead:FALSE];
209
+ }
210
+
211
+ NSString *alertMessage = [options isKindOfClass:[NSDictionary class]] ? [options objectForKey:@"alertMessage"] : nil;
212
+ self.nfcSession.alertMessage = ([alertMessage isKindOfClass:[NSString class]] && alertMessage.length > 0)
213
+ ? alertMessage
214
+ : [self localizeString:@"NFCHoldNearWritableTag" defaultValue:@"Hold near writable NFC tag to update."];
215
+ sessionCallbackId = [command.callbackId copy];
216
+
217
+ if (reusingSession) {
218
+ self.keepSessionOpen = NO;
219
+ [self eraseNDEFTag:self.nfcSession status:connectedTagStatus tag:connectedTag];
220
+ } else {
221
+ [self.nfcSession beginSession];
222
+ }
223
+ }
224
+
225
+ // nfc.makeReadOnly() on iOS (PLU-257): permanently locks the tag via NFCNDEFTag.writeLock
226
+ // (iOS 13+). Already-read-only counts as success, same as Android returning without error when
227
+ // the tag is already locked.
228
+ - (void)makeReadOnly:(CDVInvokedUrlCommand*)command {
229
+ NSLog(@"makeReadOnly");
230
+
231
+ if (@available(iOS 13.0, *)) {
232
+ // continues below
233
+ } else {
234
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"NOT_SUPPORTED" message:@"makeReadOnly requires iOS 13 or later."]];
235
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
236
+ return;
237
+ }
238
+
239
+ self.sessionMode = NfcSessionModeLock;
240
+ self.shouldUseTagReaderSession = NO;
241
+ BOOL reusingSession = NO;
242
+
243
+ NSDictionary *options = [command argumentAtIndex:0];
244
+
245
+ if (self.nfcSession && self.nfcSession.isReady) {
246
+ reusingSession = YES;
247
+ } else {
248
+ self.nfcSession = [[NFCNDEFReaderSession alloc]initWithDelegate:self queue:dispatch_get_main_queue() invalidateAfterFirstRead:FALSE];
249
+ }
250
+
251
+ NSString *alertMessage = [options isKindOfClass:[NSDictionary class]] ? [options objectForKey:@"alertMessage"] : nil;
252
+ self.nfcSession.alertMessage = ([alertMessage isKindOfClass:[NSString class]] && alertMessage.length > 0)
253
+ ? alertMessage
254
+ : [self localizeString:@"NFCHoldNearWritableTag" defaultValue:@"Hold near writable NFC tag to update."];
255
+ sessionCallbackId = [command.callbackId copy];
256
+
257
+ if (reusingSession) {
258
+ self.keepSessionOpen = NO;
259
+ [self lockNDEFTag:self.nfcSession status:connectedTagStatus tag:connectedTag];
260
+ } else {
261
+ [self.nfcSession beginSession];
262
+ }
263
+ }
264
+
171
265
  - (void)cancelScan:(CDVInvokedUrlCommand*)command API_AVAILABLE(ios(11.0)){
172
266
  NSLog(@"cancelScan");
173
267
  [self rejectPendingScan];
@@ -178,11 +272,35 @@
178
272
  connectedTagStatus = NFCNDEFStatusNotSupported;
179
273
  if (@available(iOS 13.0, *)) {
180
274
  connectedISO7816Tag = NULL;
275
+ connectedMiFareTag = NULL;
181
276
  }
182
277
  CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
183
278
  [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
184
279
  }
185
280
 
281
+ // nfc.capabilities() (PLU-258): lets an app feature-detect before calling erase/makeReadOnly/the
282
+ // NTAG password helpers instead of trying and parsing the error.
283
+ - (void)capabilities:(CDVInvokedUrlCommand *)command {
284
+ NSLog(@"capabilities");
285
+ BOOL ios13OrLater = NO;
286
+ if (@available(iOS 13.0, *)) {
287
+ ios13OrLater = YES;
288
+ }
289
+ NSDictionary *result = @{
290
+ @"erase": @(ios13OrLater),
291
+ @"makeReadOnly": @(ios13OrLater),
292
+ // PLU-255: scanTag's NFCTagReaderSession never delivered didDetectTags on the owner's
293
+ // iPhone 17 Pro / iOS 26, so raw MIFARE transceive (rawTransceive, the NTAG password
294
+ // helpers) is unproven on iOS. Keep this false until a real device confirms the tag-reader
295
+ // path (or the NDEF-session experiment in -transceive:) actually delivers a MIFARE tag,
296
+ // then flip it to ios13OrLater here.
297
+ @"ntagPassword": @NO,
298
+ @"platform": @"ios"
299
+ };
300
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
301
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
302
+ }
303
+
186
304
  - (void)invalidateSession:(CDVInvokedUrlCommand*)command {
187
305
  NSLog(@"invalidateSession");
188
306
  NSLog(@"WARNING: invalidateSession is deprecated. Use cancelScan.");
@@ -226,6 +344,11 @@
226
344
  // Requires an ISO 7816-compatible tag detected via scanTag({keepSessionOpen:true}).
227
345
  // Resolves with an ArrayBuffer containing the APDU response data with sw1/sw2
228
346
  // appended (Android IsoDep semantics).
347
+ //
348
+ // PLU-258: also supports a MIFARE/NTAG tag via NFCMiFareTag.sendMiFareCommand, which is what the
349
+ // www rawTransceive/rawAuthenticate/rawSetPassword NTAG password helpers need. Unlike the ISO 7816
350
+ // branch, the raw response has no sw1/sw2 appended - it is exactly what the tag sent back, matching
351
+ // Android's raw NTAG command responses.
229
352
  - (void)transceive:(CDVInvokedUrlCommand *)command {
230
353
  NSLog(@"transceive");
231
354
 
@@ -236,12 +359,6 @@
236
359
  return;
237
360
  }
238
361
 
239
- if (connectedISO7816Tag == nil) {
240
- CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"NOT_CONNECTED" message:@"No ISO 7816 tag connected. Only ISO 7816-compatible tags support transceive on iOS."]];
241
- [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
242
- return;
243
- }
244
-
245
362
  // Cordova-iOS converts a JS ArrayBuffer argument into an NSData.
246
363
  id arg = [command argumentAtIndex:0];
247
364
  NSData *apduData = nil;
@@ -252,37 +369,75 @@
252
369
  apduData = [self uint8ArrayToNSData:(NSArray *)arg];
253
370
  }
254
371
 
255
- if (apduData == nil || apduData.length < 4) {
256
- CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"INVALID_ARGUMENT" message:@"Invalid APDU: expected at least 4 bytes (CLA INS P1 P2)."]];
257
- [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
258
- return;
259
- }
372
+ NSString *callbackId = [command.callbackId copy];
260
373
 
261
- NFCISO7816APDU *apdu = [[NFCISO7816APDU alloc] initWithData:apduData];
262
- if (apdu == nil) {
263
- CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"INVALID_ARGUMENT" message:@"Invalid APDU encoding."]];
264
- [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
374
+ if (connectedISO7816Tag != nil) {
375
+ if (apduData == nil || apduData.length < 4) {
376
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"INVALID_ARGUMENT" message:@"Invalid APDU: expected at least 4 bytes (CLA INS P1 P2)."]];
377
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
378
+ return;
379
+ }
380
+
381
+ NFCISO7816APDU *apdu = [[NFCISO7816APDU alloc] initWithData:apduData];
382
+ if (apdu == nil) {
383
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"INVALID_ARGUMENT" message:@"Invalid APDU encoding."]];
384
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
385
+ return;
386
+ }
387
+
388
+ [connectedISO7816Tag sendCommandAPDU:apdu completionHandler:^(NSData * _Nonnull responseData, uint8_t sw1, uint8_t sw2, NSError * _Nullable error) {
389
+ if (error) {
390
+ NSLog(@"transceive error: %@", error);
391
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorFromNSError:error]];
392
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
393
+ return;
394
+ }
395
+
396
+ // Match Android IsoDep.transceive: response data with sw1/sw2 appended.
397
+ NSMutableData *full = [NSMutableData dataWithData:responseData ?: [NSData data]];
398
+ [full appendBytes:&sw1 length:1];
399
+ [full appendBytes:&sw2 length:1];
400
+
401
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:full];
402
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
403
+ }];
265
404
  return;
266
405
  }
267
406
 
268
- NSString *callbackId = [command.callbackId copy];
407
+ id<NFCMiFareTag> mifareTag = connectedMiFareTag;
408
+ if (mifareTag == nil && self->connectedTag != nil && [self->connectedTag conformsToProtocol:@protocol(NFCMiFareTag)]) {
409
+ // Experiment (PLU-258): CoreNFC doesn't document the NDEF-session tag (from
410
+ // scanNdef/scanTag without the ISO7816 branch below) as also conforming to
411
+ // NFCMiFareTag, but a concrete tag can implement more than one CoreNFC protocol at
412
+ // once, so this runtime check is free to try and may let rawTransceive work on a tag
413
+ // read via scanNdef({keepSessionOpen:true}) even when the dedicated tag-reader-session
414
+ // path (PLU-255) doesn't fire. Unverified on a real device.
415
+ mifareTag = (id<NFCMiFareTag>)self->connectedTag;
416
+ }
269
417
 
270
- [connectedISO7816Tag sendCommandAPDU:apdu completionHandler:^(NSData * _Nonnull responseData, uint8_t sw1, uint8_t sw2, NSError * _Nullable error) {
271
- if (error) {
272
- NSLog(@"transceive error: %@", error);
273
- CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorFromNSError:error]];
274
- [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
418
+ if (mifareTag != nil) {
419
+ if (apduData == nil || apduData.length == 0) {
420
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"INVALID_ARGUMENT" message:@"Invalid MIFARE command: expected at least 1 byte."]];
421
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
275
422
  return;
276
423
  }
277
424
 
278
- // Match Android IsoDep.transceive: response data with sw1/sw2 appended.
279
- NSMutableData *full = [NSMutableData dataWithData:responseData ?: [NSData data]];
280
- [full appendBytes:&sw1 length:1];
281
- [full appendBytes:&sw2 length:1];
425
+ [mifareTag sendMiFareCommand:apduData completionHandler:^(NSData * _Nonnull responseData, NSError * _Nullable error) {
426
+ if (error) {
427
+ NSLog(@"transceive (MIFARE) error: %@", error);
428
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorFromNSError:error]];
429
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
430
+ return;
431
+ }
282
432
 
283
- CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:full];
284
- [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
285
- }];
433
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArrayBuffer:responseData ?: [NSData data]];
434
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
435
+ }];
436
+ return;
437
+ }
438
+
439
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"NOT_CONNECTED" message:@"No ISO 7816 or MIFARE tag connected. Only ISO 7816-compatible and MIFARE/NTAG tags support transceive on iOS."]];
440
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
286
441
  } else {
287
442
  CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self errorWithCode:@"NOT_SUPPORTED" message:@"transceive requires iOS 13 or later."]];
288
443
  [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
@@ -383,6 +538,15 @@
383
538
  return;
384
539
  }
385
540
 
541
+ // MIFARE/NTAG + keepSessionOpen (PLU-258): hold the tag for nfc.transceive() raw MIFARE
542
+ // commands (the NTAG password helpers), same shape as the ISO 7816 branch above.
543
+ if (tag.type == NFCTagTypeMiFare && self.keepSessionOpen) {
544
+ self->connectedMiFareTag = [tag asNFCMiFareTag];
545
+ session.alertMessage = [self localizeString:@"NFCTagRead" defaultValue:@"Tag successfully read."];
546
+ [self fireTagEvent:tagMetaData];
547
+ return;
548
+ }
549
+
386
550
  [self processNDEFTag:session tag:ndefTag metaData:tagMetaData];
387
551
  }];
388
552
  }
@@ -420,6 +584,7 @@
420
584
  connectedTagStatus = NFCNDEFStatusNotSupported;
421
585
  if (@available(iOS 13.0, *)) {
422
586
  connectedISO7816Tag = NULL;
587
+ connectedMiFareTag = NULL;
423
588
  }
424
589
  if (old.isReady) {
425
590
  [old invalidateSession];
@@ -527,8 +692,8 @@
527
692
 
528
693
  - (void)beginScanSession:(CDVInvokedUrlCommand*)command {
529
694
 
530
- self.writeMode = NO;
531
-
695
+ self.sessionMode = NfcSessionModeRead;
696
+
532
697
  NSLog(@"shouldUseTagReaderSession %d", self.shouldUseTagReaderSession);
533
698
  NSLog(@"callbackOnSessionStart %d", self.sendCallbackOnSessionStart);
534
699
  NSLog(@"returnTagInCallback %d", self.returnTagInCallback);
@@ -584,15 +749,25 @@
584
749
  return;
585
750
  }
586
751
 
587
- if (self.writeMode) {
588
- [self writeNDEFTag:session status:status tag:tag];
589
- } else {
590
- // save tag & status so we can re-use in write
591
- if (self.keepSessionOpen) {
592
- self->connectedTagStatus = status;
593
- self->connectedTag = tag;
594
- }
595
- [self readNDEFTag:session status:status tag:tag metaData:metaData];
752
+ switch (self.sessionMode) {
753
+ case NfcSessionModeWrite:
754
+ [self writeNDEFTag:session status:status tag:tag];
755
+ break;
756
+ case NfcSessionModeErase:
757
+ [self eraseNDEFTag:session status:status tag:tag];
758
+ break;
759
+ case NfcSessionModeLock:
760
+ [self lockNDEFTag:session status:status tag:tag];
761
+ break;
762
+ case NfcSessionModeRead:
763
+ default:
764
+ // save tag & status so we can re-use in write
765
+ if (self.keepSessionOpen) {
766
+ self->connectedTagStatus = status;
767
+ self->connectedTag = tag;
768
+ }
769
+ [self readNDEFTag:session status:status tag:tag metaData:metaData];
770
+ break;
596
771
  }
597
772
 
598
773
  }];
@@ -660,6 +835,79 @@
660
835
  }
661
836
  }
662
837
 
838
+ // nfc.erase() (PLU-257): same write path as writeNDEFTag, but self.messageToWrite is the single
839
+ // TNF_EMPTY record built in -eraseTag:, and NOT_SUPPORTED (not NOT_NDEF) covers a tag that
840
+ // doesn't support NDEF at all - matching Android's eraseTag error code.
841
+ - (void)eraseNDEFTag:(NFCReaderSession * _Nonnull)session status:(NFCNDEFStatus)status tag:(id<NFCNDEFTag>)tag API_AVAILABLE(ios(13.0)) {
842
+ switch (status) {
843
+ case NFCNDEFStatusNotSupported:
844
+ [self closeSession:session withErrorCode:@"NOT_SUPPORTED" message:[self localizeString:@"NFCNotNdefCompliant" defaultValue:@"Tag is not NDEF compliant."]];
845
+ break;
846
+ case NFCNDEFStatusReadOnly:
847
+ [self closeSession:session withErrorCode:@"READ_ONLY" message:[self localizeString:@"NFCReadOnlyTag" defaultValue:@"Tag is read only."]];
848
+ break;
849
+ case NFCNDEFStatusReadWrite: {
850
+
851
+ [tag writeNDEF: self.messageToWrite completionHandler:^(NSError * _Nullable error) {
852
+ if (error) {
853
+ NSLog(@"%@", error);
854
+ [self closeSession:session withErrorCode:@"IO_ERROR" message:[self localizeString:@"NFCDataWriteFailed" defaultValue:@"Write failed."]];
855
+ } else {
856
+ session.alertMessage = [self localizeString:@"NFCDataWrote" defaultValue:@"Wrote data to NFC tag."];
857
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
858
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:self->sessionCallbackId];
859
+ [self closeSession:session];
860
+ }
861
+ }];
862
+ break;
863
+
864
+ }
865
+ default:
866
+ [self closeSession:session withErrorCode:@"UNKNOWN" message:[self localizeString:@"NFCUnknownNdefTag" defaultValue:@"Unknown NDEF tag status."]];
867
+ }
868
+ }
869
+
870
+ // nfc.makeReadOnly() (PLU-257): NFCNDEFTag.writeLockWithCompletionHandler (iOS 13+) permanently
871
+ // locks the tag. A tag that is already read-only is reported as success with alreadyLocked:true,
872
+ // same as Android returning without error when the tag can't be locked again.
873
+ - (void)lockNDEFTag:(NFCReaderSession * _Nonnull)session status:(NFCNDEFStatus)status tag:(id<NFCNDEFTag>)tag API_AVAILABLE(ios(13.0)) {
874
+ switch (status) {
875
+ case NFCNDEFStatusNotSupported:
876
+ [self closeSession:session withErrorCode:@"NOT_SUPPORTED" message:[self localizeString:@"NFCNotNdefCompliant" defaultValue:@"Tag is not NDEF compliant."]];
877
+ break;
878
+ case NFCNDEFStatusReadOnly: {
879
+ session.alertMessage = [self localizeString:@"NFCTagAlreadyReadOnly" defaultValue:@"Tag is already read only."];
880
+ NSDictionary *result = @{ @"alreadyLocked": @YES };
881
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
882
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:self->sessionCallbackId];
883
+ [self closeSession:session];
884
+ break;
885
+ }
886
+ case NFCNDEFStatusReadWrite: {
887
+ if (![tag respondsToSelector:@selector(writeLockWithCompletionHandler:)]) {
888
+ [self closeSession:session withErrorCode:@"NOT_SUPPORTED" message:[self localizeString:@"NFCLockNotSupported" defaultValue:@"This tag does not support being locked."]];
889
+ break;
890
+ }
891
+
892
+ [tag writeLockWithCompletionHandler:^(NSError * _Nullable error) {
893
+ if (error) {
894
+ NSLog(@"%@", error);
895
+ [self closeSession:session withErrorCode:@"IO_ERROR" message:[self localizeString:@"NFCLockFailed" defaultValue:@"Failed to lock tag."]];
896
+ } else {
897
+ session.alertMessage = [self localizeString:@"NFCTagLocked" defaultValue:@"Tag is now read only."];
898
+ NSDictionary *result = @{ @"alreadyLocked": @NO };
899
+ CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
900
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:self->sessionCallbackId];
901
+ [self closeSession:session];
902
+ }
903
+ }];
904
+ break;
905
+ }
906
+ default:
907
+ [self closeSession:session withErrorCode:@"UNKNOWN" message:[self localizeString:@"NFCUnknownNdefTag" defaultValue:@"Unknown NDEF tag status."]];
908
+ }
909
+ }
910
+
663
911
  #pragma mark - Tag Reader Helper Functions
664
912
 
665
913
  // Gets the tag meta data - type and uid
@@ -771,6 +1019,7 @@
771
1019
  connectedTagStatus = NFCNDEFStatusNotSupported;
772
1020
  if (@available(iOS 13.0, *)) {
773
1021
  connectedISO7816Tag = NULL;
1022
+ connectedMiFareTag = NULL;
774
1023
  }
775
1024
  [self markSessionClosing:session];
776
1025
  [session invalidateSession];
@@ -796,6 +1045,7 @@
796
1045
  connectedTagStatus = NFCNDEFStatusNotSupported;
797
1046
  if (@available(iOS 13.0, *)) {
798
1047
  connectedISO7816Tag = NULL;
1048
+ connectedMiFareTag = NULL;
799
1049
  }
800
1050
 
801
1051
  [self markSessionClosing:session];
package/types/index.d.ts CHANGED
@@ -275,6 +275,28 @@ export interface IWriteOptions {
275
275
  [key: string]: unknown;
276
276
  }
277
277
 
278
+ /** Options for erase() / makeReadOnly() on iOS (added v1.9.0, PLU-257) */
279
+ export interface IEraseOptions {
280
+ [key: string]: unknown;
281
+ }
282
+
283
+ /** Result of nfc.capabilities() (added v1.9.0, PLU-258) */
284
+ export interface INfcCapabilities {
285
+ /** nfc.erase() is implemented on this platform */
286
+ erase: boolean;
287
+ /** nfc.makeReadOnly() is implemented on this platform */
288
+ makeReadOnly: boolean;
289
+ /**
290
+ * NTAG password set/remove is implemented on this platform. Always true on Android
291
+ * (ntagSetPassword/ntagRemovePassword). False on iOS by default (PLU-255): the
292
+ * NFCMiFareTag raw-command path transceive() uses for the password commands is
293
+ * unverified against real NTAG213/215 hardware.
294
+ */
295
+ ntagPassword: boolean;
296
+ /** cordova.platformId */
297
+ platform: string;
298
+ }
299
+
278
300
  /** Callback-style success/failure pair */
279
301
  export type SuccessCallback<T = unknown> = (result?: T) => void;
280
302
  export type FailureCallback = (error: NfcFailure) => void;
@@ -325,13 +347,19 @@ export interface INfcPlugin {
325
347
  write(ndefMessage: INdefRecord[]): Promise<void>;
326
348
  write(ndefMessage: INdefRecord[], win?: SuccessCallback, fail?: FailureCallback, options?: IWriteOptions): void;
327
349
 
328
- /** Make the last scanned tag read-only (permanent!) - Android */
350
+ /**
351
+ * Make the last scanned tag read-only (permanent!) - Android, and iOS 13+ after
352
+ * scanNdef/scanTag({keepSessionOpen: true}) (v1.9.0, PLU-257; options only used on iOS).
353
+ */
329
354
  makeReadOnly(): Promise<void>;
330
- makeReadOnly(win?: SuccessCallback, fail?: FailureCallback): void;
355
+ makeReadOnly(win?: SuccessCallback, fail?: FailureCallback, options?: IEraseOptions): void;
331
356
 
332
- /** Erase the last scanned tag (writes an empty NDEF record) - Android */
357
+ /**
358
+ * Erase the last scanned tag (writes an empty NDEF record) - Android, and iOS 13+ after
359
+ * scanNdef/scanTag({keepSessionOpen: true}) (v1.9.0, PLU-257; options only used on iOS).
360
+ */
333
361
  erase(): Promise<void>;
334
- erase(win?: SuccessCallback, fail?: FailureCallback): void;
362
+ erase(win?: SuccessCallback, fail?: FailureCallback, options?: IEraseOptions): void;
335
363
 
336
364
  /** Android Beam was removed in Android 10: always fails with NOT_SUPPORTED */
337
365
  share(ndefMessage: INdefRecord[]): Promise<void>;
@@ -386,9 +414,26 @@ export interface INfcPlugin {
386
414
  /** Close the connect() session */
387
415
  close(): Promise<void>;
388
416
 
389
- /** Send a raw command. Android: any connected tech; iOS: ISO 7816 APDUs after scanTag({keepSessionOpen:true}) */
417
+ /**
418
+ * Send a raw command. Android: any connected tech. iOS: after scanTag({keepSessionOpen: true}),
419
+ * ISO 7816 APDUs against an ISO7816 tag, or (v1.9.0, PLU-258) MIFARE commands against a tag
420
+ * that reports as MIFARE. As an experiment covering tags the reader session classifies some
421
+ * other way, it also tries the MIFARE path, unverified on real hardware, whenever the connected
422
+ * tag happens to conform to NFCMiFareTag. Unlike the ISO 7816 path, the MIFARE response does
423
+ * NOT have SW1/SW2 appended.
424
+ */
390
425
  transceive(command: ArrayBuffer | ArrayBufferView | number[] | string): Promise<ArrayBuffer>;
391
426
 
427
+ // ========== Feature detection ==========
428
+
429
+ /**
430
+ * Report which optional features this platform actually implements (added v1.9.0, PLU-258).
431
+ * Use this instead of `cordova.platformId` checks to decide whether to offer erase,
432
+ * makeReadOnly or NTAG password UI.
433
+ */
434
+ capabilities(): Promise<INfcCapabilities>;
435
+ capabilities(win?: SuccessCallback<INfcCapabilities>, fail?: FailureCallback): void;
436
+
392
437
  // ========== iOS error codes ==========
393
438
 
394
439
  /** NFCReaderError codes carried by iOS reader-session errors (with nfc.useErrorObjects(true)) */
@@ -170,6 +170,20 @@ function execOrPromise(win, fail, action, args) {
170
170
  nfcExec(win, fail, action, args);
171
171
  }
172
172
 
173
+ // Same callback-or-Promise duality as execOrPromise(), but for an API that answers from a plain
174
+ // JS value instead of a native exec call (capabilities() on Android / unsupported platforms).
175
+ function resolveOrCall(win, fail, value) {
176
+ if (typeof win === 'function') {
177
+ win(value);
178
+ return undefined;
179
+ }
180
+ // [PLU-257 round-1] No success callback was given (win is not a function) - whether or not a
181
+ // `fail` was passed, there is no failure channel for a synthesized answer like this one, so a
182
+ // fail-only call (win undefined, fail a function) must still behave like execOrPromise and
183
+ // hand back a Promise rather than silently dropping `value`.
184
+ return Promise.resolve(value);
185
+ }
186
+
173
187
  var ndef = {
174
188
 
175
189
  // see android.nfc.NdefRecord for documentation about constants
@@ -595,7 +609,10 @@ var nfc = {
595
609
  return execOrPromise(win, fail, "writeTag", [ndefMessage]);
596
610
  },
597
611
 
598
- makeReadOnly: function (win, fail) {
612
+ makeReadOnly: function (win, fail, options) {
613
+ if (cordova.platformId === "ios") {
614
+ return execOrPromise(win, fail, "makeReadOnly", [options]);
615
+ }
599
616
  return execOrPromise(win, fail, "makeReadOnly", []);
600
617
  },
601
618
 
@@ -619,10 +636,38 @@ var nfc = {
619
636
  return execOrPromise(win, fail, "stopHandover", []);
620
637
  },
621
638
 
622
- erase: function (win, fail) {
639
+ erase: function (win, fail, options) {
640
+ if (cordova.platformId === "ios") {
641
+ return execOrPromise(win, fail, "eraseTag", [options]);
642
+ }
623
643
  return execOrPromise(win, fail, "eraseTag", [[]]);
624
644
  },
625
645
 
646
+ // Feature detection (added v1.9.0, PLU-258). Android implements erase, makeReadOnly and the
647
+ // NTAG password helpers (ntagAuthenticate/ntagSetPassword/ntagRemovePassword) natively, so
648
+ // those report true unconditionally there. iOS reports its own support (erase/makeReadOnly
649
+ // need iOS 13+); ntagPassword is hardcoded false on iOS - the NFCMiFareTag raw-command path
650
+ // used for NTAG password commands (PLU-255) is unverified against real NTAG213/215 hardware.
651
+ capabilities: function (win, fail) {
652
+ if (cordova.platformId === "android") {
653
+ return resolveOrCall(win, fail, {
654
+ erase: true,
655
+ makeReadOnly: true,
656
+ ntagPassword: true,
657
+ platform: "android"
658
+ });
659
+ }
660
+ if (cordova.platformId === "ios") {
661
+ return execOrPromise(win, fail, "capabilities", []);
662
+ }
663
+ return resolveOrCall(win, fail, {
664
+ erase: false,
665
+ makeReadOnly: false,
666
+ ntagPassword: false,
667
+ platform: cordova.platformId
668
+ });
669
+ },
670
+
626
671
  enabled: function (win, fail) {
627
672
  return execOrPromise(win, fail, "enabled", [[]]);
628
673
  },