fix: align currency and calc widget with ios#884
Conversation
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
…onents/CalculatorCard.kt Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
piotr-iohk
left a comment
There was a problem hiding this comment.
Re-tested, LGTM, Thanks @jvsena42! 🙌
# Conflicts: # CHANGELOG.md
| import to.bitkit.models.BitcoinDisplayUnit | ||
| import kotlin.test.assertEquals | ||
|
|
||
| class BitcoinVisualTransformationTest { |
There was a problem hiding this comment.
CLAUDE.md compliance — missing deterministic locale
The classic filter test (lines 18–24) calls BitcoinVisualTransformation(BitcoinDisplayUnit.CLASSIC).filter(...), which invokes formatClassicDisplay(). That method constructs DecimalFormatSymbols(Locale.getDefault()) internally — meaning the test's behaviour is tied to whatever locale the CI or developer machine happens to use.
Per CLAUDE.md:
ALWAYS use a deterministic locale in unit tests to ensure consistent results across CI and local runs.
Although the production code explicitly overrides decimalSeparator = '.' and groupingSeparator = ' ', constructing DecimalFormatSymbols from the system locale can still introduce other locale-dependent symbol differences. More importantly, the pattern in this codebase (e.g. CurrencyTest.kt) consistently pins the locale.
Suggested fix — add a @get:Rule or @Before setup:
class BitcoinVisualTransformationTest {
@Before
fun setLocale() {
Locale.setDefault(Locale.US)
}
// ...
}| raw.filter { it.isDigit() } | ||
|
|
||
| internal fun sanitizeDecimalInput(raw: String): String { | ||
| val filtered = raw.filter { it.isDigit() || it == '.' } |
There was a problem hiding this comment.
Bug — locale-specific decimal separator silently stripped
sanitizeDecimalInput only accepts '.' (ASCII period) as the decimal character:
However, CalculatorCard now uses KeyboardType.Decimal for the fiat field (always) and for the classic-mode BTC field. On Android, KeyboardType.Decimal renders the decimal key using the device's locale: on German (de), French (fr), Spanish (es), Italian (it), Russian (ru), and many other locales the key inserts a comma ',' rather than a period.
Because sanitizeDecimalInput only accepts '.', any user-typed comma (the locale's decimal separator) is silently discarded. For example, a German user trying to enter 1,50 (= 1.50 EUR) would see only 150 — ten times the intended amount.
The same issue exists in sanitizeClassicInput inside BitcoinVisualTransformation.kt.
Suggested fix — normalise the locale separator before sanitising:
internal fun sanitizeDecimalInput(raw: String): String {
val localDecimal = DecimalFormatSymbols.getInstance().decimalSeparator
val normalized = if (localDecimal == ',') raw.replace(',', '.') else raw
val filtered = normalized.filter { it.isDigit() || it == '.' }
val dotIndex = filtered.indexOf('.')
if (dotIndex == -1) return filtered
return filtered.substring(0, dotIndex + 1) +
filtered.substring(dotIndex + 1).replace(".", "")
}Apply the same normalisation at the top of sanitizeClassicInput in BitcoinVisualTransformation.kt, and update the unit-test assertion assertEquals("1234", sanitizeDecimalInput("1,234")) in CalculatorCardStateTest to reflect the new behaviour on Locale.US.
| onFiatChange = { rawValue -> | ||
| val sanitized = sanitizeDecimalInput(rawValue) | ||
| fiatValue = sanitized | ||
| btcValue = if (sanitized.isEmpty()) { |
There was a problem hiding this comment.
Bug — btcValue state stores formatted (space-grouped) strings from fiat conversion
When the user edits the fiat field, onBtcChange sanitises input through sanitizeIntegerInput / sanitizeDecimalInput before writing to btcValue. But when the user edits the fiat field, the result of CalculatorFormatter.convertFiatToBtc is written directly — and in MODERN mode that function calls formatToModernDisplay(), which formats with space group separators (e.g. "1 234 567").
This means btcValue (and the persisted store value) can contain a space-grouped string like "1 234 567" rather than a raw digit string like "1234567". Downstream code that parses btcValue without calling removeSpaces() first — including the new isZeroBtcValue check (btcValue.toBigDecimalOrNull() returns null for a space-grouped string in CLASSIC mode) — silently breaks.
Suggested fix — sanitise the result before assigning:
btcValue = if (sanitized.isEmpty()) {
""
} else {
val converted = CalculatorFormatter.convertFiatToBtc(
fiatValue = fiatValue,
displayUnit = currencyUiState.displayUnit,
currencyViewModel = currencyViewModel,
)
if (currencyUiState.displayUnit.isModern()) {
converted.filter { it.isDigit() }
} else {
converted
}
}
Fixes #881
Description
This PR:
Preview
Screen.Recording.2026-04-03.at.17.45.25.mov
QA Notes
1. Currency settings symbols
2. Calculator widget symbol truncation
Screen_recording_20260423_071500.webm
3. Calculator widget default value
Screen_recording_20260423_073524.webm
4. Calculator input behavior
Screen_recording_20260423_080014.webm
5. Calculator paste and clear edge cases
1000087188..........,,,,,)08into the empty fiat field in CLASSIC denomination and verify it behaves consistentlyScreen_recording_20260423_081652.webm