Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip partner onboarding if user is claiming partner profile #2064

Merged
merged 7 commits into from
Feb 21, 2025
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 @@ -179,8 +179,14 @@ export function OnboardingForm({
control={control}
name="country"
rules={{ required: true }}
render={({ field }) => <CountryCombobox {...field} />}
render={({ field }) => (
// Disable the combobox if the partner already has a country
<CountryCombobox {...field} disabled={!!partner?.country} />
)}
/>
<p className="mt-1.5 text-xs text-neutral-500">
Your country cannot be changed once set.
</p>
</label>

<label>
Expand All @@ -203,7 +209,7 @@ export function OnboardingForm({

<Button
type="submit"
text={`${partner ? "Update" : "Create"} partner account`}
text="Continue"
className="mt-2"
loading={isPending || isSubmitting || isSubmitSuccessful}
/>
Expand All @@ -214,9 +220,11 @@ export function OnboardingForm({
function CountryCombobox({
value,
onChange,
disabled,
}: {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}) {
const options = useMemo(
() =>
Expand Down Expand Up @@ -261,7 +269,9 @@ function CountryCombobox({
"data-[state=open]:ring-1 data-[state=open]:ring-neutral-500 data-[state=open]:border-neutral-500",
"focus:ring-1 focus:ring-neutral-500 focus:border-neutral-500 transition-none",
!value && "text-neutral-400",
disabled && "cursor-not-allowed",
),
disabled,
}}
/>
);
Expand Down
37 changes: 35 additions & 2 deletions apps/web/lib/actions/partners/create-account-link.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
"use server";

import { stripe } from "@/lib/stripe";
import { PARTNERS_DOMAIN } from "@dub/utils";
import { createConnectedAccount } from "@/lib/stripe/create-connected-account";
import { prisma } from "@dub/prisma";
import { CONNECT_SUPPORTED_COUNTRIES, PARTNERS_DOMAIN } from "@dub/utils";
import { authPartnerActionClient } from "../safe-action";

export const createAccountLinkAction = authPartnerActionClient.action(
async ({ ctx }) => {
let { partner } = ctx;

if (!partner.stripeConnectId) {
throw new Error("Partner does not have a Stripe Connect account.");
// this should never happen
if (!partner.email) {
throw new Error(
"Partner does not have a valid email. Please contact support to update your email.",
);
}

if (!partner.country) {
throw new Error(
"You haven't set your country yet. Please go to partners.dub.co/onboarding to set your country.",
);
}

// guard against unsupported countries
if (!CONNECT_SUPPORTED_COUNTRIES.includes(partner.country)) {
throw new Error(
"Your current country is not supported for Stripe Connect. Please contact support to update your country.",
);
}
// create a new account
const connectedAccount = await createConnectedAccount({
name: partner.name,
email: partner.email,
country: partner.country,
});

partner.stripeConnectId = connectedAccount.id;

await prisma.partner.update({
where: { id: partner.id },
data: { stripeConnectId: connectedAccount.id },
});
}

const { url } = partner.payoutsEnabled
Expand Down
25 changes: 17 additions & 8 deletions apps/web/lib/actions/partners/onboard-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { storage } from "@/lib/storage";
import { createConnectedAccount } from "@/lib/stripe/create-connected-account";
import { onboardPartnerSchema } from "@/lib/zod/schemas/partners";
import { prisma } from "@dub/prisma";
import { Prisma } from "@dub/prisma/client";
import { CONNECT_SUPPORTED_COUNTRIES, nanoid } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { authUserActionClient } from "../safe-action";
Expand Down Expand Up @@ -45,13 +46,27 @@ export const onboardPartnerAction = authUserActionClient
.upload(`partners/${partnerId}/image_${nanoid(7)}`, image)
.then(({ url }) => url);

const payload = {
const payload: Prisma.PartnerCreateInput = {
name,
email: user.email,
country,
country: existingPartner?.country || country, // country cannot be changed once set
...(description && { description }),
image: imageUrl,
...(connectedAccount && { stripeConnectId: connectedAccount.id }),
users: {
connectOrCreate: {
where: {
userId_partnerId: {
userId: user.id,
partnerId: partnerId,
},
},
create: {
userId: user.id,
role: "owner",
},
},
},
};

await Promise.all([
Expand All @@ -66,12 +81,6 @@ export const onboardPartnerAction = authUserActionClient
data: {
id: partnerId,
...payload,
users: {
create: {
userId: user.id,
role: "owner" as const,
},
},
},
}),

Expand Down
6 changes: 3 additions & 3 deletions apps/web/lib/middleware/partners.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parse } from "@/lib/middleware/utils";
import { NextRequest, NextResponse } from "next/server";
import { getDefaultPartner } from "./utils/get-default-partner";
import { getDefaultPartnerId } from "./utils/get-default-partner";
import { getUserViaToken } from "./utils/get-user-via-token";

const AUTHENTICATED_PATHS = [
Expand Down Expand Up @@ -38,9 +38,9 @@ export default async function PartnersMiddleware(req: NextRequest) {
),
);
} else if (user) {
const defaultPartner = await getDefaultPartner(user);
const defaultPartnerId = await getDefaultPartnerId(user);

if (!defaultPartner && !path.startsWith("/onboarding")) {
if (!defaultPartnerId && !path.startsWith("/onboarding")) {
return NextResponse.redirect(new URL("/onboarding", req.url));
} else if (path === "/" || path.startsWith("/pn_")) {
return NextResponse.redirect(new URL("/programs", req.url));
Expand Down
37 changes: 36 additions & 1 deletion apps/web/lib/middleware/utils/get-default-partner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserProps } from "@/lib/types";
import { prismaEdge } from "@dub/prisma/edge";

export async function getDefaultPartner(user: UserProps) {
export async function getDefaultPartnerId(user: UserProps) {
let defaultPartnerId = user?.defaultPartnerId;

if (!defaultPartnerId) {
Expand All @@ -24,6 +24,41 @@ export async function getDefaultPartner(user: UserProps) {
refreshedUser?.defaultPartnerId ||
refreshedUser?.partners[0]?.partnerId ||
undefined;

// if no default partner id, try and see if there is a partner profile with the same email
// if there is, link the user to the partner profile and set it as the user's default partner id
if (!defaultPartnerId) {
console.log(
"User doesn't have a default partner id, trying to find a partner with the same email",
);

const partner = await prismaEdge.partner.findUnique({
where: {
email: user.email,
},
});

// if there is already a partner profile with the same email + has a country assigned
// link the user to the partner profile
// else they need to either create a new partner profile or set their country
if (partner?.country) {
await prismaEdge.user.update({
where: {
id: user.id,
},
data: {
defaultPartnerId: partner.id,
partners: {
create: {
partnerId: partner.id,
role: "owner",
},
},
},
});
defaultPartnerId = partner.id;
}
}
}

return defaultPartnerId;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/zod/schemas/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const PartnerSchema = z.object({
email: z.string().nullable(),
image: z.string().nullable(),
description: z.string().nullish(),
country: z.string(),
country: z.string().nullable(),
status: z.nativeEnum(PartnerStatus),
stripeConnectId: z.string().nullable(),
payoutsEnabled: z.boolean(),
Expand Down
16 changes: 7 additions & 9 deletions apps/web/ui/layout/sidebar/payout-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import StripeConnectButton from "@/ui/partners/stripe-connect-button";
import { AlertCircleFill } from "@/ui/shared/icons";
import { PayoutStatus } from "@dub/prisma/client";
import { AnimatedSizeContainer, MoneyBills2, Tooltip } from "@dub/ui";
import { CONNECT_SUPPORTED_COUNTRIES, currencyFormatter } from "@dub/utils";
import { currencyFormatter } from "@dub/utils";
import { ChevronRight } from "lucide-react";
import Link from "next/link";

Expand Down Expand Up @@ -77,14 +77,12 @@ export function PayoutStats() {
)}
</div>
</div>
{partner &&
!partner.payoutsEnabled &&
CONNECT_SUPPORTED_COUNTRIES.includes(partner.country) && (
<StripeConnectButton
className="mt-4 h-9 w-full"
text="Connect payouts"
/>
)}
{partner && !partner.payoutsEnabled && (
<StripeConnectButton
className="mt-4 h-9 w-full"
text="Connect payouts"
/>
)}
</div>
</AnimatedSizeContainer>
);
Expand Down
Loading
Loading