diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 16a72c7a..6e396fa7 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -34,8 +34,8 @@ AE2B8C1D2A9678C900815B2F /* FeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2B8C1C2A9678C900815B2F /* FeeService.swift */; }; AE2B8C1F2A96797300815B2F /* RecommendedFees.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */; }; AE2F255D2BED0BFB002A9AC6 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2F255C2BED0BFB002A9AC6 /* AppError.swift */; }; - AE34DDAC2B6B31ED00F04AD4 /* SeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */; }; - AE34DDAE2B6B320F00F04AD4 /* SeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */; }; + AE34DDAC2B6B31ED00F04AD4 /* WalletRecoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */; }; + AE34DDAE2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */; }; AE3646262BEDB01200B04E25 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3646252BEDB01200B04E25 /* FileManager+Extensions.swift */; }; AE3902A42A3B4CD900BEC318 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3902A32A3B4CD900BEC318 /* HomeView.swift */; }; AE49847C2A1BBBD6009951E2 /* BDKSwiftExampleWalletApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE49847B2A1BBBD6009951E2 /* BDKSwiftExampleWalletApp.swift */; }; @@ -126,8 +126,8 @@ AE2B8C1C2A9678C900815B2F /* FeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeService.swift; sourceTree = ""; }; AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedFees.swift; sourceTree = ""; }; AE2F255C2BED0BFB002A9AC6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; - AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedView.swift; sourceTree = ""; }; - AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedViewModel.swift; sourceTree = ""; }; + AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRecoveryView.swift; sourceTree = ""; }; + AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRecoveryViewModel.swift; sourceTree = ""; }; AE3646252BEDB01200B04E25 /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; AE3902A32A3B4CD900BEC318 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; AE4984782A1BBBD6009951E2 /* BDKSwiftExampleWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BDKSwiftExampleWallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -303,7 +303,7 @@ isa = PBXGroup; children = ( AE2ADD732B61E8F500C2A823 /* SettingsView.swift */, - AE34DDAB2B6B31ED00F04AD4 /* SeedView.swift */, + AE34DDAB2B6B31ED00F04AD4 /* WalletRecoveryView.swift */, ); path = Settings; sourceTree = ""; @@ -359,7 +359,7 @@ isa = PBXGroup; children = ( AE2ADD772B61EFFE00C2A823 /* SettingsViewModel.swift */, - AE34DDAD2B6B320F00F04AD4 /* SeedViewModel.swift */, + AE34DDAD2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift */, ); path = Settings; sourceTree = ""; @@ -665,7 +665,7 @@ AEB130C92A44E4850087785B /* TransactionDetailView.swift in Sources */, AE287E772C0F6D200036A748 /* Array+Extensions.swift in Sources */, AE6715FD2A9AC056005C193F /* PriceServiceError.swift in Sources */, - AE34DDAC2B6B31ED00F04AD4 /* SeedView.swift in Sources */, + AE34DDAC2B6B31ED00F04AD4 /* WalletRecoveryView.swift in Sources */, AE2ADD742B61E8F500C2A823 /* SettingsView.swift in Sources */, AE2381AF2C605B1D00F6B00C /* ActivityListViewModel.swift in Sources */, AE6F34D82AA6C1800087E700 /* Network+Extensions.swift in Sources */, @@ -689,7 +689,7 @@ AE2381B32C60877600F6B00C /* LocalOutputListView.swift in Sources */, AE783A052AB4F51F005F0CBA /* String+Extensions.swift in Sources */, AE29ED112BBE318A00EB9C4F /* TransactionItemView.swift in Sources */, - AE34DDAE2B6B320F00F04AD4 /* SeedViewModel.swift in Sources */, + AE34DDAE2B6B320F00F04AD4 /* WalletRecoveryViewModel.swift in Sources */, AE0C30F92A804B65008F1EAE /* OnboardingViewModel.swift in Sources */, AE3902A42A3B4CD900BEC318 /* HomeView.swift in Sources */, AE0C30FD2A804BC1008F1EAE /* ReceiveViewModel.swift in Sources */, diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index ee4207f3..7765d1f2 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -243,6 +243,9 @@ } } } + }, + "Backup is not synced across devices." : { + }, "BDK Wallet" : { "extractionState" : "stale", @@ -320,6 +323,7 @@ } }, "Delete Seed" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -328,6 +332,9 @@ } } } + }, + "Delete Wallet" : { + }, "Descriptors" : { @@ -586,9 +593,6 @@ }, "Seed" : { - }, - "Seed is not synced across devices." : { - }, "Select Bitcoin Network" : { @@ -640,6 +644,7 @@ } }, "Show Seed" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -648,6 +653,9 @@ } } } + }, + "Show Wallet" : { + }, "Showing Seed Error" : { "localizations" : { diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 29fb2fde..a32d5b37 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -148,6 +148,99 @@ private class BDKService { self.wallet = wallet } + func createWallet(descriptor: String?) throws { + let documentsDirectoryURL = URL.documentsDirectory + let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") + + if FileManager.default.fileExists(atPath: walletDataDirectoryURL.path) { + try FileManager.default.removeItem(at: walletDataDirectoryURL) + } else { + } + + let baseUrl = + try keyClient.getEsploraURL() ?? Constants.Config.EsploraServerURLNetwork.Signet.mutiny + + guard let descriptorString = descriptor, !descriptorString.isEmpty else { + throw WalletError.walletNotFound + } + + let cleanDescriptor = + descriptorString.split(separator: "#").first.map(String.init) ?? descriptorString + let descriptor = try Descriptor(descriptor: cleanDescriptor, network: network) + let changeDescriptorString = cleanDescriptor.replacingOccurrences(of: "/0/*", with: "/1/*") + let changeDescriptor = try Descriptor(descriptor: changeDescriptorString, network: network) + + let backupInfo = BackupInfo( + mnemonic: "", + descriptor: descriptor.toStringWithSecret(), + changeDescriptor: changeDescriptor.toStringWithSecret() + ) + + try keyClient.saveBackupInfo(backupInfo) + try keyClient.saveNetwork(self.network.description) + try keyClient.saveEsploraURL(baseUrl) + + try FileManager.default.ensureDirectoryExists(at: walletDataDirectoryURL) + try FileManager.default.removeOldFlatFileIfNeeded(at: documentsDirectoryURL) + let persistenceBackendPath = walletDataDirectoryURL.appendingPathComponent("wallet.sqlite") + .path + let connection = try Connection(path: persistenceBackendPath) + self.connection = connection + let wallet = try Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: network, + connection: connection + ) + self.wallet = wallet + } + + func createWallet(xpub: String?) throws { + let documentsDirectoryURL = URL.documentsDirectory + let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") + + if FileManager.default.fileExists(atPath: walletDataDirectoryURL.path) { + try FileManager.default.removeItem(at: walletDataDirectoryURL) + } else { + } + + let baseUrl = + try keyClient.getEsploraURL() ?? Constants.Config.EsploraServerURLNetwork.Signet.mutiny + + guard let xpubString = xpub, !xpubString.isEmpty else { + throw WalletError.walletNotFound + } + + let descriptorString = "tr(\(xpubString)/0/*)" + let changeDescriptorString = "tr(\(xpubString)/1/*)" + let descriptor = try Descriptor(descriptor: descriptorString, network: network) + let changeDescriptor = try Descriptor(descriptor: changeDescriptorString, network: network) + + let backupInfo = BackupInfo( + mnemonic: "", + descriptor: descriptor.toStringWithSecret(), + changeDescriptor: changeDescriptor.toStringWithSecret() + ) + + try keyClient.saveBackupInfo(backupInfo) + try keyClient.saveNetwork(self.network.description) + try keyClient.saveEsploraURL(baseUrl) + + try FileManager.default.ensureDirectoryExists(at: walletDataDirectoryURL) + try FileManager.default.removeOldFlatFileIfNeeded(at: documentsDirectoryURL) + let persistenceBackendPath = walletDataDirectoryURL.appendingPathComponent("wallet.sqlite") + .path + let connection = try Connection(path: persistenceBackendPath) + self.connection = connection + let wallet = try Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: network, + connection: connection + ) + self.wallet = wallet + } + private func loadWallet(descriptor: Descriptor, changeDescriptor: Descriptor) throws { let documentsDirectoryURL = URL.documentsDirectory let walletDataDirectoryURL = documentsDirectoryURL.appendingPathComponent("wallet_data") @@ -313,7 +406,9 @@ extension BDKService { struct BDKClient { let loadWallet: () throws -> Void let deleteWallet: () throws -> Void - let createWallet: (String?) throws -> Void + let createWalletFromSeed: (String?) throws -> Void + let createWalletFromDescriptor: (String?) throws -> Void + let createWalletFromXPub: (String?) throws -> Void let getBalance: () throws -> Balance let transactions: () throws -> [CanonicalTx] let listUnspent: () throws -> [LocalOutput] @@ -338,7 +433,13 @@ extension BDKClient { static let live = Self( loadWallet: { try BDKService.shared.loadWalletFromBackup() }, deleteWallet: { try BDKService.shared.deleteWallet() }, - createWallet: { words in try BDKService.shared.createWallet(words: words) }, + createWalletFromSeed: { words in try BDKService.shared.createWallet(words: words) }, + createWalletFromDescriptor: { descriptor in + try BDKService.shared.createWallet(descriptor: descriptor) + }, + createWalletFromXPub: { xpub in + try BDKService.shared.createWallet(xpub: xpub) + }, getBalance: { try BDKService.shared.getBalance() }, transactions: { try BDKService.shared.transactions() }, listUnspent: { try BDKService.shared.listUnspent() }, @@ -387,7 +488,9 @@ extension BDKClient { static let mock = Self( loadWallet: {}, deleteWallet: {}, - createWallet: { _ in }, + createWalletFromSeed: { _ in }, + createWalletFromDescriptor: { _ in }, + createWalletFromXPub: { _ in }, getBalance: { .mock }, transactions: { return [ diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 56b7a706..012b2e18 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -17,6 +17,13 @@ class OnboardingViewModel: ObservableObject { @AppStorage("isOnboarding") var isOnboarding: Bool? @Published var createWithPersistError: CreateWithPersistError? + var isDescriptor: Bool { + words.hasPrefix("tr(") || words.hasPrefix("wpkh(") || words.hasPrefix("wsh(") + || words.hasPrefix("sh(") + } + var isXPub: Bool { + words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") + } @Published var networkColor = Color.gray @Published var onboardingViewError: AppError? @Published var selectedNetwork: Network = .signet { @@ -31,12 +38,14 @@ class OnboardingViewModel: ObservableObject { bdkClient.updateEsploraURL(selectedURL) } } - @Published var words: String = "" { - didSet { - updateWordArray() + @Published var words: String = "" + var wordArray: [String] { + if words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") { + return [] } + let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedWords.components(separatedBy: " ") } - @Published var wordArray: [String] = [] var availableURLs: [String] { switch selectedNetwork { case .bitcoin: @@ -72,7 +81,13 @@ class OnboardingViewModel: ObservableObject { func createWallet() { do { - try bdkClient.createWallet(words) + if isDescriptor { + try bdkClient.createWalletFromDescriptor(words) + } else if isXPub { + try bdkClient.createWalletFromXPub(words) + } else { + try bdkClient.createWalletFromSeed(words) + } DispatchQueue.main.async { self.isOnboarding = false } @@ -86,9 +101,4 @@ class OnboardingViewModel: ObservableObject { } } } - - private func updateWordArray() { - let trimmedWords = words.trimmingCharacters(in: .whitespacesAndNewlines) - wordArray = trimmedWords.split(separator: " ").map { String($0) } - } } diff --git a/BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift b/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift similarity index 68% rename from BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift rename to BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift index baa5523a..0d613d2b 100644 --- a/BDKSwiftExampleWallet/View Model/Settings/SeedViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift @@ -1,5 +1,5 @@ // -// SeedViewModel.swift +// WalletRecoveryViewModel.swift // BDKSwiftExampleWallet // // Created by Matthew Ramsden on 1/31/24. @@ -11,25 +11,25 @@ import SwiftUI @Observable @MainActor -class SeedViewModel { +class WalletRecoveryViewModel { let bdkClient: BDKClient var backupInfo: BackupInfo? var publicDescriptor: Descriptor? var publicChangeDescriptor: Descriptor? - var seedViewError: AppError? - var showingSeedViewErrorAlert: Bool + var walletRecoveryViewError: AppError? + var showingWalletRecoveryViewErrorAlert: Bool init( bdkClient: BDKClient = .live, backupInfo: BackupInfo? = nil, - seedViewError: AppError? = nil, - showingSeedViewErrorAlert: Bool = false + walletRecoveryViewError: AppError? = nil, + showingWalletRecoveryViewErrorAlert: Bool = false ) { self.bdkClient = bdkClient self.backupInfo = backupInfo - self.seedViewError = seedViewError - self.showingSeedViewErrorAlert = showingSeedViewErrorAlert + self.walletRecoveryViewError = walletRecoveryViewError + self.showingWalletRecoveryViewErrorAlert = showingWalletRecoveryViewErrorAlert } func getNetwork() -> Network { @@ -55,8 +55,8 @@ class SeedViewModel { self.backupInfo = backupInfo } catch { - self.seedViewError = .generic(message: error.localizedDescription) - self.showingSeedViewErrorAlert = true + self.walletRecoveryViewError = .generic(message: error.localizedDescription) + self.showingWalletRecoveryViewErrorAlert = true } } diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index db1431bb..9409413b 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -13,9 +13,14 @@ import Observation @Observable class WalletViewModel { let bdkClient: BDKClient + let keyClient: KeyClient let priceClient: PriceClient var balanceTotal: UInt64 = 0 + var canSend: Bool { + guard let backupInfo = try? keyClient.getBackupInfo() else { return false } + return backupInfo.descriptor.contains("tprv") || backupInfo.descriptor.contains("xprv") + } var inspectedScripts: UInt64 = 0 var price: Double = 0.00 var progress: Float = 0.0 @@ -35,11 +40,13 @@ class WalletViewModel { init( bdkClient: BDKClient = .live, + keyClient: KeyClient = .live, priceClient: PriceClient = .live, transactions: [CanonicalTx] = [], walletSyncState: WalletSyncState = .notStarted ) { self.bdkClient = bdkClient + self.keyClient = keyClient self.priceClient = priceClient self.transactions = transactions self.walletSyncState = walletSyncState diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 09965740..8c9bf618 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -14,6 +14,7 @@ struct OnboardingView: View { @ObservedObject var viewModel: OnboardingViewModel @State private var showingOnboardingViewErrorAlert = false @State private var showingImportView = false + @State private var showingScanner = false let pasteboard = UIPasteboard.general var body: some View { @@ -25,27 +26,42 @@ struct OnboardingView: View { VStack { HStack { + Spacer() - Button { - if viewModel.wordArray.isEmpty { + + if viewModel.words.isEmpty { + Button { + showingScanner = true + } label: { + Image(systemName: "qrcode.viewfinder") + .transition(.symbolEffect(.disappear)) + } + .tint(.secondary) + .font(.title) + .padding() + + Button { if let clipboardContent = UIPasteboard.general.string { viewModel.words = clipboardContent } - } else { + } label: { + Image(systemName: "arrow.down.square") + .contentTransition(.symbolEffect(.replace)) + } + .tint(.secondary) + .font(.title) + .padding() + } else { + Button { viewModel.words = "" + } label: { + Image(systemName: "clear") + .contentTransition(.symbolEffect(.replace)) } - } label: { - Image( - systemName: viewModel.wordArray.isEmpty - ? "arrow.down.square" : "clear" - ) - .contentTransition(.symbolEffect(.replace)) + .tint(.primary) + .font(.title) + .padding() } - .tint( - viewModel.wordArray.isEmpty ? .secondary : .primary - ) - .font(.title) - .padding() } Spacer() @@ -108,15 +124,23 @@ struct OnboardingView: View { .pickerStyle(.automatic) .tint(.primary) - if viewModel.wordArray != [] { - SeedPhraseView( - words: viewModel.wordArray, - preferredWordsPerRow: 2, - usePaging: true, - wordsPerPage: 4 - ) - .frame(height: 200) - .padding() + if !viewModel.words.isEmpty { + if viewModel.isDescriptor || viewModel.isXPub { + Text(viewModel.words) + .font(.system(.caption, design: .monospaced)) + .lineLimit(1) + .truncationMode(.middle) + .padding() + } else { + SeedPhraseView( + words: viewModel.wordArray, + preferredWordsPerRow: 2, + usePaging: true, + wordsPerPage: 4 + ) + .frame(height: 200) + .padding() + } } Spacer() @@ -145,6 +169,24 @@ struct OnboardingView: View { } ) } + .sheet(isPresented: $showingScanner) { + CustomScannerView( + codeTypes: [.qr], + completion: { result in + switch result { + case .success(let result): + viewModel.words = result.string + showingScanner = false + case .failure(let error): + viewModel.onboardingViewError = .generic( + message: error.localizedDescription + ) + showingScanner = false + } + }, + pasteAction: {} + ) + } } } diff --git a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift index 5fb8b45b..8cb6c2bd 100644 --- a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift @@ -73,7 +73,7 @@ struct SettingsView: View { Button { showingShowSeedConfirmation = true } label: { - Text(String(localized: "Show Seed")) + Text(String(localized: "Show Wallet")) .foregroundStyle(.red) } } @@ -86,7 +86,7 @@ struct SettingsView: View { showingDeleteSeedConfirmation = true } label: { HStack { - Text(String(localized: "Delete Seed")) + Text(String(localized: "Delete Wallet")) .foregroundStyle(.red) } } @@ -107,7 +107,7 @@ struct SettingsView: View { } .sheet(isPresented: $isSeedPresented) { - SeedView(viewModel: .init()) + WalletRecoveryView(viewModel: .init()) .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } diff --git a/BDKSwiftExampleWallet/View/Settings/SeedView.swift b/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift similarity index 52% rename from BDKSwiftExampleWallet/View/Settings/SeedView.swift rename to BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift index 6c3e6efa..08e648ad 100644 --- a/BDKSwiftExampleWallet/View/Settings/SeedView.swift +++ b/BDKSwiftExampleWallet/View/Settings/WalletRecoveryView.swift @@ -1,5 +1,5 @@ // -// SeedView.swift +// WalletRecoveryView.swift // BDKSwiftExampleWallet // // Created by Matthew Ramsden on 1/31/24. @@ -8,8 +8,8 @@ import BitcoinUI import SwiftUI -struct SeedView: View { - @Bindable var viewModel: SeedViewModel +struct WalletRecoveryView: View { + @Bindable var viewModel: WalletRecoveryViewModel @State private var isCopied = false @State private var showCheckmark = false @@ -24,16 +24,23 @@ struct SeedView: View { let publicDescriptor = viewModel.publicDescriptor, let publicChangeDescriptor = viewModel.publicChangeDescriptor { - - SeedPhraseView( - words: backupInfo.mnemonic.components(separatedBy: " "), - preferredWordsPerRow: 2, - usePaging: true, - wordsPerPage: 4 - ) + if backupInfo.mnemonic.isEmpty { + Text(backupInfo.descriptor) + .font(.system(.caption, design: .monospaced)) + .lineLimit(1) + .truncationMode(.middle) + .padding() + } else { + SeedPhraseView( + words: backupInfo.mnemonic.components(separatedBy: " "), + preferredWordsPerRow: 2, + usePaging: true, + wordsPerPage: 4 + ) + } VStack { - Text("Seed is not synced across devices.") + Text("Backup is not synced across devices.") Text("Please make sure to write it down and store it securely.") } .font(.caption) @@ -41,37 +48,39 @@ struct SeedView: View { .multilineTextAlignment(.center) .padding() - HStack { - Spacer() - Button { - UIPasteboard.general.string = backupInfo.mnemonic - isCopied = true - showCheckmark = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { - isCopied = false - showCheckmark = false + if !backupInfo.mnemonic.isEmpty { + HStack { + Spacer() + Button { + UIPasteboard.general.string = backupInfo.mnemonic + isCopied = true + showCheckmark = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + isCopied = false + showCheckmark = false + } + } label: { + HStack { + Image( + systemName: showCheckmark + ? "document.on.document.fill" : "document.on.document" + ) + .contentTransition(.symbolEffect(.replace)) + Text("Seed") + .bold() + } } - } label: { - HStack { - Image( - systemName: showCheckmark - ? "document.on.document.fill" : "document.on.document" + .buttonStyle( + BitcoinFilled( + width: 120, + height: 40, + tintColor: .primary, + textColor: Color(uiColor: .systemBackground), + isCapsule: true ) - .contentTransition(.symbolEffect(.replace)) - Text("Seed") - .bold() - } - } - .buttonStyle( - BitcoinFilled( - width: 120, - height: 40, - tintColor: .primary, - textColor: Color(uiColor: .systemBackground), - isCapsule: true ) - ) - Spacer() + Spacer() + } } HStack { @@ -119,12 +128,12 @@ struct SeedView: View { viewModel.getBackupInfo(network: network) } } - .alert(isPresented: $viewModel.showingSeedViewErrorAlert) { + .alert(isPresented: $viewModel.showingWalletRecoveryViewErrorAlert) { Alert( title: Text("Showing Seed Error"), - message: Text(viewModel.seedViewError?.description ?? ""), + message: Text(viewModel.walletRecoveryViewError?.description ?? ""), dismissButton: .default(Text("OK")) { - viewModel.seedViewError = nil + viewModel.walletRecoveryViewError = nil } ) } @@ -134,6 +143,6 @@ struct SeedView: View { #if DEBUG #Preview { - SeedView(viewModel: .init(bdkClient: .mock)) + WalletRecoveryView(viewModel: .init(bdkClient: .mock)) } #endif diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index b9c62370..6ed2be54 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -191,8 +191,9 @@ struct WalletView: View { NavigationLink(value: NavigationDestination.address) { Image(systemName: "qrcode.viewfinder") .font(.title) - .foregroundStyle(.primary) + .foregroundStyle(viewModel.canSend ? .primary : .secondary) } + .disabled(!viewModel.canSend) } .padding([.horizontal, .bottom])