feat: migrate KYC and KYB UX to Didit - #65
Conversation
Deploying ratio1-dapp-test with
|
| Latest commit: |
3393653
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ef397940.ratio1-dapp-test.pages.dev |
| Branch Preview URL: | https://codex-didit-migration.ratio1-dapp-test.pages.dev |
There was a problem hiding this comment.
Pull request overview
This PR migrates the dApp’s KYC/KYB user journey from a Sumsub-only screen to a provider-neutral verification session flow that can embed Didit hosted sessions (while retaining Sumsub support for rollback), adds an explicit consent gate, and introduces backend-authoritative status polling plus deterministic preview/test coverage.
Changes:
- Introduces shared verification session typing + runtime parsing/validation (including strict Didit hosted URL boundary) and wires a new
/verification/sessionbackend API into the frontend. - Reworks the
/kycpage to show a consent gate, launch/refresh provider sessions (Didit iframe or Sumsub SDK), and poll account status to a completed “status updated” stage. - Adds Vitest + CI test step, along with unit tests for session parsing and polling behavior, and expands
/playwright-previewwith deterministic verification states.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/typedefs/verification.ts | Adds provider-neutral verification session types and a strict parser (including Didit URL validation). |
| src/typedefs/verification.test.ts | Adds Vitest coverage for session parsing and URL boundary enforcement. |
| src/pages/PlaywrightPreview.tsx | Adds deterministic preview states for the new verification experience (desktop/mobile friendly). |
| src/pages/KYC.tsx | Replaces the Sumsub-only flow with consent + provider-neutral session launch, embed rendering, and status polling. |
| src/pages/EmailConfirmation.tsx | Updates user-facing copy to reference KYC/KYB instead of only KYC. |
| src/pages/Dashboard.tsx | Uses applicant type to render “KYC” vs “KYB” labels in gating CTA copy. |
| src/lib/verification-status-polling.ts | Introduces a reusable polling helper for backend-authoritative verification status refresh. |
| src/lib/verification-status-polling.test.ts | Adds unit tests ensuring polling stops/cleans up correctly. |
| src/lib/utils.tsx | Adds helper label functions for “KYC” vs “KYB” (short/long variants). |
| src/lib/routes/routes.tsx | Updates route metadata copy and simplifies navigation-route filtering logic. |
| src/lib/api/backend.tsx | Adds createVerificationSession calling /verification/session and parsing the response. |
| src/components/Verification/VerificationExperience.tsx | Adds a shared UX component for consent/loading/provider/error/status stages. |
| src/components/Profile/sections/PersonalInformation.tsx | Updates profile KYC/KYB section to link into the new session flow (no URL token). |
| src/components/Licenses/modals/LicenseLinkModal.tsx | Updates KYC copy to reflect KYC vs KYB using new label helpers. |
| src/components/Licenses/modals/LicenseBulkLinkModal.tsx | Same as above for the bulk-link flow. |
| package.json | Adds npm test and pins vitest as a dev dependency. |
| package-lock.json | Locks Vitest (and transitive deps) into the dependency tree. |
| .github/workflows/pr_lint_build.yml | Adds a CI “Test” step running npm test. |
Comments suppressed due to low confidence (1)
src/components/Verification/VerificationExperience.tsx:173
- Because this link sits inside the clickable , clicking “Terms & Conditions” can toggle the checkbox unexpectedly. Add a click handler to prevent the label default toggle behavior when the link is clicked.
<Link
className="text-primary font-medium underline"
to={localTermsPath}
target="_blank"
rel="noreferrer"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Link | ||
| className="text-primary font-medium underline" | ||
| to={localPrivacyPolicyPath} | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| > |
| const refreshSumsubAccessToken = async (currentSession: SumsubVerificationSession) => { | ||
| const refreshedSession = await createVerificationSession(currentSession.applicantType); | ||
|
|
||
| if (refreshedSession.provider !== 'sumsub') { | ||
| throw new Error('Verification provider changed. Reload the page to continue.'); | ||
| } | ||
|
|
||
| setSession(refreshedSession); | ||
| return refreshedSession.accessToken; | ||
| }; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (4)
src/components/Profile/sections/PersonalInformation.tsx:71
- On subscription update failure, the UI remains in the optimistically-toggled state and the user can also fire overlapping requests. Guard against re-entry while a request is in flight, and revert the optimistic toggle when the API call fails.
const toggleEmailSubscriptionPreference = async () => {
// We set the new value optimistically
setSubscribed(!isSubscribed);
const apiCall: () => Promise<ApiAccount> = isSubscribed ? emailUnsubscribe : emailSubscribe;
vite.config.ts:25
- The Vite proxy rewrite callback parameter is named
path, which shadows the imported Nodepathmodule in this file. This is easy to misread and can triggerno-shadowlinting rules; rename the parameter to something likerequestPath.
rewrite: (path) => path.replace(/^\/api/, ''),
src/pages/KYC.tsx:98
- Polling stops only when the backend reaches a terminal status different from
statusAtLaunch. If the account is already in a terminal status at launch (e.g., Approved) this will keep polling indefinitely and never show the confirmed status stage. Stop polling on any terminal status and surface it to the UI.
if (isStatusOutcome(latestAccount.kycStatus) && latestAccount.kycStatus !== statusAtLaunch.current) {
setConfirmedStatus(latestAccount.kycStatus);
return true;
}
src/components/Profile/sections/PersonalInformation.tsx:29
- Email subscription updates can be triggered multiple times in quick succession, creating concurrent subscribe/unsubscribe requests and leaving the UI in an inconsistent state. Re-introduce an in-flight flag/state for the subscription toggle so the handler can be made idempotent while a request is pending.
This issue also appears on line 67 of the same file.
// Switches states
const [isSubscribed, setSubscribed] = useState<boolean>(false);
const [isCompany, setCompany] = useState<boolean>(false);
const [isFetchingKycInfo, setFetchingKycInfo] = useState<boolean>(false);
| <iframe | ||
| key={session.sessionId} | ||
| src={session.url} | ||
| title={`${session.applicantType === 'company' ? 'KYB' : 'KYC'} verification powered by Didit`} | ||
| allow="camera; microphone; fullscreen; autoplay; encrypted-media" | ||
| referrerPolicy="no-referrer" | ||
| className="h-[720px] min-h-[70dvh] w-full border-0 bg-white" | ||
| /> |
Summary
verify.didit.me/session/...URL boundaryWhy
The UI must switch to Didit at the same cutover as the backend while keeping credentials out of browser URLs and preserving the familiar Sumsub-style verification journey.
Merging this PR does not activate Didit by itself; the backend provider switch remains the source of truth.
Companion
Verification
npm test— 15 testsnpm run lintnpm run buildnpm audit --omit=dev— 0 critical advisories; existing unrelated dependency debt remainsLocal Didit Sandbox E2E
Verified the full local chain using a disposable PostgreSQL database and a real SIWE-authenticated test account:
status.updatedwebhooks through a temporary HTTPS tunnel; the backend returned HTTP 202The browser-automated camera could not satisfy Didit's liveness check. A Sandbox approval was therefore simulated in the Didit console. Because the session lacked the required liveness, proof-of-address, and questionnaire evidence, the backend correctly failed closed to
onHoldwithapproval_evidence_missinginstead of blindly trusting the provider'sApprovedlabel.No production database or production webhook configuration was touched. The temporary tunnel, local services, disposable database, test credentials, and browser state were cleaned up; the Didit Sandbox webhook was restored to its previous disabled state.