feat: Implement Dub.co for conversion tracking (#16165)
* FEAT: Implement Dub.co for conversion tracking * add link creation, @vercel/functions * use refer.cal.com/:username instead of go.cal.com/r/:username * sale conversion tracking * add copy referral link * limit to IS_CALCOM only * Update yarn.lock * fix DubAnalytics * use workaround for isNewUser * pass req to getOptions * Update next-auth-options.ts * fix ts errors * only show DubAnalytics outside EU * add Dub Analytics to /signup * use WEBSITE_URL instead * on-demand generate links * add migration * add check for existing link + change fetch method to poast * remove refer.cal.com from PoweredByCal * limit DubAnalytics to /signup only * simplify generate-referral-link * restore yarn.lock * add yarn with dub sdk in * add yarn with dub sdk in * yarn * Update yarn.lock --------- Co-authored-by: sean <sean@brydon.io> Co-authored-by: Peer Richelsen <peer@cal.com>
This commit is contained in:
49 files changed
+1548
-120
No files matched your search
@@ -0,0 +1,5 @@
|
||||
import { Dub } from "dub";
|
||||
|
||||
export const dub = new Dub({
|
||||
token: process.env.DUB_API_KEY,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { getOptions } from "./next-auth-options";
|
||||
|
||||
export async function getServerSessionForAppDir() {
|
||||
return await getServerSession(getOptions({ getDclid: () => cookies().get("dclid")?.value }));
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import type { AuthOptions, Session, User } from "next-auth";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { Provider } from "next-auth/providers";
|
||||
@@ -13,7 +14,7 @@ import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/Imperso
|
||||
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { HOSTED_CAL_FEATURES, IS_CALCOM } from "@calcom/lib/constants";
|
||||
import { ENABLE_PROFILE_SWITCHER, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
@@ -29,6 +30,7 @@ import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { ErrorCode } from "./ErrorCode";
|
||||
import { dub } from "./dub";
|
||||
import { isPasswordValid } from "./isPasswordValid";
|
||||
import CalComAdapter from "./next-auth-custom-adapter";
|
||||
import { verifyPassword } from "./verifyPassword";
|
||||
@@ -235,6 +237,7 @@ const providers: Provider[] = [
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
locale: user.locale,
|
||||
profile: user.allProfiles[0],
|
||||
createdAt: user.createdDate,
|
||||
};
|
||||
},
|
||||
}),
|
||||
@@ -404,7 +407,12 @@ const mapIdentityProvider = (providerName: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const AUTH_OPTIONS: AuthOptions = {
|
||||
export const getOptions = ({
|
||||
getDclid,
|
||||
}: {
|
||||
/** so we can extract the Dub cookie in both pages and app routers */
|
||||
getDclid: () => string | undefined;
|
||||
}): AuthOptions => ({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
adapter: calcomAdapter,
|
||||
@@ -924,7 +932,43 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
return baseUrl;
|
||||
},
|
||||
},
|
||||
};
|
||||
events: {
|
||||
async signIn(message) {
|
||||
/* only run this code if:
|
||||
- it's a hosted cal account
|
||||
- DUB_API_KEY is configured
|
||||
- it's a new user
|
||||
*/
|
||||
const user = message.user as User & {
|
||||
username: string;
|
||||
createdAt: string;
|
||||
};
|
||||
// check if the user was created in the last 10 minutes
|
||||
// this is a workaround – in the future once we move to use the Account model in the DB
|
||||
// we should use NextAuth's isNewUser flag instead: https://next-auth.js.org/configuration/events#signin
|
||||
const isNewUser = new Date(user.createdAt) > new Date(Date.now() - 10 * 60 * 1000);
|
||||
if ((isENVDev || IS_CALCOM) && process.env.DUB_API_KEY && isNewUser) {
|
||||
const dclid = getDclid();
|
||||
// check if there's a dclid cookie set by @dub/analytics
|
||||
if (dclid) {
|
||||
// here we use waitUntil – meaning this code will run async to not block the main thread
|
||||
waitUntil(
|
||||
// if so, send a lead event to Dub
|
||||
// @see https://d.to/conversions/next-auth
|
||||
dub.track.lead({
|
||||
clickId: dclid,
|
||||
eventName: "Sign Up",
|
||||
customerId: user.id.toString(),
|
||||
customerName: user.name,
|
||||
customerEmail: user.email,
|
||||
customerAvatar: user.image,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Identifies the profile the user should be logged into.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/ui": "*",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dub": "^0.35.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"jose": "^4.13.1",
|
||||
"lru-cache": "^9.0.3",
|
||||
|
||||
@@ -79,6 +79,7 @@ export const generateTeamCheckoutSession = async ({
|
||||
teamName,
|
||||
teamSlug,
|
||||
userId,
|
||||
dubCustomerId: userId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
|
||||
},
|
||||
});
|
||||
return session;
|
||||
@@ -173,6 +174,7 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
teamId,
|
||||
dubCustomerId: userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@calcom/ui": "*",
|
||||
"@lexical/react": "^0.9.0",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@vercel/functions": "^1.4.0",
|
||||
"framer-motion": "^10.12.8",
|
||||
"lexical": "^0.9.0",
|
||||
"react-select": "^5.7.0",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import dynamic from "next/dynamic";
|
||||
import React from "react";
|
||||
|
||||
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
|
||||
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
|
||||
import type { SettingsLayoutProps } from "./SettingsLayoutAppDirClient";
|
||||
@@ -14,7 +13,7 @@ const SettingsLayoutAppDirClient = dynamic(() => import("./SettingsLayoutAppDirC
|
||||
type SettingsLayoutAppDir = Omit<SettingsLayoutProps, "currentOrg" | "otherTeams">;
|
||||
|
||||
export default async function SettingsLayoutAppDir(props: SettingsLayoutAppDir) {
|
||||
const session = await getServerSession(AUTH_OPTIONS);
|
||||
const session = await getServerSessionForAppDir();
|
||||
const userId = session?.user?.id ?? -1;
|
||||
const orgId = session?.user?.org?.id ?? -1;
|
||||
let currentOrg = null;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
APP_NAME,
|
||||
DESKTOP_APP_LINK,
|
||||
ENABLE_PROFILE_SWITCHER,
|
||||
IS_CALCOM,
|
||||
IS_VISUAL_REGRESSION_TESTING,
|
||||
JOIN_COMMUNITY,
|
||||
ROADMAP,
|
||||
@@ -52,6 +53,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useFormbricks } from "@calcom/lib/formbricks-client";
|
||||
import getBrandColours from "@calcom/lib/getBrandColours";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useNotifications, ButtonState } from "@calcom/lib/hooks/useNotifications";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -927,6 +929,7 @@ function SideBarContainer({ bannersHeight, isPlatformUser = false }: SideBarCont
|
||||
}
|
||||
|
||||
function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||
const { isCopied, copyToClipboard, resetCopyStatus } = useCopy();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
const orgBranding = useOrgBranding();
|
||||
const pathname = usePathname();
|
||||
@@ -960,12 +963,32 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||
},
|
||||
icon: "copy",
|
||||
},
|
||||
IS_CALCOM
|
||||
? {
|
||||
name: "copy_referral_link",
|
||||
href: "",
|
||||
onClick: async (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/generate-referral-link", {
|
||||
method: "POST",
|
||||
});
|
||||
const { shortLink } = await res.json();
|
||||
copyToClipboard(shortLink);
|
||||
|
||||
if (isCopied) {
|
||||
showToast(t("link_copied"), "success");
|
||||
}
|
||||
},
|
||||
icon: "copy",
|
||||
}
|
||||
: null,
|
||||
{
|
||||
name: "settings",
|
||||
href: user?.org ? `/settings/organizations/profile` : "/settings/my-account/profile",
|
||||
icon: "settings",
|
||||
},
|
||||
];
|
||||
].filter(Boolean) as NavigationItemType[];
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<aside
|
||||
|
||||
@@ -9,6 +9,11 @@ export function useCopy() {
|
||||
.writeText(text)
|
||||
.then(() => setIsCopied(true))
|
||||
.catch((error) => console.error("Copy to clipboard failed:", error));
|
||||
} else {
|
||||
console.warn(
|
||||
"You need to use a secure context to use clipboard \n Please use the following link: ",
|
||||
text
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ export class UserRepository {
|
||||
},
|
||||
},
|
||||
},
|
||||
createdDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "referralLinkId" TEXT;
|
||||
@@ -335,6 +335,7 @@ model User {
|
||||
smsLockState SMSLockState @default(UNLOCKED)
|
||||
smsLockReviewedByAdmin Boolean @default(false)
|
||||
NotificationsSubscriptions NotificationsSubscriptions[]
|
||||
referralLinkId String?
|
||||
|
||||
@@unique([email])
|
||||
@@unique([email, username])
|
||||
|
||||
Reference in new issue
Block a user