Skip to content

feat(wallet): tick the balance up when a scanned bill is claimed - #1369

Merged
bmc08gt merged 4 commits into
code/cashfrom
feat/wallet-claim-reveal
Aug 31, 2026
Merged

feat(wallet): tick the balance up when a scanned bill is claimed#1369
bmc08gt merged 4 commits into
code/cashfrom
feat/wallet-claim-reveal

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Put in Wallet dismissed the bill and left the user on the scanner. The money they had just taken only showed up the next time they opened the wallet, already counted.

The button now takes them to the wallet tab and the balance rolls up on arrival.

Why there's a coordinator for this

The roll needs a "before" to start from, and there isn't one by the time the wallet opens. tokenCoordinator.add credits the account when the bill is grabbed, several taps before the claim, so every flow the wallet reads has already moved.

WalletRevealCoordinator carries the pre-credit picture across that gap:

  1. CodeScanDelegate captures the totals immediately before the credit.
  2. claimReceivedFunds() arms the snapshot and dismisses the bill. It's a separate BillOperations method rather than a branch inside dismissBill(PutInWallet), because that same result also covers a grab timeout, a cancel, and a swipe-away, none of which is the user asking to see their wallet.
  3. The wallet opens on the snapshot, holds it while the bill leaves, then releases to live values.

The header and each card's balance already draw through AnimatedNumberText, so the roll itself is just the rendered string changing. The claimed token's own card balance is held back too, so its number rolls in step with the total above it instead of sitting there already updated.

When the hold ends

The bill returns on a 600ms slide and the tab crossfades under it over 300ms. A hold that ignores both runs the balance up while it is still covered, spending the animation where nobody can see it.

So the reveal ends on the later of two clocks, both started at the tap: 450ms for the bill to clear, and 150ms from the wallet reporting itself drawn, so a tab that took a while to appear still shows the pre-claim figures instead of rolling them on its first frame. A reveal nobody collects releases itself after 3s, which bounds the whole wait and also means there's no logout or reset wiring to keep in sync.

This is the same sequencing as iOS's depositRevealDelay in code-payments/code-ios-app#697.

Only scanned bills route

A cash link is claimed from a link rather than from the scanner, so nothing is captured for it. arm() returns whether there was a snapshot, claimReceivedFunds() passes that back, and the decorator navigates only on true. The cash link confirmation dismisses where it stands, as it did before. iOS is aligning to the same exemption.

New card arrival

A claim in a currency the wallet didn't hold reaches TokenCardStack as arrivingMint, with arrivalHeld set while the reveal is up. The card is in the deck throughout, so the layout is final from the first frame; it just starts 40dp low and transparent, and rises into place over 900ms once the hold ends. Nothing around it moves. This is iOS's arrivalProgress effect, at iOS's own distance and duration.

The caller names the arriving card rather than the stack diffing its own token list. The case that most needs the animation, a wallet that held nothing so the stack was never composed, is exactly the one a diff can't see.

The mint-balance rewind applies only to a currency the wallet already held, so its card number rolls in step with the total above it. A card that is only now arriving has no earlier number to roll from and rises showing what it holds.

Tests

WalletRevealCoordinatorTest covers the capture/arm/hold/release cycle: a capture is consumed once so a second tap can't replay it, arming with nothing captured publishes nothing and reports false, a redraw part way through the hold doesn't extend it, and an uncollected reveal times out. Both clocks have a case — a wallet that draws straight away still waits out the bill, and one that takes longer than the bill still gets its beat on arrival. CodeScanDelegateTest asserts the ordering the whole thing rests on, that the snapshot is taken before the credit. BillPresentationDelegateTest covers both claim paths and checks the other PutInWallet dismissals leave the reveal alone.

@bmc08gt bmc08gt self-assigned this Aug 28, 2026
"Put in Wallet" dismissed the bill and left the user on the scanner, so money
they had just taken only showed up the next time they opened the wallet,
already counted.

The button now hands them to the wallet tab and the balance rolls up on
arrival. That roll needs a "before" to start from, and there isn't one by
then: `tokenCoordinator.add` credits the account when the bill is *grabbed*,
several taps earlier, so every flow the wallet reads has already moved.
`WalletRevealCoordinator` captures the pre-credit totals at the grab,
`claimReceivedFunds` arms them, and the wallet opens on that snapshot for
450ms before releasing to live values. Both the header and each card's balance
already draw through `AnimatedNumberText`, so the roll is just the rendered
string changing.

