From bf91abdd125592e2f394f8c33d20ea10070bc6d0 Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Thu, 10 Sep 2026 10:21:26 +0300 Subject: [PATCH] Keep "currency to Sats" empty when the BTC rate is, and whole-Sats only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clearing the Manual rate cleared pricedCurrencyCodes but left rateDisplays stale, so "currency to Sats" kept showing a value computed from the old rate even after "BTC to currency" went empty. refresh()'s early-return path now clears rateDisplays too. oneCurrencyToSats() also now derives from defaultCurrencyRate() directly instead of re-reading rateDisplays independently, so the two rates structurally can't disagree about whether a rate is known, and formats to a whole number of Sats (0 decimals) instead of 2 — fractional Sats aren't a meaningful unit to show. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy --- .../satsprice/ui/ConverterUiStateDisplay.kt | 10 ++++-- .../xyz/tyiu/satsprice/ui/PriceViewModel.kt | 10 ++++-- .../ui/ConverterUiStateDisplayTest.kt | 36 +++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 shared/src/commonTest/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplayTest.kt 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 0f503ad..fed75be 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt @@ -26,10 +26,14 @@ fun ConverterUiState.exceedsMaxSupply(): Boolean { fun ConverterUiState.defaultCurrencyRate(): String = rateDisplays[defaultCurrencyCode]?.takeIf { it.isNotEmpty() } ?: "" -/** The current "1 [ConverterUiState.defaultCurrencyCode] = ? Sats" rate, as a plain number. */ +/** + * The current "1 [ConverterUiState.defaultCurrencyCode] = ? Sats" rate, as a whole (never + * fractional) number of Sats — empty whenever [defaultCurrencyRate] is, so the two rates never + * disagree about whether a rate is currently known. + */ fun ConverterUiState.oneCurrencyToSats(): String { - val rate = rateDisplays[defaultCurrencyCode]?.toBigDecimalOrNull() ?: return "" - return CurrencyConverter.satsPerCurrencyUnit(rate)?.let { formatAmount(it, 2) } ?: "" + val rate = defaultCurrencyRate().toBigDecimalOrNull() ?: return "" + return CurrencyConverter.satsPerCurrencyUnit(rate)?.let { formatAmount(it, 0) } ?: "" } /** The pinned, non-removable "Current Currency" shown in the currency picker's own section. */ diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt index 9253e16..b0fd052 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt @@ -143,10 +143,16 @@ class PriceViewModel( fun refresh() { if (currentSource === manualSource && manualSource.rate == null) { // No rate typed in (yet): nothing to price fiat currencies with, so clear any stale - // rate this might otherwise still compute fiat amounts from. + // rate this might otherwise still compute fiat amounts (or the derived "currency to + // Sats" rate) from. rates = null _uiState.update { - it.copy(isLoading = false, errorMessage = null, pricedCurrencyCodes = emptySet()) + it.copy( + isLoading = false, + errorMessage = null, + pricedCurrencyCodes = emptySet(), + rateDisplays = emptyMap(), + ) } return } diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplayTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplayTest.kt new file mode 100644 index 0000000..3c22918 --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplayTest.kt @@ -0,0 +1,36 @@ +package xyz.tyiu.satsprice.ui + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConverterUiStateDisplayTest { + + @Test + fun oneCurrencyToSats_isInverseOfTheBtcRate() { + val state = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = mapOf("USD" to "50000.00")) + + // 1 BTC = 50,000 USD, so 1 USD is worth 100,000,000 / 50,000 = 2,000 Sats. + assertEquals("2000", state.oneCurrencyToSats()) + } + + @Test + fun oneCurrencyToSats_roundsToAWholeNumberOfSats() { + // 1 BTC = 78,293.19 USD, so 1 USD is worth ~1,277.24 Sats — never shown fractionally. + val state = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = mapOf("USD" to "78293.19")) + + assertEquals("1277", state.oneCurrencyToSats()) + } + + @Test + fun oneCurrencyToSats_isEmptyWheneverDefaultCurrencyRateIs() { + // No rate for the default currency at all (e.g. Manual with nothing typed in yet). + val noRate = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = emptyMap()) + assertEquals("", noRate.defaultCurrencyRate()) + assertEquals("", noRate.oneCurrencyToSats()) + + // A rate is known, just not for the default currency. + val otherCurrencyOnly = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = mapOf("EUR" to "45000")) + assertEquals("", otherCurrencyOnly.defaultCurrencyRate()) + assertEquals("", otherCurrencyOnly.oneCurrencyToSats()) + } +}