Skip to content

feat(kyc): skippable advisory pre-empt at add/withdraw (Bridge future-dated requirements)#2255

Merged
jjramirezn merged 6 commits into
devfrom
feat/bridge-advisory-preempt
Jun 22, 2026
Merged

feat(kyc): skippable advisory pre-empt at add/withdraw (Bridge future-dated requirements)#2255
jjramirezn merged 6 commits into
devfrom
feat/bridge-advisory-preempt

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Why

A Bridge bank rail can be ENABLED today but carry a future-dated requirement. The backend (peanut-api-ts #1033, deploys first) now surfaces it as a non-blocking hintAction whose NextAction has an effectiveDate. This FE lets the user resolve it early and skippably, so the ~148 users hitting the 2026-06-29 sof_individual_primary_purpose cliff are never interrupted. (Notion TASK-20162.)

What

  • types: mirror the BE additions — RailCapability.hintActions, NextAction.effectiveDate/requirementKey.
  • capability-gate: ready now carries an optional advisory (earliest future-dated hint among ready rails). No new gate kind, no priority change — advisory strictly rides on ready; a current blocker still wins. When the date passes, the BE reclassifies it → the gate becomes fixable-rejection (the existing non-skippable modal) with zero FE cutover logic.
  • useAdvisoryPreempt + AdvisoryPreemptModal: intercept the proceed step once per session with a skippable Complete now / Not now modal. Complete now launches the RFI early (handleInitiateKyc with the advisory's levelKey); Not now continues the transaction.
  • wired at add-money/[country]/bank (intercept after amount validation) and withdraw/[country]/bank (thin wrapper around the untouched money handler).

Tests

Gate advisory derivation (ready+advisory, back-compat, cap-nudge-is-not-advisory, earliest-date-wins, blocker-still-wins) + the hook (defer / skip-runs-proceed / dismiss-once / complete-launches). Full unit suite green (the lone failing suite is the pre-existing worktree public/flags artifact).

Scope / follow-up

Covers add + withdraw (the stated scope). The claim→bank flow (BankFlowManager) is a 3rd Bridge offramp surface — same pattern applies, deferred to a fast-follow to keep this PR tight.

Cross-repo: contract is fully back-compat (all new fields optional); BE #1033 deploys first.

…ed requirements

A Bridge bank rail can be ENABLED now but carry a future-dated requirement
(the BE surfaces it as a non-blocking hintAction whose NextAction has an
effectiveDate — the 2026-06-29 sof_individual_primary_purpose cohort, ~148
users). Today the FE shows nothing until it becomes blocking; this lets the
user resolve it early, skippably, so they're never interrupted.

- types: mirror the BE contract additions — RailCapability.hintActions,
  NextAction.effectiveDate/requirementKey.
- capability-gate: the `ready` state now carries an optional `advisory`
  (earliest future-dated hint among ready rails). No new gate kind, no priority
  change — advisory strictly rides on `ready`; a current blocker still wins.
  When the date passes the BE reclassifies it to blocking and the gate becomes
  fixable-rejection on its own (the existing non-skippable modal) — no FE
  cutover logic.
- useAdvisoryPreempt + AdvisoryPreemptModal: intercept the proceed step once per
  session with a skippable 'Complete now / Not now' modal. 'Complete now'
  launches the RFI early (handleInitiateKyc with the advisory's levelKey); 'Not
  now' continues the transaction.
- wired at add-money/[country]/bank (intercept after amount validation) and
  withdraw/[country]/bank (thin wrapper around the untouched money handler).

Tests: gate advisory derivation (ready+advisory, back-compat, cap-nudge-is-not-
advisory, earliest-date-wins, blocker-still-wins) + the hook (defer, skip-runs-
proceed, dismiss-once, complete-launches). Pairs with peanut-api-ts #1033 (BE
deploys first). Follow-up: same pattern for the claim→bank flow.
@vercel

vercel Bot commented Jun 19, 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 20, 2026 9:06pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 19, 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: 5690e2c4-fee0-4000-84a0-71bdd1b2c2bf

📥 Commits

Reviewing files that changed from the base of the PR and between c19f628 and 832ec52.

📒 Files selected for processing (1)
  • src/hooks/useAdvisoryPreempt.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useAdvisoryPreempt.ts

Walkthrough

The PR adds a non-blocking "advisory pre-empt" step to the bank deposit and withdraw flows. Capability types and the gate derivation logic are extended to surface hint-based advisory requirements on a ready gate. A new useAdvisoryPreempt hook and AdvisoryPreemptModal component manage the skippable interstitial, and a new startKycAction server action is wired through useSumsubKycFlow to initiate a KYC action when the user chooses to act immediately.

Changes

Advisory Pre-Empt KYC Flow

Layer / File(s) Summary
Capability types and GateAdvisory contract
src/types/capabilities.ts, src/utils/capability-gate.ts
RailCapability gains hintActions?, NextAction gains effectiveDate? and requirementKey?, and the GateAdvisory interface plus the ready gate's optional advisory field are defined.
deriveGate advisory computation and getGateAdvisory selector
src/utils/capability-gate.ts, src/utils/capability-gate.test.ts
Internal helpers railHintActions and firstAdvisory pick the earliest future-dated hint among enabled rails; deriveGate's ready branch attaches the result; getGateAdvisory selector and full scenario tests are added.
startKycAction server action and requirementKey in self-heal
src/app/actions/sumsub.ts
StartKycActionResponse interface and startKycAction function POST to /users/kyc/start-action; initiateSelfHealResubmission conditionally includes requirementKey in the resubmit body.
useSumsubKycFlow and useMultiPhaseKycFlow: handleStartAction and requirementKey threading
src/hooks/useSumsubKycFlow.ts, src/hooks/useMultiPhaseKycFlow.ts
handleStartAction(key) is added to call startKycAction, store levelName, set the access token, and mark isActionFlow; handleSelfHealResubmit forwards the new requirementKey; both hooks expose the new handler.
useAdvisoryPreempt hook and AdvisoryPreemptModal component
src/hooks/useAdvisoryPreempt.ts, src/hooks/useAdvisoryPreempt.test.ts, src/components/Kyc/AdvisoryPreemptModal.tsx
useAdvisoryPreempt manages session-scoped dismissed/visible state with intercept, completeNow, skip, and close handlers; AdvisoryPreemptModal renders the skippable prompt with formatted effective date; full hook test suite is included.
Deposit and withdraw page advisory pre-empt wiring
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Both pages derive advisory from their capability gate, initialize useAdvisoryPreempt, wrap their proceed paths with advisoryIntercept, route onCompleteNow through handleSelfHealResubmit('BRIDGE', requirementKey), and render AdvisoryPreemptModal.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#2122: Introduced the foundational deriveGate/GateState capability gating logic that this PR extends with advisory hint-action support.
  • peanutprotocol/peanut-ui#2224: Modified deriveGate's ready-branch selection logic in the same file this PR extends to emit the advisory payload.
  • peanutprotocol/peanut-ui#2080: Modified the Bridge self-heal resubmission pipeline that this PR extends via the new requirementKey parameter in handleSelfHealResubmit.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing a skippable advisory pre-empt feature for future-dated KYC requirements at add/withdraw flows in Bridge bank rails.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation (future-dated Bridge requirements), implementation details, and integration points with appropriate scope boundaries.
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

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

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5762.99 → 5778.82 (+15.83)
Findings: +10 net (+54 new, -44 resolved)

🆕 New findings (54)

  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 102, MI 53.62, SLOC 350
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 93, MI 57.82, SLOC 326
  • critical complexity — src/hooks/useSumsubKycFlow.ts — CC 79, MI 52.54, SLOC 349
  • critical complexity — src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.72, SLOC 324
  • critical complexity — src/utils/capability-gate.ts — CC 62, MI 64.19, SLOC 167
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 52 commits, +584/-489 lines since 6 months ago
  • high hotspot — src/hooks/useSumsubKycFlow.ts — 40 commits, +583/-76 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 38 commits, +387/-170 lines since 6 months ago
  • medium react-long-component — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage is 513 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:48 — OnrampBankPage is 433 lines — split it
  • medium high-mdd — src/hooks/useSumsubKycFlow.ts:21 — useSumsubKycFlow: MDD 169.6 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage: MDD 140.6 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:48 — OnrampBankPage: MDD 136.5 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useMultiPhaseKycFlow.ts:90 — useMultiPhaseKycFlow: MDD 113.1 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage: DLT 72 (calls 72 distinct functions — high context load)
  • medium high-dlt — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:48 — OnrampBankPage: DLT 62 (calls 62 distinct functions — high context load)
  • medium high-dlt — src/hooks/useSumsubKycFlow.ts:21 — useSumsubKycFlow: DLT 37 (calls 37 distinct functions — high context load)
  • medium high-mdd — src/hooks/useSumsubKycFlow.ts:147 — : MDD 36.7 (uses across many lines from declarations)
  • medium method-complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:48 — OnrampBankPage CC 29 SLOC 157
  • medium method-complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage CC 28 SLOC 121

…and 34 more.

✅ Resolved (44)

  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 97, MI 52.72, SLOC 331
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 89, MI 57.53, SLOC 312
  • src/hooks/useSumsubKycFlow.ts — CC 74, MI 52.96, SLOC 325
  • src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.79, SLOC 322
  • src/utils/capability-gate.ts — CC 51, MI 64.81, SLOC 138
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 49 commits, +546/-473 lines since 6 months ago
  • src/hooks/useSumsubKycFlow.ts — 38 commits, +540/-72 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 35 commits, +358/-161 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage is 495 lines — split it
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:46 — OnrampBankPage is 413 lines — split it
  • src/hooks/useSumsubKycFlow.ts:16 — useSumsubKycFlow: MDD 153.1 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage: MDD 147.9 (uses across many lines from declarations)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:46 — OnrampBankPage: MDD 135.7 (uses across many lines from declarations)
  • src/hooks/useMultiPhaseKycFlow.ts:90 — useMultiPhaseKycFlow: MDD 112.5 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage: DLT 68 (calls 68 distinct functions — high context load)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:46 — OnrampBankPage: DLT 59 (calls 59 distinct functions — high context load)
  • src/hooks/useSumsubKycFlow.ts:142 — : MDD 36.7 (uses across many lines from declarations)
  • src/hooks/useSumsubKycFlow.ts:16 — useSumsubKycFlow: DLT 36 (calls 36 distinct functions — high context load)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:46 — OnrampBankPage CC 28 SLOC 148
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:61 — WithdrawBankPage CC 27 SLOC 110

…and 24 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useAdvisoryPreempt.ts 0.0 7.0 +7.0
src/components/Kyc/AdvisoryPreemptModal.tsx 0.0 5.6 +5.6
src/app/actions/sumsub.ts 9.9 10.7 +0.8
src/utils/capability-gate.ts 6.8 7.5 +0.7
src/hooks/useSumsubKycFlow.ts 16.7 17.3 +0.6
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx 19.1 19.6 +0.5

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1501 ran, 0 failed, 0 skipped, 22.5s

📊 Coverage (unit)

metric %
statements 52.8%
branches 35.4%
functions 40.3%
lines 52.6%
⏱ 10 slowest test cases
time test
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
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 9-digit US account
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid Italian IBAN
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid UK IBAN with spaces
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/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces 2
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

Hugo0 added 2 commits June 18, 2026 21:35
…, UTC date

Audit fixes:
- 'Complete now' dispatched via handleInitiateKyc -> /users/identity, which
  ignores the level and no-ops for already-approved users (the only cohort with
  an enabled advisory rail) -> SDK never opened. Add startKycAction +
  handleStartAction to POST the NextAction key to /users/kyc/start-action (the
  capability-native path that resolves key -> RFI level + mints a token).
  GateAdvisory now carries the action key (not levelKey).
- onClose (X/backdrop/Escape) stranded the user + re-prompted every click: now
  dismisses for the session without auto-triggering the money action.
- effectiveDate is date-only YYYY-MM-DD -> format in UTC, else Americas
  timezones show the day before the deadline.
- move add-money's DEPOSIT_AMOUNT_ENTERED into the proceed callback so an
  X-then-reclick can't double-count it.
Advisory Complete now called handleStartAction (start-action), whose
submission never round-trips to Bridge. Switch both add-money and withdraw
bank pages to handleSelfHealResubmit('BRIDGE', advisory.requirementKey),
the path whose webhook completion relays answers to Bridge. Thread an
optional requirementKey through initiateSelfHealResubmission and
handleSelfHealResubmit to target the future-dated advisory requirement.

handleStartAction/startKycAction are now unused (kept for a focused
follow-up cleanup to avoid the isActionFlow cascade in this diff).

@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: 1

🧹 Nitpick comments (2)
src/utils/capability-gate.ts (1)

32-33: ⚡ Quick win

Update stale actionKey contract comment to match current flow.

Line 32 still says actionKey is for POST /users/kyc/start-action, but the wired "Complete now" path uses self-heal resubmit keyed by requirementKey. This doc drift can mislead future edits.

Suggested doc-only diff
-    /** the NextAction `key` to start (POST /users/kyc/start-action) if the user completes it now. */
+    /** the originating NextAction `key` for analytics/debug correlation of the advisory hint. */
     actionKey: string
🤖 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/capability-gate.ts` around lines 32 - 33, The JSDoc comment for the
actionKey field in capability-gate.ts is outdated and references the old POST
/users/kyc/start-action endpoint, but the current implementation uses a
self-heal resubmit flow keyed by requirementKey instead. Update the comment for
the actionKey field to accurately reflect its actual role in the current
self-heal resubmit flow rather than the deprecated endpoint reference, ensuring
future maintainers understand the correct flow.
src/hooks/useAdvisoryPreempt.ts (1)

39-44: ⚡ Quick win

Add a regression test for rapid double-invocation of onCompleteNow.

A small test here would protect the new flow from accidental duplicate submissions in future refactors.

🤖 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/useAdvisoryPreempt.ts` around lines 39 - 44, Add a regression test
for the useAdvisoryPreempt hook that verifies the completeNow callback properly
handles rapid double-invocation. The test should call the completeNow function
twice in quick succession and assert that onCompleteNow is only called once and
that the state variables (dismissed, visible, and pendingProceed.current) are
set correctly to prevent duplicate submissions. This protects the callback logic
from accidental duplicate executions in future refactors.
🤖 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/hooks/useAdvisoryPreempt.ts`:
- Line 25: The completeNow function (lines 39-44) allows re-entry and multiple
submissions when clicked rapidly before parent isLoading disables the button.
Use the pendingProceed useRef to track whether a request is already in flight.
In the completeNow function, check if pendingProceed is already set to a pending
callback before executing onCompleteNow, and if it is, return early to prevent
duplicate submissions. Set pendingProceed when starting the self-heal/KYC
request and clear it when the request completes to allow subsequent submissions
only after the current one finishes.

---

Nitpick comments:
In `@src/hooks/useAdvisoryPreempt.ts`:
- Around line 39-44: Add a regression test for the useAdvisoryPreempt hook that
verifies the completeNow callback properly handles rapid double-invocation. The
test should call the completeNow function twice in quick succession and assert
that onCompleteNow is only called once and that the state variables (dismissed,
visible, and pendingProceed.current) are set correctly to prevent duplicate
submissions. This protects the callback logic from accidental duplicate
executions in future refactors.

In `@src/utils/capability-gate.ts`:
- Around line 32-33: The JSDoc comment for the actionKey field in
capability-gate.ts is outdated and references the old POST
/users/kyc/start-action endpoint, but the current implementation uses a
self-heal resubmit flow keyed by requirementKey instead. Update the comment for
the actionKey field to accurately reflect its actual role in the current
self-heal resubmit flow rather than the deprecated endpoint reference, ensuring
future maintainers understand the correct flow.
🪄 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: 893da4fb-a129-40a2-80d4-24f3b2c5224b

📥 Commits

Reviewing files that changed from the base of the PR and between 1569160 and c19f628.

📒 Files selected for processing (11)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/actions/sumsub.ts
  • src/components/Kyc/AdvisoryPreemptModal.tsx
  • src/hooks/useAdvisoryPreempt.test.ts
  • src/hooks/useAdvisoryPreempt.ts
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/hooks/useSumsubKycFlow.ts
  • src/types/capabilities.ts
  • src/utils/capability-gate.test.ts
  • src/utils/capability-gate.ts

Comment thread src/hooks/useAdvisoryPreempt.ts
…bbit)

onCompleteNow now fires a real network call (self-heal resubmit), so rapid
clicks before isLoading disables the CTA could launch duplicate requests.
Add a completingRef in-flight guard.
@jjramirezn jjramirezn marked this pull request as ready for review June 20, 2026 21:39

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

@jjramirezn reviewed mainly on code quality side, dont see anything concerning, tho i dont see any screenshots to judge/review the ui, please test it once you merge it

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.

3 participants