("content")
+ dataToSend?.let { sendNFCMessage(it) }
+ result.success(null)
+ }
+ "readNfcMessage" -> {
+ nfcReader.startNfcReading()
+ result.success(null)
+ }
+ "isNfcEnabled" -> {
+ if (isNfcEnabled()) {
+ result.success("true")
+ } else {
+ result.success("false")
+ }
+ }
+ "isNfcHceSupported" -> {
+ if (isNfcHceSupported()) {
+ result.success("true")
+ } else {
+ result.success("false")
+ }
+ }
+ "stopNfcHce" -> {
+ stopNfcHce()
+ result.success("success")
+ }
+ else -> result.notImplemented()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ }
+
+ private fun sendNFCMessage(text: String) {
+ Log.d(TAG, "message to send: $text")
+ val intent = Intent(context, MyHostApduService::class.java)
+ intent.putExtra("ndefMessage", text);
+ context.startService(intent)
+
+ }
+
+ private fun isNfcEnabled(): Boolean {
+ return nfcAdapter?.isEnabled == true
+ }
+ private fun isNfcHceSupported() =
+ isNfcEnabled() && activity?.packageManager!!.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
+
+ private fun stopNfcHce() {
+ val intent = Intent(activity, MyHostApduService::class.java)
+ // activity?.stopService(intent)
+ }
}
diff --git a/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MyHostApduService.kt b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MyHostApduService.kt
new file mode 100644
index 0000000..c9d42a9
--- /dev/null
+++ b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MyHostApduService.kt
@@ -0,0 +1,255 @@
+package com.bdk.f.bdk_flutter_app
+
+import android.content.Intent
+import android.nfc.NdefMessage
+import android.nfc.NdefRecord
+import android.nfc.cardemulation.HostApduService
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import java.util.Arrays
+
+/**
+ * This class emulates a NFC Forum Tag Type 4 containing a NDEF message
+ * The class uses the AID D2760000850101
+ */
+class MyHostApduService : HostApduService() {
+ private lateinit var mNdefRecordFile: ByteArray
+
+ override fun onCreate() {
+ super.onCreate()
+ mAppSelected = false
+ mCcSelected = false
+ mNdefSelected = false
+ createDefaultMessage()
+ }
+
+ private fun createDefaultMessage() {
+ val ndefDefaultMessage = createNdefMessage(DEFAULT_MESSAGE,"text/plain", NDEF_ID)
+ // the maximum length is 246
+ val ndefLen = ndefDefaultMessage!!.byteArrayLength
+ mNdefRecordFile = ByteArray(ndefLen + 2)
+ mNdefRecordFile[0] = ((ndefLen and 0xff00) / 256).toByte()
+ mNdefRecordFile[1] = (ndefLen and 0xff).toByte()
+ System.arraycopy(
+ ndefDefaultMessage.toByteArray(),
+ 0,
+ mNdefRecordFile,
+ 2,
+ ndefDefaultMessage.byteArrayLength
+ )
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ if (intent != null) {
+ // intent contains a text message
+ if (intent.hasExtra("ndefMessage")) {
+ val ndef = intent.getStringExtra("ndefMessage")
+ // val ndefMessage = createNdefMessage(intent.getStringExtra("ndefMessage"))
+ if (ndef != null) {
+ val ndefMessage = createNdefMessage(ndef, "text/plain", NDEF_ID)
+
+ val ndefLen = ndefMessage.byteArrayLength
+ mNdefRecordFile = ByteArray(ndefLen + 2)
+ mNdefRecordFile[0] = ((ndefLen and 0xff00) / 256).toByte()
+ mNdefRecordFile[1] = (ndefLen and 0xff).toByte()
+ System.arraycopy(
+ ndefMessage.toByteArray(),
+ 0,
+ mNdefRecordFile,
+ 2,
+ ndefMessage.byteArrayLength
+ )
+ }
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+/* private fun createNdefMessage(ndefData: String?): NdefMessage? {
+ if (ndefData!!.isEmpty()) {
+ return null
+ }
+ var ndefRecord: NdefRecord? = null
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ ndefRecord = NdefRecord.createTextRecord("en", ndefData)
+ }
+ return NdefMessage(ndefRecord)
+ } */
+
+ private fun createNdefMessage(content: String, mimeType: String, id: ByteArray): NdefMessage {
+ Log.i(TAG, "createNdefMessage(): $content")
+
+ if(mimeType == "text/plain") {
+ return createTextRecord("en", content, id);
+ }
+
+ val type = mimeType.toByteArray(charset("US-ASCII"))
+ val payload = content.toByteArray(charset("UTF-8"))
+
+ return NdefMessage(NdefRecord(NdefRecord.TNF_MIME_MEDIA, type, id, payload))
+ }
+
+ private fun createTextRecord(language: String, text: String, id: ByteArray): NdefMessage {
+ val languageBytes: ByteArray
+ val textBytes: ByteArray
+ try {
+ languageBytes = language.toByteArray(charset("US-ASCII"))
+ textBytes = text.toByteArray(charset("UTF-8"))
+ } catch (e: Error) {
+ throw AssertionError(e)
+ }
+
+ val recordPayload = ByteArray(1 + (languageBytes.size and 0x03F) + textBytes.size)
+
+ recordPayload[0] = (languageBytes.size and 0x03F).toByte()
+ System.arraycopy(languageBytes, 0, recordPayload, 1, languageBytes.size and 0x03F)
+ System.arraycopy(
+ textBytes,
+ 0,
+ recordPayload,
+ 1 + (languageBytes.size and 0x03F),
+ textBytes.size,
+ )
+
+ return NdefMessage(NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, id, recordPayload))
+ }
+
+ /**
+ * emulates an NFC Forum Tag Type 4
+ */
+ override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
+ if (extras == null) {
+ Log.d(TAG, "Received null extras in processCommandApdu")
+ // Handle the case where extras is null if necessary
+ }
+ Log.d(TAG, "commandApdu: " + Utils.bytesToHex(commandApdu))
+ //if (Arrays.equals(SELECT_APP, commandApdu)) {
+ // check if commandApdu qualifies for SELECT_APPLICATION
+ if (SELECT_APPLICATION.contentEquals(commandApdu)) {
+ mAppSelected = true
+ mCcSelected = false
+ mNdefSelected = false
+ Log.d(TAG, "responseApdu: " + Utils.bytesToHex(SUCCESS_SW))
+ return SUCCESS_SW
+ // check if commandApdu qualifies for SELECT_CAPABILITY_CONTAINER
+ } else if (mAppSelected && SELECT_CAPABILITY_CONTAINER.contentEquals(commandApdu)) {
+ mCcSelected = true
+ mNdefSelected = false
+ Log.d(TAG, "responseApdu: " + Utils.bytesToHex(SUCCESS_SW))
+ return SUCCESS_SW
+ // check if commandApdu qualifies for SELECT_NDEF_FILE
+ } else if (mAppSelected && SELECT_NDEF_FILE.contentEquals(commandApdu)) {
+ // NDEF
+ mCcSelected = false
+ mNdefSelected = true
+ Log.d(TAG, "responseApdu: " + Utils.bytesToHex(SUCCESS_SW))
+ return SUCCESS_SW
+ // check if commandApdu qualifies for // READ_BINARY
+ } else if (commandApdu[0] == 0x00.toByte() && commandApdu[1] == 0xb0.toByte()) {
+ // READ_BINARY
+ // get the offset an le (length) data
+ //System.out.println("** " + Utils.bytesToHex(commandApdu) + " in else if (commandApdu[0] == (byte)0x00 && commandApdu[1] == (byte)0xb0) {");
+ val offset =
+ (0x00ff and commandApdu[2].toInt()) * 256 + (0x00ff and commandApdu[3].toInt())
+ val le = 0x00ff and commandApdu[4].toInt()
+ val responseApdu = ByteArray(le + SUCCESS_SW.size)
+ if (mCcSelected && offset == 0 && le == CAPABILITY_CONTAINER_FILE.size) {
+ System.arraycopy(CAPABILITY_CONTAINER_FILE, offset, responseApdu, 0, le)
+ System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.size)
+ Log.d(TAG, "responseApdu: " + Utils.bytesToHex(responseApdu))
+ return responseApdu
+ } else if (mNdefSelected) {
+ if (offset + le <= mNdefRecordFile.size) {
+ System.arraycopy(mNdefRecordFile, offset, responseApdu, 0, le)
+ System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.size)
+ Log.d(TAG, "responseApdu: " + Utils.bytesToHex(responseApdu))
+ return responseApdu
+ }
+ }
+ }
+
+ // The tag should return different errors for different reasons
+ // this emulation just returns the general error message
+ Log.d(TAG, "responseApdu: " + Utils.bytesToHex(FAILURE_SW))
+ return FAILURE_SW
+ }
+
+ /**
+ * onDeactivated is called when reading ends
+ * reset the status boolean values
+ */
+ override fun onDeactivated(reason: Int) {
+ mAppSelected = false
+ mCcSelected = false
+ mNdefSelected = false
+ Log.i(TAG, "onDeactivated() Reason: $reason")
+
+ }
+
+ companion object {
+ private const val TAG = "MyHostApduService"
+ const val DEFAULT_MESSAGE = "This is the default message."
+ private var mAppSelected = false // true when SELECT_APPLICATION detected
+ private var mCcSelected = false // true when SELECT_CAPABILITY_CONTAINER detected
+ private var mNdefSelected = false // true when SELECT_NDEF_FILE detected
+ private val NDEF_ID = byteArrayOf(0xE1.toByte(), 0x04.toByte())
+
+ private val SELECT_APPLICATION = byteArrayOf(
+ 0x00.toByte(),
+ 0xA4.toByte(),
+ 0x04.toByte(),
+ 0x00.toByte(),
+ 0x07.toByte(),
+ 0xD2.toByte(),
+ 0x76.toByte(),
+ 0x00.toByte(),
+ 0x00.toByte(),
+ 0x85.toByte(),
+ 0x01.toByte(),
+ 0x01.toByte(),
+ 0x00.toByte()
+ )
+ private val SELECT_CAPABILITY_CONTAINER = byteArrayOf(
+ 0x00.toByte(),
+ 0xa4.toByte(),
+ 0x00.toByte(),
+ 0x0c.toByte(),
+ 0x02.toByte(),
+ 0xe1.toByte(),
+ 0x03.toByte()
+ )
+ private val SELECT_NDEF_FILE = byteArrayOf(
+ 0x00.toByte(),
+ 0xa4.toByte(),
+ 0x00.toByte(),
+ 0x0c.toByte(),
+ 0x02.toByte(),
+ 0xE1.toByte(),
+ 0x04.toByte()
+ )
+ private val CAPABILITY_CONTAINER_FILE = byteArrayOf(
+ 0x00,
+ 0x0f, // CCLEN
+ 0x20, // Mapping Version
+ 0x00,
+ 0x3b, // Maximum R-APDU data size
+ 0x00,
+ 0x34, // Maximum C-APDU data size
+ 0x04,
+ 0x06,
+ 0xe1.toByte(),
+ 0x04,
+ 0x00.toByte(),
+ 0xff.toByte(), // Maximum NDEF size, do NOT extend this value
+ 0x00,
+ 0xff.toByte()
+ )
+
+ // Status Word success
+ private val SUCCESS_SW = byteArrayOf(0x90.toByte(), 0x00.toByte())
+
+ // Status Word failure
+ private val FAILURE_SW = byteArrayOf(0x6a.toByte(), 0x82.toByte())
+ }
+}
diff --git a/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/NfcReader.kt b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/NfcReader.kt
new file mode 100644
index 0000000..c5e8bf2
--- /dev/null
+++ b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/NfcReader.kt
@@ -0,0 +1,104 @@
+package com.bdk.f.bdk_flutter_app
+
+import android.content.Context
+import android.content.Intent
+import android.nfc.NdefRecord
+import android.nfc.NfcAdapter
+import android.nfc.Tag
+import android.nfc.tech.Ndef
+import android.os.Build
+import android.os.Bundle
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.provider.Settings
+import android.widget.Toast
+import io.flutter.embedding.android.FlutterActivity
+import java.util.Arrays
+import io.flutter.plugin.common.MethodChannel
+
+class NfcReader(private val context: Context, private val channel: MethodChannel) : NfcAdapter.ReaderCallback {
+
+ private var mNfcAdapter: NfcAdapter? = null
+
+ fun startNfcReading() {
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(context)
+ if (mNfcAdapter != null) {
+ if (!mNfcAdapter!!.isEnabled) {
+ showWirelessSettings()
+ return
+ }
+ val options = Bundle()
+ options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
+ mNfcAdapter!!.enableReaderMode(
+ context as FlutterActivity,
+ this,
+ NfcAdapter.FLAG_READER_NFC_A or
+ NfcAdapter.FLAG_READER_NFC_B or
+ NfcAdapter.FLAG_READER_NFC_F or
+ NfcAdapter.FLAG_READER_NFC_V or
+ NfcAdapter.FLAG_READER_NFC_BARCODE or
+ NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
+ options
+ )
+ }
+ }
+
+ override fun onTagDiscovered(tag: Tag?) {
+ println("NFC tag discovered")
+ val mNdef = Ndef.get(tag)
+ if (mNdef != null) {
+ val mNdefMessage = mNdef.cachedNdefMessage
+ val record = mNdefMessage.records
+ val ndefRecordsCount = record.size
+ if (ndefRecordsCount > 0) {
+ var ndefText = ""
+ for (i in 0 until ndefRecordsCount) {
+ val ndefTnf = record[i].tnf
+ val ndefType = record[i].type
+ val ndefPayload = record[i].payload
+ if (ndefTnf == NdefRecord.TNF_WELL_KNOWN &&
+ Arrays.equals(ndefType, NdefRecord.RTD_TEXT)
+ ) {
+ ndefText += "\nrec: $i Well known Text payload\n${String(ndefPayload)} \n"
+ ndefText += Utils.parseTextrecordPayload(ndefPayload) + " \n"
+ }
+ if (ndefTnf == NdefRecord.TNF_WELL_KNOWN &&
+ Arrays.equals(ndefType, NdefRecord.RTD_URI)
+ ) {
+ ndefText += "\nrec: $i Well known Uri payload\n${String(ndefPayload)} \n"
+ ndefText += Utils.parseUrirecordPayload(ndefPayload) + " \n"
+ }
+ if (ndefTnf == NdefRecord.TNF_MIME_MEDIA) {
+ ndefText += "\nrec: $i TNF Mime Media payload\n${String(ndefPayload)} \n"
+ ndefText += "TNF Mime Media type\n${String(ndefType)} \n"
+ }
+ if (ndefTnf == NdefRecord.TNF_EXTERNAL_TYPE) {
+ ndefText += "\nrec: $i TNF External type payload\n${String(ndefPayload)} \n"
+ ndefText += "TNF External type type\n${String(ndefType)} \n"
+ }
+ }
+ channel.invokeMethod("onNfcRead", ndefText)
+ } else {
+ channel.invokeMethod("onNfcReadError", "No NDEF records found")
+ }
+ } else {
+ channel.invokeMethod("onNfcReadError", "There was an error in NDEF data")
+ }
+ doVibrate()
+ }
+
+ private fun doVibrate() {
+ val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ vibrator.vibrate(VibrationEffect.createOneShot(150, 10))
+ } else {
+ vibrator.vibrate(200)
+ }
+ }
+
+ private fun showWirelessSettings() {
+ Toast.makeText(context, "You need to enable NFC", Toast.LENGTH_SHORT).show()
+ val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS)
+ context.startActivity(intent)
+ }
+}
diff --git a/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/Utils.java b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/Utils.java
new file mode 100644
index 0000000..dbac041
--- /dev/null
+++ b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/Utils.java
@@ -0,0 +1,214 @@
+package com.bdk.f.bdk_flutter_app;
+
+import android.os.Build;
+
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Date;
+
+public class Utils {
+
+ public static String getTimestamp() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return ZonedDateTime
+ .now(ZoneId.systemDefault())
+ .format(DateTimeFormatter.ofPattern("uuuu.MM.dd HH:mm:ss"));
+ } else {
+ return new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date());
+ }
+ }
+
+ public static String removeAllNonAlphaNumeric(String s) {
+ if (s == null) {
+ return null;
+ }
+ return s.replaceAll("[^A-Za-z0-9]", "");
+ }
+
+ // position is 0 based starting from right to left
+ public static byte setBitInByte(byte input, int pos) {
+ return (byte) (input | (1 << pos));
+ }
+
+ // position is 0 based starting from right to left
+ public static byte unsetBitInByte(byte input, int pos) {
+ return (byte) (input & ~(1 << pos));
+ }
+
+ // https://stackoverflow.com/a/29396837/8166854
+ public static boolean testBit(byte b, int n) {
+ int mask = 1 << n; // equivalent of 2 to the nth power
+ return (b & mask) != 0;
+ }
+
+ // https://stackoverflow.com/a/29396837/8166854
+ public static boolean testBit(byte[] array, int n) {
+ int index = n >>> 3; // divide by 8
+ int mask = 1 << (n & 7); // n modulo 8
+ return (array[index] & mask) != 0;
+ }
+
+ public static String bytesToHex(byte[] bytes) {
+ StringBuffer result = new StringBuffer();
+ for (byte b : bytes)
+ result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
+ return result.toString();
+ }
+
+ public static byte[] hexStringToByteArray(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i + 1), 16));
+ }
+ return data;
+ }
+
+ public static String getDec(byte[] bytes) {
+ long result = 0;
+ long factor = 1;
+ for (int i = 0; i < bytes.length; ++i) {
+ long value = bytes[i] & 0xffl;
+ result += value * factor;
+ factor *= 256l;
+ }
+ return result + "";
+ }
+
+ public static String printByteBinary(byte bytes){
+ byte[] data = new byte[1];
+ data[0] = bytes;
+ return printByteArrayBinary(data);
+ }
+
+ public static String printByteArrayBinary(byte[] bytes){
+ String output = "";
+ for (byte b1 : bytes){
+ String s1 = String.format("%8s", Integer.toBinaryString(b1 & 0xFF)).replace(' ', '0');
+ //s1 += " " + Integer.toHexString(b1);
+ //s1 += " " + b1;
+ output = output + " " + s1;
+ //System.out.println(s1);
+ }
+ return output;
+ }
+
+ public static byte[] convertIntToByteArray(int value, int numberOfBytes) {
+ byte b[] = new byte[numberOfBytes];
+ int i, shift;
+ for (i = 0, shift = (b.length - 1) * 8; i < b.length; i++, shift -= 8) {
+ b[i] = (byte) (0xFF & (value >> shift));
+ }
+ return b;
+ }
+
+ public static String parseTextrecordPayload(byte[] ndefPayload) {
+ int languageCodeLength = Array.getByte(ndefPayload, 0);
+ int ndefPayloadLength = ndefPayload.length;
+ byte[] languageCode = new byte[languageCodeLength];
+ System.arraycopy(ndefPayload, 1, languageCode, 0, languageCodeLength);
+ byte[] message = new byte[ndefPayloadLength - 1 - languageCodeLength];
+ System.arraycopy(ndefPayload, 1 + languageCodeLength, message, 0, ndefPayloadLength - 1 - languageCodeLength);
+ return new String(message, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * NFC Forum "URI Record Type Definition"
+ * This is a mapping of "URI Identifier Codes" to URI string prefixes,
+ * per section 3.2.2 of the NFC Forum URI Record Type Definition document.
+ */
+ // source: https://github.com/skjolber/ndef-tools-for-android
+ private static final String[] URI_PREFIX_MAP = new String[] {
+ "", // 0x00
+ "http://www.", // 0x01
+ "https://www.", // 0x02
+ "http://", // 0x03
+ "https://", // 0x04
+ "tel:", // 0x05
+ "mailto:", // 0x06
+ "ftp://anonymous:anonymous@", // 0x07
+ "ftp://ftp.", // 0x08
+ "ftps://", // 0x09
+ "sftp://", // 0x0A
+ "smb://", // 0x0B
+ "nfs://", // 0x0C
+ "ftp://", // 0x0D
+ "dav://", // 0x0E
+ "news:", // 0x0F
+ "telnet://", // 0x10
+ "imap:", // 0x11
+ "rtsp://", // 0x12
+ "urn:", // 0x13
+ "pop:", // 0x14
+ "sip:", // 0x15
+ "sips:", // 0x16
+ "tftp:", // 0x17
+ "btspp://", // 0x18
+ "btl2cap://", // 0x19
+ "btgoep://", // 0x1A
+ "tcpobex://", // 0x1B
+ "irdaobex://", // 0x1C
+ "file://", // 0x1D
+ "urn:epc:id:", // 0x1E
+ "urn:epc:tag:", // 0x1F
+ "urn:epc:pat:", // 0x20
+ "urn:epc:raw:", // 0x21
+ "urn:epc:", // 0x22
+ };
+
+ public static String parseUrirecordPayload(byte[] ndefPayload) {
+ int uriPrefix = Array.getByte(ndefPayload, 0);
+ int ndefPayloadLength = ndefPayload.length;
+ byte[] message = new byte[ndefPayloadLength - 1];
+ System.arraycopy(ndefPayload, 1, message, 0, ndefPayloadLength - 1);
+ return URI_PREFIX_MAP[uriPrefix] + new String(message, StandardCharsets.UTF_8);
+ }
+
+ private static final byte[] SW_9000 = {
+ (byte)0x90, // SW1 Status byte 1 - Command processing status
+ (byte)0x00 // SW2 Status byte 2 - Command processing qualifier
+ };
+
+ /**
+ * Method used to check if the last command return SW1SW2 == 9000
+ *
+ * @param pByte
+ * response to the last command
+ * @return true if the status is 9000 false otherwise
+ */
+ public static boolean isSucceed(final byte[] pByte) {
+ byte[] resultValue = Arrays.copyOfRange(pByte, pByte.length - 2, pByte.length);
+ if (Arrays.equals(resultValue, SW_9000)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Constant-time Byte Array Comparison
+ * Less overheard, safer. Originally from: http://codahale.com/a-lesson-in-timing-attacks/
+ *
+ * @param a yourByteArrayA
+ * @param b yourByteArrayB
+ * @return boolean
+ *
+ */
+ public static boolean isEqual(byte[] a, byte[] b) {
+ if (a.length != b.length) {
+ return false;
+ }
+
+ int result = 0;
+ for (int i = 0; i < a.length; i++) {
+ result |= a[i] ^ b[i];
+ }
+ return result == 0;
+ }
+}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8c3faf7
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ HCE
+ HCE Flutter
+
\ No newline at end of file
diff --git a/android/app/src/main/res/xml/apduservice.xml b/android/app/src/main/res/xml/apduservice.xml
new file mode 100644
index 0000000..8a95ca8
--- /dev/null
+++ b/android/app/src/main/res/xml/apduservice.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 06e272a..5894b91 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -14,21 +14,10 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ CEAFE91F2C3EA3D300B2EC06 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2064732C3C426600EE52F7 /* CoreNFC.framework */; };
+ CEAFE9212C3EA75500B2EC06 /* NFCTagReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAFE9202C3EA75500B2EC06 /* NFCTagReader.swift */; };
/* End PBXBuildFile section */
-/* Begin PBXCopyFilesBuildPhase section */
- 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
@@ -46,6 +35,9 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
AD55739057E1D09A587F7262 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ CE2064732C3C426600EE52F7 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; };
+ CEAFE9202C3EA75500B2EC06 /* NFCTagReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFCTagReader.swift; sourceTree = ""; };
+ CEE2A7022C3C40E40098DDE1 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
F539E9F640A6AD239A77B606 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -55,6 +47,7 @@
buildActionMask = 2147483647;
files = (
2C8F608299487C1D17D67A1C /* Pods_Runner.framework in Frameworks */,
+ CEAFE91F2C3EA3D300B2EC06 /* CoreNFC.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -64,6 +57,7 @@
6B38BAF749343EC9287F5E37 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ CE2064732C3C426600EE52F7 /* CoreNFC.framework */,
AD55739057E1D09A587F7262 /* Pods_Runner.framework */,
);
name = Frameworks;
@@ -102,6 +96,8 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ CEAFE9202C3EA75500B2EC06 /* NFCTagReader.swift */,
+ CEE2A7022C3C40E40098DDE1 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -136,7 +132,6 @@
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
0938BC12637F8961C1881E9A /* [CP] Embed Pods Frameworks */,
);
@@ -276,6 +271,7 @@
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ CEAFE9212C3EA75500B2EC06 /* NFCTagReader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -358,15 +354,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 6477GJYWXR;
+ DEVELOPMENT_TEAM = AU4YV3Y44A;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.bdk.f.bdkFlutterApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.iosTestNfcKit;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -489,16 +486,20 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 6477GJYWXR;
+ DEVELOPMENT_TEAM = AU4YV3Y44A;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.bdk.f.bdkFlutterApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.iosTestNfcKit;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -512,15 +513,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 6477GJYWXR;
+ DEVELOPMENT_TEAM = AU4YV3Y44A;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.bdk.f.bdkFlutterApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.iosTestNfcKit;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 70693e4..5d592b0 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,13 +1,60 @@
import UIKit
import Flutter
+import CoreNFC
+@available(iOS 13.0, *)
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
+ private let CHANNEL = "flutter_hce"
+ private var nfcReader: NFCTagReader?
+
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
+ let nfcChannel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
+ nfcChannel.setMethodCallHandler({
+ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
+ if call.method == "readNfcMessage" {
+ self.readNFCData(result: result)
+ }else if call.method == "sendNfcMessage" {
+ if let args = call.arguments as? [String: Any],
+ let text = args["content"] as? String {
+ self.sendNfcMessage(text: text, result: result)
+ } else {
+ result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid argument for sendNfcMessage", details: nil))
+ }
+ } else {
+ result(FlutterMethodNotImplemented)
+ }
+ })
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ func readNFCData(result: @escaping FlutterResult) {
+ nfcReader = NFCTagReader()
+ nfcReader?.onNFCResult = { success, data in
+ if success {
+ result(data)
+ } else {
+ result(FlutterError(code: "NFC_READ_ERROR", message: "NFC read error", details: nil))
+ }
+ }
+ nfcReader?.beginReadSession()
+ }
+
+ func sendNfcMessage(text: String, result: @escaping FlutterResult) {
+ let payload = NFCNDEFPayload(format: .nfcWellKnown, type: Data("T".utf8), identifier: Data(), payload: Data(text.utf8))
+ let message = NFCNDEFMessage(records: [payload])
+ nfcReader = NFCTagReader()
+ nfcReader?.onNFCResult = { success, data in
+ if success {
+ result(data)
+ } else {
+ result(FlutterError(code: "NFC_WRITE_ERROR", message: data, details: nil))
+ }
+ }
+ nfcReader?.beginWriteSession(withMessage: message)
+ }
}
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 0090632..81c00ab 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,6 +2,8 @@
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
@@ -24,10 +26,18 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ NFCReaderUsageDescription
+ Read NFC tag
+ UIApplicationSupportsIndirectInputEvents
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
+ UIRequiredDeviceCapabilities
+
+ nfc
+
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
@@ -43,9 +53,13 @@
UIViewControllerBasedStatusBarAppearance
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
+ com.apple.developer.nfc.readersession.felica.systemcodes
+
+ 8008
+
+ com.apple.developer.nfc.readersession.iso7816.select-identifiers
+
+ A00000000386980701
+
diff --git a/ios/Runner/NFCTagReader.swift b/ios/Runner/NFCTagReader.swift
new file mode 100644
index 0000000..537e272
--- /dev/null
+++ b/ios/Runner/NFCTagReader.swift
@@ -0,0 +1,72 @@
+import CoreNFC
+
+@available(iOS 13.0, *)
+class NFCTagReader: NSObject, NFCNDEFReaderSessionDelegate {
+ var nfcSession: NFCNDEFReaderSession?
+ var onNFCResult: ((Bool, String) -> Void)?
+ var writeMessage: NFCNDEFMessage?
+
+ func beginReadSession() {
+ nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
+ nfcSession?.begin()
+ }
+ func beginWriteSession(withMessage message: NFCNDEFMessage) {
+ writeMessage = message
+ nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
+ nfcSession?.alertMessage = "Hold your iPhone near the NFC tag to write the message."
+ nfcSession?.begin()
+ }
+ func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
+ if let message = messages.first {
+ let payload = message.records.first
+ let payloadString = String(data: payload!.payload, encoding: .utf8) ?? ""
+ onNFCResult?(true, payloadString)
+ }
+ }
+
+ func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
+ onNFCResult?(false, error.localizedDescription)
+ }
+ func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
+ guard let tag = tags.first, let message = writeMessage else {
+ session.invalidate(errorMessage: "No tags found or no message to write.")
+ return
+ }
+
+ session.connect(to: tag) { error in
+ if let error = error {
+ self.onNFCResult?(false, error.localizedDescription)
+ session.invalidate(errorMessage: "Connection failed.")
+ return
+ }
+
+ tag.queryNDEFStatus { status, capacity, error in
+ if let error = error {
+ self.onNFCResult?(false, error.localizedDescription)
+ session.invalidate(errorMessage: "NDEF status query failed.")
+ return
+ }
+
+ guard status == .readWrite else {
+ self.onNFCResult?(false, "Tag is not writable.")
+ session.invalidate(errorMessage: "Tag is not writable.")
+ return
+ }
+
+ tag.writeNDEF(message) { error in
+ if let error = error {
+ self.onNFCResult?(false, error.localizedDescription)
+ session.invalidate(errorMessage: "Write failed.")
+ } else {
+ self.onNFCResult?(true, "Message written successfully.")
+ session.alertMessage = "Message written successfully."
+ session.invalidate()
+ }
+ }
+ }
+ }
+ }
+
+
+
+}
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner/Runner.entitlements
similarity index 65%
rename from ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
rename to ios/Runner/Runner.entitlements
index f9b0d7c..2bb4dee 100644
--- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ b/ios/Runner/Runner.entitlements
@@ -2,7 +2,9 @@
- PreviewsEnabled
-
+ com.apple.developer.nfc.readersession.formats
+
+ TAG
+
diff --git a/lib/core/flutter_hce.dart b/lib/core/flutter_hce.dart
new file mode 100644
index 0000000..cd75e49
--- /dev/null
+++ b/lib/core/flutter_hce.dart
@@ -0,0 +1,35 @@
+import 'package:bdk_flutter_demo/core/hce_platform.dart';
+
+class FlutterHce {
+ final HcePlatform _platform = HcePlatform.instance;
+
+ /// Retrieves the platform version from the native platform.
+ Future getPlatformVersion() => _platform.getPlatformVersion();
+
+ /// Sends the NFC host card emulation with the specified content.
+ /// [content]: The content to transmit via NFC.
+ Future sendNfcMessage(String content) {
+ return _platform.sendNfcMessage(content);
+ }
+ Future readNfcMessage() {
+ return _platform.readNfcMessage();
+ }
+
+ /// Stops the NFC host card emulation and deletes the saved text file with the NFC message from internal storage.
+ Future stopNfcHce() => _platform.stopNfcHce();
+
+ /// Checks if NFC HCE is supported by the platform.
+ Future isNfcHceSupported() async {
+ return await _platform.isNfcHceSupported() == 'true';
+ }
+
+ /// Checks if secure NFC functionality is enabled. This function always returns false for SDKs below Android 10 (API level 29).
+ Future isSecureNfcEnabled() async {
+ return await _platform.isSecureNfcEnabled() == 'true';
+ }
+
+ /// Determines whether NFC is enabled on the device.
+ Future isNfcEnabled() async {
+ return await _platform.isNfcEnabled() == 'true';
+ }
+}
diff --git a/lib/core/hce_method_channel.dart b/lib/core/hce_method_channel.dart
new file mode 100644
index 0000000..9308a8a
--- /dev/null
+++ b/lib/core/hce_method_channel.dart
@@ -0,0 +1,54 @@
+import 'package:bdk_flutter_demo/core/hce_platform.dart';
+import 'package:flutter/services.dart';
+
+/// An implementation of [HcePlatform] that uses method channels to communicate with the native platform.
+class HceMethodChannel extends HcePlatform {
+ /// The method channel used to interact with the native platform.
+ static const MethodChannel _methodChannel = MethodChannel('flutter_hce');
+
+ /// Retrieves the platform version from the native side.
+ @override
+ Future getPlatformVersion() async {
+ return await _methodChannel.invokeMethod('getPlatformVersion');
+ }
+
+ /// Sends an NFC HCE session with the given parameters.
+ @override
+ Future sendNfcMessage(String content) async {
+ return await _methodChannel.invokeMethod(
+ 'sendNfcMessage',
+ {"content": content},
+ );
+ }
+
+ @override
+ Future readNfcMessage() async {
+
+ String? result= await _methodChannel.invokeMethod('readNfcMessage');
+ return result;
+ }
+
+ /// Stops the ongoing NFC HCE session.
+ @override
+ Future stopNfcHce() async {
+ return await _methodChannel.invokeMethod('stopNfcHce');
+ }
+
+ /// Checks if NFC HCE is supported by the platform.
+ @override
+ Future isNfcHceSupported() async {
+ return await _methodChannel.invokeMethod('isNfcHceSupported');
+ }
+
+ /// Checks if Secure NFC is enabled on the device.
+ @override
+ Future isSecureNfcEnabled() async {
+ return await _methodChannel.invokeMethod('isSecureNfcEnabled');
+ }
+
+ /// Checks if NFC is enabled on the device.
+ @override
+ Future isNfcEnabled() async {
+ return await _methodChannel.invokeMethod('isNfcEnabled');
+ }
+}
diff --git a/lib/core/hce_platform.dart b/lib/core/hce_platform.dart
new file mode 100644
index 0000000..82a0cf2
--- /dev/null
+++ b/lib/core/hce_platform.dart
@@ -0,0 +1,43 @@
+
+import 'package:bdk_flutter_demo/core/hce_method_channel.dart';
+
+abstract class HcePlatform {
+ /// Constructs a HcePlatform.
+ HcePlatform() : super();
+
+ static HcePlatform instance = HceMethodChannel();
+
+ /// Gets the platform version.
+ Future getPlatformVersion() {
+ throw UnimplementedError('getPlatformVersion() has not been implemented.');
+ }
+
+ /// Sends NFC HCE (Host Card Emulation) with the given content.
+ Future sendNfcMessage(String content) {
+ throw UnimplementedError('startNfcHce() has not been implemented.');
+ }
+
+ Future readNfcMessage() {
+ throw UnimplementedError('readNfcMessage() has not been implemented.');
+ }
+
+ /// Stops the NFC HCE session.
+ Future stopNfcHce() {
+ throw UnimplementedError('stopNfcHce() has not been implemented.');
+ }
+
+ /// Checks if NFC HCE is supported on the platform.
+ Future isNfcHceSupported() {
+ throw UnimplementedError('isNfcHceSupported() has not been implemented.');
+ }
+
+ /// Checks if Secure NFC is enabled.
+ Future isSecureNfcEnabled() {
+ throw UnimplementedError('isSecureNfcEnabled() has not been implemented.');
+ }
+
+ /// Checks if NFC is enabled.
+ Future isNfcEnabled() {
+ throw UnimplementedError('isNfcEnabled() has not been implemented.');
+ }
+}
diff --git a/lib/managers/payjoin_manager.dart b/lib/managers/payjoin_manager.dart
index 6ed22e4..dc1232c 100644
--- a/lib/managers/payjoin_manager.dart
+++ b/lib/managers/payjoin_manager.dart
@@ -3,6 +3,8 @@ import 'dart:ffi';
import 'dart:typed_data';
import 'package:bdk_flutter/bdk_flutter.dart';
+import 'package:bdk_flutter_demo/core/flutter_hce.dart';
+import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:payjoin_flutter/receive/v1.dart' as v1;
import 'package:payjoin_flutter/receive/v1.dart';
@@ -197,4 +199,27 @@ class PayjoinManager {
var transaction = await psbt.extractTx();
return transaction;
}
+
+ Future sendNFCMessage(nfcMessage) async {
+ final flutterHce = FlutterHce();
+ try {
+ await flutterHce.sendNfcMessage(nfcMessage);
+ } on PlatformException catch (e) {
+ debugPrint("Err : $e");
+ }
+ }
+
+ Future readNfc() async {
+ const platform = MethodChannel('flutter_hce');
+
+ String nfcData;
+ try {
+ final String result = await platform.invokeMethod('readNfcMessage');
+ nfcData = 'NFC Data: $result';
+ } on PlatformException catch (e) {
+ nfcData = "Hata oluştu: '${e.message}'.";
+ }
+
+ return nfcData;
+ }
}
diff --git a/lib/screens/home.dart b/lib/screens/home.dart
index a74468a..77d5b0f 100644
--- a/lib/screens/home.dart
+++ b/lib/screens/home.dart
@@ -1,5 +1,4 @@
import 'dart:convert';
-import 'dart:io';
import 'package:bdk_flutter/bdk_flutter.dart';
import 'package:bdk_flutter_demo/managers/payjoin_manager.dart';
@@ -403,7 +402,22 @@ class _HomeState extends State {
maxLines: 5,
decoration: const InputDecoration(hintText: "Enter receiver psbt"),
),
- )
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text("Read with nfc"),
+ IconButton(
+ icon: const Icon(Icons.nfc),
+ color: Colors.deepPurple,
+ onPressed: () async {
+ final receiverPsbt = await payjoinManager.readNfc();
+ debugPrint('receiverPsbtController: $receiverPsbt');
+ receiverPsbtController.text = receiverPsbt;
+ },
+ ),
+ ],
+ ),
];
} else {
return [
@@ -422,6 +436,21 @@ class _HomeState extends State {
decoration: const InputDecoration(hintText: "Enter pjUri"),
),
),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text("Read with nfc"),
+ IconButton(
+ icon: const Icon(Icons.nfc),
+ color: Colors.deepPurple,
+ onPressed: () async {
+ final pjUriText = await payjoinManager.readNfc();
+ debugPrint('pjUriText: $pjUriText');
+ pjUriController.text = pjUriText;
+ },
+ ),
+ ],
+ ),
Center(
child: TextButton(
onPressed: () => chooseFeeRange(),
@@ -437,14 +466,33 @@ class _HomeState extends State {
Widget buildReceiverFields() {
return pjUri.isEmpty
? buildFields()
- : TextFieldContainer(
- child: TextFormField(
- controller: psbtController,
- style: Theme.of(context).textTheme.bodyLarge,
- keyboardType: TextInputType.multiline,
- maxLines: 5,
- decoration: const InputDecoration(hintText: "Enter psbt"),
- ),
+ : Column(
+ children: [
+ TextFieldContainer(
+ child: TextFormField(
+ controller: psbtController,
+ style: Theme.of(context).textTheme.bodyLarge,
+ keyboardType: TextInputType.multiline,
+ maxLines: 5,
+ decoration: const InputDecoration(hintText: "Enter psbt"),
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text("Read with nfc"),
+ IconButton(
+ icon: const Icon(Icons.nfc),
+ color: Colors.deepPurple,
+ onPressed: () async {
+ final pjUriText = await payjoinManager.readNfc();
+ debugPrint('pjUriText: $pjUriText');
+ psbtController.text = pjUriText;
+ },
+ ),
+ ],
+ ),
+ ],
);
}
@@ -461,14 +509,15 @@ class _HomeState extends State {
//Sender
Future performSender() async {
if (!isRequestSent) {
+ final pjUriText = pjUriController.text;
final (request, ctx) = await payjoinManager.buildPayjoinRequest(
wallet,
- await payjoinManager.stringToUri(pjUriController.text),
+ await payjoinManager.stringToUri(pjUriText),
feeRange?.feeValue ?? FeeRangeEnum.high.feeValue);
final originalPsbt = utf8.decode(request.body.toList());
debugPrint('Original Sender PSBT: $originalPsbt');
showBottomSheet(originalPsbt);
-
+ payjoinManager.sendNFCMessage(originalPsbt);
setState(() {
isRequestSent =
true; // Todo: this is not needed since we can check for contextV1 not being null
@@ -485,6 +534,8 @@ class _HomeState extends State {
await payjoinManager.extractPjTx(wallet, processedResponse);
final txId = await blockchain.broadcast(transaction: transaction);
print('TxId: $txId');
+ payjoinManager.sendNFCMessage(txId);
+
showBottomSheet(txId);
}
}
@@ -494,11 +545,15 @@ class _HomeState extends State {
if (pjUri.isEmpty) {
buildReceiverPjUri();
} else {
+ final sendercPsbt = psbtController.text;
+ debugPrint('Sender PSBT: $sendercPsbt');
+
final receiverPsbt =
- await payjoinManager.handlePjRequest(psbtController.text, wallet);
+ await payjoinManager.handlePjRequest(sendercPsbt, wallet);
if (receiverPsbt == null) {
return throw Exception("Response is null");
}
+ payjoinManager.sendNFCMessage(receiverPsbt);
showBottomSheet(receiverPsbt);
}
@@ -509,6 +564,7 @@ class _HomeState extends State {
double.parse(amountController.text),
recipientAddress.text,
);
+ payjoinManager.sendNFCMessage(pjStr);
setState(() {
displayText = pjStr;
diff --git a/pubspec.lock b/pubspec.lock
index 79f830a..3830152 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -224,18 +224,18 @@ packages:
dependency: transitive
description:
name: js
- sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
+ sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
- version: "0.6.5"
+ version: "0.7.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
- sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
- version: "4.8.0"
+ version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -272,10 +272,10 @@ packages:
dependency: transitive
description:
name: logging
- sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
+ sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
- version: "1.1.1"
+ version: "1.2.0"
matcher:
dependency: transitive
description: