Skip to content

fix: EEA-uplift deposit/withdraw errors + Bridge re-verification pending modal#2345

Open
kushagrasarathe wants to merge 8 commits into
mainfrom
hotfix/eea-uplift-reverify-and-errors
Open

fix: EEA-uplift deposit/withdraw errors + Bridge re-verification pending modal#2345
kushagrasarathe wants to merge 8 commits into
mainfrom
hotfix/eea-uplift-reverify-and-errors

Conversation

@kushagrasarathe

Copy link
Copy Markdown
Contributor

Targets main (hotfix — the affected users are live). Bundles three related pieces for the EEA-uplift Bridge flow; supersedes #2329 and #2332.

1. Surface deposit errors instead of failing silently (was #2329)

EEA users whose Bridge KYC was under review created deposits that failed with zero feedback. Fixed:

  • useCreateOnramp now parses the backend error body — but only for 4xx (deliberate, user-safe messages); 5xx bodies are suppressed to a generic string so internal detail can't leak.
  • the page catch shows the caught message directly (the hook's error state was read stale). Hook is now throw-only.

2. Bridge re-verification pending modal (new — the core ask)

When a user completes an EEA uplift and then tries to on/offramp while Bridge is still re-reviewing, they hit the waiting-on-provider gate. Before: deposit showed a bare inline line, withdraw did nothing (dead button). Now:

  • a dedicated KycReverificationPendingModal ("We're reviewing your details…") on both deposit + withdraw.
  • opening it re-arms the existing submission-window poller (markSubmitted), so the 4s user refetch pulls Bridge's live status and the modal auto-dismisses the moment the gate clears. No new BE endpoint — the capability pipeline already mirrors Bridge status.
  • also closes the withdraw-side silent no-op.

3. EEA-uplift funnel fires on modal-open, all remediation (was #2332)

eea_uplift_started fired only on the advisory CTA — missing abandoners and the entire urgent post-cliff (blocking) cohort. Now fires at modal-open across both remediation paths + both channels, carrying source/urgent/effective_date to split urgent vs upcoming in PostHog. Detection: reason.code eea_uplift* (blocking) + a requirement-key set (advisory), derived from prod data.

Risks

  • FE-only, no BE/contract change. User-visible: the two error/wait surfaces above; deposit backend 4xx strings now shown (already user-appropriate on this route).
  • Pending modal live-refresh rides the existing 30s submission window per open (ponytail ceiling: re-arm loop if longer waits need continuous polling). Documented in code.
  • Analytics started counts rise (now include abandoners) — intended; funnel dedups by person.

QA

  • npm test 110 suites / 1693 green · typecheck · build · prettier. New tests: useCreateOnramp (incl. 5xx leak-guard), eea-uplift.utils, rewritten useEeaUpliftFunnel (dedup/latch), and the deposit waiting-on-provider→modal assertion.

EEA-uplift users whose docs are under Bridge review were retrying deposits
with zero feedback: the onramp hook threw a hardcoded message without
reading the backend's { error } body, the page's catch read the hook's
error state before it flushed (always null on first attempt), and the
waiting-on-provider gate dead-ended the Continue button with no message.

Parse the error body, show the caught message directly, and tell
under-review users their verification is being processed.
Addresses code-review findings on the silent-failure fix:
- only surface the backend body for 4xx (client errors); 5xx bodies carry
  raw internal messages the global handler doesn't sanitize, so those fall
  back to the generic string (no internal-detail leak).
- make useCreateOnramp throw-only: its React error state had no reader after
  the page stopped destructuring it, and was the same stale-closure trap the
  page fix works around. The thrown Error carries the reason.
- reuse gate.userMessage for the waiting-on-provider copy instead of
  hardcoding, matching every other gate consumer.
- extract GENERIC_ONRAMP_ERROR so the fallback copy lives in one place.
- keep the distinct 'Unknown error' analytics label for non-Error throws.
…diation

The eea_uplift_started event fired only on the advisory modal's 'Complete
now' CTA, so it missed (a) users who open the uplift modal and abandon, and
(b) the entire urgent post-cliff cohort, whose remediation is now blocking
(fixable-rejection → KYC modal) rather than a future-dated advisory.

Fire at modal-open instead, across both remediation paths and both channels
(deposit + withdraw):
- blocking/urgent: gate.reason.code === 'eea_uplift*' (BE sets it to the
  questionnaire cluster).
- advisory/upcoming: requirementKey in the eea_uplift set (sof_individual_
  primary_purpose, has_foreign_tax_registration, place_of_birth_missing,
  nationalities) — derived from prod remediation data.

Event now carries source + urgent + effective_date so urgent vs upcoming
(dob-type) split in PostHog. PoA / gov-id-expired co-occur on the cliff but
are excluded (separate remediation) to avoid over-firing on generic doc
rejections for non-EEA users.
Addresses code-review findings on the eea_uplift funnel:
- phantom completions: the blocking path arms the started latch, but the
  InitiateKycModal onClose/onContactSupport did not reset it (only the
  advisory/Sumsub path did). A later unrelated KYC approval would fire a
  false eea_uplift_completed. Reset the latch on modal dismiss.
- re-fire: trackStarted is now idempotent per armed attempt, so repeat
  Continue clicks in one attempt don't inflate the started count.
- DRY + convention: collapse the four duplicated trigger-construction sites
  into upliftTriggerFrom{Gate,Advisory} helpers and move UpliftStartTrigger
  out of the hook into eea-uplift.utils (per the no-types-in-hooks rule).
When a user completes an EEA uplift and then tries to on/offramp via Bridge
while Bridge is still re-reviewing, the deposit page showed only an inline
message and the withdraw page silently did nothing. Show a dedicated
pending/in-progress modal instead, driven by the waiting-on-provider gate.

To reflect proper live status from Bridge, re-arm the existing submission-
window poller (markSubmitted) when the modal opens — the 4s user refetch
picks up Bridge's latest verification status and the modal auto-dismisses
the moment the gate leaves waiting-on-provider. No new backend endpoint: the
capability pipeline already mirrors Bridge status.

Also closes the withdraw-side silent no-op noted in the deposit fix.
@vercel

vercel Bot commented Jul 3, 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 Jul 3, 2026 7:06am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 3, 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: 7284c4f7-fcd2-49ee-9686-552dfdb96222

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb67af and ae974e1.

📒 Files selected for processing (2)
  • src/hooks/useWaitingOnProviderModal.ts
  • src/utils/eea-uplift.utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/hooks/useWaitingOnProviderModal.ts
  • src/utils/eea-uplift.utils.ts

Walkthrough

Adds waiting-on-provider modal handling for add-money and withdraw bank flows, centralizes EEA uplift trigger derivation and tracking, and changes onramp creation to throw user-facing errors instead of exposing hook error state.

Changes

Bank flow gating, uplift tracking, and onramp errors

Layer / File(s) Summary
EEA uplift trigger utilities
src/utils/eea-uplift.utils.ts, src/utils/eea-uplift.utils.test.ts
Defines the uplift trigger type, whitelist, and helper functions for deriving blocking and advisory triggers, with coverage for both derivation paths.
Uplift funnel tracking
src/hooks/useEeaUpliftFunnel.ts, src/hooks/useEeaUpliftFunnel.test.ts
Updates the uplift funnel hook to consume the unified trigger model, add idempotent start tracking, and emit enriched PostHog payloads, with tests updated for the new trigger semantics.
Onramp error handling
src/hooks/useCreateOnramp.ts, src/hooks/__tests__/useCreateOnramp.test.ts
Changes useCreateOnramp to throw errors with a generic fallback for server failures, removes the exposed error state, and adds tests for success and failure cases.
Waiting-on-provider modal hook
src/hooks/useWaitingOnProviderModal.ts, src/hooks/__tests__/useWaitingOnProviderModal.test.ts
Adds the hook that keeps the waiting-on-provider modal open, re-arms submission polling on an interval, and auto-closes when the gate resolves, with tests for open, close, message, and polling behavior.
Pending re-verification modal
src/components/Kyc/KycReverificationPendingModal.tsx
Adds the KycReverificationPendingModal component with ActionModal wiring, fallback copy, and a home navigation CTA.
Add-money and withdraw bank pages
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Updates the bank pages to open the pending modal for waiting-on-provider, derive uplift starts from gate and advisory states, reset the uplift funnel on KYC modal dismissal and support contact, and surface create-onramp failures from thrown errors; the add-money tests add the waiting-on-provider case and updated error expectations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: jjramirezn, Hugo0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main hotfix: deposit/withdraw EEA uplift errors and the new Bridge re-verification pending modal.
Description check ✅ Passed The description matches the changeset and accurately describes the deposit error handling, pending modal flow, and EEA uplift analytics updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5872.71 → 5890.67 (+17.96)
Findings: +5 net (+36 new, -31 resolved)

🆕 New findings (36)

  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 101, MI 54.47, SLOC 352
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 96, MI 57.55, SLOC 347
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 56 commits, +621/-485 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 49 commits, +518/-233 lines since 6 months ago
  • medium react-long-component — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage is 548 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:52 — OnrampBankPage is 482 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage: MDD 161.9 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:52 — OnrampBankPage: MDD 146.3 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage: DLT 80 (calls 80 distinct functions — high context load)
  • medium high-dlt — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:52 — OnrampBankPage: DLT 69 (calls 69 distinct functions — high context load)
  • medium method-complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:52 — OnrampBankPage CC 29 SLOC 164
  • medium method-complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage CC 28 SLOC 131
  • medium structural-dup — app/(mobile-ui)/add-money/[country]/bank/page.tsx:262 — 24 duplicate lines / 71 tokens with app/(mobile-ui)/withdraw/[country]/bank/page.tsx:220
  • medium high-mdd — src/components/Kyc/KycReverificationPendingModal.tsx:20 — KycReverificationPendingModal: MDD 20.0 (uses across many lines from declarations)
  • medium method-complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:219 — CC 19 SLOC 110
  • medium complexity — src/hooks/useCreateOnramp.ts — CC 11, MI 57.84, SLOC 49
  • medium react-effect-derives-state — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:160 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:214 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:358 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:137 — small useEffect that only sets state from deps

…and 16 more.

✅ Resolved (31)

  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 99, MI 54.88, SLOC 340
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 94, MI 57.82, SLOC 339
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 49 commits, +526/-423 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 45 commits, +451/-202 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage is 515 lines — split it
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage is 452 lines — split it
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage: MDD 149.6 (uses across many lines from declarations)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage: MDD 139.3 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage: DLT 75 (calls 75 distinct functions — high context load)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage: DLT 65 (calls 65 distinct functions — high context load)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:49 — OnrampBankPage CC 29 SLOC 165
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:60 — WithdrawBankPage CC 28 SLOC 129
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:212 — CC 18 SLOC 103
  • src/hooks/useCreateOnramp.ts — CC 6, MI 58.39, SLOC 51
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:154 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:208 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:337 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:130 — small useEffect that only sets state from deps
  • app/(mobile-ui)/add-money/[country]/bank/page.tsx:133 — 18 duplicate lines / 74 tokens with app/(mobile-ui)/withdraw/[country]/bank/page.tsx:109
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:212 — proceedWithOfframp: DLT 18 (calls 18 distinct functions — high context load)

…and 11 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useWaitingOnProviderModal.ts 0.0 5.4 +5.4
src/components/Kyc/KycReverificationPendingModal.tsx 0.0 5.0 +5.0
src/utils/eea-uplift.utils.ts 0.0 4.7 +4.7
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx 19.3 20.4 +1.2
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx 18.6 19.4 +0.9
src/hooks/useCreateOnramp.ts 8.3 8.9 +0.7

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1701 ran, 0 failed, 0 skipped, 26.8s

📊 Coverage (unit)

metric %
statements 55.3%
branches 38.4%
functions 43.3%
lines 55.2%
⏱ 10 slowest test cases
time test
3.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.2s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.2s 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/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Manteca PIX form ready shows merchant card + amount input + pay button
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claimed shows shake class + go home button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
📍 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/utils/eea-uplift.utils.ts (1)

48-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer discriminant narrowing over unsafe cast.

(gate as { reason?: { code?: string } }) bypasses GateState's discriminated union entirely. If the real reason shape changes or moves to a differently-named field, this compiles silently and returns wrong results at runtime instead of failing at compile time.

♻️ Suggested narrowing via the `kind` discriminant
 export function upliftTriggerFromGate(gate: GateState): UpliftStartTrigger | null {
-    const code = (gate as { reason?: { code?: string } }).reason?.code
-    if (!code?.startsWith('eea_uplift')) return null
-    return { requirementKey: code, source: 'blocking' }
+    if (gate.kind !== 'fixable-rejection') return null
+    const code = gate.reason?.code
+    if (!code?.startsWith('eea_uplift')) return null
+    return { requirementKey: code, source: 'blocking' }
 }

This assumes reason only exists on the fixable-rejection variant of GateState (consistent with the tests at lines 28-31 and the doc comment). Please confirm against the actual GateState definition in capability-gate.ts.

🤖 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/utils/eea-uplift.utils.ts` around lines 48 - 52, Replace the unsafe cast
in upliftTriggerFromGate with proper discriminant narrowing on GateState, using
the kind field to access the fixable-rejection variant before reading
reason.code. Keep the existing return shape, but remove the `(gate as { reason?:
{ code?: string } })` assertion so the compiler enforces the real GateState
union. Please verify the exact variant shape against GateState in
capability-gate.ts and only read reason from the matching branch.
src/hooks/useCreateOnramp.ts (1)

26-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Loosen Promise<any> return type.

Flagged by ESLint (@typescript-eslint/no-explicit-any). The success path already returns a well-shaped payload (transferId, depositInstructions, …) documented in IOnrampData in OnrampFlowContext.tsx. Typing createOnramp as Promise<IOnrampData> (or a dedicated response type) would give callers compile-time safety instead of any.

♻️ Proposed fix
+import { type IOnrampData } from '`@/context/OnrampFlowContext`'
+
 export interface UseCreateOnrampReturn {
-    createOnramp: (params: CreateOnrampParams) => Promise<any>
+    createOnramp: (params: CreateOnrampParams) => Promise<IOnrampData>
     isLoading: boolean
 }
🤖 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/hooks/useCreateOnramp.ts` around lines 26 - 29, The
`UseCreateOnrampReturn.createOnramp` signature is too loose because it returns
`Promise<any>`, which triggers the ESLint `no-explicit-any` rule and hides the
actual response shape. Update the return type in `UseCreateOnrampReturn` to use
the existing `IOnrampData` shape from `OnrampFlowContext` (or a dedicated
response type if needed), and make sure the `useCreateOnramp` implementation and
its callers are aligned with that typed payload so the success path is strongly
typed.

Source: Linters/SAST tools

🤖 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.

Inline comments:
In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 74-75: Remove the unused isRiskAccepted state from the bank
add-money page, since it is never read or passed to OnrampConfirmationModal and
is causing the lint/CI failure. Update the related handlers in the same
component, including handleWarningConfirm and handleWarningCancel, to drop the
setIsRiskAccepted(false) calls unless you wire the state into actual UI logic
such as the Continue button disabled state or the modal props.

In `@src/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx:
- Line 131: Reset the pending modal latch when the provider gate stops being
waiting-on-provider so it cannot reopen from stale state. Update the
`showPendingModal` handling in `BankWithdrawPage` (the component that owns the
`useState` and the `gate.kind` auto-hide logic) to clear
`setShowPendingModal(false)` when the gate transitions away from
`waiting-on-provider`, not only when `onClose` runs. Keep the existing auto-hide
behavior, but ensure the state is explicitly reset as part of the gate-change
effect/branch that watches `gate.kind`.

---

Nitpick comments:
In `@src/hooks/useCreateOnramp.ts`:
- Around line 26-29: The `UseCreateOnrampReturn.createOnramp` signature is too
loose because it returns `Promise<any>`, which triggers the ESLint
`no-explicit-any` rule and hides the actual response shape. Update the return
type in `UseCreateOnrampReturn` to use the existing `IOnrampData` shape from
`OnrampFlowContext` (or a dedicated response type if needed), and make sure the
`useCreateOnramp` implementation and its callers are aligned with that typed
payload so the success path is strongly typed.

In `@src/utils/eea-uplift.utils.ts`:
- Around line 48-52: Replace the unsafe cast in upliftTriggerFromGate with
proper discriminant narrowing on GateState, using the kind field to access the
fixable-rejection variant before reading reason.code. Keep the existing return
shape, but remove the `(gate as { reason?: { code?: string } })` assertion so
the compiler enforces the real GateState union. Please verify the exact variant
shape against GateState in capability-gate.ts and only read reason from the
matching branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1335424a-437e-468f-8b49-70f1f82d91b8

📥 Commits

Reviewing files that changed from the base of the PR and between adb82bb and 94d684a.

📒 Files selected for processing (10)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/components/Kyc/KycReverificationPendingModal.tsx
  • src/hooks/__tests__/useCreateOnramp.test.ts
  • src/hooks/useCreateOnramp.ts
  • src/hooks/useEeaUpliftFunnel.test.ts
  • src/hooks/useEeaUpliftFunnel.ts
  • src/utils/eea-uplift.utils.test.ts
  • src/utils/eea-uplift.utils.ts

Comment thread src/app/(mobile-ui)/add-money/[country]/bank/page.tsx Outdated
Comment thread src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx Outdated
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Addresses code-review on the reverification pending modal:
- FEATURE-BREAKING: a single 30s submission window lapsed mid-review
  (waiting-on-provider rails are requires-info, not pending, so the poller
  self-terminated) — the modal never auto-dismissed even after Bridge
  approved. Extract useWaitingOnProviderModal, which re-arms the window on a
  20s interval while open so the ~4s user poll runs for the whole wait.
- stale-flag reopen: drop the request flag when the gate resolves, so a
  transient re-flip to waiting-on-provider can't reopen the modal on its own.
- surface gate.userMessage (bridge-specific review copy) in the modal instead
  of always the generic text.
- DRY: the shared hook collapses the duplicated deposit/withdraw wiring.
- drop the redundant 'clock' as IconName cast.
Write-only (declared + set, never read) — the cherry-picked handlers
re-added the setters onto state main had already reduced to a bare
declaration. No behavior change.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kushagrasarathe kushagrasarathe marked this pull request as ready for review July 3, 2026 06:39
@kushagrasarathe kushagrasarathe requested a review from Hugo0 July 3, 2026 06:39
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 3, 2026
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr view 2345 --repo peanutprotocol/peanut-ui --json number,title,baseRefName,headRefName,commits -q '.'
echo "---DIFF FILES---"
gh pr diff 2345 --repo peanutprotocol/peanut-ui > /tmp/pr.diff
wc -l /tmp/pr.diff
echo "---useCreateOnramp---"
sed -n '1,200p' /tmp/pr.diff | grep -n "diff --git"

Length of output: 8316


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat /tmp/pr.diff | grep -n "^diff --git"

Length of output: 1488


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,182p' /tmp/pr.diff
echo "=====WITHDRAW====="
sed -n '289,405p' /tmp/pr.diff
echo "=====MODAL====="
sed -n '406,461p' /tmp/pr.diff
echo "=====useWaitingOnProviderModal====="
sed -n '938,993p' /tmp/pr.diff
echo "=====useWaitingOnProviderModal TEST====="
sed -n '557,641p' /tmp/pr.diff
echo "=====useCreateOnramp====="
sed -n '642,715p' /tmp/pr.diff
echo "=====useEeaUpliftFunnel====="
sed -n '838,937p' /tmp/pr.diff
echo "=====eea-uplift.utils====="
sed -n '1058,1129p' /tmp/pr.diff

Length of output: 36493


🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -uu "capability-gate" -e ts src
echo "---"
sed -n '1,150p' src/utils/capability-gate.ts 2>/dev/null | head -200

Length of output: 7565


🏁 Script executed:

#!/bin/bash
set -euo pipefail
grep -n "CapabilityReason" -r src/types/capabilities.ts | head -20
echo "---"
sed -n '1,60p' src/types/capabilities.ts | grep -n "interface CapabilityReason" -A 10

Length of output: 902


🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd "useSubmissionWindow" src
echo "---"
cat src/hooks/useSubmissionWindow.ts 2>/dev/null
echo "---UserAutoRefresh---"
fd "useUserAutoRefresh" src
cat src/hooks/useUserAutoRefresh.ts 2>/dev/null | head -80

Length of output: 7052


🏁 Script executed:

#!/bin/bash
set -euo pipefail
grep -n "useEeaUpliftFunnel\|trackUpliftStarted\|resetUpliftFunnel\|trackUpliftCompleted" "src/app/(mobile-ui)/add-money/[country]/bank/page.tsx" "src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx"

Length of output: 2702


🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd "useAdvisoryPreempt" src
cat src/hooks/useAdvisoryPreempt.ts 2>/dev/null

Length of output: 3335


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '90,100p' "src/app/(mobile-ui)/add-money/[country]/bank/page.tsx"
echo "---"
grep -n "onKycApproved\|handleSelfHealResubmit\|useMultiPhaseKycFlow" -r src/hooks/useMultiPhaseKycFlow.ts | head -30

Length of output: 1168


Good — confirmed the flow is shared and consistent. I now have enough to write the review.

@kushagrasarathe Reviewed in depth across all 12 changed files (including the two files not reflected in the AI summary — useWaitingOnProviderModal.ts and its test — which supersede the earlier inline markSubmitted wiring). Here's the breakdown:

Risk of new bugs / breaking existing functionality — Low

  • The commit history shows this PR already went through several self-review cycles (stale-flag reopen fix, poller-lapse fix, dead-state removal), so the obvious failure modes were caught before this review.
  • useCreateOnramp's throw-only refactor correctly removes the stale-closure trap (reading onrampError from hook state in a catch block that runs before the state flush). The 4xx/5xx split is safe: 5xx bodies are never parsed (verified in useCreateOnramp.test.tsjson() isn't even called), so there's no risk of leaking internal error strings.
  • useWaitingOnProviderModal: the REARM_INTERVAL_MS (20s) vs SUBMISSION_WINDOW_MS (30s) margin is safe, the stale-flag drop (requested && !isWaiting → false) correctly prevents a transient re-flip from reopening the modal, and cleanup (clearInterval) is correct on unmount/close.
  • trackUpliftCompleted is fired from the shared onKycApproved callback in useMultiPhaseKycFlow, so both remediation paths (self-heal-resubmit for advisory, normal KYC for blocking) correctly close the funnel loop — verified this isn't duplicated or dropped for either path.
  • One low-severity nit: useWaitingOnProviderModal.ts and eea-uplift.utils.ts both use ad-hoc type assertions (gate as { userMessage?: ...}, gate as { reason?: {...} }) instead of narrowing via the already-well-typed GateState discriminated union (waiting-on-provider already declares userMessage/reason natively in capability-gate.ts). Functionally correct today, but it bypasses the type safety the union was designed to provide and would not catch a future rename of reason.code or userMessage. Consider:
const message = gate.kind === 'waiting-on-provider' ? (gate.userMessage ?? undefined) : undefined

instead of the cast. Same idea applies to upliftTriggerFromGate.

Performance impact / latency — Negligible

  • The pending-modal poll only runs while the modal is open (bounded, cleans up on close/unmount), reuses the existing singleton useUserAutoRefresh poller (no new interval infrastructure, no N-instance fan-out), and the 4s poll cadence + in-flight guard are unchanged from the existing submission-window mechanism.
  • No new network endpoints, no additional renders introduced beyond the new showPendingModal/pendingModal state, which is scoped locally per page.

Code quality & DRY — Good, above bar for a hotfix

  • The final commit (904efd1) explicitly collapsed four duplicated trigger-construction call sites into upliftTriggerFromGate/upliftTriggerFromAdvisory, and the deposit/withdraw pending-modal wiring was extracted into the shared useWaitingOnProviderModal hook — good instinct to DRY up what would otherwise be four near-identical blocks across two pages.
  • GENERIC_ONRAMP_ERROR and upliftEventProps/sameTrigger helpers are sensible centralization points.
  • Minor: the type-assertion nit above is the only real "should be cleaner" item left.

Concise QA testing guide

1. Deposit — 4xx error surfacing

  • Trigger a deposit that the backend rejects with a 4xx (e.g. ToS not accepted / KYC required). Confirm the specific backend message is shown, not a generic one.

2. Deposit — 5xx error surfacing

  • Force a 5xx from the onramp endpoint (or mock it). Confirm only the generic "Failed to create bank transfer..." message shows — no raw backend text leaks.

3. Bridge re-verification pending modal (deposit)

  • Get an account into waiting-on-provider (post-EEA-uplift, Bridge still reviewing). Tap Continue on the deposit amount screen → the "We're reviewing your details" modal should appear (not a dead button, not the KYC modal).
  • Leave the modal open and simulate Bridge clearing the review (or wait for backend to flip status) → confirm the modal auto-dismisses within ~30s without user action.
  • Close the modal manually via "Go to Home" → confirm it navigates home and doesn't immediately reopen on the next visit unless Continue is tapped again.

4. Bridge re-verification pending modal (withdraw)

  • Repeat step 3 on the withdraw bank flow — previously this was a silent no-op; confirm the same modal now appears and auto-dismisses.

5. Analytics — eea_uplift_started

  • For a user in the advisory (future-dated) uplift state: open the amount screen and confirm eea_uplift_started fires once when the advisory modal opens (not only on "Complete now"), with source: 'advisory', urgent: false, and effective_date populated.
  • For a user in the blocking (post-cliff) uplift state: trigger the KYC modal and confirm eea_uplift_started fires with source: 'blocking', urgent: true.
  • Click Continue multiple times without completing → confirm the event fires only once per attempt (dedup).
  • Dismiss the KYC modal or contact support without completing, then later complete an unrelated KYC step → confirm eea_uplift_completed does not fire spuriously.
  • Complete the uplift successfully (either path) → confirm eea_uplift_completed fires exactly once, matching the started event's requirement_key/source.

6. Regression check

  • Confirm normal (non-EEA-uplift) deposit/withdraw KYC flows still work end-to-end unaffected (accept-tos, needs-identity, restart-identity, ready states).
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (2)
src/hooks/useWaitingOnProviderModal.ts (2)

23-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unnecessary type assertion where discriminant narrowing would work.

gate.kind === 'waiting-on-provider' is a proper discriminant on GateState, and per the capability-gate.ts definition userMessage is required (not optional) on that variant. Casting to { userMessage?: string | null } bypasses that safety instead of narrowing directly, and would silently swallow a typo/refactor mistake if the field were ever renamed.

♻️ Proposed fix
-    const isOpen = requested && isWaiting
-    const message = isOpen ? ((gate as { userMessage?: string | null }).userMessage ?? undefined) : undefined
+    const isOpen = requested && isWaiting
+    const message = isOpen && gate.kind === 'waiting-on-provider' ? (gate.userMessage ?? undefined) : undefined
🤖 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/hooks/useWaitingOnProviderModal.ts` around lines 23 - 27, The
`useWaitingOnProviderModal` hook is using an unnecessary type assertion on
`gate` when reading `userMessage`; use the `gate.kind === 'waiting-on-provider'`
discriminant to narrow `GateState` directly and access `gate.userMessage`
without casting. Update the `message` assignment in `useWaitingOnProviderModal`
so it relies on the narrowed waiting-on-provider branch rather than `{
userMessage?: string | null }`, preserving the type safety defined by
`GateState` and `capability-gate.ts`.

5-7: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Tie the re-arm cadence to the submission window. SUBMISSION_WINDOW_MS is private in useSubmissionWindow.ts, so this file can’t import it as proposed; export the shared constant or derive REARM_INTERVAL_MS from a shared helper to keep the modal poller from drifting out of sync.

🤖 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/hooks/useWaitingOnProviderModal.ts` around lines 5 - 7, The re-arm
cadence in useWaitingOnProviderModal is hard-coded and can drift from the
submission window; move SUBMISSION_WINDOW_MS to a shared export or add a shared
helper and derive REARM_INTERVAL_MS from it instead of duplicating the value.
Update the useWaitingOnProviderModal and useSubmissionWindow symbols so both
depend on the same source of truth, keeping the modal poller aligned with the
submission window.
🤖 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.

Nitpick comments:
In `@src/hooks/useWaitingOnProviderModal.ts`:
- Around line 23-27: The `useWaitingOnProviderModal` hook is using an
unnecessary type assertion on `gate` when reading `userMessage`; use the
`gate.kind === 'waiting-on-provider'` discriminant to narrow `GateState`
directly and access `gate.userMessage` without casting. Update the `message`
assignment in `useWaitingOnProviderModal` so it relies on the narrowed
waiting-on-provider branch rather than `{ userMessage?: string | null }`,
preserving the type safety defined by `GateState` and `capability-gate.ts`.
- Around line 5-7: The re-arm cadence in useWaitingOnProviderModal is hard-coded
and can drift from the submission window; move SUBMISSION_WINDOW_MS to a shared
export or add a shared helper and derive REARM_INTERVAL_MS from it instead of
duplicating the value. Update the useWaitingOnProviderModal and
useSubmissionWindow symbols so both depend on the same source of truth, keeping
the modal poller aligned with the submission window.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c248cc9-bc23-4e49-a5d3-47522e11db3b

📥 Commits

Reviewing files that changed from the base of the PR and between 94d684a and 4bb67af.

📒 Files selected for processing (5)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/components/Kyc/KycReverificationPendingModal.tsx
  • src/hooks/__tests__/useWaitingOnProviderModal.test.ts
  • src/hooks/useWaitingOnProviderModal.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Kyc/KycReverificationPendingModal.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx

Replace the ad-hoc gate casts in useWaitingOnProviderModal (userMessage) and
upliftTriggerFromGate (reason.code) with discriminated-union narrowing, so a
future rename of reason.code / userMessage fails the build instead of
silently returning undefined. No behavior change.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass. Addressed the one actionable item — the ad-hoc type assertions.

Both now narrow on the GateState discriminated union instead of casting:

  • useWaitingOnProviderModal: gate.kind === 'waiting-on-provider' ? gate.userMessage ?? undefined : undefined
  • upliftTriggerFromGate: 'reason' in gate ? gate.reason?.code : undefined

So a future rename of reason.code / userMessage now fails the build rather than silently returning undefined. Pushed in the latest commit; typecheck + suites green. The rest of the review was confirmation (no other changes needed).

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