Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
459bced
Enable read block for iOS MIFARE Classic
rushank-shah Feb 2, 2025
46380e0
Updated Readme.
rushank-shah Feb 2, 2025
ec54bef
Enable write block for iOS MIFARE Classic
rushank-shah Feb 2, 2025
6bf8b46
Updated readme
rushank-shah Feb 2, 2025
c313e54
Code Refactor
rushank-shah Feb 3, 2025
1284d67
Merge pull request #205 from rushank-shah/develop
jiegec Jul 8, 2025
6ca4f7d
Fix: Ensure NFC Handler thread is alive for Android SDK 35
Akshyappinventiv Sep 4, 2025
a8c117b
fix: add error handling for failed NFC Handler.post
Akshyappinventiv Sep 4, 2025
16e5532
Merge pull request #219 from Akshya107/fix/nfc-handler-dead-thread
Harry-Chen Sep 5, 2025
c88a240
ci: bump action version
Harry-Chen Sep 5, 2025
5ec3f6f
build: bump to Gradle 9.0.0, AGP 8.13.0, Kotlin 2.2.10
Harry-Chen Sep 5, 2025
bd14f82
build: increase max Java heap to avoid OOM when building debug variant
Harry-Chen Sep 5, 2025
c0fe665
web: refactor with dart:js_interop (fix #223)
Harry-Chen Nov 22, 2025
33235cc
ci: bump versions of actions
Harry-Chen Nov 22, 2025
e38f6e1
chore: run dart fmt
Harry-Chen Nov 22, 2025
cee698e
example: bump dependencies
Harry-Chen Nov 22, 2025
0e13120
doc: fix typo in README (fix: #221)
Harry-Chen Nov 22, 2025
506e84e
android: prevent NPE due to wrong typing of NFC API (fix: #220)
Harry-Chen Nov 22, 2025
83e07fe
doc: add comments on samsung bug (see: #190, #200)
Harry-Chen Nov 22, 2025
6c44757
android: lower minSdkVersion to 24 (see: #212)
Harry-Chen Nov 22, 2025
e7fd467
build: bump to gradle 9.2.1, AGP 8.13.0, Kotlin 2.2.21
Harry-Chen Nov 22, 2025
cd11633
example: add a missing newline
Harry-Chen Nov 22, 2025
4e58f7c
example: update web example to use latest bootstrap js
Harry-Chen Nov 22, 2025
0cf9892
feat: return empty tag stream on non-Android device
Harry-Chen Nov 22, 2025
dd7519d
Bump to v3.6.1
Harry-Chen Nov 22, 2025
9130224
ci: run dart doc and pana before building APK
Harry-Chen Nov 22, 2025
f9bd7f4
ci: split test and build example app
Harry-Chen Nov 22, 2025
94059a8
ci: remove hardcoded flutter version in publish.yaml
Harry-Chen Nov 22, 2025
3e5d62c
fix: apply suggestions from code review
Harry-Chen Nov 22, 2025
fd9be81
ios: address more review comments, add more detailed error message
Harry-Chen Nov 22, 2025
3c92517
android: refine nfc handler logic to prevent thread leaking
Harry-Chen Nov 22, 2025
0646493
ci: resolve more review comments
Harry-Chen Nov 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 34 additions & 30 deletions .github/workflows/example-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,57 @@ 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: 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: |
example/build/ios/iphoneos/Runner.app

runs-on: ${{ matrix.os }}
name: ${{ matrix.target }}-${{ matrix.variant }}

steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
if: ${{ matrix.target == 'apk' }}
with:
distribution: 'temurin'
java-version: '21'
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- run: dart pub get
- run: dart format --output=none --set-exit-if-changed .
- run: dart analyze
- run: flutter pub get
working-directory: example/
#- run: flutter test
- run: flutter build apk --${{ matrix.variant }} --verbose
working-directory: example/
- uses: actions/upload-artifact@v4
with:
name: example-apk-${{ 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]
- name: pre-build-script for ${{ matrix.target }}
run: ${{ matrix.pre-build-script }}
if: ${{ matrix.pre-build-script != '' }}

steps:
- uses: actions/checkout@v4
- 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

- 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-${{ matrix.target }}-${{ matrix.variant }}
path: |
${{ matrix.debug-artifact-path }}
${{ matrix.release-artifact-path }}
${{ matrix.artifact-path }}
8 changes: 2 additions & 6 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
name: Publish to pub.dev

env:
FLUTTER_VERSION: 3.24.5

on:
push:
tags:
Expand All @@ -15,13 +12,12 @@ 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 }}
cache: true
- name: Install dependencies
run: flutter pub get
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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 .
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,15 @@
* 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)
* 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)
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -10,9 +11,9 @@ 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 / 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)
Expand All @@ -35,7 +36,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:
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ android {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 26
minSdkVersion 24
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand Down
6 changes: 3 additions & 3 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.enableJetifier=true
AGPVersion=8.7.3
KotlinVersion=2.1.0
AGPVersion=8.13.0
KotlinVersion=2.2.21
3 changes: 1 addition & 2 deletions android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -89,6 +96,7 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
}
}
ensureNfcHandler()
if (!nfcHandler.post(handledFn)) {
result.error("500", "Failed to post job to NFC Handler thread.", null)
}
Expand Down Expand Up @@ -131,7 +139,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)"
Expand Down Expand Up @@ -241,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)
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions example/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError

android.useAndroidX=true
android.enableJetifier=true
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.21
2 changes: 1 addition & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.1-bin.zip
2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class _MyAppState extends State<MyApp> 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(
Expand Down
Loading