Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e8e5fcc
Add Veriff identity verification for partners
devkiran Mar 19, 2026
b672a44
Use camelCase for IdentityVerificationStatus enum values
devkiran Mar 19, 2026
0215029
Veriff: use native fetch in server action with Zod for request/respon…
devkiran Mar 19, 2026
2d6983a
Improve webhooks
devkiran Mar 19, 2026
79def46
Merge branch 'main' into veriff
devkiran Mar 20, 2026
8084e11
Veriff partner verification: session storage, webhooks, and rate limits
devkiran Mar 20, 2026
7cbf520
Merge branch 'main' into veriff
devkiran Mar 25, 2026
472521b
Handle Veriff decision outcomes and partner identity emails
devkiran Mar 25, 2026
dd19d90
format
devkiran Mar 25, 2026
f8e3c9c
Update route.ts
devkiran Mar 25, 2026
a3cc692
Update partners.ts
devkiran Mar 25, 2026
6835514
Update identity-verification-card.tsx
devkiran Mar 25, 2026
43952c2
fix build
devkiran Mar 25, 2026
a233e4a
Merge branch 'main' into veriff
devkiran Mar 26, 2026
a15df1c
Partner identity verification: profile section, legal name, email tem…
devkiran Mar 26, 2026
ca5dc42
Enhance identity verification section: add status badges, improve err…
devkiran Mar 26, 2026
759fcb8
Enforce identity verification attempt limits
devkiran Mar 26, 2026
176bc83
Show partner identity verification status and tighten verification ha…
devkiran Mar 26, 2026
c229abe
Merge branch 'main' into veriff
devkiran Mar 26, 2026
72cc6b7
Update apps/web/.env.example
devkiran Mar 26, 2026
1b98126
Update partners.ts
devkiran Mar 26, 2026
d2a4208
Merge branch 'veriff' of https://github.com/dubinc/dub into veriff
devkiran Mar 26, 2026
97415e5
Rename trusted badge and align verification failure payload
devkiran Mar 26, 2026
b3a40d1
Remove PartnerIdentityVerificationResubmission
devkiran Mar 26, 2026
b7195b2
Use logAndRespond across Veriff webhook flow
devkiran Mar 26, 2026
7a63837
Tighten Veriff decision handling and session resets
devkiran Mar 26, 2026
72c7f2e
Update packages/email/src/templates/partner-identity-verified.tsx
devkiran Mar 26, 2026
517b92b
Update handle-decision-event.ts
devkiran Mar 26, 2026
fb4efc3
Update route.ts
devkiran Mar 26, 2026
cdb3450
Merge branch 'veriff' of https://github.com/dubinc/dub into veriff
devkiran Mar 26, 2026
7b7b745
Update partner.prisma
devkiran Mar 26, 2026
0e6e3e0
Merge branch 'main' into veriff
devkiran Mar 26, 2026
44c67bf
Update route.ts
devkiran Mar 26, 2026
c024b3b
Add identity verification as program eligibility requirement
devkiran Mar 26, 2026
e43b69d
Merge branch 'main' into veriff
devkiran Mar 27, 2026
2731fc2
Remove legalName
devkiran Mar 27, 2026
25398a8
Update identity-verification-section.tsx
devkiran Mar 27, 2026
57c636f
Update handle-decision-event.ts
devkiran Mar 27, 2026
bab78fa
Merge branch 'veriff' into verified-eligibility-requirement
devkiran Mar 27, 2026
454bda0
Refactor eligibility conditions to include identity verification requ…
devkiran Mar 27, 2026
77ca6f1
Refactor updateApplicationSettingsAction to use new schema for input …
devkiran Mar 27, 2026
5fa8ac6
Refactor eligibility conditions to remove email domain checks and upd…
devkiran Mar 27, 2026
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
6 changes: 5 additions & 1 deletion apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,8 @@ HUBSPOT_CLIENT_SECRET=
# E2E Playwright Tests
PLAYWRIGHT_BASE_URL=http://partners.localhost:8888
E2E_PARTNER_EMAIL=
E2E_PARTNER_PASSWORD=
E2E_PARTNER_PASSWORD=

# Veriff (Identity Verification)
VERIFF_API_KEY=
VERIFF_WEBHOOK_SECRET=
2 changes: 2 additions & 0 deletions apps/web/app/(ee)/api/cron/partners/auto-approve/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export const POST = withCron(async ({ rawBody }) => {
context: {
country: programEnrollment.partner.country,
email: programEnrollment.partner.email,
identityVerificationStatus:
programEnrollment.partner.identityVerificationStatus,
},
});

Expand Down
3 changes: 3 additions & 0 deletions apps/web/app/(ee)/api/cron/partners/auto-reject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const POST = withCron(async ({ rawBody }) => {
name: true,
email: true,
country: true,
identityVerificationStatus: true,
},
},
program: {
Expand Down Expand Up @@ -65,6 +66,8 @@ export const POST = withCron(async ({ rawBody }) => {
context: {
country: programEnrollment.partner.country,
email: programEnrollment.partner.email,
identityVerificationStatus:
programEnrollment.partner.identityVerificationStatus,
},
});

Expand Down
5 changes: 5 additions & 0 deletions apps/web/app/(ee)/api/network/partners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export const GET = withWorkspace(
starredAt: partner.starredAt ? new Date(partner.starredAt) : null,
ignoredAt: partner.ignoredAt ? new Date(partner.ignoredAt) : null,
invitedAt: partner.invitedAt ? new Date(partner.invitedAt) : null,
identityVerificationStatus:
partner.identityVerificationStatus ?? null,
identityVerifiedAt: partner.identityVerifiedAt
? new Date(partner.identityVerifiedAt)
: null,
categories: partner.categories
? partner.categories.split(",").map((c: string) => c.trim())
: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function EmbedBountyDetail({
aria-label="Back to bounties"
title="Back to bounties"
onClick={onBack}
className="bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 hover:bg-bg-emphasis active:scale-95"
className="bg-bg-subtle hover:bg-bg-emphasis flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 active:scale-95"
>
<Trophy className="size-4" />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function EmbedBountySubmissionDetail({
aria-label="Back to bounties"
title="Back to bounties"
onClick={onBackToRoot}
className="bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 hover:bg-bg-emphasis active:scale-95"
className="bg-bg-subtle hover:bg-bg-emphasis flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 active:scale-95"
>
<Trophy className="size-4" />
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"use client";

import { parseActionError } from "@/lib/actions/parse-action-errors";
import { startIdentityVerificationAction } from "@/lib/actions/partners/start-identity-verification";
import usePartnerProfile from "@/lib/swr/use-partner-profile";
import { PartnerProps } from "@/lib/types";
import { MAX_PARTNER_IDENTITY_VERIFICATION_ATTEMPTS } from "@/lib/zod/schemas/partners";
import { Button, StatusBadge } from "@dub/ui";
import {
ShieldCheck,
TriangleWarning,
Veriff,
VerifiedBadge,
} from "@dub/ui/icons";
import { cn } from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import { toast } from "sonner";

export function IdentityVerificationSection({
partner,
}: {
partner?: PartnerProps;
}) {
const { mutate } = usePartnerProfile();

const { executeAsync, isPending } = useAction(
startIdentityVerificationAction,
{
onError: ({ error }) => {
toast.error(
parseActionError(error, "Failed to start identity verification."),
);
},
onSuccess: async ({ data }) => {
const { createVeriffFrame, MESSAGES } = await import(
"@veriff/incontext-sdk"
);

createVeriffFrame({
url: data.sessionUrl,
onEvent: (msg) => {
if (msg === MESSAGES.FINISHED) {
toast.success(
"Verification submitted. We'll update your status shortly.",
);
mutate();
}
},
});

mutate();
},
},
);

if (!partner) {
return null;
}

const {
identityVerificationStatus,
identityVerificationDeclineReason,
identityVerificationAttemptCount,
} = partner;

const isPendingReview =
identityVerificationStatus === "submitted" ||
identityVerificationStatus === "review";

const isMaxAttemptsReached =
identityVerificationAttemptCount >=
MAX_PARTNER_IDENTITY_VERIFICATION_ATTEMPTS &&
identityVerificationStatus !== "approved" &&
!isPendingReview;

const isFailed = [
"declined",
"resubmissionRequested",
"expired",
"abandoned",
].includes(identityVerificationStatus || "");

let buttonText = "Start verification";
let failedReason = identityVerificationDeclineReason || null;

// If the verification failed and no reason is provided, set the reason based on the status
if (isFailed && failedReason === null) {
switch (identityVerificationStatus) {
case "declined":
failedReason =
"We couldn't verify your identity. Please check your information or documents and try again.";
break;
case "resubmissionRequested":
failedReason =
"Verification couldn't be completed. Please check your information and resubmit.";
break;
case "expired":
failedReason =
"Verification attempt expired. Please start a new verification";
break;
case "abandoned":
failedReason =
"Verification attempt abandoned. Please start a new verification";
break;
}
}

switch (identityVerificationStatus) {
case "started":
buttonText = "Complete verification";
break;
case "declined":
case "resubmissionRequested":
buttonText = "Resubmit verification";
break;
}

return (
<div
className={cn(
failedReason && "overflow-hidden rounded-lg bg-amber-100 p-1",
)}
>
{failedReason && (
<div className="flex items-center gap-2 px-2 py-2">
<TriangleWarning className="size-3.5 shrink-0 text-amber-500" />
<p className="leading-0 text-sm font-medium text-amber-900">
<span className="font-semibold">Verification failed:</span>{" "}
{failedReason}
</p>
</div>
)}

<div className="border-border-subtle relative overflow-hidden rounded-lg border bg-neutral-50">
<div
className="pointer-events-none absolute inset-0 opacity-[0.4] [-webkit-mask-image:radial-gradient(ellipse_95%_85%_at_50%_42%,#000_0%,transparent_68%)] [background-image:radial-gradient(rgb(163_163_163)_1px,transparent_1px)] [background-size:4px_4px] [mask-image:radial-gradient(ellipse_95%_85%_at_50%_42%,#000_0%,transparent_68%)]"
aria-hidden
/>
<div className="relative flex flex-col items-center gap-3 px-6 py-3">
{identityVerificationStatus === "approved" ? (
<VerifiedBadge className="size-6" />
) : (
<ShieldCheck className="size-6 text-neutral-400" />
)}

{identityVerificationStatus === "approved" ? (
<StatusBadge
variant="success"
className="rounded-lg font-semibold"
icon={null}
>
Identity verified
</StatusBadge>
) : isPendingReview ? (
<StatusBadge
variant="pending"
className="rounded-lg font-semibold"
icon={null}
>
Pending review
</StatusBadge>
) : buttonText ? (
<Button
text={buttonText}
variant="secondary"
disabled={isMaxAttemptsReached}
disabledTooltip={
isMaxAttemptsReached
? "You have reached the maximum number of verification attempts. Please contact support if you need help."
: undefined
}
onClick={() => executeAsync()}
loading={isPending}
className="h-10 w-fit rounded-lg px-4 py-1.5"
/>
) : null}

<div className="flex items-center gap-1 text-xs font-medium text-neutral-400">
<span>Powered by</span>
<Veriff className="w-auto" />
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { hasPermission } from "@/lib/auth/partner-users/partner-user-permissions
import usePartnerProfile from "@/lib/swr/use-partner-profile";
import { PageContent } from "@/ui/layout/page-content";
import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper";

import { useMergePartnerAccountsModal } from "@/ui/partners/merge-accounts/merge-partner-accounts-modal";
import { ThreeDots } from "@/ui/shared/icons";
import { Button, Popover, Users2 } from "@dub/ui";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
useFormContext,
} from "react-hook-form";
import { toast } from "sonner";
import { IdentityVerificationSection } from "./identity-verification-section";
import { SettingsRow } from "./settings-row";

type BasicInfoFormData = {
Expand Down Expand Up @@ -102,6 +103,14 @@ export function ProfileDetailsForm({ partner }: { partner?: PartnerProps }) {
</FormProvider>
</SettingsRow>

<SettingsRow
id="identity-verification"
heading="Identity verification"
description="Verify your identity to build trust with programs and get approved for programs faster."
>
<IdentityVerificationSection partner={partner} />
</SettingsRow>

<SettingsRow
id="platforms"
heading="Website and socials"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function ProgramSidebar({
context: {
country: partner?.country,
email: partner?.email,
identityVerificationStatus: partner?.identityVerificationStatus,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function ApplyButton({ program }: { program: NetworkProgramProps }) {
context: {
country: partner?.country,
email: partner?.email,
identityVerificationStatus: partner?.identityVerificationStatus,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function ProgramStatusBadge({
context: {
country: partner?.country,
email: partner?.email,
identityVerificationStatus: partner?.identityVerificationStatus,
},
});

Expand Down
Loading