Skip to content

fix(tipcard): take the keyboard down before handling a deeplink - #1323

Merged
bmc08gt merged 4 commits into
code/cashfrom
fix/tip-card-deeplink-keyboard
Aug 25, 2026
Merged

fix(tipcard): take the keyboard down before handling a deeplink#1323
bmc08gt merged 4 commits into
code/cashfrom
fix/tip-card-deeplink-keyboard

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Tapping a tip link while resuming from a chat presents the tip card over a live keyboard. The chat's message input still holds focus across the background, so the window restores the IME as the link is routed — and the card resolves over a network round-trip, so the keyboard is up for that whole window too.

Changes

App.kt — dismiss the keyboard once a deeplink dispatches to a real action, before anything is routed or presented.

Scoped to any non-None action rather than just PresentTipCard: every branch there reroutes what's on screen, including /tip/{self}, which AppRouter diverts to a Menu-sheet Navigate and which had the same problem. The one path that doesn't navigate — an email code delivered in place via EmailCodeChannel — auto-verifies on arrival, so taking the keyboard down there is right too.

Cold start is untouched: onPendingAction only fires from MainRoot on the Loading route, where no keyboard can be up.

BillOverlay.kt — catches an IME the platform restores after we've routed.

Fires only on a false -> true transition. The overlay is hosted per navigation entry by NavBillOverlayEntryDecorator, so a screen opened while a bill is up composes a fresh copy with the bill already present. Unguarded, that would stomp TipCardDecorator's LaunchChat, which opens the chat with openKeyboard = true on purpose.

KeyboardController.hide() — now clears editor focus before hiding the IME. Hiding alone leaves the field focused, so the platform brings the keyboard straight back; clearing focus removes the target it would be restored onto.

This started as a separate dismiss() and folded back in, because no caller wants the other behaviour. hideIfVisible is 29 of them, and every one hides, waits out the IME animation, then pops, dismisses a sheet, or advances a flow step. The two direct hide() callers pop or dismiss on tap. Both show() callers request focus explicitly first, so nothing pairs a hide with a later show that depends on retained focus — which left retainFocus as a flag with no false branch.

So the hideIfVisible sites get the fix too. They had the same latent problem as the tip card: IME down, field still focused, keyboard restored on the way back from the background.

Tests

KeyboardControllerTest pins the focus half of hide. hideIfVisible inherits it by delegation but isn't covered: it gates on visible, read from window insets that Robolectric never reports for the IME. OnBillPresentedTest covers the transition guard, which moves out of BillOverlay into an internal OnBillPresented composable to be reachable from a test — an already-presented bill at first composition is a no-op, a bill that appears while watching is not.

Tapping a tip link while resuming from a chat presented the tip card over a
live keyboard. The chat's message input still holds focus across the
background, so the window restores the IME as the link is routed, and the card
resolves over a network round-trip with the keyboard up the whole time.

Dismiss the keyboard in App once a deeplink dispatches to a real action, before
anything is routed or presented. This covers every branch there, not just
PresentTipCard: they all reroute what's on screen, including /tip/{self}, which
AppRouter diverts to a Menu-sheet Navigate. The one path that doesn't navigate
(an email code delivered in place via EmailCodeChannel) auto-verifies on
arrival, so taking the keyboard down there is right too.

BillOverlay catches an IME the platform restores after we've routed. It fires
only on a false -> true transition: the overlay is hosted per navigation entry,
so a screen opened while a bill is up composes a fresh copy with the bill
already present, and firing there would stomp TipCardDecorator's LaunchChat,
which opens the chat with the keyboard up on purpose.

KeyboardController.dismiss() clears editor focus before hiding. hide() alone
leaves the field focused, so the platform brings the keyboard straight back.
@github-actions github-actions Bot added type: fix Bug fix area: payments Payments, transfers, intents, billing area: ui Compose UI, theme, components, resources and removed type: fix Bug fix labels Aug 25, 2026
Two behaviours carry the fix and neither had a test.

KeyboardControllerTest pins the hide/dismiss split: `hide` leaves the editor
focused, `dismiss` doesn't. Focus is the mechanism — the window restores the IME
for whatever still holds it on the way back from the background — so the tree
needs a non-editor focusable beside the field. `clearFocus` hands focus to the
next candidate rather than leaving none, and a tree whose only focusable is the
editor takes it straight back.

OnBillPresentedTest covers the transition guard, which is the part most likely to
be wrong: the overlay is hosted per navigation entry, so a bill already up at
first composition must not count as a presentation, or the fresh chat entry would
take down the keyboard that the post-tip hand-off opened on purpose.

The guard moves out of BillOverlay into an internal OnBillPresented composable to
make it reachable from a test.
@github-actions github-actions Bot added the type: fix Bug fix label Aug 25, 2026
dismiss() existed so a caller could opt into clearing editor focus before
hiding the IME. Across the codebase no caller wants the other behaviour:
hideIfVisible accounts for 29 sites and every one hides, waits out the IME
animation, then pops, dismisses a sheet, or advances a flow step; the two
direct hide() callers pop or dismiss on tap. Both show() callers request focus
explicitly first, so nothing pairs a hide with a later show that depends on the
field still holding focus.

That makes the safe behaviour the one worth defaulting to, and a retainFocus
parameter a flag with no false branch. Folding it into hide() also carries the
fix to the 29 hideIfVisible sites, which had the same latent problem as the tip
card: IME down, field still focused, keyboard restored on the way back from the
background.

Tapping the message list to put the keyboard away now deselects the composer
too, matching the rest of the app's leave-the-field behaviour.
@bmc08gt
bmc08gt merged commit 4919d89 into code/cash Aug 25, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the fix/tip-card-deeplink-keyboard branch August 25, 2026 14:07
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: ui Compose UI, theme, components, resources type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant