Skip to content

feat(home): pending Bridge verification tasks card - #2549

Merged
jjramirezn merged 10 commits into
mainfrom
feat/pending-verification-tasks
Aug 5, 2026
Merged

feat(home): pending Bridge verification tasks card#2549
jjramirezn merged 10 commits into
mainfrom
feat/pending-verification-tasks

Conversation

@abalinda

@abalinda abalinda commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Users whose Bridge customer owes "additional verification" tasks (hosted Persona re-verification and/or ToS (re-)acceptance) could only see them in Bridge's dashboard. In-app: the activation card deliberately stands down for anyone who can already transact, and these tasks arrive as orphan nextActions no rail references — so affected users saw nothing.

New self-hiding PendingVerificationTasks card on /home, sibling of ActivationCTAs (outside its stand-down on purpose). Reads top-level capability nextActions — the only surface that sees orphan actions (both blocking hosted tasks and advisory future-dated ones) — and renders one slide per task with a "Complete before {date}" line for advisories:

  • accept-tos → mounts the existing BridgeTosStep (link fetch → iframe → signedAgreementId confirm → rail await); each row opens the modal for its variant (base vs SEPA v2)
  • bridge-hosted (new kind, peanutprotocol/peanut-api-ts#1248) → exchanges the key for Bridge's hosted verification URL via start-action and opens it in the existing IframeWrapper (Persona complete postMessage already handled; user refetch on completion)

Open flows are snapshotted at tap time and mounted above the self-hiding early return — the task list re-derives from the ~4s user auto-refresh, and an open modal/iframe must survive its task flapping away mid-verification. IframeWrapper additionally gained a visible guard on its window-message listener: surfaces keep a wrapper mounted after manual close, and a sibling iframe's completion event previously fired both handlers (double ToS confirm + phantom flow transitions) — this PR adds the second ToS surface on /home that made that latent hazard reachable, so it carries the fix.

Multiple tasks render as a horizontal carousel (same embla setup as HomeCarouselCTA — full-width slides, swipe + dots); a single task looks identical to a static card. On the /home mount each slide has its own X that dismisses only that task (persisted per task key — dismissed keys stay hidden until the task resolves); the same component mounts non-dismissibly under Profile → Unlocked regions, where dismissed users find their tasks again.

No raw links are shown — both flows open in the in-app full-screen iframe, same UX as the production ToS step. Everything Sumsub-able (questionnaires, doc uploads) keeps flowing through the Sumsub WebSDK untouched.

Screenshots

Captured on the local sandbox (worktree API #1248 + this branch, seeded state mirroring prod user 7c928f39's exact classification), iPhone 14 viewport. Assets branch pr-assets-2549 — delete after merge.

Video recoridng (only thing you need)

Screen.Recording.2026-07-29.at.16.39.50.mov

Adversarial review

3 independent adversarial agents attacked both PRs before this went up; their confirmed findings are fixed in the second commit (snapshot-at-open, per-variant ToS routing, IframeWrapper visible-guard, population-aware copy, friendly errors + resync, deadline guard). Full detail in the commit message.

Risks / breaking changes

  • Deploy order: any. Old BE + this FE → the card renders only what's emitted (older BEs emit accept-tos for blocking users, for whom the ToS route already works); new BE + old FE → the new actions are orphans, provably inert to the deployed gate ladder. No NEXT_PUBLIC_API_VERSION bump (additive).
  • Base is main (per request) → back-merge debt main→dev after merge.
  • IframeWrapper visible-guard touches a shared component — behavior change only for hidden-but-mounted wrappers (they previously reacted to sibling iframes' messages; that was the bug).

QA

  • 18 Jest cases (incl. 4 dismissal cases: persist + same-set hidden + new-set re-show + profile-ignores-dismissal) (component + selector): variant routing per row, snapshot-survives-flap, hosted URL → iframe → refetch-on-complete, manual-close no-refetch, friendly-error + resync, deadline + malformed-date guard, population copy, self-hiding.
  • Full suite 169/169 green; typecheck clean; sandbox visual QA above (the seeded state also live-verified the BE emission end-to-end).

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a “Pending verification tasks” component to Home and Profile (Unlocked regions) for Bridge/hosted verification actions.
    • Added support for initiating the Bridge Hosted verification flow, including a new action kind.
  • Bug Fixes

    • Prevented hidden/off-screen verification iframes from triggering unintended close behavior.
    • Improved dismissal-state loading and persistence so tasks can re-surface when their fingerprint changes.
    • Ensured embedded ToS acceptance doesn’t interrupt the hosted verification flow.
  • Tests

    • Expanded Jest coverage for Bridge task selection, dismissal fingerprints, and embedded ToS + closing behavior.
  • Chores

    • Enhanced Jest setup for IntersectionObserver and matchMedia.

Users whose Bridge customer owes 'additional verification' tasks (the
hosted Persona re-verification and/or a ToS (re-)acceptance) could only
see them in Bridge's dashboard — in-app there was nothing actionable:
the activation card deliberately stands down for anyone who can already
transact, and advisory (future-dated) tasks arrive as orphan nextActions
no rail references.

New self-hiding PendingVerificationTasks card on /home, sibling of
ActivationCTAs (outside its stand-down on purpose). It reads top-level
capability nextActions — the only surface that catches both blocking
tasks and advisory orphans — and renders one row per task:

- accept-tos → mounts the existing BridgeTosStep wholesale (link fetch,
  iframe, signedAgreementId confirm, rail await)
- bridge-hosted (new kind, BE api#TBD) → exchanges the key for Bridge's
  hosted verification URL via start-action and opens it in the existing
  IframeWrapper (its Persona 'complete' postMessage handler already
  covers completion; refetch user on close)

Old-BE tolerance: an older backend never emits bridge-hosted and only
emits accept-tos for blocking users — the card simply renders less.
No NEXT_PUBLIC_API_VERSION bump (additive contract).
@vercel

vercel Bot commented Jul 28, 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 Aug 4, 2026 5:41pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Bridge pending-verification task cards with ToS and hosted-verification flows, fingerprint-based dismissal persistence, capability refresh handling, iframe visibility guards, and Home/Profile placement.

Changes

Bridge verification task flow

Layer / File(s) Summary
Bridge task contracts and dismissal fingerprints
src/types/capabilities.ts, src/utils/bridge-tasks.utils.ts, src/utils/general.utils.ts, src/utils/__tests__/bridge-tasks.utils.test.ts
Adds the bridge-hosted action kind, selects Bridge tasks, generates dismissal fingerprints, and documents and tests persisted fingerprint behavior.
Hosted verification request and iframe visibility
src/app/actions/sumsub.ts, src/components/Global/IframeWrapper/index.tsx
Starts hosted verification through /users/kyc/start-action and ignores iframe messages while hidden.
Pending task card behavior
src/components/Home/PendingVerificationTasks.tsx, src/components/Home/__tests__/PendingVerificationTasks.test.tsx
Renders task states, supports ToS and hosted flows, handles embedded ToS acceptance, persists dismissals, preserves active iframe state, and tests task transitions.
Home and profile placement
src/app/(mobile-ui)/home/page.tsx, src/components/Profile/views/UnlockedRegions.view.tsx
Renders the dismissible task card on Home and the non-dismissible card in Profile unlocked regions.
Carousel test environment support
jest.setup.ts
Adds IntersectionObserver and matchMedia stubs for component tests.

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

Suggested labels: enhancement

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PendingVerificationTasks
  participant startBridgeHostedVerification
  participant IframeWrapper
  participant fetchUser
  User->>PendingVerificationTasks: Open hosted verification task
  PendingVerificationTasks->>startBridgeHostedVerification: Start bridge-hosted action
  startBridgeHostedVerification-->>PendingVerificationTasks: Return verification URL
  PendingVerificationTasks->>IframeWrapper: Open verification iframe
  IframeWrapper-->>PendingVerificationTasks: Report ToS acceptance or completion
  PendingVerificationTasks->>fetchUser: Refresh user capabilities
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a Bridge verification tasks card to the home page.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pending-verification-tasks

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

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6333.48 → 6347.26 (+13.78)
Findings: +8 net (+41 new, -33 resolved)

🆕 New findings (41)

  • high complexity — src/components/Home/PendingVerificationTasks.tsx — CC 49, MI 61.9, SLOC 129
  • high hotspot — src/app/(mobile-ui)/home/page.tsx — 31 commits, +151/-136 lines since 6 months ago
  • high complexity — src/app/actions/sumsub.ts — CC 31, MI 51.45, SLOC 117
  • medium high-mdd — src/components/Profile/views/UnlockedRegions.view.tsx:65 — UnlockedRegions: MDD 92.1 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/IframeWrapper/index.tsx:16 — IframeWrapper: MDD 84.3 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/home/page.tsx:56 — Home: MDD 49.4 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/home/page.tsx:56 — Home: DLT 33 (calls 33 distinct functions — high context load)
  • medium high-mdd — src/components/Home/PendingVerificationTasks.tsx:72 — PendingVerificationTasks: MDD 32.4 (uses across many lines from declarations)
  • medium high-dlt — src/components/Profile/views/UnlockedRegions.view.tsx:65 — UnlockedRegions: DLT 30 (calls 30 distinct functions — high context load)
  • medium complexity — src/components/Global/IframeWrapper/index.tsx — CC 29, MI 65.83, SLOC 116
  • medium hotspot — src/utils/general.utils.ts — 29 commits, +180/-185 lines since 6 months ago
  • medium hotspot — src/app/actions/sumsub.ts — 26 commits, +247/-70 lines since 6 months ago
  • medium method-complexity — src/components/Profile/views/UnlockedRegions.view.tsx:65 — CC 26 SLOC 118
  • medium high-mdd — src/app/(mobile-ui)/home/page.tsx:317 — WalletBalance: MDD 21.0 (uses across many lines from declarations)
  • medium complexity — src/utils/format.utils.ts — CC 20, MI 61.55, SLOC 74
  • medium react-effect-derives-state — src/app/(mobile-ui)/home/page.tsx:99 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/components/Home/PendingVerificationTasks.tsx:95 — small useEffect that only sets state from deps
  • low high-dlt — src/components/Home/PendingVerificationTasks.tsx:72 — PendingVerificationTasks: DLT 27 (calls 27 distinct functions — high context load)
  • low high-dlt — src/components/Global/IframeWrapper/index.tsx:16 — IframeWrapper: DLT 18 (calls 18 distinct functions — high context load)
  • low structural-dup — components/Profile/views/UnlockedRegions.view.tsx:372 — 16 duplicate lines / 83 tokens with features/limits/views/LimitsPageView.tsx:146

…and 21 more.

✅ Resolved (33)

  • src/components/Profile/views/UnlockedRegions.view.tsx:64 — UnlockedRegions: MDD 87.9 (uses across many lines from declarations)
  • src/components/Global/IframeWrapper/index.tsx:16 — IframeWrapper: MDD 74.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/home/page.tsx:55 — Home: MDD 48.5 (uses across many lines from declarations)
  • src/app/(mobile-ui)/home/page.tsx:55 — Home: DLT 33 (calls 33 distinct functions — high context load)
  • src/components/Profile/views/UnlockedRegions.view.tsx:64 — UnlockedRegions: DLT 30 (calls 30 distinct functions — high context load)
  • src/app/(mobile-ui)/home/page.tsx — 29 commits, +142/-134 lines since 6 months ago
  • src/components/Global/IframeWrapper/index.tsx — CC 27, MI 66.44, SLOC 112
  • src/components/Profile/views/UnlockedRegions.view.tsx:64 — CC 26 SLOC 118
  • src/app/actions/sumsub.ts — CC 25, MI 51.96, SLOC 95
  • src/app/actions/sumsub.ts — 25 commits, +222/-70 lines since 6 months ago
  • src/utils/general.utils.ts — 25 commits, +167/-178 lines since 6 months ago
  • src/app/(mobile-ui)/home/page.tsx:310 — WalletBalance: MDD 21.0 (uses across many lines from declarations)
  • src/utils/format.utils.ts — CC 17, MI 62.06, SLOC 62
  • src/app/(mobile-ui)/home/page.tsx:98 — small useEffect that only sets state from deps
  • src/components/Global/IframeWrapper/index.tsx:16 — IframeWrapper: DLT 17 (calls 17 distinct functions — high context load)
  • components/Profile/views/UnlockedRegions.view.tsx:364 — 16 duplicate lines / 83 tokens with features/limits/views/LimitsPageView.tsx:146
  • components/Profile/views/UnlockedRegions.view.tsx:365 — 16 duplicate lines / 73 tokens with features/limits/views/LimitsPageView.tsx:189
  • src/components/Kyc/AdvisoryPreemptModal.tsx:28 — AdvisoryPreemptModal: MDD 13.5 (uses across many lines from declarations)
  • src/app/(mobile-ui)/home/page.tsx:372 — ActionButton: MDD 11.0 (uses across many lines from declarations)
  • src/components/Profile/views/UnlockedRegions.view.tsx:365 — : MDD 10.6 (uses across many lines from declarations)

…and 13 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Home/PendingVerificationTasks.tsx 0.0 9.3 +9.3
src/utils/bridge-tasks.utils.ts 0.0 2.8 +2.8
src/utils/general.utils.ts 18.0 18.6 +0.5
src/components/Global/IframeWrapper/index.tsx 7.5 8.0 +0.5
src/components/Kyc/AdvisoryPreemptModal.tsx 4.9 4.1 -0.8

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2348 ran, 0 failed, 0 skipped, 39.3s

📊 Coverage (unit)

metric %
statements 63.0%
branches 46.8%
functions 52.8%
lines 63.4%
⏱ 10 slowest test cases
time test
3.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/demo-balance.test.ts › starts at the full balance on a fresh install and stamps a timestamp
0.2s src/utils/__tests__/demo-balance.test.ts › keeps a spent-down balance across a cold start within the TTL
0.2s src/utils/__tests__/demo-balance.test.ts › auto-refills a wallet older than the TTL on cold start
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

- Open flows are snapshotted at tap time and mounted ABOVE the
  self-hiding early return: the task list re-derives from the ~4s user
  auto-refresh, and the previous shape force-unmounted an OPEN ToS
  modal / hosted iframe mid-verification when the task flapped away.
  The card hides; the flow finishes.
- Each ToS row opens the modal for ITS variant (a user can owe base AND
  SEPA v2 simultaneously — the single find() gave the second row the
  first row's copy).
- IframeWrapper ignores window messages while hidden: surfaces keep a
  wrapper mounted after manual close (multi-phase KYC ToS), and a
  sibling iframe's signedAgreementId event fired BOTH handlers — double
  ToS confirms + phantom flow transitions. This PR adds the second ToS
  surface on /home that made the latent hazard reachable, so it carries
  the guard.
- Copy is population-aware (advisory 'keep … available' vs blocking
  'enable'), start-action failures show friendly copy + resync the user
  (a 403 means the action aged out), malformed effectiveDate renders no
  deadline instead of 'Invalid Date', errors clear when the task set
  changes, and all task buttons disable while the hosted URL fetch is
  in flight (no stacked modals).
Aleks: the /home card should be closable like the carousel CTAs, without
losing the tasks. The X persists the dismissal per task-key set (a
DIFFERENT set of pending tasks re-shows the card); the same component
mounts non-dismissibly under Profile → Unlocked regions, which is where
dismissed users find their tasks again.
@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 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.

@abalinda
abalinda marked this pull request as ready for review July 28, 2026 14:23
innolope-dev added a commit that referenced this pull request Jul 29, 2026
…s, PIX exit, verification tasks

Cherry-picks the open native-facing work onto the release branch:

- KYC now routes every native entry point through the Sumsub Cordova SDK
  instead of the WebSDK (peanut-ui #2562) — the one change here that genuinely
  requires a new binary rather than an OTA.
- Deferred deep linking survives the store install, so a link followed before
  install lands on its destination on first launch (#2560, TASK-20772).
- PIX deposit "Done" exits to home from both the completed and processing
  states instead of dropping the user into a new deposit (#2548).
- Pending Bridge verification tasks card on home, dismissible and resurfaced
  under Unlocked regions (#2549).
- Native demo/passkey follow-ups: the awaited token clear in clearAuthState
  plus the two callers it changed (#2517 — the rest shipped in 1.0.40).
- Documents left running on a superseded deployment now reload (#2563).
- Android safe zone sized from natively measured insets.

Not carried: the CSP allow-list gaps (#2564). It builds on the #2519 collector
rework — a /api/csp-report route and ten commits that are not on this branch —
and the policy is report-only, so the gaps cost report fidelity, not behaviour.
With ToS + hosted verification pending together the stacked card grew
tall; reuse the HomeCarouselCTA embla setup so each task is its own
full-width slide (identical footprint to the single-task card, dots +
swipe only when there's more than one). jest.setup gains matchMedia and
IntersectionObserver stubs — embla needs both at init and jsdom has
neither, same gap the existing ResizeObserver stub covers.
Now that tasks are separate carousel slides, one X hiding ALL of them
reads as a bug — each slide gets its own X and the preference stores
dismissed task keys instead of the joined set. A dismissed key stays
hidden on /home until the task resolves (keys are a tiny stable
vocabulary, so no pruning); the Profile mount still shows everything.

@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

🤖 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/utils/general.utils.ts`:
- Around line 501-504: Align the documentation for
pendingVerificationTasksDismissed with its declared string[] type by describing
it as a sorted array of task keys, unless the surrounding implementation
intentionally serializes it as one comma-joined string, in which case change the
type accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73efa98a-789a-412e-bc5d-2d63b40a3b96

📥 Commits

Reviewing files that changed from the base of the PR and between 6f9a4c0 and 4527482.

📒 Files selected for processing (3)
  • src/components/Home/PendingVerificationTasks.tsx
  • src/components/Home/__tests__/PendingVerificationTasks.test.tsx
  • src/utils/general.utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Home/tests/PendingVerificationTasks.test.tsx
  • src/components/Home/PendingVerificationTasks.tsx

Comment thread src/utils/general.utils.ts Outdated
…hape

CodeRabbit: the JSDoc still described the pre-carousel comma-joined
set string.
@jjramirezn

Copy link
Copy Markdown
Contributor

Automated review (Claude, high effort) found two bugs worth fixing before merge.

1. A dismissed advisory card never comes back when the task turns blocking

src/components/Home/PendingVerificationTasks.tsx:109 (confirmed)

The dismissal fingerprint is the sorted task-key set only. Task keys do not change when a task goes from advisory (due later) to blocking (due now): accept-tos:sepa and the constant bridge-hosted key stay identical after the effectiveDate passes.

Failure case: the user dismisses the "Complete before Sep 1" card in July. On Sep 1 Bridge reclassifies the requirement, the rail drops to requires-info, and the user's bank transfers start to fail. The keys are unchanged, so isDismissed stays true and /home shows nothing. The user only finds out when a transfer hits a hard gate. A new, genuinely different requirement that arrives under the shared bridge-hosted key stays dismissed too.

Fix: include the advisory/blocking state (or the effectiveDate) in the fingerprint, so the transition invalidates the dismissal.

2. A ToS step inside Bridge's hosted flow closes the iframe mid-verification

src/components/Home/PendingVerificationTasks.tsx:133 + src/components/Global/IframeWrapper/index.tsx (plausible — please test manually)

IframeWrapper maps any postMessage that carries signedAgreementId to onClose('tos_accepted'). handleHostedClose treats every source except 'completed' as a plain dismiss. Bridge's hosted kyc_link flow can start with a ToS-acceptance step before the identity steps — exactly for this cohort, which never finished hosted KYC.

Failure case: the user clicks Accept on the embedded ToS page. The page posts signedAgreementId, the wrapper fires onClose('tos_accepted'), and handleHostedClose sets hostedUrl to null. The verification window vanishes before the user reaches the document steps, and no refetch runs (only 'completed' triggers fetchUser). Repeated taps loop the same truncated flow.

Fix: in the hosted context, ignore tos_accepted (keep the iframe open) or treat it as progress, not close.


Two smaller notes: the new if (!visible) return guard in IframeWrapper drops a 'complete' message that arrives just after the modal hides — Bridge records the acceptance but the app never confirms it. And dismissedKeys hydrates from localStorage in a post-paint effect, so a dismissed card flickers on every /home mount.

…he hosted flow

Review (Jota, automated high-effort pass) found two holes:

1. Dismissal persisted by task KEY alone — keys don't change when an
   advisory task turns blocking (effectiveDate passes) or a new Bridge
   requirement lands under the shared bridge-hosted key, so a user who
   dismissed a 'complete before Sep 1' reminder saw NOTHING when their
   transfers actually broke. Dismissals now persist a
   key|requirement|due fingerprint; any escalation or substance change
   re-surfaces the slide.

2. Bridge's hosted kyc_link flow can open with an embedded ToS step —
   exactly for this cohort, which owes both. Its signedAgreementId
   postMessage maps to onClose('tos_accepted'), which closed the iframe
   mid-verification with no refetch. tos_accepted now keeps the hosted
   iframe open (and syncs the acceptance); only completed/manual close.
@abalinda

Copy link
Copy Markdown
Contributor Author

Both addressed in 4a65c4c.

1 (dismissal survives escalation) — confirmed, with a note: the cited isDismissed/set-fingerprint code was already replaced by per-task-key dismissal in 4527482 (the review ran against an older checkout), but the failure mode fully survived that rewrite — keys still don't change on advisory→blocking. Fixed by persisting a key|requirementKey|effectiveDate fingerprint (bridgeTaskDismissalKey): escalation or a new requirement under the shared bridge-hosted key re-surfaces the slide. Unit tests cover both transitions.

2 (embedded ToS closes the hosted iframe) — confirmed structurally: handleHostedClose now treats tos_accepted as mid-flow progress — iframe stays open, acceptance syncs via fetchUser; only completed/manual close. Jest-covered; still worth one manual pass against a live sandbox customer that owes both ToS + KYC, since Bridge's embedded-ToS postMessage behavior is the one thing jsdom can't prove.

@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

🤖 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/components/Home/PendingVerificationTasks.tsx`:
- Around line 117-119: Update the visibleTasks logic in PendingVerificationTasks
so the dismissible branch renders no tasks until stored dismissal preferences
have finished hydrating; once ready, continue filtering with dismissedKeys and
bridgeTaskDismissalKey, while preserving the existing non-dismissible behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f75fa7c-270c-490c-999b-ba1e32fe4211

📥 Commits

Reviewing files that changed from the base of the PR and between 4527482 and 4a65c4c.

📒 Files selected for processing (5)
  • src/components/Home/PendingVerificationTasks.tsx
  • src/components/Home/__tests__/PendingVerificationTasks.test.tsx
  • src/utils/__tests__/bridge-tasks.utils.test.ts
  • src/utils/bridge-tasks.utils.ts
  • src/utils/general.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/general.utils.ts

Comment thread src/components/Home/PendingVerificationTasks.tsx Outdated
CodeRabbit: dismissedKeys loads in a post-render effect (localStorage is
SSR-unreadable), so the first paint briefly showed tasks the user had
already dismissed. null now means not-yet-hydrated and the dismissible
mount renders nothing until the effect lands.

@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/Home/PendingVerificationTasks.tsx (1)

105-108: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear dismissal state when userId changes. dismissedKeys stays in memory across a logout/login, so the next user can inherit the previous user’s hidden tasks for one render before this effect reloads their preferences. Reset it to null or key it by userId, and keep the card hidden until the new user’s dismissals are loaded.

🤖 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/Home/PendingVerificationTasks.tsx` around lines 105 - 108,
Update the PendingVerificationTasks dismissal state flow around the useEffect so
dismissedKeys is cleared or isolated immediately when userId changes, preventing
the previous user’s dismissals from being rendered for the new user. Keep the
card hidden until getUserPreferences(userId) has loaded the new user’s
pendingVerificationTasksDismissed values.
🤖 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/Home/PendingVerificationTasks.tsx`:
- Around line 105-108: Update the PendingVerificationTasks dismissal state flow
around the useEffect so dismissedKeys is cleared or isolated immediately when
userId changes, preventing the previous user’s dismissals from being rendered
for the new user. Keep the card hidden until getUserPreferences(userId) has
loaded the new user’s pendingVerificationTasksDismissed values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a9d6a44-86de-44e5-acaf-63d7fb934046

📥 Commits

Reviewing files that changed from the base of the PR and between 4a65c4c and 3ae47e1.

📒 Files selected for processing (1)
  • src/components/Home/PendingVerificationTasks.tsx

CodeRabbit: dismissedKeys stayed in memory when userId changed, so the
next user inherited the previous user's hidden tasks for the render
before the effect reloaded their preferences. Stored dismissals are now
tagged with the user they were loaded for; a mismatched tag counts as
not-hydrated and keeps the card held.
@jjramirezn

jjramirezn commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

/code-review (high) — verified findings

Multi-agent review of this PR together with peanutprotocol/peanut-api-ts#1248. 4 finder angles, independent adversarial verification per finding; 6 candidates refuted. Frontend findings below — backend findings are on the API PR.

Correctness

1. Hosted-flow ToS acceptance is never confirmed to the backendsrc/components/Home/PendingVerificationTasks.tsx:167 (CONFIRMED)
handleHostedClose('tos_accepted') only calls fetchUser(), which re-reads stored DB state (the resolver is pure — no Bridge calls). Every other ToS path (BridgeTosStep, useMultiPhaseKycFlow) calls confirmBridgeTosAndAwaitRails, whose backend re-fetches the Bridge customer and runs the status processor.
Failure: user accepts ToS inside the hosted kyc_link iframe → task card and rail gate stay stale until a Bridge webhook arrives. Tapping the still-visible ToS card can 409 (Bridge ToS already accepted) → "Could not load terms" error modal for terms already accepted.

2. Dismissal fingerprint for blocking accept-tos tasks is constant over timesrc/utils/bridge-tasks.utils.ts:25 (CONFIRMED)
Blocking accept-tos actions carry no requirementKey/effectiveDate, so every due-now ToS of a variant fingerprints as accept-tos||due-now. A NEW ToS of the same variant months later matches the old localStorage dismissal and the home card never re-surfaces — user's bank rails stay gated with no visible task except in Profile → Unlocked regions.

3. Hidden-wrapper guard drops racing completion messagessrc/components/Global/IframeWrapper/index.tsx:104 (PLAUSIBLE)
The new if (!visible) return at the top of handleMessage drops a signedAgreementId/complete message that lands just after visible flips false (manual close or parent state transition in the same instant). The acceptance then exists at Bridge but is never confirmed in-app.

Cleanup

4. Duplicated date formattersrc/components/Home/PendingVerificationTasks.tsx:47
formatDeadline re-implements formatEffectiveDate from src/components/Kyc/AdvisoryPreemptModal.tsx (same UTC-pinned parse, differs only in month: 'short' vs 'long' — same deadline renders "Mar 1, 2099" vs "March 1, 2099"). Extract one shared formatter into a utils file.

…ble; source-matched iframe messages

/code-review (high) 2026-08-04:

Finding 1 (CONFIRMED): handleHostedClose('tos_accepted') only refetched the
user — the resolver is pure, so the embedded acceptance was never recorded
until a Bridge webhook landed, and tapping the still-visible ToS card 409'd
into 'Could not load terms'. The hosted path now runs
confirmBridgeTosAndAwaitRails (the same canonical confirm every other ToS
surface uses) with the iframe staying open; on failure it still resyncs.

Finding 2 (CONFIRMED): a blocking task's dismissal fingerprint is constant
over time (accept-tos||due-now), so an old dismissal would hide a NEW
same-variant requirement while the user's rails are gated — and the orphan
bridge-hosted task has no other surface outside Profile. There is nothing
unique to fingerprint a new due-now round WITH, so the fix is one level up:
blocking (due-now) tasks no longer render an X and ignore stored
fingerprints (pre-fix localStorage entries included); advisory dismissal
behavior is unchanged.

Finding 3 (PLAUSIBLE): IframeWrapper now reacts only to messages whose
source is its OWN iframe instead of gating on visible — the sibling
double-confirm protection survives (now even against two concurrently
VISIBLE wrappers, which the visible-guard let through and finding 1's new
confirm call would have turned into a double confirm), while a completion
landing as the modal hides is no longer dropped. New IframeWrapper spec pins
own-handled / sibling-ignored / sourceless-ignored.

Finding 4: formatDeadline duplicated AdvisoryPreemptModal's formatter with a
different month style. One shared formatEffectiveDate in format.utils serves
both surfaces (long month everywhere); the add-money suite's format.utils
mock now spreads requireActual so partial mocks don't break on new exports.
@abalinda

abalinda commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

All four addressed in 4a613a0.

1 (hosted ToS never confirmed) — confirmed, fixed. handleHostedClose('tos_accepted') now runs confirmBridgeTosAndAwaitRails — the same canonical confirm every other ToS surface uses (it refetches the user itself; on failure the component still resyncs) — with the iframe staying open for the identity steps. Jest covers the confirm call and the failure fallback.

2 (constant blocking fingerprint) — confirmed, fixed one level up. There is nothing unique to fingerprint a NEW same-variant due-now ToS with (requirementKey doesn't churn across rounds), so instead of a better fingerprint: blocking (due-now) tasks no longer render an X and ignore stored fingerprints entirely — a pre-fix stored accept-tos||due-now can't hide a new one either. Dismissal remains for advisories, whose fingerprint does churn (date/requirement), and the advisory→blocking escalation is now covered twice (fingerprint change + blocking never consults dismissals). This also closes the sharpest case — the orphan bridge-hosted task, whose only surface outside Profile is this card.

3 (!visible drops racing completion) — fixed via source-matching. The wrapper now reacts only to messages whose event.source is its OWN iframe's contentWindow, instead of gating on visible. Sibling double-confirm protection survives — and strengthens: two concurrently-VISIBLE wrappers previously fired each other's handlers (the visible-guard let that through, and finding 1's new confirm call would have turned it into a double confirm) — while a completion landing in the instant the modal hides is no longer dropped. New IframeWrapper spec pins own-handled / sibling-ignored / sourceless-ignored.

4 (duplicated formatter) — fixed. formatEffectiveDate moved to format.utils; both AdvisoryPreemptModal and the tasks card import it, so the same deadline renders identically (long month) everywhere. The add-money suite's partial format.utils mock now spreads requireActual so the next new export doesn't break it again.

@jjramirezn
jjramirezn merged commit ce141dc into main Aug 5, 2026
24 of 26 checks passed
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