Add support for currency selection

This commit is contained in:
2024-09-07 05:55:34 +03:00
parent cb0397ba98
commit edb10c48e6
11 changed files with 164 additions and 87 deletions

View File

@@ -26,14 +26,14 @@ public struct ContentView: View {
@MainActor
func updatePrice() async {
do {
guard let price = try await priceFetcherDelegator.btcToUsd() else {
satsViewModel.btcToUsdString = ""
guard let price = try await priceFetcherDelegator.convertBTC(toCurrency: satsViewModel.selectedCurrency) else {
satsViewModel.btcToCurrencyString = ""
return
}
satsViewModel.btcToUsdString = "\(price)"
satsViewModel.btcToCurrencyString = "\(price)"
} catch {
satsViewModel.btcToUsdString = ""
satsViewModel.btcToCurrencyString = ""
}
satsViewModel.lastUpdated = Date.now
}
@@ -47,8 +47,18 @@ public struct ContentView: View {
}
}
Picker("Currency", selection: $satsViewModel.selectedCurrency) {
ForEach(satsViewModel.currencies, id: \.self) {
if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: $0.identifier) {
Text("\($0.identifier) - \(localizedCurrency)")
} else {
Text($0.identifier)
}
}
}
HStack {
TextField("", text: $satsViewModel.btcToUsdString)
TextField("", text: $satsViewModel.btcToCurrencyString)
.disabled(priceSource != .manual)
#if os(iOS) || SKIP
.keyboardType(.decimalPad)
@@ -64,7 +74,7 @@ public struct ContentView: View {
}
}
} header: {
Text("1 BTC to USD")
Text("1 BTC to \(satsViewModel.selectedCurrency.identifier)")
} footer: {
if priceSource != .manual, let lastUpdated = satsViewModel.lastUpdated {
Text("Last updated: \(dateFormatter.string(from: lastUpdated))")
@@ -90,17 +100,23 @@ public struct ContentView: View {
}
Section {
TextField("", text: $satsViewModel.usdString)
TextField("", text: $satsViewModel.currencyValueString)
#if os(iOS) || SKIP
.keyboardType(.decimalPad)
#endif
} header: {
Text("USD")
Text(satsViewModel.selectedCurrency.identifier)
}
}
.task {
await updatePrice()
}
.onChange(of: satsViewModel.selectedCurrency) { newCurrency in
satsViewModel.lastUpdated = nil
Task {
await updatePrice()
}
}
.onChange(of: priceSource) { newPriceSource in
satsViewModel.lastUpdated = nil
priceFetcherDelegator.priceSource = newPriceSource

View File

@@ -11,35 +11,35 @@
import Foundation
private struct CoinGeckoPriceResponse: Codable {
let bitcoin: CoinGeckoPrice
}
private struct CoinGeckoPrice: Codable {
#if !SKIP
let usd: Decimal
let bitcoin: [String: Decimal]
#else
let usd: String
let bitcoin: [String: String]
#endif
}
class CoinGeckoPriceFetcher : PriceFetcher {
private static let urlString = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&precision=18"
func urlString(toCurrency currency: Locale.Currency) -> String {
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(currency.identifier.lowercased())&precision=18"
}
func btcToUsd() async throws -> Decimal? {
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
do {
guard let urlComponents = URLComponents(string: CoinGeckoPriceFetcher.urlString), let url = urlComponents.url else {
guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else {
return nil
}
let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
let priceResponse = try JSONDecoder().decode(CoinGeckoPriceResponse.self, from: data)
let price = priceResponse.bitcoin
guard let price = priceResponse.bitcoin[currency.identifier.lowercased()] else {
return nil
}
#if !SKIP
return price.usd
return price
#else
return Decimal(price.usd)
return Decimal(price)
#endif
} catch {
return nil

View File

@@ -21,13 +21,13 @@ private struct CoinbasePrice: Codable {
}
class CoinbasePriceFetcher : PriceFetcher {
private static let urlString = "https://api.coinbase.com/v2/prices/BTC-USD/spot"
private static let btc = "BTC"
private static let usd = "USD"
func urlString(toCurrency currency: Locale.Currency) -> String {
"https://api.coinbase.com/v2/prices/BTC-\(currency.identifier)/spot"
}
func btcToUsd() async throws -> Decimal? {
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
do {
guard let urlComponents = URLComponents(string: CoinbasePriceFetcher.urlString), let url = urlComponents.url else {
guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else {
return nil
}
@@ -36,7 +36,7 @@ class CoinbasePriceFetcher : PriceFetcher {
let coinbasePriceResponse = try JSONDecoder().decode(CoinbasePriceResponse.self, from: data)
let coinbasePrice = coinbasePriceResponse.data
guard coinbasePrice.base == CoinbasePriceFetcher.btc && coinbasePrice.currency == CoinbasePriceFetcher.usd else {
guard coinbasePrice.base == "BTC" && coinbasePrice.currency == currency.identifier else {
return nil
}

View File

@@ -13,7 +13,7 @@ import Foundation
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
class FakePriceFetcher: PriceFetcher {
func btcToUsd() async throws -> Decimal? {
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
Decimal(Double.random(in: 10000...100000))
}
}

View File

@@ -14,7 +14,7 @@ import Foundation
class ManualPriceFetcher: PriceFetcher {
var price: Decimal = Decimal(1)
func btcToUsd() async throws -> Decimal? {
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
return price
}
}

View File

@@ -11,5 +11,5 @@
import Foundation
protocol PriceFetcher {
func btcToUsd() async throws -> Decimal?
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal?
}

View File

@@ -39,7 +39,7 @@ class PriceFetcherDelegator: PriceFetcher {
}
}
func btcToUsd() async throws -> Decimal? {
return try await delegate.btcToUsd()
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
return try await delegate.convertBTC(toCurrency: currency)
}
}

View File

@@ -4,11 +4,24 @@
"" : {
},
"1 BTC to USD" : {
"%@ - %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ - %2$@"
}
}
}
},
"1 BTC to %@" : {
},
"BTC" : {
},
"Currency" : {
},
"Last updated: %@" : {
@@ -18,9 +31,6 @@
},
"Sats" : {
},
"USD" : {
}
},
"version" : "1.0"

View File

@@ -15,26 +15,40 @@ import SwiftUI
class SatsViewModel: ObservableObject {
@Published var lastUpdated: Date?
@Published var btcToUsdStringInternal: String = ""
@Published var btcToCurrencyStringInternal: String = ""
@Published var satsStringInternal: String = ""
@Published var btcStringInternal: String = ""
@Published var usdStringInternal: String = ""
@Published var currencyValueStringInternal: String = ""
@Published var selectedCurrency: Locale.Currency = Locale.current.currency ?? Locale.Currency("USD")
var btcToUsdString: String {
var currencies: [Locale.Currency] {
let commonISOCurrencyCodes = Set(Locale.commonISOCurrencyCodes)
let currentCurrency = Locale.current.currency ?? Locale.Currency("USD")
if commonISOCurrencyCodes.contains(currentCurrency.identifier) {
return Locale.commonISOCurrencyCodes.map { Locale.Currency($0) }
} else {
var commonAndCurrentCurrencies = Locale.commonISOCurrencyCodes
commonAndCurrentCurrencies.append(currentCurrency.identifier)
commonAndCurrentCurrencies.sort()
return commonAndCurrentCurrencies.map { Locale.Currency($0) }
}
}
var btcToCurrencyString: String {
get {
btcToUsdStringInternal
btcToCurrencyStringInternal
}
set {
guard btcToUsdStringInternal != newValue else {
guard btcToCurrencyStringInternal != newValue else {
return
}
btcToUsdStringInternal = newValue
btcToCurrencyStringInternal = newValue
if let btc, let btcToUsd {
usdStringInternal = (btc * btcToUsd).formatString()
if let btc, let btcToCurrency {
currencyValueStringInternal = (btc * btcToCurrency).formatString()
} else {
usdStringInternal = ""
currencyValueStringInternal = ""
}
}
}
@@ -57,14 +71,14 @@ class SatsViewModel: ObservableObject {
let btc = sats.divide(Decimal(100000000), 20, java.math.RoundingMode.DOWN)
#endif
btcStringInternal = btc.formatString()
if let btcToUsd {
usdStringInternal = (btc * btcToUsd).formatString()
if let btcToCurrency {
currencyValueStringInternal = (btc * btcToCurrency).formatString()
} else {
usdStringInternal = ""
currencyValueStringInternal = ""
}
} else {
btcStringInternal = ""
usdStringInternal = ""
currencyValueStringInternal = ""
}
}
}
@@ -84,35 +98,35 @@ class SatsViewModel: ObservableObject {
let sats = btc * Decimal(100000000)
satsStringInternal = sats.formatString()
if let btcToUsd {
usdStringInternal = (btc * btcToUsd).formatString()
if let btcToCurrency {
currencyValueStringInternal = (btc * btcToCurrency).formatString()
} else {
usdStringInternal = ""
currencyValueStringInternal = ""
}
} else {
satsStringInternal = ""
usdStringInternal = ""
currencyValueStringInternal = ""
}
}
}
var usdString: String {
var currencyValueString: String {
get {
usdStringInternal
currencyValueStringInternal
}
set {
guard usdStringInternal != newValue else {
guard currencyValueStringInternal != newValue else {
return
}
usdStringInternal = newValue
currencyValueStringInternal = newValue
if let usd {
if let btcToUsd {
if let currencyValue {
if let btcToCurrency {
#if !SKIP
let btc = usd / btcToUsd
let btc = currencyValue / btcToCurrency
#else
let btc = usd.divide(btcToUsd, 20, java.math.RoundingMode.DOWN)
let btc = currencyValue.divide(btcToCurrency, 20, java.math.RoundingMode.DOWN)
#endif
btcStringInternal = btc.formatString()
@@ -120,21 +134,21 @@ class SatsViewModel: ObservableObject {
satsStringInternal = sats.formatString()
} else {
satsStringInternal = ""
usdStringInternal = ""
currencyValueStringInternal = ""
}
} else {
satsStringInternal = ""
usdStringInternal = ""
currencyValueStringInternal = ""
}
}
}
var btcToUsd: Decimal? {
var btcToCurrency: Decimal? {
#if !SKIP
return Decimal(string: btcToUsdStringInternal)
return Decimal(string: btcToCurrencyStringInternal)
#else
do {
return Decimal(btcToUsdStringInternal)
return Decimal(btcToCurrencyStringInternal)
} catch {
return nil
}
@@ -165,12 +179,12 @@ class SatsViewModel: ObservableObject {
#endif
}
var usd: Decimal? {
var currencyValue: Decimal? {
#if !SKIP
return Decimal(string: usdStringInternal)
return Decimal(string: currencyValueStringInternal)
#else
do {
return Decimal(usdStringInternal)
return Decimal(currencyValueStringInternal)
} catch {
return nil
}