Skip to content

fix(kyc): resume Sumsub SDK after PWA reload (TASK-20396)#2316

Closed
kushagrasarathe wants to merge 5 commits into
mainfrom
fix/sumsub-pwa-reload-resume
Closed

fix(kyc): resume Sumsub SDK after PWA reload (TASK-20396)#2316
kushagrasarathe wants to merge 5 commits into
mainfrom
fix/sumsub-pwa-reload-resume

Conversation

@kushagrasarathe

@kushagrasarathe kushagrasarathe commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem — TASK-20396

Users report that mid-KYC, if they tab out of the PWA (to camera, gallery, or another app) and come back, they're dropped out of Sumsub and lose their progress. Reproduced on Android.

Root cause

The app ships as a standalone PWA (display: standalone). Android aggressively evicts backgrounded standalone PWAs from memory — and opening the native camera/gallery (which the KYC flow pushes users toward) is a strong trigger. On return the page cold-reloads; there's no bfcache to restore.

The "SDK is open" state is useState in both KYC integrations, so a cold reload resets it and the SDK vanishes:

  • /card (Rain card): gated by sumsubToken !== null.
  • shared flow (add-money / withdraw / qr-pay / KYC drawer): gated by showWrapper in useSumsubKycFlow.

Fix (approach A — URL-persisted resume)

Mirror the SDK-open state to the URL (?kyc=true) via nuqs. An app-switcher relaunch restores the last route + query, so the flag survives. On mount, if set, re-acquire a fresh token for the same in-progress applicant and reopen the SDK.

  • /card — re-applyForCard({ termsAccepted: false }) (the same idempotent call the token-refresh path uses).
  • shared flow — re-handleInitiateKyc() in useMultiPhaseKycFlow, with autoStart so it lands straight in the SDK (user already consented) instead of the intro screen.

A sync effect keeps the URL flag in step with the SDK's open state (skipping the first run so the mount-time resume reads the persisted flag first).

⚠️ This is a PARTIAL fix — read before closing the ticket

What this fixes: the SDK reopening. After the reload, the user is taken straight back into Sumsub on the same applicant, with all previously-submitted steps preserved — instead of being dumped on the underlying page and having to navigate back and restart the flow.

What this does NOT fix: the data the user was actively typing on the current step (e.g. the personal-info / tax-ID form) is still lost. If they had filled fields but not yet advanced to the next step, those fields come back blank.

Why we can't fix that part:

  • Sumsub's own docs confirm the SDK autosaves in-progress applicant-data / questionnaire input to the browser's sessionStorage, scoped to the verification session (verification-steps docs): "Sumsub automatically saves the information an applicant enters ... in the browser session storage ... If the session expires before the applicant finishes entering their information, they will need to re-enter their data when they return."
  • sessionStorage survives an in-tab reload but is destroyed when the browsing context is torn down — which is exactly what Android does when it kills the backgrounded PWA. That's why submitted steps survive (they're on Sumsub's server) but the mid-typed step does not.
  • There is no Sumsub config flag / init option / dashboard setting to switch this to localStorage or make the draft durable (confirmed across WebSDK settings + get-started + about docs).
  • We cannot recover it ourselves either: the Sumsub form runs in a cross-origin iframe (static.sumsub.com), so we can't read its sessionStorage, and the WebSDK only emits step-level events (onStepCompleted, onApplicantSubmitted) — there's no field-level/keystroke event to capture and re-push.
  • We also can't reliably stop Android from killing a backgrounded standalone PWA, so preventing the reload isn't an option.

The only way to fully preserve typed data would be an architectural change: collect the personal-info / tax-ID fields in our own PWA form (persisted in our storage, which survives the kill), push them to Sumsub via the fixedInfo + /questionnaires API before the SDK opens, and enable prefill on the Sumsub level (applicantDataSettings.prefill). That's a separate, larger project touching FE + BE + Sumsub level config — tracked separately, not in this PR.

Known limitation (summary)

Recovers the bounce-out + manual re-entry, not the fields on the step the user was actively typing. See the section above for why.

Testing

  • npm run typecheck clean, npm run build passes, prettier clean.
  • jest card 137/137; useSumsubKycFlow / add-money-states / qr-pay-states suites pass (76).
  • Manual (preview): start identity verification on Android PWA → tab to camera/gallery → return → SDK should reopen into the resumed applicant instead of the underlying page.

Files

  • src/app/(mobile-ui)/card/page.tsx
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/components/Kyc/SumsubKycModals.tsx

Android evicts backgrounded standalone PWAs from memory — opening the
camera/gallery mid-KYC (the exact thing the flow pushes users toward) is
the common trigger. On return the page cold-reloads, sumsubToken (useState)
resets to null, and the user is dropped out of Sumsub back to the card
screen, losing the perceived progress. TASK-20396.

Mirror the SDK-open state to the URL (?kyc=true), which survives the
app-switcher relaunch. On mount, if the flag is set, re-apply for the card
to mint a fresh token for the same in-progress applicant and reopen the SDK
where they left off — the same idempotent applyForCard call the token
refresh path already uses.

