Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7450af
Referrals Embed link creation flow
devkiran Apr 3, 2025
67959c3
control the display of the fom or the table
devkiran Apr 3, 2025
754fc17
update link
devkiran Apr 3, 2025
0be6d3b
rename
devkiran Apr 3, 2025
5da573e
Update add-edit-link.tsx
devkiran Apr 3, 2025
1389485
add ReferralsEmbedLinkSchema
devkiran Apr 3, 2025
93181af
more cleanup
devkiran Apr 3, 2025
b59ff62
refresh the links after creating/updating
devkiran Apr 3, 2025
681a268
Update add-edit-link.tsx
devkiran Apr 3, 2025
8a688d3
Update links-list.tsx
devkiran Apr 3, 2025
9652efa
Update add-edit-link.tsx
devkiran Apr 3, 2025
2902256
Update route.ts
devkiran Apr 3, 2025
b89a9b5
Merge branch 'main' into referral-embed-link-creation
devkiran Apr 4, 2025
582e0ee
Merge branch 'main' into referral-embed-link-creation
devkiran Apr 28, 2025
7209af4
Update add-edit-link.tsx
devkiran Apr 28, 2025
b6f24e4
Update links-list.tsx
devkiran Apr 28, 2025
7db8e4c
Update route.ts
devkiran Apr 28, 2025
3e02452
Update route.ts
devkiran Apr 28, 2025
32cbfd9
send token via Header
devkiran Apr 28, 2025
e16e50c
default to the first owner user
devkiran Apr 28, 2025
7c96d3f
fix mobile auth buttons layout shift + mutatePrefix
steven-tey Apr 29, 2025
1a33603
include supportEmail
steven-tey Apr 29, 2025
1761c0a
Merge branch 'main' into referral-embed-link-creation
steven-tey Apr 29, 2025
7e8bc73
Dynamic embed host
steven-tey Apr 29, 2025
d7a0a78
bump versions
steven-tey Apr 29, 2025
00324ad
Merge branch 'embed-host' into referral-embed-link-creation
steven-tey Apr 29, 2025
0735628
remove console log
steven-tey Apr 29, 2025
99c96df
Update links-list.tsx
steven-tey Apr 29, 2025
6a8f6c4
order links by sale, leads, and clicks
steven-tey Apr 29, 2025
caac948
fix styles
steven-tey Apr 29, 2025
721a1de
fix formatting
steven-tey Apr 29, 2025
a8acf89
Merge pull request #2368 from dubinc/embed-host
steven-tey Apr 29, 2025
7775996
Merge branch 'main' into referral-embed-link-creation
steven-tey Apr 29, 2025
a091a07
Merge pull request #2255 from dubinc/referral-embed-link-creation
steven-tey Apr 29, 2025
67a500c
bump embed packages
steven-tey Apr 29, 2025
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: 3 additions & 3 deletions apps/web/app/api/embed/referrals/analytics/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { withReferralsEmbedToken } from "@/lib/embed/referrals/auth";
import { NextResponse } from "next/server";

// GET /api/embed/referrals/analytics – get timeseries analytics for a partner
export const GET = withReferralsEmbedToken(async ({ programId, partnerId }) => {
export const GET = withReferralsEmbedToken(async ({ programEnrollment }) => {
const analytics = await getAnalytics({
event: "composite",
groupBy: "timeseries",
interval: "1y",
programId,
partnerId,
programId: programEnrollment.programId,
partnerId: programEnrollment.partnerId,
});

return NextResponse.json(analytics);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/api/embed/referrals/earnings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { NextResponse } from "next/server";

// GET /api/embed/referrals/earnings – get commissions for a partner from an embed token
export const GET = withReferralsEmbedToken(
async ({ programId, partnerId, searchParams }) => {
async ({ programEnrollment, searchParams }) => {
const { page } = z
.object({ page: z.coerce.number().optional().default(1) })
.parse(searchParams);
Expand All @@ -17,8 +17,8 @@ export const GET = withReferralsEmbedToken(
earnings: {
gt: 0,
},
programId,
partnerId,
programId: programEnrollment.programId,
partnerId: programEnrollment.partnerId,
},
select: {
id: true,
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/api/embed/referrals/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NextResponse } from "next/server";
import z from "node_modules/zod/lib";

// GET /api/embed/referrals/leaderboard – get leaderboard for a program
export const GET = withReferralsEmbedToken(async ({ programId }) => {
export const GET = withReferralsEmbedToken(async ({ program }) => {
const partners = await prisma.$queryRaw`
SELECT
p.id,
Expand All @@ -26,11 +26,11 @@ export const GET = withReferralsEmbedToken(async ({ programId }) => {
SUM(sales) as totalSales,
SUM(saleAmount) as totalSaleAmount
FROM Link
WHERE programId = ${programId}
WHERE programId = ${program.id}
GROUP BY partnerId
) metrics ON metrics.partnerId = pe.partnerId
WHERE
pe.programId = ${programId}
pe.programId = ${program.id}
AND pe.status = 'approved'
ORDER BY
totalSaleAmount DESC,
Expand Down
91 changes: 91 additions & 0 deletions apps/web/app/api/embed/referrals/links/[linkId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { DubApiError, ErrorCodes } from "@/lib/api/errors";
import { processLink, updateLink } from "@/lib/api/links";
import { parseRequestBody } from "@/lib/api/utils";
import { withReferralsEmbedToken } from "@/lib/embed/referrals/auth";
import { createPartnerLinkSchema } from "@/lib/zod/schemas/partners";
import { ReferralsEmbedLinkSchema } from "@/lib/zod/schemas/referrals-embed";
import { getApexDomain } from "@dub/utils";
import { NextResponse } from "next/server";

// PATCH /api/embed/referrals/links/[linkId] - update a link for a partner
export const PATCH = withReferralsEmbedToken(
async ({ req, params, programEnrollment, program, links }) => {
const { url, key } = createPartnerLinkSchema
.pick({ url: true, key: true })
.parse(await parseRequestBody(req));

if (programEnrollment.status === "banned") {
throw new DubApiError({
code: "forbidden",
message: "You are banned from this program hence cannot create links.",
});
}

const link = links.find((link) => link.id === params.linkId);

if (!link) {
throw new DubApiError({
code: "not_found",
message: "Link not found.",
});
}

if (!program.domain || !program.url) {
throw new DubApiError({
code: "bad_request",
message:
"This program needs a domain and URL set before creating a link.",
});
}

if (url && getApexDomain(url) !== getApexDomain(program.url)) {
throw new DubApiError({
code: "bad_request",
message: `The provided URL domain (${getApexDomain(url)}) does not match the program's domain (${getApexDomain(program.url)}).`,
});
}

// if domain and key are the same, we don't need to check if the key exists
const skipKeyChecks = link.key.toLowerCase() === key?.toLowerCase();

const {
link: processedLink,
error,
code,
} = await processLink({
// @ts-expect-error
payload: {
...link,
key: key || undefined,
url: url || program.url,
},
workspace: {
id: program.workspaceId,
plan: "business",
},
userId: link.userId!,
skipKeyChecks,
skipFolderChecks: true, // can't be changed by the partner
skipProgramChecks: true, // can't be changed by the partner
skipExternalIdChecks: true, // can't be changed by the partner
});

if (error != null) {
throw new DubApiError({
code: code as ErrorCodes,
message: error,
});
}

const partnerLink = await updateLink({
oldLink: {
domain: link.domain,
key: link.key,
image: link.image,
},
updatedLink: processedLink,
});

return NextResponse.json(ReferralsEmbedLinkSchema.parse(partnerLink));
},
);
99 changes: 99 additions & 0 deletions apps/web/app/api/embed/referrals/links/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { DubApiError, ErrorCodes } from "@/lib/api/errors";
import { createLink, processLink } from "@/lib/api/links";
import { parseRequestBody } from "@/lib/api/utils";
import { PARTNER_LINKS_LIMIT } from "@/lib/embed/constants";
import { withReferralsEmbedToken } from "@/lib/embed/referrals/auth";
import { createPartnerLinkSchema } from "@/lib/zod/schemas/partners";
import { ReferralsEmbedLinkSchema } from "@/lib/zod/schemas/referrals-embed";
import { prisma } from "@dub/prisma";
import { getApexDomain } from "@dub/utils";
import { NextResponse } from "next/server";

// GET /api/embed/referrals/links – get links for a partner
export const GET = withReferralsEmbedToken(async ({ links }) => {
const partnerLinks = ReferralsEmbedLinkSchema.array().parse(links);

return NextResponse.json(partnerLinks);
});

// POST /api/embed/referrals/links – create links for a partner
export const POST = withReferralsEmbedToken(
async ({ req, programEnrollment, program, links }) => {
const { url, key } = createPartnerLinkSchema
.pick({ url: true, key: true })
.parse(await parseRequestBody(req));

if (programEnrollment.status === "banned") {
throw new DubApiError({
code: "forbidden",
message: "You are banned from this program hence cannot create links.",
});
}

if (!program.domain || !program.url) {
throw new DubApiError({
code: "bad_request",
message:
"This program needs a domain and URL set before creating a link.",
});
}

if (links.length >= PARTNER_LINKS_LIMIT) {
throw new DubApiError({
code: "bad_request",
message: `You have reached the limit of ${PARTNER_LINKS_LIMIT} program links.`,
});
}

if (url && getApexDomain(url) !== getApexDomain(program.url)) {
throw new DubApiError({
code: "bad_request",
message: `The provided URL domain (${getApexDomain(url)}) does not match the program's domain (${getApexDomain(program.url)}).`,
});
}

const workspaceOwner = await prisma.projectUsers.findFirst({
where: {
projectId: program.workspaceId,
role: "owner",
},
orderBy: {
createdAt: "desc",
},
});

const { link, error, code } = await processLink({
payload: {
key: key || undefined,
url: url || program.url,
domain: program.domain,
programId: program.id,
folderId: program.defaultFolderId,
tenantId: programEnrollment.tenantId,
partnerId: programEnrollment.partnerId,
trackConversion: true,
},
workspace: {
id: program.workspaceId,
plan: "business",
},
userId: workspaceOwner?.userId,
skipFolderChecks: true, // can't be changed by the partner
skipProgramChecks: true, // can't be changed by the partner
skipExternalIdChecks: true, // can't be changed by the partner
});

if (error != null) {
throw new DubApiError({
code: code as ErrorCodes,
message: error,
});
}

const partnerLink = await createLink(link);

return NextResponse.json(ReferralsEmbedLinkSchema.parse(partnerLink), {
status: 201,
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { createLink, processLink } from "@/lib/api/links";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { withPartnerProfile } from "@/lib/auth/partner";
import { PARTNER_LINKS_LIMIT } from "@/lib/embed/constants";
import { PartnerProfileLinkSchema } from "@/lib/zod/schemas/partner-profile";
import { createPartnerLinkSchema } from "@/lib/zod/schemas/partners";
import { getApexDomain } from "@dub/utils";
import { NextResponse } from "next/server";

const PARTNER_LINKS_LIMIT = 5;

// GET /api/partner-profile/programs/[programId]/links - get a partner's links in a program
export const GET = withPartnerProfile(async ({ partner, params }) => {
const { links } = await getProgramEnrollmentOrThrow({
Expand Down Expand Up @@ -80,7 +79,7 @@ export const POST = withPartnerProfile(
id: program.workspaceId,
plan: "business",
},
userId: session.user.id,
userId: session.user.id, // TODO: Hm, this is the partner user, not the workspace user?
skipFolderChecks: true, // can't be changed by the partner
skipProgramChecks: true, // can't be changed by the partner
skipExternalIdChecks: true, // can't be changed by the partner
Expand Down
12 changes: 8 additions & 4 deletions apps/web/app/app.dub.co/embed/referrals/activity.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CursorRays } from "@/ui/layout/sidebar/icons/cursor-rays";
import { InfoTooltip, MiniAreaChart } from "@dub/ui";
import { cn, currencyFormatter, nFormatter } from "@dub/utils";
import { fetcher } from "@dub/utils/src/functions";
import { cn, currencyFormatter, fetcher, nFormatter } from "@dub/utils";
import { useEmbedToken } from "app/app.dub.co/embed/use-embed-token";
import { AnalyticsTimeseries } from "dub/models/components";
import { SVGProps, useId } from "react";
Expand All @@ -22,8 +21,13 @@ export function ReferralsEmbedActivity({

const isEmpty = clicks === 0 && leads === 0 && sales === 0;
const { data: analytics } = useSWR<AnalyticsTimeseries[]>(
!isEmpty && `/api/embed/referrals/analytics?token=${token}`,
fetcher,
!isEmpty && "/api/embed/referrals/analytics",
(url) =>
fetcher(url, {
headers: {
Authorization: `Bearer ${token}`,
},
}),
{
keepPreviousData: true,
dedupingInterval: 60000,
Expand Down
Loading
Loading