Skip to content

fix(cash-link): hold the claim guard everywhere and show a claiming voucher - #1678

Merged
bmc08gt merged 5 commits into
code/cashfrom
fix/cash-link-claim-in-flight
Oct 5, 2026
Merged

bmc08gt merged 5 commits into
code/cashfrom
fix/cash-link-claim-in-flight

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

A cash link could be claimed twice at once, and the chat voucher gave no sign a claim was running. iOS triaged the same flow in Bugsnag 6a7b33bea531fed8b9a8b0d1 (staleState(race detected…)); this is the Android side.

Android's matching group is 6ab9844f396a2e95206cba57 (gift card balance has already been claimed, 112 events / 12 users). There is no race detected group on Android: races are retried 3× (InternalTransactionRepository.kt:37-41), and a lost race ends on the already-claimed rejection instead. #1675 already shows "Already collected" for that rejection. It isn't in production yet (2026.9.4 / 4676 predates it).

Changes

  • Guard on the "collect anyway" reclaim. The own-cash prompt called claimGiftCard after the first attempt had already cleared giftCardClaimInProgress, so re-tapping the voucher during the reclaim started a second claim. claimGiftCard now takes the slot itself and returns whether it started. The routed deeplink event is recorded only for a claim that started.
  • CashLinkClaims.claimInFlight. Publishes the delegate's slot.
  • Claim.Claiming on the chat voucher. ChatViewModel forwards claimInFlight to LinkCardResolver.markClaiming, which draws a resolved, claimable voucher with that entropy as Claiming. It's an overlay, not a stored answer. The voucher shows "Claiming…" instead of the claim pill and ignores taps.
  • Settle before freeing the slot. onReceived/onError emitted the SettledClaim after clearing the slot, so the voucher could read "Tap to claim" from the cached answer for one update before the settlement invalidated it.
  • Keep the alert for a dropped link. openCashLink cleared the bottom bar before the in-flight check, so a link opened during a claim was dropped and also dismissed the current alert.

Not in this PR

  • Treating a $0 NotClaimed card as already claimed (an iOS cause). The account fetch before each of three sampled Android rejections showed a non-zero cached balance, so this wouldn't have caught them. It's held for parity with whatever iOS ships.
  • Mapping a race that survives the retries to AlreadyClaimed. There are no Android events for it.
  • Keying the guard per card. BillTransactionManager holds one receive transactor and disposes it when a new claim starts.

"Claiming…" is placeholder copy until it's matched to iOS.

The own-cash prompt's "Collect" called claimGiftCard directly. By then
the first attempt's onError had already cleared giftCardClaimInProgress,
so tapping the same voucher again during the reclaim started a second
claim of the same card.

claimGiftCard now takes the slot itself and returns whether the claim
started. The routed deeplink event is recorded only when it did, so a
dropped duplicate is not counted as routed.
The chat voucher only learns about a claim when it settles, so it keeps
offering "Tap to claim" while the claim runs. claimInFlight publishes
the delegate's existing guard slot so a surface can show that link as
claiming.

The messenger tests' relaxed CashLinkClaims mocks now return a real
StateFlow for it, ahead of ChatViewModel collecting it.
A chat voucher kept reading "Tap to claim" until its claim settled, so a
re-tap during a slow claim looked like the first tap did nothing.

ChatViewModel forwards CashLinkClaims.claimInFlight to LinkCardResolver,
which draws a resolved Claimable voucher with that entropy as the new
Claim.Claiming. It is an overlay only: the stored lookup answer is
untouched, so the card returns to the lookup's state when the claim
settles. The voucher shows "Claiming…" in place of the pill, and a tap
on it is ignored.
…ts for dropped links

Two ordering problems in CashLinkDelegate:

- onReceived and onError cleared giftCardClaimInProgress before emitting
  the SettledClaim. ChatViewModel redraws on both, so the voucher could
  read "Tap to claim" from the cached answer for one update before the
  settlement invalidated it. The settlement is now emitted first.
- openCashLink cleared the bottom bar before the in-flight check, so a
  link opened during a claim was dropped and also dismissed whatever
  alert was on screen. It now clears only when no claim is running.
@bmc08gt bmc08gt self-assigned this Oct 5, 2026
@github-actions github-actions Bot added type: fix Bug fix area: session and removed type: fix Bug fix labels Oct 5, 2026
@bmc08gt
bmc08gt merged commit c49428d into code/cash Oct 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant