From d3f0bfff51eb3f826f1c432cadd779e14a393547 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Fri, 25 Apr 2025 09:23:56 -0600 Subject: [PATCH] wallet: Add fiat pricing to Wallet View This PR adds the fiat price to user's wallet balance. Using Coinbase API to get BTC current price and the user's locale (region) to determine their currency. Changelog-Added: Added fiat pricing to wallet balance Signed-off-by: ericholguin --- damus.xcodeproj/project.pbxproj | 8 +++ damus/Models/CoinbaseModel.swift | 84 ++++++++++++++++++++++++++++ damus/Views/Wallet/BalanceView.swift | 12 +++- damus/Views/Wallet/WalletView.swift | 2 +- 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 damus/Models/CoinbaseModel.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 52d69d6b4..a4b2180b4 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -403,6 +403,9 @@ 5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; 5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */; }; + 5C4FA7C52DBA9D5C00CE658C /* CoinbaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7C42DBA9D5800CE658C /* CoinbaseModel.swift */; }; + 5C4FA7C62DBA9D5C00CE658C /* CoinbaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7C42DBA9D5800CE658C /* CoinbaseModel.swift */; }; + 5C4FA7C72DBA9D5C00CE658C /* CoinbaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7C42DBA9D5800CE658C /* CoinbaseModel.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; }; @@ -2409,6 +2412,7 @@ 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = ""; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = ""; }; 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDraftContentView.swift; sourceTree = ""; }; + 5C4FA7C42DBA9D5800CE658C /* CoinbaseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinbaseModel.swift; sourceTree = ""; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = ""; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = ""; }; 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = ""; }; @@ -2770,6 +2774,7 @@ 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( + 5C4FA7C42DBA9D5800CE658C /* CoinbaseModel.swift */, D73BDB122D71212600D69970 /* NostrNetworkManager */, D74F43082B23F09300425B75 /* Purple */, BA3759882ABCCDE30018D73B /* Camera */, @@ -4512,6 +4517,7 @@ 4C32B9572A9AD44700DC3548 /* Root.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */, + 5C4FA7C72DBA9D5C00CE658C /* CoinbaseModel.swift in Sources */, 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */, @@ -5283,6 +5289,7 @@ 82D6FB832CD99F7900C925F4 /* SearchModel.swift in Sources */, 82D6FB842CD99F7900C925F4 /* NostrFilter+Hashable.swift in Sources */, 82D6FB852CD99F7900C925F4 /* Contacts.swift in Sources */, + 5C4FA7C62DBA9D5C00CE658C /* CoinbaseModel.swift in Sources */, 82D6FB862CD99F7900C925F4 /* CreateAccountModel.swift in Sources */, 82D6FB872CD99F7900C925F4 /* HomeModel.swift in Sources */, 82D6FB882CD99F7900C925F4 /* SignalModel.swift in Sources */, @@ -5777,6 +5784,7 @@ D73E5EFD2C6A97F4007EB227 /* InnerTimelineView.swift in Sources */, D73E5EFE2C6A97F4007EB227 /* (null) in Sources */, D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */, + 5C4FA7C52DBA9D5C00CE658C /* CoinbaseModel.swift in Sources */, D73E5EFF2C6A97F4007EB227 /* ZapsView.swift in Sources */, D73E5F002C6A97F4007EB227 /* CustomizeZapView.swift in Sources */, D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */, diff --git a/damus/Models/CoinbaseModel.swift b/damus/Models/CoinbaseModel.swift new file mode 100644 index 000000000..755813cf8 --- /dev/null +++ b/damus/Models/CoinbaseModel.swift @@ -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 "" + } + let fiat = (Double(input) / 100_000_000) * btcPrice + 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 + } + + 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)") + } + } + } + + private func cachePrice(_ price: Double) { + 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 +} + diff --git a/damus/Views/Wallet/BalanceView.swift b/damus/Views/Wallet/BalanceView.swift index 93b236f96..5d3e7dde9 100644 --- a/damus/Views/Wallet/BalanceView.swift +++ b/damus/Views/Wallet/BalanceView.swift @@ -9,6 +9,7 @@ import SwiftUI struct BalanceView: View { var balance: Int64? + @StateObject private var coinbase = CoinbaseModel() var body: some View { VStack(spacing: 5) { @@ -16,6 +17,10 @@ struct BalanceView: View { .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) } 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) } } diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift index cf644623a..c1105619a 100644 --- a/damus/Views/Wallet/WalletView.swift +++ b/damus/Views/Wallet/WalletView.swift @@ -45,7 +45,7 @@ struct WalletView: View { ) } - VStack(spacing: 5) { + VStack(spacing: 10) { BalanceView(balance: model.balance)