diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorDisplay.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorDisplay.kt index c56a191fb..c6f4a36db 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorDisplay.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorDisplay.kt @@ -100,7 +100,14 @@ internal fun formatBitcoinPlaceholder( displayUnit: BitcoinDisplayUnit, locale: Locale = Locale.getDefault(), ): String { - if (btcValue.isEmpty() || displayUnit.isModern()) return "" + if (btcValue.isEmpty()) { + return if (displayUnit.isModern()) { + ZERO_PLACEHOLDER + } else { + ZERO_PLACEHOLDER + DECIMAL_SEPARATOR + "0".repeat(CLASSIC_DECIMALS) + } + } + if (displayUnit.isModern()) return "" val normalizedBtcValue = sanitizeDecimalInput( raw = normalizeCalculatorDecimalInput( rawValue = btcValue, @@ -112,6 +119,7 @@ internal fun formatBitcoinPlaceholder( return formatMissingDecimalZeros( value = normalizedBtcValue, maxDecimalPlaces = CLASSIC_DECIMALS, + includeDecimalSeparatorIfMissing = true, ) } @@ -119,7 +127,7 @@ internal fun formatFiatPlaceholder( fiatValue: String, locale: Locale = Locale.getDefault(), ): String { - if (fiatValue.isEmpty()) return "" + if (fiatValue.isEmpty()) return ZERO_PLACEHOLDER val normalizedFiatValue = sanitizeDecimalInput( raw = normalizeCalculatorDecimalInput( rawValue = fiatValue, @@ -268,8 +276,15 @@ private fun formatGroupedIntegerPreservingZeros( private fun formatMissingDecimalZeros( value: String, maxDecimalPlaces: Int, + includeDecimalSeparatorIfMissing: Boolean = false, ): String { - if (!value.contains(PERIOD_SEPARATOR)) return "" + if (!value.contains(PERIOD_SEPARATOR)) { + return if (includeDecimalSeparatorIfMissing) { + DECIMAL_SEPARATOR + "0".repeat(maxDecimalPlaces) + } else { + "" + } + } val decimalLength = value.substringAfter(PERIOD_SEPARATOR).length val remainingDecimals = maxDecimalPlaces - decimalLength @@ -298,5 +313,6 @@ private fun BigDecimal.toSatsLongClamped(): Long { private val MAX_SATS_DECIMAL = BigDecimal.valueOf(Long.MAX_VALUE) private const val GROUP_SIZE = 3 +private const val ZERO_PLACEHOLDER = "0" private const val COMMA_SEPARATOR = ',' private const val PERIOD_SEPARATOR = '.' diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt index 272912315..116a96f0f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorPreviewScreen.kt @@ -17,6 +17,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.BitcoinDisplayUnit +import to.bitkit.models.MoneyType import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton @@ -45,6 +46,8 @@ fun CalculatorPreviewScreen( uiState = uiState, onBtcChange = viewModel::onBtcInputChanged, onFiatChange = viewModel::onFiatInputChanged, + onInputSelected = viewModel::onInputSelected, + onInputDismissed = viewModel::onInputDismissed, onClickDelete = { viewModel.removeWidget() onClose() @@ -67,6 +70,8 @@ fun CalculatorPreviewContent( uiState: CalculatorUiState = CalculatorUiState(), onBtcChange: (String) -> Unit = {}, onFiatChange: (String) -> Unit = {}, + onInputSelected: (MoneyType) -> Unit = {}, + onInputDismissed: () -> Unit = {}, ) { ScreenColumn( modifier = modifier.testTag("calculator_preview_screen") @@ -117,6 +122,8 @@ fun CalculatorPreviewContent( fiatName = uiState.selectedCurrency, fiatValue = uiState.fiatValue, onFiatChange = onFiatChange, + onInputSelected = onInputSelected, + onInputDismissed = onInputDismissed, modifier = Modifier .fillMaxWidth() .testTag("calculator_card_wide") diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModel.kt index 051f64b9c..d48db22f9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -15,6 +16,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.models.BitcoinDisplayUnit +import to.bitkit.models.FxRate +import to.bitkit.models.MoneyType import to.bitkit.models.WidgetType import to.bitkit.models.widget.CalculatorValues import to.bitkit.models.widget.resolveCalculatorSatsValue @@ -43,6 +46,8 @@ class CalculatorViewModel @Inject constructor( val uiState: StateFlow = _uiState.asStateFlow() private var pendingValues: CalculatorValues? = null private var lastCurrencyKey: CalculatorCurrencyKey? = null + private var lastRates: ImmutableList? = null + private var activeInput: MoneyType? = null val isCalculatorWidgetEnabled: StateFlow = widgetsRepo.widgetsDataFlow .map { widgetsData -> @@ -70,7 +75,16 @@ class CalculatorViewModel @Inject constructor( } } + fun onInputSelected(input: MoneyType) { + activeInput = input + } + + fun onInputDismissed() { + activeInput = null + } + fun onBtcInputChanged(rawValue: String) { + activeInput = MoneyType.BITCOIN val displayUnit = _uiState.value.displayUnit val btcValue = if (displayUnit.isModern()) { sanitizeIntegerInput(rawValue) @@ -98,6 +112,7 @@ class CalculatorViewModel @Inject constructor( } fun onFiatInputChanged(rawValue: String) { + activeInput = MoneyType.FIAT val displayUnit = _uiState.value.displayUnit val fiatValue = sanitizeDecimalInput(rawValue, maxDecimalPlaces = CALCULATOR_FIAT_DECIMAL_PLACES) val satsValue = if (fiatValue.isEmpty()) 0L else convertFiatToSats(fiatValue) @@ -152,11 +167,15 @@ class CalculatorViewModel @Inject constructor( displayUnit = currencyState.displayUnit, ) val previousCurrencyKey = lastCurrencyKey + val previousRates = lastRates lastCurrencyKey = currencyKey + lastRates = currencyState.rates - val currencyChanged = previousCurrencyKey != null && previousCurrencyKey != currencyKey + val currencyChanged = previousCurrencyKey != null && + previousCurrencyKey.selectedCurrency != currencyKey.selectedCurrency val displayUnitChanged = previousCurrencyKey != null && previousCurrencyKey.displayUnit != currencyKey.displayUnit + val ratesChanged = previousRates != null && previousRates != currencyState.rates val isInitialSync = previousCurrencyKey == null val nextActiveValues = deriveActiveValues( activeValues = activeValues, @@ -164,44 +183,85 @@ class CalculatorViewModel @Inject constructor( displayUnitChanged = displayUnitChanged, currencyKey = currencyKey, ) - val shouldRefreshFiat = isInitialSync || currencyChanged || shouldHydrateFiatFromStoredBtc( + val refreshSource = nextActiveValues.refreshSource(activeInput) + if (refreshSource == MoneyType.FIAT) { + return refreshBitcoinFromFiat( + activeValues = activeValues, + nextActiveValues = nextActiveValues, + displayUnit = currencyState.displayUnit, + ) + } + + val shouldRefreshFiat = isInitialSync || currencyChanged || ratesChanged || shouldHydrateFiatFromStoredBtc( storedBtcValue = storedValues.btcValue, storedFiatValue = storedValues.fiatValue, currentFiatValue = nextActiveValues.fiatValue, displayUnit = currencyState.displayUnit, ) - if (!shouldRefreshFiat) { - persistCanonicalValuesIfNeeded( - activeValues = activeValues, - nextActiveValues = nextActiveValues, - ) - return nextActiveValues - } + if (!shouldRefreshFiat) return persistCanonicalValues(activeValues, nextActiveValues) + + return refreshFiatFromBitcoin( + activeValues = activeValues, + nextActiveValues = nextActiveValues, + displayUnit = currencyState.displayUnit, + ) + } + + private fun refreshFiatFromBitcoin( + activeValues: CalculatorValues, + nextActiveValues: CalculatorValues, + displayUnit: BitcoinDisplayUnit, + ): CalculatorValues { if (nextActiveValues.btcValue.isEmpty() || - isZeroBtcValue(nextActiveValues.btcValue, currencyState.displayUnit) + isZeroBtcValue(nextActiveValues.btcValue, displayUnit) ) { - persistCanonicalValuesIfNeeded( - activeValues = activeValues, - nextActiveValues = nextActiveValues, - ) - return nextActiveValues + return persistCanonicalValues(activeValues, nextActiveValues) } val convertedFiat = convertSatsToFiat(nextActiveValues.resolveCalculatorSatsValue()) - if (convertedFiat.isEmpty()) { - persistCanonicalValuesIfNeeded( - activeValues = activeValues, - nextActiveValues = nextActiveValues, - ) - return nextActiveValues - } + if (convertedFiat.isEmpty()) return persistCanonicalValues(activeValues, nextActiveValues) val updatedValues = nextActiveValues.copy(fiatValue = convertedFiat) - updateCalculatorValues(updatedValues) + persistCanonicalValuesIfNeeded( + activeValues = activeValues, + nextActiveValues = updatedValues, + ) return updatedValues } + private fun refreshBitcoinFromFiat( + activeValues: CalculatorValues, + nextActiveValues: CalculatorValues, + displayUnit: BitcoinDisplayUnit, + ): CalculatorValues { + if (nextActiveValues.fiatValue.isEmpty()) return persistCanonicalValues(activeValues, nextActiveValues) + + val satsValue = convertFiatToSatsOrNull(nextActiveValues.fiatValue) + ?: return persistCanonicalValues(activeValues, nextActiveValues) + val updatedValues = nextActiveValues.copy( + btcValue = calculatorSatsToBtcValue(satsValue, displayUnit), + satsValue = satsValue, + displayUnit = displayUnit, + ) + persistCanonicalValuesIfNeeded( + activeValues = activeValues, + nextActiveValues = updatedValues, + ) + return updatedValues + } + + private fun persistCanonicalValues( + activeValues: CalculatorValues, + nextActiveValues: CalculatorValues, + ): CalculatorValues { + persistCanonicalValuesIfNeeded( + activeValues = activeValues, + nextActiveValues = nextActiveValues, + ) + return nextActiveValues + } + private fun deriveActiveValues( activeValues: CalculatorValues, isInitialSync: Boolean, @@ -302,8 +362,14 @@ class CalculatorViewModel @Inject constructor( } private fun convertFiatToSats(fiatValue: String): Long { - val fiatDecimal = fiatValue.toBigDecimalOrNull() ?: BigDecimal.ZERO - return currencyRepo.convertFiatToSats(fiatDecimal).getOrNull()?.toLong() ?: 0L + return convertFiatToSatsOrNull(fiatValue) ?: 0L + } + + private fun convertFiatToSatsOrNull(fiatValue: String): Long? { + val fiatDecimal = fiatValue.toBigDecimalOrNull() ?: return null + return runCatching { + currencyRepo.convertFiatToSats(fiatDecimal).getOrNull()?.toLong() + }.getOrNull() } } @@ -339,6 +405,15 @@ internal fun shouldHydrateFiatFromStoredBtc( return currentFiatValue.isEmpty() } +internal fun CalculatorValues.refreshSource(activeInput: MoneyType?): MoneyType { + activeInput?.let { return it } + return if (btcValue.isEmpty() && fiatValue.isNotEmpty()) { + MoneyType.FIAT + } else { + MoneyType.BITCOIN + } +} + internal fun isZeroBtcValue( btcValue: String, displayUnit: BitcoinDisplayUnit, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt index f785412d7..77ed429de 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCard.kt @@ -89,6 +89,8 @@ fun CalculatorCard( onFiatChange = calculatorViewModel::onFiatInputChanged, dismissNumberPadKey = dismissNumberPadKey, onInputActiveChange = onInputActiveChange, + onInputSelected = calculatorViewModel::onInputSelected, + onInputDismissed = calculatorViewModel::onInputDismissed, onNumberPadBoundsChanged = onNumberPadBoundsChanged, ) } @@ -103,11 +105,17 @@ fun CalculatorCardEditor( fiatName: String, fiatValue: String, onFiatChange: (String) -> Unit, + onInputSelected: (MoneyType) -> Unit, + onInputDismissed: () -> Unit, dismissNumberPadKey: Int = 0, onInputActiveChange: (Boolean) -> Unit = {}, onNumberPadBoundsChanged: (Rect?) -> Unit = {}, ) { val numpadState = rememberNumpadState() + val selectInput = { input: MoneyType -> + onInputSelected(input) + numpadState.selectInput(input) + } Column(modifier = modifier) { Content( @@ -117,7 +125,7 @@ fun CalculatorCardEditor( fiatName = fiatName, fiatValue = fiatValue, activeInput = numpadState.activeInput, - onSelectInput = numpadState::selectInput, + onSelectInput = selectInput, modifier = Modifier.fillMaxWidth(), ) @@ -130,6 +138,7 @@ fun CalculatorCardEditor( fiatValue = fiatValue, onBtcChange = onBtcChange, onFiatChange = onFiatChange, + onInputDismissed = onInputDismissed, onNumberPadBoundsChanged = onNumberPadBoundsChanged, ) } @@ -201,12 +210,14 @@ private fun ColumnScope.NumpadHost( fiatValue: String, onBtcChange: (String) -> Unit, onFiatChange: (String) -> Unit, + onInputDismissed: () -> Unit, onNumberPadBoundsChanged: (Rect?) -> Unit, ) { NumpadEffects( state = state, dismissNumberPadKey = dismissNumberPadKey, onInputActiveChange = onInputActiveChange, + onInputDismissed = onInputDismissed, onNumberPadBoundsChanged = onNumberPadBoundsChanged, ) @@ -226,9 +237,11 @@ private fun NumpadEffects( state: NumpadState, dismissNumberPadKey: Int, onInputActiveChange: (Boolean) -> Unit, + onInputDismissed: () -> Unit, onNumberPadBoundsChanged: (Rect?) -> Unit, ) { val updatedOnInputActiveChange by rememberUpdatedState(onInputActiveChange) + val updatedOnInputDismissed by rememberUpdatedState(onInputDismissed) val updatedOnNumberPadBoundsChanged by rememberUpdatedState(onNumberPadBoundsChanged) val isInputTargetActive = state.visibilityState.targetState @@ -237,6 +250,7 @@ private fun NumpadEffects( LaunchedEffect(isInputTargetActive) { updatedOnInputActiveChange(isInputTargetActive) if (!isInputTargetActive) { + updatedOnInputDismissed() updatedOnNumberPadBoundsChanged(null) } } @@ -248,7 +262,10 @@ private fun NumpadEffects( } DisposableEffect(Unit) { - onDispose { updatedOnInputActiveChange(false) } + onDispose { + updatedOnInputDismissed() + updatedOnInputActiveChange(false) + } } } @@ -509,6 +526,8 @@ private fun Preview() { fiatValue = "4.55", fiatName = "USD", onFiatChange = {}, + onInputSelected = {}, + onInputDismissed = {}, btcPrimaryDisplayUnit = BitcoinDisplayUnit.MODERN, modifier = Modifier.fillMaxWidth() ) @@ -520,6 +539,8 @@ private fun Preview() { fiatValue = "4.55", fiatName = "USD", onFiatChange = {}, + onInputSelected = {}, + onInputDismissed = {}, btcPrimaryDisplayUnit = BitcoinDisplayUnit.CLASSIC, modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModelTest.kt index 618ddfd69..5d53c98e8 100644 --- a/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/CalculatorViewModelTest.kt @@ -1,5 +1,6 @@ package to.bitkit.ui.screens.widgets.calculator +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.advanceUntilIdle @@ -12,11 +13,14 @@ import org.mockito.kotlin.whenever import to.bitkit.data.WidgetsData import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.ConvertedAmount +import to.bitkit.models.FxRate +import to.bitkit.models.MoneyType import to.bitkit.models.widget.CalculatorValues import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.CurrencyState import to.bitkit.repositories.WidgetsRepo import to.bitkit.test.BaseUnitTest +import to.bitkit.utils.ServiceError import java.math.BigDecimal import java.util.Locale import kotlin.test.assertEquals @@ -31,7 +35,8 @@ class CalculatorViewModelTest : BaseUnitTest() { private var lastConvertedSats = 0L private var fiatConversionValue: String? = null private var fiatConversionFormatted: String? = null - private var fiatToSatsValue = 12_345uL + private var fiatToSatsValue: ULong? = 12_345uL + private var updateCalculatorValuesCalls = 0 private lateinit var sut: CalculatorViewModel @@ -44,6 +49,7 @@ class CalculatorViewModelTest : BaseUnitTest() { fiatConversionValue = null fiatConversionFormatted = null fiatToSatsValue = 12_345uL + updateCalculatorValuesCalls = 0 whenever(widgetsRepo.widgetsDataFlow).thenReturn(widgetsData) whenever(currencyRepo.currencyState).thenReturn(currencyState) @@ -60,8 +66,11 @@ class CalculatorViewModelTest : BaseUnitTest() { sats = sats, ) } - whenever(currencyRepo.convertFiatToSats(any(), anyOrNull())).thenAnswer { fiatToSatsValue } + whenever(currencyRepo.convertFiatToSats(any(), anyOrNull())).thenAnswer { + fiatToSatsValue ?: throw ServiceError.CurrencyRateUnavailable() + } whenever { widgetsRepo.updateCalculatorValues(any()) }.thenAnswer { + updateCalculatorValuesCalls++ val calculatorValues = it.getArgument(0) widgetsData.value = widgetsData.value.copy(calculatorValues = calculatorValues) Unit @@ -267,6 +276,110 @@ class CalculatorViewModelTest : BaseUnitTest() { assertEquals(BitcoinDisplayUnit.MODERN, widgetsData.value.calculatorValues.displayUnit) } + @Test + fun `rate refresh preserves bitcoin input as source`() = test { + sut = createSut() + advanceUntilIdle() + + sut.onBtcInputChanged("10000") + advanceUntilIdle() + + fiatConversionValue = "7.50" + currencyState.value = currencyState.value.copy( + rates = persistentListOf(fxRate(lastPrice = "75000")), + ) + advanceUntilIdle() + + assertEquals("10000", sut.uiState.value.btcValue) + assertEquals("7.50", sut.uiState.value.fiatValue) + assertEquals(10_000L, widgetsData.value.calculatorValues.satsValue) + assertEquals("7.50", widgetsData.value.calculatorValues.fiatValue) + } + + @Test + fun `rate refresh skips persist when fiat display value is unchanged`() = test { + sut = createSut() + advanceUntilIdle() + + sut.onBtcInputChanged("10000") + advanceUntilIdle() + + val updatesBeforeRateRefresh = updateCalculatorValuesCalls + currencyState.value = currencyState.value.copy( + rates = persistentListOf(fxRate(lastPrice = "62501")), + ) + advanceUntilIdle() + + assertEquals("10000", sut.uiState.value.btcValue) + assertEquals("6.25", sut.uiState.value.fiatValue) + assertEquals(updatesBeforeRateRefresh, updateCalculatorValuesCalls) + } + + @Test + fun `currency change preserves fiat input as source`() = test { + sut = createSut() + advanceUntilIdle() + + sut.onFiatInputChanged("12.34") + advanceUntilIdle() + + fiatToSatsValue = 54_321uL + currencyState.value = CurrencyState( + selectedCurrency = "EUR", + currencySymbol = "EUR", + displayUnit = BitcoinDisplayUnit.MODERN, + rates = persistentListOf(fxRate(quote = "EUR", currencySymbol = "EUR")), + ) + advanceUntilIdle() + + assertEquals("54321", sut.uiState.value.btcValue) + assertEquals("12.34", sut.uiState.value.fiatValue) + assertEquals(54_321L, widgetsData.value.calculatorValues.satsValue) + assertEquals("12.34", widgetsData.value.calculatorValues.fiatValue) + } + + @Test + fun `active fiat refresh preserves bitcoin when conversion is unavailable`() = test { + sut = createSut() + advanceUntilIdle() + + sut.onFiatInputChanged("12.34") + advanceUntilIdle() + + fiatToSatsValue = null + val updatesBeforeRateRefresh = updateCalculatorValuesCalls + currencyState.value = currencyState.value.copy( + rates = persistentListOf(fxRate(lastPrice = "62501")), + ) + advanceUntilIdle() + + assertEquals("12345", sut.uiState.value.btcValue) + assertEquals("12.34", sut.uiState.value.fiatValue) + assertEquals(12_345L, widgetsData.value.calculatorValues.satsValue) + assertEquals(updatesBeforeRateRefresh, updateCalculatorValuesCalls) + } + + @Test + fun `active empty fiat refresh preserves bitcoin value`() = test { + sut = createSut() + advanceUntilIdle() + + sut.onInputSelected(MoneyType.FIAT) + widgetsData.value = WidgetsData( + calculatorValues = CalculatorValues( + btcValue = "10000", + fiatValue = "", + satsValue = 10_000L, + displayUnit = BitcoinDisplayUnit.MODERN, + ) + ) + advanceUntilIdle() + + assertEquals("10000", sut.uiState.value.btcValue) + assertEquals("", sut.uiState.value.fiatValue) + assertEquals(10_000L, widgetsData.value.calculatorValues.satsValue) + } + @Test fun `display unit change preserves btc amount`() = test { widgetsData.value = WidgetsData( @@ -379,4 +492,20 @@ class CalculatorViewModelTest : BaseUnitTest() { "EUR" -> "5.50" else -> "6.25" } + + private fun fxRate( + quote: String = "USD", + currencySymbol: String = "$", + lastPrice: String = "62500", + ) = FxRate( + symbol = "BTC$quote", + lastPrice = lastPrice, + base = "BTC", + baseName = "Bitcoin", + quote = quote, + quoteName = quote, + currencySymbol = currencySymbol, + currencyFlag = "", + lastUpdatedAt = 1L, + ) } diff --git a/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCardStateTest.kt b/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCardStateTest.kt index caa0712ba..4cef7df3e 100644 --- a/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCardStateTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/widgets/calculator/components/CalculatorCardStateTest.kt @@ -3,6 +3,7 @@ package to.bitkit.ui.screens.widgets.calculator.components import org.junit.Before import org.junit.Test import to.bitkit.models.BitcoinDisplayUnit +import to.bitkit.models.MoneyType import to.bitkit.models.widget.CalculatorValues import to.bitkit.models.widget.resolveCalculatorSatsValue import to.bitkit.ui.components.KEY_000 @@ -18,6 +19,7 @@ import to.bitkit.ui.screens.widgets.calculator.formatBitcoinValue import to.bitkit.ui.screens.widgets.calculator.formatFiatPlaceholder import to.bitkit.ui.screens.widgets.calculator.formatFiatValue import to.bitkit.ui.screens.widgets.calculator.isBtcValueInSatsRange +import to.bitkit.ui.screens.widgets.calculator.refreshSource import to.bitkit.ui.screens.widgets.calculator.sanitizeDecimalInput import to.bitkit.ui.screens.widgets.calculator.sanitizeIntegerInput import to.bitkit.ui.screens.widgets.calculator.shouldHydrateFiatFromStoredBtc @@ -105,6 +107,34 @@ class CalculatorCardStateTest { assertFalse(result) } + @Test + fun `refreshSource preserves active fiat input when both values exist`() { + val values = CalculatorValues(btcValue = "10000", fiatValue = "12.34") + + assertEquals(MoneyType.FIAT, values.refreshSource(activeInput = MoneyType.FIAT)) + } + + @Test + fun `refreshSource preserves active bitcoin input when fiat-only would otherwise win`() { + val values = CalculatorValues(btcValue = "", fiatValue = "12.34") + + assertEquals(MoneyType.BITCOIN, values.refreshSource(activeInput = MoneyType.BITCOIN)) + } + + @Test + fun `refreshSource falls back to fiat for fiat-only value`() { + val values = CalculatorValues(btcValue = "", fiatValue = "12.34") + + assertEquals(MoneyType.FIAT, values.refreshSource(activeInput = null)) + } + + @Test + fun `refreshSource falls back to bitcoin when both values exist`() { + val values = CalculatorValues(btcValue = "10000", fiatValue = "12.34") + + assertEquals(MoneyType.BITCOIN, values.refreshSource(activeInput = null)) + } + @Test fun `toCalculatorDisplaySymbol trims and keeps up to two chars`() { assertEquals("$", " $ ".toCalculatorDisplaySymbol()) @@ -217,7 +247,7 @@ class CalculatorCardStateTest { @Test fun `formatFiatPlaceholder returns missing decimal zeros`() { - assertEquals("", formatFiatPlaceholder("")) + assertEquals("0", formatFiatPlaceholder("")) assertEquals("", formatFiatPlaceholder("1")) assertEquals("00", formatFiatPlaceholder("1.")) assertEquals("0", formatFiatPlaceholder("1.2")) @@ -227,8 +257,9 @@ class CalculatorCardStateTest { @Test fun `formatBitcoinPlaceholder returns missing classic decimal zeros`() { - assertEquals("", formatBitcoinPlaceholder("", BitcoinDisplayUnit.CLASSIC)) - assertEquals("", formatBitcoinPlaceholder("1", BitcoinDisplayUnit.CLASSIC)) + assertEquals("0", formatBitcoinPlaceholder("", BitcoinDisplayUnit.MODERN)) + assertEquals("0.00000000", formatBitcoinPlaceholder("", BitcoinDisplayUnit.CLASSIC)) + assertEquals(".00000000", formatBitcoinPlaceholder("1", BitcoinDisplayUnit.CLASSIC)) assertEquals("", formatBitcoinPlaceholder("1.2", BitcoinDisplayUnit.MODERN)) assertEquals("00000000", formatBitcoinPlaceholder("1.", BitcoinDisplayUnit.CLASSIC)) assertEquals("0000", formatBitcoinPlaceholder("1.2345", BitcoinDisplayUnit.CLASSIC))