From 459bced20f6a7b1d5e866d180be578bdb613090c Mon Sep 17 00:00:00 2001 From: Rushank Date: Sun, 2 Feb 2025 13:38:08 +0530 Subject: [PATCH 01/30] Enable read block for iOS MIFARE Classic --- .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 3ef6b34..2411ec1 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -211,7 +211,21 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe tag.extendedReadSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, completionHandler: handler) } } else { - result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) + if case let .miFare(tag) = tag { + let blockNumber = arguments["index"] as! UInt8 + let commandPacket = Data([0x30, blockNumber]) //0x30 is the MIFARE Classic Read Command. + tag.sendMiFareCommand(commandPacket: commandPacket) { (data, error) in + if let error = error { + result(FlutterError(code: "405", message: "Something is wrong", details: nil)) + } else { + result(data) + } + } + } + else + { + result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) + } } } else if call.method == "writeBlock" { let arguments = call.arguments as! [String : Any?] From 46380e0784b3f76ca7c7b4889b4b9abbda7cae7e Mon Sep 17 00:00:00 2001 From: Rushank Date: Sun, 2 Feb 2025 13:42:43 +0530 Subject: [PATCH 02/30] Updated Readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f48442c..798c595 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This plugin's functionalities include: * ISO 18092 (NFC-F / FeliCa) * ISO 15963 (NFC-V) * R/W block / page / sector level data of tags complying with: - * MIFARE Classic / Ultralight (Android only) + * MIFARE Classic / Ultralight (Android only, MIFARE Classic Read block for iOS) * ISO 15693 (iOS only) * transceive raw commands with tags / cards complying with: * ISO 7816 Smart Cards (layer 4, in APDUs) From ec54bef9266476801fd8bdcad7b0766f6199b2b6 Mon Sep 17 00:00:00 2001 From: Rushank Date: Sun, 2 Feb 2025 19:30:30 +0530 Subject: [PATCH 03/30] Enable write block for iOS MIFARE Classic --- .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 2411ec1..f2c87d0 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -248,7 +248,23 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe tag.extendedWriteSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, dataBlock: data, completionHandler: handler) } } else { - result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) + if case let .miFare(tag) = tag { + let blockNumber = arguments["index"] as! UInt8 + let writeCommand = Data([0xA2, blockNumber]) + data //0xA2 is the MIFARE Classic Write Command to write single block. + tag.sendMiFareCommand(commandPacket: writeCommand) { (response, error) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: nil)) + } + else + { + result(nil) + } + } + } + else + { + result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) + } } } else if call.method == "readNDEF" { if tag != nil { From 6bf8b4654c27411555aac7ade7f1a79685548bbb Mon Sep 17 00:00:00 2001 From: Rushank Date: Sun, 2 Feb 2025 19:33:09 +0530 Subject: [PATCH 04/30] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 798c595..139613e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This plugin's functionalities include: * ISO 18092 (NFC-F / FeliCa) * ISO 15963 (NFC-V) * R/W block / page / sector level data of tags complying with: - * MIFARE Classic / Ultralight (Android only, MIFARE Classic Read block for iOS) + * MIFARE Classic / Ultralight (Android only, MIFARE Classic Read & Write block for iOS) * ISO 15693 (iOS only) * transceive raw commands with tags / cards complying with: * ISO 7816 Smart Cards (layer 4, in APDUs) From c313e547959dd0b225f4f4b565dc86de1e964871 Mon Sep 17 00:00:00 2001 From: Rushank Date: Mon, 3 Feb 2025 12:55:00 +0530 Subject: [PATCH 05/30] Code Refactor --- .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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 f2c87d0..2bb6254 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -210,8 +210,8 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let blockNumber = arguments["index"] as! Int tag.extendedReadSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, completionHandler: handler) } - } else { - if case let .miFare(tag) = tag { + } + else if case let .miFare(tag) = tag { let blockNumber = arguments["index"] as! UInt8 let commandPacket = Data([0x30, blockNumber]) //0x30 is the MIFARE Classic Read Command. tag.sendMiFareCommand(commandPacket: commandPacket) { (data, error) in @@ -221,11 +221,9 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe result(data) } } - } - else - { - result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) - } + } + else { + result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) } } else if call.method == "writeBlock" { let arguments = call.arguments as! [String : Any?] @@ -247,8 +245,8 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let blockNumber = arguments["index"] as! Int tag.extendedWriteSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, dataBlock: data, completionHandler: handler) } - } else { - if case let .miFare(tag) = tag { + } + else if case let .miFare(tag) = tag { let blockNumber = arguments["index"] as! UInt8 let writeCommand = Data([0xA2, blockNumber]) + data //0xA2 is the MIFARE Classic Write Command to write single block. tag.sendMiFareCommand(commandPacket: writeCommand) { (response, error) in @@ -260,11 +258,9 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe result(nil) } } - } - else - { + } + else { result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) - } } } else if call.method == "readNDEF" { if tag != nil { From 6ca4f7da99c2c8de8f61f199a301abb5794b6362 Mon Sep 17 00:00:00 2001 From: Akshya Date: Thu, 4 Sep 2025 17:41:33 +0530 Subject: [PATCH 06/30] Fix: Ensure NFC Handler thread is alive for Android SDK 35 --- .../kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt index 2ed8160..f476c22 100644 --- a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt +++ b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt @@ -89,9 +89,12 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } - if (!nfcHandler.post(handledFn)) { - result.error("500", "Failed to post job to NFC Handler thread.", null) + val looperThread = nfcHandler.looper?.thread + if (looperThread == null || !looperThread.isAlive) { + val thread = HandlerThread("FlutterNfcKit").apply { start() } + nfcHandler = Handler(thread.looper) } + nfcHandler.post(handledFn) } private fun parseTag(tag: Tag): String { From a8c117bfe6c3219dc829a77dca90eabb9b00a398 Mon Sep 17 00:00:00 2001 From: Akshya Date: Thu, 4 Sep 2025 19:52:27 +0530 Subject: [PATCH 07/30] fix: add error handling for failed NFC Handler.post --- .../kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt index f476c22..49095c3 100644 --- a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt +++ b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt @@ -94,7 +94,10 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val thread = HandlerThread("FlutterNfcKit").apply { start() } nfcHandler = Handler(thread.looper) } - nfcHandler.post(handledFn) + val posted = nfcHandler.post(handledFn) + if (!posted) { + result.error("500", "Failed to post job to NFC Handler thread.", null) + } } private fun parseTag(tag: Tag): String { From c88a2408d85bde4c71ea81444179cfcc102deb04 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Fri, 5 Sep 2025 11:00:19 +0800 Subject: [PATCH 08/30] ci: bump action version Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index f7faa45..93bfe6b 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -17,8 +17,8 @@ jobs: variant: [debug, release] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -49,7 +49,7 @@ jobs: variant: [debug, release] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: subosito/flutter-action@v2 with: channel: 'stable' From 5ec3f6f98a530cd77b2ce7cab412218e81ca83ad Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Fri, 5 Sep 2025 11:05:04 +0800 Subject: [PATCH 09/30] build: bump to Gradle 9.0.0, AGP 8.13.0, Kotlin 2.2.10 Signed-off-by: Shengqi Chen --- android/gradle.properties | 6 +++--- android/gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/gradle.properties | 4 ++-- example/android/gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 48dd80c..c800c46 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.enableJetifier=true -AGPVersion=8.7.3 -KotlinVersion=2.1.0 +AGPVersion=8.13.0 +KotlinVersion=2.2.10 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 543c293..c9c48b5 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Sep 08 22:01:14 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1e7bb61..ee38cb9 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -6,5 +6,5 @@ android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false -AGPVersion=8.7.3 -KotlinVersion=2.1.0 +AGPVersion=8.13.0 +KotlinVersion=2.2.10 diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 371c357..d054919 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip From bd14f820d4cb4afe976264e13fca3db5e6bb60d7 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Fri, 5 Sep 2025 11:09:05 +0800 Subject: [PATCH 10/30] build: increase max Java heap to avoid OOM when building debug variant Signed-off-by: Shengqi Chen --- example/android/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/android/gradle.properties b/example/android/gradle.properties index ee38cb9..0977159 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true android.enableJetifier=true From c0fe665a70bbbc808917fe3099d74dd1b5fccc23 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:23:27 +0800 Subject: [PATCH 11/30] web: refactor with dart:js_interop (fix #223) Signed-off-by: Shengqi Chen --- lib/flutter_nfc_kit_web.dart | 11 ++---- lib/webusb_interop.dart | 73 +++++++++++++++++++----------------- pubspec.yaml | 5 ++- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/lib/flutter_nfc_kit_web.dart b/lib/flutter_nfc_kit_web.dart index 1fbc876..9298e9e 100644 --- a/lib/flutter_nfc_kit_web.dart +++ b/lib/flutter_nfc_kit_web.dart @@ -1,10 +1,7 @@ import 'dart:async'; -// In order to *not* need this ignore, consider extracting the "web" version -// of your plugin as a separate package, instead of inlining it in the same -// package as the core of your plugin. -// ignore: avoid_web_libraries_in_flutter -import 'dart:html' as html show window; -import 'dart:js_util'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; +import 'package:web/web.dart' show window; import 'package:convert/convert.dart'; import 'package:flutter/services.dart'; @@ -35,7 +32,7 @@ class FlutterNfcKitWeb { Future handleMethodCall(MethodCall call) async { switch (call.method) { case 'getNFCAvailability': - if (hasProperty(html.window.navigator, 'usb')) { + if (window.navigator.hasProperty('usb'.toJS).toDart) { return 'available'; } else { return 'not_supported'; diff --git a/lib/webusb_interop.dart b/lib/webusb_interop.dart index a0a750d..7d6728d 100644 --- a/lib/webusb_interop.dart +++ b/lib/webusb_interop.dart @@ -6,10 +6,9 @@ library; import 'dart:convert'; -import 'dart:js_util'; import 'dart:async'; -import 'dart:typed_data'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe' show JSObjectUnsafeUtilExtension; import 'package:convert/convert.dart'; import 'package:flutter/services.dart'; @@ -22,7 +21,7 @@ const int USB_CLASS_CODE_VENDOR_SPECIFIC = 0xFF; @JS('navigator.usb') extension type _USB._(JSObject _) implements JSObject { - external static JSObject requestDevice(_USBDeviceRequestOptions options); + external static JSPromise requestDevice(_USBDeviceRequestOptions options); external static set ondisconnect(JSFunction value); } @@ -54,12 +53,17 @@ extension type _USBControlTransferParameters._(JSObject _) implements JSObject { /// /// Note: you should **NEVER use this class directly**, but instead use the [FlutterNfcKit] class in your project. class WebUSB { - static dynamic _device; + static JSObject? _device; static String customProbeData = ""; static Function? onDisconnect; static bool _deviceAvailable() { - return _device != null && getProperty(_device, 'opened'); + return _device != null && _device!.getProperty('opened'.toJS).toDart; + } + + static Uint8List _getDataBufferFromResponse(JSObject response) { + var dataView = response.getProperty('data'.toJS).toDart; + return dataView.buffer.asUint8List(); } static const USB_PROBE_MAGIC = '_NFC_IM_'; @@ -71,11 +75,12 @@ class WebUSB { var devicePromise = _USB.requestDevice(_USBDeviceRequestOptions( filters: [_USBDeviceFilter(classCode: USB_CLASS_CODE_VENDOR_SPECIFIC)] .toJS)); - dynamic device = await promiseToFuture(devicePromise); + var device = await devicePromise.toDart; try { - await promiseToFuture(callMethod(device, 'open', List.empty())) + var openPromise = device.callMethod('open'.toJS) as JSPromise; + await openPromise.toDart .then((_) => - promiseToFuture(callMethod(device, 'claimInterface', [1]))) + (device.callMethod('claimInterface'.toJS, 1.toJS) as JSPromise).toDart) .timeout(Duration(milliseconds: timeout)); _device = device; _USB.ondisconnect = () { @@ -95,22 +100,21 @@ class WebUSB { if (probeMagic) { try { // PROBE request - var promise = callMethod(_device, 'controlTransferIn', [ + var promise = device.callMethod('controlTransferIn'.toJS, _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 0xff, value: 0, index: 1), - 1 - ]); - var resp = await promiseToFuture(promise); - if (getProperty(resp, 'status') == 'stalled') { + 1.toJS + ) as JSPromise; + var resp = await promise.toDart ; + if (resp.getProperty('status'.toJS).toDart == 'stalled') { throw PlatformException( code: "500", message: "Device error: transfer stalled"); } - var result = - (getProperty(resp, 'data').buffer as ByteBuffer).asUint8List(); + var result = _getDataBufferFromResponse(resp); if (result.length < USB_PROBE_MAGIC.length || result.sublist(0, USB_PROBE_MAGIC.length) != Uint8List.fromList(USB_PROBE_MAGIC.codeUnits)) { @@ -129,9 +133,10 @@ class WebUSB { customProbeData = ""; } } + assert(_device != null); // get VID & PID - int vendorId = getProperty(_device, 'vendorId'); - int productId = getProperty(_device, 'productId'); + var vendorId = _device!.getProperty('vendorId'.toJS).toDartInt; + var productId = _device!.getProperty('productId'.toJS).toDartInt; String id = '${vendorId.toRadixString(16).padLeft(4, '0')}:${productId.toRadixString(16).padLeft(4, '0')}'; return json.encode({ @@ -144,33 +149,33 @@ class WebUSB { static Future _doTransceive(Uint8List capdu) async { // send a command (CMD) - var promise = callMethod(_device, 'controlTransferOut', [ + var promise = _device!.callMethod('controlTransferOut'.toJS, _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 0, value: 0, index: 1), - capdu - ]); - await promiseToFuture(promise); + capdu.toJS + ) as JSPromise; + await promise.toDart; // wait for execution to finish (STAT) while (true) { - promise = callMethod(_device, 'controlTransferIn', [ + promise = _device!.callMethod('controlTransferIn'.toJS, _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 2, value: 0, index: 1), - 1 - ]); - var resp = await promiseToFuture(promise); - if (getProperty(resp, 'status') == 'stalled') { + 1.toJS + ) as JSPromise; + var resp = await promise.toDart; + if (resp.getProperty('status'.toJS).toDart == 'stalled') { throw PlatformException( code: "500", message: "Device error: transfer stalled"); } - var code = getProperty(resp, 'data').buffer.asUint8List()[0]; + var code = _getDataBufferFromResponse(resp)[0]; if (code == 0) { break; } else if (code == 1) { @@ -181,24 +186,24 @@ class WebUSB { } } // get the response (RESP) - promise = callMethod(_device, 'controlTransferIn', [ + promise = _device!.callMethod('controlTransferIn'.toJS, _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 1, value: 0, index: 1), - 1500 - ]); - var resp = await promiseToFuture(promise); - var deviceStatus = getProperty(resp, 'status'); + 1500.toJS + ) as JSPromise; + var resp = await promise.toDart; + var deviceStatus = resp.getProperty('status'.toJS).toDart; if (deviceStatus != 'ok') { throw PlatformException( code: "500", message: "Device error: status should be \"ok\", got \"$deviceStatus\""); } - return getProperty(resp, 'data').buffer.asUint8List(); + return _getDataBufferFromResponse(resp); } /// Transceive data with polled WebUSB device according to our protocol. @@ -232,7 +237,7 @@ class WebUSB { if (_deviceAvailable()) { if (closeWebUSB) { try { - await promiseToFuture(callMethod(_device, "close", List.empty())); + await (_device!.callMethod("close".toJS) as JSPromise).toDart; } on Exception catch (e) { log.severe("Finish error: ", e); throw PlatformException( diff --git a/pubspec.yaml b/pubspec.yaml index 84eb15e..cf01a91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,14 +13,15 @@ dependencies: flutter_web_plugins: sdk: flutter json_annotation: ^4.8.1 - ndef: ^0.3.3 + ndef: ^0.4.0 convert: ^3.1.1 logging: ^1.2.0 + web: ^1.1.1 dev_dependencies: flutter_test: sdk: flutter - lints: ^5.0.0 + lints: ^6.0.0 build_runner: ^2.4.9 json_serializable: ^6.7.1 From 33235cc4ccce6aa11e4db225148602d9d6f95d21 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:24:40 +0800 Subject: [PATCH 12/30] ci: bump versions of actions Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 4 ++-- .github/workflows/publish.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index 93bfe6b..5b2709e 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -17,7 +17,7 @@ jobs: variant: [debug, release] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: 'temurin' @@ -34,7 +34,7 @@ jobs: #- run: flutter test - run: flutter build apk --${{ matrix.variant }} --verbose working-directory: example/ - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: example-apk-${{ matrix.variant }} path: | diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 7d70259..ed5c711 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,7 +1,7 @@ name: Publish to pub.dev env: - FLUTTER_VERSION: 3.24.5 + FLUTTER_VERSION: 3.38.3 on: push: @@ -15,10 +15,10 @@ jobs: id-token: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dart-lang/setup-dart@v1 - name: Setup Flutter SDK - uses: flutter-actions/setup-flutter@v3 + uses: flutter-actions/setup-flutter@v4 with: channel: stable version: ${{ env.FLUTTER_VERSION }} From e38f6e100cf66a6c17a1e90198372ad17e890877 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:25:23 +0800 Subject: [PATCH 13/30] chore: run dart fmt Signed-off-by: Shengqi Chen --- lib/webusb_interop.dart | 82 +++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/lib/webusb_interop.dart b/lib/webusb_interop.dart index 7d6728d..1c3b2df 100644 --- a/lib/webusb_interop.dart +++ b/lib/webusb_interop.dart @@ -21,7 +21,8 @@ const int USB_CLASS_CODE_VENDOR_SPECIFIC = 0xFF; @JS('navigator.usb') extension type _USB._(JSObject _) implements JSObject { - external static JSPromise requestDevice(_USBDeviceRequestOptions options); + external static JSPromise requestDevice( + _USBDeviceRequestOptions options); external static set ondisconnect(JSFunction value); } @@ -58,7 +59,8 @@ class WebUSB { static Function? onDisconnect; static bool _deviceAvailable() { - return _device != null && _device!.getProperty('opened'.toJS).toDart; + return _device != null && + _device!.getProperty('opened'.toJS).toDart; } static Uint8List _getDataBufferFromResponse(JSObject response) { @@ -80,7 +82,8 @@ class WebUSB { var openPromise = device.callMethod('open'.toJS) as JSPromise; await openPromise.toDart .then((_) => - (device.callMethod('claimInterface'.toJS, 1.toJS) as JSPromise).toDart) + (device.callMethod('claimInterface'.toJS, 1.toJS) as JSPromise) + .toDart) .timeout(Duration(milliseconds: timeout)); _device = device; _USB.ondisconnect = () { @@ -100,16 +103,16 @@ class WebUSB { if (probeMagic) { try { // PROBE request - var promise = device.callMethod('controlTransferIn'.toJS, - _USBControlTransferParameters( - requestType: 'vendor', - recipient: 'interface', - request: 0xff, - value: 0, - index: 1), - 1.toJS - ) as JSPromise; - var resp = await promise.toDart ; + var promise = device.callMethod( + 'controlTransferIn'.toJS, + _USBControlTransferParameters( + requestType: 'vendor', + recipient: 'interface', + request: 0xff, + value: 0, + index: 1), + 1.toJS) as JSPromise; + var resp = await promise.toDart; if (resp.getProperty('status'.toJS).toDart == 'stalled') { throw PlatformException( code: "500", message: "Device error: transfer stalled"); @@ -149,27 +152,27 @@ class WebUSB { static Future _doTransceive(Uint8List capdu) async { // send a command (CMD) - var promise = _device!.callMethod('controlTransferOut'.toJS, - _USBControlTransferParameters( - requestType: 'vendor', - recipient: 'interface', - request: 0, - value: 0, - index: 1), - capdu.toJS - ) as JSPromise; - await promise.toDart; - // wait for execution to finish (STAT) - while (true) { - promise = _device!.callMethod('controlTransferIn'.toJS, + var promise = _device!.callMethod( + 'controlTransferOut'.toJS, _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', - request: 2, + request: 0, value: 0, index: 1), - 1.toJS - ) as JSPromise; + capdu.toJS) as JSPromise; + await promise.toDart; + // wait for execution to finish (STAT) + while (true) { + promise = _device!.callMethod( + 'controlTransferIn'.toJS, + _USBControlTransferParameters( + requestType: 'vendor', + recipient: 'interface', + request: 2, + value: 0, + index: 1), + 1.toJS) as JSPromise; var resp = await promise.toDart; if (resp.getProperty('status'.toJS).toDart == 'stalled') { throw PlatformException( @@ -186,15 +189,15 @@ class WebUSB { } } // get the response (RESP) - promise = _device!.callMethod('controlTransferIn'.toJS, - _USBControlTransferParameters( - requestType: 'vendor', - recipient: 'interface', - request: 1, - value: 0, - index: 1), - 1500.toJS - ) as JSPromise; + promise = _device!.callMethod( + 'controlTransferIn'.toJS, + _USBControlTransferParameters( + requestType: 'vendor', + recipient: 'interface', + request: 1, + value: 0, + index: 1), + 1500.toJS) as JSPromise; var resp = await promise.toDart; var deviceStatus = resp.getProperty('status'.toJS).toDart; if (deviceStatus != 'ok') { @@ -237,7 +240,8 @@ class WebUSB { if (_deviceAvailable()) { if (closeWebUSB) { try { - await (_device!.callMethod("close".toJS) as JSPromise).toDart; + await (_device!.callMethod("close".toJS) as JSPromise) + .toDart; } on Exception catch (e) { log.severe("Finish error: ", e); throw PlatformException( From cee698e8bcb04dd8ce8285e717cd6a93a814c260 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:26:37 +0800 Subject: [PATCH 14/30] example: bump dependencies Signed-off-by: Shengqi Chen --- example/pubspec.lock | 112 +++++++++++++++++++++---------------------- example/pubspec.yaml | 2 +- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 83e871a..13dcaee 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" fixnum: dependency: transitive description: @@ -115,26 +115,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" logging: dependency: "direct main" description: @@ -147,10 +147,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -163,87 +163,79 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" ndef: dependency: "direct main" description: name: ndef - sha256: "5083507cff4bb823b2a198a27ea2c70c4d6bc27a97b66097d966a250e1615d54" + sha256: "198ba3798e80cea381648569d84059dbba64cd140079fb7b0d9c3f1e0f5973f3" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.4.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.7" typed_data: dependency: transitive description: @@ -256,26 +248,34 @@ packages: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.2" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "1.1.1" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5000b03..4bc0d17 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter_nfc_kit: path: ../ logging: ^1.3.0 - ndef: ^0.3.3 + ndef: ^0.4.0 cupertino_icons: ^1.0.8 dev_dependencies: From 0e13120c3ee4bd3bc043b9f230bd71be2770e2a8 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:27:30 +0800 Subject: [PATCH 15/30] doc: fix typo in README (fix: #221) Signed-off-by: Shengqi Chen --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 139613e..c6d328a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This plugin's functionalities include: * read metadata and read & write NDEF records of tags / cards complying with: * ISO 14443 Type A & Type B (NFC-A / NFC-B / MIFARE Classic / MIFARE Plus / MIFARE Ultralight / MIFARE DESFire) * ISO 18092 (NFC-F / FeliCa) - * ISO 15963 (NFC-V) + * ISO 15693 (NFC-V) * R/W block / page / sector level data of tags complying with: * MIFARE Classic / Ultralight (Android only, MIFARE Classic Read & Write block for iOS) * ISO 15693 (iOS only) From 506e84e5f5c6d60ddb8749bc685a6bfd88ae1a7c Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:40:47 +0800 Subject: [PATCH 16/30] android: prevent NPE due to wrong typing of NFC API (fix: #220) Signed-off-by: Shengqi Chen --- .../main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt index 49095c3..7507b77 100644 --- a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt +++ b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt @@ -137,7 +137,9 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { type = "iso7816" val isoDep = IsoDep.get(tag) tagTechnology = isoDep - historicalBytes = isoDep.historicalBytes.toHexString() + // historicalBytes() may return null but is wrongly typed as ByteArray! + // https://developer.android.com/reference/kotlin/android/nfc/tech/IsoDep#gethistoricalbytes + historicalBytes = (isoDep.historicalBytes as ByteArray?)?.toHexString() ?: "" } tag.techList.contains(MifareClassic::class.java.name) -> { standard = "ISO 14443-3 (Type A)" From 83e07fe18ad4d0120f2bcba62d9f0b3929554694 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 17:55:48 +0800 Subject: [PATCH 17/30] doc: add comments on samsung bug (see: #190, #200) Signed-off-by: Shengqi Chen --- lib/flutter_nfc_kit.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index c051837..fc18b39 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -313,6 +313,8 @@ class FlutterNfcKit { /// /// The four boolean flags [readIso14443A], [readIso14443B], [readIso18092], [readIso15693] control the NFC technology that would be tried. /// On iOS, setting any of [readIso14443A] and [readIso14443B] will enable `iso14443` in `pollingOption`. + /// On Samsung Android devices, you may need to have [readIso18092] set to read other types of cards (e.g. Felica). + /// See for detailed discussion. /// /// 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). From 6c44757b163f70c495d6f97769208fd4fbcaf636 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 18:03:04 +0800 Subject: [PATCH 18/30] android: lower minSdkVersion to 24 (see: #212) Signed-off-by: Shengqi Chen --- README.md | 2 +- android/build.gradle | 2 +- example/android/app/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c6d328a..816689a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ We have the following minimum version requirements for Android plugin: * Java 17 * Gradle 8.9 -* Android SDK 26 (you must set corresponding `jvmTarget` in you app's `build.gradle`) +* Android SDK 24 (you must set corresponding `jvmTarget` in you app's `build.gradle`) * Android Gradle Plugin 8.7 To use this plugin on Android, you also need to: diff --git a/android/build.gradle b/android/build.gradle index 374d28d..3fca3ea 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -34,7 +34,7 @@ android { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 26 + minSdkVersion 24 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 35f4d98..b2b3f17 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { applicationId "im.nfc.flutter_nfc_kit_example" - minSdkVersion 26 + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName From e7fd467db26ed0811fd59c8403cafe17df0acd4c Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 18:03:40 +0800 Subject: [PATCH 19/30] build: bump to gradle 9.2.1, AGP 8.13.0, Kotlin 2.2.21 Signed-off-by: Shengqi Chen --- android/gradle.properties | 4 ++-- android/gradle/wrapper/gradle-wrapper.properties | 3 +-- example/android/gradle.properties | 4 ++-- example/android/gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index c800c46..16d6682 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx4096M +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.enableJetifier=true AGPVersion=8.13.0 -KotlinVersion=2.2.10 +KotlinVersion=2.2.21 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c9c48b5..547f883 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Sep 08 22:01:14 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 0977159..5eb1ab1 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx4096M +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true @@ -7,4 +7,4 @@ android.nonTransitiveRClass=false android.nonFinalResIds=false AGPVersion=8.13.0 -KotlinVersion=2.2.10 +KotlinVersion=2.2.21 diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index d054919..e4d69a7 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip From cd1163384d6a5667d88f03f9bd0ed2c00095d05b Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 18:35:34 +0800 Subject: [PATCH 20/30] example: add a missing newline Signed-off-by: Shengqi Chen --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4c8156a..0d460ca 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -158,7 +158,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { padding: const EdgeInsets.symmetric(horizontal: 20), child: _tag != null ? Text( - 'ID: ${_tag!.id}\nStandard: ${_tag!.standard}\nType: ${_tag!.type}\nATQA: ${_tag!.atqa}\nSAK: ${_tag!.sak}\nHistorical Bytes: ${_tag!.historicalBytes}\nProtocol Info: ${_tag!.protocolInfo}\nApplication Data: ${_tag!.applicationData}\nHigher Layer Response: ${_tag!.hiLayerResponse}\nManufacturer: ${_tag!.manufacturer}\nSystem Code: ${_tag!.systemCode}\nDSF ID: ${_tag!.dsfId}\nNDEF Available: ${_tag!.ndefAvailable}\nNDEF Type: ${_tag!.ndefType}\nNDEF Writable: ${_tag!.ndefWritable}\nNDEF Can Make Read Only: ${_tag!.ndefCanMakeReadOnly}\nNDEF Capacity: ${_tag!.ndefCapacity}\nMifare Info:${_tag!.mifareInfo} Transceive Result:\n$_result\n\nBlock Message:\n$_mifareResult') + 'ID: ${_tag!.id}\nStandard: ${_tag!.standard}\nType: ${_tag!.type}\nATQA: ${_tag!.atqa}\nSAK: ${_tag!.sak}\nHistorical Bytes: ${_tag!.historicalBytes}\nProtocol Info: ${_tag!.protocolInfo}\nApplication Data: ${_tag!.applicationData}\nHigher Layer Response: ${_tag!.hiLayerResponse}\nManufacturer: ${_tag!.manufacturer}\nSystem Code: ${_tag!.systemCode}\nDSF ID: ${_tag!.dsfId}\nNDEF Available: ${_tag!.ndefAvailable}\nNDEF Type: ${_tag!.ndefType}\nNDEF Writable: ${_tag!.ndefWritable}\nNDEF Can Make Read Only: ${_tag!.ndefCanMakeReadOnly}\nNDEF Capacity: ${_tag!.ndefCapacity}\nMifare Info:${_tag!.mifareInfo}\nTransceive Result:\n$_result\n\nBlock Message:\n$_mifareResult') : const Text('No tag polled yet.')), ])))), Center( From 4e58f7cc9f146dd7146aa34a301791e58817f61d Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 18:39:39 +0800 Subject: [PATCH 21/30] example: update web example to use latest bootstrap js Signed-off-by: Shengqi Chen --- example/web/index.html | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/example/web/index.html b/example/web/index.html index 776346a..17556cd 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -1,16 +1,6 @@ - @@ -26,20 +16,12 @@ - flutter_web_plugin_test_example + Flutter NFC Kit Example - - - + From 0cf98929d6bfbf156a9eb63f4ed504169bc9752d Mon Sep 17 00:00:00 2001 From: Harry Chen Date: Sat, 22 Nov 2025 14:00:00 -0600 Subject: [PATCH 22/30] feat: return empty tag stream on non-Android device Signed-off-by: Harry Chen --- lib/flutter_nfc_kit.dart | 6 +++++- pubspec.yaml | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index fc18b39..1df0778 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -7,6 +7,7 @@ import 'package:ndef/ndef.dart' as ndef; import 'package:ndef/ndef.dart' show TypeNameFormat; // for generated file import 'package:ndef/utilities.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:universal_platform/universal_platform.dart'; part 'flutter_nfc_kit.g.dart'; @@ -277,7 +278,7 @@ class FlutterNfcKit { static const MethodChannel _channel = MethodChannel('flutter_nfc_kit/method'); - static const EventChannel _tagEventChannel = + static final EventChannel _tagEventChannel = EventChannel('flutter_nfc_kit/event'); /// Stream of NFC tag events. Each event is a [NFCTag] object. @@ -285,6 +286,9 @@ class FlutterNfcKit { /// This is only supported on Android. /// On other platforms, this stream will always be empty. static Stream get tagStream { + if (!UniversalPlatform.isAndroid) { + return const Stream.empty(); + } return _tagEventChannel.receiveBroadcastStream().map((dynamic event) { final Map json = jsonDecode(event as String); return NFCTag.fromJson(json); diff --git a/pubspec.yaml b/pubspec.yaml index cf01a91..75a2ef1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_nfc_kit description: Provide NFC functionality on Android, iOS & Web, including reading metadata, read & write NDEF records, and transceive layer 3 & 4 data with NFC tags / cards -version: 3.6.0 +version: 3.6.1 homepage: "https://github.com/nfcim/flutter_nfc_kit" environment: @@ -17,6 +17,7 @@ dependencies: convert: ^3.1.1 logging: ^1.2.0 web: ^1.1.1 + universal_platform: ^1.1.0 dev_dependencies: flutter_test: From dd7519d3d9b74844cad7cfb2f79adaada1c6ffb8 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 18:57:05 +0800 Subject: [PATCH 23/30] Bump to v3.6.1 Signed-off-by: Shengqi Chen --- CHANGELOG.md | 11 +++++++++++ example/pubspec.lock | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2127a..a917d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,3 +191,14 @@ * Add Swift package manager support for iOS plugin, bump dependencies * Fix WebUSB interop on Web, add onDisconnect callback * Add support for foreground polling on Android (#16, #179) + +## 3.6.1 + +* Refactor with new `dart:js_interop` APIs to fix build with WASM (#223) +* Support `readBlock` / `writeBlock` on Mifare tags on iOS (#205 by @rushank-shah) +* More robust logic on Android + * ensure NFC Handler is always alive (#219 by @Akshya107) + * prevent an NPE due to wrong API typing (#220) + * add comment on `poll` related to Samsung API bug (#190, #200) +* Bump tools to Gradle 9.2.1, AGP 8.13.1, Kotlin 2.2.21 + * Now `minSdkVersion` is lowered to 24 (#212) diff --git a/example/pubspec.lock b/example/pubspec.lock index 13dcaee..5cde388 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -92,7 +92,7 @@ packages: path: ".." relative: true source: path - version: "3.6.0" + version: "3.6.1" flutter_test: dependency: "direct dev" description: flutter @@ -244,6 +244,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" uuid: dependency: transitive description: From 91302241f8a23ab82ec228b4f06640876c6eda4a Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 18:59:58 +0800 Subject: [PATCH 24/30] ci: run dart doc and pana before building APK Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index 5b2709e..9cc9cda 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -32,6 +32,16 @@ jobs: - run: flutter pub get working-directory: example/ #- run: flutter test + - run: dart doc + - name: Upload generated dartdoc + uses: actions/upload-artifact@v4 + with: + name: docs + path: doc/ + - name: Evaluate score with pana + run: | + dart pub global activate pana + dart pub global run pana . - run: flutter build apk --${{ matrix.variant }} --verbose working-directory: example/ - uses: actions/upload-artifact@v5 From f9bd7f49dfdb37cfc625edd901d5f28982c43b79 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 19:11:26 +0800 Subject: [PATCH 25/30] ci: split test and build example app Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 66 ++++++++++++++----------------- .github/workflows/test.yml | 36 +++++++++++++++++ README.md | 3 +- 3 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index 9cc9cda..bc079e1 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -8,17 +8,34 @@ on: jobs: build: - runs-on: ubuntu-latest if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} strategy: fail-fast: false matrix: variant: [debug, release] + target: [apk, ios] + include: + - target: apk + os: ubuntu-latest + pre-build-script: "" + build-args: "" + debug-artifact-path: build/app/outputs/flutter-apk/app-debug.apk + release-artifact-path: build/app/outputs/flutter-apk/app-release.apk + - target: ios + os: macos-latest + pre-build-script: "" + build-args: "--no-codesign" + artifact-path: | + build/ios/iphoneos/Runner.app + + runs-on: ${{ matrix.os }} + name: ${{ matrix.target }}-${{ matrix.variant }} steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 + if: ${{ matrix.target == 'apk' }} with: distribution: 'temurin' java-version: '21' @@ -26,45 +43,20 @@ jobs: with: channel: 'stable' cache: true - - run: dart pub get - - run: dart format --output=none --set-exit-if-changed . - - run: dart analyze + + - name: pre-build-script script for ${{ matrix.target }} + run: ${{ matrix.pre-build-script }} + - run: flutter pub get working-directory: example/ - #- run: flutter test - - run: dart doc - - name: Upload generated dartdoc - uses: actions/upload-artifact@v4 - with: - name: docs - path: doc/ - - name: Evaluate score with pana - run: | - dart pub global activate pana - dart pub global run pana . - - run: flutter build apk --${{ matrix.variant }} --verbose + + - name: Run flutter ${{ matrix.variant }} build on ${{ matrix.target }} + run: flutter build ${{ matrix.target }} --${{ matrix.variant }} ${{ matrix.build-args }} --verbose working-directory: example/ + - uses: actions/upload-artifact@v5 with: - name: example-apk-${{ matrix.variant }} + name: example-${{ matrix.target }}-${{ matrix.variant }} path: | - example/build/app/outputs/flutter-apk/app-${{ matrix.variant }}.apk - example/build/reports/* - - build-ios: - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - variant: [debug, release] - - steps: - - uses: actions/checkout@v5 - - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - cache: true - - run: flutter pub get - working-directory: example/ - - run: flutter build ios --${{ matrix.variant }} --verbose --no-codesign - working-directory: example/ + example/${{ matrix.debug-artifact-path }} + example/${{ matrix.release-artifact-path }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0b43cb9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test + +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + test: + runs-on: ubuntu-latest + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + matrix: + channel: [stable, beta] + + steps: + - uses: actions/checkout@v6 + - uses: subosito/flutter-action@v2 + with: + channel: ${{ matrix.channel }} + cache: true + - run: dart pub get + - run: dart format --output=none --set-exit-if-changed lib/ + - run: dart analyze + # - run: dart test + - run: dart doc + - name: Upload generated dartdoc + uses: actions/upload-artifact@v5 + with: + name: docs-${{ matrix.channel }} + path: doc/ + - name: Evaluate score with pana + run: | + dart pub global activate pana + dart pub global run pana . diff --git a/README.md b/README.md index 816689a..03f9546 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Flutter NFC Kit [![pub version](https://img.shields.io/pub/v/flutter_nfc_kit)](https://pub.dev/packages/flutter_nfc_kit) -![Build Example App](https://github.com/nfcim/flutter_nfc_kit/workflows/Build%20Example%20App/badge.svg) +[![Test](https://github.com/nfcim/flutter_nfc_kit/actions/workflows/test.yml/badge.svg)](https://github.com/nfcim/flutter_nfc_kit/actions/workflows/test.yml) +[![Build Example App](https://github.com/nfcim/flutter_nfc_kit/actions/workflows/example-app.yml/badge.svg)](https://github.com/nfcim/flutter_nfc_kit/actions/workflows/example-app.yml) Yet another plugin to provide NFC functionality on Android, iOS and browsers (by WebUSB, see below). From 94059a82a0b863cf8267e13d586c2fccec3580b5 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 19:12:20 +0800 Subject: [PATCH 26/30] ci: remove hardcoded flutter version in publish.yaml Signed-off-by: Shengqi Chen --- .github/workflows/publish.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ed5c711..d38aabf 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,8 +1,5 @@ name: Publish to pub.dev -env: - FLUTTER_VERSION: 3.38.3 - on: push: tags: @@ -21,7 +18,6 @@ jobs: uses: flutter-actions/setup-flutter@v4 with: channel: stable - version: ${{ env.FLUTTER_VERSION }} cache: true - name: Install dependencies run: flutter pub get From 3e5d62ce148e634ac777fc33ad152e0cab4afba7 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 19:28:14 +0800 Subject: [PATCH 27/30] fix: apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 29 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a917d77..94cfb0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -200,5 +200,5 @@ * ensure NFC Handler is always alive (#219 by @Akshya107) * prevent an NPE due to wrong API typing (#220) * add comment on `poll` related to Samsung API bug (#190, #200) -* Bump tools to Gradle 9.2.1, AGP 8.13.1, Kotlin 2.2.21 +* Bump tools to Gradle 9.2.1, AGP 8.13.0, Kotlin 2.2.21 * Now `minSdkVersion` is lowered to 24 (#212) 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 2bb6254..1724131 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -212,16 +212,16 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe } } else if case let .miFare(tag) = tag { - let blockNumber = arguments["index"] as! UInt8 - let commandPacket = Data([0x30, blockNumber]) //0x30 is the MIFARE Classic Read Command. - tag.sendMiFareCommand(commandPacket: commandPacket) { (data, error) in - if let error = error { - result(FlutterError(code: "405", message: "Something is wrong", details: nil)) - } else { - result(data) - } - } - } + let blockNumber = arguments["index"] as! UInt8 + let commandPacket = Data([0x30, blockNumber]) //0x30 is the MIFARE Classic Read Command. + tag.sendMiFareCommand(commandPacket: commandPacket) { (data, error) in + if let error = error { + result(FlutterError(code: "405", message: "Something is wrong", details: nil)) + } else { + result(data) + } + } + } else { result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) } @@ -251,14 +251,13 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let writeCommand = Data([0xA2, blockNumber]) + data //0xA2 is the MIFARE Classic Write Command to write single block. tag.sendMiFareCommand(commandPacket: writeCommand) { (response, error) in if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: nil)) + result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) } - else - { + else { result(nil) - } + } } - } + } else { result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) } From fd9be8109e3e8146fa7f30eac84ed44d9bee21df Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 19:44:34 +0800 Subject: [PATCH 28/30] ios: address more review comments, add more detailed error message Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 2 +- CHANGELOG.md | 1 + .../flutter_nfc_kit/FlutterNfcKitPlugin.swift | 43 +++++++++++-------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index bc079e1..41e35f8 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -44,7 +44,7 @@ jobs: channel: 'stable' cache: true - - name: pre-build-script script for ${{ matrix.target }} + - name: pre-build-script for ${{ matrix.target }} run: ${{ matrix.pre-build-script }} - run: flutter pub get diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cfb0b..8e20731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -200,5 +200,6 @@ * ensure NFC Handler is always alive (#219 by @Akshya107) * prevent an NPE due to wrong API typing (#220) * add comment on `poll` related to Samsung API bug (#190, #200) +* Add more detailed error message in iOS APIs * Bump tools to Gradle 9.2.1, AGP 8.13.0, Kotlin 2.2.21 * Now `minSdkVersion` is lowered to 24 (#212) 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 1724131..3993122 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -108,12 +108,12 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe case let .iso7816(tag): let apdu: NFCISO7816APDU? = NFCISO7816APDU(data: data) if apdu == nil { - result(FlutterError(code: "400", message: "Command format error", details: nil)) + result(FlutterError(code: "400", message: "APDU format error", details: nil)) return } tag.sendCommand(apdu: apdu!) { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?) in if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + result(FlutterError(code: "500", message: "Communication error with iso7816 tag", details: error.localizedDescription)) } else { var response = response response.append(contentsOf: [sw1, sw2]) @@ -133,7 +133,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe // the first byte in data is length, and iOS will add it for us, so skip it tag.sendFeliCaCommand(commandPacket: data.advanced(by: 1)) { (response: Data, error: Error?) in if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + result(FlutterError(code: "500", message: "Communication error with felica tag", details: error.localizedDescription)) } else { if req is String { result(response.hexEncodedString()) @@ -145,7 +145,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe case let .miFare(tag): tag.sendMiFareCommand(commandPacket: data) { (response: Data, error: Error?) in if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + result(FlutterError(code: "500", message: "Communication error with mifare tag", details: error.localizedDescription)) } else { if req is String { result(response.hexEncodedString()) @@ -198,7 +198,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let extendedMode = (arguments["iso15693ExtendedMode"] as? Bool) ?? false let handler = { (dataBlock: Data, error: Error?) in if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + result(FlutterError(code: "500", message: "Cannot read iso15693 tag", details: error.localizedDescription)) } else { result(dataBlock) } @@ -213,10 +213,10 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe } else if case let .miFare(tag) = tag { let blockNumber = arguments["index"] as! UInt8 - let commandPacket = Data([0x30, blockNumber]) //0x30 is the MIFARE Classic Read Command. + let commandPacket = Data([0x30, blockNumber]) // MiFARE Classic / Ultralight READ command tag.sendMiFareCommand(commandPacket: commandPacket) { (data, error) in if let error = error { - result(FlutterError(code: "405", message: "Something is wrong", details: nil)) + result(FlutterError(code: "500", message: "Cannot read mifare tag", details: error.localizedDescription)) } else { result(data) } @@ -233,7 +233,7 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let extendedMode = (arguments["iso15693ExtendedMode"] as? Bool) ?? false let handler = { (error: Error?) in if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + result(FlutterError(code: "500", message: "Cannot write iso15693 tag", details: error.localizedDescription)) } else { result(nil) } @@ -247,19 +247,24 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe } } else if case let .miFare(tag) = tag { - let blockNumber = arguments["index"] as! UInt8 - let writeCommand = Data([0xA2, blockNumber]) + data //0xA2 is the MIFARE Classic Write Command to write single block. - tag.sendMiFareCommand(commandPacket: writeCommand) { (response, error) in - if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) - } - else { - result(nil) - } - } + let blockNumber = arguments["index"] as! UInt8 + let command = switch tag.mifareFamily { + case .ultralight: + 0xA2 // MiFARE Ultralight WRITE command + default: + 0xA0 // MiFARE Classic WRITE command + } as UInt8 + let writeCommand = Data([command, blockNumber]) + data + tag.sendMiFareCommand(commandPacket: writeCommand) { (response, error) in + if let error = error { + result(FlutterError(code: "500", message: "Cannot write mifare tag", details: error.localizedDescription)) + } else { + result(nil) + } + } } else { - result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) + result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) } } else if call.method == "readNDEF" { if tag != nil { From 3c925172482b017d7de2a6d88778455e7ff9d07e Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 20:04:59 +0800 Subject: [PATCH 29/30] android: refine nfc handler logic to prevent thread leaking Signed-off-by: Shengqi Chen --- CHANGELOG.md | 2 +- .../flutter_nfc_kit/FlutterNfcKitPlugin.kt | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e20731..c5717d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -197,7 +197,7 @@ * Refactor with new `dart:js_interop` APIs to fix build with WASM (#223) * Support `readBlock` / `writeBlock` on Mifare tags on iOS (#205 by @rushank-shah) * More robust logic on Android - * ensure NFC Handler is always alive (#219 by @Akshya107) + * ensure NFC Handler is always alive (#219) * prevent an NPE due to wrong API typing (#220) * add comment on `poll` related to Samsung API bug (#190, #200) * Add more detailed error message in iOS APIs diff --git a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt index 7507b77..e54c4c3 100644 --- a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt +++ b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt @@ -71,6 +71,13 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { return transceiveMethod.invoke(this, data) as ByteArray } + private fun ensureNfcHandler() { + if (!::nfcHandlerThread.isInitialized || !nfcHandlerThread.isAlive) { + nfcHandlerThread = HandlerThread("FlutterNfcKit-NfcHandlerThread").apply { start() } + nfcHandler = Handler(nfcHandlerThread.looper) + } + } + private fun runOnNfcThread(result: Result, desc: String, fn: () -> Unit) { val handledFn = Runnable { try { @@ -89,13 +96,8 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } - val looperThread = nfcHandler.looper?.thread - if (looperThread == null || !looperThread.isAlive) { - val thread = HandlerThread("FlutterNfcKit").apply { start() } - nfcHandler = Handler(thread.looper) - } - val posted = nfcHandler.post(handledFn) - if (!posted) { + ensureNfcHandler() + if (!nfcHandler.post(handledFn)) { result.error("500", "Failed to post job to NFC Handler thread.", null) } } @@ -249,9 +251,7 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - nfcHandlerThread = HandlerThread("NfcHandlerThread") - nfcHandlerThread.start() - nfcHandler = Handler(nfcHandlerThread.looper) + ensureNfcHandler() methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/method") methodChannel.setMethodCallHandler(this) From 064649350a59a91c41d979866c1f9de58f22ef3b Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Sat, 22 Nov 2025 20:13:51 +0800 Subject: [PATCH 30/30] ci: resolve more review comments Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 12 +++++++----- .github/workflows/test.yml | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index 41e35f8..13a840d 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -20,14 +20,14 @@ jobs: os: ubuntu-latest pre-build-script: "" build-args: "" - debug-artifact-path: build/app/outputs/flutter-apk/app-debug.apk - release-artifact-path: build/app/outputs/flutter-apk/app-release.apk + debug-artifact-path: example/build/app/outputs/flutter-apk/app-debug.apk + release-artifact-path: example/build/app/outputs/flutter-apk/app-release.apk - target: ios os: macos-latest pre-build-script: "" build-args: "--no-codesign" artifact-path: | - build/ios/iphoneos/Runner.app + example/build/ios/iphoneos/Runner.app runs-on: ${{ matrix.os }} name: ${{ matrix.target }}-${{ matrix.variant }} @@ -46,6 +46,7 @@ jobs: - name: pre-build-script for ${{ matrix.target }} run: ${{ matrix.pre-build-script }} + if: ${{ matrix.pre-build-script != '' }} - run: flutter pub get working-directory: example/ @@ -58,5 +59,6 @@ jobs: with: name: example-${{ matrix.target }}-${{ matrix.variant }} path: | - example/${{ matrix.debug-artifact-path }} - example/${{ matrix.release-artifact-path }} + ${{ matrix.debug-artifact-path }} + ${{ matrix.release-artifact-path }} + ${{ matrix.artifact-path }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b43cb9..8527b95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,8 +18,8 @@ jobs: - uses: actions/checkout@v6 - uses: subosito/flutter-action@v2 with: - channel: ${{ matrix.channel }} - cache: true + channel: ${{ matrix.channel }} + cache: true - run: dart pub get - run: dart format --output=none --set-exit-if-changed lib/ - run: dart analyze