├── .github └── workflows │ ├── example-app.yml │ └── publish.yaml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── WebUSB.md ├── analysis_options.yaml ├── android ├── .classpath ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── im │ └── nfc │ └── flutter_nfc_kit │ ├── ByteUtils.kt │ ├── FlutterNfcKitPlugin.kt │ └── MifareUtils.kt ├── example ├── .gitignore ├── .metadata ├── .pubignore ├── android │ ├── .gitignore │ ├── app │ │ ├── .classpath │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── im │ │ │ │ │ └── nfc │ │ │ │ │ └── flutter_nfc_kit │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── settings_aar.gradle ├── example.md ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── Runner-Bridging-Header.h │ │ └── Runner.entitlements ├── lib │ ├── main.dart │ └── ndef_record │ │ ├── raw_record_setting.dart │ │ ├── text_record_setting.dart │ │ └── uri_record_setting.dart ├── pubspec.lock ├── pubspec.yaml └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── ios ├── .gitignore ├── flutter_nfc_kit.podspec └── flutter_nfc_kit │ ├── .gitignore │ ├── Package.swift │ └── Sources │ └── flutter_nfc_kit │ └── FlutterNfcKitPlugin.swift ├── lib ├── flutter_nfc_kit.dart ├── flutter_nfc_kit.g.dart ├── flutter_nfc_kit_web.dart └── webusb_interop.dart └── pubspec.yaml /.github/workflows/example-app.yml: -------------------------------------------------------------------------------- 1 | name: Build Example App 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | pull_request: 7 | branches: [master, develop] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | variant: [debug, release] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-java@v4 22 | with: 23 | distribution: 'temurin' 24 | java-version: '21' 25 | - uses: subosito/flutter-action@v2 26 | with: 27 | channel: 'stable' 28 | cache: true 29 | - run: dart pub get 30 | - run: dart format --output=none --set-exit-if-changed . 31 | - run: dart analyze 32 | - run: flutter pub get 33 | working-directory: example/ 34 | #- run: flutter test 35 | - run: flutter build apk --${{ matrix.variant }} --verbose 36 | working-directory: example/ 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: example-apk-${{ matrix.variant }} 40 | path: | 41 | example/build/app/outputs/flutter-apk/app-${{ matrix.variant }}.apk 42 | example/build/reports/* 43 | 44 | build-ios: 45 | runs-on: macos-latest 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | variant: [debug, release] 50 | 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: subosito/flutter-action@v2 54 | with: 55 | channel: 'stable' 56 | cache: true 57 | - run: flutter pub get 58 | working-directory: example/ 59 | - run: flutter build ios --${{ matrix.variant }} --verbose --no-codesign 60 | working-directory: example/ 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | env: 4 | FLUTTER_VERSION: 3.24.5 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v[0-9]+.[0-9]+.[0-9]+*' 10 | 11 | jobs: 12 | publish: 13 | name: 'Publish to pub.dev' 14 | permissions: 15 | id-token: write 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: dart-lang/setup-dart@v1 20 | - name: Setup Flutter SDK 21 | uses: flutter-actions/setup-flutter@v3 22 | with: 23 | channel: stable 24 | version: ${{ env.FLUTTER_VERSION }} 25 | cache: true 26 | - name: Install dependencies 27 | run: flutter pub get 28 | - name: Publish - dry run 29 | run: flutter pub publish --dry-run 30 | - name: Publish to pub.dev 31 | run: flutter pub publish -f 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .idea 10 | .project 11 | .settings 12 | 13 | *.iml 14 | 15 | doc 16 | pubspec.lock 17 | !example/pubspec.lock 18 | 19 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * Initial release 4 | * Support reading metadata of cards of standard ISO 14443-4 Type A & Type B (NFC-A / NFC-B / Mifare Classic / Mifare Ultralight) 5 | * Support transceiving APDU with cards of standard ISO 7816 6 | 7 | ## 0.0.2 8 | 9 | * Support reading metadata of cards of standard ISO 18092 / JIS 6319 (NFC-F / Felica) 10 | * Support reading metadata of cards of standard ISO 15963 (NFC-V) 11 | * Support reading GUID of China ID Card (which is non-standard) 12 | * Add more documentation 13 | * Release with MIT License 14 | 15 | ## 0.0.3 16 | 17 | * Fix compilation errors of iOS plugin 18 | 19 | ## 0.0.4 20 | 21 | * Fix IsoDep unintentionally closing of the Android plugin 22 | * Fix incorrect standards description 23 | 24 | ## 0.0.5 25 | 26 | * Fix finish method blocking error 27 | 28 | ## 0.0.6 29 | 30 | * Avoid returning redundant result if user finish the read operation prematurely 31 | 32 | ## 0.0.7 33 | 34 | * Allow data transceive on lower layer (Android only) 35 | 36 | ## 1.0.0 37 | 38 | * Remove China ID Card support due to support of lower layer transceiving 39 | * Fix some racing problem on iOS 40 | * We are out-of beta and releasing on time! 41 | 42 | ## 1.0.1 43 | 44 | * Fix IllegalStateException & Add MethodResultWrapper (Thanks to @smlu) 45 | 46 | ## 1.0.2 47 | 48 | * Remove redundant code in Android plugin 49 | * Format dart code 50 | 51 | ## 1.1.0 52 | 53 | * Add NFC session alert/error message on iOS (Thanks to @smlu) 54 | * Support execution timeout as optional parameter on Android for `poll` and `transceive` (Thanks to @smlu) 55 | * Accept command in the type of hex string / `UInt8List` in `transceive` and return in the same type (Thanks to @smlu) 56 | 57 | ## 1.2.0 58 | 59 | * Add support for NFC 18902 on iOS 60 | * Add initial NDEF support (read only, no decoding) 61 | * Allow disabling platform sound on Android when polling card 62 | 63 | ## 2.0.0 64 | 65 | * Switch to [ndef](https://pub.dev/packages/ndef) for NDEF record encoding & decoding (breaking API change) 66 | * Support writing NDEF records 67 | * Add NDEF writing in example app 68 | 69 | ## 2.0.1 70 | 71 | * Fix compiling problem on iOS 72 | 73 | ## 2.0.2 74 | 75 | * Fix format of CHANGELOG.md 76 | * Format source code to pass static analysis 77 | 78 | ## 2.1.0 79 | 80 | * Update to latest version of `ndef` package 81 | * Update example app on writing NDEF records 82 | 83 | ## 2.2.0 84 | 85 | * Allow specifying needed NFC technologies in `poll` (fix [#15](https://github.com/nfcim/flutter_nfc_kit/issues/15)) 86 | 87 | ## 2.2.1 88 | 89 | * Disable ISO 18092 in `poll` by default due to iOS CoreNFC bug (see [#23](https://github.com/nfcim/flutter_nfc_kit/issues/23)) 90 | * Bump dependencies & fix some deprecation warnings 91 | 92 | ## 3.0.0 93 | 94 | * Upgrade to Flutter 2.0, add null-safety support for APIs 95 | * Bump dependencies (`ndef` to 0.3.1) 96 | 97 | ## 3.0.1 98 | 99 | * Remove errorous non-null assertion in `ndef.NDEFRecord.toRaw()` extension method & fix example app (#38) 100 | 101 | ## 3.0.2 102 | 103 | * Fix incorrect flags passed by `poll` method to Android NFC API (#42) 104 | 105 | ## 3.1.0 106 | 107 | * Fix inappropriate non-null type in fields of `NFCTag` (#43) 108 | 109 | ## 3.2.0 110 | 111 | * Add `makeNdefReadOnly` (#53, thanks to @timnew) 112 | * Avoid NFC API calls to block the main thread on Android (#54, thanks to @cyberbobs) 113 | * Bump dependencies of Android plugin and example App (esp. Kotlin 1.6.0) 114 | * Exclude example app in published version to reduce package size 115 | 116 | ## 3.3.0 117 | 118 | * Add Web support using own WebUSB protocol (see documentation for detail) 119 | * Bump dependencies (esp. Kotlin 1.6.21 and SDK 31) of Android plugin and example App to fix build error (#55) 120 | * Distinguish session canceled and session timeout (#58, #59, thanks to @timnew) 121 | * Minor error fixes (#61, #63, #64, #65, #71, #72, many of them come from @timnew) 122 | 123 | ## 3.3.1 124 | 125 | * Downgrade dependency `js` to 0.6.3 to maintain backward compatibility with Flutter 2 (#74) 126 | 127 | ## 3.3.2 128 | 129 | * Upgrade to Flutter 3, bump dependencies, change target SDK of Android to 33 130 | * Fix multiple issues (#88, #90, #102) 131 | 132 | ## 3.3.3 133 | 134 | * Fix build script of Android plugin and remove AGP version requirement (#110) 135 | 136 | ## 3.4.0 137 | 138 | * Add support for reading / write MIFARE Classic / Ultralight tags on Android (merged #82, partially fixes #82) 139 | * Add support for reading / write ISO 15693 tags on iOS (merged #117, partially fixes #68) 140 | * Fix compiling issues (#123) 141 | * Other minor fixes (#114, #115) 142 | 143 | ## 3.4.1 144 | 145 | **This version is *deprecated* due to a bug in Mifare tag handling. Please upgrade to 3.4.2.** 146 | 147 | * Fix & split examples to example/ dir 148 | * Publish examples to pub.dev 149 | * Support transceiving of raw ISO15693 commands on iOS 150 | 151 | ## 3.4.2 152 | 153 | * Fix polling error on Mifare tags (#126, #128, #129, #133) 154 | 155 | ## 3.5.0 156 | 157 | * Some FeliCa improvements by @shiwano: 158 | * Fix missing `id` field in FeliCa card reading on iOS (#140) 159 | * Set the IDm to the `id` and the PMm to the `manufacturer` on iOS (#140) 160 | * Add `iosRestartPolling` method by @rostopira (#151) 161 | * Fix type assertion in `authenticateSector` (fix #148) 162 | * Refine exception handling in Android plugin (fix #91 and #149) 163 | * Bump multiple dependencies: 164 | * Android plugin / example app: Java 17, AGP 7.4.2, Kotlin 1.9.23, minSdkVersion 26 (fix #127, #144, #145) 165 | * `js` library: 0.7.1 166 | 167 | ## 3.5.1 168 | 169 | * Fix multiple issues related to `authenticateSector` (#159): 170 | * Fix type checking assertions of arguments 171 | * Add missing call to `connect` in Android plugin 172 | * Add instruction on resolving `js` dependency conflict in README 173 | 174 | ## 3.5.2 175 | 176 | * Some MiFare Classic fixes by @knthm: 177 | * allow authentication of sector 0 (#157) 178 | * fix data type check in `writeBlock` (#161) 179 | 180 | ## 3.6.0 181 | 182 | * Requires Dart 3.6+ and Flutter 3.24+ 183 | * Remove annoying dependency on `js` library, replace with `dart:js_interop` 184 | * Remove dependency on `dart:io` 185 | * Contributions on Android plugin from @knthm: 186 | * Dedicated handler thread for IO operations (#167) 187 | * More elegant exception handling (#169) 188 | * Bump tool versions & dependencies of Android plugin and example app: 189 | * Related issues / PRs: #179 #184, #186, #187 190 | * Now requiring Java 17, Gradle 8.9, MinSDKVer 26, AGP 8.7, Kotlin 2.1.0 191 | * Add Swift package manager support for iOS plugin, bump dependencies 192 | * Fix WebUSB interop on Web, add onDisconnect callback 193 | * Add support for foreground polling on Android (#16, #179) 194 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to `flutter_nfc_kit`! 4 | 5 | ## GitHub Workflow 6 | 7 | We have two main branches: 8 | 9 | * `develop` branch (default) is for actively development and RC releases. 10 | * `master` branch is for stable releases. 11 | 12 | Both branches are protected. Pull requests are needed for any changes. 13 | 14 | Please always make your changes on `develop` branch, and submit your PR against it. 15 | If there are any merge conflicts, please resolve them by rebasing your commits. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nfc.im 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter NFC Kit 2 | 3 | [![pub version](https://img.shields.io/pub/v/flutter_nfc_kit)](https://pub.dev/packages/flutter_nfc_kit) 4 | ![Build Example App](https://github.com/nfcim/flutter_nfc_kit/workflows/Build%20Example%20App/badge.svg) 5 | 6 | Yet another plugin to provide NFC functionality on Android, iOS and browsers (by WebUSB, see below). 7 | 8 | This plugin's functionalities include: 9 | 10 | * read metadata and read & write NDEF records of tags / cards complying with: 11 | * ISO 14443 Type A & Type B (NFC-A / NFC-B / MIFARE Classic / MIFARE Plus / MIFARE Ultralight / MIFARE DESFire) 12 | * ISO 18092 (NFC-F / FeliCa) 13 | * ISO 15963 (NFC-V) 14 | * R/W block / page / sector level data of tags complying with: 15 | * MIFARE Classic / Ultralight (Android only) 16 | * ISO 15693 (iOS only) 17 | * transceive raw commands with tags / cards complying with: 18 | * ISO 7816 Smart Cards (layer 4, in APDUs) 19 | * other device-supported technologies (layer 3, in raw commands, see documentation for platform-specific supportability) 20 | 21 | Note that due to API limitations, not all operations are supported on all platforms. 22 | **You are welcome to submit PRs to add support for any standard-specific operations.** 23 | 24 | This library uses [ndef](https://pub.dev/packages/ndef) for NDEF record encoding & decoding. 25 | 26 | ## Contributing 27 | 28 | Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for more information. 29 | 30 | ## Setup 31 | 32 | ### Android 33 | 34 | We have the following minimum version requirements for Android plugin: 35 | 36 | * Java 17 37 | * Gradle 8.9 38 | * Android SDK 26 (you must set corresponding `jvmTarget` in you app's `build.gradle`) 39 | * Android Gradle Plugin 8.7 40 | 41 | To use this plugin on Android, you also need to: 42 | 43 | * Add [android.permission.NFC](https://developer.android.com/reference/android/Manifest.permission.html#NFC) to your `AndroidManifest.xml`. 44 | 45 | ### iOS 46 | 47 | This plugin now supports Swift package manager, and requires iOS 13+. 48 | 49 | * Add [Near Field Communication Tag Reader Session Formats Entitlements](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_nfc_readersession_formats) to your entitlements. 50 | * Add [NFCReaderUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nfcreaderusagedescription) to your `Info.plist`. 51 | * Add [com.apple.developer.nfc.readersession.felica.systemcodes](https://developer.apple.com/documentation/bundleresources/information_property_list/systemcodes) and [com.apple.developer.nfc.readersession.iso7816.select-identifiers](https://developer.apple.com/documentation/bundleresources/information_property_list/select-identifiers) to your `Info.plist` as needed. WARNING: for iOS 14.5 and earlier versions, you **MUST** add them before invoking `poll` with `readIso18092` or `readIso15693` enabled, or your NFC **WILL BE TOTALLY UNAVAILABLE BEFORE REBOOT** due to a [CoreNFC bug](https://github.com/nfcim/flutter_nfc_kit/issues/23). 52 | * Open Runner.xcworkspace with Xcode and navigate to project settings then the tab _Signing & Capabilities._ 53 | * Select the Runner in targets in left sidebar then press the "+ Capability" in the left upper corner and choose _Near Field Communication Tag Reading._ 54 | 55 | ### Web 56 | 57 | The web version of this plugin **does not actually support NFC** in browsers, but uses a specific [WebUSB protocol](https://github.com/nfcim/flutter_nfc_kit/blob/master/WebUSB.md), so that Flutter programs can communicate with dual-interface (NFC / USB) devices in a platform-independent way. 58 | 59 | Make sure you understand the statement above and the protocol before using this plugin. 60 | 61 | ## Usage 62 | 63 | We provide [simple code example](example/example.md) and a [example application](example/lib). 64 | 65 | Refer to the [documentation](https://pub.dev/documentation/flutter_nfc_kit/) for more information. 66 | 67 | ### Error codes 68 | 69 | We use error codes with similar meaning as HTTP status code. Brief explanation and error cause in string (if available) will also be returned when an error occurs. 70 | 71 | ### Operation Mode 72 | 73 | We provide two operation modes: polling (default) and event streaming. Both can give the same `NFCTag` object. Please see [example](example/example.md) for more details. 74 | -------------------------------------------------------------------------------- /WebUSB.md: -------------------------------------------------------------------------------- 1 | # WebUSB Protocol 2 | 3 | ## Overview 4 | 5 | Since NFC is inaccessible on the web, applications targeted on dual interfaces (NFC and USB) 6 | may use the following protocol to communicate with the WebUSB on Chromium-based web browsers. 7 | 8 | The communication is based on the Control Transfer. The interface with index 1 is used. 9 | 10 | Note: you **need to implement this protocol on your own USB device** before communicating with it using the web version of FlutterNfcKit. 11 | 12 | Currently, the following devices adopt this protocol: 13 | 14 | * [CanoKey](https://www.canokeys.org/) 15 | 16 | ## Messages 17 | 18 | Basically, the messages on the WebUSB interface are APDU commands. 19 | To transceive a pair of APDU commands, two phases are required: 20 | 21 | 1. Send a command APDU 22 | 2. Get the response APDU 23 | 24 | Each type of message is a vendor-specific request, defined as: 25 | 26 | | bRequest | Value | 27 | | -------- | ----- | 28 | | CMD | 00h | 29 | | RESP | 01h | 30 | | STAT | 02h | 31 | | PROBE | FFh | 32 | 33 | 1. Probe device 34 | 35 | The following control pipe request is used to probe whether the device supports this protocol. 36 | 37 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data | 38 | | ------------- | -------- | ------ | ------ | ------- | ---- | 39 | | 11000001B | PROBE | 0000h | 1 | 0 | N/A | 40 | 41 | The response data **MUST** begin with magic bytes `0x5f4e46435f494d5f` (`_NFC_IM_`) in order to be recognized. 42 | The remaining bytes can be used as custom information provided by the device. 43 | 44 | 2. Send command APDU 45 | 46 | The following control pipe request is used to send a command APDU. 47 | 48 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data | 49 | | ------------- | -------- | ------ | ------ | -------------- | ----- | 50 | | 01000001B | CMD | 0000h | 1 | length of data | bytes | 51 | 52 | 3. Get execution status 53 | 54 | The following control pipe request is used to get the status of the device. 55 | 56 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data | 57 | | ------------- | -------- | ------ | ------ | ------- | ---- | 58 | | 11000001B | STAT | 0000h | 1 | 0 | N/A | 59 | 60 | The response data is 1-byte long, `0x01` for in progress and `0x00` for finishing processing, 61 | and you can fetch the result using `RESP` command, and other values for invalid states. 62 | 63 | If the command is still under processing, the response will be empty. 64 | 65 | 4. Get response APDU 66 | 67 | The following control pipe request is used to get the response APDU. 68 | 69 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data | 70 | | ------------- | -------- | ------ | ------ | ------- | ---- | 71 | | 11000001B | RESP | 0000h | 1 | 0 | N/A | 72 | 73 | The device will send the response no more than 1500 bytes. 74 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | linter: 4 | rules: 5 | constant_identifier_names: false 6 | -------------------------------------------------------------------------------- /android/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | allprojects { 7 | repositories { 8 | gradlePluginPortal() 9 | google() 10 | mavenCentral() 11 | } 12 | } 13 | 14 | group 'im.nfc.flutter_nfc_kit' 15 | 16 | android { 17 | 18 | if (project.android.hasProperty("namespace")) { 19 | namespace 'im.nfc.flutter_nfc_kit' 20 | } 21 | 22 | compileSdk 35 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | kotlinOptions { 30 | jvmTarget = JavaVersion.VERSION_1_8 31 | } 32 | 33 | sourceSets { 34 | main.java.srcDirs += 'src/main/kotlin' 35 | } 36 | defaultConfig { 37 | minSdkVersion 26 38 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 39 | } 40 | lintOptions { 41 | disable 'InvalidPackage' 42 | } 43 | } 44 | 45 | dependencies { 46 | } 47 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableJetifier=true 3 | AGPVersion=8.7.3 4 | KotlinVersion=2.1.0 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Sep 08 22:01:14 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | plugins { 8 | id 'com.android.library' version "${AGPVersion}" 9 | id 'org.jetbrains.kotlin.android' version "${KotlinVersion}" 10 | } 11 | } 12 | 13 | rootProject.name = 'flutter_nfc_kit' 14 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/nfc/flutter_nfc_kit/ByteUtils.kt: -------------------------------------------------------------------------------- 1 | package im.nfc.flutter_nfc_kit 2 | 3 | import im.nfc.flutter_nfc_kit.ByteUtils.hexToBytes 4 | import im.nfc.flutter_nfc_kit.ByteUtils.toHexString 5 | 6 | 7 | object ByteUtils { 8 | private const val HEX_CHARS = "0123456789ABCDEF" 9 | private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray() 10 | 11 | fun String.hexToBytes(): ByteArray { 12 | if (length % 2 == 1) throw IllegalArgumentException() 13 | 14 | val result = ByteArray(length / 2) 15 | 16 | val str = this.uppercase() 17 | for (i in 0 until length step 2) { 18 | val firstIndex = HEX_CHARS.indexOf(str[i]) 19 | val secondIndex = HEX_CHARS.indexOf(str[i + 1]) 20 | require(!(firstIndex == -1 || secondIndex == -1)) 21 | val octet = (firstIndex shl 4) or secondIndex 22 | result[i shr 1] = octet.toByte() 23 | } 24 | 25 | return result 26 | } 27 | 28 | fun ByteArray.toHexString(): String { 29 | val result = StringBuffer() 30 | forEach { 31 | result.append(it.toHexString()) 32 | } 33 | return result.toString() 34 | } 35 | 36 | fun Byte.toHexString(): String { 37 | val octet = this.toInt() 38 | val firstIndex = (octet and 0xF0) ushr 4 39 | val secondIndex = octet and 0x0F 40 | return "${HEX_CHARS_ARRAY[firstIndex]}${HEX_CHARS_ARRAY[secondIndex]}" 41 | } 42 | 43 | fun canonicalizeData(data: Any): Pair { 44 | val bytes = when (data) { 45 | is String -> data.hexToBytes() 46 | else -> data as ByteArray 47 | } 48 | val hex = when (data) { 49 | is ByteArray -> data.toHexString() 50 | else -> data as String 51 | } 52 | return Pair(bytes, hex) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt: -------------------------------------------------------------------------------- 1 | package im.nfc.flutter_nfc_kit 2 | 3 | import android.app.Activity 4 | import android.nfc.FormatException 5 | import android.nfc.NdefMessage 6 | import android.nfc.NdefRecord 7 | import android.nfc.NfcAdapter 8 | import android.nfc.NfcAdapter.* 9 | import android.nfc.Tag 10 | import android.nfc.tech.* 11 | import android.os.Handler 12 | import android.os.HandlerThread 13 | import android.os.Looper 14 | import im.nfc.flutter_nfc_kit.ByteUtils.canonicalizeData 15 | import im.nfc.flutter_nfc_kit.ByteUtils.hexToBytes 16 | import im.nfc.flutter_nfc_kit.ByteUtils.toHexString 17 | import im.nfc.flutter_nfc_kit.MifareUtils.readBlock 18 | import im.nfc.flutter_nfc_kit.MifareUtils.readSector 19 | import im.nfc.flutter_nfc_kit.MifareUtils.writeBlock 20 | import io.flutter.Log 21 | import io.flutter.embedding.engine.plugins.FlutterPlugin 22 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 23 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 24 | import io.flutter.plugin.common.MethodCall 25 | import io.flutter.plugin.common.MethodChannel 26 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 27 | import io.flutter.plugin.common.MethodChannel.Result 28 | import io.flutter.plugin.common.EventChannel 29 | import io.flutter.plugin.common.EventChannel.EventSink 30 | import io.flutter.plugin.common.EventChannel.StreamHandler 31 | import org.json.JSONArray 32 | import org.json.JSONObject 33 | import java.io.IOException 34 | import java.lang.ref.WeakReference 35 | import java.lang.reflect.InvocationTargetException 36 | import java.util.* 37 | import kotlin.concurrent.schedule 38 | 39 | 40 | class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { 41 | 42 | companion object { 43 | private val TAG = FlutterNfcKitPlugin::class.java.name 44 | private var activity: WeakReference = WeakReference(null) 45 | private var pollingTimeoutTask: TimerTask? = null 46 | private var tagTechnology: TagTechnology? = null 47 | private var ndefTechnology: Ndef? = null 48 | private var mifareInfo: MifareInfo? = null 49 | 50 | private lateinit var nfcHandlerThread: HandlerThread 51 | private lateinit var nfcHandler: Handler 52 | private lateinit var methodChannel: MethodChannel 53 | private lateinit var eventChannel: EventChannel 54 | private var eventSink: EventSink? = null 55 | 56 | public fun handleTag(tag: Tag) { 57 | val result = parseTag(tag) 58 | Handler(Looper.getMainLooper()).post { 59 | eventSink?.success(result) 60 | } 61 | } 62 | 63 | private fun TagTechnology.transceive(data: ByteArray, timeout: Int?): ByteArray { 64 | if (timeout != null) { 65 | try { 66 | val timeoutMethod = this.javaClass.getMethod("setTimeout", Int::class.java) 67 | timeoutMethod.invoke(this, timeout) 68 | } catch (_: Throwable) {} 69 | } 70 | val transceiveMethod = this.javaClass.getMethod("transceive", ByteArray::class.java) 71 | return transceiveMethod.invoke(this, data) as ByteArray 72 | } 73 | 74 | private fun runOnNfcThread(result: Result, desc: String, fn: () -> Unit) { 75 | val handledFn = Runnable { 76 | try { 77 | fn() 78 | } catch (ex: Exception) { 79 | Log.e(TAG, "$desc error", ex) 80 | val excMessage = ex.localizedMessage 81 | when (ex) { 82 | is IOException -> result?.error("500", "Communication error", excMessage) 83 | is SecurityException -> result?.error("503", "Tag already removed", excMessage) 84 | is FormatException -> result?.error("400", "NDEF format error", excMessage) 85 | is InvocationTargetException -> result?.error("500", "Communication error", excMessage) 86 | is IllegalArgumentException -> result?.error("400", "Command format error", excMessage) 87 | is NoSuchMethodException -> result?.error("405", "Transceive not supported for this type of card", excMessage) 88 | else -> result?.error("500", "Unhandled error", excMessage) 89 | } 90 | } 91 | } 92 | if (!nfcHandler.post(handledFn)) { 93 | result.error("500", "Failed to post job to NFC Handler thread.", null) 94 | } 95 | } 96 | 97 | private fun parseTag(tag: Tag): String { 98 | // common fields 99 | val type: String 100 | val id = tag.id.toHexString() 101 | val standard: String 102 | // ISO 14443 Type A 103 | var atqa = "" 104 | var sak = "" 105 | // ISO 14443 Type B 106 | var protocolInfo = "" 107 | var applicationData = "" 108 | // ISO 7816 109 | var historicalBytes = "" 110 | var hiLayerResponse = "" 111 | // NFC-F / Felica 112 | var manufacturer = "" 113 | var systemCode = "" 114 | // NFC-V 115 | var dsfId = "" 116 | // NDEF 117 | var ndefAvailable = false 118 | var ndefWritable = false 119 | var ndefCanMakeReadOnly = false 120 | var ndefCapacity = 0 121 | var ndefType = "" 122 | 123 | if (tag.techList.contains(NfcA::class.java.name)) { 124 | val aTag = NfcA.get(tag) 125 | atqa = aTag.atqa.toHexString() 126 | sak = byteArrayOf(aTag.sak.toByte()).toHexString() 127 | tagTechnology = aTag 128 | when { 129 | tag.techList.contains(IsoDep::class.java.name) -> { 130 | standard = "ISO 14443-4 (Type A)" 131 | type = "iso7816" 132 | val isoDep = IsoDep.get(tag) 133 | tagTechnology = isoDep 134 | historicalBytes = isoDep.historicalBytes.toHexString() 135 | } 136 | tag.techList.contains(MifareClassic::class.java.name) -> { 137 | standard = "ISO 14443-3 (Type A)" 138 | type = "mifare_classic" 139 | with(MifareClassic.get(tag)) { 140 | tagTechnology = this 141 | mifareInfo = MifareInfo( 142 | this.type, 143 | size, 144 | MifareClassic.BLOCK_SIZE, 145 | blockCount, 146 | sectorCount 147 | ) 148 | } 149 | } 150 | tag.techList.contains(MifareUltralight::class.java.name) -> { 151 | standard = "ISO 14443-3 (Type A)" 152 | type = "mifare_ultralight" 153 | with(MifareUltralight.get(tag)) { 154 | tagTechnology = this 155 | mifareInfo = MifareInfo.fromUltralight(this.type) 156 | } 157 | } 158 | else -> { 159 | standard = "ISO 14443-3 (Type A)" 160 | type = "unknown" 161 | } 162 | } 163 | } else if (tag.techList.contains(NfcB::class.java.name)) { 164 | val bTag = NfcB.get(tag) 165 | protocolInfo = bTag.protocolInfo.toHexString() 166 | applicationData = bTag.applicationData.toHexString() 167 | if (tag.techList.contains(IsoDep::class.java.name)) { 168 | type = "iso7816" 169 | standard = "ISO 14443-4 (Type B)" 170 | val isoDep = IsoDep.get(tag) 171 | tagTechnology = isoDep 172 | hiLayerResponse = isoDep.hiLayerResponse.toHexString() 173 | } else { 174 | type = "unknown" 175 | standard = "ISO 14443-3 (Type B)" 176 | tagTechnology = bTag 177 | } 178 | } else if (tag.techList.contains(NfcF::class.java.name)) { 179 | standard = "ISO 18092 (FeliCa)" 180 | type = "iso18092" 181 | val fTag = NfcF.get(tag) 182 | manufacturer = fTag.manufacturer.toHexString() 183 | systemCode = fTag.systemCode.toHexString() 184 | tagTechnology = fTag 185 | } else if (tag.techList.contains(NfcV::class.java.name)) { 186 | standard = "ISO 15693" 187 | type = "iso15693" 188 | val vTag = NfcV.get(tag) 189 | dsfId = vTag.dsfId.toHexString() 190 | tagTechnology = vTag 191 | } else { 192 | type = "unknown" 193 | standard = "unknown" 194 | } 195 | 196 | // detect ndef 197 | if (tag.techList.contains(Ndef::class.java.name)) { 198 | val ndefTag = Ndef.get(tag) 199 | ndefTechnology = ndefTag 200 | ndefAvailable = true 201 | ndefType = ndefTag.type 202 | ndefWritable = ndefTag.isWritable 203 | ndefCanMakeReadOnly = ndefTag.canMakeReadOnly() 204 | ndefCapacity = ndefTag.maxSize 205 | } 206 | 207 | val jsonResult = JSONObject(mapOf( 208 | "type" to type, 209 | "id" to id, 210 | "standard" to standard, 211 | "atqa" to atqa, 212 | "sak" to sak, 213 | "historicalBytes" to historicalBytes, 214 | "protocolInfo" to protocolInfo, 215 | "applicationData" to applicationData, 216 | "hiLayerResponse" to hiLayerResponse, 217 | "manufacturer" to manufacturer, 218 | "systemCode" to systemCode, 219 | "dsfId" to dsfId, 220 | "ndefAvailable" to ndefAvailable, 221 | "ndefType" to ndefType, 222 | "ndefWritable" to ndefWritable, 223 | "ndefCanMakeReadOnly" to ndefCanMakeReadOnly, 224 | "ndefCapacity" to ndefCapacity, 225 | )) 226 | 227 | if (mifareInfo != null) { 228 | with(mifareInfo!!) { 229 | jsonResult.put("mifareInfo", JSONObject(mapOf( 230 | "type" to typeStr, 231 | "size" to size, 232 | "blockSize" to blockSize, 233 | "blockCount" to blockCount, 234 | "sectorCount" to sectorCount 235 | ))) 236 | } 237 | } 238 | 239 | return jsonResult.toString() 240 | } 241 | } 242 | 243 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 244 | nfcHandlerThread = HandlerThread("NfcHandlerThread") 245 | nfcHandlerThread.start() 246 | nfcHandler = Handler(nfcHandlerThread.looper) 247 | 248 | methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/method") 249 | methodChannel.setMethodCallHandler(this) 250 | 251 | eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/event") 252 | eventChannel.setStreamHandler(object : EventChannel.StreamHandler { 253 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { 254 | if (events != null) { 255 | eventSink = events 256 | } 257 | } 258 | 259 | override fun onCancel(arguments: Any?) { 260 | // No need to do anything here 261 | } 262 | }) 263 | } 264 | 265 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 266 | methodChannel.setMethodCallHandler(null) 267 | eventChannel.setStreamHandler(null) 268 | nfcHandlerThread.quitSafely() 269 | } 270 | 271 | override fun onMethodCall(call: MethodCall, result: Result) { 272 | handleMethodCall(call, MethodResultWrapper(result)) 273 | } 274 | 275 | private fun handleMethodCall(call: MethodCall, result: Result) { 276 | 277 | if (activity.get() == null) { 278 | result.error("500", "Cannot call method when not attached to activity", null) 279 | return 280 | } 281 | 282 | val nfcAdapter = getDefaultAdapter(activity.get()) 283 | 284 | if (nfcAdapter?.isEnabled != true && call.method != "getNFCAvailability") { 285 | result.error("404", "NFC not available", null) 286 | return 287 | } 288 | 289 | val ensureNDEF = { 290 | if (ndefTechnology == null) { 291 | if (tagTechnology == null) { 292 | result.error("406", "No tag polled", null) 293 | } else { 294 | result.error("405", "NDEF not supported on current tag", null) 295 | } 296 | false 297 | } else true 298 | } 299 | 300 | val switchTechnology = { target: TagTechnology, other: TagTechnology? -> 301 | if (!target.isConnected) { 302 | // close previously connected technology 303 | if (other !== null && other.isConnected) { 304 | other.close() 305 | } 306 | target.connect() 307 | } 308 | } 309 | 310 | when (call.method) { 311 | 312 | "getNFCAvailability" -> { 313 | when { 314 | nfcAdapter == null -> result.success("not_supported") 315 | nfcAdapter.isEnabled -> result.success("available") 316 | else -> result.success("disabled") 317 | } 318 | } 319 | 320 | "poll" -> { 321 | val timeout = call.argument("timeout")!! 322 | // technology and option bits are set in Dart code 323 | val technologies = call.argument("technologies")!! 324 | runOnNfcThread(result, "Poll") { 325 | pollTag(nfcAdapter, result, timeout, technologies) 326 | } 327 | } 328 | 329 | "finish" -> { 330 | pollingTimeoutTask?.cancel() 331 | runOnNfcThread(result, "Close tag") { 332 | val tagTech = tagTechnology 333 | if (tagTech != null && tagTech.isConnected) { 334 | tagTech.close() 335 | } 336 | val ndefTech = ndefTechnology 337 | if (ndefTech != null && ndefTech.isConnected) { 338 | ndefTech.close() 339 | } 340 | if (activity.get() != null) { 341 | nfcAdapter.disableReaderMode(activity.get()) 342 | } 343 | result.success("") 344 | } 345 | } 346 | 347 | "transceive" -> { 348 | val tagTech = tagTechnology 349 | val data = call.argument("data") 350 | if (data == null || (data !is String && data !is ByteArray)) { 351 | result.error("400", "Bad argument", null) 352 | return 353 | } 354 | if (tagTech == null) { 355 | result.error("406", "No tag polled", null) 356 | return 357 | } 358 | val (sendingBytes, sendingHex) = canonicalizeData(data) 359 | 360 | runOnNfcThread(result, "Transceive: $sendingHex") { 361 | switchTechnology(tagTech, ndefTechnology) 362 | val timeout = call.argument("timeout") 363 | val resp = tagTech.transceive(sendingBytes, timeout) 364 | when (data) { 365 | is String -> result.success(resp.toHexString()) 366 | else -> result.success(resp) 367 | } 368 | } 369 | } 370 | 371 | /// NDEF-related methods below 372 | "readNDEF" -> { 373 | if (!ensureNDEF()) return 374 | val ndef = ndefTechnology!! 375 | runOnNfcThread(result, "Read NDEF") { 376 | switchTechnology(ndef, tagTechnology) 377 | // read NDEF message 378 | val message: NdefMessage? = if (call.argument("cached")!!) { 379 | ndef.cachedNdefMessage 380 | } else { 381 | ndef.ndefMessage 382 | } 383 | val parsedMessages = mutableListOf>() 384 | if (message != null) { 385 | for (record in message.records) { 386 | parsedMessages.add(mapOf( 387 | "identifier" to record.id.toHexString(), 388 | "payload" to record.payload.toHexString(), 389 | "type" to record.type.toHexString(), 390 | "typeNameFormat" to when (record.tnf) { 391 | NdefRecord.TNF_ABSOLUTE_URI -> "absoluteURI" 392 | NdefRecord.TNF_EMPTY -> "empty" 393 | NdefRecord.TNF_EXTERNAL_TYPE -> "nfcExternal" 394 | NdefRecord.TNF_WELL_KNOWN -> "nfcWellKnown" 395 | NdefRecord.TNF_MIME_MEDIA -> "media" 396 | NdefRecord.TNF_UNCHANGED -> "unchanged" 397 | else -> "unknown" 398 | } 399 | )) 400 | } 401 | } 402 | result.success(JSONArray(parsedMessages).toString()) 403 | } 404 | } 405 | 406 | "writeNDEF" -> { 407 | if (!ensureNDEF()) return 408 | val ndef = ndefTechnology!! 409 | if (!ndef.isWritable) { 410 | result.error("405", "Tag not writable", null) 411 | return 412 | } 413 | runOnNfcThread(result, "Write NDEF") { 414 | switchTechnology(ndef, tagTechnology) 415 | // generate NDEF message 416 | val jsonString = call.argument("data")!! 417 | val recordData = JSONArray(jsonString) 418 | val records = Array(recordData.length(), init = { i: Int -> 419 | val record: JSONObject = recordData.get(i) as JSONObject 420 | NdefRecord( 421 | when (record.getString("typeNameFormat")) { 422 | "absoluteURI" -> NdefRecord.TNF_ABSOLUTE_URI 423 | "empty" -> NdefRecord.TNF_EMPTY 424 | "nfcExternal" -> NdefRecord.TNF_EXTERNAL_TYPE 425 | "nfcWellKnown" -> NdefRecord.TNF_WELL_KNOWN 426 | "media" -> NdefRecord.TNF_MIME_MEDIA 427 | "unchanged" -> NdefRecord.TNF_UNCHANGED 428 | else -> NdefRecord.TNF_UNKNOWN 429 | }, 430 | record.getString("type").hexToBytes(), 431 | record.getString("identifier").hexToBytes(), 432 | record.getString("payload").hexToBytes() 433 | ) 434 | }) 435 | // write NDEF message 436 | val message = NdefMessage(records) 437 | ndef.writeNdefMessage(message) 438 | result.success("") 439 | } 440 | } 441 | 442 | "makeNdefReadOnly" -> { 443 | if (!ensureNDEF()) return 444 | val ndef = ndefTechnology!! 445 | if (!ndef.isWritable) { 446 | result.error("405", "Tag not writable", null) 447 | return 448 | } 449 | runOnNfcThread(result, "Lock NDEF") { 450 | switchTechnology(ndef, tagTechnology) 451 | if (ndef.makeReadOnly()) { 452 | result.success("") 453 | } else { 454 | result.error("500", "Failed to lock NDEF tag", null) 455 | } 456 | } 457 | } 458 | 459 | /// MIFARE/NTAG-related methods below 460 | "authenticateSector" -> { 461 | val tagTech = tagTechnology 462 | if (tagTech == null || mifareInfo == null || mifareInfo!!.sectorCount == null) { 463 | result.error("406", "No Mifare Classic tag polled", null) 464 | return 465 | } 466 | val index = call.argument("index")!! 467 | val maxSector = mifareInfo!!.sectorCount!! 468 | if (index !in 0 until maxSector) { 469 | result.error("400", "Invalid sector index $index, should be in (0, $maxSector)", null) 470 | return 471 | } 472 | val keyA = call.argument("keyA") 473 | val keyB = call.argument("keyB") 474 | runOnNfcThread(result, "Authenticate sector") { 475 | val tag = tagTech as MifareClassic 476 | switchTechnology(tagTech, ndefTechnology) 477 | // key A takes precedence if present 478 | if (keyA != null) { 479 | val (key, _) = canonicalizeData(keyA) 480 | val authStatus = tag.authenticateSectorWithKeyA(index, key) 481 | result.success(authStatus) 482 | } else if (keyB != null) { 483 | val (key, _) = canonicalizeData(keyB) 484 | val authStatus = tag.authenticateSectorWithKeyB(index, key) 485 | result.success(authStatus) 486 | } else { 487 | result.error("400", "No keys provided", null) 488 | } 489 | } 490 | } 491 | 492 | "readBlock" -> { 493 | val tagTech = tagTechnology 494 | if (tagTech == null || mifareInfo == null) { 495 | result.error("406", "No Mifare tag polled", null) 496 | return 497 | } 498 | val index = call.argument("index")!! 499 | val maxBlock = mifareInfo!!.blockCount 500 | if (index !in 0 until maxBlock) { 501 | result.error("400", "Invalid block/page index $index, should be in (0, $maxBlock)", null) 502 | return 503 | } 504 | runOnNfcThread(result, "Read block") { 505 | switchTechnology(tagTech, ndefTechnology) 506 | tagTech.readBlock(index, result) 507 | } 508 | } 509 | 510 | "readSector" -> { 511 | val tagTech = tagTechnology 512 | if (tagTech == null || mifareInfo == null || mifareInfo!!.sectorCount == null) { 513 | result.error("406", "No Mifare Classic tag polled", null) 514 | return 515 | } 516 | val index = call.argument("index")!! 517 | val maxSector = mifareInfo!!.sectorCount!! 518 | if (index !in 0 until maxSector) { 519 | result.error("400", "Invalid sector index $index, should be in (0, $maxSector)", null) 520 | return 521 | } 522 | runOnNfcThread(result, "Read sector") { 523 | val tag = tagTech as MifareClassic 524 | switchTechnology(tagTech, ndefTechnology) 525 | result.success(tag.readSector(index)) 526 | } 527 | } 528 | 529 | "writeBlock" -> { 530 | val tagTech = tagTechnology 531 | if (tagTech == null || mifareInfo == null) { 532 | result.error("406", "No Mifare tag polled", null) 533 | return 534 | } 535 | val index = call.argument("index")!! 536 | val maxBlock = mifareInfo!!.blockCount 537 | if (index !in 0 until maxBlock) { 538 | result.error("400", "Invalid block/page index $index, should be in (0, $maxBlock)", null) 539 | return 540 | } 541 | val data = call.argument("data") 542 | if (data == null || (data !is String && data !is ByteArray)) { 543 | result.error("400", "Bad argument", null) 544 | return 545 | } 546 | val (bytes, _) = canonicalizeData(data) 547 | if (bytes.size != mifareInfo!!.blockSize) { 548 | result.error("400", "Invalid data size ${bytes.size}, should be ${mifareInfo!!.blockSize}", null) 549 | return 550 | } 551 | runOnNfcThread(result, "Write block") { 552 | switchTechnology(tagTech, ndefTechnology) 553 | tagTech.writeBlock(index, bytes, result) 554 | } 555 | } 556 | 557 | // do nothing, just for compatibility 558 | "setIosAlertMessage" -> { result.success("") } 559 | } 560 | } 561 | 562 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 563 | activity = WeakReference(binding.activity) 564 | } 565 | 566 | override fun onDetachedFromActivity() { 567 | pollingTimeoutTask?.cancel() 568 | pollingTimeoutTask = null 569 | tagTechnology = null 570 | ndefTechnology = null 571 | activity.clear() 572 | } 573 | 574 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} 575 | 576 | override fun onDetachedFromActivityForConfigChanges() {} 577 | 578 | private fun pollTag(nfcAdapter: NfcAdapter, result: Result, timeout: Int, technologies: Int) { 579 | 580 | pollingTimeoutTask = Timer().schedule(timeout.toLong()) { 581 | try { 582 | if (activity.get() != null) { 583 | 584 | nfcAdapter.disableReaderMode(activity.get()) 585 | } 586 | } catch (ex: Exception) { 587 | Log.w(TAG, "Cannot disable reader mode", ex) 588 | } 589 | result.error("408", "Polling tag timeout", null) 590 | } 591 | 592 | val pollHandler = NfcAdapter.ReaderCallback { tag -> 593 | pollingTimeoutTask?.cancel() 594 | 595 | val jsonResult = parseTag(tag) 596 | result.success(jsonResult) 597 | } 598 | 599 | nfcAdapter.enableReaderMode(activity.get(), pollHandler, technologies, null) 600 | } 601 | 602 | private class MethodResultWrapper(result: Result) : Result { 603 | 604 | private val methodResult: Result = result 605 | private var hasError: Boolean = false 606 | 607 | companion object { 608 | // a Handler is always thread-safe, so use a singleton here 609 | private val handler: Handler by lazy { 610 | Handler(Looper.getMainLooper()) 611 | } 612 | } 613 | 614 | override fun success(result: Any?) { 615 | handler.post { 616 | ignoreIllegalState { 617 | methodResult.success(result) 618 | } 619 | } 620 | } 621 | 622 | override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { 623 | handler.post { 624 | ignoreIllegalState { 625 | methodResult.error(errorCode, errorMessage, errorDetails) 626 | } 627 | } 628 | } 629 | 630 | override fun notImplemented() { 631 | handler.post { 632 | ignoreIllegalState { 633 | methodResult.notImplemented() 634 | } 635 | } 636 | } 637 | 638 | private fun ignoreIllegalState(fn: () -> Unit) { 639 | try { 640 | if (!hasError) fn() 641 | } catch (e: IllegalStateException) { 642 | hasError = true 643 | Log.w(TAG, "Exception occurred when using MethodChannel.Result: $e") 644 | Log.w(TAG, "Will ignore all following usage of object: $methodResult") 645 | } 646 | } 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/nfc/flutter_nfc_kit/MifareUtils.kt: -------------------------------------------------------------------------------- 1 | package im.nfc.flutter_nfc_kit 2 | 3 | import android.nfc.tech.MifareClassic 4 | import android.nfc.tech.MifareUltralight 5 | import android.nfc.tech.TagTechnology 6 | import io.flutter.plugin.common.MethodChannel.Result 7 | 8 | 9 | data class MifareInfo( 10 | val type: Int, 11 | val size: Int, 12 | val blockSize: Int, // in bytes 13 | val blockCount: Int, 14 | val sectorCount: Int?, // might be null 15 | ) { 16 | 17 | val typeStr get() = run { 18 | when (sectorCount) { 19 | null -> { 20 | // Ultralight 21 | when (type) { 22 | MifareUltralight.TYPE_ULTRALIGHT -> "ultralight" 23 | MifareUltralight.TYPE_ULTRALIGHT_C -> "ultralight_c" 24 | else -> "ultralight_unknown" 25 | } 26 | } 27 | else -> { 28 | // Classic 29 | when (type) { 30 | MifareClassic.TYPE_CLASSIC -> "classic" 31 | MifareClassic.TYPE_PLUS -> "plus" 32 | MifareClassic.TYPE_PRO -> "pro" 33 | else -> "classic_unknown" 34 | } 35 | } 36 | } 37 | } 38 | 39 | companion object { 40 | private fun ultralightPageCount(type: Int): Int { 41 | return when (type) { 42 | MifareUltralight.TYPE_ULTRALIGHT -> { 43 | (0x0F + 1) 44 | } 45 | MifareUltralight.TYPE_ULTRALIGHT_C -> { 46 | (0x2B + 1) 47 | } 48 | else -> { 49 | -1 // unknown 50 | } 51 | } 52 | } 53 | private fun ultralightSize(type: Int): Int { 54 | return when (type) { 55 | MifareUltralight.TYPE_ULTRALIGHT, MifareUltralight.TYPE_ULTRALIGHT_C -> { 56 | ultralightPageCount(type) * MifareUltralight.PAGE_SIZE 57 | } 58 | else -> { 59 | -1 // unknown 60 | } 61 | } 62 | } 63 | 64 | fun fromUltralight(type: Int): MifareInfo { 65 | return MifareInfo( 66 | type, 67 | ultralightSize(type), 68 | MifareUltralight.PAGE_SIZE, 69 | ultralightPageCount(type), 70 | null 71 | ) 72 | } 73 | } 74 | } 75 | 76 | /// These functions must not be called on UI thread 77 | object MifareUtils { 78 | 79 | private val TAG = MifareUtils::class.java.name 80 | 81 | /// read one block (16 bytes) 82 | fun TagTechnology.readBlock(offset: Int, result: Result){ 83 | when (this) { 84 | is MifareClassic -> { 85 | val data = readBlock(offset) 86 | result.success(data) 87 | return 88 | } 89 | is MifareUltralight -> { 90 | val data = readPages(offset) 91 | result.success(data) 92 | return 93 | } 94 | else -> { 95 | result.error("405", "Cannot invoke read on non-Mifare card", null) 96 | return 97 | } 98 | } 99 | } 100 | 101 | /// write one smallest unit (1 block for Classic, 1 page for Ultralight) 102 | fun TagTechnology.writeBlock(offset: Int, data: ByteArray, result: Result) { 103 | when (this) { 104 | is MifareClassic -> { 105 | writeBlock(offset, data) 106 | result.success("") 107 | return 108 | } 109 | is MifareUltralight -> { 110 | writePage(offset, data) 111 | result.success("") 112 | return 113 | } 114 | else -> { 115 | result.error("405", "Cannot invoke write on non-Mifare card", null) 116 | return 117 | } 118 | } 119 | } 120 | 121 | fun MifareClassic.readSector(index: Int): ByteArray { 122 | val begin = sectorToBlock(index) 123 | val end = begin + getBlockCountInSector(index) 124 | var data = ByteArray(0) 125 | for (i in begin until end) { 126 | data += readBlock(i) 127 | } 128 | return data 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | 35 | # Exceptions to above rules. 36 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 37 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/.pubignore: -------------------------------------------------------------------------------- 1 | android/ 2 | ios/ 3 | web/ 4 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file('local.properties') 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader('UTF-8') { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 17 | if (flutterVersionCode == null) { 18 | flutterVersionCode = '1' 19 | } 20 | 21 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 22 | if (flutterVersionName == null) { 23 | flutterVersionName = '1.0' 24 | } 25 | 26 | android { 27 | 28 | namespace 'im.nfc.flutter_nfc_kit.example' 29 | 30 | compileSdk 35 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_1_8 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | 46 | defaultConfig { 47 | applicationId "im.nfc.flutter_nfc_kit_example" 48 | minSdkVersion 26 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 53 | } 54 | 55 | buildTypes { 56 | release { 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | lint { 62 | disable 'InvalidPackage' 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | } 72 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/im/nfc/flutter_nfc_kit/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package im.nfc.flutter_nfc_kit.example 2 | 3 | import android.app.PendingIntent 4 | import android.content.Intent 5 | import android.nfc.NfcAdapter 6 | import android.nfc.Tag 7 | import io.flutter.embedding.android.FlutterActivity 8 | import im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin 9 | 10 | class MainActivity : FlutterActivity() { 11 | override fun onResume() { 12 | super.onResume() 13 | val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) 14 | val pendingIntent: PendingIntent = PendingIntent.getActivity( 15 | this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE 16 | ) 17 | // Read the docs for reading specific technologies 18 | adapter?.enableForegroundDispatch(this, pendingIntent, null, null) 19 | } 20 | 21 | override fun onPause() { 22 | super.onPause() 23 | val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) 24 | adapter?.disableForegroundDispatch(this) 25 | } 26 | 27 | override fun onNewIntent(intent: Intent) { 28 | val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) 29 | tag?.apply(FlutterNfcKitPlugin::handleTag) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | rootProject.buildDir = '../build' 10 | subprojects { 11 | project.buildDir = "${rootProject.buildDir}/${project.name}" 12 | } 13 | subprojects { 14 | project.evaluationDependsOn(':app') 15 | } 16 | 17 | tasks.register("clean", Delete) { 18 | delete rootProject.buildDir 19 | } 20 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | android.defaults.buildfeatures.buildconfig=true 6 | android.nonTransitiveRClass=false 7 | android.nonFinalResIds=false 8 | 9 | AGPVersion=8.7.3 10 | KotlinVersion=2.1.0 11 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStorePath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "${AGPVersion}" apply false 22 | id "org.jetbrains.kotlin.android" version "${KotlinVersion}" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /example/android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /example/example.md: -------------------------------------------------------------------------------- 1 | # Example of flutter_nfc_kit 2 | 3 | ## Polling 4 | 5 | This is the default operation mode and is supported on all platforms. 6 | We recommend using this method to read NFC tags to ensure the consistency of cross-platform interactions. 7 | 8 | 9 | ```dart 10 | import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; 11 | import 'package:ndef/ndef.dart' as ndef; 12 | 13 | var availability = await FlutterNfcKit.nfcAvailability; 14 | if (availability != NFCAvailability.available) { 15 | // oh-no 16 | } 17 | 18 | // timeout only works on Android, while the latter two messages are only for iOS 19 | var tag = await FlutterNfcKit.poll(timeout: Duration(seconds: 10), 20 | iosMultipleTagMessage: "Multiple tags found!", iosAlertMessage: "Scan your tag"); 21 | 22 | print(jsonEncode(tag)); 23 | if (tag.type == NFCTagType.iso7816) { 24 | var result = await FlutterNfcKit.transceive("00B0950000", Duration(seconds: 5)); // timeout is still Android-only, persist until next change 25 | print(result); 26 | } 27 | // iOS only: set alert message on-the-fly 28 | // this will persist until finish() 29 | await FlutterNfcKit.setIosAlertMessage("hi there!"); 30 | 31 | // read NDEF records if available 32 | if (tag.ndefAvailable) { 33 | /// decoded NDEF records (see [ndef.NDEFRecord] for details) 34 | /// `UriRecord: id=(empty) typeNameFormat=TypeNameFormat.nfcWellKnown type=U uri=https://github.com/nfcim/ndef` 35 | for (var record in await FlutterNfcKit.readNDEFRecords(cached: false)) { 36 | print(record.toString()); 37 | } 38 | /// raw NDEF records (data in hex string) 39 | /// `{identifier: "", payload: "00010203", type: "0001", typeNameFormat: "nfcWellKnown"}` 40 | for (var record in await FlutterNfcKit.readNDEFRawRecords(cached: false)) { 41 | print(jsonEncode(record).toString()); 42 | } 43 | } 44 | 45 | // write NDEF records if applicable 46 | if (tag.ndefWritable) { 47 | // decoded NDEF records 48 | await FlutterNfcKit.writeNDEFRecords([new ndef.UriRecord.fromUriString("https://github.com/nfcim/flutter_nfc_kit")]); 49 | // raw NDEF records 50 | await FlutterNfcKit.writeNDEFRawRecords([new NDEFRawRecord("00", "0001", "0002", "0003", ndef.TypeNameFormat.unknown)]); 51 | } 52 | 53 | // read / write block / page / sector level data 54 | // see documentation for platform-specific supportability 55 | if (tag.type == NFCTagType.iso15693) { 56 | await await FlutterNfcKit.writeBlock( 57 | 1, // index 58 | [0xde, 0xad, 0xbe, 0xff], // data 59 | iso15693RequestFlag: Iso15693RequestFlag(), // optional flags for ISO 15693 60 | iso15693ExtendedMode: false // use extended mode for ISO 15693 61 | ); 62 | } 63 | 64 | if (tag.type == NFCType.mifare_classic) { 65 | await FlutterNfcKit.authenticateSector(0, keyA: "FFFFFFFFFFFF"); 66 | var data = await FlutterNfcKit.readSector(0); // read one sector, or 67 | var data = await FlutterNfcKit.readBlock(0); // read one block 68 | } 69 | 70 | // Call finish() only once 71 | await FlutterNfcKit.finish(); 72 | // iOS only: show alert/error message on finish 73 | await FlutterNfcKit.finish(iosAlertMessage: "Success"); 74 | // or 75 | await FlutterNfcKit.finish(iosErrorMessage: "Failed"); 76 | ``` 77 | 78 | ## Event Streaming 79 | 80 | This is only supported on Android now. To receive NFC tag events even when your app is in the foreground, you can set up tag event stream support by: 81 | 82 | 1. Create a custom Activity that extends `FlutterActivity` in your Android project: 83 | 84 | ```kotlin 85 | package your.package.name 86 | 87 | import android.app.PendingIntent 88 | import android.content.Intent 89 | import android.nfc.NfcAdapter 90 | import android.nfc.Tag 91 | import io.flutter.embedding.android.FlutterActivity 92 | import im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin 93 | 94 | class MainActivity : FlutterActivity() { 95 | override fun onResume() { 96 | super.onResume() 97 | val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) 98 | val pendingIntent: PendingIntent = PendingIntent.getActivity( 99 | this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE 100 | ) 101 | // See https://developer.android.com/reference/android/nfc/NfcAdapter#enableForegroundDispatch(android.app.Activity,%20android.app.PendingIntent,%20android.content.IntentFilter[],%20java.lang.String[][]) for details 102 | adapter?.enableForegroundDispatch(this, pendingIntent, null, null) 103 | } 104 | 105 | override fun onPause() { 106 | super.onPause() 107 | val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) 108 | adapter?.disableForegroundDispatch(this) 109 | } 110 | 111 | override fun onNewIntent(intent: Intent) { 112 | val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) 113 | tag?.apply(FlutterNfcKitPlugin::handleTag) 114 | } 115 | } 116 | ``` 117 | 118 | You may also invoke `enableForegroundDispatch` and `disableForegroundDispatch` in other places as needed. 119 | 120 | 2. Update your `AndroidManifest.xml` to use it as the main activity instead of the default Flutter activity. 121 | 122 | 3. In Flutter code, listen to the tag event stream and process events: 123 | 124 | ```dart 125 | @override 126 | void initState() { 127 | super.initState(); 128 | // listen to NFC tag events 129 | FlutterNfcKit.tagStream.listen((tag) { 130 | print('Tag detected: ${tag.id}'); 131 | // process the tag as in polling mode 132 | FlutterNfcKit.transceive("xxx", ...); 133 | // DO NOT call `FlutterNfcKit.finish` in this mode! 134 | }); 135 | } 136 | ``` 137 | 138 | This will allow your app to receive NFC tag events through a stream, which is useful for scenarios where you need continuous tag reading or want to handle tags even when your app is in the foreground but not actively polling. 139 | 140 | ## GUI Application 141 | 142 | See `lib/main.dart` for a GUI application on Android / iOS / web. Skeleton code for specific platforms are not uploaded to . Please refer to the [GitHub repository](https://github.com/nfcim/flutter_nfc_kit). 143 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | 4 | DEPENDENCIES: 5 | - Flutter (from `Flutter`) 6 | 7 | EXTERNAL SOURCES: 8 | Flutter: 9 | :path: Flutter 10 | 11 | SPEC CHECKSUMS: 12 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 13 | 14 | PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 15 | 16 | COCOAPODS: 1.16.2 17 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 14 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 15 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 16 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 17 | E58C8BA72341A6FDA9D194F8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0443C20940C009428DBFC8 /* Pods_Runner.framework */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = ""; 25 | dstSubfolderSpec = 10; 26 | files = ( 27 | ); 28 | name = "Embed Frameworks"; 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 21B301EF0FC0C3405BEEB5B5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 37 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 38 | 3CA721E826034CE6B499F060 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 39 | 52D42AC623D2C3710063AB8B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 40 | 5C0443C20940C009428DBFC8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 42 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 44 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 45 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 46 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 48 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | CC17078EABDEF174C9DEB078 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 60 | E58C8BA72341A6FDA9D194F8 /* Pods_Runner.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 9740EEB11CF90186004384FC /* Flutter */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 71 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 72 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 73 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 74 | ); 75 | name = Flutter; 76 | sourceTree = ""; 77 | }; 78 | 97C146E51CF9000F007C117D = { 79 | isa = PBXGroup; 80 | children = ( 81 | 9740EEB11CF90186004384FC /* Flutter */, 82 | 97C146F01CF9000F007C117D /* Runner */, 83 | 97C146EF1CF9000F007C117D /* Products */, 84 | F893C2497E8F1D360BEBEB7F /* Pods */, 85 | AA8E6B8C7207A8A364AEEA84 /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 97C146EF1CF9000F007C117D /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 97C146EE1CF9000F007C117D /* Runner.app */, 93 | ); 94 | name = Products; 95 | sourceTree = ""; 96 | }; 97 | 97C146F01CF9000F007C117D /* Runner */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 52D42AC623D2C3710063AB8B /* Runner.entitlements */, 101 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 102 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 103 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 104 | 97C147021CF9000F007C117D /* Info.plist */, 105 | 97C146F11CF9000F007C117D /* Supporting Files */, 106 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 107 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 108 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 109 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 110 | ); 111 | path = Runner; 112 | sourceTree = ""; 113 | }; 114 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | ); 118 | name = "Supporting Files"; 119 | sourceTree = ""; 120 | }; 121 | AA8E6B8C7207A8A364AEEA84 /* Frameworks */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 5C0443C20940C009428DBFC8 /* Pods_Runner.framework */, 125 | ); 126 | name = Frameworks; 127 | sourceTree = ""; 128 | }; 129 | F893C2497E8F1D360BEBEB7F /* Pods */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 3CA721E826034CE6B499F060 /* Pods-Runner.debug.xcconfig */, 133 | CC17078EABDEF174C9DEB078 /* Pods-Runner.release.xcconfig */, 134 | 21B301EF0FC0C3405BEEB5B5 /* Pods-Runner.profile.xcconfig */, 135 | ); 136 | name = Pods; 137 | path = Pods; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | 97C146ED1CF9000F007C117D /* Runner */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 146 | buildPhases = ( 147 | F90AEBACCBF7EA4B435EAF97 /* [CP] Check Pods Manifest.lock */, 148 | 9740EEB61CF901F6004384FC /* Run Script */, 149 | 97C146EA1CF9000F007C117D /* Sources */, 150 | 97C146EB1CF9000F007C117D /* Frameworks */, 151 | 97C146EC1CF9000F007C117D /* Resources */, 152 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 153 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 154 | ); 155 | buildRules = ( 156 | ); 157 | dependencies = ( 158 | ); 159 | name = Runner; 160 | packageProductDependencies = ( 161 | 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, 162 | ); 163 | productName = Runner; 164 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 165 | productType = "com.apple.product-type.application"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | 97C146E61CF9000F007C117D /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | LastUpgradeCheck = 1510; 174 | ORGANIZATIONNAME = "The Chromium Authors"; 175 | TargetAttributes = { 176 | 97C146ED1CF9000F007C117D = { 177 | CreatedOnToolsVersion = 7.3.1; 178 | LastSwiftMigration = 1100; 179 | }; 180 | }; 181 | }; 182 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 183 | compatibilityVersion = "Xcode 3.2"; 184 | developmentRegion = en; 185 | hasScannedForEncodings = 0; 186 | knownRegions = ( 187 | en, 188 | Base, 189 | ); 190 | mainGroup = 97C146E51CF9000F007C117D; 191 | packageReferences = ( 192 | 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, 193 | ); 194 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 195 | projectDirPath = ""; 196 | projectRoot = ""; 197 | targets = ( 198 | 97C146ED1CF9000F007C117D /* Runner */, 199 | ); 200 | }; 201 | /* End PBXProject section */ 202 | 203 | /* Begin PBXResourcesBuildPhase section */ 204 | 97C146EC1CF9000F007C117D /* Resources */ = { 205 | isa = PBXResourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 209 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 210 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 211 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | }; 215 | /* End PBXResourcesBuildPhase section */ 216 | 217 | /* Begin PBXShellScriptBuildPhase section */ 218 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 219 | isa = PBXShellScriptBuildPhase; 220 | alwaysOutOfDate = 1; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | ); 224 | inputPaths = ( 225 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 226 | ); 227 | name = "Thin Binary"; 228 | outputPaths = ( 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | shellPath = /bin/sh; 232 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 233 | }; 234 | 9740EEB61CF901F6004384FC /* Run Script */ = { 235 | isa = PBXShellScriptBuildPhase; 236 | alwaysOutOfDate = 1; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | inputPaths = ( 241 | ); 242 | name = "Run Script"; 243 | outputPaths = ( 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | shellPath = /bin/sh; 247 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 248 | }; 249 | F90AEBACCBF7EA4B435EAF97 /* [CP] Check Pods Manifest.lock */ = { 250 | isa = PBXShellScriptBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | inputFileListPaths = ( 255 | ); 256 | inputPaths = ( 257 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 258 | "${PODS_ROOT}/Manifest.lock", 259 | ); 260 | name = "[CP] Check Pods Manifest.lock"; 261 | outputFileListPaths = ( 262 | ); 263 | outputPaths = ( 264 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | shellPath = /bin/sh; 268 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 269 | showEnvVarsInLog = 0; 270 | }; 271 | /* End PBXShellScriptBuildPhase section */ 272 | 273 | /* Begin PBXSourcesBuildPhase section */ 274 | 97C146EA1CF9000F007C117D /* Sources */ = { 275 | isa = PBXSourcesBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 279 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | /* End PBXSourcesBuildPhase section */ 284 | 285 | /* Begin PBXVariantGroup section */ 286 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 287 | isa = PBXVariantGroup; 288 | children = ( 289 | 97C146FB1CF9000F007C117D /* Base */, 290 | ); 291 | name = Main.storyboard; 292 | sourceTree = ""; 293 | }; 294 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 295 | isa = PBXVariantGroup; 296 | children = ( 297 | 97C147001CF9000F007C117D /* Base */, 298 | ); 299 | name = LaunchScreen.storyboard; 300 | sourceTree = ""; 301 | }; 302 | /* End PBXVariantGroup section */ 303 | 304 | /* Begin XCBuildConfiguration section */ 305 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_COMMA = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 334 | COPY_PHASE_STRIP = NO; 335 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 336 | ENABLE_NS_ASSERTIONS = NO; 337 | ENABLE_STRICT_OBJC_MSGSEND = YES; 338 | GCC_C_LANGUAGE_STANDARD = gnu99; 339 | GCC_NO_COMMON_BLOCKS = YES; 340 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 341 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 342 | GCC_WARN_UNDECLARED_SELECTOR = YES; 343 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 344 | GCC_WARN_UNUSED_FUNCTION = YES; 345 | GCC_WARN_UNUSED_VARIABLE = YES; 346 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 347 | MTL_ENABLE_DEBUG_INFO = NO; 348 | SDKROOT = iphoneos; 349 | SUPPORTED_PLATFORMS = iphoneos; 350 | TARGETED_DEVICE_FAMILY = "1,2"; 351 | VALIDATE_PRODUCT = YES; 352 | }; 353 | name = Profile; 354 | }; 355 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 356 | isa = XCBuildConfiguration; 357 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | CLANG_ENABLE_MODULES = YES; 361 | CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 362 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 363 | DEVELOPMENT_TEAM = ""; 364 | ENABLE_BITCODE = NO; 365 | FRAMEWORK_SEARCH_PATHS = ( 366 | "$(inherited)", 367 | "$(PROJECT_DIR)/Flutter", 368 | ); 369 | INFOPLIST_FILE = Runner/Info.plist; 370 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 371 | LD_RUNPATH_SEARCH_PATHS = ( 372 | "$(inherited)", 373 | "@executable_path/Frameworks", 374 | ); 375 | LIBRARY_SEARCH_PATHS = ( 376 | "$(inherited)", 377 | "$(PROJECT_DIR)/Flutter", 378 | ); 379 | PRODUCT_BUNDLE_IDENTIFIER = im.nfc.flutterNfcKitExample; 380 | PRODUCT_NAME = "$(TARGET_NAME)"; 381 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 382 | SWIFT_VERSION = 5.0; 383 | VERSIONING_SYSTEM = "apple-generic"; 384 | }; 385 | name = Profile; 386 | }; 387 | 97C147031CF9000F007C117D /* Debug */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ALWAYS_SEARCH_USER_PATHS = NO; 391 | CLANG_ANALYZER_NONNULL = YES; 392 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 393 | CLANG_CXX_LIBRARY = "libc++"; 394 | CLANG_ENABLE_MODULES = YES; 395 | CLANG_ENABLE_OBJC_ARC = YES; 396 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 397 | CLANG_WARN_BOOL_CONVERSION = YES; 398 | CLANG_WARN_COMMA = YES; 399 | CLANG_WARN_CONSTANT_CONVERSION = YES; 400 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 401 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 402 | CLANG_WARN_EMPTY_BODY = YES; 403 | CLANG_WARN_ENUM_CONVERSION = YES; 404 | CLANG_WARN_INFINITE_RECURSION = YES; 405 | CLANG_WARN_INT_CONVERSION = YES; 406 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 407 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 408 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 409 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 410 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 411 | CLANG_WARN_STRICT_PROTOTYPES = YES; 412 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 413 | CLANG_WARN_UNREACHABLE_CODE = YES; 414 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 415 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 416 | COPY_PHASE_STRIP = NO; 417 | DEBUG_INFORMATION_FORMAT = dwarf; 418 | ENABLE_STRICT_OBJC_MSGSEND = YES; 419 | ENABLE_TESTABILITY = YES; 420 | GCC_C_LANGUAGE_STANDARD = gnu99; 421 | GCC_DYNAMIC_NO_PIC = NO; 422 | GCC_NO_COMMON_BLOCKS = YES; 423 | GCC_OPTIMIZATION_LEVEL = 0; 424 | GCC_PREPROCESSOR_DEFINITIONS = ( 425 | "DEBUG=1", 426 | "$(inherited)", 427 | ); 428 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 429 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 430 | GCC_WARN_UNDECLARED_SELECTOR = YES; 431 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 432 | GCC_WARN_UNUSED_FUNCTION = YES; 433 | GCC_WARN_UNUSED_VARIABLE = YES; 434 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 435 | MTL_ENABLE_DEBUG_INFO = YES; 436 | ONLY_ACTIVE_ARCH = YES; 437 | SDKROOT = iphoneos; 438 | TARGETED_DEVICE_FAMILY = "1,2"; 439 | }; 440 | name = Debug; 441 | }; 442 | 97C147041CF9000F007C117D /* Release */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ALWAYS_SEARCH_USER_PATHS = NO; 446 | CLANG_ANALYZER_NONNULL = YES; 447 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 448 | CLANG_CXX_LIBRARY = "libc++"; 449 | CLANG_ENABLE_MODULES = YES; 450 | CLANG_ENABLE_OBJC_ARC = YES; 451 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 452 | CLANG_WARN_BOOL_CONVERSION = YES; 453 | CLANG_WARN_COMMA = YES; 454 | CLANG_WARN_CONSTANT_CONVERSION = YES; 455 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 456 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 457 | CLANG_WARN_EMPTY_BODY = YES; 458 | CLANG_WARN_ENUM_CONVERSION = YES; 459 | CLANG_WARN_INFINITE_RECURSION = YES; 460 | CLANG_WARN_INT_CONVERSION = YES; 461 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 462 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 463 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 464 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 465 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 466 | CLANG_WARN_STRICT_PROTOTYPES = YES; 467 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 468 | CLANG_WARN_UNREACHABLE_CODE = YES; 469 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 470 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 471 | COPY_PHASE_STRIP = NO; 472 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 473 | ENABLE_NS_ASSERTIONS = NO; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | GCC_C_LANGUAGE_STANDARD = gnu99; 476 | GCC_NO_COMMON_BLOCKS = YES; 477 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 478 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 479 | GCC_WARN_UNDECLARED_SELECTOR = YES; 480 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 481 | GCC_WARN_UNUSED_FUNCTION = YES; 482 | GCC_WARN_UNUSED_VARIABLE = YES; 483 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 484 | MTL_ENABLE_DEBUG_INFO = NO; 485 | SDKROOT = iphoneos; 486 | SUPPORTED_PLATFORMS = iphoneos; 487 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 488 | TARGETED_DEVICE_FAMILY = "1,2"; 489 | VALIDATE_PRODUCT = YES; 490 | }; 491 | name = Release; 492 | }; 493 | 97C147061CF9000F007C117D /* Debug */ = { 494 | isa = XCBuildConfiguration; 495 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 496 | buildSettings = { 497 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 498 | CLANG_ENABLE_MODULES = YES; 499 | CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 500 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 501 | DEVELOPMENT_TEAM = ""; 502 | ENABLE_BITCODE = NO; 503 | FRAMEWORK_SEARCH_PATHS = ( 504 | "$(inherited)", 505 | "$(PROJECT_DIR)/Flutter", 506 | ); 507 | INFOPLIST_FILE = Runner/Info.plist; 508 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 509 | LD_RUNPATH_SEARCH_PATHS = ( 510 | "$(inherited)", 511 | "@executable_path/Frameworks", 512 | ); 513 | LIBRARY_SEARCH_PATHS = ( 514 | "$(inherited)", 515 | "$(PROJECT_DIR)/Flutter", 516 | ); 517 | PRODUCT_BUNDLE_IDENTIFIER = im.nfc.flutterNfcKitExample; 518 | PRODUCT_NAME = "$(TARGET_NAME)"; 519 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 520 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 521 | SWIFT_VERSION = 5.0; 522 | VERSIONING_SYSTEM = "apple-generic"; 523 | }; 524 | name = Debug; 525 | }; 526 | 97C147071CF9000F007C117D /* Release */ = { 527 | isa = XCBuildConfiguration; 528 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 529 | buildSettings = { 530 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 531 | CLANG_ENABLE_MODULES = YES; 532 | CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 533 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 534 | DEVELOPMENT_TEAM = ""; 535 | ENABLE_BITCODE = NO; 536 | FRAMEWORK_SEARCH_PATHS = ( 537 | "$(inherited)", 538 | "$(PROJECT_DIR)/Flutter", 539 | ); 540 | INFOPLIST_FILE = Runner/Info.plist; 541 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 542 | LD_RUNPATH_SEARCH_PATHS = ( 543 | "$(inherited)", 544 | "@executable_path/Frameworks", 545 | ); 546 | LIBRARY_SEARCH_PATHS = ( 547 | "$(inherited)", 548 | "$(PROJECT_DIR)/Flutter", 549 | ); 550 | PRODUCT_BUNDLE_IDENTIFIER = im.nfc.flutterNfcKitExample; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 553 | SWIFT_VERSION = 5.0; 554 | VERSIONING_SYSTEM = "apple-generic"; 555 | }; 556 | name = Release; 557 | }; 558 | /* End XCBuildConfiguration section */ 559 | 560 | /* Begin XCConfigurationList section */ 561 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 97C147031CF9000F007C117D /* Debug */, 565 | 97C147041CF9000F007C117D /* Release */, 566 | 249021D3217E4FDB00AE95B9 /* Profile */, 567 | ); 568 | defaultConfigurationIsVisible = 0; 569 | defaultConfigurationName = Release; 570 | }; 571 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 572 | isa = XCConfigurationList; 573 | buildConfigurations = ( 574 | 97C147061CF9000F007C117D /* Debug */, 575 | 97C147071CF9000F007C117D /* Release */, 576 | 249021D4217E4FDB00AE95B9 /* Profile */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | /* End XCConfigurationList section */ 582 | 583 | /* Begin XCLocalSwiftPackageReference section */ 584 | 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { 585 | isa = XCLocalSwiftPackageReference; 586 | relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 587 | }; 588 | /* End XCLocalSwiftPackageReference section */ 589 | 590 | /* Begin XCSwiftPackageProductDependency section */ 591 | 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { 592 | isa = XCSwiftPackageProductDependency; 593 | productName = FlutterGeneratedPluginSwiftPackage; 594 | }; 595 | /* End XCSwiftPackageProductDependency section */ 596 | }; 597 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 598 | } 599 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 72 | 74 | 80 | 81 | 82 | 83 | 84 | 85 | 91 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.nfc.readersession.felica.systemcodes 6 | 7 | 8008 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_nfc_kit_example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NFCReaderUsageDescription 30 | Read SmartCard 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UIRequiredDeviceCapabilities 36 | 37 | nfc 38 | 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | com.apple.developer.nfc.readersession.iso7816.select-identifiers 55 | 56 | A00000000386980701 57 | 58 | CADisableMinimumFrameDurationOnPhone 59 | 60 | UIApplicationSupportsIndirectInputEvents 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.nfc.readersession.formats 6 | 7 | TAG 8 | NDEF 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io' show Platform, sleep; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; 8 | import 'package:logging/logging.dart'; 9 | import 'package:ndef/ndef.dart' as ndef; 10 | import 'package:ndef/utilities.dart'; 11 | 12 | import 'ndef_record/raw_record_setting.dart'; 13 | import 'ndef_record/text_record_setting.dart'; 14 | import 'ndef_record/uri_record_setting.dart'; 15 | 16 | void main() { 17 | Logger.root.level = Level.ALL; // defaults to Level.INFO 18 | Logger.root.onRecord.listen((record) { 19 | print('${record.level.name}: ${record.time}: ${record.message}'); 20 | }); 21 | runApp(MaterialApp(theme: ThemeData(useMaterial3: true), home: MyApp())); 22 | } 23 | 24 | class MyApp extends StatefulWidget { 25 | @override 26 | State createState() => _MyAppState(); 27 | } 28 | 29 | class _MyAppState extends State with SingleTickerProviderStateMixin { 30 | String _platformVersion = ''; 31 | NFCAvailability _availability = NFCAvailability.not_supported; 32 | NFCTag? _tag; 33 | String? _result, _writeResult, _mifareResult; 34 | late TabController _tabController; 35 | List? _records; 36 | 37 | @override 38 | void dispose() { 39 | _tabController.dispose(); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | if (!kIsWeb) { 47 | _platformVersion = 48 | '${Platform.operatingSystem} ${Platform.operatingSystemVersion}'; 49 | } else { 50 | _platformVersion = 'Web'; 51 | } 52 | initPlatformState(); 53 | _tabController = TabController(length: 2, vsync: this); 54 | _records = []; 55 | FlutterNfcKit.tagStream.listen((tag) { 56 | setState(() { 57 | _tag = tag; 58 | print(_tag); 59 | }); 60 | }); 61 | } 62 | 63 | // Platform messages are asynchronous, so we initialize in an async method. 64 | Future initPlatformState() async { 65 | NFCAvailability availability; 66 | try { 67 | availability = await FlutterNfcKit.nfcAvailability; 68 | } on PlatformException { 69 | availability = NFCAvailability.not_supported; 70 | } 71 | 72 | // If the widget was removed from the tree while the asynchronous platform 73 | // message was in flight, we want to discard the reply rather than calling 74 | // setState to update our non-existent appearance. 75 | if (!mounted) return; 76 | 77 | setState(() { 78 | // _platformVersion = platformVersion; 79 | _availability = availability; 80 | }); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return MaterialApp( 86 | home: Scaffold( 87 | appBar: AppBar( 88 | title: const Text('NFC Flutter Kit Example App'), 89 | bottom: TabBar( 90 | tabs: [ 91 | Tab(text: 'Read'), 92 | Tab(text: 'Write'), 93 | ], 94 | controller: _tabController, 95 | )), 96 | body: TabBarView(controller: _tabController, children: [ 97 | Scrollbar( 98 | child: SingleChildScrollView( 99 | child: Center( 100 | child: Column( 101 | mainAxisAlignment: MainAxisAlignment.center, 102 | children: [ 103 | const SizedBox(height: 20), 104 | Text('Running on: $_platformVersion\nNFC: $_availability'), 105 | const SizedBox(height: 10), 106 | ElevatedButton( 107 | onPressed: () async { 108 | try { 109 | NFCTag tag = await FlutterNfcKit.poll(); 110 | setState(() { 111 | _tag = tag; 112 | }); 113 | await FlutterNfcKit.setIosAlertMessage( 114 | "Working on it..."); 115 | _mifareResult = null; 116 | if (tag.standard == "ISO 14443-4 (Type B)") { 117 | String result1 = 118 | await FlutterNfcKit.transceive("00B0950000"); 119 | String result2 = await FlutterNfcKit.transceive( 120 | "00A4040009A00000000386980701"); 121 | setState(() { 122 | _result = '1: $result1\n2: $result2\n'; 123 | }); 124 | } else if (tag.type == NFCTagType.iso18092) { 125 | String result1 = 126 | await FlutterNfcKit.transceive("060080080100"); 127 | setState(() { 128 | _result = '1: $result1\n'; 129 | }); 130 | } else if (tag.ndefAvailable ?? false) { 131 | var ndefRecords = await FlutterNfcKit.readNDEFRecords(); 132 | var ndefString = ''; 133 | for (int i = 0; i < ndefRecords.length; i++) { 134 | ndefString += '${i + 1}: ${ndefRecords[i]}\n'; 135 | } 136 | setState(() { 137 | _result = ndefString; 138 | }); 139 | } else if (tag.type == NFCTagType.webusb) { 140 | var r = await FlutterNfcKit.transceive( 141 | "00A4040006D27600012401"); 142 | print(r); 143 | } 144 | } catch (e) { 145 | setState(() { 146 | _result = 'error: $e'; 147 | }); 148 | } 149 | 150 | // Pretend that we are working 151 | if (!kIsWeb) sleep(Duration(seconds: 1)); 152 | await FlutterNfcKit.finish(iosAlertMessage: "Finished!"); 153 | }, 154 | child: Text('Start polling'), 155 | ), 156 | const SizedBox(height: 10), 157 | Padding( 158 | padding: const EdgeInsets.symmetric(horizontal: 20), 159 | child: _tag != null 160 | ? Text( 161 | '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') 162 | : const Text('No tag polled yet.')), 163 | ])))), 164 | Center( 165 | child: Column( 166 | mainAxisAlignment: MainAxisAlignment.start, 167 | children: [ 168 | const SizedBox(height: 20), 169 | Row( 170 | mainAxisAlignment: MainAxisAlignment.spaceAround, 171 | children: [ 172 | ElevatedButton( 173 | onPressed: () async { 174 | if (_records!.isNotEmpty) { 175 | try { 176 | NFCTag tag = await FlutterNfcKit.poll(); 177 | setState(() { 178 | _tag = tag; 179 | }); 180 | if (tag.type == NFCTagType.mifare_ultralight || 181 | tag.type == NFCTagType.mifare_classic || 182 | tag.type == NFCTagType.iso15693) { 183 | await FlutterNfcKit.writeNDEFRecords(_records!); 184 | setState(() { 185 | _writeResult = 'OK'; 186 | }); 187 | } else { 188 | setState(() { 189 | _writeResult = 190 | 'error: NDEF not supported: ${tag.type}'; 191 | }); 192 | } 193 | } catch (e, stacktrace) { 194 | setState(() { 195 | _writeResult = 'error: $e'; 196 | }); 197 | print(stacktrace); 198 | } finally { 199 | await FlutterNfcKit.finish(); 200 | } 201 | } else { 202 | setState(() { 203 | _writeResult = 'error: No record'; 204 | }); 205 | } 206 | }, 207 | child: Text("Start writing"), 208 | ), 209 | ElevatedButton( 210 | onPressed: () { 211 | showDialog( 212 | context: context, 213 | builder: (BuildContext context) { 214 | return SimpleDialog( 215 | title: Text("Record Type"), 216 | children: [ 217 | SimpleDialogOption( 218 | child: Text("Text Record"), 219 | onPressed: () async { 220 | Navigator.pop(context); 221 | final result = await Navigator.push( 222 | context, MaterialPageRoute( 223 | builder: (context) { 224 | return NDEFTextRecordSetting(); 225 | })); 226 | if (result != null) { 227 | if (result is ndef.TextRecord) { 228 | setState(() { 229 | _records!.add(result); 230 | }); 231 | } 232 | } 233 | }, 234 | ), 235 | SimpleDialogOption( 236 | child: Text("Uri Record"), 237 | onPressed: () async { 238 | Navigator.pop(context); 239 | final result = await Navigator.push( 240 | context, MaterialPageRoute( 241 | builder: (context) { 242 | return NDEFUriRecordSetting(); 243 | })); 244 | if (result != null) { 245 | if (result is ndef.UriRecord) { 246 | setState(() { 247 | _records!.add(result); 248 | }); 249 | } 250 | } 251 | }, 252 | ), 253 | SimpleDialogOption( 254 | child: Text("Raw Record"), 255 | onPressed: () async { 256 | Navigator.pop(context); 257 | final result = await Navigator.push( 258 | context, MaterialPageRoute( 259 | builder: (context) { 260 | return NDEFRecordSetting(); 261 | })); 262 | if (result != null) { 263 | if (result is ndef.NDEFRecord) { 264 | setState(() { 265 | _records!.add(result); 266 | }); 267 | } 268 | } 269 | }, 270 | ), 271 | ]); 272 | }); 273 | }, 274 | child: Text("Add record"), 275 | ) 276 | ], 277 | ), 278 | const SizedBox(height: 10), 279 | Text('Result: $_writeResult'), 280 | const SizedBox(height: 10), 281 | Expanded( 282 | flex: 1, 283 | child: ListView( 284 | shrinkWrap: true, 285 | children: List.generate( 286 | _records!.length, 287 | (index) => GestureDetector( 288 | child: Padding( 289 | padding: const EdgeInsets.all(10), 290 | child: Text( 291 | 'id:${_records![index].idString}\ntnf:${_records![index].tnf}\ntype:${_records![index].type?.toHexString()}\npayload:${_records![index].payload?.toHexString()}\n')), 292 | onTap: () async { 293 | final result = await Navigator.push(context, 294 | MaterialPageRoute(builder: (context) { 295 | return NDEFRecordSetting( 296 | record: _records![index]); 297 | })); 298 | if (result != null) { 299 | if (result is ndef.NDEFRecord) { 300 | setState(() { 301 | _records![index] = result; 302 | }); 303 | } else if (result is String && 304 | result == "Delete") { 305 | _records!.removeAt(index); 306 | } 307 | } 308 | }, 309 | ))), 310 | ), 311 | ]), 312 | ) 313 | ]), 314 | ), 315 | ); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /example/lib/ndef_record/raw_record_setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:ndef/ndef.dart' as ndef; 4 | import 'package:ndef/utilities.dart'; 5 | 6 | class NDEFRecordSetting extends StatefulWidget { 7 | final ndef.NDEFRecord record; 8 | NDEFRecordSetting({super.key, ndef.NDEFRecord? record}) 9 | : record = record ?? ndef.NDEFRecord(); 10 | @override 11 | State createState() => _NDEFRecordSetting(); 12 | } 13 | 14 | class _NDEFRecordSetting extends State { 15 | final GlobalKey _formKey = GlobalKey(); 16 | late TextEditingController _identifierController; 17 | late TextEditingController _payloadController; 18 | late TextEditingController _typeController; 19 | late int _dropButtonValue; 20 | 21 | @override 22 | initState() { 23 | super.initState(); 24 | 25 | if (widget.record.id == null) { 26 | _identifierController = 27 | TextEditingController.fromValue(TextEditingValue(text: "")); 28 | } else { 29 | _identifierController = TextEditingController.fromValue( 30 | TextEditingValue(text: widget.record.id!.toHexString())); 31 | } 32 | if (widget.record.payload == null) { 33 | _payloadController = 34 | TextEditingController.fromValue(TextEditingValue(text: "")); 35 | } else { 36 | _payloadController = TextEditingController.fromValue( 37 | TextEditingValue(text: widget.record.payload!.toHexString())); 38 | } 39 | if (widget.record.encodedType == null && 40 | widget.record.decodedType == null) { 41 | // bug in ndef package (fixed in newest version) 42 | _typeController = 43 | TextEditingController.fromValue(TextEditingValue(text: "")); 44 | } else { 45 | _typeController = TextEditingController.fromValue( 46 | TextEditingValue(text: widget.record.type!.toHexString())); 47 | } 48 | _dropButtonValue = ndef.TypeNameFormat.values.indexOf(widget.record.tnf); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return MaterialApp( 54 | home: Scaffold( 55 | appBar: AppBar( 56 | title: Text('Set Record'), 57 | ), 58 | body: Center( 59 | child: Padding( 60 | padding: const EdgeInsets.symmetric(horizontal: 10), 61 | child: Form( 62 | key: _formKey, 63 | autovalidateMode: AutovalidateMode.always, 64 | child: Column( 65 | mainAxisAlignment: MainAxisAlignment.start, 66 | children: [ 67 | DropdownButton( 68 | value: _dropButtonValue, 69 | items: [ 70 | DropdownMenuItem( 71 | child: Text('empty'), 72 | value: 0, 73 | ), 74 | DropdownMenuItem( 75 | child: Text('nfcWellKnown'), 76 | value: 1, 77 | ), 78 | DropdownMenuItem( 79 | child: Text('media'), 80 | value: 2, 81 | ), 82 | DropdownMenuItem( 83 | child: Text('absoluteURI'), 84 | value: 3, 85 | ), 86 | DropdownMenuItem( 87 | child: Text('nfcExternal'), value: 4), 88 | DropdownMenuItem( 89 | child: Text('unchanged'), value: 5), 90 | DropdownMenuItem( 91 | child: Text('unknown'), 92 | value: 6, 93 | ), 94 | ], 95 | onChanged: (value) { 96 | setState(() { 97 | _dropButtonValue = value as int; 98 | }); 99 | }, 100 | ), 101 | TextFormField( 102 | decoration: 103 | InputDecoration(labelText: 'identifier'), 104 | validator: (v) { 105 | return v!.trim().length % 2 == 0 106 | ? null 107 | : 'length must be even'; 108 | }, 109 | controller: _identifierController, 110 | ), 111 | TextFormField( 112 | decoration: InputDecoration(labelText: 'type'), 113 | validator: (v) { 114 | return v!.trim().length % 2 == 0 115 | ? null 116 | : 'length must be even'; 117 | }, 118 | controller: _typeController, 119 | ), 120 | TextFormField( 121 | decoration: InputDecoration(labelText: 'payload'), 122 | validator: (v) { 123 | return v!.trim().length % 2 == 0 124 | ? null 125 | : 'length must be even'; 126 | }, 127 | controller: _payloadController, 128 | ), 129 | ElevatedButton( 130 | child: Text('OK'), 131 | onPressed: () { 132 | if ((_formKey.currentState as FormState) 133 | .validate()) { 134 | Navigator.pop( 135 | context, 136 | ndef.NDEFRecord( 137 | tnf: ndef.TypeNameFormat 138 | .values[_dropButtonValue], 139 | type: 140 | (_typeController.text).toBytes(), 141 | id: (_identifierController.text) 142 | .toBytes(), 143 | payload: (_payloadController.text) 144 | .toBytes())); 145 | } 146 | }, 147 | ), 148 | ElevatedButton( 149 | child: Text('Delete'), 150 | onPressed: () { 151 | if ((_formKey.currentState as FormState) 152 | .validate()) { 153 | Navigator.pop(context, 'Delete'); 154 | } 155 | }, 156 | ), 157 | ], 158 | )))))); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /example/lib/ndef_record/text_record_setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:ndef/ndef.dart' as ndef; 4 | 5 | class NDEFTextRecordSetting extends StatefulWidget { 6 | final ndef.TextRecord record; 7 | NDEFTextRecordSetting({super.key, ndef.TextRecord? record}) 8 | : record = record ?? ndef.TextRecord(language: 'en', text: ''); 9 | @override 10 | State createState() => _NDEFTextRecordSetting(); 11 | } 12 | 13 | class _NDEFTextRecordSetting extends State { 14 | final GlobalKey _formKey = GlobalKey(); 15 | late TextEditingController _languageController; 16 | late TextEditingController _textController; 17 | late int _dropButtonValue; 18 | 19 | @override 20 | initState() { 21 | super.initState(); 22 | 23 | _languageController = TextEditingController.fromValue( 24 | TextEditingValue(text: widget.record.language!)); 25 | _textController = TextEditingController.fromValue( 26 | TextEditingValue(text: widget.record.text!)); 27 | _dropButtonValue = ndef.TextEncoding.values.indexOf(widget.record.encoding); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return MaterialApp( 33 | home: Scaffold( 34 | appBar: AppBar( 35 | title: Text('Set Record'), 36 | ), 37 | body: Center( 38 | child: Padding( 39 | padding: const EdgeInsets.symmetric(horizontal: 10), 40 | child: Form( 41 | key: _formKey, 42 | autovalidateMode: AutovalidateMode.always, 43 | child: Column( 44 | mainAxisAlignment: MainAxisAlignment.start, 45 | children: [ 46 | DropdownButton( 47 | value: _dropButtonValue, 48 | items: [ 49 | DropdownMenuItem( 50 | child: Text('UTF-8'), value: 0), 51 | DropdownMenuItem( 52 | child: Text('UTF-16'), value: 1), 53 | ], 54 | onChanged: (value) { 55 | setState(() { 56 | _dropButtonValue = value as int; 57 | }); 58 | }, 59 | ), 60 | TextFormField( 61 | decoration: 62 | InputDecoration(labelText: 'language'), 63 | validator: (v) { 64 | return v!.trim().length % 2 == 0 65 | ? null 66 | : 'length must not be blank'; 67 | }, 68 | controller: _languageController, 69 | ), 70 | TextFormField( 71 | decoration: InputDecoration(labelText: 'text'), 72 | controller: _textController, 73 | ), 74 | ElevatedButton( 75 | child: Text('OK'), 76 | onPressed: () { 77 | if ((_formKey.currentState as FormState) 78 | .validate()) { 79 | Navigator.pop( 80 | context, 81 | ndef.TextRecord( 82 | encoding: ndef.TextEncoding 83 | .values[_dropButtonValue], 84 | language: (_languageController.text), 85 | text: (_textController.text))); 86 | } 87 | }, 88 | ), 89 | ], 90 | )))))); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example/lib/ndef_record/uri_record_setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:ndef/ndef.dart' as ndef; 4 | 5 | class NDEFUriRecordSetting extends StatefulWidget { 6 | final ndef.UriRecord record; 7 | NDEFUriRecordSetting({super.key, ndef.UriRecord? record}) 8 | : record = record ?? ndef.UriRecord(prefix: '', content: ''); 9 | @override 10 | State createState() => _NDEFUriRecordSetting(); 11 | } 12 | 13 | class _NDEFUriRecordSetting extends State { 14 | final GlobalKey _formKey = GlobalKey(); 15 | late TextEditingController _contentController; 16 | String? _dropButtonValue; 17 | 18 | @override 19 | initState() { 20 | super.initState(); 21 | 22 | _contentController = TextEditingController.fromValue( 23 | TextEditingValue(text: widget.record.content!)); 24 | _dropButtonValue = widget.record.prefix; 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return MaterialApp( 30 | home: Scaffold( 31 | appBar: AppBar( 32 | title: Text('Set Record'), 33 | ), 34 | body: Center( 35 | child: Padding( 36 | padding: const EdgeInsets.symmetric(horizontal: 10), 37 | child: Form( 38 | key: _formKey, 39 | autovalidateMode: AutovalidateMode.always, 40 | child: Column( 41 | mainAxisAlignment: MainAxisAlignment.start, 42 | children: [ 43 | DropdownButton( 44 | value: _dropButtonValue, 45 | items: ndef.UriRecord.prefixMap.map((value) { 46 | return DropdownMenuItem( 47 | child: Text(value), value: value); 48 | }).toList(), 49 | onChanged: (value) { 50 | setState(() { 51 | _dropButtonValue = value; 52 | }); 53 | }, 54 | ), 55 | TextFormField( 56 | decoration: InputDecoration(labelText: 'content'), 57 | controller: _contentController, 58 | ), 59 | ElevatedButton( 60 | child: Text('OK'), 61 | onPressed: () { 62 | if ((_formKey.currentState as FormState) 63 | .validate()) { 64 | Navigator.pop( 65 | context, 66 | ndef.UriRecord( 67 | prefix: _dropButtonValue, 68 | content: (_contentController.text), 69 | )); 70 | } 71 | }, 72 | ), 73 | ], 74 | )))))); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.18.0" 44 | convert: 45 | dependency: transitive 46 | description: 47 | name: convert 48 | sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "3.1.2" 52 | crypto: 53 | dependency: transitive 54 | description: 55 | name: crypto 56 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.0.6" 60 | cupertino_icons: 61 | dependency: "direct main" 62 | description: 63 | name: cupertino_icons 64 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.0.8" 68 | fake_async: 69 | dependency: transitive 70 | description: 71 | name: fake_async 72 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.3.1" 76 | fixnum: 77 | dependency: transitive 78 | description: 79 | name: fixnum 80 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.1.1" 84 | flutter: 85 | dependency: "direct main" 86 | description: flutter 87 | source: sdk 88 | version: "0.0.0" 89 | flutter_nfc_kit: 90 | dependency: "direct main" 91 | description: 92 | path: ".." 93 | relative: true 94 | source: path 95 | version: "3.6.0" 96 | flutter_test: 97 | dependency: "direct dev" 98 | description: flutter 99 | source: sdk 100 | version: "0.0.0" 101 | flutter_web_plugins: 102 | dependency: transitive 103 | description: flutter 104 | source: sdk 105 | version: "0.0.0" 106 | json_annotation: 107 | dependency: transitive 108 | description: 109 | name: json_annotation 110 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 111 | url: "https://pub.dev" 112 | source: hosted 113 | version: "4.9.0" 114 | leak_tracker: 115 | dependency: transitive 116 | description: 117 | name: leak_tracker 118 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 119 | url: "https://pub.dev" 120 | source: hosted 121 | version: "10.0.5" 122 | leak_tracker_flutter_testing: 123 | dependency: transitive 124 | description: 125 | name: leak_tracker_flutter_testing 126 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 127 | url: "https://pub.dev" 128 | source: hosted 129 | version: "3.0.5" 130 | leak_tracker_testing: 131 | dependency: transitive 132 | description: 133 | name: leak_tracker_testing 134 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 135 | url: "https://pub.dev" 136 | source: hosted 137 | version: "3.0.1" 138 | logging: 139 | dependency: "direct main" 140 | description: 141 | name: logging 142 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "1.3.0" 146 | matcher: 147 | dependency: transitive 148 | description: 149 | name: matcher 150 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "0.12.16+1" 154 | material_color_utilities: 155 | dependency: transitive 156 | description: 157 | name: material_color_utilities 158 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "0.11.1" 162 | meta: 163 | dependency: transitive 164 | description: 165 | name: meta 166 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "1.15.0" 170 | ndef: 171 | dependency: "direct main" 172 | description: 173 | name: ndef 174 | sha256: "5083507cff4bb823b2a198a27ea2c70c4d6bc27a97b66097d966a250e1615d54" 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "0.3.4" 178 | path: 179 | dependency: transitive 180 | description: 181 | name: path 182 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "1.9.0" 186 | sky_engine: 187 | dependency: transitive 188 | description: flutter 189 | source: sdk 190 | version: "0.0.99" 191 | source_span: 192 | dependency: transitive 193 | description: 194 | name: source_span 195 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 196 | url: "https://pub.dev" 197 | source: hosted 198 | version: "1.10.0" 199 | sprintf: 200 | dependency: transitive 201 | description: 202 | name: sprintf 203 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 204 | url: "https://pub.dev" 205 | source: hosted 206 | version: "7.0.0" 207 | stack_trace: 208 | dependency: transitive 209 | description: 210 | name: stack_trace 211 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 212 | url: "https://pub.dev" 213 | source: hosted 214 | version: "1.11.1" 215 | stream_channel: 216 | dependency: transitive 217 | description: 218 | name: stream_channel 219 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 220 | url: "https://pub.dev" 221 | source: hosted 222 | version: "2.1.2" 223 | string_scanner: 224 | dependency: transitive 225 | description: 226 | name: string_scanner 227 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 228 | url: "https://pub.dev" 229 | source: hosted 230 | version: "1.2.0" 231 | term_glyph: 232 | dependency: transitive 233 | description: 234 | name: term_glyph 235 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 236 | url: "https://pub.dev" 237 | source: hosted 238 | version: "1.2.1" 239 | test_api: 240 | dependency: transitive 241 | description: 242 | name: test_api 243 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 244 | url: "https://pub.dev" 245 | source: hosted 246 | version: "0.7.2" 247 | typed_data: 248 | dependency: transitive 249 | description: 250 | name: typed_data 251 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 252 | url: "https://pub.dev" 253 | source: hosted 254 | version: "1.4.0" 255 | uuid: 256 | dependency: transitive 257 | description: 258 | name: uuid 259 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 260 | url: "https://pub.dev" 261 | source: hosted 262 | version: "4.5.1" 263 | vector_math: 264 | dependency: transitive 265 | description: 266 | name: vector_math 267 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 268 | url: "https://pub.dev" 269 | source: hosted 270 | version: "2.1.4" 271 | vm_service: 272 | dependency: transitive 273 | description: 274 | name: vm_service 275 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 276 | url: "https://pub.dev" 277 | source: hosted 278 | version: "14.2.5" 279 | sdks: 280 | dart: ">=3.5.0 <4.0.0" 281 | flutter: ">=3.24.0" 282 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_nfc_kit_example 2 | description: Demonstrates how to use the flutter_nfc_kit plugin. 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: ">=3.2.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | flutter_nfc_kit: 12 | path: ../ 13 | logging: ^1.3.0 14 | ndef: ^0.3.3 15 | cupertino_icons: ^1.0.8 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | 21 | flutter: 22 | uses-material-design: true 23 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcim/flutter_nfc_kit/5756652a73dd3dbd3e75703959c87afb77de7b40/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | flutter_web_plugin_test_example 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_web_plugin_test_example", 3 | "short_name": "flutter_web_plugin_test_example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Demonstrates how to use the flutter_web_plugin_test plugin.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/flutter_nfc_kit.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint flutter_nfc_kit.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'flutter_nfc_kit' 7 | s.version = '3.6.0' 8 | s.summary = 'NFC support plugin of Flutter.' 9 | s.description = <<-DESC 10 | Flutter plugin to provide NFC functionality on Android and iOS, including reading metadata, read & write NDEF records, and transceive layer 3 & 4 data with NFC tags / cards. 11 | DESC 12 | s.homepage = 'https://github.com/nfcim/flutter_nfc_kit' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'nfc.im' => 'nfsee@nfc.im' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'flutter_nfc_kit/Sources/flutter_nfc_kit/**/*.swift' 17 | s.dependency 'Flutter' 18 | s.weak_frameworks = ['CoreNFC'] 19 | s.platform = :ios, '13.0' 20 | 21 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 23 | s.swift_version = '5.0' 24 | end 25 | -------------------------------------------------------------------------------- /ios/flutter_nfc_kit/.gitignore: -------------------------------------------------------------------------------- 1 | .swiftpm/ 2 | -------------------------------------------------------------------------------- /ios/flutter_nfc_kit/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "flutter_nfc_kit", 8 | platforms: [ 9 | .iOS("13.0"), 10 | ], 11 | products: [ 12 | .library(name: "flutter-nfc-kit", targets: ["flutter_nfc_kit"]) 13 | ], 14 | dependencies: [], 15 | targets: [ 16 | .target( 17 | name: "flutter_nfc_kit", 18 | dependencies: [], 19 | resources: [] 20 | ) 21 | ] 22 | ) -------------------------------------------------------------------------------- /lib/flutter_nfc_kit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:ndef/ndef.dart' as ndef; 7 | import 'package:ndef/ndef.dart' show TypeNameFormat; // for generated file 8 | import 'package:ndef/utilities.dart'; 9 | import 'package:json_annotation/json_annotation.dart'; 10 | 11 | part 'flutter_nfc_kit.g.dart'; 12 | 13 | /// Availability of the NFC reader. 14 | enum NFCAvailability { 15 | not_supported, 16 | disabled, 17 | available, 18 | } 19 | 20 | /// Type of NFC tag. 21 | enum NFCTagType { 22 | iso7816, 23 | iso15693, 24 | iso18092, 25 | mifare_classic, 26 | mifare_ultralight, 27 | mifare_desfire, 28 | mifare_plus, 29 | webusb, 30 | unknown, 31 | } 32 | 33 | /// Metadata of a MIFARE-compatible tag 34 | @JsonSerializable() 35 | class MifareInfo { 36 | /// MIFARE type 37 | final String type; 38 | 39 | /// Size in bytes 40 | final int size; 41 | 42 | /// Size of a block (Classic) / page (Ultralight) in bytes 43 | final int blockSize; 44 | 45 | /// Number of blocks (Classic) / pages (Ultralight), -1 if type is unknown 46 | final int blockCount; 47 | 48 | /// Number of sectors (Classic only) 49 | final int? sectorCount; 50 | 51 | MifareInfo( 52 | this.type, this.size, this.blockSize, this.blockCount, this.sectorCount); 53 | 54 | factory MifareInfo.fromJson(Map json) => 55 | _$MifareInfoFromJson(json); 56 | Map toJson() => _$MifareInfoToJson(this); 57 | } 58 | 59 | /// Metadata of the polled NFC tag. 60 | /// 61 | /// All fields except [type] and [standard] are in the format of hex string. 62 | /// Fields that cannot be read will be empty. 63 | @JsonSerializable() 64 | class NFCTag { 65 | /// Tag Type 66 | final NFCTagType type; 67 | 68 | /// The standard that the tag complies with (can be `unknown`) 69 | final String standard; 70 | 71 | /// Tag ID (can be `unknown`) 72 | final String id; 73 | 74 | /// ATQA (Type A only, Android only) 75 | final String? atqa; 76 | 77 | /// SAK (Type A only, Android only) 78 | final String? sak; 79 | 80 | /// Historical bytes (ISO 14443-4A only) 81 | final String? historicalBytes; 82 | 83 | /// Higher layer response (ISO 14443-4B only, Android only) 84 | final String? hiLayerResponse; 85 | 86 | /// Protocol information (Type B only, Android only) 87 | final String? protocolInfo; 88 | 89 | /// Application data (Type B only) 90 | final String? applicationData; 91 | 92 | /// Manufacturer (ISO 18092 only) 93 | final String? manufacturer; 94 | 95 | /// System code (ISO 18092 only) 96 | final String? systemCode; 97 | 98 | /// DSF ID (ISO 15693 only, Android only) 99 | final String? dsfId; 100 | 101 | /// NDEF availability 102 | final bool? ndefAvailable; 103 | 104 | /// NDEF tag type (Android only) 105 | final String? ndefType; 106 | 107 | /// Maximum NDEF message size in bytes (only meaningful when ndef available) 108 | final int? ndefCapacity; 109 | 110 | /// NDEF writebility 111 | final bool? ndefWritable; 112 | 113 | /// Indicates whether this NDEF tag can be made read-only (only works on Android, always false on iOS) 114 | final bool? ndefCanMakeReadOnly; 115 | 116 | /// Custom probe data returned by WebUSB device (see [FlutterNfcKitWeb] for detail, only on Web) 117 | final String? webUSBCustomProbeData; 118 | 119 | /// Mifare-related information (if available) 120 | final MifareInfo? mifareInfo; 121 | 122 | NFCTag( 123 | this.type, 124 | this.id, 125 | this.standard, 126 | this.atqa, 127 | this.sak, 128 | this.historicalBytes, 129 | this.protocolInfo, 130 | this.applicationData, 131 | this.hiLayerResponse, 132 | this.manufacturer, 133 | this.systemCode, 134 | this.dsfId, 135 | this.ndefAvailable, 136 | this.ndefType, 137 | this.ndefCapacity, 138 | this.ndefWritable, 139 | this.ndefCanMakeReadOnly, 140 | this.webUSBCustomProbeData, 141 | this.mifareInfo); 142 | 143 | factory NFCTag.fromJson(Map json) => _$NFCTagFromJson(json); 144 | Map toJson() => _$NFCTagToJson(this); 145 | } 146 | 147 | /// Raw data of a NDEF record. 148 | /// 149 | /// All [String] fields are in hex format. 150 | @JsonSerializable() 151 | class NDEFRawRecord { 152 | /// identifier of the payload (empty if not existed) 153 | final String identifier; 154 | 155 | /// payload 156 | final String payload; 157 | 158 | /// type of the payload 159 | final String type; 160 | 161 | /// type name format (see [ndef](https://pub.dev/packages/ndef) package for detail) 162 | final TypeNameFormat typeNameFormat; 163 | 164 | NDEFRawRecord(this.identifier, this.payload, this.type, this.typeNameFormat); 165 | 166 | factory NDEFRawRecord.fromJson(Map json) => 167 | _$NDEFRawRecordFromJson(json); 168 | Map toJson() => _$NDEFRawRecordToJson(this); 169 | } 170 | 171 | /// Extension for conversion between [NDEFRawRecord] and [ndef.NDEFRecord] 172 | extension NDEFRecordConvert on ndef.NDEFRecord { 173 | /// Convert an [ndef.NDEFRecord] to encoded [NDEFRawRecord] 174 | NDEFRawRecord toRaw() { 175 | return NDEFRawRecord(id?.toHexString() ?? '', payload?.toHexString() ?? '', 176 | type?.toHexString() ?? '', tnf); 177 | } 178 | 179 | /// Convert an [NDEFRawRecord] to decoded [ndef.NDEFRecord]. 180 | /// Use `NDEFRecordConvert.fromRaw` to invoke. 181 | static ndef.NDEFRecord fromRaw(NDEFRawRecord raw) { 182 | return ndef.decodePartialNdefMessage( 183 | raw.typeNameFormat, raw.type.toBytes(), raw.payload.toBytes(), 184 | id: raw.identifier == "" ? null : raw.identifier.toBytes()); 185 | } 186 | } 187 | 188 | /// Request flag for ISO 15693 Tags 189 | class Iso15693RequestFlags { 190 | /// bit 1 191 | bool dualSubCarriers; 192 | 193 | /// bit 2 194 | bool highDataRate; 195 | 196 | /// bit 3 197 | bool inventory; 198 | 199 | /// bit 4 200 | bool protocolExtension; 201 | 202 | /// bit 5 203 | bool select; 204 | 205 | /// bit 6 206 | bool address; 207 | 208 | /// bit 7 209 | bool option; 210 | 211 | /// bit 8 212 | bool commandSpecificBit8; 213 | 214 | /// encode bits to one byte as specified in ISO15693-3 215 | int encode() { 216 | var result = 0; 217 | if (dualSubCarriers) { 218 | result |= 0x01; 219 | } 220 | if (highDataRate) { 221 | result |= 0x02; 222 | } 223 | if (inventory) { 224 | result |= 0x04; 225 | } 226 | if (protocolExtension) { 227 | result |= 0x08; 228 | } 229 | if (select) { 230 | result |= 0x10; 231 | } 232 | if (address) { 233 | result |= 0x20; 234 | } 235 | if (option) { 236 | result |= 0x40; 237 | } 238 | if (commandSpecificBit8) { 239 | result |= 0x80; 240 | } 241 | return result; 242 | } 243 | 244 | Iso15693RequestFlags( 245 | {this.dualSubCarriers = false, 246 | this.highDataRate = false, 247 | this.inventory = false, 248 | this.protocolExtension = false, 249 | this.select = false, 250 | this.address = false, 251 | this.option = false, 252 | this.commandSpecificBit8 = false}); 253 | 254 | /// decode bits from one byte as specified in ISO15693-3 255 | factory Iso15693RequestFlags.fromRaw(int r) { 256 | assert(r >= 0 && r <= 0xFF, "raw flags must be in range [0, 255]"); 257 | var f = Iso15693RequestFlags( 258 | dualSubCarriers: (r & 0x01) != 0, 259 | highDataRate: (r & 0x02) != 0, 260 | inventory: (r & 0x04) != 0, 261 | protocolExtension: (r & 0x08) != 0, 262 | select: (r & 0x10) != 0, 263 | address: (r & 0x20) != 0, 264 | option: (r & 0x40) != 0, 265 | commandSpecificBit8: (r & 0x80) != 0); 266 | return f; 267 | } 268 | } 269 | 270 | /// Main class of NFC Kit 271 | class FlutterNfcKit { 272 | /// Default timeout for [transceive] (in milliseconds) 273 | static const int TRANSCEIVE_TIMEOUT = 5 * 1000; 274 | 275 | /// Default timeout for [poll] (in milliseconds) 276 | static const int POLL_TIMEOUT = 20 * 1000; 277 | 278 | static const MethodChannel _channel = MethodChannel('flutter_nfc_kit/method'); 279 | 280 | static const EventChannel _tagEventChannel = 281 | EventChannel('flutter_nfc_kit/event'); 282 | 283 | /// Stream of NFC tag events. Each event is a [NFCTag] object. 284 | /// 285 | /// This is only supported on Android. 286 | /// On other platforms, this stream will always be empty. 287 | static Stream get tagStream { 288 | return _tagEventChannel.receiveBroadcastStream().map((dynamic event) { 289 | final Map json = jsonDecode(event as String); 290 | return NFCTag.fromJson(json); 291 | }); 292 | } 293 | 294 | /// get the availablility of NFC reader on this device 295 | static Future get nfcAvailability async { 296 | final String availability = 297 | await _channel.invokeMethod('getNFCAvailability'); 298 | return NFCAvailability.values 299 | .firstWhere((it) => it.toString() == "NFCAvailability.$availability"); 300 | } 301 | 302 | /// Try to poll a NFC tag from reader. 303 | /// 304 | /// If tag is successfully polled, a session is started. 305 | /// 306 | /// The [timeout] parameter only works on Android & Web (default to be 20 seconds). On iOS it is ignored and decided by the OS. 307 | /// 308 | /// On iOS, set [iosAlertMessage] to display a message when the session starts (to guide users to scan a tag), 309 | /// and set [iosMultipleTagMessage] to display a message when multiple tags are found. 310 | /// 311 | /// On Android, set [androidPlatformSound] to control whether to play sound when a tag is polled, 312 | /// and set [androidCheckNDEF] to control whether check NDEF records on the tag. 313 | /// 314 | /// The four boolean flags [readIso14443A], [readIso14443B], [readIso18092], [readIso15693] control the NFC technology that would be tried. 315 | /// On iOS, setting any of [readIso14443A] and [readIso14443B] will enable `iso14443` in `pollingOption`. 316 | /// 317 | /// On Web, all parameters are ignored except [timeout] and [probeWebUSBMagic]. 318 | /// If [probeWebUSBMagic] is set, the library will use the `PROBE` request to check whether the device supports our API (see [FlutterNfcKitWeb] for details). 319 | /// 320 | /// Note: Sometimes NDEF check [leads to error](https://github.com/nfcim/flutter_nfc_kit/issues/11), and disabling it might help. 321 | /// If disabled, you will not be able to use any NDEF-related methods in the current session. 322 | static Future poll({ 323 | Duration? timeout, 324 | bool androidPlatformSound = true, 325 | bool androidCheckNDEF = true, 326 | String iosAlertMessage = "Hold your iPhone near the card", 327 | String iosMultipleTagMessage = 328 | "More than one tags are detected, please leave only one tag and try again.", 329 | bool readIso14443A = true, 330 | bool readIso14443B = true, 331 | bool readIso18092 = false, 332 | bool readIso15693 = true, 333 | bool probeWebUSBMagic = false, 334 | }) async { 335 | // use a bitmask for compact representation 336 | int technologies = 0x0; 337 | // hardcoded bits, corresponding to flags in android.nfc.NfcAdapter 338 | if (readIso14443A) technologies |= 0x1; 339 | if (readIso14443B) technologies |= 0x2; 340 | if (readIso18092) technologies |= 0x4; 341 | if (readIso15693) technologies |= 0x8; 342 | // iOS can safely ignore these option bits 343 | if (!androidCheckNDEF) technologies |= 0x80; 344 | if (!androidPlatformSound) technologies |= 0x100; 345 | final String data = await _channel.invokeMethod('poll', { 346 | 'timeout': timeout?.inMilliseconds ?? POLL_TIMEOUT, 347 | 'iosAlertMessage': iosAlertMessage, 348 | 'iosMultipleTagMessage': iosMultipleTagMessage, 349 | 'technologies': technologies, 350 | 'probeWebUSBMagic': probeWebUSBMagic, 351 | }); 352 | return NFCTag.fromJson(jsonDecode(data)); 353 | } 354 | 355 | /// Works only on iOS. 356 | /// 357 | /// Calls `NFCTagReaderSession.restartPolling()`. 358 | /// Call this if you have received "Tag connection lost" exception. 359 | /// This will allow to reconnect to tag without closing system popup. 360 | static Future iosRestartPolling() async => 361 | await _channel.invokeMethod("restartPolling"); 362 | 363 | /// Transceive data with the card / tag in the format of APDU (iso7816) or raw commands (other technologies). 364 | /// The [capdu] can be either of type Uint8List or hex string. 365 | /// Return value will be in the same type of [capdu]. 366 | /// 367 | /// There must be a valid session when invoking. 368 | /// 369 | /// On Android, [timeout] parameter will set transceive execution timeout that is persistent during a active session. 370 | /// Also, Ndef TagTechnology will be closed if active. 371 | /// On iOS, this parameter is ignored and is decided by the OS. 372 | /// On Web, [timeout] is currently not 373 | /// Timeout is reset to default value when [finish] is called, and could be changed by multiple calls to [transceive]. 374 | static Future transceive(T capdu, {Duration? timeout}) async { 375 | assert(capdu is String || capdu is Uint8List); 376 | return await _channel.invokeMethod('transceive', { 377 | 'data': capdu, 378 | 'timeout': timeout?.inMilliseconds ?? TRANSCEIVE_TIMEOUT 379 | }); 380 | } 381 | 382 | /// Read NDEF records (in decoded format, Android & iOS only). 383 | /// 384 | /// There must be a valid session when invoking. 385 | /// [cached] only works on Android, allowing cached read (may obtain stale data). 386 | /// On Android, this would cause any other open TagTechnology to be closed. 387 | /// See [ndef](https://pub.dev/packages/ndef) for usage of [ndef.NDEFRecord]. 388 | static Future> readNDEFRecords({bool? cached}) async { 389 | return (await readNDEFRawRecords(cached: cached)) 390 | .map((r) => NDEFRecordConvert.fromRaw(r)) 391 | .toList(); 392 | } 393 | 394 | /// Read NDEF records (in raw data, Android & iOS only). 395 | /// 396 | /// There must be a valid session when invoking. 397 | /// [cached] only works on Android, allowing cached read (may obtain stale data). 398 | /// On Android, this would cause any other open TagTechnology to be closed. 399 | /// Please use [readNDEFRecords] if you want decoded NDEF records 400 | static Future> readNDEFRawRecords({bool? cached}) async { 401 | final String data = 402 | await _channel.invokeMethod('readNDEF', {'cached': cached ?? false}); 403 | return (jsonDecode(data) as List) 404 | .map((object) => NDEFRawRecord.fromJson(object)) 405 | .toList(); 406 | } 407 | 408 | /// Write NDEF records (in decoded format, Android & iOS only). 409 | /// 410 | /// There must be a valid session when invoking. 411 | /// [cached] only works on Android, allowing cached read (may obtain stale data). 412 | /// On Android, this would cause any other open TagTechnology to be closed. 413 | /// See [ndef](https://pub.dev/packages/ndef) for usage of [ndef.NDEFRecord] 414 | static Future writeNDEFRecords(List message) async { 415 | return await writeNDEFRawRecords(message.map((r) => r.toRaw()).toList()); 416 | } 417 | 418 | /// Write NDEF records (in raw data, Android & iOS only). 419 | /// 420 | /// There must be a valid session when invoking. 421 | /// [message] is a list of NDEFRawRecord. 422 | static Future writeNDEFRawRecords(List message) async { 423 | var data = jsonEncode(message); 424 | return await _channel.invokeMethod('writeNDEF', {'data': data}); 425 | } 426 | 427 | /// Finish current session in polling mode. 428 | /// 429 | /// You must invoke it before start a new session. 430 | /// 431 | /// On iOS, use [iosAlertMessage] to indicate success or [iosErrorMessage] to indicate failure. 432 | /// If both parameters are set, [iosErrorMessage] will be used. 433 | /// On Web, set [closeWebUSB] to `true` to end the session, so that user can choose a different device in next [poll]. 434 | static Future finish( 435 | {String? iosAlertMessage, 436 | String? iosErrorMessage, 437 | bool? closeWebUSB}) async { 438 | return await _channel.invokeMethod('finish', { 439 | 'iosErrorMessage': iosErrorMessage, 440 | 'iosAlertMessage': iosAlertMessage, 441 | 'closeWebUSB': closeWebUSB ?? false, 442 | }); 443 | } 444 | 445 | /// iOS only, change currently displayed NFC reader session alert message with [message]. 446 | /// 447 | /// There must be a valid session when invoking. 448 | /// On Android, call to this function does nothing. 449 | static Future setIosAlertMessage(String message) async { 450 | if (!kIsWeb) { 451 | return await _channel.invokeMethod('setIosAlertMessage', message); 452 | } 453 | } 454 | 455 | /// Make the NDEF tag readonly (i.e. lock the NDEF tag, Android & iOS only). 456 | /// 457 | /// **WARNING: IT CANNOT BE UNDONE!** 458 | static Future makeNdefReadOnly() async { 459 | return await _channel.invokeMethod('makeNdefReadOnly'); 460 | } 461 | 462 | /// Authenticate against a sector of MIFARE Classic tag (Android only). 463 | /// 464 | /// Either one of [keyA] or [keyB] must be provided. 465 | /// If both are provided, [keyA] will be used. 466 | /// Returns whether authentication succeeds. 467 | static Future authenticateSector(int index, 468 | {T? keyA, T? keyB}) async { 469 | assert((keyA is String || keyA is Uint8List) || 470 | (keyB is String || keyB is Uint8List)); 471 | return await _channel.invokeMethod( 472 | 'authenticateSector', {'index': index, 'keyA': keyA, 'keyB': keyB}); 473 | } 474 | 475 | /// Read one unit of data (specified below) from: 476 | /// * MIFARE Classic / Ultralight tag: one 16B block / page (Android only) 477 | /// * ISO 15693 tag: one 4B block (iOS only) 478 | /// 479 | /// There must be a valid session when invoking. 480 | /// [index] refers to the block / page index. 481 | /// For MIFARE Classic tags, you must first authenticate against the corresponding sector. 482 | /// For MIFARE Ultralight tags, four consecutive pages will be read. 483 | /// Returns data in [Uint8List]. 484 | static Future readBlock(int index, 485 | {Iso15693RequestFlags? iso15693Flags, 486 | bool iso15693ExtendedMode = false}) async { 487 | var flags = iso15693Flags ?? Iso15693RequestFlags(); 488 | return await _channel.invokeMethod('readBlock', { 489 | 'index': index, 490 | 'iso15693Flags': flags.encode(), 491 | 'iso15693ExtendedMode': iso15693ExtendedMode, 492 | }); 493 | } 494 | 495 | /// Write one unit of data (specified below) to: 496 | /// * MIFARE Classic tag: one 16B block (Android only) 497 | /// * MIFARE Ultralight tag: one 4B page (Android only) 498 | /// * ISO 15693 tag: one 4B block (iOS only) 499 | /// 500 | /// There must be a valid session when invoking. 501 | /// [index] refers to the block / page index. 502 | /// For MIFARE Classic tags, you must first authenticate against the corresponding sector. 503 | static Future writeBlock(int index, T data, 504 | {Iso15693RequestFlags? iso15693Flags, 505 | bool iso15693ExtendedMode = false}) async { 506 | assert(data is String || data is Uint8List); 507 | var flags = iso15693Flags ?? Iso15693RequestFlags(); 508 | await _channel.invokeMethod('writeBlock', { 509 | 'index': index, 510 | 'data': data, 511 | 'iso15693Flags': flags.encode(), 512 | 'iso15693ExtendedMode': iso15693ExtendedMode, 513 | }); 514 | } 515 | 516 | /// Read one sector from MIFARE Classic tag (Android Only) 517 | /// 518 | /// There must be a valid session when invoking. 519 | /// [index] refers to the sector index. 520 | /// You must first authenticate against the corresponding sector. 521 | /// Note: not all sectors are 64B long, some tags might have 256B sectors. 522 | /// Returns data in [Uint8List]. 523 | static Future readSector(int index) async { 524 | return await _channel.invokeMethod('readSector', {'index': index}); 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /lib/flutter_nfc_kit.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'flutter_nfc_kit.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | MifareInfo _$MifareInfoFromJson(Map json) => MifareInfo( 10 | json['type'] as String, 11 | json['size'] as int, 12 | json['blockSize'] as int, 13 | json['blockCount'] as int, 14 | json['sectorCount'] as int?, 15 | ); 16 | 17 | Map _$MifareInfoToJson(MifareInfo instance) => 18 | { 19 | 'type': instance.type, 20 | 'size': instance.size, 21 | 'blockSize': instance.blockSize, 22 | 'blockCount': instance.blockCount, 23 | 'sectorCount': instance.sectorCount, 24 | }; 25 | 26 | NFCTag _$NFCTagFromJson(Map json) => NFCTag( 27 | $enumDecode(_$NFCTagTypeEnumMap, json['type']), 28 | json['id'] as String, 29 | json['standard'] as String, 30 | json['atqa'] as String?, 31 | json['sak'] as String?, 32 | json['historicalBytes'] as String?, 33 | json['protocolInfo'] as String?, 34 | json['applicationData'] as String?, 35 | json['hiLayerResponse'] as String?, 36 | json['manufacturer'] as String?, 37 | json['systemCode'] as String?, 38 | json['dsfId'] as String?, 39 | json['ndefAvailable'] as bool?, 40 | json['ndefType'] as String?, 41 | json['ndefCapacity'] as int?, 42 | json['ndefWritable'] as bool?, 43 | json['ndefCanMakeReadOnly'] as bool?, 44 | json['webUSBCustomProbeData'] as String?, 45 | json['mifareInfo'] == null 46 | ? null 47 | : MifareInfo.fromJson(json['mifareInfo'] as Map), 48 | ); 49 | 50 | Map _$NFCTagToJson(NFCTag instance) => { 51 | 'type': _$NFCTagTypeEnumMap[instance.type]!, 52 | 'standard': instance.standard, 53 | 'id': instance.id, 54 | 'atqa': instance.atqa, 55 | 'sak': instance.sak, 56 | 'historicalBytes': instance.historicalBytes, 57 | 'hiLayerResponse': instance.hiLayerResponse, 58 | 'protocolInfo': instance.protocolInfo, 59 | 'applicationData': instance.applicationData, 60 | 'manufacturer': instance.manufacturer, 61 | 'systemCode': instance.systemCode, 62 | 'dsfId': instance.dsfId, 63 | 'ndefAvailable': instance.ndefAvailable, 64 | 'ndefType': instance.ndefType, 65 | 'ndefCapacity': instance.ndefCapacity, 66 | 'ndefWritable': instance.ndefWritable, 67 | 'ndefCanMakeReadOnly': instance.ndefCanMakeReadOnly, 68 | 'webUSBCustomProbeData': instance.webUSBCustomProbeData, 69 | 'mifareInfo': instance.mifareInfo, 70 | }; 71 | 72 | const _$NFCTagTypeEnumMap = { 73 | NFCTagType.iso7816: 'iso7816', 74 | NFCTagType.iso15693: 'iso15693', 75 | NFCTagType.iso18092: 'iso18092', 76 | NFCTagType.mifare_classic: 'mifare_classic', 77 | NFCTagType.mifare_ultralight: 'mifare_ultralight', 78 | NFCTagType.mifare_desfire: 'mifare_desfire', 79 | NFCTagType.mifare_plus: 'mifare_plus', 80 | NFCTagType.webusb: 'webusb', 81 | NFCTagType.unknown: 'unknown', 82 | }; 83 | 84 | NDEFRawRecord _$NDEFRawRecordFromJson(Map json) => 85 | NDEFRawRecord( 86 | json['identifier'] as String, 87 | json['payload'] as String, 88 | json['type'] as String, 89 | $enumDecode(_$TypeNameFormatEnumMap, json['typeNameFormat']), 90 | ); 91 | 92 | Map _$NDEFRawRecordToJson(NDEFRawRecord instance) => 93 | { 94 | 'identifier': instance.identifier, 95 | 'payload': instance.payload, 96 | 'type': instance.type, 97 | 'typeNameFormat': _$TypeNameFormatEnumMap[instance.typeNameFormat]!, 98 | }; 99 | 100 | const _$TypeNameFormatEnumMap = { 101 | TypeNameFormat.empty: 'empty', 102 | TypeNameFormat.nfcWellKnown: 'nfcWellKnown', 103 | TypeNameFormat.media: 'media', 104 | TypeNameFormat.absoluteURI: 'absoluteURI', 105 | TypeNameFormat.nfcExternal: 'nfcExternal', 106 | TypeNameFormat.unknown: 'unknown', 107 | TypeNameFormat.unchanged: 'unchanged', 108 | }; 109 | -------------------------------------------------------------------------------- /lib/flutter_nfc_kit_web.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | // In order to *not* need this ignore, consider extracting the "web" version 3 | // of your plugin as a separate package, instead of inlining it in the same 4 | // package as the core of your plugin. 5 | // ignore: avoid_web_libraries_in_flutter 6 | import 'dart:html' as html show window; 7 | import 'dart:js_util'; 8 | import 'package:convert/convert.dart'; 9 | 10 | import 'package:flutter/services.dart'; 11 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 12 | import 'package:flutter_nfc_kit/webusb_interop.dart'; 13 | 14 | /// A web implementation of the FlutterNfcKit plugin. 15 | /// 16 | /// Note: you should **NEVER use this class directly**, but instead use the [FlutterNfcKit] class in your project. 17 | /// 18 | /// You **need to implement our WebUSB protocol** on the device side for this to work. 19 | /// See [WebUSB.md](https://github.com/nfcim/flutter_nfc_kit/blob/master/WebUSB.md) for protocol details. 20 | class FlutterNfcKitWeb { 21 | static void registerWith(Registrar registrar) { 22 | final MethodChannel channel = MethodChannel( 23 | 'flutter_nfc_kit/method', 24 | const StandardMethodCodec(), 25 | registrar, 26 | ); 27 | 28 | final pluginInstance = FlutterNfcKitWeb(); 29 | channel.setMethodCallHandler(pluginInstance.handleMethodCall); 30 | } 31 | 32 | /// Handles method calls over the MethodChannel of this plugin. 33 | /// Note: Check the "federated" architecture for a new way of doing this: 34 | /// https://flutter.dev/go/federated-plugins 35 | Future handleMethodCall(MethodCall call) async { 36 | switch (call.method) { 37 | case 'getNFCAvailability': 38 | if (hasProperty(html.window.navigator, 'usb')) { 39 | return 'available'; 40 | } else { 41 | return 'not_supported'; 42 | } 43 | 44 | case 'poll': 45 | int timeout = call.arguments["timeout"]; 46 | bool probe = call.arguments["probeWebUSBMagic"]; 47 | return await WebUSB.poll(timeout, probe); 48 | 49 | case 'transceive': 50 | var data = call.arguments["data"]; 51 | if (!(data is Uint8List || data is String)) { 52 | throw PlatformException( 53 | code: "400", 54 | message: 55 | "Bad argument: data should be String or Uint8List, got $data"); 56 | } 57 | // always pass hex string to [transceive] 58 | var encodedData = data; 59 | if (data is Uint8List) { 60 | encodedData = hex.encode(data); 61 | } 62 | var encodedResp = await WebUSB.transceive(encodedData); 63 | dynamic resp = encodedResp; 64 | // return type should be the same as [data] 65 | if (data is Uint8List) { 66 | resp = Uint8List.fromList(hex.decode(encodedResp)); 67 | } 68 | return resp; 69 | 70 | case 'finish': 71 | bool closeWebUSB = call.arguments["closeWebUSB"]; 72 | return await WebUSB.finish(closeWebUSB); 73 | 74 | default: 75 | throw PlatformException( 76 | code: "501", 77 | details: 78 | "flutter_nfc_kit for web does not support \"${call.method}\""); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/webusb_interop.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | 3 | /// Library that inter-ops with JavaScript on WebUSB APIs. 4 | /// 5 | /// Note: you should **NEVER use this library directly**, but instead use the [FlutterNfcKit] class in your project. 6 | library; 7 | 8 | import 'dart:convert'; 9 | import 'dart:js_util'; 10 | import 'dart:async'; 11 | import 'dart:typed_data'; 12 | import 'dart:js_interop'; 13 | 14 | import 'package:convert/convert.dart'; 15 | import 'package:flutter/services.dart'; 16 | import 'package:logging/logging.dart'; 17 | 18 | final log = Logger('FlutterNFCKit:WebUSB'); 19 | 20 | /// The USB class code used to identify a WebUSB device that supports this protocol. 21 | const int USB_CLASS_CODE_VENDOR_SPECIFIC = 0xFF; 22 | 23 | @JS('navigator.usb') 24 | extension type _USB._(JSObject _) implements JSObject { 25 | external static JSObject requestDevice(_USBDeviceRequestOptions options); 26 | external static set ondisconnect(JSFunction value); 27 | } 28 | 29 | @JS() 30 | @anonymous 31 | extension type _USBDeviceRequestOptions._(JSObject _) implements JSObject { 32 | external factory _USBDeviceRequestOptions( 33 | {JSArray<_USBDeviceFilter> filters}); 34 | } 35 | 36 | @JS() 37 | @anonymous 38 | extension type _USBDeviceFilter._(JSObject _) implements JSObject { 39 | external factory _USBDeviceFilter({int classCode}); 40 | } 41 | 42 | @JS() 43 | @anonymous 44 | extension type _USBControlTransferParameters._(JSObject _) implements JSObject { 45 | external factory _USBControlTransferParameters( 46 | {String requestType, 47 | String recipient, 48 | int request, 49 | int value, 50 | int index}); 51 | } 52 | 53 | /// Wraps around WebUSB APIs from browsers to provide low-level interfaces such as [poll] for [FlutterNfcKitWeb]. 54 | /// 55 | /// Note: you should **NEVER use this class directly**, but instead use the [FlutterNfcKit] class in your project. 56 | class WebUSB { 57 | static dynamic _device; 58 | static String customProbeData = ""; 59 | static Function? onDisconnect; 60 | 61 | static bool _deviceAvailable() { 62 | return _device != null && getProperty(_device, 'opened'); 63 | } 64 | 65 | static const USB_PROBE_MAGIC = '_NFC_IM_'; 66 | 67 | /// Try to poll a WebUSB device according to our protocol. 68 | static Future poll(int timeout, bool probeMagic) async { 69 | // request WebUSB device with custom classcode 70 | if (!_deviceAvailable()) { 71 | var devicePromise = _USB.requestDevice(_USBDeviceRequestOptions( 72 | filters: [_USBDeviceFilter(classCode: USB_CLASS_CODE_VENDOR_SPECIFIC)] 73 | .toJS)); 74 | dynamic device = await promiseToFuture(devicePromise); 75 | try { 76 | await promiseToFuture(callMethod(device, 'open', List.empty())) 77 | .then((_) => 78 | promiseToFuture(callMethod(device, 'claimInterface', [1]))) 79 | .timeout(Duration(milliseconds: timeout)); 80 | _device = device; 81 | _USB.ondisconnect = () { 82 | _device = null; 83 | onDisconnect?.call(); 84 | }.toJS; 85 | log.info("WebUSB device opened", _device); 86 | } on TimeoutException catch (_) { 87 | log.severe("Polling tag timeout"); 88 | throw PlatformException(code: "408", message: "Polling tag timeout"); 89 | } on Exception catch (e) { 90 | log.severe("Poll error", e); 91 | throw PlatformException( 92 | code: "500", message: "WebUSB API error", details: e); 93 | } 94 | 95 | if (probeMagic) { 96 | try { 97 | // PROBE request 98 | var promise = callMethod(_device, 'controlTransferIn', [ 99 | _USBControlTransferParameters( 100 | requestType: 'vendor', 101 | recipient: 'interface', 102 | request: 0xff, 103 | value: 0, 104 | index: 1), 105 | 1 106 | ]); 107 | var resp = await promiseToFuture(promise); 108 | if (getProperty(resp, 'status') == 'stalled') { 109 | throw PlatformException( 110 | code: "500", message: "Device error: transfer stalled"); 111 | } 112 | var result = 113 | (getProperty(resp, 'data').buffer as ByteBuffer).asUint8List(); 114 | if (result.length < USB_PROBE_MAGIC.length || 115 | result.sublist(0, USB_PROBE_MAGIC.length) != 116 | Uint8List.fromList(USB_PROBE_MAGIC.codeUnits)) { 117 | throw PlatformException( 118 | code: "500", 119 | message: 120 | "Device error: invalid probe response: ${hex.encode(result)}, should begin with $USB_PROBE_MAGIC"); 121 | } 122 | customProbeData = hex.encode(result.sublist(USB_PROBE_MAGIC.length)); 123 | } on Exception catch (e) { 124 | log.severe("Probe error", e); 125 | throw PlatformException( 126 | code: "500", message: "WebUSB API error", details: e); 127 | } 128 | } else { 129 | customProbeData = ""; 130 | } 131 | } 132 | // get VID & PID 133 | int vendorId = getProperty(_device, 'vendorId'); 134 | int productId = getProperty(_device, 'productId'); 135 | String id = 136 | '${vendorId.toRadixString(16).padLeft(4, '0')}:${productId.toRadixString(16).padLeft(4, '0')}'; 137 | return json.encode({ 138 | 'type': 'webusb', 139 | 'id': id, 140 | 'standard': 'nfc-im-webusb-protocol', 141 | 'customProbeData': customProbeData 142 | }); 143 | } 144 | 145 | static Future _doTransceive(Uint8List capdu) async { 146 | // send a command (CMD) 147 | var promise = callMethod(_device, 'controlTransferOut', [ 148 | _USBControlTransferParameters( 149 | requestType: 'vendor', 150 | recipient: 'interface', 151 | request: 0, 152 | value: 0, 153 | index: 1), 154 | capdu 155 | ]); 156 | await promiseToFuture(promise); 157 | // wait for execution to finish (STAT) 158 | while (true) { 159 | promise = callMethod(_device, 'controlTransferIn', [ 160 | _USBControlTransferParameters( 161 | requestType: 'vendor', 162 | recipient: 'interface', 163 | request: 2, 164 | value: 0, 165 | index: 1), 166 | 1 167 | ]); 168 | var resp = await promiseToFuture(promise); 169 | if (getProperty(resp, 'status') == 'stalled') { 170 | throw PlatformException( 171 | code: "500", message: "Device error: transfer stalled"); 172 | } 173 | var code = getProperty(resp, 'data').buffer.asUint8List()[0]; 174 | if (code == 0) { 175 | break; 176 | } else if (code == 1) { 177 | await Future.delayed(const Duration(microseconds: 100)); 178 | } else { 179 | throw PlatformException( 180 | code: "500", message: "Device error: unexpected RESP code $code"); 181 | } 182 | } 183 | // get the response (RESP) 184 | promise = callMethod(_device, 'controlTransferIn', [ 185 | _USBControlTransferParameters( 186 | requestType: 'vendor', 187 | recipient: 'interface', 188 | request: 1, 189 | value: 0, 190 | index: 1), 191 | 1500 192 | ]); 193 | var resp = await promiseToFuture(promise); 194 | var deviceStatus = getProperty(resp, 'status'); 195 | if (deviceStatus != 'ok') { 196 | throw PlatformException( 197 | code: "500", 198 | message: 199 | "Device error: status should be \"ok\", got \"$deviceStatus\""); 200 | } 201 | return getProperty(resp, 'data').buffer.asUint8List(); 202 | } 203 | 204 | /// Transceive data with polled WebUSB device according to our protocol. 205 | static Future transceive(String capdu) async { 206 | log.config('CAPDU: $capdu'); 207 | if (!_deviceAvailable()) { 208 | throw PlatformException( 209 | code: "406", message: "No tag polled or device already disconnected"); 210 | } 211 | try { 212 | var rawCAPDU = Uint8List.fromList(hex.decode(capdu)); 213 | var rawRAPDU = await _doTransceive(rawCAPDU); 214 | String rapdu = hex.encode(rawRAPDU); 215 | log.config('RAPDU: $rapdu'); 216 | return rapdu; 217 | } on TimeoutException catch (_) { 218 | log.severe("Transceive timeout"); 219 | throw PlatformException(code: "408", message: "Transceive timeout"); 220 | } on PlatformException catch (e) { 221 | log.severe("Transceive error", e); 222 | rethrow; 223 | } on Exception catch (e) { 224 | log.severe("Transceive error", e); 225 | throw PlatformException( 226 | code: "500", message: "WebUSB API error", details: e); 227 | } 228 | } 229 | 230 | /// Finish this session, also end WebUSB session if explicitly asked by user. 231 | static Future finish(bool closeWebUSB) async { 232 | if (_deviceAvailable()) { 233 | if (closeWebUSB) { 234 | try { 235 | await promiseToFuture(callMethod(_device, "close", List.empty())); 236 | } on Exception catch (e) { 237 | log.severe("Finish error: ", e); 238 | throw PlatformException( 239 | code: "500", message: "WebUSB API error", details: e); 240 | } 241 | _device = null; 242 | } 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_nfc_kit 2 | 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 3 | version: 3.6.0 4 | homepage: "https://github.com/nfcim/flutter_nfc_kit" 5 | 6 | environment: 7 | sdk: ">=3.5.0 <4.0.0" 8 | flutter: ">=3.24.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_web_plugins: 14 | sdk: flutter 15 | json_annotation: ^4.8.1 16 | ndef: ^0.3.3 17 | convert: ^3.1.1 18 | logging: ^1.2.0 19 | 20 | dev_dependencies: 21 | flutter_test: 22 | sdk: flutter 23 | lints: ^5.0.0 24 | build_runner: ^2.4.9 25 | json_serializable: ^6.7.1 26 | 27 | flutter: 28 | plugin: 29 | platforms: 30 | android: 31 | package: im.nfc.flutter_nfc_kit 32 | pluginClass: FlutterNfcKitPlugin 33 | ios: 34 | pluginClass: FlutterNfcKitPlugin 35 | web: 36 | pluginClass: FlutterNfcKitWeb 37 | fileName: flutter_nfc_kit_web.dart 38 | --------------------------------------------------------------------------------