Skip to content

fix: align currency and calc widget with ios#884

Open
piotr-iohk wants to merge 22 commits intomasterfrom
fix/currency-widget-consistency-881
Open

fix: align currency and calc widget with ios#884
piotr-iohk wants to merge 22 commits intomasterfrom
fix/currency-widget-consistency-881

Conversation

@piotr-iohk
Copy link
Copy Markdown
Collaborator

@piotr-iohk piotr-iohk commented Apr 2, 2026

Fixes #881

Description

This PR:

  1. Shows currency symbols in the "Other Currencies" section of the local currency settings (previously only currency codes were displayed)
  2. Truncates 3+ character fiat symbols in the calculator widget icon badge to prevent layout overflow, matching iOS behavior from:
  3. Pre-fills new calculator widgets with a default BTC value of 10 000 and auto-derives the fiat equivalent on first load
  4. Removes auto-clear-on-focus behavior from both BTC and fiat inputs so values persist when switching between fields
  5. Hardens calculator field input by sanitizing pasted and typed values for BTC/fiat and preventing classic-mode fiat from being rehydrated back to zero after clearing
  6. Switches fiat input keyboard to decimal type and uses space as grouping separator instead of comma

Preview

Screen.Recording.2026-04-03.at.17.45.25.mov

QA Notes

1. Currency settings symbols

  1. Go to Settings -> Local currency
  2. Scroll to "Other Currencies"
  3. Verify each currency shows its symbol in parentheses (e.g. "AED (د.إ)")
  4. Verify "Most Used" section still shows symbols as before

2. Calculator widget symbol truncation

  1. Add a calculator widget on the home screen
  2. Change local currency to CHF or XDR
  3. Verify the fiat icon badge shows a single character ("C" or "X"), not the full code
  4. Change to a 1-2 char symbol currency (USD, EUR, PLN) and verify it renders normally
Screen_recording_20260423_071500.webm

3. Calculator widget default value

  1. Remove existing calculator widget (if any)
  2. Add a new calculator widget
  3. Verify BTC field shows "10 000" and fiat field shows the converted amount
  4. Verify both fields are editable without clearing on focus
Screen_recording_20260423_073524.webm

4. Calculator input behavior

  1. Tap fiat field, type a value (e.g. "12.34") — verify decimal input works
  2. Tap BTC field — verify fiat value is preserved (not cleared)
  3. Tap fiat field again — verify BTC value is preserved
  4. Delete all in fiat field — verify you can type a new value including "."
  5. Verify fiat uses space grouping (e.g. "1 234.56" not "1,234.56")
Screen_recording_20260423_080014.webm

5. Calculator paste and clear edge cases

  1. In MODERN denomination, paste malformed BTC input containing punctuation (for example 1000087188..........,,,,,)
  2. Verify BTC shows only the sanitized sat value and does not display pasted punctuation
  3. In CLASSIC denomination, clear fiat completely and verify it stays empty instead of snapping back to 0
  4. Paste 8 into the empty fiat field in CLASSIC denomination and verify it behaves consistently
Screen_recording_20260423_081652.webm

@piotr-iohk piotr-iohk added this to the 2.2.0 milestone Apr 2, 2026
@piotr-iohk piotr-iohk self-assigned this Apr 3, 2026
@piotr-iohk piotr-iohk marked this pull request as ready for review April 3, 2026 15:57
…onents/CalculatorCard.kt

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
@piotr-iohk piotr-iohk removed this from the 2.2.0 milestone Apr 7, 2026
@piotr-iohk piotr-iohk marked this pull request as draft April 7, 2026 17:19
@ovitrif ovitrif added this to the 2.3.0 milestone Apr 7, 2026
@jvsena42 jvsena42 assigned jvsena42 and unassigned piotr-iohk and jvsena42 Apr 22, 2026
@jvsena42 jvsena42 marked this pull request as ready for review April 23, 2026 11:18
@jvsena42 jvsena42 requested review from ovitrif April 23, 2026 12:02
Copy link
Copy Markdown
Collaborator Author

@piotr-iohk piotr-iohk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-tested, LGTM, Thanks @jvsena42! 🙌

@jvsena42 jvsena42 enabled auto-merge April 27, 2026 10:15
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Comment thread CHANGELOG.md Outdated
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 3 issues. See inline comments.

import to.bitkit.models.BitcoinDisplayUnit
import kotlin.test.assertEquals

class BitcoinVisualTransformationTest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 == '.' }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug — locale-specific decimal separator silently stripped

sanitizeDecimalInput only accepts '.' (ASCII period) as the decimal character:

internal fun sanitizeDecimalInput(raw: String): String {
val filtered = raw.filter { it.isDigit() || it == '.' }
val dotIndex = filtered.indexOf('.')
if (dotIndex == -1) return filtered
return filtered.substring(0, dotIndex + 1) +
filtered.substring(dotIndex + 1).replace(".", "")
}

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()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: currency settings and calculator widget inconsistent with iOS

3 participants