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)