@@ -15,18 +15,19 @@ import androidx.compose.foundation.layout.width
1515import androidx.compose.material3.Icon
1616import androidx.compose.material3.MaterialTheme
1717import androidx.compose.runtime.Composable
18+ import androidx.compose.runtime.LaunchedEffect
1819import androidx.compose.runtime.getValue
1920import androidx.compose.runtime.mutableStateOf
2021import androidx.compose.runtime.saveable.rememberSaveable
2122import androidx.compose.runtime.setValue
2223import androidx.compose.ui.Alignment
2324import androidx.compose.ui.Modifier
2425import androidx.compose.ui.draw.clip
25- import androidx.compose.ui.focus.onFocusChanged
2626import androidx.compose.ui.graphics.Color
2727import androidx.compose.ui.platform.testTag
2828import androidx.compose.ui.res.painterResource
2929import androidx.compose.ui.res.stringResource
30+ import androidx.compose.ui.text.input.KeyboardType
3031import androidx.compose.ui.tooling.preview.Preview
3132import androidx.compose.ui.unit.dp
3233import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -43,6 +44,9 @@ import to.bitkit.ui.utils.visualTransformation.BitcoinVisualTransformation
4344import to.bitkit.ui.utils.visualTransformation.CalculatorFormatter
4445import to.bitkit.ui.utils.visualTransformation.MonetaryVisualTransformation
4546import to.bitkit.viewmodels.CurrencyViewModel
47+ import java.math.BigDecimal
48+
49+ private const val FIAT_DECIMAL_PLACES = 2
4650
4751@Composable
4852fun CalculatorCard (
@@ -55,34 +59,104 @@ fun CalculatorCard(
5559 val calculatorValues by calculatorViewModel.calculatorValues.collectAsStateWithLifecycle()
5660 var btcValue: String by rememberSaveable { mutableStateOf(calculatorValues.btcValue) }
5761 var fiatValue: String by rememberSaveable { mutableStateOf(calculatorValues.fiatValue) }
62+ val displayedBtcValue = btcValue.ifEmpty { calculatorValues.btcValue }
63+ val displayedFiatValue = fiatValue
64+
65+ LaunchedEffect (
66+ calculatorValues.btcValue,
67+ calculatorValues.fiatValue,
68+ currencyUiState.displayUnit,
69+ currencyUiState.selectedCurrency,
70+ ) {
71+ if (! shouldHydrateFiatFromStoredBtc(
72+ storedBtcValue = calculatorValues.btcValue,
73+ storedFiatValue = calculatorValues.fiatValue,
74+ currentFiatValue = fiatValue,
75+ displayUnit = currencyUiState.displayUnit,
76+ )
77+ ) {
78+ return @LaunchedEffect
79+ }
80+ val convertedFiat = CalculatorFormatter .convertBtcToFiat(
81+ btcValue = calculatorValues.btcValue,
82+ displayUnit = currencyUiState.displayUnit,
83+ currencyViewModel = currencyViewModel,
84+ ).orEmpty()
85+ if (convertedFiat.isEmpty()) {
86+ return @LaunchedEffect
87+ }
88+ fiatValue = convertedFiat
89+ calculatorViewModel.updateCalculatorValues(
90+ fiatValue = convertedFiat,
91+ btcValue = calculatorValues.btcValue,
92+ )
93+ }
94+
95+ LaunchedEffect (currencyUiState.selectedCurrency, currencyUiState.displayUnit) {
96+ val sourceBtc = btcValue.ifEmpty { calculatorValues.btcValue }
97+ if (sourceBtc.isEmpty() || isZeroBtcValue(sourceBtc, currencyUiState.displayUnit)) {
98+ return @LaunchedEffect
99+ }
100+ val convertedFiat = CalculatorFormatter .convertBtcToFiat(
101+ btcValue = sourceBtc,
102+ displayUnit = currencyUiState.displayUnit,
103+ currencyViewModel = currencyViewModel,
104+ ).orEmpty()
105+ if (convertedFiat.isEmpty()) {
106+ return @LaunchedEffect
107+ }
108+ fiatValue = convertedFiat
109+ calculatorViewModel.updateCalculatorValues(
110+ fiatValue = convertedFiat,
111+ btcValue = sourceBtc,
112+ )
113+ }
58114
59115 CalculatorCardContent (
60116 modifier = modifier,
61117 showWidgetTitle = showWidgetTitle,
62118 btcPrimaryDisplayUnit = currencyUiState.displayUnit,
63- btcValue = btcValue.ifEmpty { calculatorValues.btcValue },
64- onBtcChange = { newValue ->
65- btcValue = newValue
66- val convertedFiat = CalculatorFormatter .convertBtcToFiat(
67- btcValue = btcValue,
68- displayUnit = currencyUiState.displayUnit,
69- currencyViewModel = currencyViewModel
70- )
71- fiatValue = convertedFiat.orEmpty()
119+ btcValue = displayedBtcValue,
120+ onBtcChange = { rawValue ->
121+ val sanitized = if (currencyUiState.displayUnit.isModern()) {
122+ sanitizeIntegerInput(rawValue)
123+ } else {
124+ sanitizeDecimalInput(rawValue)
125+ }
126+ btcValue = sanitized
127+ fiatValue = if (sanitized.isEmpty()) {
128+ " "
129+ } else {
130+ CalculatorFormatter .convertBtcToFiat(
131+ btcValue = btcValue,
132+ displayUnit = currencyUiState.displayUnit,
133+ currencyViewModel = currencyViewModel,
134+ ).orEmpty()
135+ }
72136 calculatorViewModel.updateCalculatorValues(fiatValue = fiatValue, btcValue = btcValue)
73137 },
74138 fiatSymbol = currencyUiState.currencySymbol,
75139 fiatName = currencyUiState.selectedCurrency,
76- fiatValue = fiatValue.ifEmpty { calculatorValues.fiatValue },
77- onFiatChange = { newValue ->
78- fiatValue = newValue
79- btcValue = CalculatorFormatter .convertFiatToBtc(
80- fiatValue = fiatValue,
81- displayUnit = currencyUiState.displayUnit,
82- currencyViewModel = currencyViewModel
83- )
140+ fiatValue = displayedFiatValue,
141+ onFiatChange = { rawValue ->
142+ val sanitized = sanitizeDecimalInput(rawValue, maxDecimalPlaces = FIAT_DECIMAL_PLACES )
143+ fiatValue = sanitized
144+ btcValue = if (sanitized.isEmpty()) {
145+ " "
146+ } else {
147+ val converted = CalculatorFormatter .convertFiatToBtc(
148+ fiatValue = fiatValue,
149+ displayUnit = currencyUiState.displayUnit,
150+ currencyViewModel = currencyViewModel,
151+ )
152+ if (currencyUiState.displayUnit.isModern()) {
153+ converted.filter { it.isDigit() }
154+ } else {
155+ converted
156+ }
157+ }
84158 calculatorViewModel.updateCalculatorValues(fiatValue = fiatValue, btcValue = btcValue)
85- }
159+ },
86160 )
87161}
88162
@@ -115,14 +189,13 @@ fun CalculatorCardContent(
115189
116190 // Bitcoin input with visual transformation
117191 CalculatorInput (
118- modifier = Modifier
119- .fillMaxWidth()
120- .onFocusChanged { focusState -> if (focusState.hasFocus) onBtcChange(" " ) },
121192 value = btcValue,
122193 onValueChange = onBtcChange,
123194 currencySymbol = BITCOIN_SYMBOL ,
124195 currencyName = stringResource(R .string.settings__general__unit_bitcoin),
125- visualTransformation = BitcoinVisualTransformation (btcPrimaryDisplayUnit)
196+ keyboardType = if (btcPrimaryDisplayUnit.isModern()) KeyboardType .Number else KeyboardType .Decimal ,
197+ visualTransformation = BitcoinVisualTransformation (btcPrimaryDisplayUnit),
198+ modifier = Modifier .fillMaxWidth()
126199 )
127200
128201 VerticalSpacer (16 .dp)
@@ -133,15 +206,40 @@ fun CalculatorCardContent(
133206 onValueChange = onFiatChange,
134207 currencySymbol = fiatSymbol,
135208 currencyName = fiatName,
136- visualTransformation = MonetaryVisualTransformation (decimalPlaces = 2 ),
137- modifier = Modifier
138- .fillMaxWidth()
139- .onFocusChanged { focusState -> if (focusState.hasFocus) onFiatChange(" " ) }
209+ keyboardType = KeyboardType .Decimal ,
210+ visualTransformation = MonetaryVisualTransformation (decimalPlaces = FIAT_DECIMAL_PLACES ),
211+ modifier = Modifier .fillMaxWidth()
140212 )
141213 }
142214 }
143215}
144216
217+ internal fun shouldHydrateFiatFromStoredBtc (
218+ storedBtcValue : String ,
219+ storedFiatValue : String ,
220+ currentFiatValue : String ,
221+ displayUnit : BitcoinDisplayUnit ,
222+ ): Boolean {
223+ if (storedBtcValue.isEmpty()) {
224+ return false
225+ }
226+ if (isZeroBtcValue(storedBtcValue, displayUnit)) {
227+ return false
228+ }
229+ if (storedFiatValue.isNotEmpty()) {
230+ return false
231+ }
232+ return currentFiatValue.isEmpty()
233+ }
234+
235+ internal fun isZeroBtcValue (
236+ btcValue : String ,
237+ displayUnit : BitcoinDisplayUnit ,
238+ ): Boolean = when (displayUnit) {
239+ BitcoinDisplayUnit .MODERN -> btcValue == " 0"
240+ BitcoinDisplayUnit .CLASSIC -> btcValue.toBigDecimalOrNull()?.compareTo(BigDecimal .ZERO ) == 0
241+ }
242+
145243@Composable
146244private fun WidgetTitleRow () {
147245 Row (
0 commit comments