@exxili/capacitor-nfc 0.0.12 → 0.0.13

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.
@@ -1,41 +1,305 @@
1
1
  import Foundation
2
2
  import CoreNFC
3
+ // swiftlint:disable:next type_body_length
4
+ @objc public class NFCReader: NSObject, NFCTagReaderSessionDelegate, NFCNDEFReaderSessionDelegate {
5
+ public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
6
+ // Intentionally left blank; no special handling needed when tag session becomes active.
7
+ }
8
+ private enum ReaderMode {
9
+ case fullTag
10
+ case limitedTag
11
+ case ndefFallback
12
+
13
+ var alertSuffix: String {
14
+ switch self {
15
+ case .fullTag:
16
+ return "."
17
+ case .limitedTag:
18
+ return " (compatibility mode)."
19
+ case .ndefFallback:
20
+ return " (NDEF mode)."
21
+ }
22
+ }
23
+
24
+ var metadataValue: String {
25
+ switch self {
26
+ case .fullTag:
27
+ return "full"
28
+ case .limitedTag:
29
+ return "compat"
30
+ case .ndefFallback:
31
+ return "ndef"
32
+ }
33
+ }
34
+ }
3
35
 
4
- @objc public class NFCReader: NSObject, NFCTagReaderSessionDelegate {
5
36
  private var readerSession: NFCTagReaderSession?
37
+ private var ndefReaderSession: NFCNDEFReaderSession?
38
+ private var readerMode: ReaderMode = .fullTag
39
+ private var autoRestartWorkItem: DispatchWorkItem?
40
+ private var entitlementFailureDetected = false
41
+ private var lastFallbackSignature: (mode: ReaderMode, reason: String?)?
6
42
 
7
43
  public var onNDEFMessageReceived: (([NFCNDEFMessage], [String: Any]?) -> Void)?
8
44
  public var onError: ((Error) -> Void)?
9
45
 
46
+ @objc public func setPreferredReaderMode(_ rawMode: String?) {
47
+ guard let rawMode = rawMode?.lowercased() else { return }
48
+ switch rawMode {
49
+ case "full", "advanced", "forcefull":
50
+ readerMode = .fullTag
51
+ entitlementFailureDetected = false
52
+ lastFallbackSignature = nil
53
+ case "compat", "compatibility", "limited":
54
+ readerMode = .limitedTag
55
+ lastFallbackSignature = nil
56
+ case "ndef":
57
+ readerMode = .ndefFallback
58
+ lastFallbackSignature = nil
59
+ case "auto":
60
+ if entitlementFailureDetected {
61
+ if readerMode == .fullTag {
62
+ readerMode = .limitedTag
63
+ }
64
+ } else {
65
+ readerMode = .fullTag
66
+ lastFallbackSignature = nil
67
+ }
68
+ default:
69
+ break
70
+ }
71
+ if readerMode == .fullTag {
72
+ lastFallbackSignature = nil
73
+ }
74
+ }
75
+
10
76
  @objc public func startScanning() {
11
77
  print("NFCReader startScanning called")
12
-
13
78
  guard NFCNDEFReaderSession.readingAvailable else {
14
79
  print("NFC scanning not supported on this device")
15
80
  return
16
81
  }
17
- readerSession = NFCTagReaderSession(pollingOption: [.iso14443, .iso15693, .iso18092], delegate: self, queue: nil)
18
- readerSession?.alertMessage = "Hold your iPhone near the NFC tag."
19
- readerSession?.begin()
82
+ if readerMode != .fullTag {
83
+ let reason = entitlementFailureDetected ? "missing-entitlement" : nil
84
+ emitFallbackMetadata(for: readerMode, reason: reason)
85
+ }
86
+
87
+ autoRestartWorkItem?.cancel()
88
+ startCurrentModeSession()
20
89
  }
21
90
 
22
91
  @objc public func cancelScanning() {
23
- if let session = readerSession {
24
- session.invalidate()
25
- }
92
+ autoRestartWorkItem?.cancel()
93
+ autoRestartWorkItem = nil
94
+ if let session = readerSession { session.invalidate() }
95
+ if let ndef = ndefReaderSession { ndef.invalidate() }
26
96
  readerSession = nil
27
- }
28
-
29
- // NFCTagReaderSessionDelegate methods
30
- public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
31
-
97
+ ndefReaderSession = nil
98
+ lastFallbackSignature = nil
32
99
  }
33
100
 
34
101
  public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
35
102
  print("NFC reader session error: \(error.localizedDescription)")
103
+ lastFallbackSignature = nil
104
+ readerSession = nil
105
+ guard let nfcError = error as? NFCReaderError else {
106
+ onError?(error)
107
+ return
108
+ }
109
+
110
+ if handleEntitlementOrSystemErrors(nfcError) {
111
+ return
112
+ }
113
+
114
+ if nfcError.code == .readerSessionInvalidationErrorUserCanceled {
115
+ return
116
+ }
117
+
36
118
  onError?(error)
37
119
  }
38
120
 
121
+ private func startCurrentModeSession() {
122
+ switch readerMode {
123
+ case .fullTag:
124
+ beginTagSession(pollingOption: [.iso14443, .iso15693, .iso18092])
125
+ case .limitedTag:
126
+ beginTagSession(pollingOption: [.iso14443])
127
+ case .ndefFallback:
128
+ beginNDEFFallbackSession()
129
+ }
130
+ }
131
+
132
+ private func beginTagSession(pollingOption: NFCTagReaderSession.PollingOption) {
133
+ guard NFCTagReaderSession.readingAvailable else {
134
+ print("[NFC] Tag reading not supported on this device")
135
+ return
136
+ }
137
+ DispatchQueue.main.async {
138
+ if let existingSession = self.readerSession {
139
+ existingSession.invalidate()
140
+ }
141
+ self.ndefReaderSession?.invalidate()
142
+ self.ndefReaderSession = nil
143
+
144
+ guard let session = NFCTagReaderSession(pollingOption: pollingOption, delegate: self, queue: nil) else {
145
+ print("[NFC] Failed to create NFCTagReaderSession (nil).")
146
+ return
147
+ }
148
+ session.alertMessage = "Hold your iPhone near the NFC tag" + self.readerMode.alertSuffix
149
+ self.readerSession = session
150
+ session.begin()
151
+ }
152
+ }
153
+
154
+ private func beginNDEFFallbackSession() {
155
+ guard NFCNDEFReaderSession.readingAvailable else { return }
156
+ DispatchQueue.main.async {
157
+ if self.ndefReaderSession != nil { return }
158
+ let session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
159
+ session.alertMessage = "Hold your iPhone near the NFC tag. (NDEF mode)"
160
+ self.ndefReaderSession = session
161
+ session.begin()
162
+ }
163
+ }
164
+
165
+ private func scheduleRestart(after delay: TimeInterval = 0.25) {
166
+ autoRestartWorkItem?.cancel()
167
+ let workItem = DispatchWorkItem { [weak self] in
168
+ guard let self = self else { return }
169
+ self.autoRestartWorkItem = nil
170
+ self.startCurrentModeSession()
171
+ }
172
+ autoRestartWorkItem = workItem
173
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
174
+ }
175
+
176
+ private func emitFallbackMetadata(for mode: ReaderMode, reason: String? = nil) {
177
+ guard mode != .fullTag else { return }
178
+ if let last = lastFallbackSignature, last.mode == mode, last.reason == reason {
179
+ return
180
+ }
181
+ lastFallbackSignature = (mode, reason)
182
+
183
+ var metadata: [String: Any] = [
184
+ "fallback": true,
185
+ "fallbackMode": mode.metadataValue
186
+ ]
187
+ if let reason = reason {
188
+ metadata["reason"] = reason
189
+ }
190
+ DispatchQueue.main.async {
191
+ self.onNDEFMessageReceived?([], metadata)
192
+ }
193
+ }
194
+
195
+ private func handleEntitlementOrSystemErrors(_ error: NFCReaderError) -> Bool {
196
+ let description = error.localizedDescription.lowercased()
197
+
198
+ if description.contains("missing required entitlement") || error.code == .readerErrorSecurityViolation {
199
+ if readerMode == .fullTag {
200
+ print("[NFC] Missing advanced tag entitlement detected -> switching to compatibility mode")
201
+ readerMode = .limitedTag
202
+ entitlementFailureDetected = true
203
+ emitFallbackMetadata(for: readerMode, reason: "missing-entitlement")
204
+ scheduleRestart()
205
+ return true
206
+ }
207
+
208
+ if readerMode == .limitedTag {
209
+ print("[NFC] Compatibility mode also unavailable -> falling back to NDEF session")
210
+ readerMode = .ndefFallback
211
+ entitlementFailureDetected = true
212
+ emitFallbackMetadata(for: readerMode, reason: "missing-entitlement")
213
+ scheduleRestart()
214
+ return true
215
+ }
216
+ }
217
+
218
+ if description.contains("system resource unavailable") || error.code == .readerSessionInvalidationErrorSystemIsBusy {
219
+ print("[NFC] System resource unavailable; retrying current reader mode")
220
+ scheduleRestart(after: 0.35)
221
+ return true
222
+ }
223
+
224
+ return false
225
+ }
226
+
227
+ // MARK: - NFCNDEFReaderSessionDelegate
228
+ public func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
229
+ print("NDEF reader session error: \(error.localizedDescription)")
230
+ if session === ndefReaderSession {
231
+ ndefReaderSession = nil
232
+ lastFallbackSignature = nil
233
+ }
234
+ onError?(error)
235
+ }
236
+
237
+ // Called when the NFCNDEFReaderSession becomes active (needed to silence runtime delegate warning)
238
+ public func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
239
+ let isFallback = (session === ndefReaderSession)
240
+ print("NDEF reader session became active (fallback mode: \(isFallback))")
241
+ if isFallback {
242
+ let reason = entitlementFailureDetected ? "missing-entitlement" : nil
243
+ emitFallbackMetadata(for: .ndefFallback, reason: reason)
244
+ }
245
+ }
246
+
247
+ public func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
248
+ // Mirror writer logic for reading first NDEF tag
249
+ if tags.count > 1 {
250
+ let retryInterval = DispatchTimeInterval.milliseconds(500)
251
+ session.alertMessage = "More than one tag detected. Please try again."
252
+ DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) {
253
+ session.restartPolling()
254
+ }
255
+ return
256
+ }
257
+ guard let tag = tags.first else { return }
258
+ session.connect(to: tag) { connectError in
259
+ if let connectError = connectError {
260
+ session.invalidate(errorMessage: "Unable to connect to tag.")
261
+ self.onError?(connectError)
262
+ return
263
+ }
264
+ tag.queryNDEFStatus { status, _, statusError in
265
+ if let statusError = statusError {
266
+ session.invalidate(errorMessage: "Unable to query the NDEF status of tag.")
267
+ self.onError?(statusError)
268
+ return
269
+ }
270
+ guard status != .notSupported else {
271
+ session.invalidate(errorMessage: "Tag is not NDEF compliant.")
272
+ return
273
+ }
274
+ tag.readNDEF { message, readError in
275
+ if let readError = readError {
276
+ session.invalidate(errorMessage: "Failed to read NDEF message.")
277
+ self.onError?(readError)
278
+ return
279
+ }
280
+ if let message = message {
281
+ session.alertMessage = "Found 1 NDEF message."
282
+ session.invalidate()
283
+ self.onNDEFMessageReceived?([message], nil)
284
+ } else {
285
+ session.alertMessage = "No NDEF message found."
286
+ session.invalidate()
287
+ self.onNDEFMessageReceived?([], nil)
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ // Some SDK versions (and build settings) still require this legacy delegate method.
295
+ public func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
296
+ // Forward directly; no tag info available in this callback.
297
+ if !messages.isEmpty {
298
+ session.invalidate()
299
+ onNDEFMessageReceived?(messages, nil)
300
+ }
301
+ }
302
+
39
303
  public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
40
304
  if tags.count > 1 {
41
305
  let retryInterval = DispatchTimeInterval.milliseconds(500)
@@ -45,18 +309,21 @@ import CoreNFC
45
309
  }
46
310
  return
47
311
  }
48
-
49
- let tag = tags.first!
312
+
313
+ guard let tag = tags.first else {
314
+ session.invalidate(errorMessage: "No tag found.")
315
+ return
316
+ }
50
317
  session.connect(to: tag) { (error: Error?) in
51
318
  if let error = error {
52
319
  session.invalidate(errorMessage: "Unable to connect to tag.")
53
320
  self.onError?(error)
54
321
  return
55
322
  }
56
-
323
+
57
324
  // Extract tag information
58
325
  let tagInfo = self.extractTagInfo(from: tag)
59
-
326
+
60
327
  // Try to read NDEF if the tag supports it
61
328
  if case let .iso7816(iso7816Tag) = tag {
62
329
  self.handleISO7816Tag(iso7816Tag, session: session, tagInfo: tagInfo)
@@ -74,44 +341,44 @@ import CoreNFC
74
341
  }
75
342
  }
76
343
  }
77
-
344
+
78
345
  private func extractTagInfo(from tag: NFCTag) -> [String: Any] {
79
346
  var tagInfo: [String: Any] = [:]
80
347
  var techTypes: [String] = []
81
348
  var uid: String = ""
82
-
349
+
83
350
  switch tag {
84
351
  case .iso7816(let iso7816Tag):
85
352
  uid = iso7816Tag.identifier.map { String(format: "%02X", $0) }.joined()
86
353
  techTypes.append("ISO7816")
87
354
  tagInfo["type"] = "ISO7816"
88
-
355
+
89
356
  case .miFare(let miFareTag):
90
357
  uid = miFareTag.identifier.map { String(format: "%02X", $0) }.joined()
91
358
  techTypes.append("MiFare")
92
359
  tagInfo["type"] = "MiFare"
93
-
360
+
94
361
  case .feliCa(let feliCaTag):
95
362
  uid = feliCaTag.currentIDm.map { String(format: "%02X", $0) }.joined()
96
363
  techTypes.append("FeliCa")
97
364
  tagInfo["type"] = "FeliCa"
98
-
365
+
99
366
  case .iso15693(let iso15693Tag):
100
367
  uid = iso15693Tag.identifier.map { String(format: "%02X", $0) }.joined()
101
368
  techTypes.append("ISO15693")
102
369
  tagInfo["type"] = "ISO15693"
103
-
370
+
104
371
  @unknown default:
105
372
  techTypes.append("Unknown")
106
373
  tagInfo["type"] = "Unknown"
107
374
  }
108
-
375
+
109
376
  tagInfo["uid"] = uid
110
377
  tagInfo["techTypes"] = techTypes
111
-
378
+
112
379
  return tagInfo
113
380
  }
114
-
381
+
115
382
  private func handleMiFareTag(_ miFareTag: NFCMiFareTag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
116
383
  miFareTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
117
384
  if let error = error {
@@ -119,25 +386,25 @@ import CoreNFC
119
386
  self.onError?(error)
120
387
  return
121
388
  }
122
-
389
+
123
390
  if ndefStatus == .notSupported {
124
391
  session.alertMessage = "Tag detected but no NDEF message found."
125
392
  session.invalidate()
126
393
  self.onNDEFMessageReceived?([], tagInfo)
127
394
  return
128
395
  }
129
-
396
+
130
397
  var updatedTagInfo = tagInfo
131
398
  updatedTagInfo["maxSize"] = capacity
132
399
  updatedTagInfo["isWritable"] = ndefStatus == .readWrite
133
-
400
+
134
401
  miFareTag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
135
402
  if let error = error {
136
403
  session.invalidate(errorMessage: "Failed to read NDEF from tag.")
137
404
  self.onError?(error)
138
405
  return
139
406
  }
140
-
407
+
141
408
  if let message = message {
142
409
  session.alertMessage = "Found 1 NDEF message."
143
410
  session.invalidate()
@@ -150,14 +417,14 @@ import CoreNFC
150
417
  }
151
418
  }
152
419
  }
153
-
420
+
154
421
  private func handleISO7816Tag(_ iso7816Tag: NFCISO7816Tag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
155
422
  // ISO7816 tags don't typically support NDEF directly
156
423
  session.alertMessage = "Tag detected but no NDEF message found."
157
424
  session.invalidate()
158
425
  self.onNDEFMessageReceived?([], tagInfo)
159
426
  }
160
-
427
+
161
428
  private func handleFeliCaTag(_ feliCaTag: NFCFeliCaTag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
162
429
  feliCaTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
163
430
  if let error = error {
@@ -165,25 +432,25 @@ import CoreNFC
165
432
  self.onError?(error)
166
433
  return
167
434
  }
168
-
435
+
169
436
  if ndefStatus == .notSupported {
170
437
  session.alertMessage = "Tag detected but no NDEF message found."
171
438
  session.invalidate()
172
439
  self.onNDEFMessageReceived?([], tagInfo)
173
440
  return
174
441
  }
175
-
442
+
176
443
  var updatedTagInfo = tagInfo
177
444
  updatedTagInfo["maxSize"] = capacity
178
445
  updatedTagInfo["isWritable"] = ndefStatus == .readWrite
179
-
446
+
180
447
  feliCaTag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
181
448
  if let error = error {
182
449
  session.invalidate(errorMessage: "Failed to read NDEF from tag.")
183
450
  self.onError?(error)
184
451
  return
185
452
  }
186
-
453
+
187
454
  if let message = message {
188
455
  session.alertMessage = "Found 1 NDEF message."
189
456
  session.invalidate()
@@ -196,7 +463,7 @@ import CoreNFC
196
463
  }
197
464
  }
198
465
  }
199
-
466
+
200
467
  private func handleISO15693Tag(_ iso15693Tag: NFCISO15693Tag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
201
468
  iso15693Tag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
202
469
  if let error = error {
@@ -204,25 +471,25 @@ import CoreNFC
204
471
  self.onError?(error)
205
472
  return
206
473
  }
207
-
474
+
208
475
  if ndefStatus == .notSupported {
209
476
  session.alertMessage = "Tag detected but no NDEF message found."
210
477
  session.invalidate()
211
478
  self.onNDEFMessageReceived?([], tagInfo)
212
479
  return
213
480
  }
214
-
481
+
215
482
  var updatedTagInfo = tagInfo
216
483
  updatedTagInfo["maxSize"] = capacity
217
484
  updatedTagInfo["isWritable"] = ndefStatus == .readWrite
218
-
485
+
219
486
  iso15693Tag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
220
487
  if let error = error {
221
488
  session.invalidate(errorMessage: "Failed to read NDEF from tag.")
222
489
  self.onError?(error)
223
490
  return
224
491
  }
225
-
492
+
226
493
  if let message = message {
227
494
  session.alertMessage = "Found 1 NDEF message."
228
495
  session.invalidate()
@@ -236,3 +503,5 @@ import CoreNFC
236
503
  }
237
504
  }
