@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.
@@ -1,507 +1,507 @@
1
- import Foundation
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
- }
35
-
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?)?
42
-
43
- public var onNDEFMessageReceived: (([NFCNDEFMessage], [String: Any]?) -> Void)?
44
- public var onError: ((Error) -> Void)?
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
-
76
- @objc public func startScanning() {
77
- print("NFCReader startScanning called")
78
- guard NFCNDEFReaderSession.readingAvailable else {
79
- print("NFC scanning not supported on this device")
80
- return
81
- }
82
- if readerMode != .fullTag {
83
- let reason = entitlementFailureDetected ? "missing-entitlement" : nil
84
- emitFallbackMetadata(for: readerMode, reason: reason)
85
- }
86
-
87
- autoRestartWorkItem?.cancel()
88
- startCurrentModeSession()
89
- }
90
-
91
- @objc public func cancelScanning() {
92
- autoRestartWorkItem?.cancel()
93
- autoRestartWorkItem = nil
94
- if let session = readerSession { session.invalidate() }
95
- if let ndef = ndefReaderSession { ndef.invalidate() }
96
- readerSession = nil
97
- ndefReaderSession = nil
98
- lastFallbackSignature = nil
99
- }
100
-
101
- public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
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
-
118
- onError?(error)
119
- }
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
-
303
- public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
304
- if tags.count > 1 {
305
- let retryInterval = DispatchTimeInterval.milliseconds(500)
306
- session.alertMessage = "More than one tag detected. Please remove extra tags and try again."
307
- DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) {
308
- session.restartPolling()
309
- }
310
- return
311
- }
312
-
313
- guard let tag = tags.first else {
314
- session.invalidate(errorMessage: "No tag found.")
315
- return
316
- }
317
- session.connect(to: tag) { (error: Error?) in
318
- if let error = error {
319
- session.invalidate(errorMessage: "Unable to connect to tag.")
320
- self.onError?(error)
321
- return
322
- }
323
-
324
- // Extract tag information
325
- let tagInfo = self.extractTagInfo(from: tag)
326
-
327
- // Try to read NDEF if the tag supports it
328
- if case let .iso7816(iso7816Tag) = tag {
329
- self.handleISO7816Tag(iso7816Tag, session: session, tagInfo: tagInfo)
330
- } else if case let .miFare(miFareTag) = tag {
331
- self.handleMiFareTag(miFareTag, session: session, tagInfo: tagInfo)
332
- } else if case let .feliCa(feliCaTag) = tag {
333
- self.handleFeliCaTag(feliCaTag, session: session, tagInfo: tagInfo)
334
- } else if case let .iso15693(iso15693Tag) = tag {
335
- self.handleISO15693Tag(iso15693Tag, session: session, tagInfo: tagInfo)
336
- } else {
337
- // Unknown tag type, still return tag info
338
- session.alertMessage = "Tag detected but no NDEF message found."
339
- session.invalidate()
340
- self.onNDEFMessageReceived?([], tagInfo)
341
- }
342
- }
343
- }
344
-
345
- private func extractTagInfo(from tag: NFCTag) -> [String: Any] {
346
- var tagInfo: [String: Any] = [:]
347
- var techTypes: [String] = []
348
- var uid: String = ""
349
-
350
- switch tag {
351
- case .iso7816(let iso7816Tag):
352
- uid = iso7816Tag.identifier.map { String(format: "%02X", $0) }.joined()
353
- techTypes.append("ISO7816")
354
- tagInfo["type"] = "ISO7816"
355
-
356
- case .miFare(let miFareTag):
357
- uid = miFareTag.identifier.map { String(format: "%02X", $0) }.joined()
358
- techTypes.append("MiFare")
359
- tagInfo["type"] = "MiFare"
360
-
361
- case .feliCa(let feliCaTag):
362
- uid = feliCaTag.currentIDm.map { String(format: "%02X", $0) }.joined()
363
- techTypes.append("FeliCa")
364
- tagInfo["type"] = "FeliCa"
365
-
366
- case .iso15693(let iso15693Tag):
367
- uid = iso15693Tag.identifier.map { String(format: "%02X", $0) }.joined()
368
- techTypes.append("ISO15693")
369
- tagInfo["type"] = "ISO15693"
370
-
371
- @unknown default:
372
- techTypes.append("Unknown")
373
- tagInfo["type"] = "Unknown"
374
- }
375
-
376
- tagInfo["uid"] = uid
377
- tagInfo["techTypes"] = techTypes
378
-
379
- return tagInfo
380
- }
381
-
382
- private func handleMiFareTag(_ miFareTag: NFCMiFareTag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
383
- miFareTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
384
- if let error = error {
385
- session.invalidate(errorMessage: "Unable to query NDEF status of tag.")
386
- self.onError?(error)
387
- return
388
- }
389
-
390
- if ndefStatus == .notSupported {
391
- session.alertMessage = "Tag detected but no NDEF message found."
392
- session.invalidate()
393
- self.onNDEFMessageReceived?([], tagInfo)
394
- return
395
- }
396
-
397
- var updatedTagInfo = tagInfo
398
- updatedTagInfo["maxSize"] = capacity
399
- updatedTagInfo["isWritable"] = ndefStatus == .readWrite
400
-
401
- miFareTag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
402
- if let error = error {
403
- session.invalidate(errorMessage: "Failed to read NDEF from tag.")
404
- self.onError?(error)
405
- return
406
- }
407
-
408
- if let message = message {
409
- session.alertMessage = "Found 1 NDEF message."
410
- session.invalidate()
411
- self.onNDEFMessageReceived?([message], updatedTagInfo)
412
- } else {
413
- session.alertMessage = "Tag detected but no NDEF message found."
414
- session.invalidate()
415
- self.onNDEFMessageReceived?([], updatedTagInfo)
416
- }
417
- }
418
- }
419
- }
420
-
421
- private func handleISO7816Tag(_ iso7816Tag: NFCISO7816Tag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
422
- // ISO7816 tags don't typically support NDEF directly
423
- session.alertMessage = "Tag detected but no NDEF message found."
424
- session.invalidate()
425
- self.onNDEFMessageReceived?([], tagInfo)
426
- }
427
-
428
- private func handleFeliCaTag(_ feliCaTag: NFCFeliCaTag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
429
- feliCaTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
430
- if let error = error {
431
- session.invalidate(errorMessage: "Unable to query NDEF status of tag.")
432
- self.onError?(error)
433
- return
434
- }
435
-
436
- if ndefStatus == .notSupported {
437
- session.alertMessage = "Tag detected but no NDEF message found."
438
- session.invalidate()
439
- self.onNDEFMessageReceived?([], tagInfo)
440
- return
441
- }
442
-
443
- var updatedTagInfo = tagInfo
444
- updatedTagInfo["maxSize"] = capacity
445
- updatedTagInfo["isWritable"] = ndefStatus == .readWrite
446
-
447
- feliCaTag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
448
- if let error = error {
449
- session.invalidate(errorMessage: "Failed to read NDEF from tag.")
450
- self.onError?(error)
451
- return
452
- }
453
-
454
- if let message = message {
455
- session.alertMessage = "Found 1 NDEF message."
456
- session.invalidate()
457
- self.onNDEFMessageReceived?([message], updatedTagInfo)
458
- } else {
459
- session.alertMessage = "Tag detected but no NDEF message found."
460
- session.invalidate()
461
- self.onNDEFMessageReceived?([], updatedTagInfo)
462
- }
463
- }
464
- }
465
- }
466
-
467
- private func handleISO15693Tag(_ iso15693Tag: NFCISO15693Tag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
468
- iso15693Tag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
469
- if let error = error {
470
- session.invalidate(errorMessage: "Unable to query NDEF status of tag.")
471
- self.onError?(error)
472
- return
473
- }
474
-
475
- if ndefStatus == .notSupported {
476
- session.alertMessage = "Tag detected but no NDEF message found."
477
- session.invalidate()
478
- self.onNDEFMessageReceived?([], tagInfo)
479
- return
480
- }
481
-
482
- var updatedTagInfo = tagInfo
483
- updatedTagInfo["maxSize"] = capacity
484
- updatedTagInfo["isWritable"] = ndefStatus == .readWrite
485
-
486
- iso15693Tag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
487
- if let error = error {
488
- session.invalidate(errorMessage: "Failed to read NDEF from tag.")
489
- self.onError?(error)
490
- return
491
- }
492
-
493
- if let message = message {
494
- session.alertMessage = "Found 1 NDEF message."
495
- session.invalidate()
496
- self.onNDEFMessageReceived?([message], updatedTagInfo)
497
- } else {
498
- session.alertMessage = "Tag detected but no NDEF message found."
499
- session.invalidate()
500
- self.onNDEFMessageReceived?([], updatedTagInfo)
501
- }
502
- }
503
- }
504
- }
505
- }
506
-
507
- // Removed supplemental stubs (logNFCFormatsIfAvailable, tagReaderSessionDidBecomeActive) as they are no longer needed.
1
+ import Foundation
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
+ }
35
+
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?)?
42
+
43
+ public var onNDEFMessageReceived: (([NFCNDEFMessage], [String: Any]?) -> Void)?
44
+ public var onError: ((Error) -> Void)?
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
+
76
+ @objc public func startScanning() {
77
+ print("NFCReader startScanning called")
78
+ guard NFCNDEFReaderSession.readingAvailable else {
79
+ print("NFC scanning not supported on this device")
80
+ return
81
+ }
82
+ if readerMode != .fullTag {
83
+ let reason = entitlementFailureDetected ? "missing-entitlement" : nil
84
+ emitFallbackMetadata(for: readerMode, reason: reason)
85
+ }
86
+
87
+ autoRestartWorkItem?.cancel()
88
+ startCurrentModeSession()
89
+ }
90
+
91
+ @objc public func cancelScanning() {
92
+ autoRestartWorkItem?.cancel()
93
+ autoRestartWorkItem = nil
94
+ if let session = readerSession { session.invalidate() }
95
+ if let ndef = ndefReaderSession { ndef.invalidate() }
96
+ readerSession = nil
97
+ ndefReaderSession = nil
98
+ lastFallbackSignature = nil
99
+ }
100
+
101
+ public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
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
+
118
+ onError?(error)
119
+ }
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
+
303
+ public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
304
+ if tags.count > 1 {
305
+ let retryInterval = DispatchTimeInterval.milliseconds(500)
306
+ session.alertMessage = "More than one tag detected. Please remove extra tags and try again."
307
+ DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) {
308
+ session.restartPolling()
309
+ }
310
+ return
311
+ }
312
+
313
+ guard let tag = tags.first else {
314
+ session.invalidate(errorMessage: "No tag found.")
315
+ return
316
+ }
317
+ session.connect(to: tag) { (error: Error?) in
318
+ if let error = error {
319
+ session.invalidate(errorMessage: "Unable to connect to tag.")
320
+ self.onError?(error)
321
+ return
322
+ }
323
+
324
+ // Extract tag information
325
+ let tagInfo = self.extractTagInfo(from: tag)
326
+
327
+ // Try to read NDEF if the tag supports it
328
+ if case let .iso7816(iso7816Tag) = tag {
329
+ self.handleISO7816Tag(iso7816Tag, session: session, tagInfo: tagInfo)
330
+ } else if case let .miFare(miFareTag) = tag {
331
+ self.handleMiFareTag(miFareTag, session: session, tagInfo: tagInfo)
332
+ } else if case let .feliCa(feliCaTag) = tag {
333
+ self.handleFeliCaTag(feliCaTag, session: session, tagInfo: tagInfo)
334
+ } else if case let .iso15693(iso15693Tag) = tag {
335
+ self.handleISO15693Tag(iso15693Tag, session: session, tagInfo: tagInfo)
336
+ } else {
337
+ // Unknown tag type, still return tag info
338
+ session.alertMessage = "Tag detected but no NDEF message found."
339
+ session.invalidate()
340
+ self.onNDEFMessageReceived?([], tagInfo)
341
+ }
342
+ }
343
+ }
344
+
345
+ private func extractTagInfo(from tag: NFCTag) -> [String: Any] {
346
+ var tagInfo: [String: Any] = [:]
347
+ var techTypes: [String] = []
348
+ var uid: String = ""
349
+
350
+ switch tag {
351
+ case .iso7816(let iso7816Tag):
352
+ uid = iso7816Tag.identifier.map { String(format: "%02X", $0) }.joined()
353
+ techTypes.append("ISO7816")
354
+ tagInfo["type"] = "ISO7816"
355
+
356
+ case .miFare(let miFareTag):
357
+ uid = miFareTag.identifier.map { String(format: "%02X", $0) }.joined()
358
+ techTypes.append("MiFare")
359
+ tagInfo["type"] = "MiFare"
360
+
361
+ case .feliCa(let feliCaTag):
362
+ uid = feliCaTag.currentIDm.map { String(format: "%02X", $0) }.joined()
363
+ techTypes.append("FeliCa")
364
+ tagInfo["type"] = "FeliCa"
365
+
366
+ case .iso15693(let iso15693Tag):
367
+ uid = iso15693Tag.identifier.map { String(format: "%02X", $0) }.joined()
368
+ techTypes.append("ISO15693")
369
+ tagInfo["type"] = "ISO15693"
370
+
371
+ @unknown default:
372
+ techTypes.append("Unknown")
373
+ tagInfo["type"] = "Unknown"
374
+ }
375
+
376
+ tagInfo["uid"] = uid
377
+ tagInfo["techTypes"] = techTypes
378
+
379
+ return tagInfo
380
+ }
381
+
382
+ private func handleMiFareTag(_ miFareTag: NFCMiFareTag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
383
+ miFareTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
384
+ if let error = error {
385
+ session.invalidate(errorMessage: "Unable to query NDEF status of tag.")
386
+ self.onError?(error)
387
+ return
388
+ }
389
+
390
+ if ndefStatus == .notSupported {
391
+ session.alertMessage = "Tag detected but no NDEF message found."
392
+ session.invalidate()
393
+ self.onNDEFMessageReceived?([], tagInfo)
394
+ return
395
+ }
396
+
397
+ var updatedTagInfo = tagInfo
398
+ updatedTagInfo["maxSize"] = capacity
399
+ updatedTagInfo["isWritable"] = ndefStatus == .readWrite
400
+
401
+ miFareTag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
402
+ if let error = error {
403
+ session.invalidate(errorMessage: "Failed to read NDEF from tag.")
404
+ self.onError?(error)
405
+ return
406
+ }
407
+
408
+ if let message = message {
409
+ session.alertMessage = "Found 1 NDEF message."
410
+ session.invalidate()
411
+ self.onNDEFMessageReceived?([message], updatedTagInfo)
412
+ } else {
413
+ session.alertMessage = "Tag detected but no NDEF message found."
414
+ session.invalidate()
415
+ self.onNDEFMessageReceived?([], updatedTagInfo)
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ private func handleISO7816Tag(_ iso7816Tag: NFCISO7816Tag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
422
+ // ISO7816 tags don't typically support NDEF directly
423
+ session.alertMessage = "Tag detected but no NDEF message found."
424
+ session.invalidate()
425
+ self.onNDEFMessageReceived?([], tagInfo)
426
+ }
427
+
428
+ private func handleFeliCaTag(_ feliCaTag: NFCFeliCaTag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
429
+ feliCaTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
430
+ if let error = error {
431
+ session.invalidate(errorMessage: "Unable to query NDEF status of tag.")
432
+ self.onError?(error)
433
+ return
434
+ }
435
+
436
+ if ndefStatus == .notSupported {
437
+ session.alertMessage = "Tag detected but no NDEF message found."
438
+ session.invalidate()
439
+ self.onNDEFMessageReceived?([], tagInfo)
440
+ return
441
+ }
442
+
443
+ var updatedTagInfo = tagInfo
444
+ updatedTagInfo["maxSize"] = capacity
445
+ updatedTagInfo["isWritable"] = ndefStatus == .readWrite
446
+
447
+ feliCaTag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
448
+ if let error = error {
449
+ session.invalidate(errorMessage: "Failed to read NDEF from tag.")
450
+ self.onError?(error)
451
+ return
452
+ }
453
+
454
+ if let message = message {
455
+ session.alertMessage = "Found 1 NDEF message."
456
+ session.invalidate()
457
+ self.onNDEFMessageReceived?([message], updatedTagInfo)
458
+ } else {
459
+ session.alertMessage = "Tag detected but no NDEF message found."
460
+ session.invalidate()
461
+ self.onNDEFMessageReceived?([], updatedTagInfo)
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+ private func handleISO15693Tag(_ iso15693Tag: NFCISO15693Tag, session: NFCTagReaderSession, tagInfo: [String: Any]) {
468
+ iso15693Tag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, capacity: Int, error: Error?) in
469
+ if let error = error {
470
+ session.invalidate(errorMessage: "Unable to query NDEF status of tag.")
471
+ self.onError?(error)
472
+ return
473
+ }
474
+
475
+ if ndefStatus == .notSupported {
476
+ session.alertMessage = "Tag detected but no NDEF message found."
477
+ session.invalidate()
478
+ self.onNDEFMessageReceived?([], tagInfo)
479
+ return
480
+ }
481
+
482
+ var updatedTagInfo = tagInfo
483
+ updatedTagInfo["maxSize"] = capacity
484
+ updatedTagInfo["isWritable"] = ndefStatus == .readWrite
485
+
486
+ iso15693Tag.readNDEF { (message: NFCNDEFMessage?, error: Error?) in
487
+ if let error = error {
488
+ session.invalidate(errorMessage: "Failed to read NDEF from tag.")
489
+ self.onError?(error)
490
+ return
491
+ }
492
+
493
+ if let message = message {
494
+ session.alertMessage = "Found 1 NDEF message."
495
+ session.invalidate()
496
+ self.onNDEFMessageReceived?([message], updatedTagInfo)
497
+ } else {
498
+ session.alertMessage = "Tag detected but no NDEF message found."
499
+ session.invalidate()
500
+ self.onNDEFMessageReceived?([], updatedTagInfo)
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ // Removed supplemental stubs (logNFCFormatsIfAvailable, tagReaderSessionDidBecomeActive) as they are no longer needed.