diff --git a/BDKSwiftExampleWallet/Model/BalanceDisplayFormat.swift b/BDKSwiftExampleWallet/Model/BalanceDisplayFormat.swift index 1e7aa069..56b8493a 100644 --- a/BDKSwiftExampleWallet/Model/BalanceDisplayFormat.swift +++ b/BDKSwiftExampleWallet/Model/BalanceDisplayFormat.swift @@ -15,6 +15,14 @@ enum BalanceDisplayFormat: String, CaseIterable, Codable { case fiat = "usd" case bip177 = "bip177" + var displayPrefix: String { + switch self { + case .bitcoinSats, .bitcoin, .bip177: return "₿" + case .fiat: return "$" + default : return "" + } + } + var displayText: String { switch self { case .sats, .bitcoinSats: return "sats" @@ -23,6 +31,22 @@ enum BalanceDisplayFormat: String, CaseIterable, Codable { case .fiat: return "USD" } } + + func formatted(_ btcAmount: UInt64, fiatPrice: Double) -> String { + switch self { + case .sats: + return btcAmount.formatted(.number) + case .bitcoin: + return String(format: "%.8f", Double(btcAmount) / 100_000_000) + case .bitcoinSats: + return btcAmount.formattedSatoshis() + case .fiat: + let satsPrice = Double(btcAmount).valueInUSD(price: fiatPrice) + return satsPrice.formatted(.number.precision(.fractionLength(2))) + case .bip177: + return btcAmount.formattedBip177() + } + } } extension BalanceDisplayFormat { diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index d601fe7b..d8c69c10 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -204,12 +204,12 @@ } } }, - "%@%lld sats" : { + "%@%@ %@" : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$@%2$lld sats" + "value" : "%1$@%2$@ %3$@" } } } @@ -288,6 +288,7 @@ } }, "%llu sats" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { diff --git a/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift b/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift index 6a6c7195..d47807fa 100644 --- a/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift @@ -22,6 +22,7 @@ class ActivityListViewModel { var totalScripts: UInt64 = 0 var walletSyncState: WalletSyncState var walletViewError: AppError? + var fiatPrice: Double private var updateProgress: @Sendable (UInt64, UInt64) -> Void { { [weak self] inspected, total in @@ -41,11 +42,13 @@ class ActivityListViewModel { init( bdkClient: BDKClient = .live, transactions: [CanonicalTx] = [], - walletSyncState: WalletSyncState = .notStarted + walletSyncState: WalletSyncState = .notStarted, + fiatPrice: Double ) { self.bdkClient = bdkClient self.transactions = transactions self.walletSyncState = walletSyncState + self.fiatPrice = fiatPrice // Preload cached data synchronously so UI has content before first render // transactions + listUnspent items are available from the persisted wallet db diff --git a/BDKSwiftExampleWallet/View/Activity/ActivityListView.swift b/BDKSwiftExampleWallet/View/Activity/ActivityListView.swift index b64018fa..fdab7af4 100644 --- a/BDKSwiftExampleWallet/View/Activity/ActivityListView.swift +++ b/BDKSwiftExampleWallet/View/Activity/ActivityListView.swift @@ -9,6 +9,8 @@ import BitcoinDevKit import SwiftUI struct ActivityListView: View { + @AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat = + .bitcoinSats @Bindable var viewModel: ActivityListViewModel var body: some View { @@ -26,13 +28,16 @@ struct ActivityListView: View { TransactionListView( viewModel: .init(), transactions: viewModel.transactions, - walletSyncState: viewModel.walletSyncState + walletSyncState: viewModel.walletSyncState, + format: balanceFormat, + fiatPrice: viewModel.fiatPrice ) .transition(.blurReplace) } else { LocalOutputListView( localOutputs: viewModel.localOutputs, - walletSyncState: viewModel.walletSyncState + walletSyncState: viewModel.walletSyncState, + fiatPrice: viewModel.fiatPrice ) .transition(.blurReplace) } @@ -80,5 +85,5 @@ struct CustomSegmentedControl: View { } #Preview { - ActivityListView(viewModel: .init()) + ActivityListView(viewModel: .init(fiatPrice: 714.23)) } diff --git a/BDKSwiftExampleWallet/View/Activity/LocalOutputItemView.swift b/BDKSwiftExampleWallet/View/Activity/LocalOutputItemView.swift index 7020dbc9..886c640a 100644 --- a/BDKSwiftExampleWallet/View/Activity/LocalOutputItemView.swift +++ b/BDKSwiftExampleWallet/View/Activity/LocalOutputItemView.swift @@ -9,9 +9,12 @@ import BitcoinDevKit import SwiftUI struct LocalOutputItemView: View { + @AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat = + .bitcoinSats @Environment(\.dynamicTypeSize) var dynamicTypeSize let isRedacted: Bool let output: LocalOutput + let fiatPrice: Double var body: some View { HStack(spacing: 15) { @@ -53,14 +56,24 @@ struct LocalOutputItemView: View { .redacted(reason: isRedacted ? .placeholder : []) Spacer() - - Text("\(output.txout.value.toSat()) sats") - .font(.subheadline) - .fontWeight(.semibold) - .fontDesign(.rounded) - .lineLimit(1) - .redacted(reason: isRedacted ? .placeholder : []) - + + Group { + HStack { + Text(balanceFormat.displayPrefix) + Text( + balanceFormat.formatted( + output.txout.value.toSat(), + fiatPrice: fiatPrice + ) + ) + Text(balanceFormat.displayText) + } + } + .font(.subheadline) + .fontWeight(.semibold) + .fontDesign(.rounded) + .lineLimit(1) + .redacted(reason: isRedacted ? .placeholder : []) } .padding(.vertical, 15.0) .padding(.vertical, 5.0) @@ -71,6 +84,7 @@ struct LocalOutputItemView: View { #Preview { LocalOutputItemView( isRedacted: false, - output: .mock + output: .mock, + fiatPrice: 714.23 ) } diff --git a/BDKSwiftExampleWallet/View/Activity/LocalOutputListView.swift b/BDKSwiftExampleWallet/View/Activity/LocalOutputListView.swift index 558a2c35..e5cfecc4 100644 --- a/BDKSwiftExampleWallet/View/Activity/LocalOutputListView.swift +++ b/BDKSwiftExampleWallet/View/Activity/LocalOutputListView.swift @@ -11,13 +11,15 @@ import SwiftUI struct LocalOutputListView: View { let localOutputs: [LocalOutput] let walletSyncState: WalletSyncState + let fiatPrice: Double var body: some View { List { if localOutputs.isEmpty && walletSyncState == .syncing { LocalOutputItemView( isRedacted: true, - output: .mock + output: .mock, + fiatPrice: .zero ) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) @@ -30,7 +32,8 @@ struct LocalOutputListView: View { ForEach(localOutputs, id: \.outpoint) { output in LocalOutputItemView( isRedacted: false, - output: output + output: output, + fiatPrice: fiatPrice ) } .listRowInsets(EdgeInsets()) @@ -43,5 +46,5 @@ struct LocalOutputListView: View { } #Preview { - LocalOutputListView(localOutputs: [.mock], walletSyncState: .synced) + LocalOutputListView(localOutputs: [.mock], walletSyncState: .synced, fiatPrice: 714.23) } diff --git a/BDKSwiftExampleWallet/View/Activity/TransactionDetailView.swift b/BDKSwiftExampleWallet/View/Activity/TransactionDetailView.swift index 7d267876..b9d1e583 100644 --- a/BDKSwiftExampleWallet/View/Activity/TransactionDetailView.swift +++ b/BDKSwiftExampleWallet/View/Activity/TransactionDetailView.swift @@ -10,10 +10,13 @@ import BitcoinUI import SwiftUI struct TransactionDetailView: View { + @AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat = + .bitcoinSats @Bindable var viewModel: TransactionDetailViewModel @State private var isCopied = false @State private var showCheckmark = false let txDetails: TxDetails + let fiatPrice: Double var body: some View { @@ -55,8 +58,14 @@ struct TransactionDetailView: View { VStack(spacing: 8) { HStack { - Text(abs(txDetails.balanceDelta).delimiter) - Text("sats") + Text(balanceFormat.displayPrefix) + Text( + balanceFormat.formatted( + UInt64(abs(txDetails.balanceDelta)), + fiatPrice: fiatPrice + ) + ) + Text(balanceFormat.displayText) } .lineLimit(1) .minimumScaleFactor(0.5) @@ -168,7 +177,8 @@ struct TransactionDetailView: View { viewModel: .init( bdkClient: .mock ), - txDetails: .mock + txDetails: .mock, + fiatPrice: 714.23 ) } #endif diff --git a/BDKSwiftExampleWallet/View/Activity/TransactionItemView.swift b/BDKSwiftExampleWallet/View/Activity/TransactionItemView.swift index afd637c8..a83fee20 100644 --- a/BDKSwiftExampleWallet/View/Activity/TransactionItemView.swift +++ b/BDKSwiftExampleWallet/View/Activity/TransactionItemView.swift @@ -13,6 +13,20 @@ struct TransactionItemView: View { @Environment(\.dynamicTypeSize) var dynamicTypeSize let txDetails: TxDetails let isRedacted: Bool + private let format: BalanceDisplayFormat + private var fiatPrice: Double + + init( + txDetails: TxDetails, + isRedacted: Bool, + format: BalanceDisplayFormat, + fiatPrice: Double + ) { + self.txDetails = txDetails + self.isRedacted = isRedacted + self.format = format + self.fiatPrice = fiatPrice + } var body: some View { @@ -98,10 +112,11 @@ struct TransactionItemView: View { Spacer() let delta = txDetails.balanceDelta - let prefix = delta >= 0 ? "+ " : "- " - let amount = abs(delta) + let prefix = (delta >= 0 ? "+ " : "- ").appending("\(format.displayPrefix) ") + let amount = format.formatted(UInt64(abs(delta)), fiatPrice: fiatPrice) + let suffix = format.displayText - Text("\(prefix)\(amount) sats") + Text("\(prefix)\(amount) \(suffix)") .font(.subheadline) .fontWeight(.semibold) .fontDesign(.rounded) @@ -120,7 +135,9 @@ struct TransactionItemView: View { #Preview { TransactionItemView( txDetails: .mock, - isRedacted: false + isRedacted: false, + format: .bip177, + fiatPrice: 714.23 ) } #endif diff --git a/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift b/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift index c32bab03..a28aa229 100644 --- a/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift +++ b/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift @@ -13,14 +13,32 @@ struct TransactionListView: View { @Bindable var viewModel: TransactionListViewModel let transactions: [CanonicalTx] let walletSyncState: WalletSyncState - + private let format: BalanceDisplayFormat + private let fiatPrice: Double + + init( + viewModel: TransactionListViewModel, + transactions: [CanonicalTx], + walletSyncState: WalletSyncState, + format: BalanceDisplayFormat, + fiatPrice: Double + ) { + self.viewModel = viewModel + self.transactions = transactions + self.walletSyncState = walletSyncState + self.format = format + self.fiatPrice = fiatPrice + } + var body: some View { List { if transactions.isEmpty && walletSyncState == .syncing { TransactionItemView( txDetails: .mock, - isRedacted: true + isRedacted: true, + format: format, + fiatPrice: fiatPrice ) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) @@ -94,12 +112,15 @@ struct TransactionListView: View { viewModel: .init( bdkClient: .live ), - txDetails: txDetails + txDetails: txDetails, + fiatPrice: fiatPrice ) ) { TransactionItemView( txDetails: txDetails, - isRedacted: false + isRedacted: false, + format: format, + fiatPrice: fiatPrice ) } @@ -139,7 +160,9 @@ struct TransactionListView: View { transactions: [ .mock ], - walletSyncState: .synced + walletSyncState: .synced, + format: .bip177, + fiatPrice: 714.23 ) } #Preview { @@ -148,7 +171,9 @@ struct TransactionListView: View { bdkClient: .mock ), transactions: [], - walletSyncState: .synced + walletSyncState: .synced, + format: .bip177, + fiatPrice: 714.23 ) } #endif diff --git a/BDKSwiftExampleWallet/View/Home/BalanceView.swift b/BDKSwiftExampleWallet/View/Home/BalanceView.swift index 4482872f..d0d3e83c 100644 --- a/BDKSwiftExampleWallet/View/Home/BalanceView.swift +++ b/BDKSwiftExampleWallet/View/Home/BalanceView.swift @@ -14,10 +14,6 @@ struct BalanceView: View { private var format: BalanceDisplayFormat private let balance: UInt64 private var fiatPrice: Double - private var satsPrice: Double { - let usdValue = Double(balance).valueInUSD(price: fiatPrice) - return usdValue - } private var currencySymbol: some View { Image(systemName: format == .fiat ? "dollarsign" : "bitcoinsign") @@ -37,34 +33,23 @@ struct BalanceView: View { @MainActor private var formattedBalance: String { - switch format { - case .sats: - return balance.formatted(.number) - case .bitcoin: - return String(format: "%.8f", Double(balance) / 100_000_000) - case .bitcoinSats: - return balance.formattedSatoshis() - case .fiat: - return satsPrice.formatted(.number.precision(.fractionLength(2))) - case .bip177: - return balance.formattedBip177() - } + return format.formatted(balance, fiatPrice: fiatPrice) } @MainActor var balanceText: some View { - Text(format == .fiat && satsPrice == 0 ? "00.00" : formattedBalance) + Text(format == .fiat && fiatPrice == 0 ? "00.00" : formattedBalance) .contentTransition(.numericText(countsDown: true)) .fontWeight(.semibold) .fontDesign(.rounded) .foregroundStyle( - format == .fiat && satsPrice == 0 ? .secondary : .primary + format == .fiat && fiatPrice == 0 ? .secondary : .primary ) .opacity( - format == .fiat && satsPrice == 0 ? balanceTextPulsingOpacity : 1 + format == .fiat && fiatPrice == 0 ? balanceTextPulsingOpacity : 1 ) .animation(.spring(response: 0.4, dampingFraction: 0.8), value: format) - .animation(.easeInOut, value: satsPrice) + .animation(.easeInOut, value: fiatPrice) .onAppear { withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { balanceTextPulsingOpacity = 0.3 diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 695a00c9..9a33535f 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -59,7 +59,9 @@ struct WalletView: View { TransactionListView( viewModel: .init(), transactions: viewModel.recentTransactions, - walletSyncState: viewModel.walletSyncState + walletSyncState: viewModel.walletSyncState, + format: balanceFormat, + fiatPrice: viewModel.price ) .refreshable { if viewModel.isKyotoClient { @@ -151,7 +153,7 @@ struct WalletView: View { } .navigationDestination(isPresented: $showAllTransactions) { - ActivityListView(viewModel: .init()) + ActivityListView(viewModel: .init(fiatPrice: viewModel.price)) } .navigationDestination(for: NavigationDestination.self) { destination in switch destination {