Skip to content

feat(bills): scanning a tip card lands in the chat - #1461

Merged
bmc08gt merged 8 commits into
code/cashfrom
feat/scan-lands-in-chat
Sep 15, 2026
Merged

bmc08gt merged 8 commits into
code/cashfrom
feat/scan-lands-in-chat

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Node 10074:18893.

Scanning someone's tip card raised a modal over the card, took a tip amount, sent it, and only then opened the chat. Searching for the same person by @handle opened their chat straight away and let the payment happen inside it. Two routes to the same person, two different flows, and only one of them could reach a conversation that hadn't started yet.

Scanning now does what the search does. The card holds for 450ms — the same beat the modal used to take sliding up — and then the chat opens on ChatIdentifier.ByUser, started or not. The fee is charged by the chat's own call to action, which is the same fee the modal was collecting.

A card whose profile has no server-provided userId has no chat to open, so it stays on screen rather than dismissing into nothing.

The deposit gate goes with the modal: the chat is worth reaching whether or not you can afford the fee today, and the chat's send path already prompts to add money. BillOverlay's inset and vertical-bias animations go too — they existed to slide the card up out of the modal's way, and now run to a target the card already sits at.

TipUserModal is deleted. The tip-amount flow it was the only entry point to — AppRoute.Sheets.TipAmountEntry, TipAmountEntryScreen, TippingCoordinator.confirmTip, TokenPurpose.Tip — is now unreachable and comes out in a follow-up, to keep this PR about the behaviour change. action_swipeToTip stays for the same reason: the chat's own swipe label still reads it.

maestro/tip_deeplink.yaml waited on the modal's "Swipe to Tip"; it now waits on chat_screen.

The card face carries the display name alone. It read Tip Erik over the handle, and a scan no longer takes a tip. label_tipUser stays — the tip flow and the menu still use it.

On the way out the card fades and scales rather than sliding back to the wallet, and the chat is pushed 50ms into that. Dismissing and pushing in the same frame removed the card instead of animating it out; a 60fps capture had it opaque in one frame and gone 34ms later. The hand-off runs NonCancellable because dismissing clears the bill, which restarts the effect that owns it and was cancelling the push outright.

Nothing presents it: the scanned card now goes straight to the chat, and
the amount it used to take is entered in the chat instead.

action_swipeToTip stays for now — the chat's own swipe label still reads
it, and that caller goes away with the "Start Chatting" CTA change.
The inset and vertical-bias animations existed to slide the tip card up
out of the modal's way. Nothing reports a management height for a tip
card now, so both animations ran to a target they already sat at.

The min-tip affordability read went with them: the modal was its only
consumer, and the chat does its own balance check.
The flow waited on "Swipe to Tip" and asserted "Tip Brandon McAnsh",
both of which belonged to the deleted modal.

It now waits on chat_screen, the id the route registration derives, so
the assertion survives the chat's call-to-action copy changing.
@bmc08gt bmc08gt self-assigned this Sep 14, 2026
@github-actions github-actions Bot added type: feature New functionality area: payments Payments, transfers, intents, billing and removed type: feature New functionality labels Sep 14, 2026
The scanned card's dismissal used PutInWallet, whose exit is a 600ms slide
back down to the wallet. The bill overlay lives inside the navigation entry
being pushed away from, so the entry starts sliding out roughly 50ms later
and takes the card with it — a recording of the handoff shows the card
leaving at full size and full opacity, never having animated at all.

Dismiss with Grabbed instead: fadeOut(100ms) + scaleOut, the same pop the
card already enters with, short enough to finish inside the handoff. Call it
before the push so both start on the same frame.

ScannableDecoratorContext.onDismiss now takes the BillDeterminationResult,
since the result is what picks the exit; PayableDecorator passes the
PutInWallet it had hardcoded.
@github-actions github-actions Bot added the type: feature New functionality label Sep 15, 2026
The card read "Tip Erik" over the handle. Scanning it no longer takes a tip —
it opens the chat, where sending money is one of several things you can do — so
the face now carries the display name alone and leaves the verb out of it.

`label_tipUser` stays; the tip flow and menu still use it.
Dismissing the card and pushing the chat in the same frame removed the card
instead of animating it out. A 60fps capture shows it fully opaque in one frame
and gone in the next, 34ms later, where the 100ms fade-and-scale should have
spanned several. A 50ms head start puts that fade under way before the push
takes the outgoing entry; the same capture now shows the card half-faded and
scaled up while the chat is already sliding in.

The delay also has to outlive the effect it runs in. Dismissing sets the bill to
null, which flips `tipPresented` and restarts `LaunchedEffect`, and that was
cancelling the pending push — the chat never opened at all. `NonCancellable`
around the hand-off keeps it.
The hand-off into the chat dismisses the bill ~50ms before it pushes, and the
push is what eventually detaches the analyzer. For the length of the transition
the camera is still on the card with every gate open: `onCodeScan`'s "a bill is
up" check has just been released, `scannedRendezvous` is only ever written on the
cash path, and `TipCardDelegate.inFlight` cleared when the first profile fetch
returned. The card re-resolves and re-presents over the chat it just opened, and
that copy of the decorator pushes a second chat behind it.

Tip payloads now carry their own suppression, keyed on user id. A cooldown rather
than the permanent entry cash gets, because a tip card is a person and scanning
them again later is legitimate — 3s, the same window the QR analyzer already
debounces the other scan route to this card by. Accepting a scan refreshes the
window and a suppressed frame does not, so holding the camera on a card runs the
cooldown out instead of extending it.

Synchronized: MultiCodeAnalyzer decodes each frame in its own coroutine, so an
unguarded check-then-set lets two frames both through.
@bmc08gt
bmc08gt merged commit a8e1092 into code/cash Sep 15, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/scan-lands-in-chat branch September 15, 2026 18:06
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 type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant