Skip to content

TASK-23054 fix withdraw Continue on EUR amounts and the crypto address book step - #3443

Merged
Hugo0 merged 2 commits into
devfrom
task-23054-withdraw-regressions
Sep 24, 2026
Merged

Hugo0 merged 2 commits into
devfrom
task-23054-withdraw-regressions

Conversation

@Hugo0

@Hugo0 Hugo0 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Two withdraw regressions from staging QA on 2026-09-24 (TASK-23054).

1. Continue on a EUR amount did nothing (blocker)

Cause. In AmountInput, the effect that reports the typed amount to the parent listed the secondaryDenomination prop as a dependency. Every caller passes that prop as an object literal, so the effect ran again on every parent render. Since #3438 the bank-currency field reports into setDestinationAmount, which is URL state (nuqs). nuqs writes the URL on every call, even when the value is the same. Under Next.js each URL write dispatches a router restore, and a restore discards the navigation still in flight. Continue re-renders the page (setError), so the push to /withdraw/<country>/bank was discarded every time. The UI never reached the review, so the API saw no quote or offramp request.

Fix.

  • AmountInput depends on the field-keyed denominations memo, not the prop object. It reports only when an amount or a denomination really changes.
  • handleDestinationAmountChange skips an unchanged amount. A 30-second quote refresh re-reports the same bank amount and would otherwise write the URL again.

The USD amount step and Manteca are not affected: the USD step has no secondary denomination, and Manteca stores its amounts in useState.

2. Withdraw → Crypto skipped the address book

  • The full method list (?showAll=true, for example the fallback when Back leaves the crypto or Manteca page with no in-app history) went from Crypto straight to an empty destination form. With saved addresses, Crypto now returns to the saved-destinations screen, where the addresses are listed.
  • TASK-23054 send, request and receipt fixes from staging QA #3433 (QA-30) computed hasSavedDestinations from the addresses offered on the current rail. For a user whose only saved destinations are crypto addresses, Back from the bank list (rail=bank) left Withdraw instead of returning to the address book. The count now leaves the addresses out only for Send → Bank.
  • Send → Bank still shows no address book and no crypto row.

3. Withdraw quote and rate follow-ups (code audit W4, W1 rows)

  • One rate source. The bank minimum now converts with the quote rate that the amount already uses, on the amount step and at submit. The withdraw flows no longer call the old /bridge/exchange-rate hook.
  • No made-up rate. useGetExchangeRate answered '1' when the call failed. That rate showed "1 USD = 1.0000 EUR" and turned the COP minimum into $4,000. A failed call is now an error with no rate, and ExchangeRate shows "-".
  • A failed quote refresh keeps the flow mounted. useBridgeOfframpQuote keeps the last quote when a 30-second refresh fails, and isError shows that the quote is not current. The review page shows the Retry screen only before the first quote. After a failed refresh, the review shows the error inline with a retry, and an open KYC, terms or confirm step stays open. Submit is disabled, and the handler also refuses a quote that is not current, because the terms step calls the handler directly. On the amount step the field stays open with the last rate.
  • Mid-typing amounts. Before "90." or ".5" goes into the URL, it is converted to the form the API accepts ("90", "0.5"). The review page reads the URL the same way, and an amount the API refuses counts as no amount, so the review sends the user back to /withdraw instead of showing "rate unavailable" in a loop.
  • Manteca lock expiry (withdraw and QR pay). Each lock gets a deadline on the device clock when it arrives (receiveLock in src/utils/price-lock.utils.ts): the time left that api#1707 measures (expiresInMs), counted from arrival. A phone clock that runs fast no longer expires a live lock. Without the field there is no device-side check, and the provider rejects an expired lock on submit. The QR warm-up (useSmartSpendPreparation) takes the same deadline. The openapi snapshot declares no response body for these routes, so no type regeneration is needed.
  • Back without history. The crypto and Manteca pages fell back to /withdraw?showAll=true, the full method list, which skipped the saved addresses. They now fall back to /withdraw. It shows the saved destinations when the user has any, and the method list otherwise.
  • Submit Retry (Chip). After a failed submit followed by a failed quote refresh, the submit Retry is disabled like the normal submit, and only the quote Retry stays live.

Tests

  • withdraw-states.test.tsx: the real AmountInput on a EUR account, with URL writes that re-render the page. Type EUR 5, re-render, and assert that nothing writes the URL and that Continue pushes /withdraw/germany/bank?destinationAmount=5. This test fails on dev.
  • AmountInput/__tests__/reporting.test.tsx: a parent re-render with equal denominations reports nothing. A rate change still reports again.
  • WithdrawMethodView.test.tsx: Crypto with saved addresses shows them. Crypto with nothing saved opens the form. Back from the bank list returns to the address book. Send → Bank still leaves the flow on Back and never shows the address book.
  • useBridgeOfframpFlow.test.tsx: the GB minimum converts with the quote rate. A failed refresh keeps the quote but submit is blocked, also through a direct handler call. 90. and .5 are quoted as 90 and 0.5. 12.345 sends the user back to the flow entry.
  • withdraw-states.test.tsx: a failed refresh keeps the field open. £2 at 0.79 is under the £3 minimum, with no call to the old rate hook. A typed 90. is handed on as 90.
  • WithdrawBankReviewView.test.tsx: the inline quote error with retry. useBridgeOfframpQuote.test.tsx and useGetExchangeRate.test.ts cover the new failure behavior. bank-amount.test.ts covers normalizeBankAmount.
  • price-lock.utils.test.ts, manteca-withdraw-gates.test.tsx and qr-pay-states.test.tsx: a lock with no time left re-quotes. A phone clock that runs fast does not expire a live lock, and neither does a response without expiresInMs. useSmartSpendPreparation.test.tsx uses the device deadline.
  • crypto-withdraw-confirm.test.tsx and manteca-withdraw-gates.test.tsx: Back without history falls back to /withdraw. WithdrawBankReviewView.test.tsx: the submit Retry waits for a current quote.

Local: tsc --noEmit, eslint on the changed files, full jest (--runInBand) and npm run build pass.

…s book step

Continue on a EUR amount did nothing on staging. AmountInput re-reported the
amount on every parent render because it depended on the secondaryDenomination
object literal. Since the bank-currency field reports into URL state, every
render wrote the URL, and in Next.js a URL write restores the router and
discards the navigation in flight. Continue re-renders the page, so its push
to the review never landed. The field now reports only real changes, and the
flow skips writing an unchanged bank amount.

Withdraw -> Crypto on the full method list opened an empty destination form
while saved addresses existed; it now returns to the saved-destinations
screen. QA-30's per-rail address count also made Back from the bank list
leave Withdraw for users with only crypto addresses saved.

Audit follow-ups on the same flows: the minimum converts with the quote rate
(one rate source), the legacy rate hook fails instead of inventing 1:1, a
failed quote refresh keeps the review and any open KYC or terms step mounted
with an inline retry and never confirms the stale quote, mid-typing amounts
are normalized to what the quote API accepts, and the Manteca lock expiry
comes from the remaining time the API measured instead of the phone clock.
@Hugo0
Hugo0 requested a review from jjramirezn as a code owner September 24, 2026 21:39
@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 24, 2026 10:05pm UTC

Request Review

@notion-workspace

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: peanutprotocol/peanut-ui/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 3fa6534b-7558-4c5e-a229-2d8008be3739

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 8802.34 → 8807.32 (+4.98)
Findings: +3 net (+85 new, -82 resolved)

🆕 New findings (85)

  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 226, MI 50.82, SLOC 839
  • critical complexity — src/features/payments/flows/qr-pay/useQrPayFlow.ts — CC 214, MI 52.39, SLOC 872
  • critical complexity — src/features/withdraw/useWithdrawRootFlow.ts — CC 99, MI 50.08, SLOC 370
  • critical complexity — src/features/withdraw/useBridgeOfframpFlow.ts — CC 91, MI 48.56, SLOC 419
  • critical method-complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx:122 — MantecaBankWithdrawFlow CC 58 SLOC 291
  • critical complexity — src/features/withdraw/views/WithdrawMethodView.tsx — CC 58, MI 59.16, SLOC 213
  • high hotspot — src/app/(mobile-ui)/withdraw/manteca/page.tsx — 113 commits, +1410/-859 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 83 commits, +1171/-521 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 81 commits, +687/-882 lines since 6 months ago
  • high complexity — src/features/withdraw/views/WithdrawBankReviewView.tsx — CC 46, MI 56.02, SLOC 84
  • high complexity — src/services/manteca.ts — CC 43, MI 53.26, SLOC 192
  • high method-complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx:472 — CC 42 SLOC 186
  • high method-complexity — src/features/payments/flows/qr-pay/useQrPayFlow.ts:672 — CC 38 SLOC 234
  • high method-complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx:56 — WithdrawCryptoPage CC 37 SLOC 214
  • high method-complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx:542 — CC 36 SLOC 170
  • high method-complexity — src/features/withdraw/views/WithdrawBankReviewView.tsx:58 — CC 34 SLOC 54
  • high complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 31, MI 62.68, SLOC 84
  • medium react-long-component — src/app/(mobile-ui)/withdraw/crypto/page.tsx:56 — WithdrawCryptoPage is 1009 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/manteca/page.tsx:122 — MantecaBankWithdrawFlow: MDD 441.1 (uses across many lines from declarations)
  • medium high-mdd — src/features/payments/flows/qr-pay/useQrPayFlow.ts:104 — useQrPayFlowController: MDD 317.8 (uses across many lines from declarations)

…and 65 more.

✅ Resolved (82)

  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 229, MI 50.7, SLOC 842
  • src/features/payments/flows/qr-pay/useQrPayFlow.ts — CC 217, MI 52.33, SLOC 874
  • src/features/withdraw/useWithdrawRootFlow.ts — CC 105, MI 49.29, SLOC 394
  • src/features/withdraw/useBridgeOfframpFlow.ts — CC 89, MI 48.65, SLOC 418
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx:120 — MantecaBankWithdrawFlow CC 58 SLOC 291
  • src/features/withdraw/views/WithdrawMethodView.tsx — CC 55, MI 59.22, SLOC 202
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — 111 commits, +1394/-842 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — 82 commits, +1169/-520 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 80 commits, +682/-880 lines since 6 months ago
  • src/features/withdraw/views/WithdrawBankReviewView.tsx — CC 43, MI 56.12, SLOC 84
  • src/services/manteca.ts — CC 43, MI 53.27, SLOC 192
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:471 — CC 42 SLOC 186
  • src/features/payments/flows/qr-pay/useQrPayFlow.ts:666 — CC 38 SLOC 233
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:55 — WithdrawCryptoPage CC 37 SLOC 214
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx:540 — CC 36 SLOC 170
  • src/features/withdraw/views/WithdrawBankReviewView.tsx:55 — CC 31 SLOC 54
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:55 — WithdrawCryptoPage is 1009 lines — split it
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx:120 — MantecaBankWithdrawFlow: MDD 440.5 (uses across many lines from declarations)
  • src/features/payments/flows/qr-pay/useQrPayFlow.ts:103 — useQrPayFlowController: MDD 314.5 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:55 — WithdrawCryptoPage: MDD 275.0 (uses across many lines from declarations)

…and 62 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/price-lock.utils.ts 0.0 3.0 +3.0
src/features/withdraw/bank-amount.ts 3.8 5.1 +1.3

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 9919 ran, 0 failed, 0 skipped, 3.6m

📊 Coverage (unit)

metric %
statements 82.1%
branches 71.7%
functions 77.4%
lines 83.4%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

One minor retry-button issue; no blocking defect found.

Findings

  • MINOR · src/features/withdraw/views/WithdrawBankReviewView.tsx:247 · Disable the submit Retry while the quote is unavailable
    If a submit first fails and sets error.showError, then the next 30-second quote refresh fails, the new inline quote Retry appears but the footer's submit Retry remains enabled. Clicking the footer Retry calls onSubmit, which now returns immediately when isQuoteCurrent is false, so the button appears to do nothing. Disable that footer Retry when !isSubmitReady (or route it to quote refetch) while the quote is failed.

  • MAJOR · src/features/withdraw/price-lock.ts:17 · [claude-opus] Manteca lock expiry reads expiresInMs, which the API never sends
    receivePriceLock only sets deadline when the response includes expiresInMs. isPriceLockExpired returns false whenever deadline is undefined. The lockExpiresAt value passed to the Rain-cooldown recovery (two places in withdraw/manteca/page.tsx) is now priceLock.deadline; before this change it was worked out from expiresAt. In peanut-api-ts, POST /manteca/withdraw/init (src/routes/manteca/withdraw.ts:238-247) returns priceLockCode, price, expiresAt, usdAmount, fiatAmount, currency, depositAddress, legalEntity and no expiresInMs. None of the open TASK-23054 PRs in peanut-api-ts (#1702, #1705, #1706) add it. Until the API sends it, two protections that worked before are turned off on every Manteca bank withdrawal. (1) The check before signing never sees an expired quote, although the code comment says an expired quote leads to a re-lock and reconfirm, never a signature and never a submission. The app now signs the transfer and relies on Manteca to reject the expired lock. (2) The recovery step no longer knows when the lock expires, so it can no longer wait out a Rain cooldown inside the lock and re-quotes instead. The author knows the field can be missing ('as it did before TASK-23054'), but it is missing on every response today, not now and then. The other half may be an open peanut-api-ts PR this review cannot see, so this is major rather than blocking. Fix: add expiresInMs to the /withdraw/init response and its response schema in peanut-api-ts before or alongside this change. Or, when expiresInMs is missing, fall back to Date.parse(expiresAt) so this change does not weaken the current behaviour.

Checked clean

  • Exact head, PR metadata, merge base, and changed withdraw paths inspected.
  • Quote amount pattern and response fields checked against the sibling API checkout; they match.
  • AmountInput reporting and URL-change regression tests inspected.
  • Price-lock fallback matches the PR's stated API follow-up: expiresInMs is not yet sent by the sibling API.
  • At review time, typecheck and native-export passed; unit and press-contract checks were still in progress.

Security review: did not run — openrouter-http-402. This review is one reviewer short.

Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: cf3fa10f081b · Context: repo, sibling-api · Took 7m

Comment thread src/features/withdraw/views/WithdrawBankReviewView.tsx
…destinations

api#1707 sends expiresInMs with the withdraw and QR-pay locks. Both flows now
stamp each lock with a device-clock deadline when it arrives (receiveLock),
so a phone clock that runs fast no longer expires a live lock and re-quotes
on every tap. Without the field there is no device-side check. The helper
moved to utils so withdraw and QR pay share one.

Back from the crypto and Manteca pages with no in-app history landed on the
full method list (?showAll=true), which skips the saved addresses; it now
lands on /withdraw, which shows the saved destinations when there are any.

The submit Retry on the bank review now also waits for a current quote: after
a failed submit and a failed quote refresh it looked live and did nothing.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chip review — no blocking findings — this is not an approval

P1 is fixed; the Manteca lock-expiry dependency remains until the API supplies the TTL.

Findings

  • MAJOR · src/utils/price-lock.utils.ts:20 · Deploy the server-measured lock TTL before relying on it
    P2 still applies at this head and now also affects QR Pay. The current API checkout returns expiresAt/expireAt but no expiresInMs from either Manteca init route. For that response, receiveLock sets no deadline, so a lock left open past its expiry is signed and sent instead of being re-quoted before signing; fixed-amount QR warm-up also has no expiry and is skipped. The provider may reject the expired lock, but the intended pre-sign recovery is lost. Ensure the paired API TTL change is deployed before this UI behavior, or retain a safe transition path for old API responses. This is a rollout dependency, not evidence that the paired API work is absent.

Checked clean

  • P1 fixed: submit Retry now shares isSubmitReady with normal submit, with a failed-refresh test.
  • New QR and Manteca lock paths, crypto/Manteca Back fallbacks, and relevant tests inspected at this head.
  • Current sibling API policy checkout returns no expiresInMs on Manteca withdraw or QR init; an open paired API change is not in that checkout.
  • Earlier withdraw amount, quote, and deposit-copy changes remain covered by the prior review; no new finding there.
  • CI checks for this exact head were not yet available in the check-runs response at review time.

Security review: did not run — openrouter-http-402. This review is one reviewer short.

Third opinion: did not run — it reads only the first review of a pull request; the first reviewer checks later rounds. This review is one reviewer short.

Exact head: a51635dfcbdf · Context: repo, sibling-api · Took 6m

Comment thread src/utils/price-lock.utils.ts
@Hugo0

Hugo0 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

@chip defer — re the server-measured lock TTL finding: api#1707 (expiresInMs on both Manteca init routes) merges before this PR. Without it the UI skips the phone-clock check entirely, i.e. the pre-api#1703 behaviour, never worse.

@github-actions

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 5 screens moved

7 of 164 shots changed · 157 identical · baseline 338e436 → head a51635d

worst % screen widths
69.72% early-user 320, 430
52.67% avatar-picker 430
1.50% guest-invite 320, 430
0.52% setup-pending 320
0.23% empty-history 430

job summary · before/after/diff images — artifact

Fixture screenshots, no backend. Advisory — this check never blocks a merge. Posted from the default branch by ds-shots-comment.yml; the report it renders is untrusted data.

@Hugo0
Hugo0 merged commit 908950d into dev Sep 24, 2026
40 of 43 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

English · Español · Español (Argentina) · Português (Brasil)

Open screen library dashboard

After merge: f67137f → 908950d. Capture complete in all locales.

This branch was successfully deployed

1 active deployment
Preview — a51635df Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant