From 6596e3945d394d68e61c5b58a19477a1e7965296 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 30 Sep 2024 12:27:35 +0530 Subject: [PATCH 01/88] create public token --- apps/web/app/api/referrals/tokens/route.ts | 47 ++++++++++++++++++++++ apps/web/lib/referrals/constants.ts | 4 ++ apps/web/lib/zod/schemas/referrals.ts | 10 +++++ apps/web/prisma/schema/link.prisma | 2 + apps/web/prisma/schema/referral.prisma | 12 ++++++ 5 files changed, 75 insertions(+) create mode 100644 apps/web/app/api/referrals/tokens/route.ts create mode 100644 apps/web/lib/zod/schemas/referrals.ts create mode 100644 apps/web/prisma/schema/referral.prisma diff --git a/apps/web/app/api/referrals/tokens/route.ts b/apps/web/app/api/referrals/tokens/route.ts new file mode 100644 index 0000000000..8643ad9046 --- /dev/null +++ b/apps/web/app/api/referrals/tokens/route.ts @@ -0,0 +1,47 @@ +import { DubApiError } from "@/lib/api/errors"; +import { parseRequestBody } from "@/lib/api/utils"; +import { withWorkspace } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { + REFERRAL_PUBLIC_TOKEN_EXPIRY, + REFERRAL_PUBLIC_TOKEN_LENGTH, +} from "@/lib/referrals/constants"; +import { + createReferralTokenSchema, + referralTokenSchema, +} from "@/lib/zod/schemas/referrals"; +import { nanoid } from "@dub/utils"; +import { NextResponse } from "next/server"; + +// GET /api/referrals/tokens - create a new referral token for the given link +export const POST = withWorkspace(async ({ workspace, req }) => { + const { linkId } = createReferralTokenSchema.parse( + await parseRequestBody(req), + ); + + const link = await prisma.link.findUniqueOrThrow({ + where: { + id: linkId, + projectId: workspace.id, + }, + }); + + if (!link.trackConversion) { + throw new DubApiError({ + code: "forbidden", + message: "Conversion tracking is not enabled for this link.", + }); + } + + const referralToken = await prisma.referralPublicToken.create({ + data: { + linkId, + expires: new Date(Date.now() + REFERRAL_PUBLIC_TOKEN_EXPIRY), + publicToken: nanoid(REFERRAL_PUBLIC_TOKEN_LENGTH), + }, + }); + + return NextResponse.json(referralTokenSchema.parse(referralToken), { + status: 201, + }); +}); diff --git a/apps/web/lib/referrals/constants.ts b/apps/web/lib/referrals/constants.ts index c5793bb0b2..7bda65072e 100644 --- a/apps/web/lib/referrals/constants.ts +++ b/apps/web/lib/referrals/constants.ts @@ -2,3 +2,7 @@ export const REFERRAL_SIGNUPS_MAX = 32; export const REFERRAL_CLICKS_QUOTA_BONUS = 500; export const REFERRAL_CLICKS_QUOTA_BONUS_MAX = 16000; export const REFERRAL_REVENUE_SHARE = 0.2; + +// Referral public token +export const REFERRAL_PUBLIC_TOKEN_LENGTH = 36; +export const REFERRAL_PUBLIC_TOKEN_EXPIRY = 1000 * 60 * 60 * 2; // 2 hours diff --git a/apps/web/lib/zod/schemas/referrals.ts b/apps/web/lib/zod/schemas/referrals.ts new file mode 100644 index 0000000000..27bc116e1a --- /dev/null +++ b/apps/web/lib/zod/schemas/referrals.ts @@ -0,0 +1,10 @@ +import z from "@/lib/zod"; + +export const createReferralTokenSchema = z.object({ + linkId: z.string().min(1), +}); + +export const referralTokenSchema = z.object({ + publicToken: z.string(), + expires: z.date(), +}); diff --git a/apps/web/prisma/schema/link.prisma b/apps/web/prisma/schema/link.prisma index fd921135f6..ce45a28bed 100644 --- a/apps/web/prisma/schema/link.prisma +++ b/apps/web/prisma/schema/link.prisma @@ -62,6 +62,8 @@ model Link { // Comments on the particular shortlink comments String? @db.LongText + referralPublicTokens ReferralPublicToken[] + @@unique([domain, key]) @@unique([projectId, externalId]) @@index(projectId) diff --git a/apps/web/prisma/schema/referral.prisma b/apps/web/prisma/schema/referral.prisma new file mode 100644 index 0000000000..93aef82b67 --- /dev/null +++ b/apps/web/prisma/schema/referral.prisma @@ -0,0 +1,12 @@ +model ReferralPublicToken { + id String @id @default(cuid()) + linkId String + publicToken String @unique + expires DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + link Link @relation(fields: [linkId], references: [id], onDelete: Cascade) + + @@index([linkId]) +} From a7df9906c0bab5a23226838fc52b7ef6b7aa63ea Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 30 Sep 2024 22:06:03 +0530 Subject: [PATCH 02/88] add events and analytics --- apps/web/app/api/analytics/client/route.ts | 17 +++ apps/web/app/api/events/client/route.ts | 17 +++ apps/web/lib/referrals/auth.ts | 136 +++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 apps/web/app/api/analytics/client/route.ts create mode 100644 apps/web/app/api/events/client/route.ts create mode 100644 apps/web/lib/referrals/auth.ts diff --git a/apps/web/app/api/analytics/client/route.ts b/apps/web/app/api/analytics/client/route.ts new file mode 100644 index 0000000000..3335215dd6 --- /dev/null +++ b/apps/web/app/api/analytics/client/route.ts @@ -0,0 +1,17 @@ +import { getAnalytics } from "@/lib/analytics/get-analytics"; +import { withAuth } from "@/lib/referrals/auth"; +import { analyticsQuerySchema } from "@/lib/zod/schemas/analytics"; +import { NextResponse } from "next/server"; + +// GET /api/analytics/client - get analytics for the current link +export const GET = withAuth(async ({ workspace, link, searchParams }) => { + const parsedParams = analyticsQuerySchema.parse(searchParams); + + const response = await getAnalytics({ + ...parsedParams, + linkId: link.id, + workspaceId: workspace.id, + }); + + return NextResponse.json(response); +}); diff --git a/apps/web/app/api/events/client/route.ts b/apps/web/app/api/events/client/route.ts new file mode 100644 index 0000000000..0a771e2642 --- /dev/null +++ b/apps/web/app/api/events/client/route.ts @@ -0,0 +1,17 @@ +import { getEvents } from "@/lib/analytics/get-events"; +import { withAuth } from "@/lib/referrals/auth"; +import { eventsQuerySchema } from "@/lib/zod/schemas/analytics"; +import { NextResponse } from "next/server"; + +// GET /api/events/client - get events for the current link +export const GET = withAuth(async ({ searchParams, workspace, link }) => { + const parsedParams = eventsQuerySchema.parse(searchParams); + + const response = await getEvents({ + ...parsedParams, + linkId: link.id, + workspaceId: workspace.id, + }); + + return NextResponse.json(response); +}); diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts new file mode 100644 index 0000000000..8ac0d0c0ba --- /dev/null +++ b/apps/web/lib/referrals/auth.ts @@ -0,0 +1,136 @@ +import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors"; +import { prisma } from "@/lib/prisma"; +import { ratelimit } from "@/lib/upstash"; +import { getSearchParams } from "@dub/utils"; +import { Link, Project } from "@prisma/client"; +import { AxiomRequest, withAxiom } from "next-axiom"; + +interface WithAuthHandler { + ({ + req, + params, + searchParams, + workspace, + link, + }: { + req: Request; + params: Record; + searchParams: Record; + workspace: Project; + link: Link; + }): Promise; +} + +export const withAuth = (handler: WithAuthHandler) => { + return withAxiom( + async ( + req: AxiomRequest, + { params = {} }: { params: Record | undefined }, + ) => { + let headers = {}; + + try { + let link: Link | undefined = undefined; + let publicToken: string | undefined = undefined; + + const rateLimit = 100; + const searchParams = getSearchParams(req.url); + const authorizationHeader = req.headers.get("Authorization"); + + if (!authorizationHeader) { + throw new DubApiError({ + code: "unauthorized", + message: "Missing Authorization header.", + }); + } + + if (!authorizationHeader.includes("Bearer ")) { + throw new DubApiError({ + code: "bad_request", + message: + "Misconfigured authorization header. Did you forget to add 'Bearer '? Learn more: https://d.to/auth", + }); + } + + publicToken = authorizationHeader.replace("Bearer ", ""); + + if (!publicToken) { + throw new DubApiError({ + code: "unauthorized", + message: "Missing Authorization header.", + }); + } + + const referralToken = + await prisma.referralPublicToken.findUniqueOrThrow({ + where: { + publicToken, + }, + }); + + if (referralToken.expires < new Date()) { + throw new DubApiError({ + code: "unauthorized", + message: "Public token expired.", + }); + } + + link = await prisma.link.findUniqueOrThrow({ + where: { + id: referralToken.linkId, + }, + }); + + if (!link.trackConversion) { + throw new DubApiError({ + code: "forbidden", + message: "Conversion tracking is not enabled for this link.", + }); + } + + const workspace = await prisma.project.findUniqueOrThrow({ + where: { + id: link.projectId!, + }, + }); + + const { success, limit, reset, remaining } = await ratelimit( + rateLimit, + "1 m", + ).limit(publicToken); + + if (!success) { + throw new DubApiError({ + code: "rate_limit_exceeded", + message: "Too many requests.", + }); + } + + headers = { + "Retry-After": reset.toString(), + "X-RateLimit-Limit": limit.toString(), + "X-RateLimit-Remaining": remaining.toString(), + "X-RateLimit-Reset": reset.toString(), + }; + + if (!success) { + throw new DubApiError({ + code: "rate_limit_exceeded", + message: "Too many requests.", + }); + } + + return await handler({ + req, + params, + searchParams, + workspace, + link, + }); + } catch (error) { + req.log.error(error); + return handleAndReturnErrorResponse(error, headers); + } + }, + ); +}; From 816c8fe2ee5e1d99f6b303f4dc56f850937208e1 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 30 Sep 2024 22:24:10 +0530 Subject: [PATCH 03/88] update --- apps/web/lib/referrals/auth.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts index 8ac0d0c0ba..63c8924bae 100644 --- a/apps/web/lib/referrals/auth.ts +++ b/apps/web/lib/referrals/auth.ts @@ -61,12 +61,18 @@ export const withAuth = (handler: WithAuthHandler) => { }); } - const referralToken = - await prisma.referralPublicToken.findUniqueOrThrow({ - where: { - publicToken, - }, + const referralToken = await prisma.referralPublicToken.findUnique({ + where: { + publicToken, + }, + }); + + if (!referralToken) { + throw new DubApiError({ + code: "unauthorized", + message: "Invalid public token.", }); + } if (referralToken.expires < new Date()) { throw new DubApiError({ @@ -99,13 +105,6 @@ export const withAuth = (handler: WithAuthHandler) => { "1 m", ).limit(publicToken); - if (!success) { - throw new DubApiError({ - code: "rate_limit_exceeded", - message: "Too many requests.", - }); - } - headers = { "Retry-After": reset.toString(), "X-RateLimit-Limit": limit.toString(), From 8a8c4b2d8f64b262f27d675d965cbb0b7dc206e1 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Tue, 1 Oct 2024 20:13:42 +0530 Subject: [PATCH 04/88] rearrange --- apps/web/lib/referrals/auth.ts | 54 +++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts index 63c8924bae..cebd5ac7fc 100644 --- a/apps/web/lib/referrals/auth.ts +++ b/apps/web/lib/referrals/auth.ts @@ -31,7 +31,7 @@ export const withAuth = (handler: WithAuthHandler) => { try { let link: Link | undefined = undefined; - let publicToken: string | undefined = undefined; + let tokenFromHeader: string | undefined = undefined; const rateLimit = 100; const searchParams = getSearchParams(req.url); @@ -52,58 +52,39 @@ export const withAuth = (handler: WithAuthHandler) => { }); } - publicToken = authorizationHeader.replace("Bearer ", ""); + tokenFromHeader = authorizationHeader.replace("Bearer ", ""); - if (!publicToken) { + if (!tokenFromHeader) { throw new DubApiError({ code: "unauthorized", message: "Missing Authorization header.", }); } - const referralToken = await prisma.referralPublicToken.findUnique({ + const publicToken = await prisma.referralPublicToken.findUnique({ where: { - publicToken, + publicToken: tokenFromHeader, }, }); - if (!referralToken) { + if (!publicToken) { throw new DubApiError({ code: "unauthorized", message: "Invalid public token.", }); } - if (referralToken.expires < new Date()) { + if (publicToken.expires < new Date()) { throw new DubApiError({ code: "unauthorized", message: "Public token expired.", }); } - link = await prisma.link.findUniqueOrThrow({ - where: { - id: referralToken.linkId, - }, - }); - - if (!link.trackConversion) { - throw new DubApiError({ - code: "forbidden", - message: "Conversion tracking is not enabled for this link.", - }); - } - - const workspace = await prisma.project.findUniqueOrThrow({ - where: { - id: link.projectId!, - }, - }); - const { success, limit, reset, remaining } = await ratelimit( rateLimit, "1 m", - ).limit(publicToken); + ).limit(tokenFromHeader); headers = { "Retry-After": reset.toString(), @@ -119,6 +100,25 @@ export const withAuth = (handler: WithAuthHandler) => { }); } + link = await prisma.link.findUniqueOrThrow({ + where: { + id: publicToken.linkId, + }, + }); + + if (!link.trackConversion) { + throw new DubApiError({ + code: "forbidden", + message: "Conversion tracking is not enabled for this link.", + }); + } + + const workspace = await prisma.project.findUniqueOrThrow({ + where: { + id: link.projectId!, + }, + }); + return await handler({ req, params, From 9bc3e7d16cde391f76e5342ff9942cb57441d2cd Mon Sep 17 00:00:00 2001 From: Kiran K Date: Wed, 2 Oct 2024 00:12:15 +0530 Subject: [PATCH 05/88] wip --- .../{activity-list.tsx => events.tsx} | 14 ++---- .../settings/referrals/placeholders.tsx | 50 +++++++++++++++++++ packages/blocks/package.json | 3 +- packages/blocks/src/hooks/use-analytics.ts | 12 +++++ packages/blocks/src/hooks/use-events.ts | 12 +++++ packages/blocks/src/index.ts | 2 + 6 files changed, 83 insertions(+), 10 deletions(-) rename apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/{activity-list.tsx => events.tsx} (97%) create mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/placeholders.tsx create mode 100644 packages/blocks/src/hooks/use-analytics.ts create mode 100644 packages/blocks/src/hooks/use-events.ts diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/activity-list.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx similarity index 97% rename from apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/activity-list.tsx rename to apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx index a92c79b012..50f37bf588 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/activity-list.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx @@ -20,15 +20,11 @@ import { } from "dub/dist/commonjs/models/components"; import { useSearchParams } from "next/navigation"; -export function ActivityList({ - events, - totalEvents, - demo, -}: { - events: ConversionEvent[]; - totalEvents: number; - demo?: boolean; -}) { +interface EventsProps { + // +} + +export const Events = (props: EventsProps) => { const searchParams = useSearchParams(); const event = (searchParams.get("event") || "clicks") as EventType; diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/placeholders.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/placeholders.tsx new file mode 100644 index 0000000000..5ebaa1a852 --- /dev/null +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/placeholders.tsx @@ -0,0 +1,50 @@ +import { randomValue } from "@dub/utils"; +import { subDays } from "date-fns"; + +export const placeholderEvents = { + clicks: [...Array(10)].map( + (_, idx) => + ({ + timestamp: subDays(new Date(), idx).toISOString(), + click_id: "1", + link_id: "1", + domain: "refer.dub.co", + key: "", + url: "https://dub.co", + country: randomValue(["US", "GB", "CA", "AU", "DE", "FR", "ES", "IT"]), + }) as any, + ), + + leads: [...Array(10)].map( + (_, idx) => + ({ + timestamp: subDays(new Date(), idx).toISOString(), + click_id: "1", + link_id: "1", + domain: "refer.dub.co", + key: "", + url: "https://dub.co", + country: randomValue(["US", "GB", "CA", "AU", "DE", "FR", "ES", "IT"]), + }) as any, + ), + + sales: [...Array(10)].map( + (_, idx) => + ({ + timestamp: subDays(new Date(), idx).toISOString(), + click_id: "1", + link_id: "1", + domain: "refer.dub.co", + key: "", + url: "https://dub.co", + country: randomValue(["US", "GB", "CA", "AU", "DE", "FR", "ES", "IT"]), + event_name: [ + "Subscription creation", + "Subscription paid", + "Plan upgraded", + ][idx % 3], + // TODO update to saleAmount + amount: [1100, 4900, 2400][idx % 3], + }) as any, + ), +}; diff --git a/packages/blocks/package.json b/packages/blocks/package.json index f366666dde..2872efa211 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -43,7 +43,8 @@ "@visx/scale": "^3.3.0", "@visx/shape": "^2.12.2", "class-variance-authority": "^0.7.0", - "framer-motion": "^10.16.16" + "framer-motion": "^10.16.16", + "swr": "^2.1.5" }, "author": "Steven Tey ", "homepage": "https://dub.co", diff --git a/packages/blocks/src/hooks/use-analytics.ts b/packages/blocks/src/hooks/use-analytics.ts new file mode 100644 index 0000000000..9bd91503ed --- /dev/null +++ b/packages/blocks/src/hooks/use-analytics.ts @@ -0,0 +1,12 @@ +import { fetcher } from "@dub/utils"; +import useSWR from "swr"; + +export const useAnalytics = () => { + const { error, data, isLoading } = useSWR(`/api/analytics/client`, fetcher); + + return { + analytics: data, + error, + isLoading, + }; +}; diff --git a/packages/blocks/src/hooks/use-events.ts b/packages/blocks/src/hooks/use-events.ts new file mode 100644 index 0000000000..d299dea28a --- /dev/null +++ b/packages/blocks/src/hooks/use-events.ts @@ -0,0 +1,12 @@ +import { fetcher } from "@dub/utils"; +import useSWR from "swr"; + +export const useEvents = () => { + const { error, data, isLoading } = useSWR(`/api/events/client`, fetcher); + + return { + events: data, + error, + isLoading, + }; +}; diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index 3a63f7417a..3a7736b51e 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -4,6 +4,8 @@ import "./styles.css"; export * from "./empty-state"; export * from "./event-list"; export * from "./gauge"; +export * from "./hooks/use-analytics"; +export * from "./hooks/use-events"; export * from "./mini-area-chart"; export * from "./pagination-controls"; export * from "./stats-card"; From 9db82ae9b5c872c08ab4252d4904a8cfab2dc6e5 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Wed, 2 Oct 2024 00:18:01 +0530 Subject: [PATCH 06/88] wip --- .../[slug]/settings/referrals/page.tsx | 100 ++---------- .../[slug]/settings/referrals/stats.tsx | 143 ++++++------------ apps/web/lib/edge-config/get-feature-flags.ts | 2 +- pnpm-lock.yaml | 41 +---- 4 files changed, 60 insertions(+), 226 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx index 76e3825939..ee1e896eba 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx @@ -1,24 +1,18 @@ -import { getConversionEvents } from "@/lib/actions/get-conversion-events"; -import { getReferralLink } from "@/lib/actions/get-referral-link"; -import { getTotalEvents } from "@/lib/actions/get-total-events"; import { EventType } from "@/lib/analytics/types"; import { REFERRAL_CLICKS_QUOTA_BONUS, REFERRAL_CLICKS_QUOTA_BONUS_MAX, REFERRAL_REVENUE_SHARE, } from "@/lib/referrals/constants"; -import { EventListSkeleton } from "@dub/blocks"; import { Wordmark } from "@dub/ui"; import { Check } from "@dub/ui/src/icons"; -import { nFormatter, randomValue } from "@dub/utils"; -import { subDays } from "date-fns"; +import { nFormatter } from "@dub/utils"; import { Suspense } from "react"; -import { ActivityList } from "./activity-list"; import { EventTabs } from "./event-tabs"; +import { Events } from "./events"; import { HeroBackground } from "./hero-background"; import ReferralsPageClient from "./page-client"; import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; -import { Stats } from "./stats"; export const revalidate = 0; @@ -93,96 +87,20 @@ export default function ReferralsPage({

-
- -
+ + {/*
+ +
*/} + + {/* Events */}

Activity

- } - > - - +
); } - -const placeholderEvents = { - clicks: [...Array(10)].map( - (_, idx) => - ({ - timestamp: subDays(new Date(), idx).toISOString(), - click_id: "1", - link_id: "1", - domain: "refer.dub.co", - key: "", - url: "https://dub.co", - country: randomValue(["US", "GB", "CA", "AU", "DE", "FR", "ES", "IT"]), - }) as any, - ), - leads: [...Array(10)].map( - (_, idx) => - ({ - timestamp: subDays(new Date(), idx).toISOString(), - click_id: "1", - link_id: "1", - domain: "refer.dub.co", - key: "", - url: "https://dub.co", - country: randomValue(["US", "GB", "CA", "AU", "DE", "FR", "ES", "IT"]), - }) as any, - ), - sales: [...Array(10)].map( - (_, idx) => - ({ - timestamp: subDays(new Date(), idx).toISOString(), - click_id: "1", - link_id: "1", - domain: "refer.dub.co", - key: "", - url: "https://dub.co", - country: randomValue(["US", "GB", "CA", "AU", "DE", "FR", "ES", "IT"]), - event_name: [ - "Subscription creation", - "Subscription paid", - "Plan upgraded", - ][idx % 3], - // TODO update to saleAmount - amount: [1100, 4900, 2400][idx % 3], - }) as any, - ), -}; - -async function ActivityListRSC({ - slug, - event, - page, -}: { - slug: string; - event: EventType; - page: number; -}) { - const link = await getReferralLink(slug); - if (!link) { - return ( - - ); - } - - const [events, totalEvents] = await Promise.all([ - getConversionEvents({ linkId: link.id, event, page }), - getTotalEvents(link.id), - ]); - - return ; -} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx index 8715125d8b..01ff1387e6 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx @@ -1,4 +1,5 @@ -import { getReferralLink } from "@/lib/actions/get-referral-link"; +"use client"; + import { getTotalEvents } from "@/lib/actions/get-total-events"; import { dub } from "@/lib/dub"; import { @@ -11,113 +12,61 @@ import { MiniAreaChart, StatsCard, StatsCardSkeleton, + useAnalytics, } from "@dub/blocks"; import { CountingNumbers } from "@dub/ui"; import { User } from "@dub/ui/src/icons"; -import { nFormatter, randomValue } from "@dub/utils"; -import { subDays } from "date-fns"; import { AnalyticsTimeseries } from "dub/dist/commonjs/models/components"; -import { Suspense } from "react"; -export function Stats({ slug }: { slug: string }) { - return ( -
- ( - - ))} - > - - -
- ); +export function Stats() { + const { analytics, isLoading } = useAnalytics(); + + return ; + + // return ( + //
+ // {isLoading ? ( + // [...Array(2)].map(() => ) + // ) : ( + // + // )} + //
+ // ); } -async function StatsInner({ slug }: { slug: string }) { - try { - const link = await getReferralLink(slug); - if (!link) { - return ( - <> - { - const x = (idx - 7.5) / 4; - const curve1 = 800 * Math.exp(-Math.pow(x + 0.5, 2)); - const curve2 = 600 * Math.exp(-Math.pow(x - 0.5, 2)); - return { - date: subDays(new Date(), 15 - idx), - value: Math.floor( - 1500 + curve1 + curve2 + (Math.random() - 0.5) * 200, - ), - }; - })} - /> - } - > - ${Math.floor(Math.random() * 50) + 50} - - -
- - 5 -
- - } - > - {nFormatter(randomValue([1000, 1500, 2000, 2500, 3000]), { - full: true, - })} -
- - ); - } +const StatsInner = () => { + const { totalSales, sales, referredSignups, clicksQuotaBonus } = + await loadData(link.id); - const { totalSales, sales, referredSignups, clicksQuotaBonus } = - await loadData(link.id); + return ( + <> + } + > + + {(totalSales / 100) * REFERRAL_REVENUE_SHARE} + + - return ( - <> - } - > - - {(totalSales / 100) * REFERRAL_REVENUE_SHARE} - - - -
- - {referredSignups} -
- - } - > - {clicksQuotaBonus} -
- - ); - } catch (e) { - console.error("Failed to load referral stats", e); - } + +
+ + {referredSignups} +
+ + } + > + {clicksQuotaBonus} +
+ + ); return [...Array(2)].map(() => ); -} +}; async function loadData(linkId: string) { const [clicks, sales, totalEvents] = await Promise.all([ diff --git a/apps/web/lib/edge-config/get-feature-flags.ts b/apps/web/lib/edge-config/get-feature-flags.ts index d649eb90b3..1b6a2fb840 100644 --- a/apps/web/lib/edge-config/get-feature-flags.ts +++ b/apps/web/lib/edge-config/get-feature-flags.ts @@ -17,7 +17,7 @@ export const getFeatureFlags = async ({ } const workspaceFeatures: Record = { - referrals: false, + referrals: true, webhooks: false, newlinkbuilder: false, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 770370ded3..e09addc7eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -450,6 +450,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + swr: + specifier: ^2.1.5 + version: 2.1.5(react@18.2.0) devDependencies: '@dub/tailwind-config': specifier: workspace:* @@ -708,7 +711,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.1.3 - version: 6.1.3(typescript@5.1.6) + version: 6.1.3(postcss@8.4.31)(typescript@5.1.6) typescript: specifier: ^5.1.6 version: 5.1.6 @@ -20237,42 +20240,6 @@ packages: - ts-node dev: true - /tsup@6.1.3(typescript@5.1.6): - resolution: {integrity: sha512-eRpBnbfpDFng+EJNTQ90N7QAf4HAGGC7O3buHIjroKWK7D1ibk9/YnR/3cS8HsMU5T+6Oi+cnF+yU5WmCnB//Q==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: ^4.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - dependencies: - bundle-require: 3.1.2(esbuild@0.14.54) - cac: 6.7.14 - chokidar: 3.5.3 - debug: 4.3.4 - esbuild: 0.14.54 - execa: 5.1.1 - globby: 11.1.0 - joycon: 3.1.1 - postcss-load-config: 3.1.4(postcss@8.4.38) - resolve-from: 5.0.0 - rollup: 2.79.1 - source-map: 0.8.0-beta.0 - sucrase: 3.34.0 - tree-kill: 1.2.2 - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - - ts-node - dev: true - /tsutils@3.21.0(typescript@4.9.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} From 04fb198fd6002a357b401b705d4f0d157ac8f0fc Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 3 Oct 2024 12:19:41 +0530 Subject: [PATCH 07/88] wip --- .../[slug]/settings/referrals/events.tsx | 78 ++++++++++--------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx index 50f37bf588..60691e07af 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx @@ -3,7 +3,7 @@ import { ConversionEvent } from "@/lib/actions/get-conversion-events"; import { EventType } from "@/lib/analytics/types"; import { REFERRAL_REVENUE_SHARE } from "@/lib/referrals/constants"; -import { EventList } from "@dub/blocks"; +import { EventList, useEvents } from "@dub/blocks"; import { CaretUpFill, ChartActivity2, @@ -26,44 +26,48 @@ interface EventsProps { export const Events = (props: EventsProps) => { const searchParams = useSearchParams(); + const { events, isLoading } = useEvents(); + const event = (searchParams.get("event") || "clicks") as EventType; - return ( -
- { - const Icon = { - clicks: CursorRays, - leads: UserCheck, - sales: InvoiceDollar, - }[event]; - return { - icon: , - content: { - clicks: , - leads: , - sales: , - }[event], - right: e.timestamp ? ( -
- {timeAgo(new Date(e.timestamp), { withAgo: true })} -
- ) : null, - }; - })} - totalEvents={totalEvents} - emptyState={{ - icon: ChartActivity2, - title: `${capitalize(event)} Activity`, - description: `No referral ${event} have been recorded yet.`, - learnMore: "https://d.to/conversions", - }} - /> - {demo && ( -
- )} -
- ); + return
Events
; + + // return ( + //
+ // { + // const Icon = { + // clicks: CursorRays, + // leads: UserCheck, + // sales: InvoiceDollar, + // }[event]; + // return { + // icon: , + // content: { + // clicks: , + // leads: , + // sales: , + // }[event], + // right: e.timestamp ? ( + //
+ // {timeAgo(new Date(e.timestamp), { withAgo: true })} + //
+ // ) : null, + // }; + // })} + // totalEvents={totalEvents} + // emptyState={{ + // icon: ChartActivity2, + // title: `${capitalize(event)} Activity`, + // description: `No referral ${event} have been recorded yet.`, + // learnMore: "https://d.to/conversions", + // }} + // /> + // {demo && ( + //
+ // )} + //
+ // ); } function ClickDescription({ event }: { event: ClickEvent }) { From 1215a43b0eba5f900d35d07d136b634deecb4a15 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 3 Oct 2024 15:07:47 +0530 Subject: [PATCH 08/88] wip --- apps/web/app/api/analytics/client/route.ts | 1 + .../[slug]/settings/referrals/events.tsx | 16 ++++--- apps/web/lib/referrals/auth.ts | 48 ++++++++++--------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/apps/web/app/api/analytics/client/route.ts b/apps/web/app/api/analytics/client/route.ts index 3335215dd6..b6a2d17996 100644 --- a/apps/web/app/api/analytics/client/route.ts +++ b/apps/web/app/api/analytics/client/route.ts @@ -7,6 +7,7 @@ import { NextResponse } from "next/server"; export const GET = withAuth(async ({ workspace, link, searchParams }) => { const parsedParams = analyticsQuerySchema.parse(searchParams); + const response = await getAnalytics({ ...parsedParams, linkId: link.id, diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx index 60691e07af..ad6fb43a09 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx @@ -28,6 +28,8 @@ export const Events = (props: EventsProps) => { const searchParams = useSearchParams(); const { events, isLoading } = useEvents(); + console.log("events", events); + const event = (searchParams.get("event") || "clicks") as EventType; return
Events
; @@ -70,6 +72,13 @@ export const Events = (props: EventsProps) => { // ); } +const saleText = { + "Subscription creation": "upgraded their account", + "Subscription paid": "paid their subscription", + "Plan upgraded": "upgraded their plan", + default: "made a payment", +}; + function ClickDescription({ event }: { event: ClickEvent }) { return ( <> @@ -116,12 +125,7 @@ function LeadDescription({ event }: { event: LeadEvent }) { ); } -const saleText = { - "Subscription creation": "upgraded their account", - "Subscription paid": "paid their subscription", - "Plan upgraded": "upgraded their plan", - default: "made a payment", -}; + function SaleDescription({ event }: { event: SaleEvent }) { return ( diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts index cebd5ac7fc..cd80149c6e 100644 --- a/apps/web/lib/referrals/auth.ts +++ b/apps/web/lib/referrals/auth.ts @@ -37,29 +37,31 @@ export const withAuth = (handler: WithAuthHandler) => { const searchParams = getSearchParams(req.url); const authorizationHeader = req.headers.get("Authorization"); - if (!authorizationHeader) { - throw new DubApiError({ - code: "unauthorized", - message: "Missing Authorization header.", - }); - } - - if (!authorizationHeader.includes("Bearer ")) { - throw new DubApiError({ - code: "bad_request", - message: - "Misconfigured authorization header. Did you forget to add 'Bearer '? Learn more: https://d.to/auth", - }); - } - - tokenFromHeader = authorizationHeader.replace("Bearer ", ""); - - if (!tokenFromHeader) { - throw new DubApiError({ - code: "unauthorized", - message: "Missing Authorization header.", - }); - } + // if (!authorizationHeader) { + // throw new DubApiError({ + // code: "unauthorized", + // message: "Missing Authorization header.", + // }); + // } + + // if (!authorizationHeader.includes("Bearer ")) { + // throw new DubApiError({ + // code: "bad_request", + // message: + // "Misconfigured authorization header. Did you forget to add 'Bearer '? Learn more: https://d.to/auth", + // }); + // } + + // tokenFromHeader = authorizationHeader.replace("Bearer ", ""); + + // if (!tokenFromHeader) { + // throw new DubApiError({ + // code: "unauthorized", + // message: "Missing Authorization header.", + // }); + // } + + tokenFromHeader = "Wu2HvXx2w99h1nFnXY2rnVE6532bVqLAoJht"; const publicToken = await prisma.referralPublicToken.findUnique({ where: { From abf4fda38377b27e7a92fa86063ad295f32a2c53 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 3 Oct 2024 16:25:13 +0530 Subject: [PATCH 09/88] display events --- apps/web/app/api/analytics/client/route.ts | 1 - apps/web/app/api/analytics/route.ts | 6 +- apps/web/app/api/events/route.ts | 6 +- .../[slug]/settings/referrals/event-tabs.tsx | 1 + .../[slug]/settings/referrals/events.tsx | 122 +++++++++--------- .../[slug]/settings/referrals/page.tsx | 8 +- apps/web/lib/analytics/get-analytics.ts | 2 + apps/web/lib/referrals/auth.ts | 2 +- packages/blocks/src/hooks/use-events.ts | 21 ++- packages/blocks/src/index.ts | 2 +- 10 files changed, 97 insertions(+), 74 deletions(-) diff --git a/apps/web/app/api/analytics/client/route.ts b/apps/web/app/api/analytics/client/route.ts index b6a2d17996..3335215dd6 100644 --- a/apps/web/app/api/analytics/client/route.ts +++ b/apps/web/app/api/analytics/client/route.ts @@ -7,7 +7,6 @@ import { NextResponse } from "next/server"; export const GET = withAuth(async ({ workspace, link, searchParams }) => { const parsedParams = analyticsQuerySchema.parse(searchParams); - const response = await getAnalytics({ ...parsedParams, linkId: link.id, diff --git a/apps/web/app/api/analytics/route.ts b/apps/web/app/api/analytics/route.ts index 1f0ee4bd8d..6c7672c8c3 100644 --- a/apps/web/app/api/analytics/route.ts +++ b/apps/web/app/api/analytics/route.ts @@ -41,9 +41,9 @@ export const GET = withWorkspace( } = parsedParams; let link: Link | null = null; - if (domain) { - await getDomainOrThrow({ workspace, domain }); - } + // if (domain) { + // await getDomainOrThrow({ workspace, domain }); + // } if (linkId || externalId || (domain && key)) { link = await getLinkOrThrow({ diff --git a/apps/web/app/api/events/route.ts b/apps/web/app/api/events/route.ts index f543318ea9..807b184587 100644 --- a/apps/web/app/api/events/route.ts +++ b/apps/web/app/api/events/route.ts @@ -18,9 +18,9 @@ export const GET = withWorkspace( parsedParams; let link: Link | null = null; - if (domain) { - await getDomainOrThrow({ workspace, domain }); - } + // if (domain) { + // await getDomainOrThrow({ workspace, domain }); + // } if (linkId || externalId || (domain && key)) { link = await getLinkOrThrow({ diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/event-tabs.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/event-tabs.tsx index dad47c15ce..1079bba6fc 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/event-tabs.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/event-tabs.tsx @@ -5,6 +5,7 @@ import { ToggleGroup } from "@dub/ui/src/toggle-group"; export function EventTabs() { const { queryParams, searchParams } = useRouterStuff(); + return ( { - const searchParams = useSearchParams(); - const { events, isLoading } = useEvents(); - - console.log("events", events); - - const event = (searchParams.get("event") || "clicks") as EventType; - - return
Events
; - - // return ( - //
- // { - // const Icon = { - // clicks: CursorRays, - // leads: UserCheck, - // sales: InvoiceDollar, - // }[event]; - // return { - // icon: , - // content: { - // clicks: , - // leads: , - // sales: , - // }[event], - // right: e.timestamp ? ( - //
- // {timeAgo(new Date(e.timestamp), { withAgo: true })} - //
- // ) : null, - // }; - // })} - // totalEvents={totalEvents} - // emptyState={{ - // icon: ChartActivity2, - // title: `${capitalize(event)} Activity`, - // description: `No referral ${event} have been recorded yet.`, - // learnMore: "https://d.to/conversions", - // }} - // /> - // {demo && ( - //
- // )} - //
- // ); -} +const iconMap: Record = { + clicks: CursorRays, + leads: UserCheck, + sales: InvoiceDollar, +}; const saleText = { "Subscription creation": "upgraded their account", @@ -79,7 +36,56 @@ const saleText = { default: "made a payment", }; -function ClickDescription({ event }: { event: ClickEvent }) { +export const Events = ({ event, page }: EventsProps) => { + const { events, isLoading } = useEvents({ + event, + interval: "all", + page, + }); + + if (isLoading || !events) { + return ; + } + + const Icon = iconMap[event]; + + return ( +
+ { + const content = { + clicks: , + leads: , + sales: , + }[event]; + + return { + icon: , + content, + right: e.timestamp ? ( +
+ {timeAgo(new Date(e.timestamp), { withAgo: true })} +
+ ) : null, + }; + })} + totalEvents={events?.length || 0} + emptyState={{ + icon: ChartActivity2, + title: `${capitalize(event)} Activity`, + description: `No referral ${event} have been recorded yet.`, + learnMore: "https://d.to/conversions", + }} + /> + + {/* {demo && ( +
+ )} */} +
+ ); +}; + +const ClickDescription = ({ event }: { event: ClickEvent }) => { return ( <> Someone from{" "} @@ -100,9 +106,9 @@ function ClickDescription({ event }: { event: ClickEvent }) { clicked on your link ); -} +}; -function LeadDescription({ event }: { event: LeadEvent }) { +const LeadDescription = ({ event }: { event: LeadEvent }) => { return ( <> Someone from{" "} @@ -123,11 +129,9 @@ function LeadDescription({ event }: { event: LeadEvent }) { signed up for an account ); -} - - +}; -function SaleDescription({ event }: { event: SaleEvent }) { +const SaleDescription = ({ event }: { event: SaleEvent }) => { return (
@@ -164,4 +168,4 @@ function SaleDescription({ event }: { event: SaleEvent }) { )}
); -} +}; diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx index ee1e896eba..74dbd0eaaa 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx @@ -21,10 +21,10 @@ export default function ReferralsPage({ searchParams, }: { params: { slug: string }; - searchParams: { event?: string; page?: string }; + searchParams: { event?: EventType; page?: string }; }) { - const event = (searchParams.event ?? "clicks") as EventType; - const page = parseInt(searchParams.page ?? "1") || 1; + const event = searchParams.event || "clicks"; + const page = searchParams.page || "1"; return ( @@ -98,7 +98,7 @@ export default function ReferralsPage({

Activity

- + diff --git a/apps/web/lib/analytics/get-analytics.ts b/apps/web/lib/analytics/get-analytics.ts index 0357c1da75..c2e4600731 100644 --- a/apps/web/lib/analytics/get-analytics.ts +++ b/apps/web/lib/analytics/get-analytics.ts @@ -87,6 +87,8 @@ export const getAnalytics = async (params: AnalyticsFilters) => { timezone, }); + console.log("getAnalytics", response); + if (groupBy === "count") { // Return the count value for deprecated endpoints if (isDeprecatedClicksEndpoint) { diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts index cd80149c6e..f7a6448486 100644 --- a/apps/web/lib/referrals/auth.ts +++ b/apps/web/lib/referrals/auth.ts @@ -61,7 +61,7 @@ export const withAuth = (handler: WithAuthHandler) => { // }); // } - tokenFromHeader = "Wu2HvXx2w99h1nFnXY2rnVE6532bVqLAoJht"; + tokenFromHeader = "i2yisemInUCWbWnEXB1WR4b3ROe2lLccYulj"; const publicToken = await prisma.referralPublicToken.findUnique({ where: { diff --git a/packages/blocks/src/hooks/use-events.ts b/packages/blocks/src/hooks/use-events.ts index d299dea28a..e33fbbe00e 100644 --- a/packages/blocks/src/hooks/use-events.ts +++ b/packages/blocks/src/hooks/use-events.ts @@ -1,8 +1,25 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; -export const useEvents = () => { - const { error, data, isLoading } = useSWR(`/api/events/client`, fetcher); +const EVENT_TYPES = ["clicks", "leads", "sales"] as const; + +interface Props { + event: (typeof EVENT_TYPES)[number]; + interval: string; + page: string; +} + +export const useEvents = ({ event, interval, page }: Props) => { + const searchParams = new URLSearchParams(); + + searchParams.set("event", event); + searchParams.set("interval", interval); + searchParams.set("page", page); + + const { error, data, isLoading } = useSWR<[]>( + `/api/events/client?${searchParams.toString()}`, + fetcher, + ); return { events: data, diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index 3a7736b51e..5137ee0e1f 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -8,4 +8,4 @@ export * from "./hooks/use-analytics"; export * from "./hooks/use-events"; export * from "./mini-area-chart"; export * from "./pagination-controls"; -export * from "./stats-card"; +export * from "./stats-card"; \ No newline at end of file From 9dcc14ee2eb27bfc1910e2cae2c5fe5181d5d020 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 3 Oct 2024 17:51:32 +0530 Subject: [PATCH 10/88] display stats --- apps/web/app/api/analytics/client/route.ts | 2 +- apps/web/app/api/analytics/route.ts | 1 - apps/web/app/api/events/route.ts | 1 - .../[slug]/settings/referrals/page.tsx | 6 +- .../[slug]/settings/referrals/stats.tsx | 115 ++++++++---------- apps/web/lib/analytics/get-analytics.ts | 2 - packages/blocks/src/hooks/use-analytics.ts | 24 +++- packages/blocks/src/hooks/use-events.ts | 19 ++- packages/blocks/src/index.ts | 2 +- packages/blocks/src/types.ts | 1 + 10 files changed, 89 insertions(+), 84 deletions(-) create mode 100644 packages/blocks/src/types.ts diff --git a/apps/web/app/api/analytics/client/route.ts b/apps/web/app/api/analytics/client/route.ts index 3335215dd6..357d4f4a86 100644 --- a/apps/web/app/api/analytics/client/route.ts +++ b/apps/web/app/api/analytics/client/route.ts @@ -9,8 +9,8 @@ export const GET = withAuth(async ({ workspace, link, searchParams }) => { const response = await getAnalytics({ ...parsedParams, - linkId: link.id, workspaceId: workspace.id, + linkId: link.id, }); return NextResponse.json(response); diff --git a/apps/web/app/api/analytics/route.ts b/apps/web/app/api/analytics/route.ts index 6c7672c8c3..012c7014c2 100644 --- a/apps/web/app/api/analytics/route.ts +++ b/apps/web/app/api/analytics/route.ts @@ -1,7 +1,6 @@ import { VALID_ANALYTICS_ENDPOINTS } from "@/lib/analytics/constants"; import { getAnalytics } from "@/lib/analytics/get-analytics"; import { validDateRangeForPlan } from "@/lib/analytics/utils"; -import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw"; import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw"; import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks"; import { withWorkspace } from "@/lib/auth"; diff --git a/apps/web/app/api/events/route.ts b/apps/web/app/api/events/route.ts index 807b184587..5f1e38fb5c 100644 --- a/apps/web/app/api/events/route.ts +++ b/apps/web/app/api/events/route.ts @@ -1,6 +1,5 @@ import { getEvents } from "@/lib/analytics/get-events"; import { validDateRangeForPlan } from "@/lib/analytics/utils"; -import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw"; import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw"; import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks"; import { withWorkspace } from "@/lib/auth"; diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx index 74dbd0eaaa..6f8534e57e 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx @@ -13,6 +13,7 @@ import { Events } from "./events"; import { HeroBackground } from "./hero-background"; import ReferralsPageClient from "./page-client"; import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; +import { Stats } from "./stats"; export const revalidate = 0; @@ -88,9 +89,10 @@ export default function ReferralsPage({ - {/*
+ {/* Stats */} +
-
*/} +
{/* Events */}
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx index 01ff1387e6..50f919fc35 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx @@ -1,7 +1,5 @@ "use client"; -import { getTotalEvents } from "@/lib/actions/get-total-events"; -import { dub } from "@/lib/dub"; import { REFERRAL_CLICKS_QUOTA_BONUS, REFERRAL_CLICKS_QUOTA_BONUS_MAX, @@ -16,33 +14,65 @@ import { } from "@dub/blocks"; import { CountingNumbers } from "@dub/ui"; import { User } from "@dub/ui/src/icons"; -import { AnalyticsTimeseries } from "dub/dist/commonjs/models/components"; +import { + AnalyticsCount, + AnalyticsTimeseries, +} from "dub/dist/commonjs/models/components"; -export function Stats() { - const { analytics, isLoading } = useAnalytics(); +interface StatsInnerProps { + totalEvents: AnalyticsCount; + sales: AnalyticsTimeseries[]; +} - return ; +export const Stats = () => { + const { analytics: sales, isLoading: isLoadingSales } = useAnalytics({ + event: "sales", + interval: "90d", + groupBy: "timeseries", + }); - // return ( - //
- // {isLoading ? ( - // [...Array(2)].map(() => ) - // ) : ( - // - // )} - //
- // ); -} + const { analytics: totalEvents, isLoading: isLoadingTotalEvents } = + useAnalytics({ + event: "composite", + interval: "all_unfiltered", + groupBy: "count", + }); -const StatsInner = () => { - const { totalSales, sales, referredSignups, clicksQuotaBonus } = - await loadData(link.id); + const loading = isLoadingSales || isLoadingTotalEvents; + + return ( +
+ {loading ? ( + [...Array(2)].map(() => ) + ) : ( + + )} +
+ ); +}; + +const StatsInner = ({ totalEvents, sales }: StatsInnerProps) => { + const totalLeads = Math.min(totalEvents.leads ?? 0, 32); + const totalSales = totalEvents.saleAmount ?? 0; + + const clicksQuotaBonus = Math.min( + totalLeads * REFERRAL_CLICKS_QUOTA_BONUS, + REFERRAL_CLICKS_QUOTA_BONUS_MAX, + ); + + const salesData = sales.map((sale) => ({ + date: new Date(sale.start), + value: sale.saleAmount ?? 0, + })); return ( <> } + graphic={} > {(totalSales / 100) * REFERRAL_REVENUE_SHARE} @@ -55,7 +85,7 @@ const StatsInner = () => {
- {referredSignups} + {totalLeads}
} @@ -64,47 +94,4 @@ const StatsInner = () => {
); - - return [...Array(2)].map(() => ); }; - -async function loadData(linkId: string) { - const [clicks, sales, totalEvents] = await Promise.all([ - // Clicks timeseries - dub.analytics.retrieve({ - linkId, - event: "clicks", - interval: "30d", - groupBy: "timeseries", - }) as Promise, - - // Sales timeseries - dub.analytics.retrieve({ - linkId, - event: "sales", - interval: "30d", - groupBy: "timeseries", - }) as Promise, - - // Total events - getTotalEvents(linkId), - ]); - - return { - totalClicks: totalEvents.clicks, - clicks: clicks.map((d) => ({ - date: new Date(d.start), - value: d.clicks, - })), - totalSales: totalEvents.saleAmount ?? 0, - sales: sales.map((d) => ({ - date: new Date(d.start), - value: d.saleAmount ?? 0, - })), - referredSignups: Math.min(totalEvents.leads ?? 0, 32), - clicksQuotaBonus: Math.min( - (totalEvents.leads ?? 0) * REFERRAL_CLICKS_QUOTA_BONUS, - REFERRAL_CLICKS_QUOTA_BONUS_MAX, - ), - }; -} diff --git a/apps/web/lib/analytics/get-analytics.ts b/apps/web/lib/analytics/get-analytics.ts index c2e4600731..0357c1da75 100644 --- a/apps/web/lib/analytics/get-analytics.ts +++ b/apps/web/lib/analytics/get-analytics.ts @@ -87,8 +87,6 @@ export const getAnalytics = async (params: AnalyticsFilters) => { timezone, }); - console.log("getAnalytics", response); - if (groupBy === "count") { // Return the count value for deprecated endpoints if (isDeprecatedClicksEndpoint) { diff --git a/packages/blocks/src/hooks/use-analytics.ts b/packages/blocks/src/hooks/use-analytics.ts index 9bd91503ed..daa44ed860 100644 --- a/packages/blocks/src/hooks/use-analytics.ts +++ b/packages/blocks/src/hooks/use-analytics.ts @@ -1,8 +1,28 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; +import { EventType } from "../types"; -export const useAnalytics = () => { - const { error, data, isLoading } = useSWR(`/api/analytics/client`, fetcher); +interface UseAnalyticsParams { + event: EventType; + interval: string; + groupBy: "timeseries" | "top_links" | "devices" | "count"; +} + +export const useAnalytics = ({ + event, + interval, + groupBy, +}: UseAnalyticsParams) => { + const searchParams = new URLSearchParams({ + event, + interval, + groupBy, + }); + + const { error, data, isLoading } = useSWR( + `/api/analytics/client?${searchParams.toString()}`, + fetcher, + ); return { analytics: data, diff --git a/packages/blocks/src/hooks/use-events.ts b/packages/blocks/src/hooks/use-events.ts index e33fbbe00e..5ea3bfbd96 100644 --- a/packages/blocks/src/hooks/use-events.ts +++ b/packages/blocks/src/hooks/use-events.ts @@ -1,20 +1,19 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; +import { EventType } from "../types"; -const EVENT_TYPES = ["clicks", "leads", "sales"] as const; - -interface Props { - event: (typeof EVENT_TYPES)[number]; +interface UseEventsParams { + event: EventType; interval: string; page: string; } -export const useEvents = ({ event, interval, page }: Props) => { - const searchParams = new URLSearchParams(); - - searchParams.set("event", event); - searchParams.set("interval", interval); - searchParams.set("page", page); +export const useEvents = ({ event, interval, page }: UseEventsParams) => { + const searchParams = new URLSearchParams({ + event, + interval, + page, + }); const { error, data, isLoading } = useSWR<[]>( `/api/events/client?${searchParams.toString()}`, diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index 5137ee0e1f..3a7736b51e 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -8,4 +8,4 @@ export * from "./hooks/use-analytics"; export * from "./hooks/use-events"; export * from "./mini-area-chart"; export * from "./pagination-controls"; -export * from "./stats-card"; \ No newline at end of file +export * from "./stats-card"; diff --git a/packages/blocks/src/types.ts b/packages/blocks/src/types.ts new file mode 100644 index 0000000000..510cddc5ce --- /dev/null +++ b/packages/blocks/src/types.ts @@ -0,0 +1 @@ +export type EventType = "clicks" | "leads" | "sales" | "composite"; From 29982cd3680b7f202a2bb08b6dbd78d2e8a198d5 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 3 Oct 2024 17:58:20 +0530 Subject: [PATCH 11/88] remove actions --- apps/web/lib/actions/get-conversion-events.ts | 28 ------------------- apps/web/lib/actions/get-total-events.ts | 12 -------- 2 files changed, 40 deletions(-) delete mode 100644 apps/web/lib/actions/get-conversion-events.ts delete mode 100644 apps/web/lib/actions/get-total-events.ts diff --git a/apps/web/lib/actions/get-conversion-events.ts b/apps/web/lib/actions/get-conversion-events.ts deleted file mode 100644 index aaadd28577..0000000000 --- a/apps/web/lib/actions/get-conversion-events.ts +++ /dev/null @@ -1,28 +0,0 @@ -"use server"; - -import { - ClickEvent, - LeadEvent, - SaleEvent, -} from "dub/dist/commonjs/models/components"; -import { EventType } from "../analytics/types"; -import { dub } from "../dub"; - -export type ConversionEvent = ClickEvent | LeadEvent | SaleEvent; - -export const getConversionEvents = async ({ - linkId, - event, - page, -}: { - linkId: string; - event: EventType; - page: number; -}) => { - return (await dub.events.list({ - linkId, - event, - interval: "all", - page, - })) as ConversionEvent[]; -}; diff --git a/apps/web/lib/actions/get-total-events.ts b/apps/web/lib/actions/get-total-events.ts deleted file mode 100644 index 7d29ded170..0000000000 --- a/apps/web/lib/actions/get-total-events.ts +++ /dev/null @@ -1,12 +0,0 @@ -"use server"; - -import { dub } from "@/lib/dub"; -import { AnalyticsCount } from "dub/dist/commonjs/models/components"; - -export const getTotalEvents = async (linkId: string) => { - return (await dub.analytics.retrieve({ - linkId, - event: "composite", - interval: "all_unfiltered", - })) as AnalyticsCount; -}; From 6f2ac076f30a8d79fd86c350ec856964b936a281 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Fri, 4 Oct 2024 00:19:34 +0530 Subject: [PATCH 12/88] use public token --- apps/web/app/api/referrals/tokens/route.ts | 32 +---- .../[idOrSlug]/referrals-token/route.ts | 26 ++++ .../[slug]/settings/referrals/page-client.tsx | 56 ++++++++- .../[slug]/settings/referrals/page.tsx | 100 +-------------- .../settings/referrals/referral-link.tsx | 3 +- .../[slug]/settings/referrals/referrals.tsx | 115 ++++++++++++++++++ .../[slug]/settings/referrals/stats.tsx | 2 + apps/web/app/app.dub.co/embed/page.tsx | 3 + apps/web/lib/middleware/app.ts | 4 + apps/web/lib/referrals/auth.ts | 19 ++- apps/web/lib/referrals/token.ts | 37 ++++++ packages/blocks/src/context.tsx | 27 ++++ packages/blocks/src/hooks/use-analytics.ts | 4 + packages/blocks/src/hooks/use-events.ts | 4 + packages/blocks/src/index.ts | 1 + 15 files changed, 295 insertions(+), 138 deletions(-) create mode 100644 apps/web/app/api/workspaces/[idOrSlug]/referrals-token/route.ts create mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx create mode 100644 apps/web/app/app.dub.co/embed/page.tsx create mode 100644 apps/web/lib/referrals/token.ts create mode 100644 packages/blocks/src/context.tsx diff --git a/apps/web/app/api/referrals/tokens/route.ts b/apps/web/app/api/referrals/tokens/route.ts index 8643ad9046..aeb2952b6d 100644 --- a/apps/web/app/api/referrals/tokens/route.ts +++ b/apps/web/app/api/referrals/tokens/route.ts @@ -1,16 +1,10 @@ -import { DubApiError } from "@/lib/api/errors"; import { parseRequestBody } from "@/lib/api/utils"; import { withWorkspace } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; -import { - REFERRAL_PUBLIC_TOKEN_EXPIRY, - REFERRAL_PUBLIC_TOKEN_LENGTH, -} from "@/lib/referrals/constants"; +import { createPublicToken } from "@/lib/referrals/token"; import { createReferralTokenSchema, referralTokenSchema, } from "@/lib/zod/schemas/referrals"; -import { nanoid } from "@dub/utils"; import { NextResponse } from "next/server"; // GET /api/referrals/tokens - create a new referral token for the given link @@ -19,29 +13,9 @@ export const POST = withWorkspace(async ({ workspace, req }) => { await parseRequestBody(req), ); - const link = await prisma.link.findUniqueOrThrow({ - where: { - id: linkId, - projectId: workspace.id, - }, - }); - - if (!link.trackConversion) { - throw new DubApiError({ - code: "forbidden", - message: "Conversion tracking is not enabled for this link.", - }); - } - - const referralToken = await prisma.referralPublicToken.create({ - data: { - linkId, - expires: new Date(Date.now() + REFERRAL_PUBLIC_TOKEN_EXPIRY), - publicToken: nanoid(REFERRAL_PUBLIC_TOKEN_LENGTH), - }, - }); + const token = await createPublicToken({ linkId, workspaceId: workspace.id }); - return NextResponse.json(referralTokenSchema.parse(referralToken), { + return NextResponse.json(referralTokenSchema.parse(token), { status: 201, }); }); diff --git a/apps/web/app/api/workspaces/[idOrSlug]/referrals-token/route.ts b/apps/web/app/api/workspaces/[idOrSlug]/referrals-token/route.ts new file mode 100644 index 0000000000..66a31c301b --- /dev/null +++ b/apps/web/app/api/workspaces/[idOrSlug]/referrals-token/route.ts @@ -0,0 +1,26 @@ +import { DubApiError } from "@/lib/api/errors"; +import { withWorkspace } from "@/lib/auth"; +import { createPublicToken } from "@/lib/referrals/token"; +import { referralTokenSchema } from "@/lib/zod/schemas/referrals"; +import { NextResponse } from "next/server"; + +// GET /api/workspaces/[idOrSlug]/referrals-token - create a new referral token for the workspace +export const POST = withWorkspace(async ({ workspace }) => { + const { referralLinkId, id } = workspace; + + if (!referralLinkId) { + throw new DubApiError({ + code: "bad_request", + message: "Referral link not found for this workspace.", + }); + } + + const token = await createPublicToken({ + linkId: referralLinkId, + workspaceId: id, + }); + + return NextResponse.json(referralTokenSchema.parse(token), { + status: 201, + }); +}); diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index acac0c8944..aa637d4dd2 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -1,19 +1,63 @@ "use client"; +import { EventType } from "@/lib/analytics/types"; import useWorkspace from "@/lib/swr/use-workspace"; import { redirect } from "next/navigation"; -import { ReactNode } from "react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Referrals } from "./referrals"; + +// TODO: +// event and page should be part of iframe +interface ReferralsPageClientProps { + event: EventType | undefined; + page: string | undefined; +} export default function ReferralsPageClient({ - children, -}: { - children: ReactNode; -}) { + event, + page, +}: ReferralsPageClientProps) { const { slug, flags } = useWorkspace(); + const [publicToken, setPublicToken] = useState(null); + + // Get publicToken from server when component mounts + const createPublicToken = async () => { + const response = await fetch(`/api/workspaces/${slug}/referrals-token`, { + method: "POST", + }); + + if (!response.ok) { + throw toast.error("Failed to create public token"); + } + + const { publicToken } = (await response.json()) as { + publicToken: string; + }; + + setPublicToken(publicToken); + }; + + useEffect(() => { + createPublicToken(); + }, []); if (!flags?.referrals) { redirect(`/${slug}/settings`); } - return <>{children}; + if (!publicToken) { + return null; + } + + return ( + <> + + + ); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx index 6f8534e57e..3c0e349ab6 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx @@ -1,108 +1,16 @@ import { EventType } from "@/lib/analytics/types"; -import { - REFERRAL_CLICKS_QUOTA_BONUS, - REFERRAL_CLICKS_QUOTA_BONUS_MAX, - REFERRAL_REVENUE_SHARE, -} from "@/lib/referrals/constants"; -import { Wordmark } from "@dub/ui"; -import { Check } from "@dub/ui/src/icons"; -import { nFormatter } from "@dub/utils"; -import { Suspense } from "react"; -import { EventTabs } from "./event-tabs"; -import { Events } from "./events"; -import { HeroBackground } from "./hero-background"; import ReferralsPageClient from "./page-client"; -import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; -import { Stats } from "./stats"; - -export const revalidate = 0; export default function ReferralsPage({ - params: { slug }, searchParams, }: { - params: { slug: string }; searchParams: { event?: EventType; page?: string }; }) { - const event = searchParams.event || "clicks"; - const page = searchParams.page || "1"; + const { event, page } = searchParams; return ( - -
-
-
- - - - -
-

- Refer and earn -

- - {/* Benefits */} -
- {[ - { - title: `${nFormatter(REFERRAL_REVENUE_SHARE * 100)}% recurring revenue`, - description: "per paying customer (up to 1 year)", - }, - { - title: `${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS)} extra clicks quota per month`, - description: `per signup (up to ${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS_MAX, { full: true })} total)`, - }, - ].map(({ title, description }) => ( -
-
-
- -
-
-
-

- {title} -

-

{description}

-
-
- ))} -
- - {/* Referral link + invite button or empty/error states */} - }> - - -
-
- - {/* Powered by Dub Conversions */} - - -

- Powered by Dub Conversions -

-
-
- - {/* Stats */} -
- -
- - {/* Events */} -
-
-

Activity

- -
- -
-
-
+ <> + + ); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx index 7be3b58c09..abc64d83dc 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx @@ -1,11 +1,10 @@ -import { getReferralLink } from "@/lib/actions/get-referral-link"; import { CopyButton } from "@dub/ui"; import { getPrettyUrl } from "@dub/utils"; import { GenerateButton } from "./generate-button"; import { InviteButton } from "./invite-button"; export default async function ReferralLink({ slug }: { slug: string }) { - const { shortLink } = (await getReferralLink(slug)) || {}; + const shortLink = "demo"; return (
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx new file mode 100644 index 0000000000..10d924c046 --- /dev/null +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { EventType } from "@/lib/analytics/types"; +import { + REFERRAL_CLICKS_QUOTA_BONUS, + REFERRAL_CLICKS_QUOTA_BONUS_MAX, + REFERRAL_REVENUE_SHARE, +} from "@/lib/referrals/constants"; +import { DubProvider } from "@dub/blocks"; +import { Wordmark } from "@dub/ui"; +import { Check } from "@dub/ui/src/icons"; +import { nFormatter } from "@dub/utils"; +import { Suspense } from "react"; +import { EventTabs } from "./event-tabs"; +import { Events } from "./events"; +import { HeroBackground } from "./hero-background"; +import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; +import { Stats } from "./stats"; + +interface ReferralsProps { + slug: string; + event: EventType | undefined; + page: string | undefined; + publicToken: string | undefined | null; +} + +export const Referrals = ({ + slug, + event, + page, + publicToken, +}: ReferralsProps) => { + if (!publicToken) { + return ; + } + + return ( + +
+
+
+ + + + +
+

+ Refer and earn +

+ + {/* Benefits */} +
+ {[ + { + title: `${nFormatter(REFERRAL_REVENUE_SHARE * 100)}% recurring revenue`, + description: "per paying customer (up to 1 year)", + }, + { + title: `${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS)} extra clicks quota per month`, + description: `per signup (up to ${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS_MAX, { full: true })} total)`, + }, + ].map(({ title, description }) => ( +
+
+
+ +
+
+
+

+ {title} +

+

{description}

+
+
+ ))} +
+ + {/* Referral link + invite button or empty/error states */} + }> + + +
+
+ + {/* Powered by Dub Conversions */} + + +

+ Powered by Dub Conversions +

+
+
+ + {/* Stats */} +
+ +
+ + {/* Events */} +
+
+

Activity

+ +
+ +
+
+
+ ); +}; diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx index 50f919fc35..2925b93fca 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx @@ -40,6 +40,8 @@ export const Stats = () => { const loading = isLoadingSales || isLoadingTotalEvents; + console.log({ sales, totalEvents }); + return (
{loading ? ( diff --git a/apps/web/app/app.dub.co/embed/page.tsx b/apps/web/app/app.dub.co/embed/page.tsx new file mode 100644 index 0000000000..23e123c025 --- /dev/null +++ b/apps/web/app/app.dub.co/embed/page.tsx @@ -0,0 +1,3 @@ +export default function EmbedPage() { + return
Embed Page
; +} diff --git a/apps/web/lib/middleware/app.ts b/apps/web/lib/middleware/app.ts index 9b8daa6330..a173b09390 100644 --- a/apps/web/lib/middleware/app.ts +++ b/apps/web/lib/middleware/app.ts @@ -12,6 +12,10 @@ export default async function AppMiddleware(req: NextRequest) { const user = await getUserViaToken(req); const isWorkspaceInvite = req.nextUrl.searchParams.get("invite"); + if (path.startsWith("/embed")) { + return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url)); + } + // if there's no user and the path isn't /login or /register, redirect to /login if ( !user && diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts index f7a6448486..cb19197e1f 100644 --- a/apps/web/lib/referrals/auth.ts +++ b/apps/web/lib/referrals/auth.ts @@ -33,9 +33,10 @@ export const withAuth = (handler: WithAuthHandler) => { let link: Link | undefined = undefined; let tokenFromHeader: string | undefined = undefined; - const rateLimit = 100; + const rateLimit = 60; const searchParams = getSearchParams(req.url); - const authorizationHeader = req.headers.get("Authorization"); + + // const authorizationHeader = req.headers.get("Authorization"); // if (!authorizationHeader) { // throw new DubApiError({ @@ -61,11 +62,19 @@ export const withAuth = (handler: WithAuthHandler) => { // }); // } - tokenFromHeader = "i2yisemInUCWbWnEXB1WR4b3ROe2lLccYulj"; + // Read token from query params + const tokenFromQuery = searchParams["publicToken"]; + + if (!tokenFromQuery) { + throw new DubApiError({ + code: "unauthorized", + message: "Missing public token.", + }); + } const publicToken = await prisma.referralPublicToken.findUnique({ where: { - publicToken: tokenFromHeader, + publicToken: tokenFromQuery, }, }); @@ -86,7 +95,7 @@ export const withAuth = (handler: WithAuthHandler) => { const { success, limit, reset, remaining } = await ratelimit( rateLimit, "1 m", - ).limit(tokenFromHeader); + ).limit(tokenFromQuery); headers = { "Retry-After": reset.toString(), diff --git a/apps/web/lib/referrals/token.ts b/apps/web/lib/referrals/token.ts new file mode 100644 index 0000000000..04e8638ec4 --- /dev/null +++ b/apps/web/lib/referrals/token.ts @@ -0,0 +1,37 @@ +import { prisma } from "@/lib/prisma"; +import { nanoid } from "@dub/utils"; +import { DubApiError } from "../api/errors"; +import { + REFERRAL_PUBLIC_TOKEN_EXPIRY, + REFERRAL_PUBLIC_TOKEN_LENGTH, +} from "./constants"; + +export const createPublicToken = async ({ + linkId, + workspaceId, +}: { + linkId: string; + workspaceId: string; +}) => { + const link = await prisma.link.findUniqueOrThrow({ + where: { + id: linkId, + projectId: workspaceId, + }, + }); + + if (!link.trackConversion) { + throw new DubApiError({ + code: "forbidden", + message: "Conversion tracking is not enabled for this link.", + }); + } + + return await prisma.referralPublicToken.create({ + data: { + linkId, + expires: new Date(Date.now() + REFERRAL_PUBLIC_TOKEN_EXPIRY), + publicToken: nanoid(REFERRAL_PUBLIC_TOKEN_LENGTH), + }, + }); +}; diff --git a/packages/blocks/src/context.tsx b/packages/blocks/src/context.tsx new file mode 100644 index 0000000000..44b818ed41 --- /dev/null +++ b/packages/blocks/src/context.tsx @@ -0,0 +1,27 @@ +import React, { createContext, useContext } from "react"; + +interface DubContextType { + publicToken: string; +} + +const DubContext = createContext(undefined); + +export const DubProvider: React.FC< + DubContextType & { children: React.ReactNode } +> = ({ publicToken, children }) => { + return ( + + {children} + + ); +}; + +export const useDub = () => { + const context = useContext(DubContext); + + if (context === undefined) { + throw new Error("useDub must be used within a DubProvider"); + } + + return context; +}; diff --git a/packages/blocks/src/hooks/use-analytics.ts b/packages/blocks/src/hooks/use-analytics.ts index daa44ed860..24086cda7a 100644 --- a/packages/blocks/src/hooks/use-analytics.ts +++ b/packages/blocks/src/hooks/use-analytics.ts @@ -1,5 +1,6 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; +import { useDub } from "../context"; import { EventType } from "../types"; interface UseAnalyticsParams { @@ -13,10 +14,13 @@ export const useAnalytics = ({ interval, groupBy, }: UseAnalyticsParams) => { + const { publicToken } = useDub(); + const searchParams = new URLSearchParams({ event, interval, groupBy, + publicToken, }); const { error, data, isLoading } = useSWR( diff --git a/packages/blocks/src/hooks/use-events.ts b/packages/blocks/src/hooks/use-events.ts index 5ea3bfbd96..b575a758c7 100644 --- a/packages/blocks/src/hooks/use-events.ts +++ b/packages/blocks/src/hooks/use-events.ts @@ -1,5 +1,6 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; +import { useDub } from "../context"; import { EventType } from "../types"; interface UseEventsParams { @@ -9,10 +10,13 @@ interface UseEventsParams { } export const useEvents = ({ event, interval, page }: UseEventsParams) => { + const { publicToken } = useDub(); + const searchParams = new URLSearchParams({ event, interval, page, + publicToken, }); const { error, data, isLoading } = useSWR<[]>( diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index 3a7736b51e..cfba1d9737 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -1,6 +1,7 @@ // styles import "./styles.css"; +export * from "./context"; export * from "./empty-state"; export * from "./event-list"; export * from "./gauge"; From 44578137ba5a039e3e71604f45473360f7d14741 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Fri, 4 Oct 2024 16:58:35 +0530 Subject: [PATCH 13/88] run prettier --- .../[slug]/settings/referrals/page-client.tsx | 9 +++++---- apps/web/ui/modals/link-builder/index.tsx | 8 +------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index aa637d4dd2..cd84d350d7 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -3,8 +3,9 @@ import { EventType } from "@/lib/analytics/types"; import useWorkspace from "@/lib/swr/use-workspace"; import { redirect } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; +import { ReferralLinkSkeleton } from "./referral-link"; import { Referrals } from "./referrals"; // TODO: @@ -22,7 +23,7 @@ export default function ReferralsPageClient({ const [publicToken, setPublicToken] = useState(null); // Get publicToken from server when component mounts - const createPublicToken = async () => { + const createPublicToken = useCallback(async () => { const response = await fetch(`/api/workspaces/${slug}/referrals-token`, { method: "POST", }); @@ -36,7 +37,7 @@ export default function ReferralsPageClient({ }; setPublicToken(publicToken); - }; + }, [slug]); useEffect(() => { createPublicToken(); @@ -47,7 +48,7 @@ export default function ReferralsPageClient({ } if (!publicToken) { - return null; + return ; } return ( diff --git a/apps/web/ui/modals/link-builder/index.tsx b/apps/web/ui/modals/link-builder/index.tsx index cdd7d99e40..045039c7ba 100644 --- a/apps/web/ui/modals/link-builder/index.tsx +++ b/apps/web/ui/modals/link-builder/index.tsx @@ -169,13 +169,7 @@ function LinkBuilderInner({ isSubmitSuccessful || (props && !isDirty), ); - }, [ - showLinkBuilder, - isSubmitting, - isSubmitSuccessful, - props, - isDirty, - ]); + }, [showLinkBuilder, isSubmitting, isSubmitSuccessful, props, isDirty]); const keyRef = useRef(null); useEffect(() => { From 7dc970018f44043e05b3d56ce9b57da0ceaf2cd7 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Fri, 4 Oct 2024 17:00:10 +0530 Subject: [PATCH 14/88] revert --- apps/web/app/api/analytics/route.ts | 7 ++++--- apps/web/app/api/events/route.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/web/app/api/analytics/route.ts b/apps/web/app/api/analytics/route.ts index 012c7014c2..1f0ee4bd8d 100644 --- a/apps/web/app/api/analytics/route.ts +++ b/apps/web/app/api/analytics/route.ts @@ -1,6 +1,7 @@ import { VALID_ANALYTICS_ENDPOINTS } from "@/lib/analytics/constants"; import { getAnalytics } from "@/lib/analytics/get-analytics"; import { validDateRangeForPlan } from "@/lib/analytics/utils"; +import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw"; import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw"; import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks"; import { withWorkspace } from "@/lib/auth"; @@ -40,9 +41,9 @@ export const GET = withWorkspace( } = parsedParams; let link: Link | null = null; - // if (domain) { - // await getDomainOrThrow({ workspace, domain }); - // } + if (domain) { + await getDomainOrThrow({ workspace, domain }); + } if (linkId || externalId || (domain && key)) { link = await getLinkOrThrow({ diff --git a/apps/web/app/api/events/route.ts b/apps/web/app/api/events/route.ts index 5f1e38fb5c..f543318ea9 100644 --- a/apps/web/app/api/events/route.ts +++ b/apps/web/app/api/events/route.ts @@ -1,5 +1,6 @@ import { getEvents } from "@/lib/analytics/get-events"; import { validDateRangeForPlan } from "@/lib/analytics/utils"; +import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw"; import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw"; import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks"; import { withWorkspace } from "@/lib/auth"; @@ -17,9 +18,9 @@ export const GET = withWorkspace( parsedParams; let link: Link | null = null; - // if (domain) { - // await getDomainOrThrow({ workspace, domain }); - // } + if (domain) { + await getDomainOrThrow({ workspace, domain }); + } if (linkId || externalId || (domain && key)) { link = await getLinkOrThrow({ From 2d01ee012bf271a151b7279f1f98ddbf949c9486 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Fri, 4 Oct 2024 19:02:12 +0530 Subject: [PATCH 15/88] wip iframe --- .../[slug]/settings/referrals/referrals.tsx | 9 ++++++++- .../[slug]/settings/referrals/stats.tsx | 2 -- apps/web/app/app.dub.co/embed/page.tsx | 17 +++++++++++++++-- apps/web/lib/middleware/app.ts | 8 +++++++- apps/web/lib/referrals/auth.ts | 19 +++++++++++++++---- packages/blocks/src/hooks/use-analytics.ts | 4 ---- packages/blocks/src/hooks/use-events.ts | 4 ---- 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx index 10d924c046..e0feaeeb4b 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx @@ -31,7 +31,14 @@ export const Referrals = ({ publicToken, }: ReferralsProps) => { if (!publicToken) { - return ; + return ( +
+

+ Unavailable +

+

Sorry, the referral token is not found.

+
+ ); } return ( diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx index 2925b93fca..50f919fc35 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx @@ -40,8 +40,6 @@ export const Stats = () => { const loading = isLoadingSales || isLoadingTotalEvents; - console.log({ sales, totalEvents }); - return (
{loading ? ( diff --git a/apps/web/app/app.dub.co/embed/page.tsx b/apps/web/app/app.dub.co/embed/page.tsx index 23e123c025..2152305194 100644 --- a/apps/web/app/app.dub.co/embed/page.tsx +++ b/apps/web/app/app.dub.co/embed/page.tsx @@ -1,3 +1,16 @@ -export default function EmbedPage() { - return
Embed Page
; +import { EventType } from "@/lib/analytics/types"; +import { Referrals } from "../(dashboard)/[slug]/settings/referrals/referrals"; + +export default async function EmbedPage({ + searchParams, +}: { + searchParams: { token: string; event?: EventType; page?: string }; +}) { + const { token, event, page } = searchParams; + + return ( + <> + + + ); } diff --git a/apps/web/lib/middleware/app.ts b/apps/web/lib/middleware/app.ts index a173b09390..ee21346047 100644 --- a/apps/web/lib/middleware/app.ts +++ b/apps/web/lib/middleware/app.ts @@ -13,7 +13,13 @@ export default async function AppMiddleware(req: NextRequest) { const isWorkspaceInvite = req.nextUrl.searchParams.get("invite"); if (path.startsWith("/embed")) { - return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url)); + return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url), { + headers: { + // TODO: Need better cookie name + // Maybe move this to a API route level? + "Set-Cookie": `token=${req.nextUrl.searchParams.get("token")}; HttpOnly; Path=/`, + }, + }); } // if there's no user and the path isn't /login or /register, redirect to /login diff --git a/apps/web/lib/referrals/auth.ts b/apps/web/lib/referrals/auth.ts index cb19197e1f..b73cc9d551 100644 --- a/apps/web/lib/referrals/auth.ts +++ b/apps/web/lib/referrals/auth.ts @@ -4,6 +4,7 @@ import { ratelimit } from "@/lib/upstash"; import { getSearchParams } from "@dub/utils"; import { Link, Project } from "@prisma/client"; import { AxiomRequest, withAxiom } from "next-axiom"; +import { cookies } from "next/headers"; interface WithAuthHandler { ({ @@ -63,9 +64,19 @@ export const withAuth = (handler: WithAuthHandler) => { // } // Read token from query params - const tokenFromQuery = searchParams["publicToken"]; + // const tokenFromQuery = searchParams["publicToken"]; - if (!tokenFromQuery) { + // if (!tokenFromQuery) { + // throw new DubApiError({ + // code: "unauthorized", + // message: "Missing public token.", + // }); + // } + + const cookieStore = cookies(); + const tokenFromCookie = cookieStore.get("token")?.value; + + if (!tokenFromCookie) { throw new DubApiError({ code: "unauthorized", message: "Missing public token.", @@ -74,7 +85,7 @@ export const withAuth = (handler: WithAuthHandler) => { const publicToken = await prisma.referralPublicToken.findUnique({ where: { - publicToken: tokenFromQuery, + publicToken: tokenFromCookie, }, }); @@ -95,7 +106,7 @@ export const withAuth = (handler: WithAuthHandler) => { const { success, limit, reset, remaining } = await ratelimit( rateLimit, "1 m", - ).limit(tokenFromQuery); + ).limit(tokenFromCookie); headers = { "Retry-After": reset.toString(), diff --git a/packages/blocks/src/hooks/use-analytics.ts b/packages/blocks/src/hooks/use-analytics.ts index 24086cda7a..daa44ed860 100644 --- a/packages/blocks/src/hooks/use-analytics.ts +++ b/packages/blocks/src/hooks/use-analytics.ts @@ -1,6 +1,5 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; -import { useDub } from "../context"; import { EventType } from "../types"; interface UseAnalyticsParams { @@ -14,13 +13,10 @@ export const useAnalytics = ({ interval, groupBy, }: UseAnalyticsParams) => { - const { publicToken } = useDub(); - const searchParams = new URLSearchParams({ event, interval, groupBy, - publicToken, }); const { error, data, isLoading } = useSWR( diff --git a/packages/blocks/src/hooks/use-events.ts b/packages/blocks/src/hooks/use-events.ts index b575a758c7..5ea3bfbd96 100644 --- a/packages/blocks/src/hooks/use-events.ts +++ b/packages/blocks/src/hooks/use-events.ts @@ -1,6 +1,5 @@ import { fetcher } from "@dub/utils"; import useSWR from "swr"; -import { useDub } from "../context"; import { EventType } from "../types"; interface UseEventsParams { @@ -10,13 +9,10 @@ interface UseEventsParams { } export const useEvents = ({ event, interval, page }: UseEventsParams) => { - const { publicToken } = useDub(); - const searchParams = new URLSearchParams({ event, interval, page, - publicToken, }); const { error, data, isLoading } = useSWR<[]>( From 9631595359b97c431034b667fb20c0dc046df578 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Fri, 4 Oct 2024 19:19:50 +0530 Subject: [PATCH 16/88] add sample iframe --- .../[slug]/settings/referrals/page-client.tsx | 13 ++----------- .../[slug]/settings/referrals/referrals.tsx | 15 +++++++++++++++ apps/web/next.config.js | 9 +++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index cd84d350d7..d908bc0c07 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -6,7 +6,7 @@ import { redirect } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { ReferralLinkSkeleton } from "./referral-link"; -import { Referrals } from "./referrals"; +import { ReferralsEmbed } from "./referrals"; // TODO: // event and page should be part of iframe @@ -51,14 +51,5 @@ export default function ReferralsPageClient({ return ; } - return ( - <> - - - ); + return ; } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx index e0feaeeb4b..56da1b98e4 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx @@ -120,3 +120,18 @@ export const Referrals = ({ ); }; + +export const ReferralsEmbed = ({ publicToken }: { publicToken: string }) => { + return ( + <> + + + ); +}; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 3e1a4860ab..4512003839 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -94,6 +94,15 @@ module.exports = withAxiom({ }, ], }, + { + source: "/embed", + headers: [ + { + key: "X-Frame-Options", + value: "ALLOW", + }, + ], + }, ]; }, async redirects() { From abf09096bee676d671c3388420357d091710b21c Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 7 Oct 2024 16:28:27 +0530 Subject: [PATCH 17/88] move the hero section --- .../[slug]/settings/referrals/page-client.tsx | 89 +++++++++++++++--- .../[slug]/settings/referrals/page.tsx | 11 +-- .../settings/referrals/referral-link.tsx | 3 +- .../[slug]/settings/referrals/referrals.tsx | 92 ++----------------- apps/web/lib/actions/get-referral-link.ts | 2 + 5 files changed, 90 insertions(+), 107 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index d908bc0c07..a9147c229b 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -1,24 +1,21 @@ "use client"; -import { EventType } from "@/lib/analytics/types"; +import { + REFERRAL_CLICKS_QUOTA_BONUS, + REFERRAL_CLICKS_QUOTA_BONUS_MAX, + REFERRAL_REVENUE_SHARE, +} from "@/lib/referrals/constants"; import useWorkspace from "@/lib/swr/use-workspace"; +import { Check, Wordmark } from "@dub/ui"; +import { nFormatter } from "@dub/utils"; import { redirect } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; -import { ReferralLinkSkeleton } from "./referral-link"; +import { HeroBackground } from "./hero-background"; +import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; import { ReferralsEmbed } from "./referrals"; -// TODO: -// event and page should be part of iframe -interface ReferralsPageClientProps { - event: EventType | undefined; - page: string | undefined; -} - -export default function ReferralsPageClient({ - event, - page, -}: ReferralsPageClientProps) { +export default function ReferralsPageClient() { const { slug, flags } = useWorkspace(); const [publicToken, setPublicToken] = useState(null); @@ -51,5 +48,67 @@ export default function ReferralsPageClient({ return ; } - return ; + return ( +
+
+
+ + + + +
+

+ Refer and earn +

+ + {/* Benefits */} +
+ {[ + { + title: `${nFormatter(REFERRAL_REVENUE_SHARE * 100)}% recurring revenue`, + description: "per paying customer (up to 1 year)", + }, + { + title: `${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS)} extra clicks quota per month`, + description: `per signup (up to ${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS_MAX, { full: true })} total)`, + }, + ].map(({ title, description }) => ( +
+
+
+ +
+
+
+

+ {title} +

+

{description}

+
+
+ ))} +
+ + {/* Referral link + invite button or empty/error states */} + }> + + +
+
+ + {/* Powered by Dub Conversions */} + + +

+ Powered by Dub Conversions +

+
+
+ ; +
+ ); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx index 3c0e349ab6..7eb9276d35 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page.tsx @@ -1,16 +1,9 @@ -import { EventType } from "@/lib/analytics/types"; import ReferralsPageClient from "./page-client"; -export default function ReferralsPage({ - searchParams, -}: { - searchParams: { event?: EventType; page?: string }; -}) { - const { event, page } = searchParams; - +export default async function ReferralsPage() { return ( <> - + ); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx index abc64d83dc..7be3b58c09 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referral-link.tsx @@ -1,10 +1,11 @@ +import { getReferralLink } from "@/lib/actions/get-referral-link"; import { CopyButton } from "@dub/ui"; import { getPrettyUrl } from "@dub/utils"; import { GenerateButton } from "./generate-button"; import { InviteButton } from "./invite-button"; export default async function ReferralLink({ slug }: { slug: string }) { - const shortLink = "demo"; + const { shortLink } = (await getReferralLink(slug)) || {}; return (
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx index 56da1b98e4..0b72e98c47 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx @@ -1,20 +1,9 @@ "use client"; import { EventType } from "@/lib/analytics/types"; -import { - REFERRAL_CLICKS_QUOTA_BONUS, - REFERRAL_CLICKS_QUOTA_BONUS_MAX, - REFERRAL_REVENUE_SHARE, -} from "@/lib/referrals/constants"; import { DubProvider } from "@dub/blocks"; -import { Wordmark } from "@dub/ui"; -import { Check } from "@dub/ui/src/icons"; -import { nFormatter } from "@dub/utils"; -import { Suspense } from "react"; import { EventTabs } from "./event-tabs"; import { Events } from "./events"; -import { HeroBackground } from "./hero-background"; -import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; import { Stats } from "./stats"; interface ReferralsProps { @@ -43,79 +32,18 @@ export const Referrals = ({ return ( -
-
-
- - - - -
-

- Refer and earn -

- - {/* Benefits */} -
- {[ - { - title: `${nFormatter(REFERRAL_REVENUE_SHARE * 100)}% recurring revenue`, - description: "per paying customer (up to 1 year)", - }, - { - title: `${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS)} extra clicks quota per month`, - description: `per signup (up to ${nFormatter(REFERRAL_CLICKS_QUOTA_BONUS_MAX, { full: true })} total)`, - }, - ].map(({ title, description }) => ( -
-
-
- -
-
-
-

- {title} -

-

{description}

-
-
- ))} -
- - {/* Referral link + invite button or empty/error states */} - }> - - -
-
- - {/* Powered by Dub Conversions */} - - -

- Powered by Dub Conversions -

-
-
- - {/* Stats */} -
- -
+ {/* Stats */} +
+ +
- {/* Events */} -
-
-

Activity

- -
- + {/* Events */} +
+
+

Activity

+
+
); diff --git a/apps/web/lib/actions/get-referral-link.ts b/apps/web/lib/actions/get-referral-link.ts index 5af75ebec7..19644c4339 100644 --- a/apps/web/lib/actions/get-referral-link.ts +++ b/apps/web/lib/actions/get-referral-link.ts @@ -9,9 +9,11 @@ export const getReferralLink = async (slug: string) => { slug, }, }); + if (!workspace || !workspace.referralLinkId) { return null; } + return await dub.links.get({ linkId: workspace.referralLinkId, }); From aca0fbc891f07af31b210c9ae50eec7d6716f6bd Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 7 Oct 2024 16:34:46 +0530 Subject: [PATCH 18/88] move the files to /embed --- .../(dashboard)/[slug]/settings/referrals/constants.ts | 0 .../[slug]/settings/referrals/page-client.tsx | 2 +- .../[slug]/settings/referrals => embed}/event-tabs.tsx | 0 .../[slug]/settings/referrals => embed}/events.tsx | 0 apps/web/app/app.dub.co/embed/page.tsx | 2 +- .../[slug]/settings/referrals => embed}/placeholders.tsx | 0 .../[slug]/settings/referrals => embed}/referrals.tsx | 9 ++------- .../[slug]/settings/referrals => embed}/stats.tsx | 0 8 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/constants.ts rename apps/web/app/app.dub.co/{(dashboard)/[slug]/settings/referrals => embed}/event-tabs.tsx (100%) rename apps/web/app/app.dub.co/{(dashboard)/[slug]/settings/referrals => embed}/events.tsx (100%) rename apps/web/app/app.dub.co/{(dashboard)/[slug]/settings/referrals => embed}/placeholders.tsx (100%) rename apps/web/app/app.dub.co/{(dashboard)/[slug]/settings/referrals => embed}/referrals.tsx (89%) rename apps/web/app/app.dub.co/{(dashboard)/[slug]/settings/referrals => embed}/stats.tsx (100%) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/constants.ts b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/constants.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index a9147c229b..94d56952c9 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -13,7 +13,7 @@ import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { HeroBackground } from "./hero-background"; import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; -import { ReferralsEmbed } from "./referrals"; +import { ReferralsEmbed } from "../../../../embed/referrals"; export default function ReferralsPageClient() { const { slug, flags } = useWorkspace(); diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/event-tabs.tsx b/apps/web/app/app.dub.co/embed/event-tabs.tsx similarity index 100% rename from apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/event-tabs.tsx rename to apps/web/app/app.dub.co/embed/event-tabs.tsx diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx b/apps/web/app/app.dub.co/embed/events.tsx similarity index 100% rename from apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/events.tsx rename to apps/web/app/app.dub.co/embed/events.tsx diff --git a/apps/web/app/app.dub.co/embed/page.tsx b/apps/web/app/app.dub.co/embed/page.tsx index 2152305194..ac800f5962 100644 --- a/apps/web/app/app.dub.co/embed/page.tsx +++ b/apps/web/app/app.dub.co/embed/page.tsx @@ -1,5 +1,5 @@ import { EventType } from "@/lib/analytics/types"; -import { Referrals } from "../(dashboard)/[slug]/settings/referrals/referrals"; +import { Referrals } from "./referrals"; export default async function EmbedPage({ searchParams, diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/placeholders.tsx b/apps/web/app/app.dub.co/embed/placeholders.tsx similarity index 100% rename from apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/placeholders.tsx rename to apps/web/app/app.dub.co/embed/placeholders.tsx diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx b/apps/web/app/app.dub.co/embed/referrals.tsx similarity index 89% rename from apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx rename to apps/web/app/app.dub.co/embed/referrals.tsx index 0b72e98c47..0869b901fe 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/referrals.tsx +++ b/apps/web/app/app.dub.co/embed/referrals.tsx @@ -13,19 +13,14 @@ interface ReferralsProps { publicToken: string | undefined | null; } -export const Referrals = ({ - slug, - event, - page, - publicToken, -}: ReferralsProps) => { +export const Referrals = ({ event, page, publicToken }: ReferralsProps) => { if (!publicToken) { return (

Unavailable

-

Sorry, the referral token is not found.

+

The referral token is not found.

); } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx b/apps/web/app/app.dub.co/embed/stats.tsx similarity index 100% rename from apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/stats.tsx rename to apps/web/app/app.dub.co/embed/stats.tsx From 19cdefaecb5fc8577213cdfc6ea9624a13e2732e Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 7 Oct 2024 16:39:19 +0530 Subject: [PATCH 19/88] move to @dub/blocks --- .../[slug]/settings/referrals/page-client.tsx | 4 ++-- apps/web/app/app.dub.co/embed/referrals.tsx | 15 --------------- packages/blocks/src/index.ts | 1 + packages/blocks/src/referrals-embed.tsx | 14 ++++++++++++++ 4 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 packages/blocks/src/referrals-embed.tsx diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index 94d56952c9..ba12d164c5 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -6,6 +6,7 @@ import { REFERRAL_REVENUE_SHARE, } from "@/lib/referrals/constants"; import useWorkspace from "@/lib/swr/use-workspace"; +import { ReferralsEmbed } from "@dub/blocks"; import { Check, Wordmark } from "@dub/ui"; import { nFormatter } from "@dub/utils"; import { redirect } from "next/navigation"; @@ -13,7 +14,6 @@ import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { HeroBackground } from "./hero-background"; import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; -import { ReferralsEmbed } from "../../../../embed/referrals"; export default function ReferralsPageClient() { const { slug, flags } = useWorkspace(); @@ -91,7 +91,7 @@ export default function ReferralsPageClient() { {/* Referral link + invite button or empty/error states */} }> - + {/* */}
diff --git a/apps/web/app/app.dub.co/embed/referrals.tsx b/apps/web/app/app.dub.co/embed/referrals.tsx index 0869b901fe..4d912de946 100644 --- a/apps/web/app/app.dub.co/embed/referrals.tsx +++ b/apps/web/app/app.dub.co/embed/referrals.tsx @@ -43,18 +43,3 @@ export const Referrals = ({ event, page, publicToken }: ReferralsProps) => {
); }; - -export const ReferralsEmbed = ({ publicToken }: { publicToken: string }) => { - return ( - <> - - - ); -}; diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index cfba1d9737..b77b774e74 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -9,4 +9,5 @@ export * from "./hooks/use-analytics"; export * from "./hooks/use-events"; export * from "./mini-area-chart"; export * from "./pagination-controls"; +export * from "./referrals-embed"; export * from "./stats-card"; diff --git a/packages/blocks/src/referrals-embed.tsx b/packages/blocks/src/referrals-embed.tsx new file mode 100644 index 0000000000..3a9e20d8bb --- /dev/null +++ b/packages/blocks/src/referrals-embed.tsx @@ -0,0 +1,14 @@ +export const ReferralsEmbed = ({ publicToken }: { publicToken: string }) => { + return ( + <> + + + ); +}; From 06b00e0f8b2e1a3f4325fc2e7b35e449e84533d9 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 7 Oct 2024 17:11:58 +0530 Subject: [PATCH 20/88] format --- .../(dashboard)/[slug]/settings/referrals/page-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index ba12d164c5..520bf7996e 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -13,7 +13,7 @@ import { redirect } from "next/navigation"; import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { HeroBackground } from "./hero-background"; -import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; +import { ReferralLinkSkeleton } from "./referral-link"; export default function ReferralsPageClient() { const { slug, flags } = useWorkspace(); From 4b0c3c5bacda76c53fdcb9a3147e2c09320cee9c Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 7 Oct 2024 17:20:43 +0530 Subject: [PATCH 21/88] use APP_DOMAIN --- .../[slug]/settings/referrals/page-client.tsx | 7 ++++--- packages/blocks/src/referrals-embed.tsx | 20 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx index 520bf7996e..4fd978c840 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/referrals/page-client.tsx @@ -13,7 +13,7 @@ import { redirect } from "next/navigation"; import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { HeroBackground } from "./hero-background"; -import { ReferralLinkSkeleton } from "./referral-link"; +import ReferralLink, { ReferralLinkSkeleton } from "./referral-link"; export default function ReferralsPageClient() { const { slug, flags } = useWorkspace(); @@ -91,7 +91,7 @@ export default function ReferralsPageClient() { {/* Referral link + invite button or empty/error states */} }> - {/* */} +
@@ -108,7 +108,8 @@ export default function ReferralsPageClient() {

- ; + {/* Embed Dub */} +
); } diff --git a/packages/blocks/src/referrals-embed.tsx b/packages/blocks/src/referrals-embed.tsx index 3a9e20d8bb..718c57a0d4 100644 --- a/packages/blocks/src/referrals-embed.tsx +++ b/packages/blocks/src/referrals-embed.tsx @@ -1,14 +1,14 @@ +import { APP_DOMAIN } from "@dub/utils"; + export const ReferralsEmbed = ({ publicToken }: { publicToken: string }) => { return ( - <> - - +