From d68945e620c1a5a560dd3334baf4b9ac1aa0f04f Mon Sep 17 00:00:00 2001 From: Flutter <38366712+gizemececetin@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:16:49 +0200 Subject: [PATCH] NFC functionality added --- android/app/src/main/AndroidManifest.xml | 15 +- .../com/bdk/f/bdk_flutter_app/MainActivity.kt | 86 ++++++ .../f/bdk_flutter_app/MyHostApduService.kt | 255 ++++++++++++++++++ .../com/bdk/f/bdk_flutter_app/NfcReader.kt | 104 +++++++ .../com/bdk/f/bdk_flutter_app/Utils.java | 214 +++++++++++++++ android/app/src/main/res/values/strings.xml | 4 + android/app/src/main/res/xml/apduservice.xml | 10 + ios/Runner.xcodeproj/project.pbxproj | 42 +-- ios/Runner/AppDelegate.swift | 61 ++++- ios/Runner/Info.plist | 22 +- ios/Runner/NFCTagReader.swift | 72 +++++ .../Runner.entitlements} | 6 +- lib/core/flutter_hce.dart | 35 +++ lib/core/hce_method_channel.dart | 54 ++++ lib/core/hce_platform.dart | 43 +++ lib/managers/payjoin_manager.dart | 25 ++ lib/screens/home.dart | 82 +++++- pubspec.lock | 12 +- 18 files changed, 1089 insertions(+), 53 deletions(-) create mode 100644 android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MyHostApduService.kt create mode 100644 android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/NfcReader.kt create mode 100644 android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/Utils.java create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/xml/apduservice.xml create mode 100644 ios/Runner/NFCTagReader.swift rename ios/{Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings => Runner/Runner.entitlements} (65%) create mode 100644 lib/core/flutter_hce.dart create mode 100644 lib/core/hce_method_channel.dart create mode 100644 lib/core/hce_platform.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4d6450a..bc13e1f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ - + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MainActivity.kt b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MainActivity.kt index 2544ed4..f736752 100644 --- a/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/bdk/f/bdk_flutter_app/MainActivity.kt @@ -1,6 +1,92 @@ package com.bdk.f.bdk_flutter_app +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import android.nfc.NfcAdapter +import io.flutter.Log import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.util.Calendar +import java.util.Timer +import java.util.TimerTask + class MainActivity : FlutterActivity() { + private val CHANNEL = "flutter_hce" + private var nfcAdapter: NfcAdapter? = null + private var pendingIntent: PendingIntent? = null + private val TAG = "MainActivity" + private lateinit var nfcReader: NfcReader + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + nfcReader = NfcReader(this, methodChannel) // Pass context and channel + + methodChannel.setMethodCallHandler { call, result -> + when (call.method) { + "sendNfcMessage" -> { + val dataToSend = call.argument("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: