feat(home): pending Bridge verification tasks card - #2549
Conversation
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).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesBridge verification task flow
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested labels: 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Code-analysis diffPainscore total: 6333.48 → 6347.26 (+13.78) 🆕 New findings (41)
…and 21 more. ✅ Resolved (33)
…and 13 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
- 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.
|
@coderabbitai review |
✅ Action performedReview finished.
|
…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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/components/Home/PendingVerificationTasks.tsxsrc/components/Home/__tests__/PendingVerificationTasks.test.tsxsrc/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
…hape CodeRabbit: the JSDoc still described the pre-carousel comma-joined set string.
|
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
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): 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 Fix: include the advisory/blocking state (or the 2. A ToS step inside Bridge's hosted flow closes the iframe mid-verification
Failure case: the user clicks Accept on the embedded ToS page. The page posts Fix: in the hosted context, ignore Two smaller notes: the new |
…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.
|
Both addressed in 4a65c4c. 1 (dismissal survives escalation) — confirmed, with a note: the cited 2 (embedded ToS closes the hosted iframe) — confirmed structurally: |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
src/components/Home/PendingVerificationTasks.tsxsrc/components/Home/__tests__/PendingVerificationTasks.test.tsxsrc/utils/__tests__/bridge-tasks.utils.test.tssrc/utils/bridge-tasks.utils.tssrc/utils/general.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/utils/general.utils.ts
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.
There was a problem hiding this comment.
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 winClear dismissal state when
userIdchanges.dismissedKeysstays 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 tonullor key it byuserId, 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
📒 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.
/code-review (high) — verified findingsMulti-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. Correctness1. Hosted-flow ToS acceptance is never confirmed to the backend — 2. Dismissal fingerprint for blocking accept-tos tasks is constant over time — 3. Hidden-wrapper guard drops racing completion messages — Cleanup4. Duplicated date formatter — |
…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.
|
All four addressed in 4a613a0. 1 (hosted ToS never confirmed) — confirmed, fixed. 2 (constant blocking fingerprint) — confirmed, fixed one level up. There is nothing unique to fingerprint a NEW same-variant due-now ToS with ( 3 (!visible drops racing completion) — fixed via source-matching. The wrapper now reacts only to messages whose 4 (duplicated formatter) — fixed. |
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
nextActionsno 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 capabilitynextActions— 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 existingBridgeTosStep(link fetch → iframe →signedAgreementIdconfirm → 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 existingIframeWrapper(PersonacompletepostMessage 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.
IframeWrapperadditionally gained avisibleguard 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 branchpr-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
accept-tosfor 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. NoNEXT_PUBLIC_API_VERSIONbump (additive).main(per request) → back-merge debt main→dev after merge.IframeWrappervisible-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
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores