feat(wallet): tick the balance up when a scanned bill is claimed - #1369
Merged
Conversation
"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
force-pushed
the
feat/wallet-claim-reveal
branch
from
August 28, 2026 21:52
e204102 to
10b4a63
Compare
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.
… 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.
This was referenced Aug 31, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Put in Walletdismissed 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.addcredits the account when the bill is grabbed, several taps before the claim, so every flow the wallet reads has already moved.WalletRevealCoordinatorcarries the pre-credit picture across that gap:CodeScanDelegatecaptures the totals immediately before the credit.claimReceivedFunds()arms the snapshot and dismisses the bill. It's a separateBillOperationsmethod rather than a branch insidedismissBill(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.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
depositRevealDelayin 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 ontrue. 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
TokenCardStackasarrivingMint, witharrivalHeldset 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'sarrivalProgresseffect, 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
WalletRevealCoordinatorTestcovers 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 reportsfalse, 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.CodeScanDelegateTestasserts the ordering the whole thing rests on, that the snapshot is taken before the credit.BillPresentationDelegateTestcovers both claim paths and checks the otherPutInWalletdismissals leave the reveal alone.