Skip to content
7 changes: 6 additions & 1 deletion apps/web/app/api/customers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ export const PATCH = withWorkspace(
include: {
link: {
include: {
programEnrollment: true,
programEnrollment: {
include: {
partner: true,
discount: true,
},
},
},
},
},
Expand Down
7 changes: 0 additions & 7 deletions apps/web/app/api/customers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,6 @@ export const POST = withWorkspace(
projectId: workspace.id,
projectConnectId: workspace.stripeConnectId,
},
include: {
link: {
include: {
programEnrollment: true,
},
},
},
});

return NextResponse.json(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default function ProgramOverviewPageClient() {
<p className="relative text-xl text-white">
<ProgramCommissionDescription
program={program}
discount={program.discounts?.[0]}
amountClassName="text-blue-400 font-medium"
periodClassName="text-white font-medium"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { EmbedDocsSheet } from "@/ui/partners/embed-docs-sheet";
import { ProgramCommissionDescription } from "@/ui/partners/program-commission-description";
import { AnimatedSizeContainer, Button } from "@dub/ui";
import { CircleCheckFill, Code, LoadingSpinner } from "@dub/ui/icons";
import { cn, pluralize } from "@dub/utils";
import { cn, INFINITY_NUMBER, pluralize } from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import { useState } from "react";
import {
Expand Down Expand Up @@ -52,11 +52,10 @@ export function ProgramSettings() {

type FormData = Pick<
ProgramProps,
| "recurringCommission"
| "recurringDuration"
| "isLifetimeRecurring"
| "commissionType"
| "commissionAmount"
| "commissionType"
| "commissionDuration"
| "commissionInterval"
>;

function ProgramSettingsForm({ program }: { program: ProgramProps }) {
Expand All @@ -66,10 +65,7 @@ function ProgramSettingsForm({ program }: { program: ProgramProps }) {
const form = useForm<FormData>({
mode: "onBlur",
defaultValues: {
recurringCommission: program.recurringCommission,
recurringDuration: program.recurringDuration,
isLifetimeRecurring: program.isLifetimeRecurring,
commissionType: program.commissionType,
...program,
commissionAmount:
program.commissionType === "flat"
? program.commissionAmount / 100
Expand All @@ -87,15 +83,15 @@ function ProgramSettingsForm({ program }: { program: ProgramProps }) {
} = form;

const [
recurringCommission,
recurringDuration,
isLifetimeRecurring,
commissionAmount,
commissionType,
commissionDuration,
commissionInterval,
] = watch([
"recurringCommission",
"recurringDuration",
"isLifetimeRecurring",
"commissionAmount",
"commissionType",
"commissionDuration",
"commissionInterval",
]);

const { executeAsync } = useAction(updateProgramAction, {
Expand Down Expand Up @@ -157,62 +153,67 @@ function ProgramSettingsForm({ program }: { program: ProgramProps }) {
>
<div className="p-1">
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{commissionTypes.map((commissionType) => (
<label
key={commissionType.label}
className={cn(
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
recurringCommission === commissionType.recurring &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
>
<input
type="radio"
value={commissionType.label}
className="hidden"
checked={
recurringCommission === commissionType.recurring
}
onChange={(e) => {
if (e.target.checked) {
setValue(
"recurringCommission",
commissionType.recurring,
{ shouldDirty: true },
);
{commissionTypes.map((commissionType) => {
const isSelected =
commissionDuration &&
commissionDuration > 1 === commissionType.recurring
? true
: false;

// If not recurring, set lifetime recurring to false
if (!commissionType.recurring)
setValue("isLifetimeRecurring", false, {
shouldDirty: true,
});
}
}}
/>
<div className="flex grow flex-col text-sm">
<span className="font-medium">
{commissionType.label}
</span>
<span>{commissionType.description}</span>
</div>
<CircleCheckFill
return (
<label
key={commissionType.label}
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
recurringCommission === commissionType.recurring &&
"scale-100 opacity-100",
"relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50",
"transition-all duration-150",
isSelected &&
"border-black bg-neutral-50 text-neutral-900 ring-1 ring-black",
)}
/>
</label>
))}
>
<input
type="radio"
value={commissionType.label}
className="hidden"
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
setValue(
"commissionDuration",
commissionType.recurring ? 12 : 1,
{ shouldDirty: true },
);
}
}}
/>
<div className="flex grow flex-col text-sm">
<span className="font-medium">
{commissionType.label}
</span>
<span>{commissionType.description}</span>
</div>
<CircleCheckFill
className={cn(
"-mr-px -mt-px flex size-4 scale-75 items-center justify-center rounded-full opacity-0 transition-[transform,opacity] duration-150",
isSelected && "scale-100 opacity-100",
)}
/>
</label>
);
})}
</div>
<div
className={cn(
"transition-opacity duration-200",
recurringCommission ? "h-auto" : "h-0 opacity-0",
commissionDuration && commissionDuration > 1
? "h-auto"
: "h-0 opacity-0",
)}
aria-hidden={!recurringCommission}
{...{ inert: !recurringCommission ? "" : undefined }}
aria-hidden={
!(commissionDuration && commissionDuration > 1)
}
{...{
inert: !(commissionDuration && commissionDuration > 1),
}}
>
<div className="pt-6">
<label
Expand All @@ -221,42 +222,26 @@ function ProgramSettingsForm({ program }: { program: ProgramProps }) {
>
Duration
</label>
<div className="relative mt-2 rounded-md shadow-sm">
<div className="relati`ve mt-2 rounded-md shadow-sm">
<select
className="block w-full rounded-md border-neutral-300 text-neutral-900 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
onChange={(e) => {
const value = parseInt(e.target.value);

if (value === 0)
setValue("isLifetimeRecurring", true, {
shouldDirty: true,
});
else if (isLifetimeRecurring)
setValue("isLifetimeRecurring", false, {
shouldDirty: true,
});

setValue("recurringDuration", value, {
setValue("commissionDuration", value, {
shouldDirty: true,
});
}}
value={
(isLifetimeRecurring ? 0 : recurringDuration) ?? 1
}
value={commissionDuration ?? 1}
>
{(program.recurringInterval === "year"
{(commissionInterval === "year"
? [1, 2]
: [1, 3, 6, 12, 18, 24]
).map((v) => (
<option value={v} key={v}>
{v}{" "}
{pluralize(
program.recurringInterval ?? "month",
v,
)}
{v} {pluralize(commissionInterval ?? "month", v)}
</option>
))}
<option value={0}>Lifetime</option>
<option value={INFINITY_NUMBER}>Lifetime</option>
</select>
</div>
</div>
Expand Down Expand Up @@ -349,7 +334,7 @@ function Summary({ program }: { program: ProgramProps }) {
...program,
commissionAmount:
program.commissionType === "flat"
? program.commissionAmount / 100
? program.commissionAmount * 100
: program.commissionAmount,
},
}) as FormData;
Expand All @@ -365,7 +350,6 @@ function Summary({ program }: { program: ProgramProps }) {
<ProgramCommissionDescription
program={{
...data,
recurringInterval: program.recurringInterval,
commissionAmount:
(data.commissionType === "flat"
? data.commissionAmount * 100
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/app.dub.co/embed/inline/faq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function EmbedFAQ({ program }: { program: Program }) {
const items = [
{
title: `What is the ${program.name} Affiliate Program`,
content: `The ${program.name} Affiliate Program is a way for you to earn money by referring new customers to ${program.name}. For each new customer you refer, you'll earn a ${program.commissionAmount}% commission on their subscription for up to ${program.recurringDuration} ${program.recurringInterval}s. There are no limits to how much you can earn.`,
content: `The ${program.name} Affiliate Program is a way for you to earn money by referring new customers to ${program.name}. For each new customer you refer, you'll earn a ${program.commissionAmount}% commission on their subscription for up to ${program.commissionDuration} ${program.commissionInterval}s. There are no limits to how much you can earn.`,
},
{
title: "What counts as a successful conversion?",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/app/app.dub.co/embed/inline/page-client.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { DiscountProps } from "@/lib/types";
import { ProgramCommissionDescription } from "@/ui/partners/program-commission-description";
import { Link, Program } from "@dub/prisma/client";
import {
Expand All @@ -26,9 +27,11 @@ import { EmbedSales } from "./sales";
export function EmbedInlinePageClient({
program,
link,
discount,
}: {
program: Program;
link: Link;
discount?: DiscountProps | null;
}) {
const [copied, copyToClipboard] = useCopyToClipboard();

Expand All @@ -50,7 +53,10 @@ export function EmbedInlinePageClient({
Refer and earn
</span>
<div className="relative mt-16 text-lg text-neutral-900 sm:max-w-[50%]">
<ProgramCommissionDescription program={program} />
<ProgramCommissionDescription
program={program}
discount={discount}
/>
</div>
<span className="mb-1.5 mt-6 block text-sm text-neutral-800">
Referral link
Expand Down
6 changes: 4 additions & 2 deletions apps/web/app/app.dub.co/embed/inline/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export default async function EmbedInlinePage({
}) {
const { token } = searchParams;

const { link, program } = await getEmbedData(token);
const { link, program, discount } = await getEmbedData(token);

return <EmbedInlinePageClient program={program} link={link} />;
return (
<EmbedInlinePageClient program={program} link={link} discount={discount} />
);
}
15 changes: 5 additions & 10 deletions apps/web/app/app.dub.co/embed/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { embedToken } from "@/lib/embed/embed-token";
import { DiscountSchema } from "@/lib/zod/schemas/discount";
import { prisma } from "@dub/prisma";
import { notFound } from "next/navigation";

Expand All @@ -17,11 +18,7 @@ export const getEmbedData = async (token: string) => {
program: true,
programEnrollment: {
select: {
partner: {
select: {
users: true,
},
},
discount: true,
},
},
},
Expand All @@ -39,12 +36,10 @@ export const getEmbedData = async (token: string) => {

return {
program,
// check if the user has an active profile on Dub Partners
hasPartnerProfile:
programEnrollment && programEnrollment.partner.users.length > 0
? true
: false,
link,
discount: programEnrollment?.discount
? DiscountSchema.parse(programEnrollment?.discount)
: null,
earnings:
(program.commissionType === "percentage" ? link.saleAmount : link.sales) *
(program.commissionAmount / 100),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Program } from "@dub/prisma/client";
import { Calendar6, MoneyBills2 } from "@dub/ui/icons";
import { cn, currencyFormatter } from "@dub/utils";
import { cn, currencyFormatter, INFINITY_NUMBER } from "@dub/utils";

export function DetailsGrid({
program,
Expand All @@ -26,9 +26,10 @@ export function DetailsGrid({
{
icon: Calendar6,
title: "Duration",
value: program.isLifetimeRecurring
? "Lifetime"
: `${program.recurringDuration} ${program.recurringInterval}s`,
value:
program.commissionDuration === INFINITY_NUMBER
? "Lifetime"
: `${program.commissionDuration} ${program.commissionInterval}s`,
},
].map(({ icon: Icon, title, value }) => (
<div className="rounded-xl bg-neutral-100 p-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ export default function ProgramPageClient() {
</span>
<div className="relative mt-24 text-lg text-neutral-900 sm:max-w-[50%]">
{program ? (
<ProgramCommissionDescription program={program} />
<ProgramCommissionDescription
program={program}
discount={programEnrollment?.discount}
/>
) : (
<div className="h-7 w-5/6 animate-pulse rounded-md bg-neutral-200" />
)}
Expand Down
Loading
Loading