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
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ export async function POST(req: Request) {
syncTotalCommissions({
partnerId: targetPartnerId,
programId,
mode: "direct",
}),
),
]);
Expand Down
74 changes: 36 additions & 38 deletions apps/web/app/(ee)/api/stripe/integration/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,45 +134,43 @@ export const POST = withAxiom(async (req: Request) => {
response: result.response,
};

waitUntil(
(async () => {
// if workspaceId is returned as undefined
// AND the response does not contain "Workspace not found" (indicating the workspace doesn't exist)
// we try to find the workspace ID from the Stripe account ID
if (
!result.workspaceId &&
!result.response.startsWith("Workspace not found") &&
event.account
) {
const stripeWebhookWorkspace = await prisma.project.findUnique({
where: {
stripeConnectId: event.account,
},
select: {
id: true,
},
});
if (stripeWebhookWorkspace) {
// if workspace exists, we set the workspace ID
result.workspaceId = stripeWebhookWorkspace.id;
}
}
// if workspaceId is returned as undefined
// AND the response does not contain "Workspace not found" (indicating the workspace doesn't exist)
// we try to find the workspace ID from the Stripe account ID
if (
!result.workspaceId &&
!result.response.startsWith("Workspace not found") &&
event.account
) {
const stripeWebhookWorkspace = await prisma.project.findUnique({
where: {
stripeConnectId: event.account,
},
select: {
id: true,
},
});
if (stripeWebhookWorkspace) {
// if workspace exists, we set the workspace ID
result.workspaceId = stripeWebhookWorkspace.id;
}
}

// if workspace ID exists, we capture the webhook log
if (result.workspaceId) {
await captureWebhookLog({
workspaceId: result.workspaceId,
method: req.method,
path: "/stripe/integration/webhook",
statusCode: 200,
duration: Date.now() - startTime,
requestBody: event,
responseBody,
userAgent: req.headers.get("user-agent"),
});
}
})(),
);
// if workspace ID exists, we capture the webhook log
if (result.workspaceId) {
waitUntil(
captureWebhookLog({
workspaceId: result.workspaceId,
method: req.method,
path: "/stripe/integration/webhook",
statusCode: 200,
duration: Date.now() - startTime,
requestBody: event,
responseBody,
userAgent: req.headers.get("user-agent"),
}),
);
}

console.log(`[${event.type}]: ${result.response}`);

Expand Down
27 changes: 20 additions & 7 deletions apps/web/app/(ee)/partners.dub.co/(onboarding)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ export default function PartnerOnboardingLayout({
))}
</div>

<div className="relative flex min-h-[100dvh] min-h-screen w-full flex-col items-center justify-between">
<div className="grow basis-0">
<div className="pt-4">
<Link href="https://dub.co/home" target="_blank" className="block">
<div className="relative flex min-h-[100dvh] min-h-screen w-full flex-col items-center overflow-hidden md:justify-between">
<div className="w-full px-4 md:grow md:basis-0 md:px-0">
<div className="flex justify-center pt-4">
<Link
href="https://dub.co/home"
target="_blank"
className="block w-fit"
>
<Wordmark className="h-8" />
<div className="text-center text-sm font-semibold text-black/80">
Partners
Expand All @@ -60,14 +64,23 @@ export default function PartnerOnboardingLayout({
</div>
</div>

<div className="w-full py-16">{children}</div>
<div className="w-full flex-1 overflow-y-auto md:flex-none md:overflow-visible">
<div className="w-full pt-8 pb-8 sm:pb-4 md:py-16 px-5 md:px-0">{children}</div>
</div>

<div className="w-full md:hidden">
<SignedInHint />
</div>

{/* Empty div to center main content */}
<div className="grow basis-0" />
<div className="hidden md:block md:grow md:basis-0" />
</div>

<div className="hidden md:block">
<SignedInHint />
</div>

<Toolbar show={["help"]} />
<SignedInHint />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function OnboardingForm({
render={({ field }) => (
<FileUpload
accept="images"
className="mt-1.5 size-20 rounded-full border border-neutral-300"
className="mt-1.5 size-20 shrink-0 rounded-full border border-neutral-300 transition-[border-color,box-shadow] focus-within:border-neutral-500 focus-within:ring-1 focus-within:ring-neutral-500"
iconClassName="size-5"
previewClassName="size-20 rounded-full"
variant="plain"
Expand All @@ -162,11 +162,11 @@ export function OnboardingForm({
)}
/>
<div>
<p className="text-xs text-neutral-500">
Square image recommended, up to 2 MB.
<p className="text-xs font-medium text-neutral-600">
Visible to programs and helps with approvals
</p>
<p className="mt-0.5 text-xs font-medium text-neutral-500">
Adding an image can improve your approval rates.
<p className="mt-0.5 text-xs text-neutral-500">
Max 2 MB
</p>
</div>
</div>
Expand Down Expand Up @@ -218,7 +218,7 @@ export function OnboardingForm({

<label>
<span className="text-sm font-medium text-neutral-800">
Description
About you
<span className="font-normal text-neutral-500"> (optional)</span>
</span>
<div>
Expand All @@ -229,7 +229,7 @@ export function OnboardingForm({
? "border-red-300 pr-10 text-red-900 placeholder-red-300 focus:border-red-500 focus:ring-red-500"
: "border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-neutral-500",
)}
placeholder="Tell us about the kind of content you create – e.g. tech, travel, fashion, etc."
placeholder="Share who you are, what you do, and who your audience is."
maxLength={MAX_PARTNER_DESCRIPTION_LENGTH}
minRows={3}
onKeyDown={handleKeyDown}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { OnboardingForm } from "./onboarding-form";

export default function PartnerOnboarding() {
return (
<div className="mx-auto flex w-full max-w-[480px] flex-col items-center md:mt-4">
<h1 className="animate-slide-up-fade text-lg font-medium [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
<div className="mx-auto flex w-full max-w-[430px] flex-col items-center md:mt-4">
<h1 className="animate-slide-up-fade text-center text-xl font-semibold [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
Create your partner profile
</h1>
<div className="animate-slide-up-fade w-full rounded-xl p-8 [--offset:10px] [animation-delay:500ms] [animation-duration:1s] [animation-fill-mode:both]">
<div className="animate-slide-up-fade w-full pt-8 [--offset:10px] [animation-delay:500ms] [animation-duration:1s] [animation-fill-mode:both]">
<Suspense fallback={<OnboardingForm />}>
<OnboardingFormRSC />
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { Suspense } from "react";

export default function OnboardingVerificationPage() {
return (
<div className="relative mx-auto my-10 flex w-full max-w-[640px] flex-col items-center px-4 text-center sm:px-6 md:mt-6">
<h1 className="animate-slide-up-fade text-content-emphasis text-xl font-semibold [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
<div className="relative mx-auto flex w-full max-w-[600px] flex-col items-center text-center md:mt-6">
<h1 className="animate-slide-up-fade text-content-emphasis text-xl text-center font-semibold [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
Connect payouts
</h1>
<p className="animate-slide-up-fade mt-1 text-base font-medium text-neutral-500 [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
<p className="animate-slide-up-fade mt-1 text-center text-base text-neutral-500 [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
Connect your preferred payout method to receive payments.
</p>
<div className="animate-slide-up-fade relative mt-12 w-full [--offset:10px] [animation-delay:500ms] [animation-duration:1s] [animation-fill-mode:both]">
<div className="animate-slide-up-fade relative mt-10 w-full [--offset:10px] [animation-delay:500ms] [animation-duration:1s] [animation-fill-mode:both]">
<Suspense fallback={<PayoutSkeleton />}>
<PayoutRSC />
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@ import { OnboardingPlatformsPageClient } from "./page-client";

export default function OnboardingPlatformsPage() {
return (
<div className="relative mx-auto w-full max-w-[416px] text-center md:mt-4">
<h1 className="animate-slide-up-fade text-lg font-medium [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
<div className="mx-auto flex w-full max-w-[430px] flex-col text-center md:mt-4">
<h1 className="animate-slide-up-fade text-center text-xl font-semibold [--offset:8px] [animation-delay:250ms] [animation-duration:1s] [animation-fill-mode:both]">
Your social and web platforms
</h1>

<p className="animate-slide-up-fade text-content-subtle mt-1 text-sm [animation-delay:500ms] [animation-duration:1s] [animation-fill-mode:both]">
<p className="animate-slide-up-fade text-content-subtle mt-1 text-base [animation-delay:500ms] [animation-duration:1s] [animation-fill-mode:both]">
Verifying your social and web platforms will improve your reputation
score and rank you higher in our partner network.
</p>

<div className="animate-slide-up-fade mt-8 grid gap-4 [animation-delay:750ms] [animation-duration:1s] [animation-fill-mode:both]">
<Suspense fallback={<PartnerPlatformsForm partner={null} />}>
<OnboardingPlatformsFormRSC />
</Suspense>
<div className="animate-slide-up-fade w-full rounded-xl py-8 [animation-delay:750ms] [animation-duration:1s] [animation-fill-mode:both]">
<div className="grid gap-4">
<Suspense fallback={<PartnerPlatformsForm partner={null} />}>
<OnboardingPlatformsFormRSC />
</Suspense>
</div>
</div>
</div>
);
Expand Down
61 changes: 23 additions & 38 deletions apps/web/app/api/veriff/webhook/handle-decision-event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { computeVeriffIdentityHash } from "@/lib/veriff/compute-veriff-identity-hash";
import { VeriffDecisionEvent } from "@/lib/veriff/schema";
import { detectDuplicateIdentityFraud } from "@/lib/api/fraud/detect-duplicate-identity-fraud";
import {
VeriffDecisionEvent,
VeriffRiskLabel,
veriffRiskLabels,
} from "@/lib/veriff/schema";
import {
mergeVeriffMetadata,
parseVeriffMetadata,
Expand All @@ -10,6 +14,7 @@ import PartnerIdentityVerified from "@dub/email/templates/partner-identity-verif
import { prisma } from "@dub/prisma";
import { IdentityVerificationStatus, Partner } from "@dub/prisma/client";
import { DUPLICATE_IDENTITY_DECLINE_REASON } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { logAndRespond } from "app/(ee)/api/cron/utils";

const veriffStatusMap: Record<
Expand All @@ -27,7 +32,8 @@ const veriffStatusMap: Record<
export const handleDecisionEvent = async ({
verification,
}: VeriffDecisionEvent) => {
const { id, status, decisionTime, reason, attemptId } = verification;
const { id, status, decisionTime, reason, attemptId, riskLabels } =
verification;

let effectiveStatus = status;

Expand Down Expand Up @@ -55,7 +61,6 @@ export const handleDecisionEvent = async ({

// since we're skipping verified partners, by default identityVerifiedAt is null
let identityVerifiedAt: Date | null = null;
let veriffIdentityHash: string | null | undefined = undefined;

let { sessionUrl, attemptCount, declineReason, sessionExpiresAt } =
parseVeriffMetadata(partner.veriffMetadata);
Expand All @@ -65,20 +70,28 @@ export const handleDecisionEvent = async ({

// If the verification was approved, check for country mismatch
if (effectiveStatus === "approved") {
veriffIdentityHash = computeVeriffIdentityHash(verification);
const isDuplicate = await checkDuplicateIdentity({
partner,
veriffIdentityHash,
});
const hasDuplicateRiskLabel =
riskLabels &&
riskLabels?.length > 0 &&
riskLabels.some(({ label }) =>
veriffRiskLabels.includes(label as VeriffRiskLabel),
);

const isCountryMismatch = checkCountryMismatch({
partner,
verification,
});

if (isDuplicate) {
if (hasDuplicateRiskLabel) {
effectiveStatus = "declined";
declineReason = DUPLICATE_IDENTITY_DECLINE_REASON;

waitUntil(
detectDuplicateIdentityFraud({
veriffSessionId: id,
riskLabels,
}),
);
} else if (isCountryMismatch) {
effectiveStatus = "declined";
declineReason = `Your document country (${verification.document?.country}) does not match your account country (${partner.country})`;
Expand Down Expand Up @@ -109,8 +122,6 @@ export const handleDecisionEvent = async ({
data: {
identityVerificationStatus: veriffStatusMap[effectiveStatus],
identityVerifiedAt,
veriffIdentityHash:
effectiveStatus === "approved" ? veriffIdentityHash : null,
veriffMetadata,
},
select: {
Expand Down Expand Up @@ -148,32 +159,6 @@ function checkCountryMismatch({
return partner.country.toUpperCase() !== veriffCountry;
}

async function checkDuplicateIdentity({
partner,
veriffIdentityHash,
}: {
partner: Pick<Partner, "id">;
veriffIdentityHash: string | null;
}): Promise<boolean> {
if (!veriffIdentityHash) {
return false;
}

const duplicatePartner = await prisma.partner.findFirst({
where: {
veriffIdentityHash,
id: {
not: partner.id,
},
},
select: {
id: true,
},
});

return !!duplicatePartner;
}

async function sendEmailNotification({
partner,
attemptId,
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/api/veriff/webhook/handle-session-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const handleSessionEvent = async ({
},
data: {
identityVerificationStatus: action,
veriffIdentityHash: null,
veriffMetadata,
},
});
Expand Down
6 changes: 4 additions & 2 deletions apps/web/app/app.dub.co/(onboarding)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ export default function Layout({ children }: PropsWithChildren) {
return (
<>
{children}
<Toolbar show={["help"]} />
<SignedInHint />
<SignedInHint className="hidden md:block" />
<div className="hidden md:block">
<Toolbar show={["help"]} />
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function DefaultDomainSelector() {

return (
<>
<div className="animate-fade-in mx-auto grid w-full max-w-[312px] gap-4 sm:max-w-[600px] sm:grid-cols-2">
<div className="animate-fade-in mx-auto grid w-full gap-4 sm:max-w-[600px] sm:grid-cols-2">
<DomainOption
step="domain/custom"
icon="https://assets.dub.co/icons/domain-sign.webp"
Expand Down
Loading
Loading