Skip to content

fix(ui): harden 4 dead-button / silent-no-op handlers (card-setup lockout, KYC region verify, cancel-deposit, ToS confirm)#2229

Open
Hugo0 wants to merge 2 commits into
devfrom
hotfix/dead-button-hardening
Open

fix(ui): harden 4 dead-button / silent-no-op handlers (card-setup lockout, KYC region verify, cancel-deposit, ToS confirm)#2229
Hugo0 wants to merge 2 commits into
devfrom
hotfix/dead-button-hardening

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the withdraw + Send dead-button fixes. The dead-button audit's 4 remaining actionable findings — handlers whose failure path leaves an unresponsive control with no feedback.

# Sev Where Symptom Fix
2 HIGH EnableAutoBalanceBanner non-dismissible 'Finish setting up your card' modal; void grant() discards error → cancelled/failed passkey traps the user surface error + 'Skip for now' escape on failure (grant re-prompts on first card spend) — + tests
5 HIGH UnlockedRegions 'verify now' closes the modal before the await; initiate errors land in an off-screen inline <p> → 'nothing happens' (customer-reported, Federico; blocks EU onboarding) dismissible error modal (Try again + Contact support) on flow.error
3 MED CancelDepositActions cancel failures only console.error'd → user thinks deposit cancelled when it isn't error state + ErrorAlert
1 MED useMultiPhaseKycFlow ToS-confirm await has no try/catch → ~30s frozen 'preparing' modal try/catch → surface recovery state immediately (mirrors BridgeTosStep)

Not fixed: payQR non-MANTECA no-op — currently unreachable (the pay screen only renders for MANTECA); adding handling to an impossible path is the opposite anti-pattern. Documented only.

Risk: low — all changes are additive error-handling on failure paths; happy paths unchanged. Verified: prettier ✓, typecheck ✓, eslint not-worsened (0 new errors), unit tests pass.

Same class as the withdraw Continue throw + Send card: handlers whose failure
path produces no visible change on a control the user can't escape. From the
dead-button audit, the 4 remaining actionable ones:

- HIGH EnableAutoBalanceBanner ('Finish setting up your card'): the modal is
  preventClose + hideModalCloseButton and 'void grant()' discarded the result,
  so a cancelled/failed passkey (common on iOS/1Password) trapped the user with
  a CTA that did nothing. Now surfaces the error and adds a 'Skip for now'
  escape once a grant fails (the grant re-prompts on first card spend anyway).
  + tests.
- HIGH UnlockedRegions ('verify now' → nothing happens; customer-reported by
  Federico): setSelectedRegion(null) closed the modal before the await, then any
  initiate error landed in an off-screen inline <p>. Now a dismissible error
  modal (Try again + Contact support) surfaces flow.error.
- MED CancelDepositActions: cancel failures were only console.error'd, so users
  thought a deposit was cancelled when it wasn't. Added an error state + alert.
- MED useMultiPhaseKycFlow ToS confirm: the await had no try/catch, freezing the
  'preparing' modal ~30s with no feedback. Wrapped it; on failure surface the
  recovery state immediately (mirrors BridgeTosStep).

#5 (payQR non-MANTECA no-op) left as-is: currently unreachable, adding handling
to an impossible path is the opposite anti-pattern. Documented only.
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jun 16, 2026 2:45am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c688e88-99e8-4adb-a149-31735272eb83

📥 Commits

Reviewing files that changed from the base of the PR and between 9819498 and 1821233.

📒 Files selected for processing (1)
  • src/components/Profile/views/UnlockedRegions.view.tsx

Walkthrough

Four components receive error-handling improvements: useMultiPhaseKycFlow adds a try/catch around ToS confirmation to set preparingTimedOut on failure; EnableAutoBalanceBanner tracks grant errors with a dismissed escape hatch and conditional CTA labels; UnlockedRegions.view replaces inline error text with a dismissible ActionModal; and CancelDepositActions surfaces cancellation failures via an ErrorAlert.

Changes

Error Handling UX Improvements

Layer / File(s) Summary
KYC flow ToS acceptance error guard
src/hooks/useMultiPhaseKycFlow.ts
tos_accepted branch now wraps confirmBridgeTosAndAwaitRails + completeFlow in a try/catch; failures transition to preparingTimedOut instead of leaving the flow stuck in preparing.
EnableAutoBalanceBanner grant error state and dismiss UX
src/components/Home/EnableAutoBalanceBanner.tsx, src/components/Home/__tests__/EnableAutoBalanceBanner.test.tsx
Introduces dismissed state, derives hardError from lastError.kind, conditionally labels CTAs ("Try again"/"Continue"/"Working…"), appends a "Skip for now" escape-hatch CTA on any error, and gates modal visibility on dismissed. Three behavioral tests cover initial state, user-cancelled escape hatch, and hard-error messaging.
UnlockedRegions KYC initiation error modal
src/components/Profile/views/UnlockedRegions.view.tsx
Adds errorAcknowledged state reset on each KYC start; derives failedRegionRetriable from the region intent provider lookup; replaces inline red-text error with an ActionModal offering "Try again" and "Contact support" CTAs for retriable regions or a single "Got it" for unavailable regions, both suppressing re-display by setting errorAcknowledged.
CancelDepositActions cancellation error display
src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
Adds local error state; reworks wrapAction to clear and capture errors on failure; introduces a withError helper that renders ErrorAlert alongside cancel buttons; all three cancellation render branches now use withError.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main objective: hardening four dead-button handlers across four distinct components (card-setup, KYC region verify, cancel-deposit, ToS confirm) by adding error feedback and recovery paths.
Description check ✅ Passed The description provides comprehensive context with a detailed table mapping each fix to its severity, location, symptom, and solution, directly aligned with the file changes and pr_objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5797.23 → 5798.91 (+1.68)
Findings: 0 net (+16 new, -16 resolved)

🆕 New findings (16)

  • critical complexity — src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.79, SLOC 322
  • critical complexity — src/components/Profile/views/UnlockedRegions.view.tsx — CC 62, MI 62.4, SLOC 179
  • medium high-mdd — src/hooks/useMultiPhaseKycFlow.ts:90 — useMultiPhaseKycFlow: MDD 112.5 (uses across many lines from declarations)
  • medium high-mdd — src/components/Profile/views/UnlockedRegions.view.tsx:60 — UnlockedRegions: MDD 82.0 (uses across many lines from declarations)
  • medium complexity — src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx — CC 28, MI 66.56, SLOC 79
  • medium method-complexity — src/components/Profile/views/UnlockedRegions.view.tsx:60 — CC 26 SLOC 112
  • medium high-mdd — src/components/Home/EnableAutoBalanceBanner.tsx:26 — EnableAutoBalanceBanner: MDD 20.5 (uses across many lines from declarations)
  • medium hotspot — src/hooks/useMultiPhaseKycFlow.ts — 21 commits, +513/-107 lines since 6 months ago
  • low high-dlt — src/components/Profile/views/UnlockedRegions.view.tsx:60 — UnlockedRegions: DLT 27 (calls 27 distinct functions — high context load)
  • low high-mdd — src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx:28 — CancelDepositActions: MDD 17.1 (uses across many lines from declarations)
  • low high-dlt — src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx:28 — CancelDepositActions: DLT 17 (calls 17 distinct functions — high context load)
  • low structural-dup — src/components/Profile/views/UnlockedRegions.view.tsx:347 — 16 duplicate lines / 83 tokens with src/features/limits/views/LimitsPageView.tsx:146
  • low structural-dup — src/components/Profile/views/UnlockedRegions.view.tsx:348 — 16 duplicate lines / 73 tokens with src/features/limits/views/LimitsPageView.tsx:189
  • low high-mdd — src/components/Profile/views/UnlockedRegions.view.tsx:348 — : MDD 10.6 (uses across many lines from declarations)
  • low missing-return-type — src/components/Home/EnableAutoBalanceBanner.tsx:26 — EnableAutoBalanceBanner: exported fn missing return type annotation
  • low missing-return-type — src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx:28 — CancelDepositActions: exported fn missing return type annotation

