Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -112,14 +119,15 @@ internal fun formatBitcoinPlaceholder(
return formatMissingDecimalZeros(
value = normalizedBtcValue,
maxDecimalPlaces = CLASSIC_DECIMALS,
includeDecimalSeparatorIfMissing = true,
)
}

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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = '.'
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,6 +46,8 @@ fun CalculatorPreviewScreen(
uiState = uiState,
onBtcChange = viewModel::onBtcInputChanged,
onFiatChange = viewModel::onFiatInputChanged,
onInputSelected = viewModel::onInputSelected,
onInputDismissed = viewModel::onInputDismissed,
onClickDelete = {
viewModel.removeWidget()
onClose()
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -43,6 +46,8 @@ class CalculatorViewModel @Inject constructor(
val uiState: StateFlow<CalculatorUiState> = _uiState.asStateFlow()
private var pendingValues: CalculatorValues? = null
private var lastCurrencyKey: CalculatorCurrencyKey? = null
private var lastRates: ImmutableList<FxRate>? = null
private var activeInput: MoneyType? = null

val isCalculatorWidgetEnabled: StateFlow<Boolean> = widgetsRepo.widgetsDataFlow
.map { widgetsData ->
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -152,56 +167,101 @@ 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,
isInitialSync = isInitialSync,
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,
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading