Skip to content

fix: open review for requests with an amount - #1763

Open
jvsena42 wants to merge 2 commits into
masterfrom
fix/scan-amount-to-confirm
Open

jvsena42 wants to merge 2 commits into
masterfrom
fix/scan-amount-to-confirm

Conversation

@jvsena42

@jvsena42 jvsena42 commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

This PR:

  1. Opens Confirm directly when an on-chain BIP21 request with an amount is scanned, pasted or opened as a link and the Savings balance covers it
  2. Opens the USDT review directly when a USDT payment request with an amount is opened, matching iOS
  3. Updates the journeys whose route these change, adds two Send journeys, and corrects the fixed-price Paykit journey that still described the old Amount detour

Motivation, from the Slack report:

A lock priced at $2.50, scanned with USDT not enabled and paid in BTC, showed a loading spinner on the swipe slider for a few seconds, then landed on the Amount screen with most of the UI disabled, showing the BTC amount for the dollar value. Requests priced in BTC go to Confirm as expected.

Description

  • On-chain BIP21 with an amount now opens Confirm instead of Amount, because the amount is already known and Amount only made the user press Continue. The balance and dust checks run first; the fee and auto coin selection are prepared as Amount's Continue would, and a hardware wallet's fee is estimated before Confirm opens.
  • Back on that Confirm, from the toolbar arrow or system Back, replaces it with Amount holding the same amount, so the amount can still be edited; Back from Amount then returns where the send started.
  • Zero-amount and dust-amount BIP21, manual coin selection, contact payments and requests with a usable Lightning invoice keep their existing routes.
  • A USDT payment link with an amount now runs the quote once on entry, so it opens on review the way tapping Review would. A failed quote, such as an insufficient USDT balance, shows the same error on the Amount screen. Going back or editing the amount never quotes again.
  • The Slack report predates fix: confirm fixed Paykit requests directly #1581, which merged a few hours after the report and shipped in v3.1.0. On this branch a fixed 2.50 USD request with USDT off goes from Preparing straight to Confirm, with Bitcoin only and with Bitcoin plus USDT. No further code change was needed; this PR adds a unit test for the Bitcoin plus USDT case and updates fixed-price-bitcoin.xml and the payment-requests README, which still described the Amount detour.

Out of Scope

  • viewmodels/AppViewModel.kt: a unified BIP21 whose Lightning invoice has no amount but whose on-chain part does keeps opening Amount on Spending.
  • viewmodels/AppViewModel.kt: manual coin selection keeps going through Amount before the coin picker; opening the picker straight from a scan needs a sheet route the scan path cannot target today.
  • iOS: the matching on-chain change and journeys are handled in a separate bitkit-ios PR.
  • bitkit-e2e-tests: send.e2e.ts expectations for on-chain BIP21 with an amount are updated in test: expect review screen for bip21 with an amount bitkit-e2e-tests#290.

Design

N/A — no design available.

Preview

On-chain BIP21 with an amount, opened as a link, goes straight to Confirm; Back opens Amount with the same amount; Continue returns to Confirm:

onchain-amount-to-confirm.mp4

The same URI entered through Send → Enter Manually opens Confirm:

onchain-amount-manual-entry.mp4

BIP21 without an amount still opens Amount:

onchain-no-amount-to-amount.mp4

Manual coin selection still goes through Amount and then the coin picker:

onchain-amount-manual-coins.mp4

USDT link with an amount: after Enable USDT, the quote runs on entry and the insufficient-balance error appears without tapping Review (the wallet has no USDT, so the success review could not be recorded):

usdt-amount-auto-quote-error.mp4

Fixed 2.50 USD Paykit request with USDT off goes from Preparing straight to Confirm at 2 500 sats:

paykit-usd-request-to-confirm.mp4

QA Notes

Setup

Dev regtest build from this branch on an emulator, against the staging regtest backend the dev flavor targets, with a wallet holding about 700 000 sats in Savings and a Lightning channel. Coin Selection on Autopilot. The Paykit cases used the paykit-fixture-sender from bitkit-e2e-tests main, linked to a Pubky profile created on the payer. USDT cases enable USDT through the link's prompt; the wallet has no USDT balance.

