Skip to content

Commit e8769ed

Browse files
committed
feat: add balance formatting across the app [#issue 307]
1 parent de4e9ca commit e8769ed

File tree

8 files changed

+95
-36
lines changed

8 files changed

+95
-36
lines changed

BDKSwiftExampleWallet/Model/BalanceDisplayFormat.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ enum BalanceDisplayFormat: String, CaseIterable, Codable {
1515
case fiat = "usd"
1616
case bip177 = "bip177"
1717

18+
var displayPrefix: String {
19+
switch self {
20+
case .bitcoinSats, .bitcoin, .bip177: return ""
21+
case .fiat: return "$"
22+
default : return ""
23+
}
24+
}
25+
1826
var displayText: String {
1927
switch self {
2028
case .sats, .bitcoinSats: return "sats"
@@ -23,6 +31,22 @@ enum BalanceDisplayFormat: String, CaseIterable, Codable {
2331
case .fiat: return "USD"
2432
}
2533
}
34+
35+
func formatted(_ btcAmount: UInt64, fiatPrice: Double) -> String {
36+
switch self {
37+
case .sats:
38+
return btcAmount.formatted(.number)
39+
case .bitcoin:
40+
return String(format: "%.8f", Double(btcAmount) / 100_000_000)
41+
case .bitcoinSats:
42+
return btcAmount.formattedSatoshis()
43+
case .fiat:
44+
let satsPrice = Double(btcAmount).valueInUSD(price: fiatPrice)
45+
return satsPrice.formatted(.number.precision(.fractionLength(2)))
46+
case .bip177:
47+
return btcAmount.formattedBip177()
48+
}
49+
}
2650
}
2751

2852
extension BalanceDisplayFormat {

BDKSwiftExampleWallet/Resources/Localizable.xcstrings

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,12 @@
204204
}
205205
}
206206
},
207-
"%@%lld sats" : {
207+
"%@%@ %@" : {
208208
"localizations" : {
209209
"en" : {
210210
"stringUnit" : {
211211
"state" : "new",
212-
"value" : "%1$@%2$lld sats"
212+
"value" : "%1$@%2$@ %3$@"
213213
}
214214
}
215215
}

BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ActivityListViewModel {
2222
var totalScripts: UInt64 = 0
2323
var walletSyncState: WalletSyncState
2424
var walletViewError: AppError?
25+
var fiatPrice: Double
2526

2627
private var updateProgress: @Sendable (UInt64, UInt64) -> Void {
2728
{ [weak self] inspected, total in
@@ -41,11 +42,13 @@ class ActivityListViewModel {
4142
init(
4243
bdkClient: BDKClient = .live,
4344
transactions: [CanonicalTx] = [],
44-
walletSyncState: WalletSyncState = .notStarted
45+
walletSyncState: WalletSyncState = .notStarted,
46+
fiatPrice: Double
4547
) {
4648
self.bdkClient = bdkClient
4749
self.transactions = transactions
4850
self.walletSyncState = walletSyncState
51+
self.fiatPrice = fiatPrice
4952

5053
// Preload cached data synchronously so UI has content before first render
5154
// transactions + listUnspent items are available from the persisted wallet db

BDKSwiftExampleWallet/View/Activity/ActivityListView.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import BitcoinDevKit
99
import SwiftUI
1010

1111
struct ActivityListView: View {
12+
@AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat =
13+
.bitcoinSats
1214
@Bindable var viewModel: ActivityListViewModel
1315

1416
var body: some View {
@@ -26,7 +28,9 @@ struct ActivityListView: View {
2628
TransactionListView(
2729
viewModel: .init(),
2830
transactions: viewModel.transactions,
29-
walletSyncState: viewModel.walletSyncState
31+
walletSyncState: viewModel.walletSyncState,
32+
format: balanceFormat,
33+
fiatPrice: viewModel.fiatPrice
3034
)
3135
.transition(.blurReplace)
3236
} else {
@@ -80,5 +84,5 @@ struct CustomSegmentedControl: View {
8084
}
8185

8286
#Preview {
83-
ActivityListView(viewModel: .init())
87+
ActivityListView(viewModel: .init(fiatPrice: 714.23))
8488
}

BDKSwiftExampleWallet/View/Activity/TransactionItemView.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ struct TransactionItemView: View {
1313
@Environment(\.dynamicTypeSize) var dynamicTypeSize
1414
let txDetails: TxDetails
1515
let isRedacted: Bool
16+
private let format: BalanceDisplayFormat
17+
private var fiatPrice: Double
18+
19+
init(
20+
txDetails: TxDetails,
21+
isRedacted: Bool,
22+
format: BalanceDisplayFormat,
23+
fiatPrice: Double
24+
) {
25+
self.txDetails = txDetails
26+
self.isRedacted = isRedacted
27+
self.format = format
28+
self.fiatPrice = fiatPrice
29+
}
1630

1731
var body: some View {
1832

@@ -98,10 +112,11 @@ struct TransactionItemView: View {
98112
Spacer()
99113

100114
let delta = txDetails.balanceDelta
101-
let prefix = delta >= 0 ? "+ " : "- "
102-
let amount = abs(delta)
115+
let prefix = (delta >= 0 ? "+ " : "- ").appending("\(format.displayPrefix) ")
116+
let amount = format.formatted(UInt64(abs(delta)), fiatPrice: fiatPrice)
117+
let suffix = format.displayText
103118

104-
Text("\(prefix)\(amount) sats")
119+
Text("\(prefix)\(amount) \(suffix)")
105120
.font(.subheadline)
106121
.fontWeight(.semibold)
107122
.fontDesign(.rounded)
@@ -120,7 +135,9 @@ struct TransactionItemView: View {
120135
#Preview {
121136
TransactionItemView(
122137
txDetails: .mock,
123-
isRedacted: false
138+
isRedacted: false,
139+
format: .bip177,
140+
fiatPrice: 714.23
124141
)
125142
}
126143
#endif

BDKSwiftExampleWallet/View/Activity/TransactionListView.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,32 @@ struct TransactionListView: View {
1313
@Bindable var viewModel: TransactionListViewModel
1414
let transactions: [CanonicalTx]
1515
let walletSyncState: WalletSyncState
16-
16+
private let format: BalanceDisplayFormat
17+
private let fiatPrice: Double
18+
19+
init(
20+
viewModel: TransactionListViewModel,
21+
transactions: [CanonicalTx],
22+
walletSyncState: WalletSyncState,
23+
format: BalanceDisplayFormat,
24+
fiatPrice: Double
25+
) {
26+
self.viewModel = viewModel
27+
self.transactions = transactions
28+
self.walletSyncState = walletSyncState
29+
self.format = format
30+
self.fiatPrice = fiatPrice
31+
}
32+
1733
var body: some View {
1834

1935
List {
2036
if transactions.isEmpty && walletSyncState == .syncing {
2137
TransactionItemView(
2238
txDetails: .mock,
23-
isRedacted: true
39+
isRedacted: true,
40+
format: format,
41+
fiatPrice: fiatPrice
2442
)
2543
.listRowInsets(EdgeInsets())
2644
.listRowSeparator(.hidden)
@@ -99,7 +117,9 @@ struct TransactionListView: View {
99117
) {
100118
TransactionItemView(
101119
txDetails: txDetails,
102-
isRedacted: false
120+
isRedacted: false,
121+
format: format,
122+
fiatPrice: fiatPrice
103123
)
104124
}
105125

@@ -139,7 +159,9 @@ struct TransactionListView: View {
139159
transactions: [
140160
.mock
141161
],
142-
walletSyncState: .synced
162+
walletSyncState: .synced,
163+
format: .bip177,
164+
fiatPrice: 714.23
143165
)
144166
}
145167
#Preview {
@@ -148,7 +170,9 @@ struct TransactionListView: View {
148170
bdkClient: .mock
149171
),
150172
transactions: [],
151-
walletSyncState: .synced
173+
walletSyncState: .synced,
174+
format: .bip177,
175+
fiatPrice: 714.23
152176
)
153177
}
154178
#endif

BDKSwiftExampleWallet/View/Home/BalanceView.swift

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ struct BalanceView: View {
1414
private var format: BalanceDisplayFormat
1515
private let balance: UInt64
1616
private var fiatPrice: Double
17-
private var satsPrice: Double {
18-
let usdValue = Double(balance).valueInUSD(price: fiatPrice)
19-
return usdValue
20-
}
2117

2218
private var currencySymbol: some View {
2319
Image(systemName: format == .fiat ? "dollarsign" : "bitcoinsign")
@@ -37,34 +33,23 @@ struct BalanceView: View {
3733

3834
@MainActor
3935
private var formattedBalance: String {
40-
switch format {
41-
case .sats:
42-
return balance.formatted(.number)
43-
case .bitcoin:
44-
return String(format: "%.8f", Double(balance) / 100_000_000)
45-
case .bitcoinSats:
46-
return balance.formattedSatoshis()
47-
case .fiat:
48-
return satsPrice.formatted(.number.precision(.fractionLength(2)))
49-
case .bip177:
50-
return balance.formattedBip177()
51-
}
36+
return format.formatted(balance, fiatPrice: fiatPrice)
5237
}
5338

5439
@MainActor
5540
var balanceText: some View {
56-
Text(format == .fiat && satsPrice == 0 ? "00.00" : formattedBalance)
41+
Text(format == .fiat && fiatPrice == 0 ? "00.00" : formattedBalance)
5742
.contentTransition(.numericText(countsDown: true))
5843
.fontWeight(.semibold)
5944
.fontDesign(.rounded)
6045
.foregroundStyle(
61-
format == .fiat && satsPrice == 0 ? .secondary : .primary
46+
format == .fiat && fiatPrice == 0 ? .secondary : .primary
6247
)
6348
.opacity(
64-
format == .fiat && satsPrice == 0 ? balanceTextPulsingOpacity : 1
49+
format == .fiat && fiatPrice == 0 ? balanceTextPulsingOpacity : 1
6550
)
6651
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: format)
67-
.animation(.easeInOut, value: satsPrice)
52+
.animation(.easeInOut, value: fiatPrice)
6853
.onAppear {
6954
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
7055
balanceTextPulsingOpacity = 0.3

BDKSwiftExampleWallet/View/WalletView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ struct WalletView: View {
5959
TransactionListView(
6060
viewModel: .init(),
6161
transactions: viewModel.recentTransactions,
62-
walletSyncState: viewModel.walletSyncState
62+
walletSyncState: viewModel.walletSyncState,
63+
format: balanceFormat,
64+
fiatPrice: viewModel.price
6365
)
6466
.refreshable {
6567
if viewModel.isKyotoClient {
@@ -151,7 +153,7 @@ struct WalletView: View {
151153

152154
}
153155
.navigationDestination(isPresented: $showAllTransactions) {
154-
ActivityListView(viewModel: .init())
156+
ActivityListView(viewModel: .init(fiatPrice: viewModel.price))
155157
}
156158
.navigationDestination(for: NavigationDestination.self) { destination in
157159
switch destination {

0 commit comments

Comments
 (0)