Skip to content
Open
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
13 changes: 8 additions & 5 deletions app/src/main/java/to/bitkit/viewmodels/AmountInputViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand All @@ -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
Expand All @@ -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) }
}
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/558.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the sats amount keypad blocking extra digits and ignoring delete presses after using a preset amount or switching units.
2 changes: 2 additions & 0 deletions journeys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ fixtures, push notifications) live in each suite's README.
| [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 |
| [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 |
Expand Down Expand Up @@ -154,6 +155,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691):
| `transfers/closed-channel-transfer-settles.xml` | not ported — the closed-channel and order-closure settle rules are an iOS follow-up |
| `deeplinks/*` | not ported — iOS registers the `bitkit` scheme but has no screen or sheet router |
| `backup-restore/restore-keeps-tags-and-closed-channels.xml` | not ported yet — iOS already gates uploads across the whole restore (`AppScene.restoreFromMostRecentBackup` sets `BackupService.setRestoring(true)` before the timestamp probe), but still applies the three activity slices in one block (`BackupService.performFullRestoreFromLatestBackup`), which is the half this journey pins; port it with the iOS slice fix |
| `amount-limits/transfer-spending-preset-delete.xml` | not ported yet — the same fix shipped in synonymdev/bitkit-ios#289, so this one should port |
| `home/pull-to-refresh-rates.xml` | not ported — iOS does not refresh exchange rates on pull to refresh |
| `security/pin-result-long-label.xml` | not ported — the toggle exists on the iOS security success screen, but the overlap check is a follow-up |
| `tags/activity-tag-length-cap.xml` | not ported — iOS has no 20-character cap on tag input |
Expand Down
47 changes: 47 additions & 0 deletions journeys/amount-limits/transfer-spending-preset-delete.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<journey name="transfer to spending preset amount deletes one digit per press">
<description>
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 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.
</description>
<actions>
<action>Tap the Savings balance card (testTag "ActivitySavings") on the home screen</action>
<action>Tap "Transfer To Spending" (testTag "TransferToSpending")</action>
<action>If the spending intro screen appears, tap "Get Started" (testTag "SpendingIntro-button")</action>
<action>Verify the spending amount screen (testTag "SpendingAmount") is visible</action>
<action>Wait until the available amount (testTag "SpendingAmountUnit") finishes loading and shows a positive value</action>
<action>Tap the 25% button (testTag "SpendingAmountQuarter")</action>
<action>Verify the amount in the input field (testTag "SpendingAmountNumberField") shows a grouped value with a space, e.g. "78 777"</action>
<action>Tap the delete key (testTag "NRemove") repeatedly until the amount is 0, reading the amount after each press</action>
<action>Verify every delete press changed the amount, e.g. "78 777" → "7 877" → "787" → "78" → "7" → "0", with no press leaving the amount unchanged</action>
<action>Tap the number pad unit toggle (testTag "SpendingNumberPadUnit") so it shows the fiat currency</action>
<action>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</action>
<action>Tap the number pad unit toggle (testTag "SpendingNumberPadUnit") so it shows "BITCOIN"</action>
<action>Verify the amount in the input field is a grouped sats value of at least 4 digits</action>
<action>Tap "1" (testTag "N1")</action>
<action>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</action>
<action>Tap the delete key (testTag "NRemove") repeatedly until the amount is 0, reading the amount after each press</action>
<action>Verify every delete press changed the amount</action>
<action>Tap the back button (testTag "NavigationBack") to leave without transferring</action>
</actions>
</journey>
Loading