-
Notifications
You must be signed in to change notification settings - Fork 296
wallet: Add fiat pricing to Wallet View #3001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,84 @@ | ||||||
// | ||||||
// CoinbaseModel.swift | ||||||
// damus | ||||||
// | ||||||
// Created by eric on 4/24/25. | ||||||
// | ||||||
|
||||||
import Foundation | ||||||
import SwiftUI | ||||||
|
||||||
class CoinbaseModel: ObservableObject { | ||||||
|
||||||
@Published var btcPrice: Double? = nil { | ||||||
didSet { | ||||||
if let btcPrice = btcPrice { | ||||||
cachePrice(btcPrice) | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
var cacheKey: String { | ||||||
"btc_price_\(currency.lowercased())" | ||||||
} | ||||||
|
||||||
var currency: String = Locale.current.currency?.identifier ?? "USD" | ||||||
|
||||||
let numberFormatter: NumberFormatter = { | ||||||
let formatter = NumberFormatter() | ||||||
formatter.locale = Locale.current | ||||||
formatter.numberStyle = .decimal | ||||||
formatter.maximumFractionDigits = 2 | ||||||
return formatter | ||||||
}() | ||||||
|
||||||
func satsToFiat(input: Int64) -> String { | ||||||
guard let btcPrice = btcPrice, input > 0 else { | ||||||
return "" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use more expressive semantics of the result, and let the view choose how to handle failures. Instead of defaulting to an empty string or value when something fails, we should:
|
||||||
} | ||||||
let fiat = (Double(input) / 100_000_000) * btcPrice | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
let amount = numberFormatter.string(from: NSNumber(value: fiat)) ?? "" | ||||||
let symbol: String = numberFormatter.currencySymbol | ||||||
if symbol.isEmpty || symbol == currency { | ||||||
return amount + " " + currency | ||||||
} | ||||||
return symbol + amount + " " + currency | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be making some assumptions about currency value formatting that might not be true for some locales. I believe Apple provides a way to format currencies in a simpler and localization-friendly way, by using |
||||||
} | ||||||
|
||||||
func fetchFromCoinbase() { | ||||||
let urlString = "https://api.coinbase.com/v2/prices/BTC-\(currency)/spot" | ||||||
guard let url = URL(string: urlString) else { return } | ||||||
|
||||||
Task { | ||||||
do { | ||||||
let (data, _) = try await URLSession.shared.data(from: url) | ||||||
let decoded = try JSONDecoder().decode(CoinbaseResponse.self, from: data) | ||||||
DispatchQueue.main.async { | ||||||
self.btcPrice = decoded.data.amount | ||||||
} | ||||||
} catch { | ||||||
print("Failed to fetch BTC price from Coinbase: \(error)") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am a bit worried about how this error is being handled. If something is broken or the API stops working, the interface will just keep displaying the last known value forever, which might be misleading and hard to notice. |
||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
private func cachePrice(_ price: Double) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
UserDefaults.standard.set(price, forKey: cacheKey) | ||||||
} | ||||||
|
||||||
func loadCachedPrice() { | ||||||
let cached = UserDefaults.standard.double(forKey: cacheKey) | ||||||
if cached > 0 { | ||||||
btcPrice = cached | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
struct CoinbaseResponse: Codable { | ||||||
let data: CoinbasePrice | ||||||
} | ||||||
|
||||||
struct CoinbasePrice: Codable { | ||||||
let amount: Double | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick tip: The structs struct CoinbaseModel {
...
struct Response: Codable {
let data: Price
struct Price: Codable {
let amount: Double
}
}
} |
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,13 +9,18 @@ import SwiftUI | |||||||||||||||||||
|
||||||||||||||||||||
struct BalanceView: View { | ||||||||||||||||||||
var balance: Int64? | ||||||||||||||||||||
@StateObject private var coinbase = CoinbaseModel() | ||||||||||||||||||||
|
||||||||||||||||||||
var body: some View { | ||||||||||||||||||||
VStack(spacing: 5) { | ||||||||||||||||||||
Text("Current balance", comment: "Label for displaying current wallet balance") | ||||||||||||||||||||
.foregroundStyle(DamusColors.neutral6) | ||||||||||||||||||||
if let balance { | ||||||||||||||||||||
self.numericalBalanceView(text: NumberFormatter.localizedString(from: NSNumber(integerLiteral: Int(balance)), number: .decimal)) | ||||||||||||||||||||
|
||||||||||||||||||||
Text(coinbase.satsToFiat(input: balance)) | ||||||||||||||||||||
.fontWeight(.medium) | ||||||||||||||||||||
.foregroundStyle(DamusColors.neutral6) | ||||||||||||||||||||
Comment on lines
+21
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||
} | ||||||||||||||||||||
else { | ||||||||||||||||||||
// Make sure we do not show any numeric value to the user when still loading (or when failed to load) | ||||||||||||||||||||
|
@@ -25,7 +30,12 @@ struct BalanceView: View { | |||||||||||||||||||
.shimmer(true) | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
.onAppear { | ||||||||||||||||||||
coinbase.loadCachedPrice() | ||||||||||||||||||||
coinbase.fetchFromCoinbase() | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
func numericalBalanceView(text: String) -> some View { | ||||||||||||||||||||
HStack { | ||||||||||||||||||||
|
@@ -43,7 +53,7 @@ struct BalanceView: View { | |||||||||||||||||||
.foregroundStyle(PinkGradient) | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
.padding(.bottom) | ||||||||||||||||||||
.padding(.bottom, 5) | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.