✅ Resolved (16)

  • src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.89, SLOC 319
  • src/components/Profile/views/UnlockedRegions.view.tsx — CC 55, MI 62.46, SLOC 150
  • src/hooks/useMultiPhaseKycFlow.ts:90 — useMultiPhaseKycFlow: MDD 110.6 (uses across many lines from declarations)
  • src/components/Profile/views/UnlockedRegions.view.tsx:60 — UnlockedRegions: MDD 72.8 (uses across many lines from declarations)
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx — CC 26, MI 67.15, SLOC 70
  • src/components/Profile/views/UnlockedRegions.view.tsx:60 — CC 23 SLOC 92
  • src/interfaces/interfaces.ts — 21 commits, +124/-122 lines since 6 months ago
  • src/components/Profile/views/UnlockedRegions.view.tsx:60 — UnlockedRegions: DLT 26 (calls 26 distinct functions — high context load)
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx:26 — CancelDepositActions: MDD 18.6 (uses across many lines from declarations)
  • src/components/Profile/views/UnlockedRegions.view.tsx:300 — 16 duplicate lines / 83 tokens with src/features/limits/views/LimitsPageView.tsx:146
  • src/components/Profile/views/UnlockedRegions.view.tsx:301 — 16 duplicate lines / 73 tokens with src/features/limits/views/LimitsPageView.tsx:189
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx:26 — CancelDepositActions: DLT 15 (calls 15 distinct functions — high context load)
  • src/components/Profile/views/UnlockedRegions.view.tsx:301 — : MDD 10.6 (uses across many lines from declarations)
  • src/components/Home/EnableAutoBalanceBanner.tsx:21 — EnableAutoBalanceBanner: exported fn missing return type annotation
  • src/components/Home/EnableAutoBalanceBanner.tsx:1 — 'use client' but no hooks/handlers/browser API
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx:26 — CancelDepositActions: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Home/EnableAutoBalanceBanner.tsx 4.8 5.7 +0.8

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1450 ran, 0 failed, 0 skipped, 20.1s

📊 Coverage (unit)

metric %
statements 52.3%
branches 34.7%
functions 39.2%
lines 52.2%
⏱ 10 slowest test cases
time test
0.5s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid UK IBAN with spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid German IBAN
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address in lowercase
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (too short)
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx (1)

126-137: ⚠️ Potential issue | 🟠 Major

Avoid redundant bridge cancel calls on retry

The REQUEST flow (lines 126–136) executes cancelOnramp(bridgeTransferId) then chargesApi.cancel(transaction.id). If the charge cancel fails, wrapAction catches the error and shows the user "Please try again." On retry, the entire sequence runs again, re-executing cancelOnramp even if it already succeeded on the first attempt.

While REST DELETE endpoints are conventionally idempotent, this creates unnecessary API overhead and obscures which operation failed on retry. Consider tracking or checkpointing that the bridge cancellation succeeded before attempting the charge cancellation, so retries skip the already-completed bridge step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx`
around lines 126 - 137, The wrapAction callback in the REQUEST flow is executing
cancelOnramp(bridgeTransferId) unconditionally every time the action runs,
including on retries. If chargesApi.cancel fails and the user retries,
cancelOnramp will be called again even though it likely already succeeded on the
first attempt. Refactor the code to track or checkpoint that the bridge
cancellation has already completed successfully, then conditionally skip the
cancelOnramp call on subsequent retries. This avoids redundant API calls and
makes it clearer which operation actually failed during the retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx`:
- Around line 126-137: The wrapAction callback in the REQUEST flow is executing
cancelOnramp(bridgeTransferId) unconditionally every time the action runs,
including on retries. If chargesApi.cancel fails and the user retries,
cancelOnramp will be called again even though it likely already succeeded on the
first attempt. Refactor the code to track or checkpoint that the bridge
cancellation has already completed successfully, then conditionally skip the
cancelOnramp call on subsequent retries. This avoids redundant API calls and
makes it clearer which operation actually failed during the retry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e7494110-3955-4ad8-a270-bc123165c388

📥 Commits

Reviewing files that changed from the base of the PR and between c5a5b9d and 9819498.

📒 Files selected for processing (5)
  • src/components/Home/EnableAutoBalanceBanner.tsx
  • src/components/Home/__tests__/EnableAutoBalanceBanner.test.tsx
  • src/components/Profile/views/UnlockedRegions.view.tsx
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
  • src/hooks/useMultiPhaseKycFlow.ts

Code-review catch: a ROW (rest-of-world) region has no provider/rail, so an
initiate returns a terminal 'not available in your region yet'. The new error
modal was showing 'Verification couldn't start' + 'Try again', which re-issues
the identical ROW request and loops. Gate retriability on the region's provider:
terminal ROW → neutral title + 'Got it'; transient → 'Try again' + support.
@Hugo0 Hugo0 marked this pull request as ready for review June 16, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant