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:
Steven Tey
2024-09-12 12:33:10 -07:00
committed by GitHub
co-authored by sean Peer Richelsen
parent f4bc7d2ea4
commit cf16cd3ed2
49 changed files with 1548 additions and 120 deletions
+1
View File
@@ -215,6 +215,7 @@ const generateTeamCheckoutSession = async ({
metadata: {
pendingPaymentTeamId,
ownerId,
dubCustomerId: ownerId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
},
});
@@ -1,10 +1,9 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
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 { ScheduleRepository } from "@calcom/lib/server/repository/schedule";
import { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
import { UserRepository } from "@calcom/lib/server/repository/user";
@@ -34,7 +33,7 @@ const Page = async ({ params }: PageProps) => {
}
const scheduleId = Number(params.schedule);
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const userId = session?.user?.id;
if (!userId) {
notFound();
+2 -3
View File
@@ -1,9 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
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 AvailabilityPage from "~/availability/availability-view";
@@ -16,7 +15,7 @@ export const generateMetadata = async () => {
};
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const userId = session?.user?.id;
const orgId = session?.user?.org?.id;
if (!userId || !orgId) {
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -13,7 +12,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import ImpersonationView from "~/settings/admin/impersonation-view";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import AdminOrgTable from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import BillingView from "~/settings/billing/billing-view";
@@ -14,7 +13,8 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,7 +1,6 @@
import { getFixedT, _generateMetadata } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { APP_NAME } from "@calcom/lib/constants";
@@ -15,7 +14,8 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import type { PageProps } from "app/_types";
import { getFixedT, _generateMetadata } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view";
import { APP_NAME } from "@calcom/lib/constants";
@@ -15,7 +14,7 @@ export const generateMetadata = async () =>
);
const Page = async ({ params }: PageProps) => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
const id = typeof params?.id === "string" ? params.id : undefined;
@@ -1,7 +1,6 @@
import { getFixedT, _generateMetadata } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view";
import { APP_NAME } from "@calcom/lib/constants";
@@ -13,7 +12,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import AppearancePage from "~/settings/my-account/appearance-view";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { Button } from "@calcom/ui";
@@ -15,7 +14,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
const AddCalendarButton = () => {
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { Button } from "@calcom/ui";
@@ -15,7 +14,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
const AddConferencingButton = () => {
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import GeneralQueryView from "~/settings/my-account/general-view";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import CreateNewOutOfOfficeEntryButton from "@calcom/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton";
import { OutOfOfficeEntriesList } from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { APP_NAME } from "@calcom/lib/constants";
@@ -15,7 +14,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,7 +1,6 @@
import { getFixedT, _generateMetadata } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -12,7 +11,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,7 +1,6 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
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 DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -12,7 +11,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,7 +1,6 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
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 PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -12,7 +11,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import ProfileImpersonationViewWrapper from "~/settings/security/impersonation-view";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import PasswordViewWrapper from "~/settings/security/password-view";
@@ -14,7 +13,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SAMLSSO from "@calcom/features/ee/sso/page/user-sso-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -13,7 +12,7 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,8 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { getFixedT } from "app/_utils";
import { getServerSession } from "next-auth";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import TwoFactorAuthView from "~/settings/security/two-factor-auth-view";
@@ -13,7 +12,7 @@ export const generateMetadata = async () =>
(t) => t("add_an_extra_layer_of_security")
);
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
@@ -1,11 +1,10 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { z } from "zod";
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 LegacyPage from "@calcom/features/ee/workflows/pages/workflow";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
@@ -30,7 +29,8 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
export const generateStaticParams = () => [];
const Page = async ({ params, searchParams }: PageProps) => {
const session = await getServerSession(AUTH_OPTIONS);
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
const session = await getServerSessionForAppDir();
const user = session?.user;
const parsed = querySchema.safeParse({ ...params, ...searchParams });
if (!parsed.success) {
+3 -3
View File
@@ -1,9 +1,8 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
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 LegacyPage from "@calcom/features/ee/workflows/pages/index";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
@@ -15,7 +14,8 @@ export const generateMetadata = async () =>
);
const Page = async ({ params, searchParams }: PageProps) => {
const session = await getServerSession(AUTH_OPTIONS);
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
const session = await getServerSessionForAppDir();
const user = session?.user;
const filters = getTeamsFiltersFromQuery({ ...searchParams, ...params });
@@ -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 SettingsLayoutAppDir from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
import type { AdminLayoutProps } from "./AdminLayoutAppDirClient";
@@ -14,7 +13,8 @@ const AdminLayoutAppDirClient = dynamic(() => import("./AdminLayoutAppDirClient"
type AdminLayoutAppDirProps = Omit<AdminLayoutProps, "userRole">;
export default async function AdminLayoutAppDir(props: AdminLayoutAppDirProps) {
const session = await getServerSession(AUTH_OPTIONS);
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
const session = await getServerSessionForAppDir();
const userRole = session?.user?.role;
return await SettingsLayoutAppDir({ children: <AdminLayoutAppDirClient {...props} userRole={userRole} /> });
@@ -1,12 +1,13 @@
import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/feature-auth/lib/getServerSession";
import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options";
import { getOptions } from "@calcom/feature-auth/lib/next-auth-options";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerSession({
req: context.req,
authOptions: AUTH_OPTIONS,
res: context.res,
authOptions: getOptions({ getDclid: () => context.req.cookies.dclid }),
});
// Disable this check if we ever make this self serve.
if (session?.user.role !== "ADMIN") {
+1
View File
@@ -44,6 +44,7 @@
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.59.0",
"@daily-co/daily-react": "^0.17.2",
"@dub/analytics": "^0.0.15",
"@formkit/auto-animate": "1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@hookform/error-message": "^2.0.0",
+7 -2
View File
@@ -1,5 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import { getOptions } from "@calcom/features/auth/lib/next-auth-options";
export default NextAuth(AUTH_OPTIONS);
// pass req to NextAuth: https://github.com/nextauthjs/next-auth/discussions/469
const handler = (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, getOptions({ getDclid: () => req.cookies.dclid }));
export default handler;
@@ -0,0 +1,49 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { dub } from "@calcom/features/auth/lib/dub";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getServerSession({ req, res });
if (!session?.user?.username) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
return await dub.links.get({
externalId: `ext_${session.user.id.toString()}`,
});
} catch (error) {
console.log("Referral link not found, creating...");
}
const { id: referralLinkId, shortLink } = await dub.links.create({
domain: "refer.cal.com",
key: session.user.username,
url: "https://cal.com",
externalId: session.user.id.toString(), // @see https://d.to/externalId
trackConversion: true, // enable conversion tracking @see https://d.to/conversions
});
/*
Even though we are relying in externalId for retrieving the referral link,
we still save the referralLinkId in the database for future reference.
E.g. we can use this to tell how many users created a referral link.
*/
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
referralLinkId,
},
});
return res.status(200).json({ shortLink });
}
+8
View File
@@ -1,5 +1,6 @@
"use client";
import { Analytics as DubAnalytics } from "@dub/analytics/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { Trans } from "next-i18next";
@@ -26,7 +27,9 @@ import {
CLOUDFLARE_SITE_ID,
WEBSITE_PRIVACY_POLICY_URL,
WEBSITE_TERMS_URL,
WEBSITE_URL,
} from "@calcom/lib/constants";
import { isENVDev } from "@calcom/lib/env";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { pushGTMEvent } from "@calcom/lib/gtm";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
@@ -309,6 +312,11 @@ export default function Signup({
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
}}
/>
<DubAnalytics
cookieOptions={{
domain: isENVDev ? undefined : `.${new URL(WEBSITE_URL).hostname}`,
}}
/>
</>
) : null}
<div
@@ -34,7 +34,10 @@ export function createWorkflowPageFixture(page: Page) {
};
const saveWorkflow = async () => {
const submitPromise = page.waitForResponse("/api/trpc/workflows/update?batch=1");
await page.getByTestId("save-workflow").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
};
const assertListCount = async (count: number) => {
+1 -1
View File
@@ -26,7 +26,7 @@ test.describe("Workflow Tab - Event Type", () => {
await fillNameInput("Edited Workflow");
await saveWorkflow();
await page.getByTestId("go-back-button").click();
await page.waitForLoadState("networkidle");
page.getByTestId("workflow-list").waitFor();
await hasWorkflowInList("Edited Workflow");
});
@@ -213,6 +213,7 @@
"view_notifications": "View notifications",
"view_public_page": "View public page",
"copy_public_page_link": "Copy public page link",
"copy_referral_link": "Copy referral link",
"sign_out": "Sign out",
"add_another": "Add another",
"install_another": "Install another",
@@ -37,6 +37,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const availability = await checkUsername(usernameParam, currentOrgDomain);
if (availability.available && availability.premium && IS_PREMIUM_USERNAME_ENABLED) {
const stripePremiumUrl = await getStripePremiumUsernameUrl({
userId: session.user.id.toString(),
userEmail: session.user.email,
username: usernameParam,
successDestination,
@@ -104,12 +105,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
type GetStripePremiumUsernameUrl = {
userId: string;
userEmail: string;
username: string;
successDestination: string;
};
const getStripePremiumUsernameUrl = async ({
userId,
userEmail,
username,
successDestination,
@@ -136,6 +139,9 @@ const getStripePremiumUsernameUrl = async ({
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}${successDestination}&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com",
allow_promotion_codes: true,
metadata: {
dubCustomerId: userId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
},
});
return checkoutSession.url;
+5
View File
@@ -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.
+1
View File
@@ -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,
},
},
});
+1
View File
@@ -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;
+24 -1
View File
@@ -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
+5
View File
@@ -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
);
}
};
+1
View File
@@ -225,6 +225,7 @@ export class UserRepository {
},
},
},
createdDate: true,
},
});
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "referralLinkId" TEXT;
+1
View File
@@ -335,6 +335,7 @@ model User {
smsLockState SMSLockState @default(UNLOCKED)
smsLockReviewedByAdmin Boolean @default(false)
NotificationsSubscriptions NotificationsSubscriptions[]
referralLinkId String?
@@unique([email])
@@unique([email, username])
+1
View File
@@ -289,6 +289,7 @@
"DATABASE_DIRECT_URL",
"DATABASE_URL",
"DEBUG",
"DUB_API_KEY",
"E2E_TEST_APPLE_CALENDAR_EMAIL",
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
"E2E_TEST_CALCOM_QA_EMAIL",
+1312 -32
View File
File diff suppressed because it is too large Load Diff