From dbfc9f708360cbba5771cbe4d2157a89ae1bc112 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Mon, 19 Oct 2026 21:17:07 +0800 Subject: [PATCH 1/2] feat(ios): allow skipping NDEF discovery and release stale reader state --- .github/workflows/test.yml | 1 + CHANGELOG.md | 3 + README.md | 25 +++ .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 177 +++++++++++------- lib/flutter_nfc_kit.dart | 12 +- test/poll_test.dart | 80 ++++++++ 6 files changed, 224 insertions(+), 74 deletions(-) create mode 100644 test/poll_test.dart diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74c0458..09cdb59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,7 @@ jobs: channel: ${{ matrix.channel }} cache: true - run: flutter pub get + - run: flutter test - run: dart format --output=none --set-exit-if-changed lib/ - run: dart analyze - name: Install dartdoc diff --git a/CHANGELOG.md b/CHANGELOG.md index f738a04..e686eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -222,3 +222,6 @@ * Update Android build tooling to AGP 9.0.1 and Kotlin 2.3.20; AGP 9 release builds using optimized resource shrinking require `android.nonFinalResIds=true`. ## Unreleased + +* Add `iosCheckNDEF` to optionally skip automatic NDEF discovery after connection (enabled by default), alongside Android's existing `androidCheckNDEF` option. +* Release iOS reader state after invalidation even when `poll()` has already returned, clean up failed connections, and ignore stale discovery/setup callbacks. diff --git a/README.md b/README.md index dbccc75..28ef689 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,31 @@ We use error codes with similar meaning as HTTP status code. Brief explanation a We provide two operation modes: polling (default) and event streaming. Both can give the same `NFCTag` object. Please see the [example](https://github.com/nfcim/flutter_nfc_kit/blob/master/example/example.md) for more details. +### Skipping automatic NDEF discovery (Android and iOS) + +Applications that communicate with a card through raw commands can skip the +initial NDEF check on both platforms: + +```dart +final tag = await FlutterNfcKit.poll( + androidCheckNDEF: false, + iosCheckNDEF: false, +); +``` + +Both options default to `true`; each is ignored on the other platforms. On +Android, `androidCheckNDEF: false` sets `FLAG_READER_SKIP_NDEF_CHECK`, so the +platform's NDEF methods are unavailable for that session. On iOS, +`iosCheckNDEF: false` skips `queryNDEFStatus` after connecting: NDEF metadata stays +at its default values (`false`, zero or empty), even for an NDEF-capable tag. +The iOS NDEF methods still contact the tag when explicitly invoked. Raw +`transceive` commands remain available on both platforms. FeliCa PMm metadata +is still collected on iOS. + +This avoids an extra exchange before the first application command. The time +saved depends on the device and tag; it does not change the polling technology +selection or guarantee a higher discovery success rate. Web ignores both options. + ### Performance Optimization (Android) For Android applications, you can optimize NFC tag detection performance using the `androidReaderModeFlags` parameter: diff --git a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift index adf0e28..dc2fbc4 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -35,6 +35,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe var result: FlutterResult? var tag: NFCTag? var multipleTagMessage: String? + private var checkNDEF = true public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "flutter_nfc_kit/method", binaryMessenger: registrar.messenger()) @@ -63,6 +64,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe } else { let arguments = call.arguments as! [String: Any?] let technologies = arguments["technologies"] as! Int + checkNDEF = arguments["iosCheckNDEF"] as? Bool ?? true // TODO: derive pollingOption from technology flags var pollingOption: NFCTagReaderSession.PollingOption = [] if (technologies & 0x3) != 0 { @@ -74,7 +76,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe if (technologies & 0x8) != 0 { pollingOption.insert(.iso15693) } - session = NFCTagReaderSession(pollingOption: pollingOption, delegate: self) + session = NFCTagReaderSession(pollingOption: pollingOption, delegate: self, queue: DispatchQueue.main) if let alertMessage = arguments["iosAlertMessage"] as? String { session?.alertMessage = alertMessage } @@ -82,7 +84,12 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe self.multipleTagMessage = multipleTagMessage } self.result = result - session?.begin() + guard let session = session else { + self.result = nil + result(FlutterError(code: "405", message: "Unable to create NFC session", details: nil)) + return + } + session.begin() } } else if call.method == "transceive" { if tag != nil { @@ -394,8 +401,9 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe result(FlutterError(code: "406", message: "No tag polled", details: nil)) } } else if call.method == "finish" { - self.result?(FlutterError(code: "406", message: "Session not active", details: nil)) + let pending = self.result self.result = nil + pending?(FlutterError(code: "406", message: "Session not active", details: nil)) if let session = session { let arguments = call.arguments as! [String: Any?] @@ -464,9 +472,8 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe // from NFCTagReaderSessionDelegate public func tagReaderSession(_ invalidatedSession: NFCTagReaderSession, didInvalidateWithError error: Error) { - // Core NFC calls this on its own queue, but `poll()` and `finish()` change - // `session` / `result` on the main thread. Hop to main so the identity check and - // the state it guards happen in one step, ordered against those method calls. + // Keep the identity check and reader-state cleanup together on main, + // ordered against `poll()` and `finish()` method calls. DispatchQueue.main.async { [self] in // `finish()` invalidates the session and clears `self.session` straight away, // but iOS can deliver that session's invalidation after the next `poll()` has @@ -474,38 +481,42 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe // result or reset state — otherwise the stale error reaches the new poll, and // clearing `self.session` / `self.tag` orphans a session still on screen. guard invalidatedSession === session else { return } - guard result != nil else { return } + let pending = result + result = nil + session = nil + tag = nil + // A successful poll has already consumed its result. The reader can + // still time out or be canceled, and must be released in that case. + guard let pending = pending else { return } if let nfcError = error as? NFCReaderError { NSLog("Got NFCError when reading NFC: %@", nfcError.localizedDescription) switch nfcError.errorCode { case NFCReaderError.Code.readerSessionInvalidationErrorUserCanceled.rawValue: - result?(FlutterError(code: "409", message: "SessionCanceled", details: error.localizedDescription)) + pending(FlutterError(code: "409", message: "SessionCanceled", details: error.localizedDescription)) case NFCReaderError.Code.readerSessionInvalidationErrorSessionTimeout.rawValue: - result?(FlutterError(code: "408", message: "SessionTimeOut", details: error.localizedDescription)) + pending(FlutterError(code: "408", message: "SessionTimeOut", details: error.localizedDescription)) default: - result?(FlutterError(code: "500", message: "Generic NFC Error", details: error.localizedDescription)) + pending(FlutterError(code: "500", message: "Generic NFC Error", details: error.localizedDescription)) } } else { NSLog("Got unknown when reading NFC: %@", error.localizedDescription) - result?(FlutterError(code: "500", message: "Invalidate session with error", details: error.localizedDescription)) + pending(FlutterError(code: "500", message: "Invalidate session with error", details: error.localizedDescription)) } - - result = nil - session = nil - tag = nil } } // from NFCTagReaderSessionDelegate public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + guard self.session === session, self.result != nil, !tags.isEmpty else { return } if tags.count > 1 { // Restart polling in 500ms let retryInterval = DispatchTimeInterval.milliseconds(500) if multipleTagMessage != nil { session.alertMessage = multipleTagMessage! } - DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) { + DispatchQueue.main.asyncAfter(deadline: .now() + retryInterval) { + guard self.session === session, self.result != nil else { return } session.restartPolling() } return @@ -570,68 +581,90 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe } session.connect(to: firstTag) { (error: Error?) in - if let error = error { - self.result?(FlutterError(code: "500", message: "Error connecting to card", details: error.localizedDescription)) - self.result = nil - return - } - self.tag = firstTag - - var ndefTag: NFCNDEFTag? - switch self.tag { - case let .iso7816(tag): - ndefTag = tag - case let .miFare(tag): - ndefTag = tag - case let .feliCa(tag): - ndefTag = tag - case let .iso15693(tag): - ndefTag = tag - default: - ndefTag = nil - } - - if ndefTag != nil { - ndefTag!.queryNDEFStatus() { (status: NFCNDEFStatus, capacity: Int, error: Error?) in - if error == nil { - if status != NFCNDEFStatus.notSupported { - result["ndefAvailable"] = true - } - if status == NFCNDEFStatus.readWrite { - result["ndefWritable"] = true - result["ndefCanMakeReadOnly"] = true - } - result["ndefCapacity"] = capacity - } - // ignore error, just return with ndef disabled - switch self.tag { - case let .feliCa(tag): - tag.polling(systemCode: tag.currentSystemCode, requestCode: .noRequest, timeSlot: .max16) { (pmm: Data, _: Data, error: Error?) in - if let error = error { - self.result?(FlutterError(code: "500", message: "Communication error on connect", details: error.localizedDescription)) - self.result = nil - } else { - result["manufacturer"] = pmm.hexEncodedString() + DispatchQueue.main.async { + guard self.session === session, self.result != nil else { return } + if let error = error { + self.failPoll(session, error: FlutterError(code: "500", message: "Error connecting to card", details: error.localizedDescription)) + return + } + self.tag = firstTag - let jsonData = try! JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) - self.result?(jsonString) - self.result = nil + var ndefTag: NFCNDEFTag? + switch firstTag { + case let .iso7816(tag): + ndefTag = tag + case let .miFare(tag): + ndefTag = tag + case let .feliCa(tag): + ndefTag = tag + case let .iso15693(tag): + ndefTag = tag + default: + ndefTag = nil + } + + guard self.checkNDEF, let ndefTag = ndefTag else { + self.completePoll(session, metadata: result) + return + } + ndefTag.queryNDEFStatus() { (status: NFCNDEFStatus, capacity: Int, error: Error?) in + DispatchQueue.main.async { + guard self.session === session, self.result != nil else { return } + if error == nil { + if status != NFCNDEFStatus.notSupported { + result["ndefAvailable"] = true } + if status == NFCNDEFStatus.readWrite { + result["ndefWritable"] = true + result["ndefCanMakeReadOnly"] = true + } + result["ndefCapacity"] = capacity } - default: - let jsonData = try! JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) - self.result?(jsonString) - self.result = nil + // Ignore NDEF errors and return with NDEF metadata disabled. + self.completePoll(session, metadata: result) } } - } else { - let jsonData = try! JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) - self.result?(jsonString) - self.result = nil } } } + + // Called on main, including completion handlers that update reader state. + private func completePoll(_ session: NFCTagReaderSession, metadata: [String: Any]) { + guard self.session === session, self.result != nil else { return } + if case let .feliCa(tag) = self.tag { + // PMm is tag metadata, not NDEF metadata. Preserve it even when the + // caller skips queryNDEFStatus. + tag.polling(systemCode: tag.currentSystemCode, requestCode: .noRequest, timeSlot: .max16) { (pmm: Data, _: Data, error: Error?) in + DispatchQueue.main.async { + guard self.session === session, self.result != nil else { return } + if let error = error { + self.failPoll(session, error: FlutterError(code: "500", message: "Communication error on connect", details: error.localizedDescription)) + } else { + var metadata = metadata + metadata["manufacturer"] = pmm.hexEncodedString() + self.returnPoll(metadata) + } + } + } + } else { + returnPoll(metadata) + } + } + + private func returnPoll(_ metadata: [String: Any]) { + let pending = result + result = nil + let jsonData = try! JSONSerialization.data(withJSONObject: metadata) + pending?(String(data: jsonData, encoding: .utf8)) + } + + private func failPoll(_ session: NFCTagReaderSession, error: FlutterError) { + guard self.session === session else { return } + let pending = result + result = nil + self.session = nil + tag = nil + session.invalidate() + pending?(error) + } } diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index ce53702..abec4b2 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -325,6 +325,10 @@ class FlutterNfcKit { /// /// On iOS, set [iosAlertMessage] to display a message when the session starts (to guide users to scan a tag), /// and set [iosMultipleTagMessage] to display a message when multiple tags are found. + /// Set [iosCheckNDEF] to false to skip automatic NDEF discovery after connecting. + /// It defaults to true and is ignored on Android and Web. When skipped, NDEF + /// metadata is left at its default values (false, zero or empty); this does not + /// mean the tag lacks NDEF support. Raw [transceive] commands remain available. /// /// On Android, set [androidPlatformSound] to control whether to play sound when a tag is polled, /// and set [androidCheckNDEF] to control whether check NDEF records on the tag. @@ -341,8 +345,10 @@ class FlutterNfcKit { /// On Web, all parameters are ignored except [timeout] and [probeWebUSBMagic]. /// If [probeWebUSBMagic] is set, the library will use the `PROBE` request to check whether the device supports our API (see [FlutterNfcKitWeb] for details). /// - /// Note: Sometimes NDEF check [leads to error](https://github.com/nfcim/flutter_nfc_kit/issues/11), and disabling it might help. - /// If disabled, you will not be able to use any NDEF-related methods in the current session. + /// Note: On Android, sometimes NDEF check [leads to error](https://github.com/nfcim/flutter_nfc_kit/issues/11), and disabling it might help. + /// If [androidCheckNDEF] is disabled, Android NDEF-related methods are unavailable + /// in the current session. On iOS, [iosCheckNDEF] only skips the initial query; + /// NDEF methods still contact the tag, but capability metadata is not populated. /// /// **Android Reader Mode Flags** ([androidReaderModeFlags]): /// @@ -371,6 +377,7 @@ class FlutterNfcKit { bool androidPlatformSound = true, bool androidCheckNDEF = true, int? androidReaderModeFlags, + bool iosCheckNDEF = true, String iosAlertMessage = "Hold your iPhone near the card", String iosMultipleTagMessage = "More than one tags are detected, please leave only one tag and try again.", @@ -394,6 +401,7 @@ class FlutterNfcKit { final String data = await _channel.invokeMethod('poll', { 'timeout': timeout?.inMilliseconds ?? POLL_TIMEOUT, 'iosAlertMessage': iosAlertMessage, + 'iosCheckNDEF': iosCheckNDEF, 'iosMultipleTagMessage': iosMultipleTagMessage, 'technologies': technologies, 'probeWebUSBMagic': probeWebUSBMagic, diff --git a/test/poll_test.dart b/test/poll_test.dart new file mode 100644 index 0000000..577ece6 --- /dev/null +++ b/test/poll_test.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const channel = MethodChannel('flutter_nfc_kit/method'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + final calls = []; + + setUp(() { + calls.clear(); + messenger.setMockMethodCallHandler(channel, (call) async { + calls.add(call); + switch (call.method) { + case 'poll': + return jsonEncode({ + 'type': 'iso7816', + 'standard': 'ISO 14443', + 'id': '00', + 'ndefAvailable': false, + 'ndefWritable': false, + 'ndefCapacity': 0, + 'ndefType': '', + 'ndefCanMakeReadOnly': false, + }); + case 'transceive': + return '9000'; + case 'finish': + return null; + default: + fail('Unexpected method: ${call.method}'); + } + }); + }); + + tearDown(() { + messenger.setMockMethodCallHandler(channel, null); + }); + + test('poll preserves automatic NDEF discovery by default', () async { + await FlutterNfcKit.poll(); + final args = calls.single.arguments as Map; + expect(args['iosCheckNDEF'], isTrue); + expect(args['technologies'], 0x0b); + }); + + test('skipping iOS NDEF discovery does not alter Android flags', () async { + await FlutterNfcKit.poll(iosCheckNDEF: false); + final args = calls.single.arguments as Map; + expect(args['iosCheckNDEF'], isFalse); + expect(args['technologies'], 0x0b); + }); + + test('Android NDEF skip flag leaves iOS discovery enabled', () async { + await FlutterNfcKit.poll(androidCheckNDEF: false); + final args = calls.single.arguments as Map; + expect(args['iosCheckNDEF'], isTrue); + expect(args['technologies'], 0x8b); + }); + + test('both platforms can skip discovery before raw commands', () async { + final tag = await FlutterNfcKit.poll( + androidCheckNDEF: false, + iosCheckNDEF: false, + ); + final args = calls.single.arguments as Map; + expect(args['iosCheckNDEF'], isFalse); + expect(args['technologies'], 0x8b); + expect(tag.ndefAvailable, isFalse); + expect(tag.ndefWritable, isFalse); + expect(tag.ndefCapacity, 0); + expect(await FlutterNfcKit.transceive('00A4040000'), '9000'); + await FlutterNfcKit.finish(); + expect(calls.map((call) => call.method), ['poll', 'transceive', 'finish']); + }); +} From c0b51a69182dcf0f91673385e2e70b82a5126365 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Mon, 19 Oct 2026 21:34:57 +0800 Subject: [PATCH 2/2] refactor: unify NDEF discovery option across Android and iOS --- CHANGELOG.md | 2 +- README.md | 17 +++-- .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 2 +- lib/flutter_nfc_kit.dart | 28 ++++---- test/poll_test.dart | 67 ++++++++++++------- 5 files changed, 70 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e686eb8..feb6b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -223,5 +223,5 @@ ## Unreleased -* Add `iosCheckNDEF` to optionally skip automatic NDEF discovery after connection (enabled by default), alongside Android's existing `androidCheckNDEF` option. +* Add cross-platform `checkNDEF` to control automatic NDEF discovery on Android and iOS (enabled by default). Deprecate `androidCheckNDEF`, retaining it as an explicit Android-only override for compatibility. * Release iOS reader state after invalidation even when `poll()` has already returned, clean up failed connections, and ignore stale discovery/setup callbacks. diff --git a/README.md b/README.md index 28ef689..6447a61 100644 --- a/README.md +++ b/README.md @@ -81,15 +81,14 @@ initial NDEF check on both platforms: ```dart final tag = await FlutterNfcKit.poll( - androidCheckNDEF: false, - iosCheckNDEF: false, + checkNDEF: false, ); ``` -Both options default to `true`; each is ignored on the other platforms. On -Android, `androidCheckNDEF: false` sets `FLAG_READER_SKIP_NDEF_CHECK`, so the -platform's NDEF methods are unavailable for that session. On iOS, -`iosCheckNDEF: false` skips `queryNDEFStatus` after connecting: NDEF metadata stays +`checkNDEF` defaults to `true` and is ignored on Web. On Android, setting it to +`false` sets `FLAG_READER_SKIP_NDEF_CHECK`, so the platform's NDEF methods are +unavailable for that session. On iOS, +`checkNDEF: false` skips `queryNDEFStatus` after connecting: NDEF metadata stays at its default values (`false`, zero or empty), even for an NDEF-capable tag. The iOS NDEF methods still contact the tag when explicitly invoked. Raw `transceive` commands remain available on both platforms. FeliCa PMm metadata @@ -97,7 +96,11 @@ is still collected on iOS. This avoids an extra exchange before the first application command. The time saved depends on the device and tag; it does not change the polling technology -selection or guarantee a higher discovery success rate. Web ignores both options. +selection or guarantee a higher discovery success rate. + +The existing `androidCheckNDEF` parameter is deprecated. If explicitly provided, +it overrides `checkNDEF` on Android only, preserving existing Android-only +behavior. Replace it with `checkNDEF` to control discovery on both platforms. ### Performance Optimization (Android) diff --git a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift index dc2fbc4..ca34cce 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -64,7 +64,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe } else { let arguments = call.arguments as! [String: Any?] let technologies = arguments["technologies"] as! Int - checkNDEF = arguments["iosCheckNDEF"] as? Bool ?? true + checkNDEF = arguments["checkNDEF"] as? Bool ?? true // TODO: derive pollingOption from technology flags var pollingOption: NFCTagReaderSession.PollingOption = [] if (technologies & 0x3) != 0 { diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index abec4b2..cd4a495 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -325,13 +325,16 @@ class FlutterNfcKit { /// /// On iOS, set [iosAlertMessage] to display a message when the session starts (to guide users to scan a tag), /// and set [iosMultipleTagMessage] to display a message when multiple tags are found. - /// Set [iosCheckNDEF] to false to skip automatic NDEF discovery after connecting. - /// It defaults to true and is ignored on Android and Web. When skipped, NDEF - /// metadata is left at its default values (false, zero or empty); this does not - /// mean the tag lacks NDEF support. Raw [transceive] commands remain available. /// - /// On Android, set [androidPlatformSound] to control whether to play sound when a tag is polled, - /// and set [androidCheckNDEF] to control whether check NDEF records on the tag. + /// [checkNDEF] controls automatic NDEF discovery on Android and iOS (default + /// true). Set it to false for raw-command applications that do not need the + /// initial NDEF check. It is ignored on Web. Skipping the check leaves NDEF + /// metadata at its defaults (false, zero or empty), even for NDEF-capable tags. + /// Raw [transceive] commands remain available. + /// + /// On Android, [androidPlatformSound] controls whether to play sound when a tag + /// is polled. The deprecated [androidCheckNDEF] overrides [checkNDEF] on Android + /// only when explicitly provided; it has no effect on iOS. /// Set [extraReaderPresenceCheckDelay] to control the interval Android uses /// when checking whether a discovered tag is still present. The value is /// Android-only and is expressed as a [Duration]. When omitted, Android's @@ -346,8 +349,8 @@ class FlutterNfcKit { /// If [probeWebUSBMagic] is set, the library will use the `PROBE` request to check whether the device supports our API (see [FlutterNfcKitWeb] for details). /// /// Note: On Android, sometimes NDEF check [leads to error](https://github.com/nfcim/flutter_nfc_kit/issues/11), and disabling it might help. - /// If [androidCheckNDEF] is disabled, Android NDEF-related methods are unavailable - /// in the current session. On iOS, [iosCheckNDEF] only skips the initial query; + /// Disabling NDEF discovery makes Android NDEF-related methods unavailable + /// in the current session. On iOS, [checkNDEF] only skips the initial query; /// NDEF methods still contact the tag, but capability metadata is not populated. /// /// **Android Reader Mode Flags** ([androidReaderModeFlags]): @@ -375,9 +378,10 @@ class FlutterNfcKit { static Future poll({ Duration? timeout, bool androidPlatformSound = true, - bool androidCheckNDEF = true, + bool checkNDEF = true, + @Deprecated('Use checkNDEF instead. This override only affects Android.') + bool? androidCheckNDEF, int? androidReaderModeFlags, - bool iosCheckNDEF = true, String iosAlertMessage = "Hold your iPhone near the card", String iosMultipleTagMessage = "More than one tags are detected, please leave only one tag and try again.", @@ -396,12 +400,12 @@ class FlutterNfcKit { if (readIso18092) technologies |= 0x4; if (readIso15693) technologies |= 0x8; // iOS can safely ignore these option bits - if (!androidCheckNDEF) technologies |= 0x80; + if (!(androidCheckNDEF ?? checkNDEF)) technologies |= 0x80; if (!androidPlatformSound) technologies |= 0x100; final String data = await _channel.invokeMethod('poll', { 'timeout': timeout?.inMilliseconds ?? POLL_TIMEOUT, 'iosAlertMessage': iosAlertMessage, - 'iosCheckNDEF': iosCheckNDEF, + 'checkNDEF': checkNDEF, 'iosMultipleTagMessage': iosMultipleTagMessage, 'technologies': technologies, 'probeWebUSBMagic': probeWebUSBMagic, diff --git a/test/poll_test.dart b/test/poll_test.dart index 577ece6..3f64ec9 100644 --- a/test/poll_test.dart +++ b/test/poll_test.dart @@ -44,37 +44,54 @@ void main() { test('poll preserves automatic NDEF discovery by default', () async { await FlutterNfcKit.poll(); final args = calls.single.arguments as Map; - expect(args['iosCheckNDEF'], isTrue); + expect(args['checkNDEF'], isTrue); expect(args['technologies'], 0x0b); }); - test('skipping iOS NDEF discovery does not alter Android flags', () async { - await FlutterNfcKit.poll(iosCheckNDEF: false); - final args = calls.single.arguments as Map; - expect(args['iosCheckNDEF'], isFalse); - expect(args['technologies'], 0x0b); - }); - - test('Android NDEF skip flag leaves iOS discovery enabled', () async { + test('legacy Android opt-out leaves iOS discovery enabled', () async { await FlutterNfcKit.poll(androidCheckNDEF: false); final args = calls.single.arguments as Map; - expect(args['iosCheckNDEF'], isTrue); + expect(args['checkNDEF'], isTrue); expect(args['technologies'], 0x8b); }); - test('both platforms can skip discovery before raw commands', () async { - final tag = await FlutterNfcKit.poll( - androidCheckNDEF: false, - iosCheckNDEF: false, - ); - final args = calls.single.arguments as Map; - expect(args['iosCheckNDEF'], isFalse); - expect(args['technologies'], 0x8b); - expect(tag.ndefAvailable, isFalse); - expect(tag.ndefWritable, isFalse); - expect(tag.ndefCapacity, 0); - expect(await FlutterNfcKit.transceive('00A4040000'), '9000'); - await FlutterNfcKit.finish(); - expect(calls.map((call) => call.method), ['poll', 'transceive', 'finish']); - }); + test( + 'legacy Android opt-in overrides shared opt-out on Android only', + () async { + await FlutterNfcKit.poll(checkNDEF: false, androidCheckNDEF: true); + final args = calls.single.arguments as Map; + expect(args['checkNDEF'], isFalse); + expect(args['technologies'], 0x0b); + }, + ); + + test( + 'legacy Android opt-out overrides shared opt-in on Android only', + () async { + await FlutterNfcKit.poll(checkNDEF: true, androidCheckNDEF: false); + final args = calls.single.arguments as Map; + expect(args['checkNDEF'], isTrue); + expect(args['technologies'], 0x8b); + }, + ); + + test( + 'shared opt-out skips discovery on both platforms before raw commands', + () async { + final tag = await FlutterNfcKit.poll(checkNDEF: false); + final args = calls.single.arguments as Map; + expect(args['checkNDEF'], isFalse); + expect(args['technologies'], 0x8b); + expect(tag.ndefAvailable, isFalse); + expect(tag.ndefWritable, isFalse); + expect(tag.ndefCapacity, 0); + expect(await FlutterNfcKit.transceive('00A4040000'), '9000'); + await FlutterNfcKit.finish(); + expect(calls.map((call) => call.method), [ + 'poll', + 'transceive', + 'finish', + ]); + }, + ); }