From 4a68dcbf3e0858ad877c24f167d2ef5e9c527403 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 16 Sep 2026 17:40:05 -0300 Subject: [PATCH 1/4] fix: strip sats separators from raw input Co-Authored-By: Claude Opus 5 (1M context) --- .../bitkit/viewmodels/AmountInputViewModel.kt | 13 +++-- .../viewmodels/AmountInputViewModelTest.kt | 57 +++++++++++++++++++ changelog.d/next/558.fixed.md | 1 + 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 changelog.d/next/558.fixed.md diff --git a/app/src/main/java/to/bitkit/viewmodels/AmountInputViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AmountInputViewModel.kt index 7a4e0b24f1..124ed07631 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AmountInputViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AmountInputViewModel.kt @@ -147,7 +147,7 @@ class AmountInputViewModel @Inject constructor( // Update raw input text based on the formatted display rawInputText = when (primaryDisplay) { PrimaryDisplay.FIAT -> _uiState.value.text.replace(",", "") - else -> _uiState.value.text + else -> _uiState.value.text.stripSatsGrouping() } } @@ -171,7 +171,7 @@ class AmountInputViewModel @Inject constructor( // Update raw input text based on the new display rawInputText = when (newPrimaryDisplay) { PrimaryDisplay.FIAT -> _uiState.value.text.replace(",", "") - else -> _uiState.value.text + else -> _uiState.value.text.stripSatsGrouping() } } else if (currentRawInput.isNotEmpty()) { // Convert the raw input from the old currency to the new currency @@ -190,8 +190,9 @@ class AmountInputViewModel @Inject constructor( // Converting from fiat to bitcoin val sats = convertFiatToSats(currentRawInput) if (sats != null) { - rawInputText = formatBitcoinFromSats(sats, isModern) - _uiState.update { it.copy(text = rawInputText) } + val formatted = formatBitcoinFromSats(sats, isModern) + rawInputText = formatted.stripSatsGrouping() + _uiState.update { it.copy(text = formatted) } } } } @@ -335,6 +336,8 @@ class AmountInputViewModel @Inject constructor( return if (isModern) sats.formatToModernDisplay() else sats.formatToClassicDisplay() } + private fun String.stripSatsGrouping(): String = replace("$SATS_GROUPING_SEPARATOR", "") + private fun convertToSats( text: String, primaryDisplay: PrimaryDisplay, @@ -351,7 +354,7 @@ class AmountInputViewModel @Inject constructor( if (text.isEmpty()) return 0 return if (isModern) { - text.replace("$SATS_GROUPING_SEPARATOR", "").toLongOrDefault() + text.stripSatsGrouping().toLongOrDefault() } else { runCatching { val btcBigDecimal = BigDecimal(text) diff --git a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt index 32edff9dd7..f131bf1d1c 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt @@ -23,6 +23,7 @@ import to.bitkit.models.FIAT_DECIMALS import to.bitkit.models.FxRate import to.bitkit.models.PrimaryDisplay import to.bitkit.models.STUB_RATE +import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.CurrencyState import to.bitkit.services.CurrencyService @@ -365,6 +366,62 @@ class AmountInputViewModelTest : BaseUnitTest() { assertEquals("12 345", viewModel.uiState.value.text) } + @Test + fun `setSats in modern bitcoin allows digits up to max amount`() = test { + val currency = mockCurrency(PrimaryDisplay.BITCOIN, BitcoinDisplayUnit.MODERN) + + viewModel.setSats(12_345_678L, currency) + assertEquals("12 345 678", viewModel.uiState.value.text) + + viewModel.handleNumberPadInput("9", currency) + + assertEquals(123_456_789L, viewModel.uiState.value.sats) + assertEquals("123 456 789", viewModel.uiState.value.text) + assertNull(viewModel.uiState.value.errorKey) + } + + @Test + fun `setSats in modern bitcoin then delete removes a digit on every press`() = test { + val currency = mockCurrency(PrimaryDisplay.BITCOIN, BitcoinDisplayUnit.MODERN) + + viewModel.setSats(1_234L, currency) + assertEquals("1 234", viewModel.uiState.value.text) + + viewModel.handleNumberPadInput(KEY_DELETE, currency) + assertEquals(123L, viewModel.uiState.value.sats) + assertEquals("123", viewModel.uiState.value.text) + + viewModel.handleNumberPadInput(KEY_DELETE, currency) + assertEquals(12L, viewModel.uiState.value.sats) + + viewModel.handleNumberPadInput(KEY_DELETE, currency) + assertEquals(1L, viewModel.uiState.value.sats) + + viewModel.handleNumberPadInput(KEY_DELETE, currency) + assertEquals(0L, viewModel.uiState.value.sats) + assertEquals("", viewModel.uiState.value.text) + } + + @Test + fun `switchUnit from fiat to modern bitcoin accepts appended digit`() = test { + val fiat = mockCurrency(PrimaryDisplay.FIAT) + val modernBtc = mockCurrency(PrimaryDisplay.BITCOIN, BitcoinDisplayUnit.MODERN) + + "11515".forEach { viewModel.handleNumberPadInput(it.toString(), fiat) } + val satsBefore = viewModel.uiState.value.sats + assertTrue(satsBefore >= 10_000_000L) + + viewModel.switchUnit(fiat) + assertEquals(satsBefore, viewModel.uiState.value.sats) + + viewModel.handleNumberPadInput("1", modernBtc) + + val expectedSats = satsBefore * 10 + 1 + assertEquals(expectedSats, viewModel.uiState.value.sats) + assertEquals(expectedSats.formatToModernDisplay(), viewModel.uiState.value.text) + assertNull(viewModel.uiState.value.errorKey) + } + @Test fun `setSats works with fiat currency`() = test { val currency = mockCurrency(PrimaryDisplay.FIAT) diff --git a/changelog.d/next/558.fixed.md b/changelog.d/next/558.fixed.md new file mode 100644 index 0000000000..28391aac40 --- /dev/null +++ b/changelog.d/next/558.fixed.md @@ -0,0 +1 @@ +Fixed the sats amount keypad blocking extra digits and ignoring delete presses after using a preset amount or switching units. From 70388b6a67072698c0cda364de8dd1d113bfcab3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 10:18:38 -0300 Subject: [PATCH 2/4] docs: add transfer spending preset delete journey Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 3 +- .../transfer-spending-preset-delete.xml | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 journeys/amount-limits/transfer-spending-preset-delete.xml diff --git a/journeys/README.md b/journeys/README.md index 403c5e2309..2214d04d32 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -114,7 +114,7 @@ fixtures, push notifications) live in each suite's README. | Suite | Journeys | Notes | | --- | --- | --- | -| [amount-limits](amount-limits) | 4 | Number pad caps on all four amount screens | +| [amount-limits](amount-limits) | 5 | Number pad caps on all four amount screens, plus preset/unit-switch delete | | [cjit-notifications](cjit-notifications) | 3 | CJIT channel-ready notifications; needs FCM push | | [deeplinks](deeplinks) | 2 | `bitkit://screen/…` and sheet routing behind the dev-mode gate; no README | | [hardware-wallet](hardware-wallet) | 17 | Trezor over USB; needs the Trezor emulator | @@ -142,6 +142,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `hardware-wallet/receive-onchain.xml`, `hardware-wallet/send-onchain.xml` | not ported | | `payment-requests/requested-resolution-failure.xml` | not ported | | `deeplinks/*` | not ported — iOS registers the `bitkit` scheme but has no screen or sheet router | +| `amount-limits/transfer-spending-preset-delete.xml` | not ported yet — the same fix shipped in synonymdev/bitkit-ios#289, so this one should port | | — | `hardware-wallet/transfer-to-spending-over-max.xml` exists only on iOS | ### Running one on iOS diff --git a/journeys/amount-limits/transfer-spending-preset-delete.xml b/journeys/amount-limits/transfer-spending-preset-delete.xml new file mode 100644 index 0000000000..6000eb8231 --- /dev/null +++ b/journeys/amount-limits/transfer-spending-preset-delete.xml @@ -0,0 +1,36 @@ + + + Verifies that after the "Transfer to Spending" amount is filled by a preset (25% or MAX) or by + switching the number pad unit from fiat, every delete press removes exactly one digit and + appended digits are accepted. The displayed amount keeps its sats grouping spaces, but the + number pad input underneath does not, so no press is spent on a space (synonymdev/bitkit-android#558). + + Precondition: onboarded dev wallet with the display unit set to modern Bitcoin (sats), primary + display Bitcoin, a POSITIVE on-chain Savings balance of at least 40 000 sats, and a running node + connected to the LSP. Start on the wallet home screen. No funds move: the journey never taps + Continue. + + iOS counterpart: the same fix shipped in synonymdev/bitkit-ios#289, but the journey is not + ported to the iOS corpus yet. + + + Tap the Savings balance card (testTag "ActivitySavings") on the home screen + Tap "Transfer To Spending" (testTag "TransferToSpending") + If the spending intro screen appears, tap "Get Started" (testTag "SpendingIntro-button") + Verify the spending amount screen (testTag "SpendingAmount") is visible + Wait until the available amount (testTag "SpendingAmountUnit") finishes loading and shows a positive value + Tap the 25% button (testTag "SpendingAmountQuarter") + Verify the amount in the input field (testTag "SpendingAmountNumberField") shows a grouped value with a space, e.g. "78 777" + Tap the delete key (testTag "NRemove") repeatedly until the amount is 0, reading the amount after each press + Verify every delete press changed the amount, e.g. "78 777" → "7 877" → "787" → "78" → "7" → "0", with no press leaving the amount unchanged + Tap the number pad unit toggle (testTag "SpendingNumberPadUnit") so it shows the fiat currency + Tap "1" (testTag "N1") and "0" (testTag "N0") + Tap the number pad unit toggle (testTag "SpendingNumberPadUnit") so it shows "BITCOIN" + Verify the amount in the input field is a grouped sats value of at least 4 digits + Tap "1" (testTag "N1") + Verify the amount gained exactly one trailing digit "1" and is not over the available amount + Tap the delete key (testTag "NRemove") repeatedly until the amount is 0, reading the amount after each press + Verify every delete press changed the amount + Tap the back button (testTag "NavigationBack") to leave without transferring + + From 08eafe9d93f1b8d69ec17ffefaff1d367c46846a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 12:26:11 -0300 Subject: [PATCH 3/4] docs: fix spending preset journey balance precondition Co-Authored-By: Claude Opus 5 (1M context) --- .../transfer-spending-preset-delete.xml | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/journeys/amount-limits/transfer-spending-preset-delete.xml b/journeys/amount-limits/transfer-spending-preset-delete.xml index 6000eb8231..cda6165264 100644 --- a/journeys/amount-limits/transfer-spending-preset-delete.xml +++ b/journeys/amount-limits/transfer-spending-preset-delete.xml @@ -6,9 +6,20 @@ number pad input underneath does not, so no press is spent on a space (synonymdev/bitkit-android#558). Precondition: onboarded dev wallet with the display unit set to modern Bitcoin (sats), primary - display Bitcoin, a POSITIVE on-chain Savings balance of at least 40 000 sats, and a running node - connected to the LSP. Start on the wallet home screen. No funds move: the journey never taps - Continue. + display Bitcoin, a POSITIVE on-chain Savings balance of at least 250 000 sats, LSP limits above + that, and a running node connected to the LSP. Start on the wallet home screen. No funds move: + the journey never taps Continue. + + Why 250 000: the number pad rejects any sats input above its max + (AmountInputViewModel.handleNumberPadInput, the guard comparing the new amount to maxAmount), + and that max is `maxAllowedToSend` — the Savings balance less the quoted LSP order fee, also + capped by the LSP's own max client balance (TransferViewModel, SpendingAmountScreen's + `setMaxAmount`). It is the value shown by "SpendingAmountUnit". Entering 10 fiat units and + switching back to sats gives about 10 000 sats at a BTC price near 100 000 fiat units, and the + append in the step below makes that about 100 001, so a smaller balance has the digit dropped + and the max toast shown instead, which false-fails the step. The guard in the fiat-entry step + below keeps that true if the price moves far enough for 10 fiat units to be worth more than a + tenth of the max. iOS counterpart: the same fix shipped in synonymdev/bitkit-ios#289, but the journey is not ported to the iOS corpus yet. @@ -24,11 +35,11 @@ Tap the delete key (testTag "NRemove") repeatedly until the amount is 0, reading the amount after each press Verify every delete press changed the amount, e.g. "78 777" → "7 877" → "787" → "78" → "7" → "0", with no press leaving the amount unchanged Tap the number pad unit toggle (testTag "SpendingNumberPadUnit") so it shows the fiat currency - Tap "1" (testTag "N1") and "0" (testTag "N0") + Tap "1" (testTag "N1") and "0" (testTag "N0") to enter 10 fiat units; if 10 fiat units are worth more than a tenth of the amount shown in "SpendingAmountUnit", tap only "1" (testTag "N1") instead, so the digit appended below still fits under that amount Tap the number pad unit toggle (testTag "SpendingNumberPadUnit") so it shows "BITCOIN" Verify the amount in the input field is a grouped sats value of at least 4 digits Tap "1" (testTag "N1") - Verify the amount gained exactly one trailing digit "1" and is not over the available amount + Verify the amount gained exactly one trailing digit "1", is not over the amount shown in "SpendingAmountUnit", and that no "Spending Balance Maximum" warning toast appeared Tap the delete key (testTag "NRemove") repeatedly until the amount is 0, reading the amount after each press Verify every delete press changed the amount Tap the back button (testTag "NavigationBack") to leave without transferring From e7407e46f5d009b64dc23191c5bb99edcdf09300 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 21 Sep 2026 08:24:30 -0300 Subject: [PATCH 4/4] docs: restore journey suite table order after merge Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/journeys/README.md b/journeys/README.md index 0f8ffa32e9..bb297beb16 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -115,9 +115,8 @@ fixtures, push notifications) live in each suite's README. | Suite | Journeys | Notes | | --- | --- | --- | | [activity](activity) | 1 | Date range sheet under rapid month taps; needs no backend, no README | -| [amount-limits](amount-limits) | 4 | Number pad caps on all four amount screens | -| [backup-restore](backup-restore) | 1 | VSS restore keeps tags and closed channels; wipes the wallet | | [amount-limits](amount-limits) | 5 | Number pad caps on all four amount screens, plus preset/unit-switch delete | +| [backup-restore](backup-restore) | 1 | VSS restore keeps tags and closed channels; wipes the wallet | | [cjit-notifications](cjit-notifications) | 3 | CJIT channel-ready notifications; needs FCM push | | [coin-selection](coin-selection) | 1 | Manual coin selection screen; needs 3+ on-chain UTXOs; no README | | [deeplinks](deeplinks) | 2 | `bitkit://screen/…` and sheet routing behind the dev-mode gate; no README |