Skip to content

fix(kyc): stand the hosted task down beside a native step, and settle after the return (TASK-22818) [dev copy of #3277] - #3278

Merged
abalinda merged 4 commits into
devfrom
hotfix/22818-hosted-task-native-first-dev
Sep 20, 2026
Merged

abalinda merged 4 commits into
devfrom
hotfix/22818-hosted-task-native-first-dev

Conversation

@abalinda

@abalinda abalinda commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Dev-first. This is the live PR for this change. Its main twin, #3277, was closed in favour of it: the change ships with the next release train, not as a hotfix. Review history lives on #3277 (Chip: clean at 06a5571 with one advisory, fixed in the two commits after it; its automatic re-review of the final head failed on Chip's side; ten /code-review high findings applied). The four commits here are the same patches cherry-picked onto dev; the thirteen changed files are byte-identical to the closed PR's branch. Draft until a human opens it for review.

Summary

Client half of TASK-22818. A Bridge user who owed a proof-of-address document saw two tasks for one requirement, took the hosted one (Persona identity, which cannot collect the document), came back to an app that looked unchanged, and ran it again several times. The resolver fix is peanutprotocol/peanut-api-ts#1641; the poll expedite is peanutprotocol/peanut-api-ts#1642 (dev twins of the closed #1639 and #1640). This PR closes the client side of the loop, in three parts:

  • The hosted task stands down while a Bridge rail carries a native sumsub step (selectBridgeTasks, now rail-aware). The same rule the resolver applies after Peanut wallet #1641, so older API responses read the same way. Advisory (future-dated) hosted tasks stay. When the hosted task stood down for that reason, the prep screen says the document is uploaded in the app and points at the profile, instead of "nothing left to do".
  • A Bridge return opens one settle window per launch (useHostedVerification): ask the API to expedite the provider poll (POST /users/kyc/refresh, best effort), re-arm the shared user poller (markSubmitted: 30s of 4s refetches with its in-flight guard), refetch once, and repeat the nudge at 20s and 40s until the task clears or a minute passes. The provider's page never hands the user back (Bridge issues no redirect for most customers), the ~4s auto-refresh only runs while a rail is pending, and a webhook can arrive hours later or never. Same handling for every return signal: in-app browser close (iOS and Android), the programmatic close of the universal-link leg, tab focus on web, BFCache restore. A signal before any launch, inside an open window, or on the Rain portal return is one refetch, as before.
  • The prep screen says what is happening. While the window runs the CTA is held with "Checking with our payment partner…"; if the task is still pending when it ends, the screen says there is nothing more to do and that starting again creates a new check. Copy in en, es-419, pt-BR.

Not in this PR, deliberately: the tab-launch SecurityError on Chrome Mobile and Mobile Safari (Sentry PEANUT-UI-T07, T4W). Both engines allow the current order in a headless probe, so a reorder would be a guess; it needs a device-level look.

Review round 1 (/code-review high, 10 findings) reshaped the return leg: one window per launch instead of one per visibility flip, a wall-clock deadline instead of a round count, the shared poller instead of a second hand-rolled one, Bridge-only (the Rain portal return no longer runs a Bridge refresh), and the API is not asked again once it answers that there is nothing to expedite.

Task

TASK-22818: https://app.notion.com/p/3df8381175798102ac1de417db7f6629

Risks / breaking changes

  • Cross-repo: refreshKycState calls a route that lands with peanut-api-ts#1642. Until it deploys the call answers 404 and reads as "not expedited"; the window then only refetches. Safe in either deploy order.
  • Native: no layout, positioning, input or viewport change. Two new Notification rows and one alternate panel on the prep screen. Reaches installed apps through the normal OTA path.
  • Per return: at most 3 expedite calls (the route allows 15/min per IP) and one markSubmitted per nudge; the shared poller's refetches are the ones it already runs after any Sumsub submission.

QA

  • Unit: 108 tests across the six touched suites, 14 new (selector stand-down and its non-triggers, the exported predicate; card hides beside a native step; window opens once per launch and later signals only refetch; nudges at 20s and 40s; early stop when the task clears; still-pending state and reset on a new start; stop-on-negative; Rain return and pre-launch restore only refetch; the native-instead panel; the server action's happy path and both failure shapes). Full suite green locally.
  • Screenshots: ⚠️ the two new prep-screen states are transient (they exist only after a return from the vendor) and no fixture can stage a return; the native-instead panel and the Home change are a removal for the double-task cohort. Visual review needs a sandbox Bridge customer with a proof-of-address document due; the fixture-covered screens are unchanged and stay under ds-shots.

🤖 Generated with Claude Code

… after the return (TASK-22818)

A Bridge user who owed a proof-of-address document saw two tasks for one
requirement, took the hosted one (Persona identity, which cannot collect
the document), came back to an app that looked unchanged, and ran it
again several times.

The hosted task now stands down while a Bridge rail carries a native
sumsub step: the resolver stopped emitting the pair (peanut-api-ts#1639),
this keeps older responses honest on Home and on the prep screen. Coming
back opens a settle window: refetch at once, ask the API to re-read the
provider (peanut-api-ts#1640, best effort), keep asking every 5s for a
minute until the task clears. The vendor's page never hands the user
back and nothing else polls a requires-info rail, so a finished check
used to look like nothing had happened. The prep screen says what is
going on while the window runs, and says so when it ends with the task
still pending, instead of re-offering the same button in silence.
…the expedite answer

The API's refresh route now expedites the poller instead of reading Bridge
in the request path (peanut-api-ts#1640), so one call per return is enough
and three keep the row on the fresh cadence if the vendor is slow. The
refetch every 5s is unchanged and cheap. The route answers { expedited },
not { refreshed }.
…oad in the app" when the hosted task stands down

Review round 1 of the return leg: a window opened on every tab switch and
never closed for good, its deadline moved with slow requests, it ran a
second poller beside the shared one, the Rain portal return ran a Bridge
refresh, and a deep link to the prep screen read "nothing left to do" to
a user still blocked on a document.

A Bridge return now opens one window per launch: expedite the provider
poll, re-arm the shared user poller (markSubmitted), refetch once, and
nudge again at 20s and 40s until the task clears or a minute passes on a
wall clock. The API is not asked again once it answers that there is
nothing to expedite. Rain returns, pre-launch restores and signals inside
an open window are one refetch, as before. When the hosted task stood
down behind a Sumsub step, the prep screen points at the upload in the
profile instead of declaring the step done. The server action gets its
wire-level tests.
…en the window opens, name the reviewer branch

Review round 2 (Chip advisory + CodeRabbit): the "still pending" copy
promised minutes where product truth says a reviewer can take 1 to 3
business days; the launch flag was set before the handoff, so a failed
start still turned the next restore into a settle window; the window
never consumed the launch, so a signal after it closed opened another
one; and the task-pending ref was written during render.

The flag is now set only when the vendor page actually opened (or right
before the same-tab navigation whose return is a BFCache restore), the
window consumes it, and the ref moves into the effect. The copy names
both branches, as the prep copy two keys away already does.
@vercel

vercel Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 20, 2026 1:33pm UTC

Request Review

@notion-workspace

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: peanutprotocol/peanut-ui/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ba0f496a-dc61-4616-86cd-53d77d9b95eb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 8465.42 → 8469.79 (+4.37)
Findings: +1 net (+15 new, -14 resolved)

🆕 New findings (15)

  • critical complexity — src/app/actions/sumsub.ts — CC 59, MI 50.66, SLOC 261
  • high complexity — src/hooks/useHostedVerification.ts — CC 48, MI 62.06, SLOC 215
  • high complexity — src/components/Home/PendingVerificationTasks.tsx — CC 39, MI 62.05, SLOC 98
  • medium high-mdd — src/hooks/useHostedVerification.ts:53 — useHostedVerification: MDD 55.3 (uses across many lines from declarations)
  • medium high-mdd — src/components/Kyc/AdditionalVerificationView.tsx:50 — AdditionalVerificationView: MDD 38.1 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/useHostedVerification.ts:53 — useHostedVerification: DLT 33 (calls 33 distinct functions — high context load)
  • medium high-mdd — src/components/Home/PendingVerificationTasks.tsx:74 — PendingVerificationTasks: MDD 30.2 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useHostedVerification.ts:138 — : MDD 26.8 (uses across many lines from declarations)
  • medium complexity — src/components/Kyc/AdditionalVerificationView.tsx — CC 18, MI 57.17, SLOC 53
  • medium method-complexity — src/components/Kyc/AdditionalVerificationView.tsx:50 — CC 15 SLOC 46
  • low high-dlt — src/components/Home/PendingVerificationTasks.tsx:74 — PendingVerificationTasks: DLT 23 (calls 23 distinct functions — high context load)
  • low high-mdd — src/components/Home/PendingVerificationTasks.tsx:160 — : MDD 14.8 (uses across many lines from declarations)
  • low high-mdd — src/hooks/useHostedVerification.ts:252 — : MDD 12.8 (uses across many lines from declarations)
  • low structural-dup — app/actions/sumsub.ts:300 — 9 duplicate lines / 50 tokens with app/actions/sumsub.ts:357
  • low missing-return-type — src/components/Home/PendingVerificationTasks.tsx:74 — PendingVerificationTasks: exported fn missing return type annotation

✅ Resolved (14)

  • src/app/actions/sumsub.ts — CC 57, MI 50.49, SLOC 245
  • src/components/Home/PendingVerificationTasks.tsx — CC 39, MI 62.28, SLOC 97
  • src/hooks/useHostedVerification.ts — CC 30, MI 63.86, SLOC 134
  • src/hooks/useHostedVerification.ts:25 — useHostedVerification: MDD 38.4 (uses across many lines from declarations)
  • src/components/Kyc/AdditionalVerificationView.tsx:43 — AdditionalVerificationView: MDD 31.7 (uses across many lines from declarations)
  • src/components/Home/PendingVerificationTasks.tsx:72 — PendingVerificationTasks: MDD 30.2 (uses across many lines from declarations)
  • src/hooks/useHostedVerification.ts:38 — : MDD 26.0 (uses across many lines from declarations)
  • src/components/Kyc/AdditionalVerificationView.tsx — CC 9, MI 61.25, SLOC 43
  • src/components/Home/PendingVerificationTasks.tsx:72 — PendingVerificationTasks: DLT 23 (calls 23 distinct functions — high context load)
  • src/hooks/useHostedVerification.ts:25 — useHostedVerification: DLT 22 (calls 22 distinct functions — high context load)
  • src/components/Home/PendingVerificationTasks.tsx:158 — : MDD 14.8 (uses across many lines from declarations)
  • src/hooks/useHostedVerification.ts:144 — : MDD 12.8 (uses across many lines from declarations)
  • app/actions/sumsub.ts:300 — 9 duplicate lines / 50 tokens with app/actions/sumsub.ts:336
  • src/components/Home/PendingVerificationTasks.tsx:72 — PendingVerificationTasks: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useHostedVerification.ts 6.3 7.7 +1.4
src/utils/bridge-tasks.utils.ts 2.3 3.7 +1.4
src/components/Kyc/AdditionalVerificationView.tsx 6.3 7.6 +1.3

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 8377 ran, 0 failed, 0 skipped, 3.2m

📊 Coverage (unit)

metric %
statements 80.4%
branches 68.5%
functions 75.1%
lines 81.6%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
3.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 2 screens moved

3 of 144 shots changed · 141 identical · baseline 12bd900 → head 8d2367d

worst % screen widths
69.67% early-user 430
43.59% avatar-picker 320, 430

job summary · before/after/diff images — artifact

Fixture screenshots, no backend. Advisory — this check never blocks a merge. Posted from the default branch by ds-shots-comment.yml; the report it renders is untrusted data.

@abalinda
abalinda marked this pull request as ready for review September 20, 2026 13:59
Copilot AI lite review requested due to automatic review settings September 20, 2026 13:59
@abalinda
abalinda merged commit 852cb57 into dev Sep 20, 2026
33 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

English · Español · Español (Argentina) · Português (Brasil)

Open screen library dashboard

After merge: 12bd900 → 852cb57. Capture complete in all locales.

This branch was successfully deployed

1 active deployment
Preview — 8d2367de Deployed Sep 20, 2026 by vercel[bot]
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