Skip to content

fix: block numberpad input exceeding max amount#908

Merged
jvsena42 merged 24 commits into
masterfrom
fix/block-input-over-max
Jun 10, 2026
Merged

fix: block numberpad input exceeding max amount#908
jvsena42 merged 24 commits into
masterfrom
fix/block-input-over-max

Conversation

@ovitrif

@ovitrif ovitrif commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #801

Follow-up to #870 (merged) that capped onchain send validation. That PR disabled the Continue button when an amount exceeded the balance, but users could still type whatever they wanted with no explanation of why the button was disabled — poor UX.

This PR brings the Android number pad to parity with the iOS work in synonymdev/bitkit-ios#584 and synonymdev/bitkit-ios#585.

Description

Prevents users from entering amounts above the allowed maximum on numberpad screens, using the same pattern as the React Native / iOS apps:

  • Hard blocks the keypress at numberpad level (nothing enters the input field)
  • Reuses the existing error animation — pressed key flashes red for 500ms
  • Shows a short WARNING toast (1.5s) explaining the limit, with context-aware messaging (see below)

Applied to every amount-entry screen that has a contextual maximum:

  1. Send amount (onchain / lightning) — blocks at availableAmount
  2. LNURL Withdraw — blocks at maxWithdrawableSat (same Send screen)
  3. LNURL Pay — blocks at min(maxSendableSat, availableAmount), matching the MAX button
  4. Transfer to Spending — blocks at maxAllowedToSend (accounts for LSP fees)
  5. Receiving Capacity (Advanced) — blocks at transferValues.maxLspBalance
  6. Manual external channel funding — blocks at the channel-fundable balance, and Continue is now also gated on that balance (previously enabled above it)

Receive / CJIT amount screens are intentionally left uncapped — a receive amount has no balance ceiling.

Implementation

  • Added maxAmount field (defaults to MAX_AMOUNT) and setMaxAmount() setter to AmountInputViewModel
  • Added AmountInputEffect.MaxExceeded one-shot event via SharedFlow so screens can show a toast
  • Each screen wires setMaxAmount(limit) via LaunchedEffect so the limit stays in sync as fees/balances update
  • MaxExceeded is only emitted when a dynamic limit is set (not on the global 999M sats cap)
  • Added Toast.VISIBILITY_TIME_SHORT = 1500L constant

Two behavioral fixes (matching iOS) on top of the original cap:

  • Delete is never trapped above the cap — when a cap drops below the current amount (e.g. fee/balance refresh) or an amount is prefilled above it, delete keystrokes always apply so the user can reduce the amount instead of being stuck. Deleting no longer triggers the error flash or the MaxExceeded toast.
  • Zero balance keeps the pad usable — a 0 (or negative) cap is treated as "no cap", so the pad still accepts input at a zero balance (Continue just stays disabled) instead of freezing and toasting on every keystroke.

The manual external funding screen was also migrated off its old toast-on-change validation in ExternalNodeViewModel to the shared cap mechanism.

Context-aware over-limit messages (follow-up)

The over-limit toast on the Send screen previously always read "Insufficient balance / The amount exceeds your available balance", which was misleading for LNURL flows where the binding limit is the invoice/withdraw maximum rather than the wallet balance. The toast text now matches the actual constraint:

  • LNURL Pay — when the invoice's maxSendable is lower than the available spending balance → "Amount Too High / The amount exceeds this invoice's maximum."
  • LNURL Withdraw — over maxWithdrawable"Amount Too High / The amount exceeds the maximum you can withdraw."
  • All other cases (on-chain / lightning balance) keep "Insufficient balance / The amount exceeds your available balance."

Added string resources wallet__lnurl_pay__error_max__{title,description} and wallet__lnurl_w_error_max__{title,description}.

Fresh limit in over-limit toasts (follow-up)

On the Transfer → Spending screens the over-limit toast formats the limit into its message ("… {amount}"). The toast is fired from a long-lived LaunchedEffect(Unit) collector, which could format a stale value (often the initial 0) captured before the limit finished loading — so a correctly-blocked keypress could show a "₿ 0" maximum. Both screens now read the current limit via rememberUpdatedState before building the toast (SpendingAmountScreen, SpendingAdvancedScreen).

"Available" matches the sendable max on Transfer to Spending (follow-up)

On the Transfer to Spending screen the "Available" row showed the gross on-chain balance while the MAX button and input cap used the lower after-LSP-fee amount, so the displayed number didn't match what could actually be sent. balanceAfterFee now equals maxAllowedToSend (the after-fee, capped value), so the displayed "Available", the MAX button, the input cap, and the 25% button are all consistent. This is a deliberate divergence from iOS/RN, which keep "Available" showing the gross balance.

Tests

Added unit tests in AmountInputViewModelTest mirroring iOS NumberPadTests:

  • delete is allowed when amount is above the cap
  • no max exceeded effect emitted on delete above cap
  • setMaxAmount with zero keeps input usable
  • setMaxAmount with negative value keeps input usable

Full unit suite and detekt pass.

QA Notes

Send (onchain)

  • Fund savings wallet, go to Send → address → amount screen
  • Type an amount exceeding the "Available" balance
  • Verify: keypress blocked, key flashes red, WARNING toast "Insufficient balance" appears (~1.5s)
  • Verify: tapping "Available" still works to set max
send-on-chain.webm

Send (lightning)

  • Same flow with a BOLT11 invoice
  • Verify same blocking + toast behavior
  • Switch between savings/spending — limit updates dynamically
send-lightning.webm

LNURL Withdraw / Pay

  • LNURL Withdraw → type past the max withdrawable: capped, toast reads "Amount Too High / The amount exceeds the maximum you can withdraw."
  • LNURL Pay → type past the max sendable: capped at min(maxSendable, available); MAX button and pad agree
  • LNURL Pay, when maxSendable < spending balance → toast reads "Amount Too High / The amount exceeds this invoice's maximum." (not "Insufficient balance")
lnulr-withdraw.webm
lnurl-pay.webm

Transfer to Spending

  • Transfer → Transfer to Spending, type above maxAllowedToSend
  • Verify keypress blocked + red flash + "Spending Balance Maximum" toast with the actual limit (not "₿ 0")
  • 25% and Max buttons still work
  • Verify the "Available" amount equals the value the Max button fills (after LSP fee)
transfer.webm

Receiving Capacity (Advanced)

  • Transfer → Spending → Advanced, type above maxLspBalance
  • Verify blocking + red flash + "Receiving Capacity Maximum" toast
  • Min/Default/Max buttons still work
transfer-and-receive-capacity.webm

Manual external channel funding

  • Transfer → external node funding → amount, type above the fundable balance: capped
  • Enter an amount above the fundable balance: Continue stays disabled
  • Quarter / Max buttons still work
external-node.webm

Edge cases

  • Entering exactly the max succeeds (not blocked)
  • Switching sats/fiat near the limit still enforces correctly
  • regression: zero available balance → pad still accepts input (not frozen), Continue stays disabled
  • regression: an over-cap amount (cap dropped after input) can be reduced with delete without being rejected

ovitrif and others added 2 commits April 23, 2026 23:30
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ovitrif ovitrif marked this pull request as draft April 23, 2026 21:51
@ovitrif ovitrif added this to the 2.4.0 milestone May 20, 2026
@jvsena42 jvsena42 self-assigned this Jun 8, 2026
@jvsena42 jvsena42 marked this pull request as ready for review June 9, 2026 10:27

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae21007459

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

ovitrif

This comment was marked as resolved.

@jvsena42 jvsena42 marked this pull request as draft June 9, 2026 12:15
@jvsena42 jvsena42 marked this pull request as ready for review June 9, 2026 12:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 503470d080

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@jvsena42

jvsena42 commented Jun 9, 2026

Copy link
Copy Markdown
Member

PR ready for review

@jvsena42

This comment was marked as resolved.

@jvsena42 jvsena42 marked this pull request as ready for review June 9, 2026 14:12
@jvsena42 jvsena42 requested a review from piotr-iohk June 9, 2026 14:36

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

tAck

PS. Given that I'm the author of first commits and PR I am not allowed to approve based on repos' GH config

PPS. Was a pleasure watching AI run the test journeys 🤖 👏🏻 .

@jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
Member

tAck

PS. Given that I'm the author of first commits and PR I am not allowed to approve based on repos' GH config

PPS. Was a pleasure watching AI run the test journeys 🤖 👏🏻 .

@piotr-iohk could you approved after testing if you don't find issues?

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tACK

@jvsena42 jvsena42 enabled auto-merge June 10, 2026 13:41
@jvsena42 jvsena42 merged commit 7948a5b into master Jun 10, 2026
17 checks passed
@jvsena42 jvsena42 deleted the fix/block-input-over-max branch June 10, 2026 13:43
CypherPoet pushed a commit to CypherPoet/bitkit-ios that referenced this pull request Jun 10, 2026
Matches the Android behavior from synonymdev/bitkit-android#908: a keypress
blocked by the screen-specific cap now shows a short warning toast, with
accessibilityIdentifier SendAmountExceededToast on the send-flow screens
for E2E parity.

Also replaces the onAppear + onChange cap-update duplication with a single
onChange(initial: true) across the amount screens, per review feedback.
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]: transfer to spending capped at 0 with on-chain balance

3 participants