diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 6e396fa7..92ea6c8e 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ AE4984882A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BDKSwiftExampleWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AE49848C2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletTests.swift; sourceTree = ""; }; AE4984A52A1BBCB8009951E2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + AE6474732CE559E000A270C6 /* BDKSwiftExampleWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BDKSwiftExampleWallet.entitlements; sourceTree = ""; }; AE6715F92A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletPriceServiceTests.swift; sourceTree = ""; }; AE6715FC2A9AC056005C193F /* PriceServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceServiceError.swift; sourceTree = ""; }; AE6715FE2A9AC066005C193F /* FeeServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeServiceError.swift; sourceTree = ""; }; @@ -387,6 +388,7 @@ AE49847A2A1BBBD6009951E2 /* BDKSwiftExampleWallet */ = { isa = PBXGroup; children = ( + AE6474732CE559E000A270C6 /* BDKSwiftExampleWallet.entitlements */, AE11D5EB2B784B2900D67366 /* Info.plist */, AE1C34222A424440008F807A /* App */, AE7F670A2A7451B600CED561 /* Model */, @@ -854,6 +856,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; @@ -862,6 +865,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "BDK Wallet"; + INFOPLIST_KEY_NFCReaderUsageDescription = "We need NFC access to verify addresses."; INFOPLIST_KEY_NSCameraUsageDescription = "\"To scan QR codes\""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -887,6 +891,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; @@ -895,6 +900,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "BDK Wallet"; + INFOPLIST_KEY_NFCReaderUsageDescription = "We need NFC access to verify addresses."; INFOPLIST_KEY_NSCameraUsageDescription = "\"To scan QR codes\""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements b/BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements new file mode 100644 index 00000000..2bb4dee1 --- /dev/null +++ b/BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.nfc.readersession.formats + + TAG + + + diff --git a/BDKSwiftExampleWallet/Info.plist b/BDKSwiftExampleWallet/Info.plist index 0e664efa..990bd2ef 100644 --- a/BDKSwiftExampleWallet/Info.plist +++ b/BDKSwiftExampleWallet/Info.plist @@ -5,8 +5,6 @@ ITSAppUsesNonExemptEncryption NSPasteboardUsageDescription - "To allow users to copy and paste text between the app and other apps" - NSCameraUsageDescription - "To scan QR codes" + "To allow users to copy and paste text between the app and other apps" diff --git a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift index f7354e42..46669a36 100644 --- a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift @@ -6,13 +6,16 @@ // import BitcoinDevKit +import CoreNFC import Foundation import Observation @Observable -class ReceiveViewModel { +class ReceiveViewModel: NSObject, NFCNDEFReaderSessionDelegate { let bdkClient: BDKClient + private var nfcSession: NFCNDEFReaderSession? + var address: String = "" var receiveViewError: AppError? var showingReceiveViewErrorAlert = false @@ -35,3 +38,83 @@ class ReceiveViewModel { } } + +extension ReceiveViewModel { + func startNFCSession() { + guard NFCNDEFReaderSession.readingAvailable else { + receiveViewError = .generic(message: "NFC not available on this device") + showingReceiveViewErrorAlert = true + return + } + + nfcSession = NFCNDEFReaderSession( + delegate: self, + queue: nil, + invalidateAfterFirstRead: false + ) + nfcSession?.alertMessage = "Hold your iPhone near the Coldcard to verify the address" + nfcSession?.begin() + } + + // MARK: - NFCNDEFReaderSessionDelegate + + func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { + guard let tag = tags.first else { + return + } + + session.connect(to: tag) { error in + if let error = error { + session.invalidate(errorMessage: "Connection error: \(error.localizedDescription)") + return + } + + let payload = NFCNDEFPayload( + format: .media, + type: "text/plain".data(using: .utf8)!, + identifier: Data(), + payload: self.address.data(using: .utf8)! + ) + + let message = NFCNDEFMessage(records: [payload]) + + tag.writeNDEF(message) { error in + if let error = error { + session.invalidate(errorMessage: "Write failed: \(error.localizedDescription)") + } else { + session.alertMessage = "Address passed to Coldcard successfully" + session.invalidate() + } + } + + } + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + if let nfcError = error as? NFCReaderError, + nfcError.code == .readerSessionInvalidationErrorUserCanceled + { + return + } + + DispatchQueue.main.async { + self.receiveViewError = .generic(message: error.localizedDescription) + self.showingReceiveViewErrorAlert = true + } + } + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { + // Required delegate method, but no action needed when session becomes active + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + if let message = messages.first, + let record = message.records.first, + let payload = String(data: record.payload, encoding: .utf8) + { + // Handle response + } + session.invalidate() + } + +} diff --git a/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift b/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift index 0cecaf52..86fd3537 100644 --- a/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift +++ b/BDKSwiftExampleWallet/View/Receive/ReceiveView.swift @@ -6,6 +6,7 @@ // import BitcoinUI +import CoreNFC import SwiftUI struct ReceiveView: View { @@ -51,6 +52,13 @@ struct ReceiveView: View { ) .padding() + Button { + viewModel.startNFCSession() + } label: { + Image(systemName: "wave.3.right") + .foregroundColor(.primary) + } + HStack { Button { UIPasteboard.general.string = viewModel.address