Journeys

  • J1: new scan-onchain-amount-to-confirm.xml — BIP21 with an amount opens Confirm directly, Back (arrow and system Back) opens Amount with the amount, over-balance shows the insufficient savings toast
  • J2: new scan-onchain-no-amount-to-amount.xml — BIP21 without an amount, or with amount 0, still opens Amount
  • J3: updated usdt-payment-links-and-scans.xml — a USDT link with an amount opens review without tapping Review, or shows the quote error on Amount
  • J4: updated fixed-quoted-confirmation.xml — a fixed USD request accepting Bitcoin and USDT opens Bitcoin Confirm with USDT off
  • J5: updated fixed-price-bitcoin.xml — fixed-price requests open confirmation directly with 21 000 and 50 000 sats
  • J6: updated onchain-accepted-result.xml — the fixed-amount send starts on Confirm
  • J7: updated rbf-own-send.xml — the boosted send starts on Confirm
  • J8: updated pin-lock-on-resume.xml — the payment URI opened while locked opens Confirm after unlock
  • J9: updated signin-with-second-link.xml — the payment link opens Confirm once the sign-in sheet closes

Manual Tests

N/A

Automated Checks

  • added UsdtWalletScreenTest.kt — the initial quote runs only with a recipient and a positive, valid USDT amount
  • updated AppViewModelSendFlowTest.kt — on-chain BIP21 with an amount opens Confirm from the main scanner and from Send; zero, dust and manual coin selection open Amount; a fixed USD request accepting USDT opens Bitcoin Confirm while USDT is off
  • updated SendSheetNavigationTest.kt — Back from a Confirm opened by an invoice amount replaces it with Amount, from both start destinations; Back from a first-screen Confirm opens Amount only when it can return to Amount

Evidence

Verified on a Pixel 9 emulator (API 37) with the dev debug build of this branch:

  • J1: link and Enter Manually both open Confirm at 10 000 sats with recipient and fee; Back opens Amount at 10 000; Continue returns to Confirm; amount=0.01 shows the Insufficient Savings toast. A cold-start link waits through Connecting to network and then opens Confirm.
  • J1, review round 1 (1d280b6): system Back (KEYCODE_BACK) on a link-opened Confirm at 10 000 sats opens Amount at 10 000; a second Back closes the sheet. Before, it closed the sheet. Video in the PR comments.
  • J2: bitcoin:<addr> and ?amount=0 open Amount at 0; entering 10 000 continues to Confirm.
  • Manual coin selection: the same URI opens Amount, and Continue opens Coin Selection, so the coin-selection journeys are unchanged.
  • J3: the link quotes once on entry and shows "Not enough USDT to cover the amount and fees."; going back to the recipient and continuing does not quote again; tapping Review shows the same error; a link without uint256 opens an empty Amount with no quote.
  • J4, J5: 2.50 USD at 0.00001 opens Confirm at 2 500 sats with Bitcoin only and with Bitcoin plus USDT; the usd and btc presets open Confirm at 21 000 and 50 000 sats.
  • J6: amount=0.00001 opens Confirm at 1 000 sats with Savings and the recipient.
  • J8: with PIN on and the Receive sheet open, the URI opened in the background shows the PIN pad, then Confirm at 10 000 sats with the Receive sheet gone.
  • ./gradlew testDevDebugUnitTest (4 861 tests), ./gradlew detekt and ./gradlew lintDevDebug passed locally.

Gaps

  • J3's success branch, opening on review after a successful quote, was not verified: the emulator wallet has no USDT, and the funded USDT fixture is not available locally.
  • J6 and J7 were driven only up to Confirm; nothing was broadcast or boosted.
  • J9 was not driven: it needs a requesting app that issues fresh signin_grant URLs. Its Confirm step follows the same PIN-gated link route verified in J8.
  • Hardware-wallet funding goes through the fee estimate before Confirm, covered by reading the code against Amount's Continue; it was not driven with the Trezor emulator.
  • The Slack case was a Pubky Market lock, which can carry a Lightning endpoint. The fixture sender only publishes on-chain and USDT endpoints, so a fixed USD request paid over Lightning was not reproduced.
  • bitkit-e2e-tests: updated in test: expect review screen for bip21 with an amount bitkit-e2e-tests#290 on the same-named branch the e2e gate picks up; not driven locally. Before that change, send.e2e.ts would break: @send_1 taps AddressContinue for bitcoin:bcrt1q07x3…?amount=0.0001 and then expects Amount (verifyAmountToSend(10_000), ContinueAmount); @send_2 sends unified2 (on-chain 0.0002 with a 200 000 sat Lightning invoice above the Spending balance) and expects AssetButton-savings and ContinueAmount.

Models used

  • Planning/scoping: claude-opus-5-5 (reasoning: Not exposed)
  • Implementation: claude-opus-5-5 (reasoning: Not exposed)
  • Review: Not performed

The reasoning setting is not exposed in this session's metadata.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the latest change fixes Android Back without changing the existing toolbar behavior.

Summary

Amount-bearing Bitcoin requests open Confirm directly, and amount-bearing USDT links request a quote on entry.

  • Payable on-chain requests open Confirm, and Back still lets users edit the amount.
  • USDT payment links with an amount request their quote as soon as they open.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Request[Bitcoin request with payable amount] --> Confirm
  Confirm -->|Toolbar or Android Back| Amount
  Amount -->|Continue| Confirm
  Amount -->|Back| Origin[Previous screen or close sheet]
Loading

Reviews (2) · Last reviewed commit: "fix: route system back on direct confirm..." · Reviewed by Greptile

@ovitrif ovitrif added this to the 3.2.0 milestone Nov 3, 2026
@ovitrif

ovitrif commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Added to the 3.2.0 milestone: this fix was opened after the 3.1.0 cut and closes no issue, so this PR carries the milestone itself.

@talosmachina talosmachina left a comment

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.

No findings. On-chain BIP21 with a payable amount now opens Confirm (Back swaps it for Amount), and a USDT payment link with an amount quotes once on entry. Reviewed 5e95204, full tier, static: the branch was not built or run for this review, so the evidence is the code and CI.

What I checked, and 7 candidates I ruled out

Read in full: the diff; onScanOnchain, onAmountContinue, prepareCoinSelection, prepareHardwareSendFee, validateAmount, refreshOnchainSendIfNeeded, navigateToSendRoute, resetSendState in AppViewModel.kt; the Confirm and Amount routes and navigateToSendAmount in SendSheet.kt; UsdtWalletScreen.kt, UsdtViewModel.quote, UsdtRepo.quote, PaykitAmount.parse.
Call sites traced: openOnchainConfirmForInvoiceAmount (one, after the balance and dust checks), navigateToAmountFromConfirm, Routes.UsdtPayment (built only by handleUsdtPaymentInput).
CI: build-local and Greptile green; build-and-test, lint, compose-ui and the local e2e shards still running when this was posted.

Ruled out

  • Confirm skips a step Amount's Continue does: for auto coin selection prepareCoinSelection returns true with no side effect, selectedUtxos is already null after the reset, the hardware fee is prepared before navigating, and refreshOnchainSendIfNeeded follows; Lightning routing estimation does not apply to an on-chain pay method.
  • Manual coin selection or a contact payment reaching Confirm without the picker: both return false before any state change and fall through to Amount, covered by the manual-coin-selection test.
  • Back loops or strands the sheet: from the main scanner the stack becomes [Amount] and Amount's Back hides the sheet; from the send sheet it becomes [Recipient, ..., Amount]; after Continue, Back pops to Amount through the existing popBackStack(SendRoute.Amount) branch. canReturnToAmount lives in SendUiState, so every new send clears it through resetSendState.
  • Amount loses the invoice amount on the way back: SendAmountScreen seeds its input from uiState.amount when it is positive.
  • A hardware fee estimate failure: it toasts and returns false, then the scan falls through to Amount, the same place it went before.
  • USDT quote repeats on rotation or process death: isRequested is saveable and the effect is keyed on Unit, so Back, Edit and a restore never quote again; canQuote guards a busy state.
  • USDT amount string rejected by the quote: the route amount is PaykitAmount.value (a plain dot decimal), which both PaykitAmount.parseInput and UsdtRepo.quote accept, the same string the Review button sends.

Merge confidence: 4/5, not built here and CI still running; the e2e send suite expects Amount for this route until the bitkit-e2e-tests change named in the PR lands.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 1d280b6 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42

jvsena42 commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

System Back on a Confirm opened straight from a bitcoin:…?amount=0.0001 link, after 1d280b6: Confirm (10 000 sats) → system Back → Amount with 10 000 sats kept.

system-back-from-confirm.mp4

@jvsena42
jvsena42 marked this pull request as ready for review November 3, 2026 13:51
@jvsena42 jvsena42 self-assigned this Nov 3, 2026
@jvsena42
jvsena42 requested review from a team, coreyphillips and pwltr and removed request for a team November 3, 2026 13:55

@coreyphillips coreyphillips left a comment

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.

Two independent reviews.

needs changing before merge

  • E2E send shard breaks: e2e main still expects Amount for BIP21 with amount (app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:5402)

worth doing, does not block

  • USDT journey clipboard step still says the amount screen opens (journeys/usdt-payment-links-and-scans.xml:6)
  • Hardware-wallet direct-Confirm path has no test (app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:5426)
  • Tapping the amount on direct Confirm does nothing (app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:5434)
  • The PIN journey expects Home one Back press too early (journeys/security/pin-lock-on-resume.xml:45-46)

nits

  • KDoc on a private function (app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt:5415-5418)

return
}

if (amount > 0u && openOnchainConfirmForInvoiceAmount(amount, fromMainScanner, walletGeneration)) return

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.

needs changing before merge: E2E send shard breaks: e2e main still expects Amount for BIP21 with amount

This change makes @send_1 and @send_2 on bitkit-e2e-tests main fail, and nothing lets CI pick up an updated spec. How the E2E workflow behaves: - .github/workflows/e2e.yml runs on every non-draft PR that touches app/**. - default-feature-branch resolves to an e2e branch with the same name as the app branch, and falls back to main otherwise. - e2e-status fails when any shard fails. - synonymdev/bitkit-e2e-tests has no fix/scan-amount-to-confirm branch (gh api repos/synonymdev/bitkit-e2e-tests/branches/fix%2Fscan-amount-to-confirm returns 404), and the PR body has no Companion: line. What the specs on e2e main expect: - send.e2e.ts:157-169 (@send_1) enters bitcoin:bcrt1q07x3…?amount=0.0001 and taps AddressContinue. It then calls verifyAmountToSend(10_000) and waits for ContinueAmount. - send.e2e.ts:356-366 (@send_2 @ios_gate) enters an on-chain 0.0002 BIP21. Its 200 000 sat Lightning invoice is above Spending. It then waits for AssetButton-savings and taps ContinueAmount. With this PR, both inputs reach openOnchainConfirmForInvoiceAmount (AppViewModel.kt:5402) and open Confirm instead. The PR's own test send sheet onchain address with payable amount navigates to confirm asserts exactly that. How I confirmed it: I downloaded send.e2e.ts from e2e main and traced both inputs through onScanOnchain. I also checked the branch lookup and the workflow's ref resolution. The PR's live send shard (run 41648112580) had not finished when I checked at 14:40 UTC, so I have not seen the failure itself. The author's Gaps section predicts the same break. Once this merges, the send shard fails on every later Android PR that touches app/** until e2e main is updated. Fix: push the spec update to fix/scan-amount-to-confirm in bitkit-e2e-tests so this PR's E2E run picks it up, link it as Companion:, and merge the two in lockstep. @send_2 is also an iOS gate, so coordinate with the iOS twin too.

<action>Enable USDT in the introductory sheet or Settings, General, Payments before starting this flow.</action>
<action>Open an ethereum: USDT receive link with chain 42161, the pinned Arbitrum token, an owned recipient and uint256=10001. Verify the USDT amount screen preserves the recipient and exact input 0.010001 USDT; the review rounds its displayed value to $0.01. Verify no payment occurs without approval.</action>
<action>Open an ethereum: USDT receive link with chain 42161, the pinned Arbitrum token, an owned recipient and uint256=10001. Verify the link opens on payment review without tapping Review, preserving the recipient and exact input 0.010001 USDT; the review rounds its displayed value to $0.01. With a USDT balance below the amount plus fees, verify the amount screen stays open with the same insufficient-balance or fee error that tapping Review shows. Go back to the recipient and continue again; verify the review is not requested again until Review is tapped. Verify no payment occurs without approval.</action>
<action>Copy the USDT receive URI, return to Home and accept the clipboard prompt. Verify the USDT amount screen opens with the copied recipient and amount. Cancel without paying. Repeat with a wrong-chain URI and verify no payment prompt appears.</action>

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.

worth doing, does not block: USDT journey clipboard step still says the amount screen opens

Step 6 of usdt-payment-links-and-scans.xml is stale. The clipboard prompt goes through handleUsdtPaymentInput, which navigates to Routes.UsdtPayment(recipient, amount) (AppViewModel.kt:4942). Links use the same route, so InitialUsdtQuoteEffect now quotes on entry for a copied URI too. With enough USDT, a copied URI that carries an amount opens on review. Step 6 still says "Verify the USDT amount screen opens with the copied recipient and amount". The PR updated step 2 for links but not this step. Reword it the way step 2 now reads: the review opens, or the quote error shows on Amount.

): Boolean {
if (_sendUiState.value.payMethod != SendMethod.ONCHAIN || hasActiveContactPaymentContext()) return false
if (!validateAmount(amount)) return false
val isHardware = _sendUiState.value.hardwareWalletId != null

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.

worth doing, does not block: Hardware-wallet direct-Confirm path has no test

openOnchainConfirmForInvoiceAmount (AppViewModel.kt:5426-5430) has one new branch that does real work before navigating. For a hardware wallet it calls prepareHardwareSendFee(). If that fails, it shows the error toast and falls through to Amount. None of the added tests sets a hardware wallet. The PR also says the Trezor flow was not driven on a device. The existing suite already stubs hwWalletRepo.estimateFundingMiningFee (for example AppViewModelSendFlowTest.kt:6489). That makes two cheap tests possible: - A successful estimate opens Confirm with the fee in onchainFeeUi. - A failed estimate opens Amount.

Comment on lines +5415 to +5418
/**
* An on-chain invoice with a payable amount opens on Confirm, as Amount's continue would.
* Manual coin selection and contact payments keep going through Amount.
*/

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.

nit: KDoc on a private function

openOnchainConfirmForInvoiceAmount is private but has a KDoc block. CLAUDE.md says "NEVER add code comments to private functions".


Logger.info("Found payable amount in invoice, proceeding to confirm", context = TAG)
_sendUiState.update { it.copy(isAmountInputValid = true, canReturnToAmount = true) }
navigateToSendRoute(fromMainScanner, SendRoute.Confirm, SendEffect.NavigateToConfirm, walletGeneration)

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.

worth doing, does not block: Tapping the amount on direct Confirm does nothing

Open a funded BIP21 request with an amount, then tap the amount on Confirm. Nothing happens: BackToAmount still emits PopBack(Amount), but this shortcut creates no Amount entry. The amount tap needs the same fallback as the Back arrow.

Comment on lines +45 to 46
<action>Verify the Send Confirm screen opens (title "Confirm", amount testTag "ReviewAmount") showing 10 000 sats and the Receive sheet (testTag "ReceiveScreen") is gone</action>
<action>Press back to close the Send sheet and verify the wallet home screen is visible</action>

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.

worth doing, does not block: The PIN journey expects Home one Back press too early

J8's final assertion fails under the new navigation. After unlocking opens Confirm, one Back press now opens Amount, but the next action expects the sheet to close and Home to appear. The journey needs another Back press before checking Home.

@pwltr pwltr left a comment

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.

Reviewed commit 1d280b6c59a51ede602149b453981acf160656b7 through general, security and cross-platform source review, followed by targeted finding verification.

[P2] Tapping the amount cannot edit it after opening confirmation directly

For a valid amount-bearing Bitcoin request with Autopilot coin selection, the new flow opens Confirm without adding Amount to the navigation stack. Tapping the amount still emits BackToAmount, which becomes PopBack(Amount). SendSheet.kt only calls popBackStack for that effect, so the tap silently does nothing when Amount is absent.

The new fallback is wired to toolbar/system Back, which remains a workaround, but the amount-edit control does not use it. Route that control through the same fallback, or explicitly open Amount when it is missing. This is an introduced navigation regression confirmed by source tracing; it does not itself submit a payment.

Review details

General review
Model: Claude Opus 5.5
Reasoning effort: xhigh

Security review
Model: Kimi K3
Reasoning effort: max

Cross-platform review and verification
Model: GPT-6.1-sol
Reasoning effort: xhigh

This branch has not been deployed

No deployments
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.

5 participants