Add a "1 <currency> to Sats" field
Shows the inverse of the existing "1 BTC to <currency>" 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
@@ -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 {
|
if let error = state.errorMessage {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import xyz.tyiu.satsprice.ui.PriceViewModel
|
|||||||
import xyz.tyiu.satsprice.ui.currentCurrency
|
import xyz.tyiu.satsprice.ui.currentCurrency
|
||||||
import xyz.tyiu.satsprice.ui.defaultCurrencyRate
|
import xyz.tyiu.satsprice.ui.defaultCurrencyRate
|
||||||
import xyz.tyiu.satsprice.ui.exceedsMaxSupply
|
import xyz.tyiu.satsprice.ui.exceedsMaxSupply
|
||||||
|
import xyz.tyiu.satsprice.ui.oneCurrencyToSats
|
||||||
import xyz.tyiu.satsprice.ui.selectedOtherCurrencies
|
import xyz.tyiu.satsprice.ui.selectedOtherCurrencies
|
||||||
import xyz.tyiu.satsprice.ui.statusLine
|
import xyz.tyiu.satsprice.ui.statusLine
|
||||||
import xyz.tyiu.satsprice.ui.unselectedCurrencies
|
import xyz.tyiu.satsprice.ui.unselectedCurrencies
|
||||||
@@ -34,6 +35,7 @@ data class IosConverterState(
|
|||||||
val errorMessage: String?,
|
val errorMessage: String?,
|
||||||
val statusLine: String,
|
val statusLine: String,
|
||||||
val defaultCurrencyRate: String,
|
val defaultCurrencyRate: String,
|
||||||
|
val oneCurrencyToSats: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,4 +87,5 @@ private fun ConverterUiState.toIosState(): IosConverterState = IosConverterState
|
|||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
statusLine = statusLine(),
|
statusLine = statusLine(),
|
||||||
defaultCurrencyRate = defaultCurrencyRate(),
|
defaultCurrencyRate = defaultCurrencyRate(),
|
||||||
|
oneCurrencyToSats = oneCurrencyToSats(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,4 +44,8 @@ object CurrencyConverter {
|
|||||||
|
|
||||||
fun fiatToSats(fiat: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? =
|
fun fiatToSats(fiat: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? =
|
||||||
fiatToBtc(fiat, rates, currency)?.let { btc -> btcToSats(btc) }
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import kotlinx.datetime.TimeZone
|
|||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import xyz.tyiu.satsprice.CurrencyInfo
|
import xyz.tyiu.satsprice.CurrencyInfo
|
||||||
import xyz.tyiu.satsprice.domain.CurrencyConverter
|
import xyz.tyiu.satsprice.domain.CurrencyConverter
|
||||||
|
import xyz.tyiu.satsprice.domain.formatAmount
|
||||||
import xyz.tyiu.satsprice.domain.toBigDecimalOrNull
|
import xyz.tyiu.satsprice.domain.toBigDecimalOrNull
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@@ -25,6 +26,12 @@ fun ConverterUiState.exceedsMaxSupply(): Boolean {
|
|||||||
fun ConverterUiState.defaultCurrencyRate(): String =
|
fun ConverterUiState.defaultCurrencyRate(): String =
|
||||||
rateDisplays[defaultCurrencyCode]?.takeIf { it.isNotEmpty() } ?: ""
|
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. */
|
/** The pinned, non-removable "Current Currency" shown in the currency picker's own section. */
|
||||||
fun ConverterUiState.currentCurrency(): CurrencyInfo =
|
fun ConverterUiState.currentCurrency(): CurrencyInfo =
|
||||||
availableFiatCurrencies.find { it.code == defaultCurrencyCode } ?: CurrencyInfo(defaultCurrencyCode, defaultCurrencyCode)
|
availableFiatCurrencies.find { it.code == defaultCurrencyCode } ?: CurrencyInfo(defaultCurrencyCode, defaultCurrencyCode)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<string name="currency_option_label">%1$s - %2$s</string>
|
<string name="currency_option_label">%1$s - %2$s</string>
|
||||||
<string name="currency_option_label_local">%1$s - %2$s</string>
|
<string name="currency_option_label_local">%1$s - %2$s</string>
|
||||||
<string name="currency_options_content_description">Options for %1$s</string>
|
<string name="currency_options_content_description">Options for %1$s</string>
|
||||||
|
<string name="currency_to_sats">1 %1$s to Sats</string>
|
||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
<string name="exceeds_max_supply">Exceeds the %1$s maximum supply</string>
|
<string name="exceeds_max_supply">Exceeds the %1$s maximum supply</string>
|
||||||
<string name="move_currency_down_content_description">Move down</string>
|
<string name="move_currency_down_content_description">Move down</string>
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ class CurrencyConverterTest {
|
|||||||
assertEquals(BigDecimal.fromLong(100_000_000), sats)
|
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
|
@Test
|
||||||
fun handlesAmountsFarBeyondDoubleOrLongRange() {
|
fun handlesAmountsFarBeyondDoubleOrLongRange() {
|
||||||
// 10^30 BTC: overflows both Double's exact-integer range and Long.MAX_VALUE.
|
// 10^30 BTC: overflows both Double's exact-integer range and Long.MAX_VALUE.
|
||||||
|
|||||||
Reference in New Issue
Block a user