Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/lib/utils/wave/liveness.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import z from 'zod';
import { authenticatedCall } from './call';
import { redirect } from '@sveltejs/kit';
import { authenticatedCall, LivenessCheckpointRequiredError } from './call';
import parseRes from './utils/parse-res';

/** Areas that a checkpoint can be required for. */
Expand Down Expand Up @@ -29,6 +30,26 @@ export async function getLivenessCheckpointStatus(f = fetch, purpose: LivenessCh
);
}

/**
* Re-throws anything that isn't the API refusing on a due checkpoint; for that
* one, redirects into the challenge flow instead.
*
* Whether a check is due is a question only the API can answer, and only per
* *grant*: grants belonging to a KYB'd org are exempt, because their withdrawers
* are vouched for by the company record rather than by a personal identity
* check. None of that is visible from a user-level status call, so the client
* finds out by making the request it actually wants and reacting to the refusal.
*
* Pre-gating on `getLivenessCheckpointStatus` instead is what sent KYB
* withdrawers — who have no personal KYC by design — into the `kyc-required`
* dead end.
*/
export function handleCheckpointRequired(err: unknown, backTo: string): never {
if (!(err instanceof LivenessCheckpointRequiredError)) throw err;

throw redirect(302, `/wave/checkpoint?backTo=${encodeURIComponent(backTo)}`);
}

/**
* Starts (or resumes) a check and returns a SumSub token scoped to it. Throws
* if the user isn't currently allowed to start one.
Expand Down
43 changes: 0 additions & 43 deletions src/routes/(pages)/wave/(base-layout)/rewards/+layout.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/routes/(pages)/wave/(base-layout)/rewards/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import modal from '$lib/stores/modal';
import type { GrantDetailDto, GrantDto, GrantStatus } from '$lib/utils/wave/types/grant.js';
import { getGrant } from '$lib/utils/wave/grants.js';
import { LivenessCheckpointRequiredError } from '$lib/utils/wave/call.js';
import { goto } from '$app/navigation';

let { data } = $props();

Expand Down Expand Up @@ -74,6 +76,15 @@
? { stellarAddress: lastTest.stellarAddress, memo: lastTest.memoValue ?? undefined }
: undefined;
modal.show(Stepper, undefined, withdrawalFlow(grant, prefill, kybData));
} catch (err) {
// A check can fall due mid-session, between this page loading and the
// user opening the flow. Send them through it rather than failing the
// click silently.
if (err instanceof LivenessCheckpointRequiredError) {
await goto(`/wave/checkpoint?backTo=${encodeURIComponent('/wave/rewards')}`);
return;
}
throw err;
} finally {
loadingWithdrawalGrantId = null;
}
Expand Down
7 changes: 6 additions & 1 deletion src/routes/(pages)/wave/(base-layout)/rewards/+page.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { getGrants } from '$lib/utils/wave/grants.js';
import { getKycStatus } from '$lib/utils/wave/kyc.js';
import { getKybStatus, type KybStatus } from '$lib/utils/wave/kyb.js';
import { handleCheckpointRequired } from '$lib/utils/wave/liveness.js';
import { redirect } from '@sveltejs/kit';

export const load = async ({ parent, url, fetch, depends }) => {
depends('wave:rewards');
depends('wave:liveness-checkpoint');

const { user } = await parent();

if (!user) {
throw redirect(302, `/wave/login?backTo=${encodeURIComponent(url.pathname + url.search)}`);
}

// The API gates the grants endpoints on the liveness checkpoint and is the
// only thing that knows whether this user's grants need one, so we ask by
// fetching and let the refusal route us.
const [grants, kycStatus] = await Promise.all([
getGrants(fetch, { page: 1, limit: 100 }),
getKycStatus(fetch),
]);
]).catch((err) => handleCheckpointRequired(err, url.pathname + url.search));

// Fetch KYB status for each unique org among org grants
const orgIds = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { getGrant } from '$lib/utils/wave/grants.js';
import { getKycStatus } from '$lib/utils/wave/kyc.js';
import { getKybStatus } from '$lib/utils/wave/kyb.js';
import { handleCheckpointRequired } from '$lib/utils/wave/liveness.js';
import { error, redirect } from '@sveltejs/kit';

export const load = async ({ parent, url, fetch, params, depends }) => {
depends('wave:rewards');
depends('wave:liveness-checkpoint');

const { user } = await parent();

if (!user) {
throw redirect(302, `/wave/login?backTo=${encodeURIComponent(url.pathname + url.search)}`);
}

// Refused only when *this* grant needs a check — an exempt KYB org grant is
// served as normal. See handleCheckpointRequired.
const [grant, kycStatus] = await Promise.all([
getGrant(fetch, params.grantId),
getKycStatus(fetch),
]);
]).catch((err) => handleCheckpointRequired(err, url.pathname + url.search));

if (!grant) {
throw error(404, 'Grant not found');
Expand Down
Loading