What
-
Card withdraw: moves money from the card account to the user's USD wallet through KeySecure
account/transfer Out.
-
Card detail redesign: the screen now has View / Withdraw / Add fund / Setting actions.
-
Card tab redesign: follows Figma. It has Virtual/Physical tabs (Physical shows Coming soon) and Default + Backup card sections.
-
Card history: moved to its own
/card-history screen.
Backend
-
New route:
POST /card-account/withdraw takes { amount, passcode }.
- Guarded by passcode and listed in MONEY_ROUTES.
- Free; minimum 1 USD, at most 2 decimals.
- Pending swipe holds are excluded from the withdrawable balance.
-
Request time: one DB transaction does three things:
- debits the card ledger (
WITHDRAW_OUT);
- moves the mirror from normal.CARD_USD to reserve.CARD_USD;
- writes a
CARD_WITHDRAW transaction in PROCESSING and a card_transfers Out outbox row.
-
onApplied: credits the wallet from RESERVE USD and emits the socket event
card.withdraw.settled.
-
onFailed: re-credits the card (
WITHDRAW_REVERSAL) and reverses the mirror. The reserve.CARD_USD leg may go negative here (allowNegativeCardReserve, which only that account accepts).
-
Provider answers: every Out 3002/3008 and every 3004 is resolved by looking up the
transfer/list record status.
- Success → applied
- Pending → pending
- Fail → failed
- unknown → pending + alert
- failed lookup → pending
-
No migration: the new enum values go into varchar columns.
Frontend
-
/transfer?direction=out: From is the card, To is the fiat wallet. Max and validation use the spendable balance.
- Fee now shows
0.00 USD in both directions, replacing a hard-coded 0 PRV.
- The receipt says "Withdrawal submitted / processing".
- History shows a
Card Withdrawal label.
- The socket listener for
card.withdraw.settled is added, and withdraw is on the passcode-401 rule.
Verification
- be: card-account, transactions and auth jest pass (295 card-account tests);
yarn build is clean.
- fe: full suite passes (1312 tests), type-check is clean, lint has 0 errors.
-
reconciliation.card-flow.postgres.spec passes 10/10 on a throwaway Postgres, including the withdraw settle and refuse cases.
- The Opus review gate passed in round 4.
- Simulator screenshots were taken of the Card tab, Detail card, Card history and Withdraw.
-
Not yet verified: a real withdrawal on staging. The test card balance is 0.
Follow-ups (not in this MR)
-
card-add-fund.tsx and CardStack are now unused.
- The pre-existing lock inversion in deposit
onFailed.
- There is no hold for a pending Out against concurrent swipes.
- The FE assumes the card deposit fee is 0.
🤖 Generated with Claude Code