Note: this cannot recover the fields on the step the user was actively
typing (that lives in Sumsub's iframe, gone with the process) — only the
bounce-out to the card screen and the manual re-entry.
@vercel

vercel Bot commented Jul 1, 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 1, 2026 8:41am

Request Review

@notion-workspace

Copy link
Copy Markdown

@coderabbitai

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

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: 8397d76a-2cfd-4556-ae99-bbc654de1e7c

📥 Commits

Reviewing files that changed from the base of the PR and between a82a079 and 836c3a2.

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

Walkthrough

The KYC flow now returns explicit initiation outcomes, adds a reload-resume hook keyed by the kyc URL flag, and wires resume-aware auto-start state through the multi-phase hook, card page, and Sumsub modal wrapper.

Changes

Sumsub resume and initiation flow

Layer / File(s) Summary
Initiation outcome booleans
src/hooks/useSumsubKycFlow.ts
handleInitiateKyc now returns explicit boolean results across terminal error paths, native execution, and successful web-wrapper startup.
Reload resume hook
src/hooks/useSumsubReloadResume.ts
useSumsubReloadResume reads and synchronizes the kyc query flag, runs a one-time resume attempt on mount, and clears the flag when reopening does not succeed.
Resume-aware wiring
src/hooks/useMultiPhaseKycFlow.ts, src/app/(mobile-ui)/card/page.tsx, src/components/Kyc/SumsubKycModals.tsx
useMultiPhaseKycFlow exposes sdkAutoStart and wires the reload-resume callback, card/page.tsx reopens Sumsub with a fresh token on reload, and SumsubKycModals selects the wrapper auto-start value from the new state.

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

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: jjramirezn

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: resuming Sumsub KYC after a PWA reload.
Description check ✅ Passed The description matches the changes and explains the PWA reload-resume Sumsub KYC fix.
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

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

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5853.13 → 5860.23 (+7.1)
Findings: +2 net (+21 new, -19 resolved)

🆕 New findings (21)

  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 99, MI 57.29, SLOC 387
  • critical complexity — src/hooks/useSumsubKycFlow.ts — CC 79, MI 52.38, SLOC 353
  • critical complexity — src/hooks/useMultiPhaseKycFlow.ts — CC 71, MI 57.56, SLOC 348
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 42 commits, +1142/-521 lines since 6 months ago
  • high hotspot — src/hooks/useSumsubKycFlow.ts — 41 commits, +595/-82 lines since 6 months ago
  • medium high-mdd — src/hooks/useSumsubKycFlow.ts:21 — useSumsubKycFlow: MDD 171.9 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useMultiPhaseKycFlow.ts:99 — useMultiPhaseKycFlow: MDD 118.6 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/card/page.tsx:53 — CardPage: MDD 107.3 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/card/page.tsx:53 — CardPage: DLT 49 (calls 49 distinct functions — high context load)
  • medium high-dlt — src/hooks/useMultiPhaseKycFlow.ts:99 — useMultiPhaseKycFlow: DLT 40 (calls 40 distinct functions — high context load)
  • medium high-mdd — src/hooks/useSumsubKycFlow.ts:147 — : MDD 37.0 (uses across many lines from declarations)
  • medium hotspot — src/hooks/useMultiPhaseKycFlow.ts — 27 commits, +613/-149 lines since 6 months ago
  • medium method-complexity — src/hooks/useSumsubKycFlow.ts:147 — CC 21 SLOC 95
  • medium method-complexity — src/app/(mobile-ui)/card/page.tsx:451 — CC 15 SLOC 47
  • medium nextjs-missing-use-client — src/hooks/useSumsubReloadResume.ts:1 — Hooks used without use client directive
  • low high-mdd — src/app/(mobile-ui)/card/page.tsx:320 — : MDD 15.8 (uses across many lines from declarations)
  • low high-mdd — src/app/(mobile-ui)/card/page.tsx:451 — renderState: MDD 14.4 (uses across many lines from declarations)
  • low high-mdd — src/hooks/useMultiPhaseKycFlow.ts:270 — : MDD 10.0 (uses across many lines from declarations)
  • low missing-return-type — src/hooks/useMultiPhaseKycFlow.ts:51 — confirmBridgeTosAndAwaitRails: exported fn missing return type annotation
  • low missing-return-type — src/hooks/useMultiPhaseKycFlow.ts:99 — useMultiPhaseKycFlow: exported fn missing return type annotation

…and 1 more.

✅ Resolved (19)

  • src/app/(mobile-ui)/card/page.tsx — CC 95, MI 57.39, SLOC 376
  • src/hooks/useSumsubKycFlow.ts — CC 79, MI 52.54, SLOC 349
  • src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.68, SLOC 325
  • src/app/(mobile-ui)/card/page.tsx — 40 commits, +1082/-477 lines since 6 months ago
  • src/hooks/useSumsubKycFlow.ts — 40 commits, +583/-76 lines since 6 months ago
  • src/hooks/useSumsubKycFlow.ts:21 — useSumsubKycFlow: MDD 169.6 (uses across many lines from declarations)
  • src/hooks/useMultiPhaseKycFlow.ts:98 — useMultiPhaseKycFlow: MDD 116.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:52 — CardPage: MDD 101.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:52 — CardPage: DLT 48 (calls 48 distinct functions — high context load)
  • src/hooks/useMultiPhaseKycFlow.ts:98 — useMultiPhaseKycFlow: DLT 37 (calls 37 distinct functions — high context load)
  • src/hooks/useSumsubKycFlow.ts:147 — : MDD 36.7 (uses across many lines from declarations)
  • src/hooks/useMultiPhaseKycFlow.ts — 23 commits, +535/-109 lines since 6 months ago
  • src/hooks/useSumsubKycFlow.ts:147 — CC 21 SLOC 91
  • src/app/(mobile-ui)/card/page.tsx:435 — CC 15 SLOC 47
  • src/app/(mobile-ui)/card/page.tsx:319 — : MDD 15.8 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:435 — renderState: MDD 14.4 (uses across many lines from declarations)
  • src/hooks/useMultiPhaseKycFlow.ts:264 — : MDD 10.0 (uses across many lines from declarations)
  • src/hooks/useMultiPhaseKycFlow.ts:50 — confirmBridgeTosAndAwaitRails: exported fn missing return type annotation
  • src/hooks/useMultiPhaseKycFlow.ts:98 — useMultiPhaseKycFlow: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useSumsubReloadResume.ts 0.0 5.4 +5.4
src/hooks/useMultiPhaseKycFlow.ts 12.6 13.4 +0.8

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1636 ran, 0 failed, 0 skipped, 25.4s

📊 Coverage (unit)

metric %
statements 54.6%
branches 37.3%
functions 42.4%
lines 54.5%
⏱ 10 slowest test cases
time test
3.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
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 updateUserById
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
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 too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces 2
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

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

🤖 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/app/`(mobile-ui)/card/page.tsx:
- Around line 418-421: The one-shot KYC resume effect in useEffect is marking
didAttemptKycResumeRef.current too early, before the card gate/loading checks
have settled. Move the ref assignment to after the loading guards for
cardInfo/overview and the existing kycInProgress/sumsubToken checks so resume is
only consumed when prerequisites are ready, and update the effect’s dependency
list to include every value it reads instead of suppressing exhaustive-deps.
- Around line 433-435: The KYC resume flow in the catch block is swallowing
transient failures and immediately clearing the in-progress state, which removes
the retry path. Update the resume logic around the catch in the card page so
failures during reload do not silently reset the KYC flag; instead, preserve the
existing resume state or route the error into applyError so the user can retry.
Use the surrounding KYC resume handler and setKycInProgress/applyError flow to
keep the ?kyc=true state intact on recoverable errors.
🪄 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: 4fad7228-224c-40b9-9129-273f2c1b651b

📥 Commits

Reviewing files that changed from the base of the PR and between 9df5c58 and 1d3ccd3.

📒 Files selected for processing (1)
  • src/app/(mobile-ui)/card/page.tsx

Comment thread src/app/(mobile-ui)/card/page.tsx Outdated
Comment thread src/app/(mobile-ui)/card/page.tsx Outdated
Comment on lines +433 to +435
} catch {
void setKycInProgress(false)
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Don’t silently discard the resume state on transient failure.

A network/auth hiccup during reload currently clears ?kyc=true and gives no user-facing error, so the retry path is lost. Preserve the flag or surface applyError so the user can retry the resume.

Suggested direction
-            } catch {
-                void setKycInProgress(false)
+            } catch (e) {
+                const message = e instanceof Error ? e.message : 'Failed to resume verification'
+                console.error('[card apply] kyc resume error:', e)
+                setApplyError(message)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
void setKycInProgress(false)
}
} catch (e) {
const message = e instanceof Error ? e.message : 'Failed to resume verification'
console.error('[card apply] kyc resume error:', e)
setApplyError(message)
}
🤖 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/app/`(mobile-ui)/card/page.tsx around lines 433 - 435, The KYC resume
flow in the catch block is swallowing transient failures and immediately
clearing the in-progress state, which removes the retry path. Update the resume
logic around the catch in the card page so failures during reload do not
silently reset the KYC flag; instead, preserve the existing resume state or
route the error into applyError so the user can retry. Use the surrounding KYC
resume handler and setKycInProgress/applyError flow to keep the ?kyc=true state
intact on recoverable errors.

Same PWA-eviction bug as the /card fix, now for the shared multi-phase
identity flow (add-money / withdraw / qr-pay / KYC drawer). A backgrounded
standalone PWA gets killed by Android; on return the page cold-reloads,
`showWrapper` (useState) resets to false, and the SDK is gone.

Mirror the SDK-open state to the URL (?kyc=true) via useMultiPhaseKycFlow.
On mount, if set, re-initiate to mint a fresh token for the existing
applicant and reopen the SDK — with autoStart so it lands straight in the
SDK (user already consented) rather than the intro screen. TASK-20396.
@kushagrasarathe kushagrasarathe changed the title fix(card): resume Sumsub SDK after PWA reload (TASK-20396) fix(kyc): resume Sumsub SDK after PWA reload (TASK-20396) Jul 1, 2026
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 1, 2026

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useMultiPhaseKycFlow.ts (1)

51-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return failure when Bridge ToS is still required.

Line 55 ignores the second confirmation result, and Lines 66-74 fall through even if stillNeedsTos remains true after all retries. The caller then runs completeFlow(), which can close the KYC flow while backend capabilities still require ToS.

🐛 Proposed fix
 export async function confirmBridgeTosAndAwaitRails(fetchUser: () => Promise<IUserProfile | null>) {
-    const result = await confirmBridgeTos()
+    let result = await confirmBridgeTos()
     if (!result.data?.accepted) {
         await new Promise((resolve) => setTimeout(resolve, 2000))
-        await confirmBridgeTos()
+        result = await confirmBridgeTos()
+        if (!result.data?.accepted) {
+            throw new Error('Bridge ToS was not accepted')
+        }
     }
 
@@
     for (let i = 0; i < 3; i++) {
         const updatedUser = await fetchUser()
@@
         const stillNeedsTos = deriveCapabilityPhaseSignals(updatedUser?.capabilities).needsTos
         if (!stillNeedsTos) break
+        if (i === 2) {
+            throw new Error('Bridge ToS acceptance did not propagate')
+        }
         if (i < 2) await new Promise((resolve) => setTimeout(resolve, 2000))
     }
 }
🤖 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/useMultiPhaseKycFlow.ts` around lines 51 - 75, The
confirmBridgeTosAndAwaitRails flow can still succeed even when Bridge ToS
remains required, so update this function to surface failure instead of falling
through. In confirmBridgeTosAndAwaitRails, check the result of the second
confirmBridgeTos() call and, if it is still not accepted, stop and return
failure or throw. Also, after the retry loop using
deriveCapabilityPhaseSignals(updatedUser?.capabilities).needsTos, explicitly
fail when stillNeedsTos remains true after all attempts so callers like
completeFlow cannot proceed while ToS is still pending.
🤖 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/useMultiPhaseKycFlow.ts`:
- Around line 298-323: The resume path in useMultiPhaseKycFlow can leave the kyc
URL flag stuck when handleInitiateKyc() resolves without reopening Sumsub
because the showWrapper-driven sync effect never runs again after its initial
skip. Update the resume logic so the URL flag is explicitly cleared when the
flow is resolved in-place, using the existing useEffect blocks,
didAttemptResumeRef, kycSyncSkipRef, and setKycInProgress to ensure ?kyc=false
is written even when showWrapper stays false.
- Around line 296-309: The resume path in useMultiPhaseKycFlow leaves
sdkAutoStart stuck on true after calling handleInitiateKyc, which can affect
later manual launches. Reset sdkAutoStart once the resume-triggered SDK reopen
has been handled, or separate the resume flow from the normal initiate flow so
the auto-start flag is only preserved for the intended path. Use the useEffect
that checks didAttemptResumeRef, the setSdkAutoStart state setter, and
handleInitiateKyc to place the reset in the right resume-specific branch.

---

Outside diff comments:
In `@src/hooks/useMultiPhaseKycFlow.ts`:
- Around line 51-75: The confirmBridgeTosAndAwaitRails flow can still succeed
even when Bridge ToS remains required, so update this function to surface
failure instead of falling through. In confirmBridgeTosAndAwaitRails, check the
result of the second confirmBridgeTos() call and, if it is still not accepted,
stop and return failure or throw. Also, after the retry loop using
deriveCapabilityPhaseSignals(updatedUser?.capabilities).needsTos, explicitly
fail when stillNeedsTos remains true after all attempts so callers like
completeFlow cannot proceed while ToS is still pending.
🪄 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: 6c910dd0-3644-422d-bed1-f3e16e3b53cb

📥 Commits

Reviewing files that changed from the base of the PR and between 1d3ccd3 and 9ce6923.

📒 Files selected for processing (2)
  • src/components/Kyc/SumsubKycModals.tsx
  • src/hooks/useMultiPhaseKycFlow.ts

Comment thread src/hooks/useMultiPhaseKycFlow.ts Outdated
Comment thread src/hooks/useMultiPhaseKycFlow.ts Outdated
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -t f "page.tsx" src/app/\(mobile-ui\)/card || true
echo "---"
cat -n "src/app/(mobile-ui)/card/page.tsx"
echo "=== SumsubKycModals.tsx ==="
cat -n "src/components/Kyc/SumsubKycModals.tsx"
echo "=== useMultiPhaseKycFlow.ts ==="
cat -n "src/hooks/useMultiPhaseKycFlow.ts"

Length of output: 62793


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== usages of autoStartSdk ==="
rg -n "autoStartSdk" -C3 --type=ts --type=tsx 2>/dev/null || rg -n "autoStartSdk" -C3
echo "=== useSumsubKycFlow.ts handleInitiateKyc & catch handling ==="
fd -t f "useSumsubKycFlow.ts" src
cat -n src/hooks/useSumsubKycFlow.ts 2>/dev/null | sed -n '1,400p'

Length of output: 663


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== usages of autoStartSdk ==="
rg -n "autoStartSdk" -C3 -g '*.ts' -g '*.tsx' || true
echo "=== find useSumsubKycFlow.ts ==="
fd -t f "useSumsubKycFlow.ts"
echo "=== cat useSumsubKycFlow.ts ==="
fd -t f "useSumsubKycFlow.ts" -x cat -n {}

Length of output: 443


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== repo root check ==="
ls
echo "=== search for SumsubKycModals usage ==="
rg -n "SumsubKycModals" -C2
echo "=== search useSumsubKycFlow ==="
rg -n "useSumsubKycFlow" -l
echo "=== fd for kyc flow hooks ==="
fd -i "sumsub" -t f

Length of output: 1148


🏁 Script executed:

#!/bin/bash
set +e
echo "=== search for SumsubKycModals usage ==="
rg -n "SumsubKycModals" -C2 src
echo "exit: $?"
echo "=== search useSumsubKycFlow ==="
rg -n "useSumsubKycFlow" src -l
echo "exit: $?"
echo "=== fd for kyc flow hooks ==="
fd -i "sumsub" -t f src

Length of output: 16776


🏁 Script executed:

#!/bin/bash
set -e
cat -n src/hooks/useSumsubKycFlow.ts

Length of output: 26463


🏁 Script executed:

#!/bin/bash
set -e
ast-grep run --pattern 'autoStart' --lang tsx src/components/Kyc/SumsubKycWrapper.tsx | head -50
echo "---"
rg -n "autoStart" -C3 src/components/Kyc/SumsubKycWrapper.tsx

Length of output: 2103


@kushagrasarathe Here's the in-depth review.

Risk of new bugs / breaking existing functionality

  1. Scope mismatch vs. PR description — The PR summary says "the shared useMultiPhaseKycFlow path is left for a follow-up," but the diff (per the AI summary) already modifies src/hooks/useMultiPhaseKycFlow.ts and src/components/Kyc/SumsubKycModals.tsx with the same resume pattern. Worth confirming whether that's intentional (matches the commit message, which does describe the shared-flow fix) or stale PR copy — it changes what needs testing.

  2. Resume path loses context for non-handleInitiateKyc flows (in useMultiPhaseKycFlow.ts) — On mount, if kyc=true and showWrapper is false, the resume effect always calls the generic handleInitiateKyc() with no args. But showWrapper is also flipped true by handleStartAction, handleSelfHealResubmit, and handleRestartIdentity — none of which are reproduced by a bare handleInitiateKyc() call. If a PWA reload happens mid self-heal resubmission, mid advisory "start action," or mid cross-region/targetCountry-specific initiate, resuming will silently take the wrong path (standard KYC init) instead of continuing the actual in-progress flow. This is a real functional gap for add-money, withdraw, qr-pay, and the KYC drawer.

  3. No error-path cleanup of the kyc flag in useMultiPhaseKycFlow.tscard/page.tsx's resume effect explicitly calls setKycInProgress(false) on both the non-Sumsub-response branch and the catch. The hook's resume effect has no equivalent: if handleInitiateKyc() fails (surfaces via setError, showWrapper stays false), the sync effect (keyed only on showWrapper) never re-fires, so kyc=true stays stuck in the URL and every future reload retries the initiate call indefinitely.

  4. SumsubKycModals.tsx: autoStart={autoStartSdk || flow.sdkAutoStart} — using || means an explicit autoStartSdk={false} from a future caller would be silently overridden by flow.sdkAutoStart. No current caller passes false (verified via rg), so it's latent, but ?? is the correct/safer operator here.

  5. Ordering-dependent "skip first run" contract — Both implementations rely on the resume effect being declared (and thus run) before the sync effect so kycSyncSkipRef/didAttemptResumeRef interplay works. It's correct today, but nothing enforces that order if the effects get reshuffled later — a small structural risk to keep in mind.

Performance / latency

Negligible. The resume path reuses the same idempotent applyForCard/initiateSumsubKyc calls the app already makes elsewhere (token refresh, post-poll), so there's no new network pattern — just one extra round trip on a cold PWA reload, which is the intended trade-off. nuqs's URL sync is a cheap history.replaceState, not a navigation. The one redundant setKycInProgress(true) after a successful resume (sync effect re-firing because sumsubToken/showWrapper changed) is a harmless extra state write.

Code quality / DRY

The "mirror SDK-open state to URL + resume on mount" pattern (didAttemptResumeRef / useQueryState('kyc', ...) / kycSyncSkipRef + two effects) is duplicated nearly verbatim between card/page.tsx and useMultiPhaseKycFlow.ts — and it's already diverged once (the error-cleanup gap above). Recommend extracting it into a small shared hook, e.g.:

function useKycUrlResumeFlag(isOpen: boolean, onResume: () => void | Promise<void>)

that owns the kyc query param, the skip-first-run ref, and a consistent error/cleanup contract — both call sites would then just supply isOpen and their resume callback.

Testing guide for QA

  1. On Android Chrome, install the PWA (Add to Home Screen) and open /card.
  2. Start "Get your card," reach the Sumsub SDK, begin verification.
  3. Mid-verification, open the camera/gallery (or switch apps long enough to get evicted) and return — confirm the app resumes directly into Sumsub with a fresh token, not dropped back to the card screen.
  4. Complete verification after resuming — confirm it finishes normally (polling → next screen).
  5. Manually close Sumsub (X button) — confirm the kyc URL param clears and a later reload does not auto-resume.
  6. Repeat steps 2–4 on add-money/[country]/bank, withdraw/[country]/bank, qr-pay, and the KYC status drawer — pay extra attention to self-heal resubmission, advisory "start action," and cross-region LATAM flows, since these may not resume correctly per the risk noted above.
  7. Sanity-check desktop/iOS (no eviction issue) — the kyc param should be inert there, no regressions to normal flow or back/deep-link navigation with card_state/kyc both in the URL.
  8. Force an apply/initiate failure while kyc=true (e.g., airplane mode) and reload again — confirm it doesn't get stuck endlessly retrying.
✅ 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.

Addresses CodeRabbit feedback on the PWA-reload resume:

- Extract the duplicated "mirror SDK-open to URL + resume on mount" logic
  (card page + useMultiPhaseKycFlow) into a single useSumsubReloadResume
  hook. One place owns the query param, the skip-first-run ordering, and
  the error/cleanup contract.
- handleInitiateKyc now reports whether the SDK actually opened. The hook's
  onResume returns that boolean; a resume that doesn't reopen (init error,
  or a remediation flow like self-heal / start-action / restart-identity
  that a bare initiate can't reconstruct) clears the ?kyc flag instead of
  falling back to the standard level or leaving the flag stuck to retry on
  every future reload.
- SumsubKycModals: autoStart uses ?? not || so an explicit autoStartSdk=false
  isn't overridden by the flow flag.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Addressed the review in cdb191b:

  • feat: bigger peanutman and goerli/optigoerli balance #5 duplication + ordering — extracted the shared useSumsubReloadResume(isOpen, onResume) hook (src/hooks/useSumsubReloadResume.ts). Both call sites (card page + useMultiPhaseKycFlow) now supply just isOpen + a resume callback; the hook owns the ?kyc query param, the skip-first-run ordering, and the error/cleanup contract in one place.

  • Fetch Chain details from SDK #2 wrong-path resume + hotfix: sdk version update #3 stuck flag — fixed with the same lever: handleInitiateKyc (and the useSumsubKycFlow implementation) now returns whether the SDK actually opened. The hook's onResume returns that boolean, and a resume that doesn't reopen clears the flag. So a reload mid self-heal / start-action / restart-identity no longer silently reopens the standard level — for those (base-approved) users handleInitiateKyc resolves without a token → returns false → flag cleared, SDK not reopened (graceful fallback to today's manual re-entry, not a regression). Same path fixes the stuck-flag-on-init-error case.

  • Claim #4 ||??SumsubKycModals now uses autoStart={autoStartSdk ?? flow.sdkAutoStart} so an explicit false from a caller wins.

  • added wagmi-->ethers signer conversion for sdk compatibility #1 scope — the PR description was updated earlier; the shared-flow changes are intentional (they match the commit).

Remaining known edge (documented, accepted): restart-identity for a Manteca wrong-country user could resume into the standard identity level rather than the exact restart level — narrow remediation case, and re-doing identity is close to intended. Not worth per-flow arg persistence here.

Verified: typecheck clean, npm run build passes, prettier clean, jest 213 passed across the KYC/card suites.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useMultiPhaseKycFlow.ts (1)

51-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail the ToS flow when confirmation never propagates.

Line 72 breaks only when needsTos clears, but Lines 66-74 still resolve after all retries even if Bridge still requires ToS. Downstream callers treat resolution as completion, so the modal can close while terms are still required.

🐛 Proposed fix
 export async function confirmBridgeTosAndAwaitRails(fetchUser: () => Promise<IUserProfile | null>) {
-    const result = await confirmBridgeTos()
+    let result = await confirmBridgeTos()
     if (!result.data?.accepted) {
         await new Promise((resolve) => setTimeout(resolve, 2000))
-        await confirmBridgeTos()
+        result = await confirmBridgeTos()
+    }
+
+    if (!result.data?.accepted) {
+        throw new Error('Bridge ToS confirmation was not accepted')
     }
@@
     for (let i = 0; i < 3; i++) {
         const updatedUser = await fetchUser()
@@
         const stillNeedsTos = deriveCapabilityPhaseSignals(updatedUser?.capabilities).needsTos
-        if (!stillNeedsTos) break
+        if (!stillNeedsTos) return
         if (i < 2) await new Promise((resolve) => setTimeout(resolve, 2000))
     }
+
+    throw new Error('Bridge ToS confirmation has not propagated yet')
 }
🤖 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/useMultiPhaseKycFlow.ts` around lines 51 - 75, The
confirmBridgeTosAndAwaitRails flow currently resolves even when the ToS
confirmation never propagates, so downstream callers can treat a failed
confirmation as success. Update confirmBridgeTosAndAwaitRails to track whether
deriveCapabilityPhaseSignals(...).needsTos ever clears after the retries, and
explicitly throw or reject if it remains true after the loop. Keep the
retry/markSubmitted logic intact, but ensure the function only completes
successfully when the fresh fetchUser() result shows ToS is no longer required.
🤖 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/app/`(mobile-ui)/card/page.tsx:
- Around line 406-419: The card reload-resume flow restores the Sumsub token but
does not trigger the wrapper to reopen automatically, so the resume path can
still fall back to StartVerificationView. Update the `/card` resume logic around
useSumsubReloadResume and the SumsubKycWrapper props so the resume state also
sets autoStart to true when a fresh token is recovered, matching the existing
token-refresh behavior and ensuring the SDK reopens instead of showing the start
screen.

---

Outside diff comments:
In `@src/hooks/useMultiPhaseKycFlow.ts`:
- Around line 51-75: The confirmBridgeTosAndAwaitRails flow currently resolves
even when the ToS confirmation never propagates, so downstream callers can treat
a failed confirmation as success. Update confirmBridgeTosAndAwaitRails to track
whether deriveCapabilityPhaseSignals(...).needsTos ever clears after the
retries, and explicitly throw or reject if it remains true after the loop. Keep
the retry/markSubmitted logic intact, but ensure the function only completes
successfully when the fresh fetchUser() result shows ToS is no longer required.
🪄 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: d7cc4ad8-4af3-4d53-af00-06d80214999f

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce6923 and cdb191b.

📒 Files selected for processing (5)
  • src/app/(mobile-ui)/card/page.tsx
  • src/components/Kyc/SumsubKycModals.tsx
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/hooks/useSumsubKycFlow.ts
  • src/hooks/useSumsubReloadResume.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Kyc/SumsubKycModals.tsx

Comment on lines +406 to +419
// PWA-reload resume (see useSumsubReloadResume). On a reload mid-Sumsub,
// re-apply to mint a fresh token for the same in-progress applicant and
// reopen the SDK — same idempotent call the token-refresh path uses.
useSumsubReloadResume(sumsubToken !== null, async () => {
const res = await rainApi.applyForCard({ termsAccepted: false })
if ((res.status === 'incomplete' || res.status === 'main-kyc-required') && 'sumsubAccessToken' in res) {
setSumsubToken(res.sumsubAccessToken)
posthog.capture(ANALYTICS_EVENTS.CARD_SUMSUB_OPENED)
return true
}
// user advanced past Sumsub while backgrounded — route normally
advanceFromApplyResponse(res)
return false
})

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and inspect their structure first.
git ls-files 'src/**' | rg 'Sumsub|sumsub|card/page.tsx|kyc|reloadResume|useSumsubReloadResume' -n

echo '--- outline card page ---'
ast-grep outline 'src/app/(mobile-ui)/card/page.tsx' --view expanded

echo '--- outline Sumsub wrapper candidates ---'
fd -a 'SumsubKycWrapper*' src || true
fd -a '*KycWrapper*' src || true
fd -a '*sumsub*' src || true

Repository: peanutprotocol/peanut-ui

Length of output: 2156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for wrapper prop definitions/usages and the reload-resume hook.
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  'SumsubKycWrapper|useSumsubReloadResume|autoStart|termsAccepted|sumsubAccessToken|CARD_SUMSUB_OPENED' src

echo '--- card page snippet context ---'
sed -n '360,460p' 'src/app/(mobile-ui)/card/page.tsx'

echo '--- shared KYC flow candidates ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  'autoStart=.*SumsubKycWrapper|<SumsubKycWrapper|SumsubKycWrapper\(' src

Repository: peanutprotocol/peanut-ui

Length of output: 12558


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- SumsubKycWrapper props and autoStart handling ---'
sed -n '1,120p' 'src/components/Kyc/SumsubKycWrapper.tsx'
echo '--- relevant autoStart logic ---'
sed -n '200,340p' 'src/components/Kyc/SumsubKycWrapper.tsx'

echo '--- card page wrapper usage ---'
sed -n '580,650p' 'src/app/(mobile-ui)/card/page.tsx'

echo '--- shared KYC modal wrapper usage ---'
sed -n '1,80p' 'src/components/Kyc/SumsubKycModals.tsx'

Repository: peanutprotocol/peanut-ui

Length of output: 14488


Pass autoStart on the card Sumsub resume path. SumsubKycWrapper already supports autoStart; when it’s falsy, it shows StartVerificationView instead of reopening the SDK. The /card reload-resume flow restores sumsubToken but never sets autoStart, so resume still lands on the start screen.

🤖 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/app/`(mobile-ui)/card/page.tsx around lines 406 - 419, The card
reload-resume flow restores the Sumsub token but does not trigger the wrapper to
reopen automatically, so the resume path can still fall back to
StartVerificationView. Update the `/card` resume logic around
useSumsubReloadResume and the SumsubKycWrapper props so the resume state also
sets autoStart to true when a fresh token is recovered, matching the existing
token-refresh behavior and ensuring the SDK reopens instead of showing the start
screen.

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== useSumsubReloadResume.ts ==="
cat -n src/hooks/useSumsubReloadResume.ts
echo "=== card/page.tsx relevant section ==="
rg -n "useSumsubReloadResume|kycInProgress|advanceFromApplyResponse|sumsubToken" -C4 "src/app/(mobile-ui)/card/page.tsx"
echo "=== useMultiPhaseKycFlow.ts full ==="
cat -n src/hooks/useMultiPhaseKycFlow.ts
echo "=== SumsubKycModals.tsx ==="
cat -n src/components/Kyc/SumsubKycModals.tsx
echo "=== useSumsubKycFlow.ts handleInitiateKyc ==="
cat -n src/hooks/useSumsubKycFlow.ts

Length of output: 59731


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== SumsubKycWrapper.tsx autoStart usage ==="
rg -n "autoStart" -C6 src/components/Kyc/SumsubKycWrapper.tsx
echo "=== full component outline ==="
ast-grep run --pattern 'export const SumsubKycWrapper = $_ => { $$$ }' --lang tsx src/components/Kyc/SumsubKycWrapper.tsx | head -100

Length of output: 11825


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '195,235p' src/components/Kyc/SumsubKycWrapper.tsx

Length of output: 1683


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "setSdkAutoStart|sdkAutoStart" -C3 src/hooks/useMultiPhaseKycFlow.ts src/components/Kyc/SumsubKycModals.tsx
echo "=== check callers passing autoStartSdk explicitly ==="
rg -n "autoStartSdk" -C3
echo "=== check SumsubKycModals callers (add-money, withdraw, qr-pay, drawer) ==="
rg -n "SumsubKycModals" -C3 src

Length of output: 2986


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "SumsubKycModals" -C2 --type ts --type tsx src 2>/dev/null || rg -n "SumsubKycModals" -C2 src -g '*.tsx' -g '*.ts'

Length of output: 15937


@kushagrasarathe Re-reviewed the follow-up commit in depth.

Confirmed fixed from the previous round

  • Duplication/orderinguseSumsubReloadResume.ts is clean: didResumeRef (mount-once resume) and syncSkipRef (skip-first sync) live in the same hook, so the ordering contract can't be broken by future reshuffling across files.
  • Wrong-path resume / stuck flaghandleInitiateKyc now consistently returns a boolean across every exit path (error, unsupported-region, bridge-direct, already-approved-no-token, native-SDK branches, success). useSumsubReloadResume clears ?kyc whenever onResume() resolves falsy or throws, so self-heal/start-action/restart-identity reloads degrade gracefully instead of looping or reopening the wrong level.
  • ||?? — verified; no current caller passes autoStartSdk={false}, so this is purely a latent-safety fix, no behavior change today.

New finding: sdkAutoStart never resets to false (useMultiPhaseKycFlow.ts)

const [sdkAutoStart, setSdkAutoStart] = useState(false)
useSumsubReloadResume(showWrapper, async () => {
    setSdkAutoStart(true)
    return handleInitiateKyc()
})

setSdkAutoStart(true) is the only setter call in the file — there's no path that flips it back to false. SumsubKycWrapper's [visible, autoStart] effect skips StartVerificationView any time visible turns true while autoStart is true:

} else if (autoStart) {
    setIsVerificationStarted(true) // skips StartVerificationView
}

So once any reload-resume attempt fires in a component's lifetime — even a failed one where the SDK never reopens — sdkAutoStart is stuck true for the rest of that mount. Any later genuine reopen in the same session (handleSelfHealResubmit, handleStartAction, handleRestartIdentity, or a plain retry via handleInitiateKyc) will silently skip the start/consent view too, even though the user never went through a reload for that attempt. This affects every caller that doesn't pass an explicit autoStartSdk (i.e. relies on flow.sdkAutoStart): SumsubKycFlow.tsx, MantecaFlowManager.tsx, BankFlowManager.view.tsx, withdraw/[country]/bank/page.tsx, withdraw/manteca/page.tsx, qr-pay/page.tsx, AddWithdrawCountriesList.tsx.

Suggested fix — reset it alongside the other per-attempt state in the wrapped handleInitiateKyc, or scope it to just the resumed attempt:

const handleInitiateKyc = useCallback(
    async (...) => {
        ...
        setSdkAutoStart(false) // clear any stale resume flag before a fresh attempt
        ...
    },
    [...]
)

(with the resume callback still setting it true right before its own call, after this reset runs.)

Minor: /card doesn't wire autoStart

card/page.tsx's <SumsubKycWrapper> doesn't pass an autoStart prop at all, unlike the shared multi-phase flow. So on Android PWA resume, /card will re-mint the token and reopen the modal, but the user lands back on StartVerificationView and has to tap through again before the SDK actually launches — inconsistent with the seamless resume on add-money/withdraw/qr-pay/drawer. Not a functional break, just worth confirming it's intentional (card's start view may carry required terms) rather than an oversight.

Performance / latency

No change from the prior round — still just one idempotent re-apply/re-initiate call on cold reload, cheap history.replaceState for the URL sync.

Code quality / DRY

Good improvement — the shared hook removed the duplication and the ordering hazard flagged previously. The sdkAutoStart leak above is the main quality gap remaining in this round.

Concise QA guide

  1. Android PWA, /card: start verification → background (camera/gallery) → return → confirm resume works, note the extra "Start Verification" tap (see minor finding above).
  2. Repeat on add-money, withdraw, qr-pay, KYC drawer → confirm resume reopens the SDK directly (no start view), completes normally.
  3. New scenario to add: after a successful (or failed) reload-resume on add-money/withdraw/qr-pay/drawer, without reloading again, trigger a different genuine re-open in the same session (e.g. get rejected → tap "Retry"/self-heal resubmit, or a Manteca restart-identity CTA) → verify StartVerificationView still shows (currently it won't, per the finding above).
  4. Manually close Sumsub → confirm ?kyc clears; reload afterward → confirm no auto-resume.
  5. Force apply/initiate failure while kyc=true (airplane mode) → reload → confirm flag clears, no infinite retry loop.
  6. Sanity-check desktop/iOS — kyc param inert, no regressions.

Round-2 review: sdkAutoStart was set true on resume but never reset, so a
later genuine reopen in the same session (retry after rejection, self-heal
resubmit, start-action, restart-identity) would also skip
StartVerificationView.

Reset it on a failed resume (SDK never opened) and on the falling edge of
showWrapper (resumed session closed). Only the reload-resume open skips the
start view now; every other open shows it. Falling-edge reset is race-safe
(fires only on true->false), so it never disarms the pending resume open.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Addressed round-2 in a82a079.

sdkAutoStart never resets (valid, fixed). Note the suggested one-liner (reset inside handleInitiateKyc) is actually racy — the resume calls setSdkAutoStart(true) before handleInitiateKyc(), so a reset inside it would clobber the resume's own autostart. Fixed it correctly instead:

  • On a failed resume (handleInitiateKyc() returns false, SDK never opened) → disarm immediately in the resume callback.
  • On the falling edge of showWrapper (resumed session closed) → disarm via a prevShowWrapperRef effect that fires only on true → false, so it never disarms the pending resume open (no race with the intermediate showWrapper=false renders while the resume's re-init is in flight).

Result: only the reload-resume open skips StartVerificationView; a later retry-after-rejection / self-heal / start-action / restart-identity in the same session shows it again. Covers your new QA scenario #3.

/card doesn't wire autoStart (minor) — intentional for now. Making card's resume seamless would mean duplicating this same arm/disarm state machine (fresh card opens must still show the consent view, only resume skips it) into the card page's bespoke integration. For a separate flow the reporters aren't hitting, that complexity isn't worth it — the extra "Start Verification" tap on card resume is harmless (it's a light consent screen). Easy follow-up if we want parity later.

Verified: typecheck clean, npm run build passes, prettier clean, jest 213 passed across the KYC/card suites.

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== useMultiPhaseKycFlow.ts ==="
cat -n src/hooks/useMultiPhaseKycFlow.ts
echo "=== useSumsubReloadResume.ts ==="
cat -n src/hooks/useSumsubReloadResume.ts
echo "=== card/page.tsx relevant ==="
rg -n "useSumsubReloadResume|kycInProgress|advanceFromApplyResponse|sumsubToken" -C4 "src/app/(mobile-ui)/card/page.tsx"
echo "=== SumsubKycModals.tsx ==="
cat -n src/components/Kyc/SumsubKycModals.tsx
echo "=== useSumsubKycFlow.ts ==="
cat -n src/hooks/useSumsubKycFlow.ts

Length of output: 60628


@kushagrasarathe Re-reviewed the round-3 commit (a82a079) in depth.

Risk of new bugs / breaking existing functionality

All three previously-flagged issues are now correctly resolved, and the fix for sdkAutoStart is the right one:

  • No race — because setSdkAutoStart(true) happens strictly before originalHandleInitiateKyc can flip showWrapper to true, the falling-edge effect (prevShowWrapperRef.current && !showWrapper) never fires during that in-flight window (showWrapper stays false the whole time, so there's no true → false transition to trigger a premature disarm). Good catch avoiding the naive "reset inside handleInitiateKyc" suggestion I made last round.
  • Failed resume — disarmed synchronously in the same callback (if (!opened) setSdkAutoStart(false)), so a resume that resolves without reopening (self-heal/start-action/restart-identity cases the bare resume can't reconstruct) correctly falls back to showing the start view on the next genuine open.
  • Session-end disarm — the true → false edge on showWrapper fires for any close of a resumed session (manual close, SDK complete, error), and since it's edge-triggered (not level-triggered), it can't misfire while showWrapper is still false pre-resume.
  • Verified useSumsubKycFlow.ts's handleInitiateKyc returns a boolean on every exit path, and useSumsubReloadResume.ts's mount/sync effect ordering + .catch(() => false) error handling are unchanged and correct.
  • SumsubKycModals ?? and /card's intentional no-autoStart are as discussed — no issues.

Minor (optional) nit

The wrapped handleInitiateKyc in useMultiPhaseKycFlow.ts (Line 267-270) fires KYC_INITIATED/MANTECA_KYC_INITIATED on every call, including the resume-triggered one from useSumsubReloadResume's callback. That means a PWA-reload resume looks identical to a fresh initiation in analytics — could slightly inflate "initiated" counts relative to genuine new sessions. Not a functional bug, just worth a distinguishing property (e.g. { resumed: true }) on the capture call if that metric matters for funnel analysis. Not blocking.

Performance / latency

Unchanged from prior rounds — one idempotent re-apply/re-initiate call on cold reload, history.replaceState for URL sync, no new polling patterns.

Code quality / DRY

No concerns remaining. The shared hook + boolean-return contract + edge-triggered disarm is a clean, well-documented state machine, and the reasoning in the commit message matches what's actually in the code.

Concise QA guide

  1. /card Android PWA: start verification → background (camera/gallery) → return → confirm resume reopens Sumsub with a fresh token (note: lands on Start Verification screen, expected/accepted).
  2. add-money / withdraw / qr-pay / KYC drawer: same reload-resume → confirm SDK reopens directly (skips start view), completes normally.
  3. After a resumed session completes or is manually closed, trigger a genuine new open in the same session (retry after rejection, self-heal resubmit, start-action, restart-identity) → confirm the start/consent view shows again (this was the round-2 bug — verify it's fixed).
  4. Force apply/initiate failure while ?kyc=true (airplane mode), reload → confirm the flag clears and there's no retry loop.
  5. Manually close Sumsub → confirm ?kyc clears from the URL.
  6. Desktop/iOS sanity check → no regressions, kyc param inert.

[resolve]

A PWA-reload resume drives handleInitiateKyc and previously fired
KYC_INITIATED identically to a fresh start, inflating "initiated" counts in
the funnel. Add resumed: true/false to the capture so resumes are
distinguishable. Round-3 review nit.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Round-3 nit addressed in 836c3a2 — resume-triggered initiations now carry resumed: true on the KYC_INITIATED/MANTECA_KYC_INITIATED capture, so a PWA-reload resume is distinguishable from a genuine new session in the funnel. No signature change (ref-based, read at the synchronous capture, reset after the resume settles).

That was the last open item — CodeRabbit's round-3 review confirmed all prior findings resolved with no blocking concerns. PR is ready for human review.

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.

1 participant