wave: require KYC to apply for issues + KYC prompt step#1906
Conversation
Companion to the backend gate. Block the apply page when the user lacks verified KYC, and add a dedicated "Verify your identity" step that's surfaced once after login. - New /wave/kyc-required step with warning AnnotationBox and Verify / Skip buttons, shown after login for users without verified KYC. RED (rejected) users skip the prompt — the pitch is misleading for them. - apply page: fetches KYC status and disables the form (FormField `disabled`), hides the slots AnnotationBox, and shows a red blocker with a Verify identity action when not verified. Backend 403 still guards submission. - Profile settings KYC nudge copy aligned with the new gate framing. - Minor support page response-time copy tweak.
There was a problem hiding this comment.
Pull request overview
Adds a frontend KYC gate earlier in the Wave contributor funnel by introducing a post-login “KYC required” prompt and disabling issue applications for users without verified identity.
Changes:
- Redirects newly-logged-in, non-verified users to a new
/wave/kyc-requiredstep (with a “verify” CTA and a “skip” option). - Fetches KYC status on the issue apply route and blocks the apply form UI when the user is not KYC-verified.
- Updates small bits of copy on the support page and the profile settings KYC nudge.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/(pages)/wave/(flows)/support/+page.svelte | Updates support response-time copy. |
| src/routes/(pages)/wave/(flows)/login/callback/+page.svelte | Adds post-login KYC status fetch + redirect to /wave/kyc-required for non-verified users. |
| src/routes/(pages)/wave/(flows)/kyc-required/+page.ts | Implements server load: validates backTo, fetches KYC status, redirects if already verified, otherwise requires auth. |
| src/routes/(pages)/wave/(flows)/kyc-required/+page.svelte | New UI step prompting identity verification with “Verify identity” / “Skip for now”. |
| src/routes/(pages)/wave/(flows)/[waveProgramSlug]/issues/[issueId]/apply/+layout.ts | Adds KYC status fetch and computes isKycVerified for the apply page. |
| src/routes/(pages)/wave/(flows)/[waveProgramSlug]/issues/[issueId]/apply/+page.svelte | Blocks apply UI when not KYC-verified; adds KYC-required AnnotationBox and disables form + submit. |
| src/routes/(pages)/wave/(base-layout)/settings/profile/+page.svelte | Adjusts KYC nudge copy to match new gating framing. |
Comments suppressed due to low confidence (1)
src/routes/(pages)/wave/(flows)/[waveProgramSlug]/issues/[issueId]/apply/+page.svelte:203
- Even when the form is "disabled" via the
FormFieldwrapper, theTurnstilecomponent still mounts and runs itsonMountlogic (polling for the script + rendering the widget). For non-KYC users this is wasted work and may still create network/activity. Consider conditionally rendering the Turnstile (and/or the whole verification FormField) only whendata.isKycVerifiedis true.
<FormField
title="Verification*"
description="Confirm you're not a robot with a quick check."
type="div"
disabled={!data.isKycVerified}
>
<div class="turnstile-wrapper">
<Turnstile ontoken={(t) => (turnstileToken = t)} />
</div>
</FormField>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Previously a transient failure on /api/kyc/status would silently coerce isKycVerified to false, blocking already-verified users with a misleading "please verify" banner. The call is rare and unexpected to fail, so we let it bubble to SvelteKit's error boundary instead.
Wrap the post-login getKycStatus() call in try/catch and proceed with the normal welcome/backTo flow on failure instead of throwing the user into the generic login-error state. The kyc-required nudge is a best-effort hint, not a login prerequisite.
Mirrors the login callback's behavior: a user whose KYC was rejected shouldn't see the "please verify your identity" pitch when they land on /wave/kyc-required directly (back button, stale bookmark, etc).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/routes/(pages)/wave/(flows)/kyc-required/+page.ts:27
- The
catchredirects to/wave/loginfor any error fromgetKycStatus(). If the user is already logged in (JWT present) but the KYC status endpoint errors (5xx/network),/wave/loginimmediately redirects back tobackTo(this page), creating an infinite redirect loop. Consider checkingparent()foruserand only redirecting to login when unauthenticated/401; for other errors, render the page (best-effort) or redirect tosafeBackToinstead of login.
try {
const kycStatus = await getKycStatus(fetch);
const isKycVerified =
kycStatus.status === 'applicantReviewed' && kycStatus.reviewAnswer === 'GREEN';
// RED users have already attempted KYC; the "please verify" pitch on this
// page is misleading for them. Mirror the login callback's behavior.
const isKycRejected = kycStatus.reviewAnswer === 'RED';
if (isKycVerified || isKycRejected) {
throw redirect(302, safeBackTo);
}
} catch (err) {
if (err && typeof err === 'object' && 'status' in err && err.status === 302) {
throw err;
}
// If not authenticated or other error, redirect to login
throw redirect(302, `/wave/login?backTo=${encodeURIComponent(url.pathname + url.search)}`);
}
url.searchParams.get() already returns the decoded value, so a second decodeURIComponent would either double-decode (turning encoded slashes back into path separators) or throw on a literal % in the input.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/routes/(pages)/wave/(flows)/kyc-required/+page.ts:26
- The catch-all error handler redirects to
/wave/loginfor any failure (including transient backend/network errors while the user is already authenticated). This can cause confusing login loops and hides real failures. Consider only redirecting to login on 401/unauthenticated, and otherwise either allow rendering the page (best-effort) or surface an error state.
} catch (err) {
if (err && typeof err === 'object' && 'status' in err && err.status === 302) {
throw err;
}
// If not authenticated or other error, redirect to login
throw redirect(302, `/wave/login?backTo=${encodeURIComponent(url.pathname + url.search)}`);
}
If a user is bounced from /wave/kyc-required through /wave/login and back, the backTo arriving at the login callback already points at kyc-required. Wrapping it again produced /wave/kyc-required?backTo= %2Fwave%2Fkyc-required%3F..., which would land Skip-for-now back on kyc-required and grow the URL on every login attempt. Extract the inner backTo before wrapping (capped to avoid pathological deep nests).
Aligns backTo parsing with /wave/login and /wave/verify-phone instead of reimplementing the decode + safety dance inline.
Previously any error from getKycStatus (5xx, network, parsing) would bounce the user to /wave/login, which masks real outages and can cause loops for already-authenticated users. Restrict the login redirect to 401 only; let everything else propagate to the error boundary.
FormField's disabled prop only added a visual style (opacity + pointer-events: none on the wrapper) — keyboard users could still tab into the TextArea and Cloudflare's Turnstile iframe and interact with them. - Add a disabled prop to TextArea that passes through to the native <textarea>, which properly removes it from the tab order and blocks typing. - Conditionally render <Turnstile> only when KYC is verified so the Cloudflare iframe isn't mounted (and can't be focused) until then.
Summary
Frontend companion to drips-network/wave#596 for drips-network/wave#592. Pushes KYC earlier in the contributor funnel: blocks the apply page when the user is not verified, and surfaces a dedicated "Verify your identity" step once after login.
/wave/kyc-requiredstep: yellow warning AnnotationBox + primary "Verify identity" / ghost "Skip for now" buttons. Reachable only from the post-login redirect (and direct nav). Self-resolves if the user is already verified./wave/kyc-required. RED-reviewed (rejected) users skip the prompt — the "please verify your identity" pitch is misleading for them.apply/+layout.tsfetches KYC status and returnsisKycVerified. No redirect — the apply page is reachable, just blocked.apply/+page.svelteshows a redIdentity verification (KYC) requiredAnnotationBox with a "Verify identity" action; both FormFields (application text + Turnstile) getdisabledso the wrapper greys out and ignores input; the application-slots info box is hidden in this state. Backend 403 still guards submission.Test plan
/wave/kyc-requiredafter the callback (wrapping the welcome flow for new users).backTo, no kyc-required step./wave/<slug>/issues/<id>/applywithout KYC → expect the red KYC AnnotationBox, disabled form, no slots info box; "Verify identity" button links to/wave/kyc?backTo=…./wave/kyc-required: "Skip for now" lands onbackTo; "Verify identity" lands on/wave/kyc?backTo=…./wave/kyc-requireddirectly while already verified → expect redirect tobackTo.Note
The "Learn more" link in the warning AnnotationBox points to
docs.drips.network/wave/contributors/solving-issues-and-earning-rewards#verifying-your-identity— that docs section needs to be authored before merge / shortly after.