├── .github └── workflows │ └── build-example-app.yml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README-xcode.png ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── semler │ └── nfc_in_flutter │ ├── NfcInFlutterException.java │ └── NfcInFlutterPlugin.java ├── example ├── .gitignore ├── .metadata ├── README-photo.png ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── dev │ │ │ │ │ └── semler │ │ │ │ │ └── nfc_in_flutter_example │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── 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 │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Flutter.podspec │ │ └── 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.h │ │ ├── AppDelegate.m │ │ ├── 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.entitlements │ │ └── main.m ├── lib │ ├── main.dart │ ├── read_example_screen.dart │ └── write_example_screen.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── NfcInFlutterPlugin.h │ └── NfcInFlutterPlugin.m └── nfc_in_flutter.podspec ├── lib ├── nfc_in_flutter.dart └── src │ ├── api.dart │ └── exceptions.dart ├── nfc_in_flutter.iml ├── pubspec.lock ├── pubspec.yaml └── test └── nfc_in_flutter_test.dart /.github/workflows/build-example-app.yml: -------------------------------------------------------------------------------- 1 | name: Build example app 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-android: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: Download Flutter 11 | run: curl -o flutter_linux_v1.12.13+hotfix.9-stable.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.9-stable.tar.xz 12 | - name: Unpack Flutter 13 | run: tar xf flutter_linux_v1.12.13+hotfix.9-stable.tar.xz 14 | - name: Run Flutter doctor 15 | run: ./flutter/bin/flutter doctor 16 | - name: Build example/ 17 | run: cd example && ../flutter/bin/flutter build appbundle 18 | build-ios: 19 | runs-on: macOS-latest 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: Download Flutter 23 | run: curl -o flutter_macos_v1.12.13+hotfix.9-stable.zip https://storage.googleapis.com/flutter_infra/releases/stable/macos/flutter_macos_v1.12.13+hotfix.9-stable.zip 24 | - name: Unpack Flutter 25 | run: unzip flutter_macos_v1.12.13+hotfix.9-stable.zip 26 | - name: Run Flutter doctor 27 | run: ./flutter/bin/flutter doctor 28 | - name: Build example/ 29 | run: cd example && ../flutter/bin/flutter build ios --no-codesign 30 | timeout-minutes: 10 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .idea 10 | -------------------------------------------------------------------------------- /.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: bc7bc940836f1f834699625426795fd6f07c18ec 8 | channel: beta 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "example/lib/main.dart" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.5 2 | 3 | - Better reading reliability on iOS 4 | - New property `rawPayload` on `NDEFRecord` which contains the full, unedited record payload (credit to GitHub user @laurensfischer). 5 | 6 | ## 2.0.4 7 | 8 | - CoreNFC is now a `weak_framework`. This should fix a crash on iOS devices that doesn't support NFC. (credit to GitHub user @mxpazyj) 9 | - Added `alertMessage` argument to `readNDEF()`. This controls the message on the iOS NFC modal. (credit to GitHub user @dghilardi) 10 | - Actually fixed `NDEFRecord.languageCode` being ignored when writing 11 | - Support for reading and writing to empty tags on Android 12 | - Fixed CoreNFC crashing after cancelling reading multiple times in a row (credit to GitHub user @martyfuhry) 13 | 14 | ## 2.0.3 15 | 16 | - ~~Fixed `NDEFRecord.languageCode` being ignored when writing~~ 17 | 18 | ## 2.0.2 19 | 20 | - Fixed a crash when reading tags containing records with a custom url protocol and well known type URL 21 | 22 | ## 2.0.1 23 | 24 | - Fixed writing TNF text records on iOS (credit to GitHub user @janipiippow) 25 | 26 | ## 2.0.0 27 | 28 | - Added `noSounds` flag to `NFCNormalReaderMode` 29 | 30 | On Android, this tells the system not to play sounds when a NFC chip is scanned. 31 | 32 | - Support for writing NDEF messages has been added 33 | 34 | Get acccess to tags using the new `.tag` property on messages, which you can 35 | use to connect and write to tags. 36 | 37 | - Added the following methods for constructing NDEF messages: 38 | 39 | `NDEFRecord.empty` for empty records 40 | 41 | `NDEFRecord.plain` for `text/plain` records 42 | 43 | `NDEFRecord.type` for records with custom types 44 | 45 | `NDEFRecord.text` for records with well known text types 46 | 47 | `NDEFRecord.uri` for records with well known URI types 48 | 49 | `NDEFRecord.absoluteUri` 50 | 51 | `NDEFRecord.external` 52 | 53 | `NDEFRecord.custom` 54 | 55 | - **COULD BE BREAKING**: Records with type T and U (with well known TNF) will 56 | now be correctly constructed. URI records will have the URL prefix added to the 57 | `.payload` and Text records will now correctly have thr first prefix byte removed from the `.payload`. If you want the precise value, you can use the new `.data` property which excludes the URL prefix of URI records and language codes of Text records. 58 | 59 | - Added `.data` property to `NDEFRecord` which excludes URL prefixes and 60 | language codes from records with well known types. 61 | 62 | - Added `.languageCode` property to `NDEFRecord` which will contain the language 63 | code of a record with a well known text type. 64 | 65 | - Updated the `.tnf` property on `NDEFRecord`s. This is now an enumerable 66 | (`NFCTypeNameFormat`) with it's value mapped to the correct TNF value. 67 | This works on both Android and iOS where as it previously did not. 68 | 69 | ## 1.2.0 70 | 71 | - Added `id` property to `NDEFMessage` which contains the NFC tag's UID 72 | - Support for more card types on Android 73 | 74 | ## 1.1.1 75 | 76 | - Bugfix: Android sessions are now closed properly 77 | 78 | ## 1.1.0 79 | 80 | - Added support for reading from emulated host cards 81 | 82 | ## 1.0.0 83 | 84 | - First release, woohoo! 85 | - Support for reading NDEF formatted NFC tags on both Android and iOS 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andi Robin Halgren Semler 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-xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/README-xcode.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nfc_in_flutter 2 | 3 | NFC in Flutter is a plugin for reading and writing NFC tags in Flutter. It works on both Android and iOS with a simple stream interface. 4 | 5 | ⚠️ Currently only NDEF formatted tags are supported. 6 | 7 | ## Usage 8 | 9 | ### Read NFC tags 10 | 11 | ```dart 12 | // NFC.readNDEF returns a stream of NDEFMessage 13 | Stream stream = NFC.readNDEF(); 14 | 15 | stream.listen((NDEFMessage message) { 16 | print("records: ${message.records.length}"); 17 | }); 18 | ``` 19 | 20 | ### Read one NFC tag 21 | 22 | ```dart 23 | NDEFMessage message = await NFC.readNDEF(once: true).first; 24 | print("payload: ${message.payload}"); 25 | // once: true` only scans one tag! 26 | ``` 27 | 28 | ### Writing to tags 29 | 30 | You can access a message's NFC tag using the `NDEFMessage`'s `.tag` property. The tag has a `.write` method, which allows you to write a NDEF message to the tag. 31 | 32 | _Note that the read stream must still be open when the `.write` method is called. This means that the `once` argument in `.readNDEF()` cannot be used._ 33 | 34 | ```dart 35 | Stream stream = NFC.readNDEF(); 36 | 37 | stream.listen((NDEFMessage message) { 38 | NDEFMessage newMessage = NDEFMessage.withRecords( 39 | NDEFRecord.mime("text/plain", "hello world") 40 | ); 41 | message.tag.write(newMessage); 42 | }); 43 | ``` 44 | 45 | You can also use the `NFC.writeNDEF(NDEFMessage)` method, which wraps the code above with support for the `once` argument. 46 | 47 | ```dart 48 | NDEFMessage newMessage = NDEFMessage.withRecords( 49 | NDEFRecord.mime("text/plain", "hello world") 50 | ); 51 | Stream stream = NFC.writeNDEF(newMessage); 52 | 53 | stream.listen((NDEFTag tag) { 54 | print("wrote to tag"); 55 | }); 56 | ``` 57 | 58 | If you only want to write to one tag, you can set the `once` argument to true. 59 | 60 | ```dart 61 | NDEFMessage newMessage = NDEFMessage.withRecords( 62 | NDEFRecord.mime("text/plain", "hello world") 63 | ); 64 | Stream stream = NFC.writeNDEF(newMessage, once: true); 65 | 66 | stream.listen((NDEFTag tag) { 67 | print("only wrote to one tag!"); 68 | }); 69 | ``` 70 | 71 | And if you would rather use a `Future` based API, you can await the returned stream's `.first` method. 72 | 73 | ```dart 74 | NDEFMessage newMessage = NDEFMessage.withRecords( 75 | NDEFRecord.type("text/plain", "hello world") 76 | ); 77 | 78 | await NFC.writeNDEF(newMessage, once: true).first; 79 | ``` 80 | 81 | ## Example 82 | 83 | ```dart 84 | import 'package:nfc_in_flutter/nfc_in_flutter.dart'; 85 | 86 | class NFCReader extends StatefulWidget { 87 | @override 88 | _NFCReaderState createState() => _NFCReaderState(); 89 | } 90 | 91 | class _NFCReaderState extends State { 92 | bool _supportsNFC = false; 93 | bool _reading = false; 94 | StreamSubscription _stream; 95 | 96 | @override 97 | void initState() { 98 | super.initState(); 99 | // Check if the device supports NFC reading 100 | NFC.isNDEFSupported 101 | .then((bool isSupported) { 102 | setState(() { 103 | _supportsNFC = isSupported; 104 | }); 105 | }); 106 | } 107 | 108 | @override 109 | Widget build(BuildContext context) { 110 | if (!_supportsNFC) { 111 | return RaisedButton( 112 | child: const Text("You device does not support NFC"), 113 | onPressed: null, 114 | ); 115 | } 116 | 117 | return RaisedButton( 118 | child: Text(_reading ? "Stop reading" : "Start reading"), 119 | onPressed: () { 120 | if (_reading) { 121 | _stream?.cancel(); 122 | setState(() { 123 | _reading = false; 124 | }); 125 | } else { 126 | setState(() { 127 | _reading = true; 128 | // Start reading using NFC.readNDEF() 129 | _stream = NFC.readNDEF( 130 | once: true, 131 | throwOnUserCancel: false, 132 | ).listen((NDEFMessage message) { 133 | print("read NDEF message: ${message.payload}"), 134 | }, onError: (e) { 135 | // Check error handling guide below 136 | }); 137 | }); 138 | } 139 | } 140 | ); 141 | } 142 | } 143 | ``` 144 | 145 | Full example in [example directory](https://github.com/semlette/nfc_in_flutter/tree/master/example) 146 | 147 | ## Installation 148 | 149 | Add `nfc_in_flutter` to your `pubspec.yaml` 150 | 151 | ```yaml 152 | dependencies: 153 | nfc_in_flutter: 2.0.5 154 | ``` 155 | 156 | ### iOS 157 | 158 | On iOS you must add turn on the Near Field Communication capability, add a NFC usage description and a NFC entitlement. 159 | 160 | #### Turn on Near Field Communication Tag Reading 161 | 162 | Open your iOS project in Xcode, find your project's target and navigate to Capabilities. Scroll down to 'Near Field Communication Tag Reading' and turn it on. 163 | 164 | Turning on 'Near Field Communication Tag reading' 165 | 166 | - Adds the NFC tag-reading feature to the App ID. 167 | - Adds the Near Field Communication Tag Reader Session Formats Entitlement to the entitlements file. 168 | 169 | from [developer.apple.com: Building an NFC Tag-Reader app](https://developer.apple.com/documentation/corenfc/building_an_nfc_tag-reader_app?language=objc) 170 | 171 | !['Turn on Near Field Communication Tag Reading' capability turned on for a project in Xcode](README-xcode.png) 172 | 173 | #### NFC Usage Description 174 | 175 | Open your `ios/Runner/Info.plist` file and add a new `NFCReaderUsageDescription` key. It's value should be a description of what you plan on using NFC for. 176 | 177 | ```xml 178 | NFCReaderUsageDescription 179 | ... 180 | ``` 181 | 182 | ### Android 183 | 184 | Add the following to your app's `AndroidManifest.xml` file: 185 | 186 | ```xml 187 | 188 | ``` 189 | 190 | If your app **requires** NFC, you can add the following to only allow it to be downloaded on devices that supports NFC: 191 | 192 | ```xml 193 | 194 | ``` 195 | 196 | ## "What is NDEF?" 197 | 198 | If you're new to NFC you may come to expect a lot of `readNFC()` calls, but instead you see `readNDEF()` and `NDEFMessage`. NDEF is just a formatting standard the tags can be encoded in. There are other encodings than NDEF, but NDEF is the most common one. Currently NFC in Flutter only supports NDEF formatted tags. 199 | 200 | ## Host Card Emulation 201 | 202 | NFC in Flutter supports reading from emulated host cards\*. 203 | 204 | To read from emulated host cards, you need to do a few things. 205 | 206 | - Call `readNDEF()` with the `readerMode` argument set to an instance of `NFCDispatchReaderMode`. 207 | - Insert the following `` in your `AndroidManifest.xml` activity: 208 | 209 | ```xml 210 | 211 | 212 | 213 | 214 | ``` 215 | 216 | - Not properly tested on iOS 217 | 218 | ### ⚠️ Multiple reader modes 219 | 220 | If you start a `readNDEF()` stream with the reader mode set to an instance of `NFCDispatchReaderMode`, while another stream is active with the `NFCNormalReaderMode`, it will throw a `NFCMultipleReaderModesException`. 221 | 222 | ## Platform differences 223 | 224 | When you call `readNDEF()` on iOS, Core NFC (the iOS framework that allows NFC reading) opens a little window. On Android it just starts listening for NFC tag reads in the background. 225 | 226 | ![](https://developer.apple.com/design/human-interface-guidelines/ios/images/Core_NFC.png) 227 | 228 | image from [developer.apple.com: Near Field Communication](https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/near-field-communication/) 229 | 230 | ⚠️ This will also freeze Flutter while open. Please send a Pull Request if you can fix this. 231 | 232 | ## Error handling 233 | 234 | Errors are no exception to NFC in Flutter (_hah, get it_). The stream returned by `NFC.readNDEF()` can send 7 different exceptions, and even worse: they are different for each platform! 235 | 236 | See the full example in the [example directory](/tree/master/example) for an example on how to check for errors. 237 | 238 | ### Exceptions for both platforms 239 | 240 | #### `NDEFReadingUnsupportedException` 241 | 242 | Thrown when a reading session is started, but not actually supported. 243 | 244 | ### iOS 245 | 246 | #### `NFCUserCanceledSessionException` 247 | 248 | Thrown when the user clicks Cancel/Done core NFC popup. If you don't need to know if the user canceled the session you can start reading with the `throwOnUserCancel` argument set to `false` like so: `readNDEF(throwOnUserCancel: false)` 249 | 250 | #### `NFCSessionTimeoutException` 251 | 252 | Core NFC limits NFC reading sessions to 60 seconds. `NFCSessionTimeoutException` is thrown when the session has been active for 60 seconds. 253 | 254 | #### `NFCSessionTerminatedUnexpectedlyException` 255 | 256 | Thrown when the reading session terminates unexpectedly. 257 | 258 | #### `NFCSystemIsBusyException` 259 | 260 | Throw when the reading session fails because the system is too busy. 261 | 262 | ### Android 263 | 264 | #### `NFCIOException` 265 | 266 | Thrown when a I/O exception occurs. Will for example happen if a tag is lost while being read or a tag could not be connected to. 267 | 268 | #### `NDEFBadFormatException` 269 | 270 | Thrown when the tag is expected to NDEF formatted, but it is incorrectly formatted. 271 | -------------------------------------------------------------------------------- /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 | group 'dev.semler.nfc_in_flutter' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.2.1' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 30 26 | 27 | defaultConfig { 28 | minSdkVersion 19 29 | targetSdkVersion 30 30 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 31 | } 32 | lintOptions { 33 | disable 'InvalidPackage' 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 17 20:15:30 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'nfc_in_flutter' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/dev/semler/nfc_in_flutter/NfcInFlutterException.java: -------------------------------------------------------------------------------- 1 | package dev.semler.nfc_in_flutter; 2 | 3 | class NfcInFlutterException extends Exception { 4 | String code; 5 | String message; 6 | Object details; 7 | 8 | NfcInFlutterException(String code, String message, Object details) { 9 | this.code = code; 10 | this.message = message; 11 | this.details = details; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/src/main/java/dev/semler/nfc_in_flutter/NfcInFlutterPlugin.java: -------------------------------------------------------------------------------- 1 | package dev.semler.nfc_in_flutter; 2 | 3 | import android.app.Activity; 4 | import android.app.PendingIntent; 5 | import android.content.Intent; 6 | import android.nfc.FormatException; 7 | import android.nfc.NdefMessage; 8 | import android.nfc.NdefRecord; 9 | import android.nfc.NfcAdapter; 10 | import android.nfc.Tag; 11 | import android.nfc.tech.Ndef; 12 | import android.nfc.tech.NdefFormatable; 13 | import android.os.AsyncTask; 14 | import android.os.Bundle; 15 | import android.os.Handler; 16 | import android.util.Log; 17 | 18 | import java.io.IOException; 19 | import java.math.BigInteger; 20 | import java.nio.ByteBuffer; 21 | import java.nio.charset.Charset; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.ArrayList; 24 | import java.util.Arrays; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Locale; 28 | import java.util.Map; 29 | import java.util.concurrent.ExecutionException; 30 | 31 | import io.flutter.plugin.common.EventChannel; 32 | import io.flutter.plugin.common.MethodCall; 33 | import io.flutter.plugin.common.MethodChannel; 34 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 35 | import io.flutter.plugin.common.MethodChannel.Result; 36 | import io.flutter.plugin.common.PluginRegistry; 37 | import io.flutter.plugin.common.PluginRegistry.Registrar; 38 | 39 | /** 40 | * NfcInFlutterPlugin 41 | */ 42 | public class NfcInFlutterPlugin implements MethodCallHandler, 43 | EventChannel.StreamHandler, 44 | PluginRegistry.NewIntentListener, 45 | NfcAdapter.ReaderCallback { 46 | 47 | private static final String NORMAL_READER_MODE = "normal"; 48 | private static final String DISPATCH_READER_MODE = "dispatch"; 49 | private final int DEFAULT_READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B | NfcAdapter.FLAG_READER_NFC_F | NfcAdapter.FLAG_READER_NFC_V; 50 | private static final String LOG_TAG = "NfcInFlutterPlugin"; 51 | 52 | private final Activity activity; 53 | private NfcAdapter adapter; 54 | private EventChannel.EventSink events; 55 | 56 | private String currentReaderMode = null; 57 | private Tag lastTag = null; 58 | 59 | /** 60 | * Plugin registration. 61 | */ 62 | public static void registerWith(Registrar registrar) { 63 | final MethodChannel channel = new MethodChannel(registrar.messenger(), "nfc_in_flutter"); 64 | final EventChannel tagChannel = new EventChannel(registrar.messenger(), "nfc_in_flutter/tags"); 65 | NfcInFlutterPlugin plugin = new NfcInFlutterPlugin(registrar.activity()); 66 | registrar.addNewIntentListener(plugin); 67 | channel.setMethodCallHandler(plugin); 68 | tagChannel.setStreamHandler(plugin); 69 | } 70 | 71 | private NfcInFlutterPlugin(Activity activity) { 72 | this.activity = activity; 73 | } 74 | 75 | @Override 76 | public void onMethodCall(MethodCall call, Result result) { 77 | switch (call.method) { 78 | case "readNDEFSupported": 79 | result.success(nfcIsEnabled()); 80 | break; 81 | case "startNDEFReading": 82 | if (!(call.arguments instanceof HashMap)) { 83 | result.error("MissingArguments", "startNDEFReading was called with no arguments", ""); 84 | return; 85 | } 86 | HashMap args = (HashMap) call.arguments; 87 | String readerMode = (String) args.get("reader_mode"); 88 | if (readerMode == null) { 89 | result.error("MissingReaderMode", "startNDEFReading was called without a reader mode", ""); 90 | return; 91 | } 92 | 93 | if (currentReaderMode != null && !readerMode.equals(currentReaderMode)) { 94 | // Throw error if the user tries to start reading with another reading mode 95 | // than the one currently active 96 | result.error("NFCMultipleReaderModes", "multiple reader modes", ""); 97 | return; 98 | } 99 | currentReaderMode = readerMode; 100 | switch (readerMode) { 101 | case NORMAL_READER_MODE: 102 | boolean noSounds = (boolean) args.get("no_platform_sounds"); 103 | startReading(noSounds); 104 | break; 105 | case DISPATCH_READER_MODE: 106 | startReadingWithForegroundDispatch(); 107 | break; 108 | default: 109 | result.error("NFCUnknownReaderMode", "unknown reader mode: " + readerMode, ""); 110 | return; 111 | } 112 | result.success(null); 113 | break; 114 | case "writeNDEF": 115 | HashMap writeArgs = call.arguments(); 116 | if (writeArgs == null) { 117 | result.error("NFCMissingArguments", "missing arguments", null); 118 | break; 119 | } 120 | try { 121 | Map messageMap = (Map) writeArgs.get("message"); 122 | if (messageMap == null) { 123 | result.error("NFCMissingNDEFMessage", "a ndef message was not given", null); 124 | break; 125 | } 126 | NdefMessage message = formatMapToNDEFMessage(messageMap); 127 | writeNDEF(message); 128 | result.success(null); 129 | } catch (NfcInFlutterException e) { 130 | result.error(e.code, e.message, e.details); 131 | } 132 | break; 133 | default: 134 | result.notImplemented(); 135 | } 136 | } 137 | 138 | private Boolean nfcIsEnabled() { 139 | NfcAdapter adapter = NfcAdapter.getDefaultAdapter(activity); 140 | if (adapter == null) return false; 141 | return adapter.isEnabled(); 142 | } 143 | 144 | private void startReading(boolean noSounds) { 145 | adapter = NfcAdapter.getDefaultAdapter(activity); 146 | if (adapter == null) return; 147 | Bundle bundle = new Bundle(); 148 | int flags = DEFAULT_READER_FLAGS; 149 | if (noSounds) { 150 | flags = flags | NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS; 151 | } 152 | adapter.enableReaderMode(activity, this, flags, bundle); 153 | } 154 | 155 | private void startReadingWithForegroundDispatch() { 156 | adapter = NfcAdapter.getDefaultAdapter(activity); 157 | if (adapter == null) return; 158 | Intent intent = new Intent(activity.getApplicationContext(), activity.getClass()); 159 | intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 160 | 161 | PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0); 162 | String[][] techList = new String[][]{}; 163 | 164 | adapter.enableForegroundDispatch(activity, pendingIntent, null, techList); 165 | } 166 | 167 | @Override 168 | public void onListen(Object args, EventChannel.EventSink eventSink) { 169 | events = eventSink; 170 | } 171 | 172 | @Override 173 | public void onCancel(Object args) { 174 | if (adapter != null) { 175 | switch (currentReaderMode) { 176 | case NORMAL_READER_MODE: 177 | adapter.disableReaderMode(activity); 178 | break; 179 | case DISPATCH_READER_MODE: 180 | adapter.disableForegroundDispatch(activity); 181 | break; 182 | default: 183 | Log.e(LOG_TAG, "unknown reader mode: " + currentReaderMode); 184 | } 185 | } 186 | events = null; 187 | } 188 | 189 | @Override 190 | public void onTagDiscovered(Tag tag) { 191 | lastTag = tag; 192 | Ndef ndef = Ndef.get(tag); 193 | NdefFormatable formatable = NdefFormatable.get(tag); 194 | if (ndef != null) { 195 | boolean closed = false; 196 | try { 197 | ndef.connect(); 198 | NdefMessage message = ndef.getNdefMessage(); 199 | if (message == null) { 200 | eventSuccess(formatEmptyNDEFMessage(ndef)); 201 | return; 202 | } 203 | try { 204 | ndef.close(); 205 | closed = true; 206 | } catch (IOException e) { 207 | Log.e(LOG_TAG, "close NDEF tag error: " + e.getMessage()); 208 | } 209 | eventSuccess(formatNDEFMessageToResult(ndef, message)); 210 | } catch (IOException e) { 211 | Map details = new HashMap<>(); 212 | details.put("fatal", true); 213 | eventError("IOError", e.getMessage(), details); 214 | } catch (FormatException e) { 215 | eventError("NDEFBadFormatError", e.getMessage(), null); 216 | } finally { 217 | // Close if the tag connection if it isn't already 218 | if (!closed) { 219 | try { 220 | ndef.close(); 221 | } catch (IOException e) { 222 | Log.e(LOG_TAG, "close NDEF tag error: " + e.getMessage()); 223 | } 224 | } 225 | } 226 | } else if (formatable != null) { 227 | eventSuccess(formatEmptyWritableNDEFMessage()); 228 | } 229 | } 230 | 231 | @Override 232 | public boolean onNewIntent(Intent intent) { 233 | String action = intent.getAction(); 234 | if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) { 235 | Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); 236 | lastTag = tag; 237 | handleNDEFTagFromIntent(tag); 238 | return true; 239 | } 240 | return false; 241 | } 242 | 243 | private String getNDEFTagID(Ndef ndef) { 244 | byte[] idByteArray = ndef.getTag().getId(); 245 | // Fancy string formatting snippet is from 246 | // https://gist.github.com/luixal/5768921#gistcomment-1788815 247 | return String.format("%0" + (idByteArray.length * 2) + "X", new BigInteger(1, idByteArray)); 248 | } 249 | 250 | private void handleNDEFTagFromIntent(Tag tag) { 251 | Ndef ndef = Ndef.get(tag); 252 | NdefFormatable formatable = NdefFormatable.get(tag); 253 | 254 | Map result; 255 | if (ndef != null) { 256 | NdefMessage message = ndef.getCachedNdefMessage(); 257 | try { 258 | ndef.close(); 259 | } catch (IOException e) { 260 | Log.e(LOG_TAG, "close NDEF tag error: " + e.getMessage()); 261 | } 262 | result = formatNDEFMessageToResult(ndef, message); 263 | } else if (formatable != null) { 264 | result = formatEmptyWritableNDEFMessage(); 265 | } else { 266 | return; 267 | } 268 | 269 | eventSuccess(result); 270 | } 271 | 272 | private Map formatEmptyWritableNDEFMessage() { 273 | final Map result = new HashMap<>(); 274 | result.put("id", ""); 275 | result.put("message_type", "ndef"); 276 | result.put("type", ""); 277 | result.put("writable", true); 278 | List> records = new ArrayList<>(); 279 | Map emptyRecord = new HashMap<>(); 280 | emptyRecord.put("tnf", "empty"); 281 | emptyRecord.put("id", ""); 282 | emptyRecord.put("type", ""); 283 | emptyRecord.put("payload", ""); 284 | emptyRecord.put("data", ""); 285 | emptyRecord.put("languageCode", ""); 286 | records.add(emptyRecord); 287 | result.put("records", records); 288 | return result; 289 | } 290 | 291 | private Map formatEmptyNDEFMessage(Ndef ndef) { 292 | final Map result = formatEmptyWritableNDEFMessage(); 293 | result.put("id", getNDEFTagID(ndef)); 294 | result.put("writable", ndef.isWritable()); 295 | return result; 296 | } 297 | 298 | private Map formatNDEFMessageToResult(Ndef ndef, NdefMessage message) { 299 | final Map result = new HashMap<>(); 300 | List> records = new ArrayList<>(); 301 | for (NdefRecord record : message.getRecords()) { 302 | Map recordMap = new HashMap<>(); 303 | byte[] recordPayload = record.getPayload(); 304 | Charset charset = StandardCharsets.UTF_8; 305 | short tnf = record.getTnf(); 306 | byte[] type = record.getType(); 307 | if (tnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(type, NdefRecord.RTD_TEXT)) { 308 | charset = ((recordPayload[0] & 128) == 0) ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16; 309 | } 310 | recordMap.put("rawPayload", recordPayload); 311 | 312 | // If the record's tnf is well known and the RTD is set to URI, 313 | // the URL prefix should be added to the payload 314 | if (tnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(type, NdefRecord.RTD_URI)) { 315 | recordMap.put("data", new String(recordPayload, 1, recordPayload.length - 1, charset)); 316 | 317 | String url = ""; 318 | byte prefixByte = recordPayload[0]; 319 | // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/nfc/NdefRecord.java#238 320 | switch (prefixByte) { 321 | case 0x01: 322 | url = "http://www."; 323 | break; 324 | case 0x02: 325 | url = "https://www."; 326 | break; 327 | case 0x03: 328 | url = "http://"; 329 | break; 330 | case 0x04: 331 | url = "https://"; 332 | break; 333 | case 0x05: 334 | url = "tel:"; 335 | break; 336 | case 0x06: 337 | url = "mailto:"; 338 | break; 339 | case 0x07: 340 | url = "ftp://anonymous:anonymous@"; 341 | break; 342 | case 0x08: 343 | url = "ftp://ftp."; 344 | break; 345 | case 0x09: 346 | url = "ftps://"; 347 | break; 348 | case 0x0A: 349 | url = "sftp://"; 350 | break; 351 | case 0x0B: 352 | url = "smb://"; 353 | break; 354 | case 0x0C: 355 | url = "nfs://"; 356 | break; 357 | case 0x0D: 358 | url = "ftp://"; 359 | break; 360 | case 0x0E: 361 | url = "dav://"; 362 | break; 363 | case 0x0F: 364 | url = "news:"; 365 | break; 366 | case 0x10: 367 | url = "telnet://"; 368 | break; 369 | case 0x11: 370 | url = "imap:"; 371 | break; 372 | case 0x12: 373 | url = "rtsp://"; 374 | break; 375 | case 0x13: 376 | url = "urn:"; 377 | break; 378 | case 0x14: 379 | url = "pop:"; 380 | break; 381 | case 0x15: 382 | url = "sip:"; 383 | break; 384 | case 0x16: 385 | url = "sips"; 386 | break; 387 | case 0x17: 388 | url = "tftp:"; 389 | break; 390 | case 0x18: 391 | url = "btspp://"; 392 | break; 393 | case 0x19: 394 | url = "btl2cap://"; 395 | break; 396 | case 0x1A: 397 | url = "btgoep://"; 398 | break; 399 | case 0x1B: 400 | url = "btgoep://"; 401 | break; 402 | case 0x1C: 403 | url = "irdaobex://"; 404 | break; 405 | case 0x1D: 406 | url = "file://"; 407 | break; 408 | case 0x1E: 409 | url = "urn:epc:id:"; 410 | break; 411 | case 0x1F: 412 | url = "urn:epc:tag:"; 413 | break; 414 | case 0x20: 415 | url = "urn:epc:pat:"; 416 | break; 417 | case 0x21: 418 | url = "urn:epc:raw:"; 419 | break; 420 | case 0x22: 421 | url = "urn:epc:"; 422 | break; 423 | case 0x23: 424 | url = "urn:nfc:"; 425 | break; 426 | } 427 | recordMap.put("payload", url + new String(recordPayload, 1, recordPayload.length - 1, charset)); 428 | } else if (tnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(type, NdefRecord.RTD_TEXT)) { 429 | int languageCodeLength = (recordPayload[0] & 0x3f) + 1; 430 | recordMap.put("payload", new String(recordPayload, 1, recordPayload.length - 1, charset)); 431 | recordMap.put("languageCode", new String(recordPayload, 1, languageCodeLength - 1, charset)); 432 | recordMap.put("data", new String(recordPayload, languageCodeLength, recordPayload.length - languageCodeLength, charset)); 433 | } else { 434 | recordMap.put("payload", new String(recordPayload, charset)); 435 | recordMap.put("data", new String(recordPayload, charset)); 436 | } 437 | 438 | recordMap.put("id", new String(record.getId(), StandardCharsets.UTF_8)); 439 | recordMap.put("type", new String(record.getType(), StandardCharsets.UTF_8)); 440 | 441 | String tnfValue; 442 | switch (tnf) { 443 | case NdefRecord.TNF_EMPTY: 444 | tnfValue = "empty"; 445 | break; 446 | case NdefRecord.TNF_WELL_KNOWN: 447 | tnfValue = "well_known"; 448 | break; 449 | case NdefRecord.TNF_MIME_MEDIA: 450 | tnfValue = "mime_media"; 451 | break; 452 | case NdefRecord.TNF_ABSOLUTE_URI: 453 | tnfValue = "absolute_uri"; 454 | break; 455 | case NdefRecord.TNF_EXTERNAL_TYPE: 456 | tnfValue = "external_type"; 457 | break; 458 | case NdefRecord.TNF_UNCHANGED: 459 | tnfValue = "unchanged"; 460 | break; 461 | default: 462 | tnfValue = "unknown"; 463 | } 464 | 465 | recordMap.put("tnf", tnfValue); 466 | records.add(recordMap); 467 | } 468 | result.put("id", getNDEFTagID(ndef)); 469 | result.put("message_type", "ndef"); 470 | result.put("type", ndef.getType()); 471 | result.put("records", records); 472 | result.put("writable", ndef.isWritable()); 473 | return result; 474 | } 475 | 476 | private NdefMessage formatMapToNDEFMessage(Map map) throws IllegalArgumentException { 477 | Object mapRecordsObj = map.get("records"); 478 | if (mapRecordsObj == null) { 479 | throw new IllegalArgumentException("missing records"); 480 | } else if (!(mapRecordsObj instanceof List)) { 481 | throw new IllegalArgumentException("map key 'records' is not a list"); 482 | } 483 | List mapRecords = (List) mapRecordsObj; 484 | int amountOfRecords = mapRecords.size(); 485 | NdefRecord[] records = new NdefRecord[amountOfRecords]; 486 | for (int i = 0; i < amountOfRecords; i++) { 487 | Object mapRecordObj = mapRecords.get(i); 488 | if (!(mapRecordObj instanceof Map)) { 489 | throw new IllegalArgumentException("record is not a map"); 490 | } 491 | Map mapRecord = (Map) mapRecordObj; 492 | String id = (String) mapRecord.get("id"); 493 | if (id == null) { 494 | id = ""; 495 | } 496 | String type = (String) mapRecord.get("type"); 497 | if (type == null) { 498 | type = ""; 499 | } 500 | String languageCode = (String) mapRecord.get("languageCode"); 501 | if (languageCode == null) { 502 | languageCode = Locale.getDefault().getLanguage(); 503 | } 504 | String payload = (String) mapRecord.get("payload"); 505 | if (payload == null) { 506 | payload = ""; 507 | } 508 | String tnf = (String) mapRecord.get("tnf"); 509 | if (tnf == null) { 510 | throw new IllegalArgumentException("record tnf is null"); 511 | } 512 | 513 | byte[] idBytes = id.getBytes(); 514 | byte[] typeBytes = type.getBytes(); 515 | byte[] languageCodeBytes = languageCode.getBytes(StandardCharsets.US_ASCII); 516 | byte[] payloadBytes = payload.getBytes(); 517 | 518 | short tnfValue; 519 | // Construct record 520 | switch (tnf) { 521 | case "empty": 522 | // Empty records are not allowed to have a ID, type or payload. 523 | tnfValue = NdefRecord.TNF_EMPTY; 524 | idBytes = null; 525 | typeBytes = null; 526 | payloadBytes = null; 527 | break; 528 | case "well_known": 529 | tnfValue = NdefRecord.TNF_WELL_KNOWN; 530 | if (Arrays.equals(typeBytes, NdefRecord.RTD_TEXT)) { 531 | // The following code basically constructs a text record like NdefRecord.createTextRecord() does, 532 | // however NdefRecord.createTextRecord() is only available in SDK 21+ while nfc_in_flutter 533 | // goes down to SDK 19. 534 | ByteBuffer buffer = ByteBuffer.allocate(1 + languageCodeBytes.length + payloadBytes.length); 535 | byte status = (byte) (languageCodeBytes.length & 0xFF); 536 | buffer.put(status); 537 | buffer.put(languageCodeBytes); 538 | buffer.put(payloadBytes); 539 | payloadBytes = buffer.array(); 540 | } else if (Arrays.equals(typeBytes, NdefRecord.RTD_URI)) { 541 | // Instead of manually constructing a URI payload with the correct prefix and 542 | // everything, create a record using NdefRecord.createUri and copy it's payload. 543 | NdefRecord uriRecord = NdefRecord.createUri(payload); 544 | payloadBytes = uriRecord.getPayload(); 545 | } 546 | break; 547 | case "mime_media": 548 | tnfValue = NdefRecord.TNF_MIME_MEDIA; 549 | break; 550 | case "absolute_uri": 551 | tnfValue = NdefRecord.TNF_ABSOLUTE_URI; 552 | break; 553 | case "external_type": 554 | tnfValue = NdefRecord.TNF_EXTERNAL_TYPE; 555 | break; 556 | case "unchanged": 557 | throw new IllegalArgumentException("records are not allowed to have their TNF set to UNCHANGED"); 558 | default: 559 | tnfValue = NdefRecord.TNF_UNKNOWN; 560 | typeBytes = null; 561 | } 562 | records[i] = new NdefRecord(tnfValue, typeBytes, idBytes, payloadBytes); 563 | } 564 | return new NdefMessage(records); 565 | } 566 | 567 | private static class FormatRequest { 568 | final NdefFormatable formatable; 569 | final NdefMessage message; 570 | 571 | FormatRequest(NdefFormatable formatable, NdefMessage message) { 572 | this.formatable = formatable; 573 | this.message = message; 574 | } 575 | } 576 | 577 | /** 578 | * FormatTask formats a NdefFormatable tag. 579 | * NdefFormatable.format() must not be called on the main thread, so it 580 | * will be called in a seperate thread by this AsyncTask. 581 | */ 582 | private static class FormatTask extends AsyncTask { 583 | @Override 584 | protected NfcInFlutterException doInBackground(FormatRequest... formatRequests) { 585 | for (FormatRequest request : formatRequests) { 586 | try { 587 | request.formatable.connect(); 588 | request.formatable.format(request.message); 589 | request.formatable.close(); 590 | } catch (IOException e) { 591 | return new NfcInFlutterException("IOError", e.getMessage(), null); 592 | } catch (FormatException e) { 593 | return new NfcInFlutterException("NDEFBadFormatError", e.getMessage(), null); 594 | } 595 | } 596 | return null; 597 | } 598 | } 599 | 600 | private void writeNDEF(NdefMessage message) throws NfcInFlutterException { 601 | Ndef ndef = Ndef.get(lastTag); 602 | NdefFormatable formatable = NdefFormatable.get(lastTag); 603 | 604 | // Absolute try-catch monstrosity 605 | 606 | if (ndef != null) { 607 | try { 608 | ndef.connect(); 609 | if (ndef.getMaxSize() < message.getByteArrayLength()) { 610 | HashMap details = new HashMap<>(); 611 | details.put("maxSize", ndef.getMaxSize()); 612 | throw new NfcInFlutterException("NFCTagSizeTooSmallError", "message is too large for this tag", details); 613 | } 614 | try { 615 | ndef.writeNdefMessage(message); 616 | } catch (IOException e) { 617 | throw new NfcInFlutterException("IOError", "write to tag error: " + e.getMessage(), null); 618 | } catch (FormatException e) { 619 | throw new NfcInFlutterException("NDEFBadFormatError", e.getMessage(), null); 620 | } 621 | } catch (IOException e) { 622 | throw new NfcInFlutterException("IOError", e.getMessage(), null); 623 | } finally { 624 | try { 625 | ndef.close(); 626 | } catch (IOException e) { 627 | Log.e(LOG_TAG, "close NDEF tag error: " + e.getMessage()); 628 | } 629 | } 630 | } else if (formatable != null) { 631 | FormatTask task = new FormatTask(); 632 | FormatRequest request = new FormatRequest(formatable, message); 633 | try { 634 | NfcInFlutterException result = task.execute(request).get(); 635 | if (result != null) { 636 | throw result; 637 | } 638 | } catch (ExecutionException e) { 639 | // TODO 640 | throw new NfcInFlutterException("ExecutionError", e.getMessage(), null); 641 | } catch (InterruptedException e) { 642 | // TODO 643 | throw new NfcInFlutterException("InterruptedException", e.getMessage(), null); 644 | } 645 | } else { 646 | throw new NfcInFlutterException("NDEFUnsupported", "tag doesn't support NDEF", null); 647 | } 648 | } 649 | 650 | private void eventSuccess(final Object result) { 651 | Handler mainThread = new Handler(activity.getMainLooper()); 652 | Runnable runnable = new Runnable() { 653 | @Override 654 | public void run() { 655 | if (events != null) { 656 | // Event stream must be handled on main/ui thread 657 | events.success(result); 658 | } 659 | } 660 | }; 661 | mainThread.post(runnable); 662 | } 663 | 664 | private void eventError(final String code, final String message, final Object details) { 665 | Handler mainThread = new Handler(activity.getMainLooper()); 666 | Runnable runnable = new Runnable() { 667 | @Override 668 | public void run() { 669 | if (events != null) { 670 | // Event stream must be handled on main/ui thread 671 | events.error(code, message, details); 672 | } 673 | } 674 | }; 675 | mainThread.post(runnable); 676 | } 677 | } 678 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | ios/Flutter/flutter_export_environment.sh 2 | .flutter-plugins-dependencies 3 | 4 | # Miscellaneous 5 | *.class 6 | *.log 7 | *.pyc 8 | *.swp 9 | .DS_Store 10 | .atom/ 11 | .buildlog/ 12 | .history 13 | .svn/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Android related 36 | **/android/**/gradle-wrapper.jar 37 | **/android/.gradle 38 | **/android/captures/ 39 | **/android/gradlew 40 | **/android/gradlew.bat 41 | **/android/local.properties 42 | **/android/**/GeneratedPluginRegistrant.java 43 | 44 | # iOS/XCode related 45 | **/ios/**/*.mode1v3 46 | **/ios/**/*.mode2v3 47 | **/ios/**/*.moved-aside 48 | **/ios/**/*.pbxuser 49 | **/ios/**/*.perspectivev3 50 | **/ios/**/*sync/ 51 | **/ios/**/.sconsign.dblite 52 | **/ios/**/.tags* 53 | **/ios/**/.vagrant/ 54 | **/ios/**/DerivedData/ 55 | **/ios/**/Icon? 56 | **/ios/**/Pods/ 57 | **/ios/**/.symlinks/ 58 | **/ios/**/profile 59 | **/ios/**/xcuserdata 60 | **/ios/.generated/ 61 | **/ios/Flutter/App.framework 62 | **/ios/Flutter/Flutter.framework 63 | **/ios/Flutter/Generated.xcconfig 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /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: bc7bc940836f1f834699625426795fd6f07c18ec 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/example/README-photo.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # NFC in Flutter example 2 | 3 | An example application to demonstrate NFC in Flutter. 4 | 5 | ![Screenshot of the example app](README-photo.png) 6 | 7 | It is easiest to run the app on an Android device as the iOS simulator can't do NFC and running it on a real device requires a bunch of code signing nonsense. 8 | 9 | ```shell 10 | $ flutter run 11 | ``` 12 | 13 | ## Implementation 14 | 15 | The app has a button in the top right corner that starts or stops listening for NFC tags. 16 | 17 | ```dart 18 | RaisedButton( 19 | child: Text(reading ? "Stop reading" : "Start reading"), 20 | onPressed: () { 21 | if (!reading) { 22 | setState(() { 23 | reading = true; 24 | session = NFC.readNDEF() 25 | .listen((tag) { 26 | 27 | }); 28 | }); 29 | } else { 30 | session?.cancel(); 31 | setState(() { 32 | reading = false; 33 | }); 34 | } 35 | } 36 | ); 37 | ``` 38 | 39 | When a tag is scanned, it inserts it in a list of scanned tags, which is then rendered in a `ListView`. 40 | 41 | ```dart 42 | NFC.readNDEF() 43 | .listen((tag) ( 44 | setState(() { 45 | tags.insert(0, tag); 46 | }); 47 | )); 48 | ``` 49 | 50 | When an error occurs it will show an `AlertDialog`, unless the error is a `NFCUserCanceledSessionException`. 51 | 52 | ```dart 53 | NFC.readNDEF() 54 | .listen((tag) { 55 | // ... 56 | }, onError: (error) { 57 | if (!(error is NFCUserCanceledSessionException)) { 58 | // It is up to you how many exceptions you want to check for. 59 | showDialog( 60 | context: context, 61 | builder: (context) => AlertDialog( 62 | title: const Text("Error!"), 63 | content: Text(e.toString()), 64 | ), 65 | ); 66 | } 67 | }); 68 | ``` -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 30 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "dev.semler.nfc_in_flutter_example" 37 | minSdkVersion 19 38 | targetSdkVersion 30 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 10 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/dev/semler/nfc_in_flutter_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package dev.semler.nfc_in_flutter_example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity {} 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.1.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 22 21:52:42 CEST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | 8.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/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'High-performance, high-fidelity mobile apps.' 10 | s.homepage = 'https://flutter.io' 11 | s.license = { :type => 'MIT' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '8.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /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, '9.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 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - nfc_in_flutter (1.0.0): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - nfc_in_flutter (from `.symlinks/plugins/nfc_in_flutter/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | nfc_in_flutter: 14 | :path: ".symlinks/plugins/nfc_in_flutter/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 18 | nfc_in_flutter: c656fbfb1ec5b9d021da87b0c87629d62fd5264d 19 | 20 | PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d 21 | 22 | COCOAPODS: 1.10.1 23 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 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 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 13 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 14 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 15 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 16 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 17 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 18 | C88B2C11DB0646AEB74B987B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 069286D6B571AF6ABDCE509A /* libPods-Runner.a */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXCopyFilesBuildPhase section */ 22 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 23 | isa = PBXCopyFilesBuildPhase; 24 | buildActionMask = 2147483647; 25 | dstPath = ""; 26 | dstSubfolderSpec = 10; 27 | files = ( 28 | ); 29 | name = "Embed Frameworks"; 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXCopyFilesBuildPhase section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 069286D6B571AF6ABDCE509A /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 37 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 39 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 40 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 41 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 42 | 8F2994926B93272D5B801B25 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 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 | CDED2553FE6B4A9B18395D76 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 52 | D2D1ED04193177BFDD1FD89C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 53 | FD14CDF723AAB91400901652 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; }; 54 | FD54E08B22BE9C3B005AAA88 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | C88B2C11DB0646AEB74B987B /* libPods-Runner.a in Frameworks */, 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | 007162F45332D7C71CFF5888 /* Pods */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | D2D1ED04193177BFDD1FD89C /* Pods-Runner.debug.xcconfig */, 73 | CDED2553FE6B4A9B18395D76 /* Pods-Runner.release.xcconfig */, 74 | 8F2994926B93272D5B801B25 /* Pods-Runner.profile.xcconfig */, 75 | ); 76 | name = Pods; 77 | sourceTree = ""; 78 | }; 79 | 437C372638BB776C97315D3E /* Frameworks */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | FD14CDF723AAB91400901652 /* CoreNFC.framework */, 83 | 069286D6B571AF6ABDCE509A /* libPods-Runner.a */, 84 | ); 85 | name = Frameworks; 86 | sourceTree = ""; 87 | }; 88 | 9740EEB11CF90186004384FC /* Flutter */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 92 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 93 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 94 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 95 | ); 96 | name = Flutter; 97 | sourceTree = ""; 98 | }; 99 | 97C146E51CF9000F007C117D = { 100 | isa = PBXGroup; 101 | children = ( 102 | 9740EEB11CF90186004384FC /* Flutter */, 103 | 97C146F01CF9000F007C117D /* Runner */, 104 | 97C146EF1CF9000F007C117D /* Products */, 105 | 007162F45332D7C71CFF5888 /* Pods */, 106 | 437C372638BB776C97315D3E /* Frameworks */, 107 | ); 108 | sourceTree = ""; 109 | }; 110 | 97C146EF1CF9000F007C117D /* Products */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 97C146EE1CF9000F007C117D /* Runner.app */, 114 | ); 115 | name = Products; 116 | sourceTree = ""; 117 | }; 118 | 97C146F01CF9000F007C117D /* Runner */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | FD54E08B22BE9C3B005AAA88 /* Runner.entitlements */, 122 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 123 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 124 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 125 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 126 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 127 | 97C147021CF9000F007C117D /* Info.plist */, 128 | 97C146F11CF9000F007C117D /* Supporting Files */, 129 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 130 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 131 | ); 132 | path = Runner; 133 | sourceTree = ""; 134 | }; 135 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 97C146F21CF9000F007C117D /* main.m */, 139 | ); 140 | name = "Supporting Files"; 141 | sourceTree = ""; 142 | }; 143 | /* End PBXGroup section */ 144 | 145 | /* Begin PBXNativeTarget section */ 146 | 97C146ED1CF9000F007C117D /* Runner */ = { 147 | isa = PBXNativeTarget; 148 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 149 | buildPhases = ( 150 | D337359B02C7585443A3ED66 /* [CP] Check Pods Manifest.lock */, 151 | 9740EEB61CF901F6004384FC /* Run Script */, 152 | 97C146EA1CF9000F007C117D /* Sources */, 153 | 97C146EB1CF9000F007C117D /* Frameworks */, 154 | 97C146EC1CF9000F007C117D /* Resources */, 155 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 156 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = Runner; 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 = 0910; 174 | ORGANIZATIONNAME = "The Chromium Authors"; 175 | TargetAttributes = { 176 | 97C146ED1CF9000F007C117D = { 177 | CreatedOnToolsVersion = 7.3.1; 178 | DevelopmentTeam = J9YJ7WGJ6A; 179 | SystemCapabilities = { 180 | com.apple.NearFieldCommunicationTagReading = { 181 | enabled = 1; 182 | }; 183 | }; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 188 | compatibilityVersion = "Xcode 3.2"; 189 | developmentRegion = English; 190 | hasScannedForEncodings = 0; 191 | knownRegions = ( 192 | English, 193 | en, 194 | Base, 195 | ); 196 | mainGroup = 97C146E51CF9000F007C117D; 197 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 198 | projectDirPath = ""; 199 | projectRoot = ""; 200 | targets = ( 201 | 97C146ED1CF9000F007C117D /* Runner */, 202 | ); 203 | }; 204 | /* End PBXProject section */ 205 | 206 | /* Begin PBXResourcesBuildPhase section */ 207 | 97C146EC1CF9000F007C117D /* Resources */ = { 208 | isa = PBXResourcesBuildPhase; 209 | buildActionMask = 2147483647; 210 | files = ( 211 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 212 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 213 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 214 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 215 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXResourcesBuildPhase section */ 220 | 221 | /* Begin PBXShellScriptBuildPhase section */ 222 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 223 | isa = PBXShellScriptBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | ); 227 | inputPaths = ( 228 | ); 229 | name = "Thin Binary"; 230 | outputPaths = ( 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | shellPath = /bin/sh; 234 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 235 | }; 236 | 9740EEB61CF901F6004384FC /* Run Script */ = { 237 | isa = PBXShellScriptBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | ); 241 | inputPaths = ( 242 | ); 243 | name = "Run Script"; 244 | outputPaths = ( 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | shellPath = /bin/sh; 248 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 249 | }; 250 | D337359B02C7585443A3ED66 /* [CP] Check Pods Manifest.lock */ = { 251 | isa = PBXShellScriptBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | ); 255 | inputPaths = ( 256 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 257 | "${PODS_ROOT}/Manifest.lock", 258 | ); 259 | name = "[CP] Check Pods Manifest.lock"; 260 | outputPaths = ( 261 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | shellPath = /bin/sh; 265 | 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"; 266 | showEnvVarsInLog = 0; 267 | }; 268 | /* End PBXShellScriptBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 97C146EA1CF9000F007C117D /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 276 | 97C146F31CF9000F007C117D /* main.m in Sources */, 277 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | /* End PBXSourcesBuildPhase section */ 282 | 283 | /* Begin PBXVariantGroup section */ 284 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 285 | isa = PBXVariantGroup; 286 | children = ( 287 | 97C146FB1CF9000F007C117D /* Base */, 288 | ); 289 | name = Main.storyboard; 290 | sourceTree = ""; 291 | }; 292 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 293 | isa = PBXVariantGroup; 294 | children = ( 295 | 97C147001CF9000F007C117D /* Base */, 296 | ); 297 | name = LaunchScreen.storyboard; 298 | sourceTree = ""; 299 | }; 300 | /* End PBXVariantGroup section */ 301 | 302 | /* Begin XCBuildConfiguration section */ 303 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | ALWAYS_SEARCH_USER_PATHS = NO; 307 | CLANG_ANALYZER_NONNULL = YES; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 309 | CLANG_CXX_LIBRARY = "libc++"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 313 | CLANG_WARN_BOOL_CONVERSION = YES; 314 | CLANG_WARN_COMMA = YES; 315 | CLANG_WARN_CONSTANT_CONVERSION = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 325 | CLANG_WARN_STRICT_PROTOTYPES = YES; 326 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 327 | CLANG_WARN_UNREACHABLE_CODE = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 330 | COPY_PHASE_STRIP = NO; 331 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 332 | ENABLE_NS_ASSERTIONS = NO; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | GCC_C_LANGUAGE_STANDARD = gnu99; 335 | GCC_NO_COMMON_BLOCKS = YES; 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 343 | MTL_ENABLE_DEBUG_INFO = NO; 344 | OTHER_LDFLAGS = ( 345 | "$(inherited)", 346 | "-ObjC", 347 | "-l\"nfc_in_flutter\"", 348 | "-framework", 349 | "\"Flutter\"", 350 | "-weak_framework", 351 | "\"CoreNFC\"", 352 | ); 353 | SDKROOT = iphoneos; 354 | TARGETED_DEVICE_FAMILY = "1,2"; 355 | VALIDATE_PRODUCT = YES; 356 | }; 357 | name = Profile; 358 | }; 359 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 360 | isa = XCBuildConfiguration; 361 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 365 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 366 | DEVELOPMENT_TEAM = J9YJ7WGJ6A; 367 | ENABLE_BITCODE = NO; 368 | FRAMEWORK_SEARCH_PATHS = ( 369 | "$(inherited)", 370 | "$(PROJECT_DIR)/Flutter", 371 | ); 372 | INFOPLIST_FILE = Runner/Info.plist; 373 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 374 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 375 | LIBRARY_SEARCH_PATHS = ( 376 | "$(inherited)", 377 | "$(PROJECT_DIR)/Flutter", 378 | ); 379 | OTHER_LDFLAGS = ( 380 | "$(inherited)", 381 | "-ObjC", 382 | "-l\"nfc_in_flutter\"", 383 | "-framework", 384 | "\"Flutter\"", 385 | "-weak_framework", 386 | "\"CoreNFC\"", 387 | ); 388 | PRODUCT_BUNDLE_IDENTIFIER = dev.semler.nfcInFlutterExample; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | VERSIONING_SYSTEM = "apple-generic"; 391 | }; 392 | name = Profile; 393 | }; 394 | 97C147031CF9000F007C117D /* Debug */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ALWAYS_SEARCH_USER_PATHS = NO; 398 | CLANG_ANALYZER_NONNULL = YES; 399 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 400 | CLANG_CXX_LIBRARY = "libc++"; 401 | CLANG_ENABLE_MODULES = YES; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_EMPTY_BODY = YES; 409 | CLANG_WARN_ENUM_CONVERSION = YES; 410 | CLANG_WARN_INFINITE_RECURSION = YES; 411 | CLANG_WARN_INT_CONVERSION = YES; 412 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 413 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 415 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 416 | CLANG_WARN_STRICT_PROTOTYPES = YES; 417 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 418 | CLANG_WARN_UNREACHABLE_CODE = YES; 419 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 420 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 421 | COPY_PHASE_STRIP = NO; 422 | DEBUG_INFORMATION_FORMAT = dwarf; 423 | ENABLE_STRICT_OBJC_MSGSEND = YES; 424 | ENABLE_TESTABILITY = YES; 425 | GCC_C_LANGUAGE_STANDARD = gnu99; 426 | GCC_DYNAMIC_NO_PIC = NO; 427 | GCC_NO_COMMON_BLOCKS = YES; 428 | GCC_OPTIMIZATION_LEVEL = 0; 429 | GCC_PREPROCESSOR_DEFINITIONS = ( 430 | "DEBUG=1", 431 | "$(inherited)", 432 | ); 433 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 434 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 435 | GCC_WARN_UNDECLARED_SELECTOR = YES; 436 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 437 | GCC_WARN_UNUSED_FUNCTION = YES; 438 | GCC_WARN_UNUSED_VARIABLE = YES; 439 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 440 | MTL_ENABLE_DEBUG_INFO = YES; 441 | ONLY_ACTIVE_ARCH = YES; 442 | OTHER_LDFLAGS = ( 443 | "$(inherited)", 444 | "-ObjC", 445 | "-l\"nfc_in_flutter\"", 446 | "-framework", 447 | "\"Flutter\"", 448 | "-weak_framework", 449 | "\"CoreNFC\"", 450 | ); 451 | SDKROOT = iphoneos; 452 | TARGETED_DEVICE_FAMILY = "1,2"; 453 | }; 454 | name = Debug; 455 | }; 456 | 97C147041CF9000F007C117D /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ALWAYS_SEARCH_USER_PATHS = NO; 460 | CLANG_ANALYZER_NONNULL = YES; 461 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 462 | CLANG_CXX_LIBRARY = "libc++"; 463 | CLANG_ENABLE_MODULES = YES; 464 | CLANG_ENABLE_OBJC_ARC = YES; 465 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 466 | CLANG_WARN_BOOL_CONVERSION = YES; 467 | CLANG_WARN_COMMA = YES; 468 | CLANG_WARN_CONSTANT_CONVERSION = YES; 469 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 470 | CLANG_WARN_EMPTY_BODY = YES; 471 | CLANG_WARN_ENUM_CONVERSION = YES; 472 | CLANG_WARN_INFINITE_RECURSION = YES; 473 | CLANG_WARN_INT_CONVERSION = YES; 474 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 476 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 477 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 478 | CLANG_WARN_STRICT_PROTOTYPES = YES; 479 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 480 | CLANG_WARN_UNREACHABLE_CODE = YES; 481 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 482 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 483 | COPY_PHASE_STRIP = NO; 484 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 485 | ENABLE_NS_ASSERTIONS = NO; 486 | ENABLE_STRICT_OBJC_MSGSEND = YES; 487 | GCC_C_LANGUAGE_STANDARD = gnu99; 488 | GCC_NO_COMMON_BLOCKS = YES; 489 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 490 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 491 | GCC_WARN_UNDECLARED_SELECTOR = YES; 492 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 493 | GCC_WARN_UNUSED_FUNCTION = YES; 494 | GCC_WARN_UNUSED_VARIABLE = YES; 495 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 496 | MTL_ENABLE_DEBUG_INFO = NO; 497 | OTHER_LDFLAGS = ( 498 | "$(inherited)", 499 | "-ObjC", 500 | "-l\"nfc_in_flutter\"", 501 | "-framework", 502 | "\"Flutter\"", 503 | "-weak_framework", 504 | "\"CoreNFC\"", 505 | ); 506 | SDKROOT = iphoneos; 507 | TARGETED_DEVICE_FAMILY = "1,2"; 508 | VALIDATE_PRODUCT = YES; 509 | }; 510 | name = Release; 511 | }; 512 | 97C147061CF9000F007C117D /* Debug */ = { 513 | isa = XCBuildConfiguration; 514 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 515 | buildSettings = { 516 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 517 | CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 518 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 519 | DEVELOPMENT_TEAM = J9YJ7WGJ6A; 520 | ENABLE_BITCODE = NO; 521 | FRAMEWORK_SEARCH_PATHS = ( 522 | "$(inherited)", 523 | "$(PROJECT_DIR)/Flutter", 524 | ); 525 | INFOPLIST_FILE = Runner/Info.plist; 526 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 527 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 528 | LIBRARY_SEARCH_PATHS = ( 529 | "$(inherited)", 530 | "$(PROJECT_DIR)/Flutter", 531 | ); 532 | OTHER_LDFLAGS = ( 533 | "$(inherited)", 534 | "-ObjC", 535 | "-l\"nfc_in_flutter\"", 536 | "-framework", 537 | "\"Flutter\"", 538 | "-weak_framework", 539 | "\"CoreNFC\"", 540 | ); 541 | PRODUCT_BUNDLE_IDENTIFIER = dev.semler.nfcInFlutterExample; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | VERSIONING_SYSTEM = "apple-generic"; 544 | }; 545 | name = Debug; 546 | }; 547 | 97C147071CF9000F007C117D /* Release */ = { 548 | isa = XCBuildConfiguration; 549 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 550 | buildSettings = { 551 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 552 | CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 553 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 554 | DEVELOPMENT_TEAM = J9YJ7WGJ6A; 555 | ENABLE_BITCODE = NO; 556 | FRAMEWORK_SEARCH_PATHS = ( 557 | "$(inherited)", 558 | "$(PROJECT_DIR)/Flutter", 559 | ); 560 | INFOPLIST_FILE = Runner/Info.plist; 561 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 562 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 563 | LIBRARY_SEARCH_PATHS = ( 564 | "$(inherited)", 565 | "$(PROJECT_DIR)/Flutter", 566 | ); 567 | OTHER_LDFLAGS = ( 568 | "$(inherited)", 569 | "-ObjC", 570 | "-l\"nfc_in_flutter\"", 571 | "-framework", 572 | "\"Flutter\"", 573 | "-weak_framework", 574 | "\"CoreNFC\"", 575 | ); 576 | PRODUCT_BUNDLE_IDENTIFIER = dev.semler.nfcInFlutterExample; 577 | PRODUCT_NAME = "$(TARGET_NAME)"; 578 | VERSIONING_SYSTEM = "apple-generic"; 579 | }; 580 | name = Release; 581 | }; 582 | /* End XCBuildConfiguration section */ 583 | 584 | /* Begin XCConfigurationList section */ 585 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 586 | isa = XCConfigurationList; 587 | buildConfigurations = ( 588 | 97C147031CF9000F007C117D /* Debug */, 589 | 97C147041CF9000F007C117D /* Release */, 590 | 249021D3217E4FDB00AE95B9 /* Profile */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 596 | isa = XCConfigurationList; 597 | buildConfigurations = ( 598 | 97C147061CF9000F007C117D /* Debug */, 599 | 97C147071CF9000F007C117D /* Release */, 600 | 249021D4217E4FDB00AE95B9 /* Profile */, 601 | ); 602 | defaultConfigurationIsVisible = 0; 603 | defaultConfigurationName = Release; 604 | }; 605 | /* End XCConfigurationList section */ 606 | }; 607 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 608 | } 609 | -------------------------------------------------------------------------------- /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 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /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.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/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 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | nfc_in_flutter_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | NFCReaderUsageDescription 26 | Test NFC tag scanning 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.nfc.readersession.formats 6 | 7 | NDEF 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:nfc_in_flutter/nfc_in_flutter.dart'; 5 | 6 | import './read_example_screen.dart'; 7 | import './write_example_screen.dart'; 8 | 9 | void main() => runApp(ExampleApp()); 10 | 11 | class ExampleApp extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | home: Scaffold( 16 | appBar: AppBar( 17 | title: const Text("NFC in Flutter examples"), 18 | ), 19 | body: Builder(builder: (context) { 20 | return ListView( 21 | children: [ 22 | ListTile( 23 | title: const Text("Read NFC"), 24 | onTap: () { 25 | Navigator.pushNamed(context, "/read_example"); 26 | }, 27 | ), 28 | ListTile( 29 | title: const Text("Write NFC"), 30 | onTap: () { 31 | Navigator.pushNamed(context, "/write_example"); 32 | }, 33 | ), 34 | ], 35 | ); 36 | }), 37 | ), 38 | routes: { 39 | "/read_example": (context) => ReadExampleScreen(), 40 | "/write_example": (context) => WriteExampleScreen(), 41 | }, 42 | ); 43 | } 44 | } 45 | 46 | class MyApp extends StatefulWidget { 47 | @override 48 | _MyAppState createState() => _MyAppState(); 49 | } 50 | 51 | class _MyAppState extends State { 52 | // _stream is a subscription to the stream returned by `NFC.read()`. 53 | // The subscription is stored in state so the stream can be canceled later 54 | StreamSubscription? _stream; 55 | 56 | // _tags is a list of scanned tags 57 | List _tags = []; 58 | 59 | bool _supportsNFC = false; 60 | 61 | // _readNFC() calls `NFC.readNDEF()` and stores the subscription and scanned 62 | // tags in state 63 | void _readNFC(BuildContext context) { 64 | try { 65 | // ignore: cancel_subscriptions 66 | StreamSubscription subscription = NFC.readNDEF().listen( 67 | (tag) { 68 | // On new tag, add it to state 69 | setState(() { 70 | _tags.insert(0, tag); 71 | }); 72 | }, 73 | // When the stream is done, remove the subscription from state 74 | onDone: () { 75 | setState(() { 76 | _stream = null; 77 | }); 78 | }, 79 | // Errors are unlikely to happen on Android unless the NFC tags are 80 | // poorly formatted or removed too soon, however on iOS at least one 81 | // error is likely to happen. NFCUserCanceledSessionException will 82 | // always happen unless you call readNDEF() with the `throwOnUserCancel` 83 | // argument set to false. 84 | // NFCSessionTimeoutException will be thrown if the session timer exceeds 85 | // 60 seconds (iOS only). 86 | // And then there are of course errors for unexpected stuff. Good fun! 87 | onError: (e) { 88 | setState(() { 89 | _stream = null; 90 | }); 91 | 92 | if (!(e is NFCUserCanceledSessionException)) { 93 | showDialog( 94 | context: context, 95 | builder: (context) => AlertDialog( 96 | title: const Text("Error!"), 97 | content: Text(e.toString()), 98 | ), 99 | ); 100 | } 101 | }); 102 | 103 | setState(() { 104 | _stream = subscription; 105 | }); 106 | } catch (err) { 107 | print("error: $err"); 108 | } 109 | } 110 | 111 | // _stopReading() cancels the current reading stream 112 | void _stopReading() { 113 | _stream?.cancel(); 114 | setState(() { 115 | _stream = null; 116 | }); 117 | } 118 | 119 | @override 120 | void initState() { 121 | super.initState(); 122 | NFC.isNDEFSupported.then((supported) { 123 | setState(() { 124 | _supportsNFC = true; 125 | }); 126 | }); 127 | } 128 | 129 | @override 130 | void dispose() { 131 | super.dispose(); 132 | _stream?.cancel(); 133 | } 134 | 135 | @override 136 | Widget build(BuildContext context) { 137 | return MaterialApp( 138 | theme: ThemeData( 139 | primarySwatch: Colors.grey, 140 | ), 141 | home: Scaffold( 142 | appBar: AppBar( 143 | backgroundColor: Colors.white, 144 | title: const Text('NFC in Flutter'), 145 | actions: [ 146 | Builder( 147 | builder: (context) { 148 | if (!_supportsNFC) { 149 | return TextButton( 150 | child: Text("NFC unsupported"), 151 | onPressed: null, 152 | ); 153 | } 154 | return TextButton( 155 | child: 156 | Text(_stream == null ? "Start reading" : "Stop reading"), 157 | onPressed: () { 158 | if (_stream == null) { 159 | _readNFC(context); 160 | } else { 161 | _stopReading(); 162 | } 163 | }, 164 | ); 165 | }, 166 | ), 167 | IconButton( 168 | icon: Icon(Icons.clear_all), 169 | onPressed: () { 170 | setState(() { 171 | _tags.clear(); 172 | }); 173 | }, 174 | tooltip: "Clear", 175 | ), 176 | ], 177 | ), 178 | // Render list of scanned tags 179 | body: ListView.builder( 180 | itemCount: _tags.length, 181 | itemBuilder: (context, index) { 182 | const TextStyle payloadTextStyle = const TextStyle( 183 | fontSize: 15, 184 | color: const Color(0xFF454545), 185 | ); 186 | 187 | return Padding( 188 | padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), 189 | child: Column( 190 | crossAxisAlignment: CrossAxisAlignment.start, 191 | children: [ 192 | const Text("NDEF Tag", 193 | style: const TextStyle(fontWeight: FontWeight.bold)), 194 | Builder( 195 | builder: (context) { 196 | // Build list of records 197 | List records = []; 198 | for (int i = 0; i < _tags[index].records.length; i++) { 199 | records.add(Column( 200 | crossAxisAlignment: CrossAxisAlignment.start, 201 | children: [ 202 | Text( 203 | "Record ${i + 1} - ${_tags[index].records[i].type}", 204 | style: const TextStyle( 205 | fontSize: 13, 206 | color: const Color(0xFF666666), 207 | ), 208 | ), 209 | Text( 210 | _tags[index].records[i].payload, 211 | style: payloadTextStyle, 212 | ), 213 | Text( 214 | _tags[index].records[i].data, 215 | style: payloadTextStyle, 216 | ), 217 | ], 218 | )); 219 | } 220 | return Column( 221 | crossAxisAlignment: CrossAxisAlignment.start, 222 | children: records, 223 | ); 224 | }, 225 | ) 226 | ], 227 | ), 228 | ); 229 | }, 230 | ), 231 | ), 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /example/lib/read_example_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:nfc_in_flutter/nfc_in_flutter.dart'; 4 | 5 | class ReadExampleScreen extends StatefulWidget { 6 | @override 7 | _ReadExampleScreenState createState() => _ReadExampleScreenState(); 8 | } 9 | 10 | class _ReadExampleScreenState extends State { 11 | StreamSubscription? _stream; 12 | 13 | void _startScanning() { 14 | setState(() { 15 | _stream = NFC 16 | .readNDEF(alertMessage: "Custom message with readNDEF#alertMessage") 17 | .listen((NDEFMessage message) { 18 | if (message.isEmpty) { 19 | print("Read empty NDEF message"); 20 | return; 21 | } 22 | print("Read NDEF message with ${message.records.length} records"); 23 | for (NDEFRecord record in message.records) { 24 | print( 25 | "Record '${record.id ?? "[NO ID]"}' with TNF '${record.tnf}', type '${record.type}', payload '${record.payload}' and data '${record.data}' and language code '${record.languageCode}'"); 26 | } 27 | }, onError: (error) { 28 | setState(() { 29 | _stream = null; 30 | }); 31 | if (error is NFCUserCanceledSessionException) { 32 | print("user canceled"); 33 | } else if (error is NFCSessionTimeoutException) { 34 | print("session timed out"); 35 | } else { 36 | print("error: $error"); 37 | } 38 | }, onDone: () { 39 | setState(() { 40 | _stream = null; 41 | }); 42 | }); 43 | }); 44 | } 45 | 46 | void _stopScanning() { 47 | _stream?.cancel(); 48 | setState(() { 49 | _stream = null; 50 | }); 51 | } 52 | 53 | void _toggleScan() { 54 | if (_stream == null) { 55 | _startScanning(); 56 | } else { 57 | _stopScanning(); 58 | } 59 | } 60 | 61 | @override 62 | void dispose() { 63 | super.dispose(); 64 | _stopScanning(); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Scaffold( 70 | appBar: AppBar( 71 | title: const Text("Read NFC example"), 72 | ), 73 | body: Center( 74 | child: ElevatedButton( 75 | child: const Text("Toggle scan"), 76 | onPressed: _toggleScan, 77 | )), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/lib/write_example_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nfc_in_flutter/nfc_in_flutter.dart'; 3 | import 'dart:async'; 4 | import 'dart:io'; 5 | 6 | class RecordEditor { 7 | final TextEditingController mediaTypeController = TextEditingController(); 8 | final TextEditingController payloadController = TextEditingController(); 9 | } 10 | 11 | class WriteExampleScreen extends StatefulWidget { 12 | @override 13 | _WriteExampleScreenState createState() => _WriteExampleScreenState(); 14 | } 15 | 16 | class _WriteExampleScreenState extends State { 17 | StreamSubscription? _stream; 18 | List _records = []; 19 | bool _hasClosedWriteDialog = false; 20 | 21 | void _addRecord() { 22 | setState(() { 23 | _records.add(RecordEditor()); 24 | }); 25 | } 26 | 27 | void _write(BuildContext context) async { 28 | List records = _records.map((record) { 29 | return NDEFRecord.type( 30 | record.mediaTypeController.text, 31 | record.payloadController.text, 32 | ); 33 | }).toList(); 34 | NDEFMessage message = NDEFMessage.withRecords(records); 35 | 36 | // Show dialog on Android (iOS has it's own one) 37 | if (Platform.isAndroid) { 38 | showDialog( 39 | context: context, 40 | builder: (context) => AlertDialog( 41 | title: const Text("Scan the tag you want to write to"), 42 | actions: [ 43 | TextButton( 44 | child: const Text("Cancel"), 45 | onPressed: () { 46 | _hasClosedWriteDialog = true; 47 | _stream?.cancel(); 48 | Navigator.pop(context); 49 | }, 50 | ), 51 | ], 52 | ), 53 | ); 54 | } 55 | 56 | // Write to the first tag scanned 57 | await NFC.writeNDEF(message).first; 58 | if (!_hasClosedWriteDialog) { 59 | Navigator.pop(context); 60 | } 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | return Scaffold( 66 | appBar: AppBar( 67 | title: const Text("Write NFC example"), 68 | ), 69 | body: ListView( 70 | padding: const EdgeInsets.all(20), 71 | children: [ 72 | Center( 73 | child: OutlinedButton( 74 | child: const Text("Add record"), 75 | onPressed: _addRecord, 76 | ), 77 | ), 78 | for (var record in _records) 79 | Padding( 80 | padding: const EdgeInsets.only(bottom: 30), 81 | child: Column( 82 | crossAxisAlignment: CrossAxisAlignment.start, 83 | children: [ 84 | Text("Record", style: Theme.of(context).textTheme.bodyText1), 85 | TextFormField( 86 | controller: record.mediaTypeController, 87 | decoration: InputDecoration( 88 | hintText: "Media type", 89 | ), 90 | ), 91 | TextFormField( 92 | controller: record.payloadController, 93 | decoration: InputDecoration( 94 | hintText: "Payload", 95 | ), 96 | ) 97 | ], 98 | ), 99 | ), 100 | Center( 101 | child: ElevatedButton( 102 | child: const Text("Write to tag"), 103 | onPressed: _records.length > 0 ? () => _write(context) : null, 104 | ), 105 | ), 106 | ], 107 | ), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /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 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.5.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | cupertino_icons: 47 | dependency: "direct main" 48 | description: 49 | name: cupertino_icons 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.1.2" 53 | fake_async: 54 | dependency: transitive 55 | description: 56 | name: fake_async 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.2.0" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.10" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.3.0" 84 | nfc_in_flutter: 85 | dependency: "direct dev" 86 | description: 87 | path: ".." 88 | relative: true 89 | source: path 90 | version: "3.0.0" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.8.0" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.8.0" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.0" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.0" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.2.19" 145 | typed_data: 146 | dependency: transitive 147 | description: 148 | name: typed_data 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.3.0" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.1.0" 159 | sdks: 160 | dart: ">=2.12.0 <3.0.0" 161 | flutter: ">=1.10.0" 162 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nfc_in_flutter_example 2 | description: Demonstrates how to use the nfc_in_flutter plugin. 3 | publish_to: "none" 4 | version: 1.0.0 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | # The following adds the Cupertino Icons font to your application. 14 | # Use with the CupertinoIcons class for iOS style icons. 15 | cupertino_icons: ^0.1.2 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | 21 | nfc_in_flutter: 22 | path: ../ 23 | 24 | # For information on the generic Dart part of this file, see the 25 | # following page: https://dart.dev/tools/pub/pubspec 26 | 27 | # The following section is specific to Flutter. 28 | flutter: 29 | # The following line ensures that the Material Icons font is 30 | # included with your application, so that you can use the icons in 31 | # the material Icons class. 32 | uses-material-design: true 33 | # To add assets to your application, add an assets section, like this: 34 | # assets: 35 | # - images/a_dot_burr.jpeg 36 | # - images/a_dot_ham.jpeg 37 | # An image asset can refer to one or more resolution-specific "variants", see 38 | # https://flutter.dev/assets-and-images/#resolution-aware. 39 | # For details regarding adding assets from package dependencies, see 40 | # https://flutter.dev/assets-and-images/#from-packages 41 | # To add custom fonts to your application, add a fonts section here, 42 | # in this "flutter" section. Each entry in this list should have a 43 | # "family" key with the font family name, and a "fonts" key with a 44 | # list giving the asset and other descriptors for the font. For 45 | # example: 46 | # fonts: 47 | # - family: Schyler 48 | # fonts: 49 | # - asset: fonts/Schyler-Regular.ttf 50 | # - asset: fonts/Schyler-Italic.ttf 51 | # style: italic 52 | # - family: Trajan Pro 53 | # fonts: 54 | # - asset: fonts/TrajanPro.ttf 55 | # - asset: fonts/TrajanPro_Bold.ttf 56 | # weight: 700 57 | # 58 | # For details regarding fonts from package dependencies, 59 | # see https://flutter.dev/custom-fonts/#from-packages 60 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semlette/nfc_in_flutter/79c845025dd13ab70584d1ebe45c1de1f5f0ea97/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/NfcInFlutterPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @protocol NFCWrapper 5 | - (void)startReading:(BOOL)once alertMessage:(NSString* _Nonnull)alertMessage; 6 | - (BOOL)isEnabled; 7 | - (void)writeToTag:(NSDictionary* _Nonnull)data completionHandler:(void (^_Nonnull) (FlutterError * _Nullable error))completionHandler; 8 | @end 9 | 10 | 11 | @interface NfcInFlutterPlugin : NSObject { 12 | FlutterEventSink events; 13 | NSObject* wrapper; 14 | } 15 | @end 16 | 17 | API_AVAILABLE(ios(11)) 18 | @interface NFCWrapperBase : NSObject { 19 | FlutterEventSink events; 20 | NFCNDEFReaderSession* session; 21 | } 22 | - (void)readerSession:(nonnull NFCNDEFReaderSession *)session didInvalidateWithError:(nonnull NSError *)error; 23 | 24 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events; 25 | 26 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments; 27 | 28 | - (NSDictionary * _Nonnull)formatMessageWithIdentifier:(NSString* _Nonnull)identifier message:(NFCNDEFMessage* _Nonnull)message; 29 | 30 | - (NFCNDEFMessage * _Nonnull)formatNDEFMessageWithDictionary:(NSDictionary* _Nonnull)dictionary; 31 | @end 32 | 33 | API_AVAILABLE(ios(11)) 34 | @interface NFCWrapperImpl : NFCWrapperBase { 35 | FlutterMethodChannel* methodChannel; 36 | dispatch_queue_t dispatchQueue; 37 | } 38 | -(id _Nullable )init:(FlutterMethodChannel*_Nonnull)methodChannel dispatchQueue:(dispatch_queue_t _Nonnull )dispatchQueue; 39 | @end 40 | 41 | API_AVAILABLE(ios(13)) 42 | @interface NFCWritableWrapperImpl : NFCWrapperImpl 43 | 44 | @property (atomic, retain) __kindof id _Nullable lastTag; 45 | 46 | @end 47 | 48 | @interface NFCUnsupportedWrapper : NSObject 49 | @end 50 | -------------------------------------------------------------------------------- /ios/Classes/NfcInFlutterPlugin.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "NfcInFlutterPlugin.h" 3 | 4 | @implementation NfcInFlutterPlugin { 5 | dispatch_queue_t dispatchQueue; 6 | } 7 | + (void)registerWithRegistrar:(NSObject*)registrar { 8 | dispatch_queue_t dispatchQueue = dispatch_queue_create("dev.semler.nfc_in_flutter.dispatch_queue", NULL); 9 | 10 | FlutterMethodChannel* channel = [FlutterMethodChannel 11 | methodChannelWithName:@"nfc_in_flutter" 12 | binaryMessenger:[registrar messenger]]; 13 | 14 | FlutterEventChannel* tagChannel = [FlutterEventChannel 15 | eventChannelWithName:@"nfc_in_flutter/tags" 16 | binaryMessenger:[registrar messenger]]; 17 | 18 | NfcInFlutterPlugin* instance = [[NfcInFlutterPlugin alloc] 19 | init:dispatchQueue 20 | channel:channel]; 21 | 22 | [registrar addMethodCallDelegate:instance channel:channel]; 23 | [tagChannel setStreamHandler:instance->wrapper]; 24 | } 25 | 26 | - (id)init:(dispatch_queue_t)dispatchQueue channel:(FlutterMethodChannel*)channel { 27 | self->dispatchQueue = dispatchQueue; 28 | if (@available(iOS 13.0, *)) { 29 | wrapper = [[NFCWritableWrapperImpl alloc] init:channel dispatchQueue:dispatchQueue]; 30 | } else if (@available(iOS 11.0, *)) { 31 | wrapper = [[NFCWrapperImpl alloc] init:channel dispatchQueue:dispatchQueue]; 32 | } else { 33 | wrapper = [[NFCUnsupportedWrapper alloc] init]; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 39 | dispatch_async(dispatchQueue, ^{ 40 | [self handleMethodCallAsync:call result:result]; 41 | }); 42 | } 43 | 44 | - (void)handleMethodCallAsync:(FlutterMethodCall*)call result:(FlutterResult)result { 45 | if ([@"readNDEFSupported" isEqualToString:call.method]) { 46 | result([NSNumber numberWithBool:[wrapper isEnabled]]); 47 | } else if ([@"startNDEFReading" isEqualToString:call.method]) { 48 | NSDictionary* args = call.arguments; 49 | [wrapper startReading:[args[@"scan_once"] boolValue] alertMessage:args[@"alert_message"]]; 50 | result(nil); 51 | } else if ([@"writeNDEF" isEqualToString:call.method]) { 52 | NSDictionary* args = call.arguments; 53 | [wrapper writeToTag:args completionHandler:^(FlutterError * _Nullable error) { 54 | result(error); 55 | }]; 56 | } else { 57 | result(FlutterMethodNotImplemented); 58 | } 59 | } 60 | 61 | @end 62 | 63 | 64 | @implementation NFCWrapperBase 65 | 66 | - (void)readerSession:(nonnull NFCNDEFReaderSession *)session didInvalidateWithError:(nonnull NSError *)error API_AVAILABLE(ios(11.0)) { 67 | // When a session has been invalidated it needs to be created again to work. 68 | // Since this function is called when it invalidates, the session can safely be removed. 69 | // A new session doesn't have to be created immediately as that will happen the next time 70 | // startReading() is called. 71 | self->session = nil; 72 | 73 | // If the event stream is closed we can't send the error 74 | if (self->events == nil) { 75 | return; 76 | } 77 | dispatch_async(dispatch_get_main_queue(), ^{ 78 | switch ([error code]) { 79 | case NFCReaderSessionInvalidationErrorFirstNDEFTagRead: 80 | // When this error is returned it doesn't need to be sent to the client 81 | // as it cancels the stream after 1 read anyways 82 | //self->events(FlutterEndOfEventStream); 83 | return; 84 | case NFCReaderErrorUnsupportedFeature: 85 | self->events([FlutterError 86 | errorWithCode:@"NDEFUnsupportedFeatureError" 87 | message:error.localizedDescription 88 | details:nil]); 89 | break; 90 | case NFCReaderSessionInvalidationErrorUserCanceled: 91 | self->events([FlutterError 92 | errorWithCode:@"UserCanceledSessionError" 93 | message:error.localizedDescription 94 | details:nil]); 95 | break; 96 | case NFCReaderSessionInvalidationErrorSessionTimeout: 97 | self->events([FlutterError 98 | errorWithCode:@"SessionTimeoutError" 99 | message:error.localizedDescription 100 | details:nil]); 101 | break; 102 | case NFCReaderSessionInvalidationErrorSessionTerminatedUnexpectedly: 103 | self->events([FlutterError 104 | errorWithCode:@"SessionTerminatedUnexpectedlyError" 105 | message:error.localizedDescription 106 | details:nil]); 107 | break; 108 | case NFCReaderSessionInvalidationErrorSystemIsBusy: 109 | self->events([FlutterError 110 | errorWithCode:@"SystemIsBusyError" 111 | message:error.localizedDescription 112 | details:nil]); 113 | break; 114 | default: 115 | self->events([FlutterError 116 | errorWithCode:@"SessionError" 117 | message:error.localizedDescription 118 | details:nil]); 119 | } 120 | }); 121 | } 122 | 123 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { 124 | self->events = events; 125 | return nil; 126 | } 127 | 128 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { 129 | if (session != nil) { 130 | if ([session isReady]) { 131 | [session invalidateSession]; 132 | } 133 | session = nil; 134 | } 135 | events = nil; 136 | return nil; 137 | } 138 | 139 | // formatMessageWithIdentifier turns a NFCNDEFMessage into a NSDictionary that 140 | // is ready to be sent to Flutter 141 | - (NSDictionary * _Nonnull)formatMessageWithIdentifier:(NSString* _Nonnull)identifier message:(NFCNDEFMessage* _Nonnull)message { 142 | NSMutableArray* records = [[NSMutableArray alloc] initWithCapacity:message.records.count]; 143 | for (NFCNDEFPayload* payload in message.records) { 144 | NSString* type; 145 | type = [[NSString alloc] 146 | initWithData:payload.type 147 | encoding:NSUTF8StringEncoding]; 148 | 149 | NSString* payloadData; 150 | NSString* data; 151 | NSString* languageCode; 152 | if ([@"T" isEqualToString:type]) { 153 | // Remove the first byte from the payload 154 | payloadData = [[NSString alloc] 155 | initWithData:[payload.payload 156 | subdataWithRange:NSMakeRange(1, payload.payload.length-1)] 157 | encoding:NSUTF8StringEncoding]; 158 | 159 | const unsigned char* bytes = [payload.payload bytes]; 160 | int languageCodeLength = bytes[0] & 0x3f; 161 | languageCode = [[NSString alloc] 162 | initWithData:[payload.payload 163 | subdataWithRange:NSMakeRange(1, languageCodeLength)] 164 | encoding:NSUTF8StringEncoding]; 165 | // Exclude the language code from the data 166 | data = [[NSString alloc] 167 | initWithData:[payload.payload 168 | subdataWithRange:NSMakeRange(languageCodeLength+1, payload.payload.length-languageCodeLength-1)] 169 | encoding:NSUTF8StringEncoding]; 170 | } else if ([@"U" isEqualToString:type]) { 171 | NSString* url; 172 | const unsigned char* bytes = [payload.payload bytes]; 173 | int prefixByte = bytes[0]; 174 | switch (prefixByte) { 175 | case 0x01: 176 | url = @"http://www."; 177 | break; 178 | case 0x02: 179 | url = @"https://www."; 180 | break; 181 | case 0x03: 182 | url = @"http://"; 183 | break; 184 | case 0x04: 185 | url = @"https://"; 186 | break; 187 | case 0x05: 188 | url = @"tel:"; 189 | break; 190 | case 0x06: 191 | url = @"mailto:"; 192 | break; 193 | case 0x07: 194 | url = @"ftp://anonymous:anonymous@"; 195 | break; 196 | case 0x08: 197 | url = @"ftp://ftp."; 198 | break; 199 | case 0x09: 200 | url = @"ftps://"; 201 | break; 202 | case 0x0A: 203 | url = @"sftp://"; 204 | break; 205 | case 0x0B: 206 | url = @"smb://"; 207 | break; 208 | case 0x0C: 209 | url = @"nfs://"; 210 | break; 211 | case 0x0D: 212 | url = @"ftp://"; 213 | break; 214 | case 0x0E: 215 | url = @"dav://"; 216 | break; 217 | case 0x0F: 218 | url = @"news:"; 219 | break; 220 | case 0x10: 221 | url = @"telnet://"; 222 | break; 223 | case 0x11: 224 | url = @"imap:"; 225 | break; 226 | case 0x12: 227 | url = @"rtsp://"; 228 | break; 229 | case 0x13: 230 | url = @"urn:"; 231 | break; 232 | case 0x14: 233 | url = @"pop:"; 234 | break; 235 | case 0x15: 236 | url = @"sip:"; 237 | break; 238 | case 0x16: 239 | url = @"sips"; 240 | break; 241 | case 0x17: 242 | url = @"tftp:"; 243 | break; 244 | case 0x18: 245 | url = @"btspp://"; 246 | break; 247 | case 0x19: 248 | url = @"btl2cap://"; 249 | break; 250 | case 0x1A: 251 | url = @"btgoep://"; 252 | break; 253 | case 0x1B: 254 | url = @"btgoep://"; 255 | break; 256 | case 0x1C: 257 | url = @"irdaobex://"; 258 | break; 259 | case 0x1D: 260 | url = @"file://"; 261 | break; 262 | case 0x1E: 263 | url = @"urn:epc:id:"; 264 | break; 265 | case 0x1F: 266 | url = @"urn:epc:tag:"; 267 | break; 268 | case 0x20: 269 | url = @"urn:epc:pat:"; 270 | break; 271 | case 0x21: 272 | url = @"urn:epc:raw:"; 273 | break; 274 | case 0x22: 275 | url = @"urn:epc:"; 276 | break; 277 | case 0x23: 278 | url = @"urn:nfc:"; 279 | break; 280 | default: 281 | url = @""; 282 | } 283 | // Remove the first byte from and add the URL prefix to the payload 284 | NSString* trimmedPayload = [[NSString alloc] initWithData: 285 | [payload.payload subdataWithRange:NSMakeRange(1, payload.payload.length-1)] encoding:NSUTF8StringEncoding]; 286 | NSMutableString* payloadString = [[NSMutableString alloc] 287 | initWithString:trimmedPayload]; 288 | [payloadString insertString:url atIndex:0]; 289 | payloadData = payloadString; 290 | // Remove the prefix from the payload 291 | data = [[NSString alloc] 292 | initWithData:[payload.payload 293 | subdataWithRange:NSMakeRange(1, payload.payload.length-1)] 294 | encoding:NSUTF8StringEncoding]; 295 | } else { 296 | payloadData = [[NSString alloc] 297 | initWithData:payload.payload 298 | encoding:NSUTF8StringEncoding]; 299 | data = payloadData; 300 | } 301 | 302 | NSString* identifier; 303 | identifier = [[NSString alloc] 304 | initWithData:payload.identifier 305 | encoding:NSUTF8StringEncoding]; 306 | 307 | NSString* tnf; 308 | switch (payload.typeNameFormat) { 309 | case NFCTypeNameFormatEmpty: 310 | tnf = @"empty"; 311 | break; 312 | case NFCTypeNameFormatNFCWellKnown: 313 | tnf = @"well_known"; 314 | break; 315 | case NFCTypeNameFormatMedia: 316 | tnf = @"mime_media"; 317 | break; 318 | case NFCTypeNameFormatAbsoluteURI: 319 | tnf = @"absolute_uri"; 320 | break; 321 | case NFCTypeNameFormatNFCExternal: 322 | tnf = @"external_type"; 323 | break; 324 | case NFCTypeNameFormatUnchanged: 325 | tnf = @"unchanged"; 326 | break; 327 | default: 328 | tnf = @"unknown"; 329 | } 330 | 331 | NSMutableDictionary* record = [[NSMutableDictionary alloc] 332 | initWithObjectsAndKeys:type, @"type", 333 | payloadData, @"payload", 334 | data, @"data", 335 | identifier, @"id", 336 | tnf, @"tnf", nil]; 337 | if (languageCode != nil) { 338 | [record setObject:languageCode forKey:@"languageCode"]; 339 | } 340 | 341 | [record setObject:[FlutterStandardTypedData typedDataWithBytes:payload.payload] forKey:@"rawPayload"]; 342 | 343 | [records addObject:record]; 344 | } 345 | NSDictionary* result = @{ 346 | @"id": identifier, 347 | @"message_type": @"ndef", 348 | @"records": records, 349 | }; 350 | return result; 351 | } 352 | 353 | - (NFCNDEFMessage* _Nonnull)formatNDEFMessageWithDictionary:(NSDictionary* _Nonnull)dictionary API_AVAILABLE(ios(13.0)) { 354 | NSMutableArray* ndefRecords = [[NSMutableArray alloc] init]; 355 | 356 | NSDictionary *message = [dictionary valueForKey:@"message"]; 357 | NSArray* records = [message valueForKey:@"records"]; 358 | for (NSDictionary* record in records) { 359 | NSString* recordID = [record valueForKey:@"id"]; 360 | NSString* recordType = [record valueForKey:@"type"]; 361 | NSString* recordPayload = [record valueForKey:@"payload"]; 362 | NSString* recordTNF = [record valueForKey:@"tnf"]; 363 | NSString* recordLanguageCode = [record valueForKey:@"languageCode"]; 364 | 365 | NSData* idData; 366 | if (recordID) { 367 | idData = [recordID dataUsingEncoding:NSUTF8StringEncoding]; 368 | } else { 369 | idData = [NSData data]; 370 | } 371 | NSData* payloadData; 372 | if (recordPayload) { 373 | payloadData = [recordPayload dataUsingEncoding:NSUTF8StringEncoding]; 374 | } else { 375 | payloadData = [NSData data]; 376 | } 377 | NSData* typeData; 378 | if (recordType) { 379 | typeData = [recordType dataUsingEncoding:NSUTF8StringEncoding]; 380 | } else { 381 | typeData = [NSData data]; 382 | } 383 | NFCTypeNameFormat tnfValue; 384 | 385 | if ([@"empty" isEqualToString:recordTNF]) { 386 | // Empty records are not allowed to have a ID, type or payload. 387 | NFCNDEFPayload* ndefRecord = [[NFCNDEFPayload alloc] initWithFormat:NFCTypeNameFormatEmpty type:[[NSData alloc] init] identifier:[[NSData alloc] init] payload:[[NSData alloc] init]]; 388 | [ndefRecords addObject:ndefRecord]; 389 | continue; 390 | } else if ([@"well_known" isEqualToString:recordTNF]) { 391 | if ([@"T" isEqualToString:recordType]) { 392 | NSLocale* locale = [NSLocale localeWithLocaleIdentifier:recordLanguageCode]; 393 | NFCNDEFPayload* ndefRecord = [NFCNDEFPayload wellKnownTypeTextPayloadWithString:recordPayload locale:locale]; 394 | [ndefRecords addObject:ndefRecord]; 395 | continue; 396 | } else if ([@"U" isEqualToString:recordType]) { 397 | NFCNDEFPayload* ndefRecord = [NFCNDEFPayload wellKnownTypeURIPayloadWithString:recordPayload]; 398 | [ndefRecords addObject:ndefRecord]; 399 | continue; 400 | } else { 401 | tnfValue = NFCTypeNameFormatNFCWellKnown; 402 | } 403 | } else if ([@"mime_media" isEqualToString:recordTNF]) { 404 | tnfValue = NFCTypeNameFormatMedia; 405 | } else if ([@"absolute_uri" isEqualToString:recordTNF]) { 406 | tnfValue = NFCTypeNameFormatAbsoluteURI; 407 | } else if ([@"external_type" isEqualToString:recordTNF]) { 408 | tnfValue = NFCTypeNameFormatNFCExternal; 409 | } else if ([@"unchanged" isEqualToString:recordTNF]) { 410 | // TODO: Return error, not supposed to change the TNF value 411 | tnfValue = NFCTypeNameFormatUnchanged; 412 | continue; 413 | } else { 414 | tnfValue = NFCTypeNameFormatUnknown; 415 | // Unknown records are not allowed to have a type 416 | typeData = [[NSData alloc] init]; 417 | } 418 | 419 | NFCNDEFPayload* ndefRecord = [[NFCNDEFPayload alloc] initWithFormat:tnfValue type:typeData identifier:idData payload:payloadData]; 420 | [ndefRecords addObject:ndefRecord]; 421 | } 422 | 423 | return [[NFCNDEFMessage alloc] initWithNDEFRecords:ndefRecords]; 424 | } 425 | 426 | @end 427 | 428 | @implementation NFCWrapperImpl 429 | 430 | - (id)init:(FlutterMethodChannel*)methodChannel dispatchQueue:(dispatch_queue_t)dispatchQueue { 431 | self->methodChannel = methodChannel; 432 | self->dispatchQueue = dispatchQueue; 433 | return self; 434 | } 435 | 436 | - (void)startReading:(BOOL)once alertMessage:(NSString* _Nonnull)alertMessage { 437 | if (self->session == nil) { 438 | self->session = [[NFCNDEFReaderSession alloc]initWithDelegate:self queue:self->dispatchQueue invalidateAfterFirstRead: once]; 439 | self->session.alertMessage = alertMessage; 440 | } 441 | [self->session beginSession]; 442 | } 443 | 444 | - (BOOL)isEnabled { 445 | return NFCNDEFReaderSession.readingAvailable; 446 | } 447 | 448 | - (void)readerSession:(nonnull NFCNDEFReaderSession *)session didDetectNDEFs:(nonnull NSArray *)messages API_AVAILABLE(ios(11.0)) { 449 | // Iterate through the messages and send them to Flutter with the following structure: 450 | // { Map 451 | // "message_type": "ndef", 452 | // "records": [ List 453 | // { Map 454 | // "type": "The record's content type", 455 | // "payload": "The record's payload", 456 | // "id": "The record's identifier", 457 | // } 458 | // ] 459 | // } 460 | for (NFCNDEFMessage* message in messages) { 461 | NSDictionary* result = [self formatMessageWithIdentifier:@"" message:message]; 462 | dispatch_async(dispatch_get_main_queue(), ^{ 463 | if (self->events != nil) { 464 | self->events(result); 465 | } 466 | }); 467 | } 468 | } 469 | 470 | - (void)readerSession:(NFCNDEFReaderSession *)session 471 | didDetectTags:(NSArray<__kindof id> *)tags API_AVAILABLE(ios(13.0)) { 472 | // Iterate through the tags and send them to Flutter with the following structure: 473 | // { Map 474 | // "id": "", // empty 475 | // "message_type": "ndef", 476 | // "records": [ List 477 | // { Map 478 | // "type": "The record's content type", 479 | // "payload": "The record's payload", 480 | // "id": "The record's identifier", 481 | // } 482 | // ] 483 | // } 484 | 485 | for (id tag in tags) { 486 | [session connectToTag:tag completionHandler:^(NSError * _Nullable error) { 487 | if (error != nil) { 488 | NSLog(@"connect error: %@", error.localizedDescription); 489 | return; 490 | } 491 | [tag readNDEFWithCompletionHandler:^(NFCNDEFMessage * _Nullable message, NSError * _Nullable error) { 492 | 493 | if (error != nil) { 494 | NSLog(@"ERROR: %@", error.localizedDescription); 495 | return; 496 | } 497 | 498 | NSDictionary* result = [self formatMessageWithIdentifier:@"" message:message]; 499 | dispatch_async(dispatch_get_main_queue(), ^{ 500 | if (self->events != nil) { 501 | self->events(result); 502 | } 503 | }); 504 | }]; 505 | }]; 506 | } 507 | } 508 | 509 | - (void)readerSessionDidBecomeActive:(NFCNDEFReaderSession *)session API_AVAILABLE(ios(13.0)) {} 510 | 511 | - (void)writeToTag:(NSDictionary*)data completionHandler:(void (^_Nonnull) (FlutterError * _Nullable error))completionHandler { 512 | completionHandler(nil); 513 | } 514 | 515 | @end 516 | 517 | @implementation NFCWritableWrapperImpl 518 | 519 | @synthesize lastTag; 520 | 521 | - (void)readerSession:(NFCNDEFReaderSession *)session 522 | didDetectTags:(NSArray<__kindof id> *)tags API_AVAILABLE(ios(13.0)) { 523 | [super readerSession:session didDetectTags:tags]; 524 | 525 | // Set the last tags scanned 526 | lastTag = tags[[tags count] - 1]; 527 | } 528 | 529 | - (void)writeToTag:(NSDictionary*)data completionHandler:(void (^_Nonnull) (FlutterError * _Nullable error))completionHandler { 530 | NFCNDEFMessage* ndefMessage = [self formatNDEFMessageWithDictionary:data]; 531 | 532 | if (lastTag != nil) { 533 | if (!lastTag.available) { 534 | completionHandler([FlutterError errorWithCode:@"NFCTagUnavailable" message:@"the tag is unavailable for writing" details:nil]); 535 | return; 536 | } 537 | 538 | // Connect to the tag. 539 | // The tag might already be connected to, but it doesn't hurt to do it again. 540 | [session connectToTag:lastTag completionHandler:^(NSError * _Nullable error) { 541 | if (error != nil) { 542 | completionHandler([FlutterError errorWithCode:@"IOError" message:[NSString stringWithFormat:@"could not connect to tag: %@", error.localizedDescription] details:nil]); 543 | return; 544 | } 545 | // Get the tag's read/write status 546 | [self->lastTag queryNDEFStatusWithCompletionHandler:^(NFCNDEFStatus status, NSUInteger capacity, NSError* _Nullable error) { 547 | 548 | if (error != nil) { 549 | completionHandler([FlutterError errorWithCode:@"NFCUnexpectedError" message:error.localizedDescription details:nil]); 550 | return; 551 | } 552 | 553 | // Write to the tag if possible 554 | if (status == NFCNDEFStatusReadWrite) { 555 | [self->lastTag writeNDEF:ndefMessage completionHandler:^(NSError* _Nullable error) { 556 | if (error != nil) { 557 | FlutterError *flutterError; 558 | switch (error.code) { 559 | case NFCReaderSessionInvalidationErrorUserCanceled: 560 | flutterError = [FlutterError errorWithCode:@"UserCanceledSessionError" message:@"the user has canceled the reading session" details:nil]; 561 | break; 562 | case NFCNdefReaderSessionErrorTagNotWritable: 563 | flutterError = [FlutterError errorWithCode:@"NFCTagNotWritableError" message:@"the tag is not writable" details:nil]; 564 | break; 565 | case NFCNdefReaderSessionErrorTagSizeTooSmall: { 566 | NSDictionary *details = @{ 567 | @"maxSize": [NSNumber numberWithInt:capacity], 568 | }; 569 | flutterError = [FlutterError errorWithCode:@"NFCTagSizeTooSmallError" message:@"the tag's memory size is too small" details:details]; 570 | break; 571 | } 572 | case NFCNdefReaderSessionErrorTagUpdateFailure: 573 | flutterError = [FlutterError errorWithCode:@"NFCUpdateTagError" message:@"the reader failed to update the tag" details:nil]; 574 | break; 575 | default: 576 | flutterError = [FlutterError errorWithCode:@"NFCUnexpectedError" message:error.localizedDescription details:nil]; 577 | } 578 | completionHandler(flutterError); 579 | } else { 580 | // Successfully wrote data to the tag 581 | completionHandler(nil); 582 | } 583 | }]; 584 | } else { 585 | // Writing is not supported on this tag 586 | completionHandler([FlutterError errorWithCode:@"NFCTagNotWritableError" message:@"the tag is not writable" details:nil]); 587 | } 588 | }]; 589 | }]; 590 | } else { 591 | completionHandler([FlutterError errorWithCode:@"NFCTagUnavailable" message:@"no tag to write to" details:nil]); 592 | } 593 | } 594 | 595 | @end 596 | 597 | @implementation NFCUnsupportedWrapper 598 | 599 | - (BOOL)isEnabled { 600 | // https://knowyourmeme.com/photos/1483348-bugs-bunnys-no 601 | return NO; 602 | } 603 | - (void)startReading:(BOOL)once alertMessage:(NSString* _Nonnull)alertMessage { 604 | return; 605 | } 606 | 607 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { 608 | return [FlutterError 609 | errorWithCode:@"NDEFUnsupportedFeatureError" 610 | message:nil 611 | details:nil]; 612 | } 613 | 614 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { 615 | return nil; 616 | } 617 | 618 | - (void)writeToTag:(NSDictionary*)data completionHandler:(void (^_Nonnull) (FlutterError * _Nullable error))completionHandler { 619 | completionHandler([FlutterError 620 | errorWithCode:@"NFCWritingUnsupportedFeatureError" 621 | message:nil 622 | details:nil]); 623 | } 624 | 625 | @end 626 | -------------------------------------------------------------------------------- /ios/nfc_in_flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'nfc_in_flutter' 6 | s.version = '1.0.0' 7 | s.summary = 'Flutter plugin for reading NFC tags' 8 | s.description = 'Flutter plugin for reading NFC tags' 9 | s.homepage = 'https://github.com/semlette/nfc_in_flutter' 10 | s.license = { :file => '../LICENSE' } 11 | s.author = { 'Andi Robin Halgren Semler' => 'andirobinsemler@gmail.com' } 12 | s.source = { :path => '.' } 13 | s.source_files = 'Classes/**/*' 14 | s.public_header_files = 'Classes/**/*.h' 15 | s.dependency 'Flutter' 16 | s.weak_frameworks = ['CoreNFC'] 17 | 18 | s.ios.deployment_target = '8.0' 19 | end 20 | 21 | -------------------------------------------------------------------------------- /lib/nfc_in_flutter.dart: -------------------------------------------------------------------------------- 1 | export './src/api.dart'; 2 | export "./src/exceptions.dart"; 3 | -------------------------------------------------------------------------------- /lib/src/api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:core'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flutter/services.dart'; 6 | 7 | import './exceptions.dart'; 8 | 9 | class NFC { 10 | static MethodChannel _channel = MethodChannel("nfc_in_flutter"); 11 | static const EventChannel _eventChannel = 12 | const EventChannel("nfc_in_flutter/tags"); 13 | 14 | static Stream? _tagStream; 15 | 16 | static Stream _createTagStream() { 17 | return _eventChannel.receiveBroadcastStream().where((tag) { 18 | // In the future when more tag types are supported, this must be changed. 19 | assert(tag is Map); 20 | return tag["message_type"] == "ndef"; 21 | }).map((tag) { 22 | assert(tag is Map); 23 | 24 | List records = []; 25 | for (var record in tag["records"]) { 26 | NFCTypeNameFormat tnf; 27 | switch (record["tnf"]) { 28 | case "empty": 29 | tnf = NFCTypeNameFormat.empty; 30 | break; 31 | case "well_known": 32 | tnf = NFCTypeNameFormat.well_known; 33 | break; 34 | case "mime_media": 35 | tnf = NFCTypeNameFormat.mime_media; 36 | break; 37 | case "absolute_uri": 38 | tnf = NFCTypeNameFormat.absolute_uri; 39 | break; 40 | case "external_type": 41 | tnf = NFCTypeNameFormat.external; 42 | break; 43 | case "unchanged": 44 | tnf = NFCTypeNameFormat.unchanged; 45 | break; 46 | default: 47 | tnf = NFCTypeNameFormat.unknown; 48 | } 49 | 50 | records.add(NDEFRecord._internal( 51 | record["id"], 52 | record["payload"], 53 | record["type"], 54 | tnf, 55 | record["data"], 56 | record["languageCode"], 57 | record["rawPayload"], 58 | )); 59 | } 60 | 61 | return NDEFMessage._internal(tag["id"], tag["type"], records); 62 | }); 63 | } 64 | 65 | static void _startReadingNDEF( 66 | bool once, String alertMessage, NFCReaderMode readerMode) { 67 | // Start reading 68 | Map arguments = { 69 | "scan_once": once, 70 | "alert_message": alertMessage, 71 | "reader_mode": readerMode.name, 72 | }..addAll(readerMode._options); 73 | _channel.invokeMethod("startNDEFReading", arguments); 74 | } 75 | 76 | /// readNDEF starts listening for NDEF formatted tags. Any non-NDEF formatted 77 | /// tags will be filtered out. 78 | static Stream readNDEF({ 79 | /// once will stop reading after the first tag has been read. 80 | bool once = false, 81 | 82 | /// throwOnUserCancel decides if a [NFCUserCanceledSessionException] error 83 | /// should be thrown on iOS when the user clicks Cancel/Done. 84 | bool throwOnUserCancel = true, 85 | 86 | /// alertMessage sets the message on the iOS NFC modal. 87 | String alertMessage = "", 88 | 89 | /// readerMode specifies which mode the reader should use. By default it 90 | /// will use the normal mode, which scans for tags normally without 91 | /// support for peer-to-peer operations, such as emulated host cards. 92 | /// 93 | /// This is ignored on iOS as it only has one reading mode. 94 | NFCReaderMode readerMode = const NFCNormalReaderMode(), 95 | }) { 96 | _tagStream ??= _createTagStream(); 97 | // Create a StreamController to wrap the tag stream. Any errors will be 98 | // converted to their matching exception classes. The controller stream will 99 | // be closed if the errors are fatal. 100 | StreamController controller = StreamController(); 101 | final stream = once ? _tagStream!.take(1) : _tagStream!; 102 | // Listen for tag reads. 103 | final subscription = stream.listen( 104 | (message) => controller.add(message), 105 | onError: (error) { 106 | error = _mapException(error); 107 | if (!throwOnUserCancel && error is NFCUserCanceledSessionException) { 108 | return; 109 | } 110 | controller.addError(error); 111 | controller.close(); 112 | }, 113 | onDone: () async { 114 | _tagStream = null; 115 | await controller.close(); 116 | }, 117 | // cancelOnError: false 118 | // cancelOnError cannot be used as the stream would cancel BEFORE the error 119 | // was sent to the controller stream 120 | ); 121 | controller.onCancel = () { 122 | subscription.cancel(); 123 | }; 124 | 125 | try { 126 | _startReadingNDEF( 127 | once, 128 | alertMessage, 129 | const NFCNormalReaderMode(), 130 | ); 131 | } on PlatformException catch (err) { 132 | if (err.code == "NFCMultipleReaderModes") { 133 | throw NFCMultipleReaderModesException(); 134 | } else if (err.code == "SystemIsBusyError") { 135 | throw NFCSystemIsBusyException(err.message); 136 | } 137 | throw err; 138 | } catch (error) { 139 | throw error; 140 | } 141 | 142 | return controller.stream; 143 | } 144 | 145 | /// writeNDEF will write [newMessage] to all NDEF compatible tags scanned while 146 | /// the stream is active. 147 | /// If you only want to write to the first tag, you can set the [once] 148 | /// argument to `true` and use the `.first` method on the returned `Stream`. 149 | static Stream writeNDEF( 150 | NDEFMessage newMessage, { 151 | 152 | /// once will stop reading after the first tag has been read. 153 | bool once = false, 154 | 155 | /// message specify the message shown to the user when the NFC modal is 156 | /// open 157 | /// 158 | /// This is ignored on Android as it does not have NFC modal 159 | String message = "", 160 | 161 | /// readerMode specifies which mode the reader should use. 162 | NFCReaderMode readerMode = const NFCNormalReaderMode(), 163 | }) { 164 | _tagStream ??= _createTagStream(); 165 | 166 | StreamController controller = StreamController(); 167 | 168 | int writes = 0; 169 | final stream = _tagStream!.listen( 170 | (msg) async { 171 | NDEFMessage message = msg; 172 | if (message.tag.writable) { 173 | try { 174 | await message.tag.write(newMessage); 175 | } catch (err) { 176 | controller.addError(err); 177 | controller.close(); 178 | return; 179 | } 180 | writes++; 181 | controller.add(message.tag); 182 | } 183 | 184 | if (once && writes > 0) { 185 | controller.close(); 186 | } 187 | }, 188 | onError: (error) { 189 | error = _mapException(error); 190 | controller.addError(error); 191 | controller.close(); 192 | }, 193 | onDone: () async { 194 | _tagStream = null; 195 | await controller.close(); 196 | }, 197 | // cancelOnError: false 198 | // cancelOnError cannot be used as the stream would cancel BEFORE the error 199 | // was sent to the controller stream 200 | ); 201 | controller.onCancel = () { 202 | stream.cancel(); 203 | }; 204 | 205 | try { 206 | _startReadingNDEF(once, message, readerMode); 207 | } on PlatformException catch (err) { 208 | if (err.code == "NFCMultipleReaderModes") { 209 | throw NFCMultipleReaderModesException(); 210 | } 211 | throw err; 212 | } 213 | 214 | return controller.stream; 215 | } 216 | 217 | /// isNDEFSupported checks if the device supports reading NDEF tags 218 | static Future get isNDEFSupported async { 219 | final supported = await _channel.invokeMethod("readNDEFSupported"); 220 | assert(supported is bool); 221 | return supported as bool; 222 | } 223 | } 224 | 225 | /// NFCReaderMode is an interface for different reading modes 226 | // The reading modes are implemented as classes instead of enums, so they could 227 | // support options in the future without breaking changes. 228 | abstract class NFCReaderMode { 229 | String get name; 230 | 231 | Map get _options; 232 | } 233 | 234 | /// NFCNormalReaderMode uses the platform's normal reading mode. This does not 235 | /// allow reading from emulated host cards. 236 | class NFCNormalReaderMode implements NFCReaderMode { 237 | String get name => "normal"; 238 | 239 | /// noSounds tells the platform not to play any sounds when a tag has been 240 | /// read. 241 | /// Android only 242 | final bool noSounds; 243 | 244 | const NFCNormalReaderMode({ 245 | this.noSounds = false, 246 | }); 247 | 248 | @override 249 | Map get _options { 250 | return { 251 | "no_platform_sounds": noSounds, 252 | }; 253 | } 254 | } 255 | 256 | /// NFCDispatchReaderMode uses the Android NFC Foreground Dispatch API to read 257 | /// tags with. 258 | class NFCDispatchReaderMode implements NFCReaderMode { 259 | String get name => "dispatch"; 260 | 261 | @override 262 | Map get _options { 263 | return {}; 264 | } 265 | } 266 | 267 | enum MessageType { 268 | NDEF, 269 | } 270 | 271 | abstract class NFCMessage { 272 | MessageType get messageType; 273 | String? get id; 274 | 275 | NFCTag get tag; 276 | } 277 | 278 | abstract class NFCTag { 279 | String? get id; 280 | bool get writable; 281 | } 282 | 283 | class NDEFMessage implements NFCMessage { 284 | final String? id; 285 | final String? type; 286 | final List records; 287 | 288 | NDEFMessage.withRecords(this.records, {this.id}) : type = null; 289 | 290 | NDEFMessage(this.type, this.records) : id = null; 291 | 292 | NDEFMessage._internal(this.id, this.type, this.records); 293 | 294 | // payload returns the payload of the first non-empty record. If all records 295 | // are empty it will return null. 296 | String? get payload { 297 | for (var record in records) { 298 | if (record.payload != "") { 299 | return record.payload; 300 | } 301 | } 302 | return null; 303 | } 304 | 305 | bool get isEmpty { 306 | if (records.length == 0) { 307 | return true; 308 | } 309 | if (records.length == 1 && records[0].tnf == NFCTypeNameFormat.empty) { 310 | return true; 311 | } 312 | return false; 313 | } 314 | 315 | // data returns the contents of the first non-empty record. If all records 316 | // are empty it will return null. 317 | String? get data { 318 | for (var record in records) { 319 | if (record.data != "") { 320 | return record.data; 321 | } 322 | } 323 | return null; 324 | } 325 | 326 | @override 327 | MessageType get messageType => MessageType.NDEF; 328 | 329 | @override 330 | NDEFTag get tag { 331 | return NDEFTag._internal(id, true); 332 | } 333 | 334 | Map _toMap() { 335 | return { 336 | "id": id, 337 | "type": type, 338 | "records": records.map((record) => record._toMap()).toList(), 339 | }; 340 | } 341 | } 342 | 343 | enum NFCTypeNameFormat { 344 | empty, 345 | well_known, 346 | mime_media, 347 | absolute_uri, 348 | external, 349 | unknown, 350 | unchanged, 351 | } 352 | 353 | class NDEFRecord { 354 | final String? id; 355 | final String payload; 356 | final String type; 357 | final String data; 358 | final NFCTypeNameFormat tnf; 359 | 360 | /// languageCode will be the language code of a well known text record. If the 361 | /// record is not created with the well known TNF and Text RTD, this will be 362 | /// null. 363 | final String? languageCode; 364 | 365 | /// rawPayload contains the raw payload provided by the reader. 366 | /// It will only be set when reading NDEF tags. Otherwise it will be null. 367 | final Uint8List? rawPayload; 368 | 369 | NDEFRecord.empty() 370 | : id = null, 371 | type = "", 372 | payload = "", 373 | data = "", 374 | tnf = NFCTypeNameFormat.empty, 375 | languageCode = null, 376 | rawPayload = null; 377 | 378 | NDEFRecord.plain(String data) 379 | : id = null, 380 | type = "text/plain", 381 | payload = data, 382 | this.data = data, 383 | tnf = NFCTypeNameFormat.mime_media, 384 | languageCode = null, 385 | rawPayload = null; 386 | 387 | NDEFRecord.type(this.type, String payload) 388 | : id = null, 389 | this.payload = payload, 390 | data = payload, 391 | tnf = NFCTypeNameFormat.mime_media, 392 | languageCode = null, 393 | rawPayload = null; 394 | 395 | NDEFRecord.text(String message, {languageCode = "en"}) 396 | : id = null, 397 | data = message, 398 | payload = message, 399 | type = "T", 400 | tnf = NFCTypeNameFormat.well_known, 401 | this.languageCode = languageCode, 402 | rawPayload = null; 403 | 404 | NDEFRecord.uri(Uri uri) 405 | : id = null, 406 | data = uri.toString(), 407 | payload = uri.toString(), 408 | type = "U", 409 | tnf = NFCTypeNameFormat.well_known, 410 | languageCode = null, 411 | rawPayload = null; 412 | 413 | NDEFRecord.absoluteUri(Uri uri) 414 | : id = null, 415 | data = uri.toString(), 416 | payload = uri.toString(), 417 | type = "", 418 | tnf = NFCTypeNameFormat.absolute_uri, 419 | languageCode = null, 420 | rawPayload = null; 421 | 422 | NDEFRecord.external(this.type, String payload) 423 | : id = null, 424 | data = payload, 425 | this.payload = payload, 426 | tnf = NFCTypeNameFormat.external, 427 | languageCode = null, 428 | rawPayload = null; 429 | 430 | NDEFRecord.custom({ 431 | this.id, 432 | this.payload = "", 433 | this.type = "", 434 | this.tnf = NFCTypeNameFormat.unknown, 435 | this.languageCode, 436 | }) : data = payload, 437 | rawPayload = null; 438 | 439 | NDEFRecord._internal( 440 | this.id, 441 | this.payload, 442 | this.type, 443 | this.tnf, 444 | this.data, 445 | this.languageCode, 446 | this.rawPayload, 447 | ); 448 | 449 | Map _toMap() { 450 | String tnf; 451 | switch (this.tnf) { 452 | case NFCTypeNameFormat.empty: 453 | tnf = "empty"; 454 | break; 455 | case NFCTypeNameFormat.well_known: 456 | tnf = "well_known"; 457 | break; 458 | case NFCTypeNameFormat.mime_media: 459 | tnf = "mime_media"; 460 | break; 461 | case NFCTypeNameFormat.absolute_uri: 462 | tnf = "absolute_uri"; 463 | break; 464 | case NFCTypeNameFormat.external: 465 | tnf = "external_type"; 466 | break; 467 | case NFCTypeNameFormat.unchanged: 468 | tnf = "unchanged"; 469 | break; 470 | default: 471 | tnf = "unknown"; 472 | } 473 | 474 | return { 475 | "id": id ?? "", 476 | "payload": payload, 477 | "type": type, 478 | "tnf": tnf, 479 | "languageCode": languageCode, 480 | }; 481 | } 482 | } 483 | 484 | class NDEFTag implements NFCTag { 485 | final String? id; 486 | final bool writable; 487 | 488 | NDEFTag._internal(this.id, this.writable); 489 | 490 | Future write(NDEFMessage message) async { 491 | if (!writable) { 492 | throw NFCTagUnwritableException(); 493 | } 494 | try { 495 | return NFC._channel.invokeMethod("writeNDEF", { 496 | // TODO: Is id ever used by the native layer? 497 | "id": id, 498 | "message": message._toMap(), 499 | }); 500 | } on PlatformException catch (e) { 501 | switch (e.code) { 502 | case "NFCUnexpectedError": 503 | final msg = 'nfc: unexpected error'; 504 | throw Exception(e.message != null ? "$msg: ${e.message}" : msg); 505 | case "IOError": 506 | throw NFCIOException(e.message); 507 | case "NFCTagUnavailable": 508 | throw NFCTagUnavailableException(); 509 | case "NDEFUnsupported": 510 | throw NDEFUnsupportedException(); 511 | case "NDEFBadFormatError": 512 | throw NDEFBadFormatException(e.message); 513 | case "NFCTagNotWritableError": 514 | throw NFCTagNotWritableException(); 515 | case "NFCTagSizeTooSmallError": 516 | throw NFCTagSizeTooSmallException(e.details["maxSize"] ?? 0); 517 | case "NFCUpdateTagError": 518 | throw NFCUpdateTagException(); 519 | default: 520 | throw e; 521 | } 522 | } catch (error) { 523 | throw error; 524 | } 525 | } 526 | } 527 | 528 | Exception _mapException(dynamic error) { 529 | if (error is PlatformException) { 530 | switch (error.code) { 531 | case "NDEFUnsupportedFeatureError": 532 | error = NDEFReadingUnsupportedException(); 533 | break; 534 | case "UserCanceledSessionError": 535 | error = NFCUserCanceledSessionException(); 536 | break; 537 | case "SessionTimeoutError": 538 | error = NFCSessionTimeoutException(); 539 | break; 540 | case "SessionTerminatedUnexpectedlyErorr": 541 | error = NFCSessionTerminatedUnexpectedlyException(error.message); 542 | break; 543 | case "SystemIsBusyError": 544 | error = NFCSystemIsBusyException(error.message); 545 | break; 546 | case "IOError": 547 | error = NFCIOException(error.message); 548 | break; 549 | case "NDEFBadFormatError": 550 | error = NDEFBadFormatException(error.message); 551 | break; 552 | } 553 | } 554 | return error; 555 | } 556 | -------------------------------------------------------------------------------- /lib/src/exceptions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | /// NDEFReadingUnsupportedException is thrown if reading NDEF tags are either 4 | /// not supported or not enabled on the device. 5 | class NDEFReadingUnsupportedException implements Exception { 6 | @override 7 | String toString() => "NDEF reading is not supported on this device"; 8 | } 9 | 10 | /// NFCMultipleReaderModesException is thrown when multiple reading streams 11 | /// are open, but they use different reading modes. Only 1 reading mode can 12 | /// be used at the same time. 13 | class NFCMultipleReaderModesException implements Exception { 14 | @override 15 | String toString() => 16 | "started reading with a different reader mode than the one already in use"; 17 | } 18 | 19 | /// NFCUserCanceledSessionException is thrown on iOS when the users cancels the 20 | /// reading session (Clicks OK/done). 21 | class NFCUserCanceledSessionException implements Exception { 22 | @override 23 | String toString() => "the user has cancelled the reading session"; 24 | } 25 | 26 | /// NFCSessionTimeoutException is thrown on iOS when the session has been active 27 | /// for 60 seconds. 28 | class NFCSessionTimeoutException implements Exception { 29 | @override 30 | String toString() => "the reading session timed out"; 31 | } 32 | 33 | /// NFCSessionTerminatedUnexpectedlyException is thrown on iOS when "The reader 34 | /// session terminated unexpectedly". 35 | class NFCSessionTerminatedUnexpectedlyException implements Exception { 36 | final String? message; 37 | 38 | NFCSessionTerminatedUnexpectedlyException(this.message); 39 | 40 | @override 41 | String toString() => message ?? 'NFCSessionTerminatedUnexpectedlyException'; 42 | } 43 | 44 | /// NFCSystemIsBusyException is thrown on iOS when "the reader session 45 | /// failed because the system is busy". 46 | class NFCSystemIsBusyException implements Exception { 47 | final String? message; 48 | 49 | NFCSystemIsBusyException(this.message); 50 | 51 | @override 52 | String toString() => message ?? 'NFCSystemIsBusyException'; 53 | } 54 | 55 | /// NFCIOException is an I/O exception. Will happen if a tag is lost while being 56 | /// read or a tag could not be connected to. NFCIOException is only thrown on 57 | /// Android. 58 | class NFCIOException extends IOException { 59 | final String? message; 60 | 61 | NFCIOException(this.message); 62 | 63 | @override 64 | String toString() => message ?? 'NFCIOException'; 65 | } 66 | 67 | /// NDEFBadFormatException is thrown when a tag is read as NDEF, but it is not 68 | /// properly formatted. 69 | class NDEFBadFormatException implements Exception { 70 | final String? message; 71 | 72 | NDEFBadFormatException(this.message); 73 | 74 | @override 75 | String toString() => message ?? 'NDEFBadFormatException'; 76 | } 77 | 78 | /// NFCTagNotWritableException is thrown when an unwritable tag is 'written to'. 79 | /// This could be because the reader does not support writing to NFC tags or 80 | /// simply the tag is read-only. 81 | class NFCTagUnwritableException implements Exception { 82 | final message = "tag is not writable"; 83 | 84 | @override 85 | String toString() => message; 86 | } 87 | 88 | /// NFCTagUnavailableException is thrown when the NFC tag being written to 89 | /// is no longer in reach. 90 | class NFCTagUnavailableException implements Exception { 91 | final message = "tag is no longer available"; 92 | 93 | @override 94 | String toString() => message; 95 | } 96 | 97 | /// NDEFUnsupportedException is thrown when a tag does not support NDEF, 98 | /// but is being written an NDEFMessage. 99 | class NDEFUnsupportedException implements Exception { 100 | final message = "tag does not support NDEF formatting"; 101 | 102 | @override 103 | String toString() => message; 104 | } 105 | 106 | /// NFCTagNotWritableException is thrown when a non-writable tag is being 107 | /// written to. 108 | class NFCTagNotWritableException implements Exception { 109 | static const message = "the tag does not support writing"; 110 | 111 | @override 112 | String toString() => message; 113 | } 114 | 115 | /// NFCTagSizeTooSmallException is thrown when a NDEF message larger than 116 | /// the tag's maximum size is being written to tag. 117 | class NFCTagSizeTooSmallException implements Exception { 118 | final int maxSize; 119 | 120 | const NFCTagSizeTooSmallException(this.maxSize); 121 | 122 | @override 123 | String toString() => 124 | "the new payload exceeds the tag's maximum payload size (maximum $maxSize bytes)"; 125 | } 126 | 127 | /// NFCUpdateTagException is thrown when the reader failed to update the tag. 128 | /// 129 | /// NFCUpdateTagException is only thrown on iOS and is mapped to the [NFCNdefReaderSessionErrorTagUpdateFailure](https://developer.apple.com/documentation/corenfc/nfcreadererror/nfcndefreadersessionerrortagupdatefailure?language=objc) error. 130 | class NFCUpdateTagException implements Exception { 131 | static const message = "failed to update the tag"; 132 | 133 | @override 134 | String toString() => message; 135 | } 136 | -------------------------------------------------------------------------------- /nfc_in_flutter.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.5.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | matcher: 64 | dependency: transitive 65 | description: 66 | name: matcher 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "0.12.10" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.3.0" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.8.0" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.dartlang.org" 94 | source: hosted 95 | version: "1.8.0" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.10.0" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "2.1.0" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.1.0" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.2.0" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "0.2.19" 131 | typed_data: 132 | dependency: transitive 133 | description: 134 | name: typed_data 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.3.0" 138 | vector_math: 139 | dependency: transitive 140 | description: 141 | name: vector_math 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "2.1.0" 145 | sdks: 146 | dart: ">=2.12.0 <3.0.0" 147 | flutter: ">=1.10.0" 148 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nfc_in_flutter 2 | description: Flutter plugin to read and write NFC tags on both Android and iOS. Currently it only supports reading NDEF formatted tags. 3 | version: 3.0.0 4 | homepage: https://github.com/semlette/nfc_in_flutter 5 | repository: https://github.com/semlette/nfc_in_flutter 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | flutter: ">=1.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | plugin: 21 | platforms: 22 | android: 23 | package: dev.semler.nfc_in_flutter 24 | pluginClass: NfcInFlutterPlugin 25 | ios: 26 | pluginClass: NfcInFlutterPlugin 27 | -------------------------------------------------------------------------------- /test/nfc_in_flutter_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | --------------------------------------------------------------------------------