238
505
  }
506
+
507
+ // Removed supplemental stubs (logNFCFormatsIfAvailable, tagReaderSessionDidBecomeActive) as they are no longer needed.
@@ -31,7 +31,7 @@ import CoreNFC
31
31
  }
32
32
 
33
33
  public func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
34
-
34
+
35
35
  }
36
36
 
37
37
  public func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
@@ -53,7 +53,7 @@ import CoreNFC
53
53
  return
54
54
  }
55
55
 
56
- tag.queryNDEFStatus { (ndefStatus, capacity, error) in
56
+ tag.queryNDEFStatus { (ndefStatus, _, error) in
57
57
  if let error = error {
58
58
  session.invalidate(errorMessage: "Unable to query the NDEF status of tag.")
59
59
  self.onError?(error)
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "@exxili/capacitor-nfc",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A Capacitor plugin for reading and writing NFC NDEFtags.",
5
+ "type": "module",
5
6
  "main": "dist/plugin.cjs.js",
6
7
  "module": "dist/esm/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/esm/index.js",
11
+ "require": "./dist/plugin.cjs.js",
12
+ "types": "./dist/esm/index.d.ts"
13
+ }
14
+ },
7
15
  "types": "dist/esm/index.d.ts",
8
16
  "unpkg": "dist/plugin.js",
9
17
  "files": [
@@ -38,7 +46,7 @@
38
46
  "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
39
47
  "eslint": "eslint . --ext ts",
40
48
  "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
41
- "swiftlint": "node-swiftlint",
49
+ "swiftlint": "node-swiftlint lint ios/Sources/NFCPlugin --config .swiftlint.yml",
42
50
  "docgen": "docgen --api NFCPlugin --output-readme README.md --output-json dist/docs.json",
43
51
  "build": "npm run clean && tsc && rollup -c rollup.config.js && rimraf *.tgz && npm pack",
44
52
  "clean": "rimraf ./dist",