Skip to content

fix(kyc): launch Sumsub SDK when the modal portal mounts the container late#2440

Merged
Hugo0 merged 2 commits into
mainfrom
hotfix/sumsub-sdk-never-completes
Jul 17, 2026
Merged

fix(kyc): launch Sumsub SDK when the modal portal mounts the container late#2440
Hugo0 merged 2 commits into
mainfrom
hotfix/sumsub-sdk-never-completes

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

P0 — card creation has been at zero for 15h+. The Sumsub WebSDK opens but nobody can complete verification, so no one gets a card.

The numbers (PostHog, prod):

event 07-14 07-15 07-16 07-17
card_sumsub_opened 198 129 188 137 ← normal
card_sumsub_completed 85 59 49 0
kyc_submitted 76 40 27 0
kyc_approved 42 17 16 0
card_sumsub_closed 58 29 46 51 ← users giving up

Users sit on a spinner and walk away. Downstream this is a full card outage: no new verified users, and the 334 already-approved-but-cardless users get routed back through the same SDK for Rain's extra docs. card_apply terms-required collapsed from 27–42% of outcomes to 1.4%, and 0 cards were created in 15h+ (baseline 24–43/day). Last card: 2026-07-16 21:16.

Corroborated by a user report: "When I try to set up the new card, it just spins, nothing appears on the display."

Root cause

#2407 replaced the StartVerificationView click gate with auto-init on visible. The init effect bails on !sdkContainerRef.current — but Modal is a headlessui <Transition>/<Dialog> that renders through a Portal, and the portal target is created in the portal's own effect. So the container mounts a commit after visible flips true.

A ref is not reactive and was not in the dep array, so the effect read null on its only run and never re-ran — the SDK was never launched. The old click gate hid this by guaranteeing the container was mounted long before init.

It only reproduces when the wrapper is already mounted (the real SumsubKycModals case) so sdkLoaded has settled before visible flips. Mounting straight to visible lets the sdkLoaded flip re-run the effect and masks the bug — which is why it escaped review.

Fix

Hold the container in state via a callback ref, so attachment re-runs the init effect. Keeps #2407's intended no-interstitial UX rather than reverting it.

I built a surgical revert first and threw it away once the mechanism was proven — the forward fix keeps the UX win and is provable.

Risks / breaking changes

  • Blast radius: the Sumsub modal only. 1 source file, +1 test. No API/contract change, no cross-repo deploy order.
  • Every SumsubKycModals surface benefits (card apply, Manteca add-money, claim/bank flows, profile regions) — all were equally broken.
  • Targets main (prod hotfix) → back-merge debt main→dev.

QA

  • New suite SumsubKycWrapper.test.tsx reproduces the portal's late mount. Fails on current prod code (launch called 0 times), passes with the fix. Also asserts it does not launch while hidden or without a token.
  • Full local gate: typecheck clean, prettier clean, 3/3 new tests. Full suite: 4 pre-existing failures identical on clean origin/main (content-submodule/worktree env), none from this change.

Design notes / accepted trade-offs

…r late

Card creation has been at zero since the #2407 release went live: the Sumsub
WebSDK opens but nobody can finish. card_sumsub_opened is normal (137/day)
while card_sumsub_completed went 49-85/day -> 0, kyc_submitted 27-76 -> 0 and
kyc_approved 16-42 -> 0. Users sit on a spinner and give up (51 closes, zero
completions today). Downstream that is a full card outage — no new verified
users, and the 334 approved-but-cardless users are routed back through the
same SDK for Rain's extra docs, so card_apply terms-required collapsed from
27-42% of outcomes to 1.4% and 0 cards were created in 15h+ (baseline 24-43).

Root cause: #2407 replaced the StartVerificationView click gate with auto-init
on `visible`. The init effect bails on `!sdkContainerRef.current`, but Modal is
a headlessui <Transition>/<Dialog> that renders through a Portal — the portal
target is created in the portal's own effect, so the container mounts a commit
AFTER `visible` flips true. A ref is not reactive and was not in the dep array,
so the effect read null on its only run and never re-ran: the SDK was never
launched. The old click gate hid this by guaranteeing the container was mounted
long before init. It only reproduces when the wrapper is already mounted (the
real SumsubKycModals case) so `sdkLoaded` has settled before `visible` flips —
mounting straight to visible lets the sdkLoaded flip re-run the effect.

Fix: hold the container in state via a callback ref so attachment re-runs the
init effect. Keeps #2407's intended no-interstitial UX rather than reverting.

Test reproduces the portal's late mount and fails on the current prod code
(launch called 0 times), passes with the fix.
@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 17, 2026 12:57pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35b72f17-9bd8-40a5-a3cc-85c1f32d9ba5

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hotfix/sumsub-sdk-never-completes

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

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6206.95 → 6206.95 (0)
Findings: 0 net (+0 new, -0 resolved)

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — 🔴 1 failing

Suites

  • 🔴 unit: 1995 ran, 1 failed, 0 skipped, 33.1s

🔴 Failing tests

  • src/components/Kyc/__tests__/SumsubKycWrapper.test.tsx › SumsubKycWrapper › launches the SDK when an already-mounted wrapper is opened (portal mounts container late) — 1.05s

📊 Coverage (unit)

metric %
statements 59.4%
branches 43.2%
functions 48.3%
lines 59.6%
⏱ 10 slowest test cases
time test
3.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
1.0s src/components/Kyc/__tests__/SumsubKycWrapper.test.tsx › launches the SDK when an already-mounted wrapper is opened (portal mounts container late)
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
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/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.1s src/utils/__tests__/demo-balance.test.ts › auto-refills a stored balance that has no timestamp (legacy install)
0.1s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

Review pass on my own test: replace a tautological waitFor that did not
actually guarantee sdkLoaded had settled (the premise the repro depends on),
reset the mock's mounted flag so a close/re-open replays the late mount like
the real portal, name the mock component so rules-of-hooks recognises it
(it was adding 2 eslint errors), and stop leaking window.snsWebSdk.
@Hugo0
Hugo0 marked this pull request as ready for review July 17, 2026 12:52
@Hugo0
Hugo0 merged commit e22e51e into main Jul 17, 2026
16 of 21 checks passed
Hugo0 added a commit that referenced this pull request Jul 17, 2026
…llback-ref

fix(kyc): actually apply the callback-ref fix (#2440 shipped the test only)
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.

1 participant