From 947f8bf81aed9e89a31cccbf8d3392d7fc86e20c Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Thu, 10 Sep 2026 10:04:38 +0300 Subject: [PATCH] Add a "1 to Sats" field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows the inverse of the existing "1 BTC to " rate — how many Sats one unit of the default currency is worth — right below it. Useful since many currencies are worth less than, or a low multiple of, a single Sat. CurrencyConverter.satsPerCurrencyUnit(rate) does the actual division; ConverterUiState.oneCurrencyToSats() derives the display string from the already-fetched rate the same way defaultCurrencyRate() does, so no new ViewModel state was needed. Wired into both Compose and the iOS bridge/SwiftUI. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy --- iosApp/iosApp/ContentView.swift | 11 +++++++++++ .../kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt | 3 +++ .../xyz/tyiu/satsprice/domain/CurrencyConverter.kt | 4 ++++ .../xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt | 7 +++++++ .../kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt | 12 ++++++++++++ .../src/commonMain/moko-resources/base/strings.xml | 1 + .../tyiu/satsprice/domain/CurrencyConverterTest.kt | 11 +++++++++++ 7 files changed, 49 insertions(+) diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 50d2153..53ed6eb 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -105,6 +105,17 @@ struct ContentView: View { } } } + + if !state.oneCurrencyToSats.isEmpty { + HStack { + Text(IosLocalizationKt.localizedFormattedString( + resource: MR.strings.shared.currency_to_sats, + args: [state.defaultCurrencyCode] + )) + Spacer() + Text(NumberFormatKt.groupDigits(value: state.oneCurrencyToSats)) + } + } } if let error = state.errorMessage { diff --git a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt index e3eccd4..13e7a31 100644 --- a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt +++ b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt @@ -9,6 +9,7 @@ import xyz.tyiu.satsprice.ui.PriceViewModel import xyz.tyiu.satsprice.ui.currentCurrency import xyz.tyiu.satsprice.ui.defaultCurrencyRate import xyz.tyiu.satsprice.ui.exceedsMaxSupply +import xyz.tyiu.satsprice.ui.oneCurrencyToSats import xyz.tyiu.satsprice.ui.selectedOtherCurrencies import xyz.tyiu.satsprice.ui.statusLine import xyz.tyiu.satsprice.ui.unselectedCurrencies @@ -34,6 +35,7 @@ data class IosConverterState( val errorMessage: String?, val statusLine: String, val defaultCurrencyRate: String, + val oneCurrencyToSats: String, ) /** @@ -85,4 +87,5 @@ private fun ConverterUiState.toIosState(): IosConverterState = IosConverterState errorMessage = errorMessage, statusLine = statusLine(), defaultCurrencyRate = defaultCurrencyRate(), + oneCurrencyToSats = oneCurrencyToSats(), ) diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt index 2166c11..c3e0ec4 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt @@ -44,4 +44,8 @@ object CurrencyConverter { fun fiatToSats(fiat: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? = fiatToBtc(fiat, rates, currency)?.let { btc -> btcToSats(btc) } + + /** Sats worth 1 unit of a currency whose "1 BTC = ?" rate is [rate], or null if [rate] is zero. */ + fun satsPerCurrencyUnit(rate: BigDecimal): BigDecimal? = + if (rate.isZero()) null else SATS_PER_BTC.divide(rate, DIVISION_MODE) } diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt index fcc0194..0f503ad 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt @@ -4,6 +4,7 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import xyz.tyiu.satsprice.CurrencyInfo import xyz.tyiu.satsprice.domain.CurrencyConverter +import xyz.tyiu.satsprice.domain.formatAmount import xyz.tyiu.satsprice.domain.toBigDecimalOrNull import kotlin.time.Instant @@ -25,6 +26,12 @@ fun ConverterUiState.exceedsMaxSupply(): Boolean { fun ConverterUiState.defaultCurrencyRate(): String = rateDisplays[defaultCurrencyCode]?.takeIf { it.isNotEmpty() } ?: "" +/** The current "1 [ConverterUiState.defaultCurrencyCode] = ? Sats" rate, as a plain number. */ +fun ConverterUiState.oneCurrencyToSats(): String { + val rate = rateDisplays[defaultCurrencyCode]?.toBigDecimalOrNull() ?: return "" + return CurrencyConverter.satsPerCurrencyUnit(rate)?.let { formatAmount(it, 2) } ?: "" +} + /** The pinned, non-removable "Current Currency" shown in the currency picker's own section. */ fun ConverterUiState.currentCurrency(): CurrencyInfo = availableFiatCurrencies.find { it.code == defaultCurrencyCode } ?: CurrencyInfo(defaultCurrencyCode, defaultCurrencyCode) diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt index dc411e1..d68e1d7 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt @@ -213,6 +213,18 @@ fun PriceScreen( } } } + + val oneCurrencyToSats = state.oneCurrencyToSats() + if (oneCurrencyToSats.isNotEmpty()) { + Text( + text = stringResource(MR.strings.currency_to_sats, state.defaultCurrencyCode), + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = groupDigits(oneCurrencyToSats), + style = MaterialTheme.typography.headlineSmall, + ) + } } } diff --git a/shared/src/commonMain/moko-resources/base/strings.xml b/shared/src/commonMain/moko-resources/base/strings.xml index 85998ae..fdbb31f 100644 --- a/shared/src/commonMain/moko-resources/base/strings.xml +++ b/shared/src/commonMain/moko-resources/base/strings.xml @@ -11,6 +11,7 @@ %1$s - %2$s %1$s - %2$s Options for %1$s + 1 %1$s to Sats Done Exceeds the %1$s maximum supply Move down diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt index 6e49a92..4cce7c6 100644 --- a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt @@ -57,6 +57,17 @@ class CurrencyConverterTest { assertEquals(BigDecimal.fromLong(100_000_000), sats) } + @Test + fun satsPerCurrencyUnit_isInverseOfTheBtcRate() { + // 1 BTC = 50,000 USD, so 1 USD is worth 100,000,000 / 50,000 = 2,000 Sats. + assertEquals(BigDecimal.fromLong(2_000), CurrencyConverter.satsPerCurrencyUnit(BigDecimal.fromLong(50_000))) + } + + @Test + fun satsPerCurrencyUnit_returnsNullForZeroRate() { + assertNull(CurrencyConverter.satsPerCurrencyUnit(BigDecimal.fromLong(0))) + } + @Test fun handlesAmountsFarBeyondDoubleOrLongRange() { // 10^30 BTC: overflows both Double's exact-integer range and Long.MAX_VALUE.