Only a scanned bill routes. A cash link is claimed from a link rather than
from the scanner, so nothing is captured for it, `arm()` reports that, and its
confirmation dismisses where it stands as before.

A claim in a currency the wallet didn't hold is withheld from the deck for the
same beat, then passed to `TokenCardStack` as `enteringMint`, which opens a
slot for it and fades it in. The caller names the card rather than the stack
diffing its own token list, because the case that most needs the animation, a
wallet that held nothing so the stack was never composed, is the one a diff
can't see.

The hold is timed from the wallet reporting itself drawn rather than from the
tap, so a slow tab doesn't spend it behind the loading spinner, and a reveal
nobody collects releases itself after 3s.
@bmc08gt
bmc08gt force-pushed the feat/wallet-claim-reveal branch from e204102 to 10b4a63 Compare August 28, 2026 21:52
@github-actions github-actions Bot added type: feature New functionality area: payments Payments, transfers, intents, billing area: tokens Token accounts, balances, token info area: session and removed type: feature New functionality labels Aug 28, 2026
The reveal released 450 ms after the wallet reported itself drawn, a clock
unrelated to when the bill it came from finishes leaving. The bill returns on a
600 ms slide and the tab crossfades under it over 300 ms, so the balance
regularly ran up while it was still covered and the arrival went unseen.

Release now waits on the later of two clocks, both started at the tap: 450 ms
for the bill to clear, and 150 ms from the wallet reporting itself drawn, so a
tab that took a while to appear still shows the pre-claim figures instead of
rolling them on its first frame. The unclaimed-reveal fuse is unchanged and now
bounds the whole wait. Same sequencing as iOS's depositRevealDelay (#697).

The card-entry retention window runs from the reveal ending rather than from the
mint being flagged, because the hold before it no longer has a fixed length.
@github-actions github-actions Bot added the type: feature New functionality label Aug 31, 2026
… open

The claimed card was kept out of the token list until the reveal ended, then
inserted, so the cards below it shifted down as its slot opened. iOS animates the
same arrival with the deck's layout final from the first frame: only the arriving
card's own offset and opacity move, rising 40dp into a slot already made for it.

Match that. TokenCardStack takes `arrivingMint` + `arrivalHeld` in place of
`enteringMint`: the card is in the deck throughout, held off-stage while the
reveal is up, and released to rise over 900ms, iOS's own duration. The placement
pass loses its slot-opening shift, so nothing around the card moves.

The mint-balance rewind now applies only to a currency the wallet already held. A
card that is only now arriving has no earlier number to roll from, and rises
showing what it holds.
… below it

Dollars and Dad Cash traded places in the card stack while the user watched. Both
read $1.00, so nothing visible decided which sat on top — but the sort compared
raw `Fiat`, which carries six decimal places against the two USD displays, and a
launchpad currency's value moves in those hidden digits on every price refresh.
Each refresh re-decided the order and the cards swapped.

Sort on the displayed value instead, leaving the existing name tiebreak to settle
cards that read the same. The comparator moves to a named `BalanceOrder` so the
behaviour can be tested directly.

The arrival animation made this easy to hit: the new card is now in the deck from
the first frame, so refreshes land while it is on screen rather than before it is
drawn. iOS sorts on the exact value too and has the same swap latent in it.
bmc08gt added a commit to code-payments/code-ios-app that referenced this pull request Aug 31, 2026
The wallet's card stack sorted on `StoredBalance.usdf`, which carries six
decimals against the two USD renders. Reserves sit at exactly 1.000000, while a
launchpad currency's USD worth is a bonding-curve result landing on arbitrary
sixth decimals that moves on every price refresh. With both cards reading
$1.00, each refresh re-decided which was greater and the two traded places on
screen. The stack positions cards by index with no per-card position animation,
so the swap reads as a jump.

Extract the comparator as `StoredBalance.walletOrder` and compare the values
rounded to USD display precision. The existing alphabetical-by-name tiebreak
then settles cards showing the same figure, identically on every refresh.

Ports the Android fix in code-payments/code-android-app#1369.
@bmc08gt
bmc08gt merged commit 0d7e0cb into code/cash Aug 31, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/wallet-claim-reveal branch September 5, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: payments Payments, transfers, intents, billing area: session area: tokens Token accounts, balances, token info type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant