Skip to content

feat(withdraw): tap the balance to withdraw everything - #3005

Merged
innolope-dev merged 24 commits into
devfrom
feat/withdraw-use-full-balance
Sep 6, 2026
Merged

feat(withdraw): tap the balance to withdraw everything#3005
innolope-dev merged 24 commits into
devfrom
feat/withdraw-use-full-balance

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Replaces #2842 (closed unmerged — same branch, full commit history, no rebase). Review history and Chip's rounds live there.

Base is dev: #2902 has merged, so this is no longer stacked. It pairs with peanut-api-ts#1480, which can deploy in either order now that both sides tolerate either counterpart.

Summary

The withdrawal amount screen showed the available balance but made users type it. Withdrawing everything was guesswork.

The balance amount is now a button. One tap fills it, and the amount stays editable afterwards.

Task: TASK-21899 — "Add a Use full balance action to withdrawal amount"

What changed

  • AmountInput takes an optional balanceFillAmount. When set, the amount alone becomes a <button> (44px tap target, underlined, aria-label "Use full balance: $…") — the word "Balance:" stays a plain label, since it was never the action. Without the prop the row renders as before, so every other caller keeps its plain balance row.
  • The withdraw amount screen (/withdraw, the shared USD step for the crypto, bank and Manteca paths) passes maxDecimalAmount, the same number validateAmount and the Continue gate compare against.
  • The field shows cents; the crypto withdrawal moves everything. The balance label already truncates to 2 decimals (formatNumberForDisplay, roundingMode: 'trunc'), so the fill matches the number under the user's thumb and never claims more than the wallet holds. Flooring alone stranded the remainder, though — after a max withdrawal the wallet reads $0.00 while still holding dust the row is too small to offer again. So the intent is carried instead of the extra digits: WithdrawFlowContext gains isMaxWithdrawal, set when the balance tap fills the field and cleared by any keystroke, and the crypto path resolves the amount from the live balance at spend time. Bank and Manteca settle in fiat cents and are untouched.
  • Guard rails on that resolution (resolveWithdrawAmount, unit-tested): the live balance is used only while it still floors to the amount on screen. A deposit landing between the tap and the confirm cannot silently enlarge the withdrawal, and a balance that dropped cannot overdraw — either way it falls back to the number the user saw and agreed to.
  • The value is computed from the number, never parsed back out of the label.
  • Drive-by: the balance row wrote $ 10.12 with a space. A currency symbol sits against the number and an ISO code takes a space (USD 10.12) — the CLDR rule for en-US, which is how the amount itself is formatted. This one lands on every screen with a balance row, not just withdraw.
  • New strings in en, es-419, pt-BR (es-AR inherits es-419).

Screenshots

375x667. Balance is $10.126123, displayed and filled as 10.12.

Balance row, no fill prop With the fill prop After tapping
How every other screen keeps rendering. Only the amount is underlined and tappable. Fills 10.12 — the crypto withdrawal then moves the full 10.126123.

Both columns already show the $10.12 spacing fix, which applies to the row either way.

Captured from the real AmountInput with the props the withdraw amount screen passes, rendered on a scratch /dev route. Re-captured after the merge with dev, which moved this component onto the DS bordered container (#2813 line) — the earlier v3 shots showed the pre-DS render. The full /withdraw screen could not be driven end to end here: the local sandbox API does not boot on this machine for an unrelated reason (@rhino.fi/sdk no longer exports RhinoSdk against the installed 1.12.1). Nothing outside this row changes on that screen.

Design notes / accepted trade-offs

  • Two decimals on screen, exact maximum in the transaction. The task's implementation note asked for the exact unrounded maximum in the field. Split instead, per review: the interface stays at 2 digits (a 6-decimal amount in a big input, and on every screen after Continue, is not something a user reads), while the crypto withdrawal settles the exact balance. Chip asked for the exact maximum and Slava for a 2-digit interface; this satisfies both. Passing the exact number up the flow instead would have surfaced $10.126123 on the bank confirm and success screens, which interpolate amountToWithdraw raw ([country]/bank/page.tsx:457, :571).
  • Exact max only applies to crypto. Bank withdrawals settle in fiat cents and /withdraw/manteca is 2 decimals behind a price lock, so a 6-decimal amount there is meaningless.
  • Opt-in prop, not automatic. AmountInput already receives walletBalance on the send, request, add-money and QR-pay screens. Making every balance row tappable is a product decision beyond this task, so the behavior only turns on where a caller asks for it.
  • The local-currency amount screen at /withdraw/manteca is not covered — it is denominated in ARS/BRL at 2 decimals behind a price lock, so a "full balance" there means converting the USD balance at the sell rate, which can land above the balance. It needs its own design pass. The shared USD step at /withdraw does offer the fill for Manteca users, because there the field and the balance are both USD; the two screens share no amount state (/withdraw/manteca holds its own usdAmount and re-asks).
  • The fill targets the displayed spendable total, which is what the screen's validation and Continue gate compare against (useWallet documents that this total can briefly exceed what is available while collateral settles). Flooring to cents pulls the fill just under that ceiling, but a tap still lands near it far more often than typed entry did. It fails safe, with the settling message.
  • The displayed amount is a snapshot. If the balance changes after a tap, the field keeps the old number (existing isEditingRef behavior for any edited field). Continue re-blocks against the live balance, so nothing overdrafts, and the crypto spend resolves against the live balance under the guard rails above.
  • A balance under a cent leaves the row inert — filling it would only put an unusable amount on screen.

Design system

The branch now sits on dev's DS line (merged in, not rebased — the repo blocks force-push):

  • The balance row rides dev's DS-migrated AmountInput: semantic tokens only (text-foreground-secondary), no legacy palette classes (design.md laws 1–2; the legacyColorClasses ratchet stays at 0).
  • The balance action draws the law-8 keyboard focus ring: 3px action-focus, the default treatment (LinkButton's 2px is the documented sole exception).

Flagged, not changed (design.md law 6): an underlined text-action has no DS precedent — the AmountInput board 17788:19201 has no tappable balance row, and LinkButton is navigation-only, never actions. Logged in mono design/design.md → open conflicts ("amountinput balance action"). @kushagrasarathe rules adopt-or-dismiss, including the figma half ( frame next to the AmountInput board + ChangeLog entry — the dev seat is read-only). Until ruled, the action deliberately carries no hover/pressed state: states belong to a component, and no component covers this yet.

Risk

Frontend only. Two blast radii worth separating:

  • The fill action is behind a prop no existing caller passes, so it appears on the withdraw amount screen and nowhere else.
  • The $10.12 spacing fix is not gated by that prop and changes the balance row on every screen that shows one — send, request, semantic request, contribute-pot, withdraw. Text only, no behavior.

This now touches a money path. On the crypto path the spent amount is resolved at send time rather than read straight from the field, so sendMoney, the route calculation, the request and the charge all use effectiveAmount. Worth reading the diff in withdraw/crypto/page.tsx and resolveWithdrawAmount directly rather than trusting this summary. Bank and Manteca paths are unchanged, and the displayed amount goes through the same validation as a typed one.

QA

  • npm test — 293 suites / 3648 tests green, including 8 resolveWithdrawAmount tests (remainder settled, mid-flow deposit ignored, balance drop never overdraws, sub-cent movement either side, loading, unparseable), 12 AmountInput tests (floor-to-cents, never rounds up, coarse and fine denominations, amount-only tap target, symbol-vs-ISO spacing, zero balance, sub-cent balance, re-fill after an edit, keyboard) and 5 withdraw-page tests (flag set on tap and cleared on edit, full-precision prop handed down while the field shows cents, Continue enabling, below-minimum still blocked, no action while the balance loads).
  • npm run typecheck, pnpm prettier --check ., npm run build — all clean.
  • Re-run after the merge with dev + focus-ring commit: full suite green, typecheck and prettier clean.

Screenshots live on the pr-assets-2842 branch; delete it after merge.

Since the last review (2026-09-01)

Chip's three SDA-fee findings and the kimi-k3 overdraw finding reduce to one root cause, fixed upstream: the "fee" a full-balance withdraw could not afford was a phantom from Rhino's public quote (our account is 1:1 — $415.81 shown vs $0.14 deducted over 60 days; mono ops/rhino-fee-display-fix.md). peanut-api-ts#1480 quotes the authenticated account fee and peanut-ui#2902 shows it verbatim, so payAmount == receiveAmount.

This PR adds two guards on top (c92839b84):

  • A withdraw quotes the SDA in pay mode by the source amount (useCrossChainTransfer, context withdraw): the pay side is the live spend by construction, so a max withdrawal can never quote above the balance, and any fee Rhino ever quotes comes out of the delivery. Pay-request and claim keep receive mode. Under today's config no number changes.
  • The pre-sign gate covers every path: insufficientBalance (was insufficientForFee) blocks Confirm when the kernel spend (payAmount) exceeds the live balance, cross-chain or not — the kimi-k3 "balance dropped after the tap" case now stops at Confirm with an honest message. resolveWithdrawAmount stays as is.

Tests: useCrossChainTransfer.test.ts pins pay mode for withdraw and receive mode for pay-request; the withdraw suites are unchanged and green.


Since the original PR

Landed on this branch after review (the BLOCKING one is a change of mechanism, not a detail):

  • The spend is frozen with the charge. Deriving it live let the balance drift under a prepared charge while both values still floored to the same displayed cents, so the wallet would underpay its own charge — the validator rejects that, and the trusted collateral path books the stale requested amount instead. resolveWithdrawAmount is now called once, at preparation, and everything downstream reads the frozen result.
  • Latest-wins quoting. A max withdrawal re-quotes on every sub-cent balance change, and with no generation guard an older quote finishing last overwrote the newer numbers — including the ones the affordability gate checks.
  • The pre-sign gate compares what each path actually sends. Same-chain that is the frozen amount, not payAmount (the charge's destination amount, usdValue / token.price) — a routine 0.9999 USDC price made the latter exceed the balance and disabled the CTA on a withdrawal that fits.
  • Test coverage for the parts that had none. isAmountWithinBalance is no longer stubbed to always return true, and the confirm-view mock now mirrors the real component (its error-state Retry is genuinely not disabled) rather than hiding the gap. Every added test was verified to fail against the code it guards.

Known open, tracked separately

  • The error-state Retry path still enters the confirm handler without the affordability gate (task).
  • This PR quotes a withdraw in pay mode on the source amount, while peanut-api-ts's server-side minimum guard still derives it in receive mode on the charge amount. Latent while the account config quotes a $0 fee. Fixed in peanut-api-ts#1539 (Update points card and add empty state #1480 had already merged).

abalinda and others added 24 commits August 27, 2026 11:47
Users had to retype their balance to withdraw it all, and the displayed
number is rounded to two decimals, so an exact full-balance withdrawal was
guesswork. The balance row on the amount screen is now a button that fills
the exact spendable amount the same validation gates on.

TASK-21899
…e CTA

The form wrapper focuses the amount field on any click inside it, so the
fill button's click bubbled straight into it.
Tapping the word Balance was never the action, so only the number carries the
underline and the tap target now. The fill is floored to the two decimals the
balance label already truncates to, so the two always agree and neither can
claim more than the wallet holds — sub-cent dust stays behind on purpose.
$10.12, not $ 10.12. A currency symbol sits against the number and an ISO
code takes a space (USD 10.12) — the CLDR rule for en-US, which is how the
amount itself is formatted. Affects every balance row, not just withdraw.
min-h-11 only guaranteed the height. The narrowest fillable balance renders
47.6px wide today, so this changes nothing visually — it just keeps the 44px
target if font metrics ever render it tighter.
The balance tap fills the amount rounded down to cents, and that rounded
number is what the user reads on every screen. Flooring alone stranded the
sub-cent remainder: the wallet then reads $0.00 while holding dust the row is
too small to offer again.

Carry the intent instead of the extra digits. WithdrawFlowContext gains
isMaxWithdrawal, set when the balance tap fills the field and cleared by any
keystroke. The crypto path resolves the amount from the live balance at spend
time, so the wallet reaches a true zero.

Guard rails, in resolveWithdrawAmount: the live balance is only used while it
still floors to the amount on screen, so a deposit landing mid-flow cannot
enlarge the withdrawal and a drop cannot overdraw. Bank and Manteca settle in
fiat cents and are untouched.
The docstring claimed a dropped balance 'must not overdraw', and a test was
named for a guarantee it did not assert. The fallback returns the amount the
user saw, unchanged; the shortfall is caught downstream, as it always has been
for a typed amount. Behaviour unchanged, the claim was wrong.
…tInput

dev's DS pass (ui#2813 line) moved this file to semantic tokens and the
bordered container while this branch was open. The balance row keeps the
branch's behavior on dev's tokens: text-foreground-secondary, never
text-grey-1 (design.md laws 1-2; legacyColorClasses ratchets at 0).

Claude-Session: https://claude.ai/code/session_01LFPygqFLtVPMYDvUNpwbXv
A raw button gets only the browser default outline — no global rule
exists; every DS component carries its own ring. 3px action-focus, the
law-8 default (LinkButton's 2px is the documented sole exception).

Claude-Session: https://claude.ai/code/session_01LFPygqFLtVPMYDvUNpwbXv
…nd on every path

Chip's open findings on this PR all reduce to one thing: a full-balance
withdraw asked Rhino for "deliver X" (receive mode), Rhino answered "pay X
plus fee", and the confirm gate refused. That fee was a phantom — the
public quote's generic schedule; our account is 1:1 (peanut-api-ts#1480,
peanut-ui#2902, which this branch now stacks on). As insurance against a
future config change, a withdraw now quotes the SDA in pay mode by the
source amount, so the pay side can never exceed the balance and any fee
comes out of the delivery. The pre-sign gate no longer cares whether the
route is cross-chain: the kernel spend must fit the live balance on every
path, which also answers the kimi-k3 overdraw thread.

TASK-21899
…' into feat/withdraw-use-full-balance

# Conflicts:
#	src/app/(mobile-ui)/withdraw/crypto/page.tsx
Conflicts, all in code this PR also touches:
- withdraw/page.tsx: dev wrapped AmountInput in FieldColumn; keep the
  wrapper and the balance-fill props.
- en / es-419 / pt-BR `global.amountInput`: dev added switchCurrency
  next to this PR's useFullBalance; keep both.

dev also moved client-side amount validation off the flow-level
setError onto the field's own error, so the sub-minimum full-balance
test asserts the field error instead.
…d, not setError

dev moved client-side amount validation off the flow-level setError onto
the field's own error (FieldColumn). The typed sub-minimum test was
updated with it; this one, added on this branch, still asserted the old
channel and failed on the merge.
…lly spends

Both from the first Chip review this PR has had — as a stacked PR it never got
one until it was retargeted onto dev.

A max withdrawal re-quotes on every sub-cent balance change, so two calculates
are routinely in flight, and useCrossChainTransfer had no generation guard: an
older one finishing last overwrote the newer transactions/payAmount, and the
affordability gate then checked a number the user was not about to send. Each
calculate now takes a generation and writes state only while it is the newest,
the same way the claim flow already does. A superseded quote also no longer
clears the spinner or installs its own error.

The pre-sign gate stopped being cross-chain-only in this branch, which put it
on the highest-volume route — but it compared `payAmount`, and same-chain that
is the CHARGE's destination amount (`usdValue / token.price`), not the spend.
The kernel sends `effectiveAmount` there. A routine USDC price of 0.9999 made
payAmount a few base units more than the balance, so a full-balance withdrawal
that fits would have sat behind a permanently disabled CTA reading "not enough
balance". It now compares the number each path actually sends.

`isAmountWithinBalance` is no longer stubbed in the confirm suite — a money
guard mocked to always return true cannot fail the way the real comparison
does, which is why neither bug had a test. Four added, each verified to fail
without its fix: out-of-order quotes, exact equality, one base unit short, and
cross-chain still gating on the quote pay side.
…t live

The BLOCKING review finding, and it is the mechanism rather than a detail.

A max withdrawal resolves to the live balance, which keeps moving; the charge
records one number and the API validator settles against that number. Deriving
the spend live through the confirm screen let the two drift apart while both
still floored to the same displayed cents — prepare at 10.126123, let the
balance fall to 10.121111, and the wallet would send 10.121111 against a charge
requiring 10.126123. The validator rejects the underpayment; on the trusted
collateral path it completes and books the stale requested amount instead.

`resolveWithdrawAmount` is now called once, inside handleSetupReview, and every
number that preparation derives — the minimum check, the destination token
amount, the charge itself — comes from that single resolution. It is frozen
into `WithdrawFlowContext.preparedAmount` alongside the charge it built, and
from then on the quote, the pre-sign gate and sendMoney all read it. Editing
the amount clears both the charge and the freeze; a preparation that throws
leaves the flow re-armed rather than pinned to an amount that never reached the
backend.

Three tests, each verified to fail against the old live derivation: a balance
rise does not enlarge the send, a drop is refused rather than silently
underpaying, and — the gap the whole feature rested on with no coverage — a max
withdrawal actually hands sendMoney the sub-cent remainder.

Also corrects the confirm-view mock from the previous commit: it disabled the
CTA in every state, including the error-state Retry that the real view renders
with disabled={false}. Mirroring the real component keeps the retry-path gap
visible instead of hiding it behind a friendly stub.
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
peanut-wallet Building Building Preview Sep 6, 2026 4:22pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 6, 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: e21c55f1-d4c2-439c-9a4a-e5183a13a7d6

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 6, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7292.16 → 7294.72 (+2.56)
Findings: +2 net (+50 new, -48 resolved)

🆕 New findings (50)

  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 125, MI 53.32, SLOC 373
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 101, MI 49.58, SLOC 472
  • critical complexity — src/components/Global/AmountInput/index.tsx — CC 82, MI 59.68, SLOC 219
  • critical complexity — src/utils/withdraw.utils.ts — CC 77, MI 53.53, SLOC 268
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 62 commits, +723/-369 lines since 6 months ago
  • high complexity — src/features/payments/shared/hooks/useCrossChainTransfer.ts — CC 46, MI 46.4, SLOC 389
  • medium react-long-component — src/app/(mobile-ui)/withdraw/crypto/page.tsx:49 — WithdrawCryptoPage is 750 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage is 491 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:49 — WithdrawCryptoPage: MDD 157.0 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage: MDD 130.5 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/AmountInput/index.tsx:45 — AmountInput: MDD 75.8 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/crypto/page.tsx:49 — WithdrawCryptoPage: DLT 73 (calls 73 distinct functions — high context load)
  • medium high-mdd — src/context/WithdrawFlowContext.tsx:101 — WithdrawFlowContextProvider: MDD 69.5 (uses across many lines from declarations)
  • medium high-mdd — src/features/payments/shared/hooks/useCrossChainTransfer.ts:178 — useCrossChainTransfer: MDD 56.5 (uses across many lines from declarations)
  • medium high-mdd — src/features/payments/shared/hooks/useCrossChainTransfer.ts:247 — : MDD 53.6 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage: DLT 52 (calls 52 distinct functions — high context load)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:377 — : MDD 40.1 (uses across many lines from declarations)
  • medium high-dlt — src/features/payments/shared/hooks/useCrossChainTransfer.ts:178 — useCrossChainTransfer: DLT 38 (calls 38 distinct functions — high context load)
  • medium high-dlt — src/components/Global/AmountInput/index.tsx:45 — AmountInput: DLT 35 (calls 35 distinct functions — high context load)
  • medium method-complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx:377 — CC 26 SLOC 149

…and 30 more.

✅ Resolved (48)

  • src/app/(mobile-ui)/withdraw/page.tsx — CC 123, MI 53.22, SLOC 362
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 99, MI 49.45, SLOC 458
  • src/components/Global/AmountInput/index.tsx — CC 70, MI 59.96, SLOC 183
  • src/utils/withdraw.utils.ts — CC 70, MI 53.67, SLOC 252
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — 58 commits, +633/-326 lines since 6 months ago
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts — CC 34, MI 45.46, SLOC 340
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:49 — WithdrawCryptoPage is 703 lines — split it
  • src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage is 471 lines — split it
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:49 — WithdrawCryptoPage: MDD 151.6 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage: MDD 125.5 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:49 — WithdrawCryptoPage: DLT 71 (calls 71 distinct functions — high context load)
  • src/components/Global/AmountInput/index.tsx:38 — AmountInput: MDD 63.1 (uses across many lines from declarations)
  • src/context/WithdrawFlowContext.tsx:77 — WithdrawFlowContextProvider: MDD 63.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage: DLT 51 (calls 51 distinct functions — high context load)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:342 — : MDD 40.1 (uses across many lines from declarations)
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts:178 — useCrossChainTransfer: DLT 34 (calls 34 distinct functions — high context load)
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts:178 — useCrossChainTransfer: MDD 31.9 (uses across many lines from declarations)
  • src/components/Global/AmountInput/index.tsx:38 — AmountInput: DLT 30 (calls 30 distinct functions — high context load)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:342 — CC 26 SLOC 149
  • src/app/(mobile-ui)/withdraw/page.tsx:43 — WithdrawPage CC 26 SLOC 125

…and 28 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Global/AmountInput/index.tsx 9.9 11.1 +1.2
src/context/WithdrawFlowContext.tsx 9.0 9.7 +0.7
src/utils/withdraw.utils.ts 8.4 8.9 +0.5

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 6077 ran, 0 failed, 0 skipped, 2.0m

📊 Coverage (unit)

metric %
statements 74.9%
branches 60.5%
functions 69.0%
lines 75.8%
⏱ 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.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 › 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 › 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 › 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 › 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 › 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 › a refused idempotency key tells the user to scan again, not to contact support
3.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
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`.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 7 screens moved

9 of 66 shots changed · 57 identical · baseline eaa3840 → head 24518c7

worst % screen widths
3.19% profile 320
1.65% avatar-picker 430
0.42% request 320, 430
0.07% limits 320, 430
0.03% badges 430
0.03% empty-accounts 430
0.03% send 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.

@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

No new untracked defects found at the supplied head. The full-balance intent is floored for display, frozen with its charge, quoted and spent consistently, and checked against the live balance; the documented Retry affordability exception is already linked to separate follow-up work in the trusted PR description.

Checked clean

  • Verified the detached HEAD, supplied base SHA, merge base, trusted author, base ref, and PR metadata.
  • Reviewed the AmountInput opt-in fill action, cents-flooring, edit retirement, accessibility, translations, and the global currency-spacing change.
  • Traced the exact amount through max-intent resolution, request and charge creation, preparedAmount freezing, confirm rendering, affordability checks, and same-chain and cross-chain execution.
  • Reviewed Rhino withdraw pay-mode versus pay-request receive-mode, quote-generation ordering, SDA and bridge transaction construction, expiry handling, and minimum guards.
  • Cross-checked the sibling API policy-branch contracts and the mono Rhino fee plan for authenticated quote semantics, current zero-fee configuration, server-owned minimum enforcement, and executed-actual accounting.
  • All exact-head CI checks completed successfully, including unit, typecheck, eslint, format, DS checks, native export, analysis, and deploy preview.
  • Focused local Jest execution was unavailable because this detached read-only worktree has no installed Jest binary; the exact-head unit checks passed in CI.
  • The error-state Retry affordability exception remains explicitly documented and linked to separate work in the PR description; no new untracked failure path was found.

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

Third opinion by claude-opus: 0 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: 24518c738144 · Context: repo, product · Took 19m

@innolope-dev
innolope-dev merged commit 0f48920 into dev Sep 6, 2026
42 checks passed
jjramirezn added a commit that referenced this pull request Sep 8, 2026
… flow (PR #3005)

The features/withdraw rebuild kept #3005's charge-pinned broadcast
(setupAmountRef) but dropped the max-withdrawal half: the balance tap
was gone from the amount step, and the crypto path validated and signed
the displayed 2-decimal amount — so 'withdraw everything' stranded the
sub-cent remainder as un-withdrawable dust again (TASK-21899).

Re-ported onto this branch's architecture: isMaxWithdrawal on the flow
context, balanceFillAmount/onBalanceFilled through WithdrawRoot and
WithdrawAmountView (retired on any edit), and resolveWithdrawAmount in
the crypto page (validated, quoted, pinned at charge creation, and used
as every fallback the pinned amount had). Tests: the five balance-fill
cases in withdraw-states and the frozen-spend group (dust remainder,
balance drop, balance rise) in crypto-withdraw-confirm, driven through
the real setup path so the pin is exercised.
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.

2 participants