diff --git a/.env.example b/.env.example index 33f70bb046..b04ed91301 100644 --- a/.env.example +++ b/.env.example @@ -509,4 +509,7 @@ SEED_PLATFORM_OAUTH_CLIENT_SECRET= ENABLE_ASYNC_TASKER="false" # set to "true" to enable TRIGGER_SECRET_KEY= TRIGGER_API_URL=https://api.trigger.dev -TRIGGER_DEV_PROJECT_REF= \ No newline at end of file +TRIGGER_DEV_PROJECT_REF= + +GOOGLE_ADS_ENABLED=1 # To enable Google Ads tracking (gclid) +LINKEDIN_ADS_ENABLED=1 # To enable LinkedIn Ads tracking (li_fat_id) diff --git a/apps/web/lib/settings/license-key/new/getServerSideProps.tsx b/apps/web/lib/settings/license-key/new/getServerSideProps.tsx index 46ed5cbf3b..71567b373f 100644 --- a/apps/web/lib/settings/license-key/new/getServerSideProps.tsx +++ b/apps/web/lib/settings/license-key/new/getServerSideProps.tsx @@ -2,12 +2,14 @@ import type { GetServerSidePropsContext } from "next"; import { getServerSession } from "@calcom/feature-auth/lib/getServerSession"; import { getOptions } from "@calcom/feature-auth/lib/next-auth-options"; +import { getTrackingFromCookies } from "@calcom/lib/tracking"; export const getServerSideProps = async (context: GetServerSidePropsContext) => { const session = await getServerSession({ req: context.req, authOptions: getOptions({ getDubId: () => context.req.cookies.dub_id || context.req.cookies.dclid, + getTrackingData: () => getTrackingFromCookies(context.req.cookies), }), }); // Disable this check if we ever make this self serve. diff --git a/apps/web/pages/api/auth/[...nextauth].ts b/apps/web/pages/api/auth/[...nextauth].ts index b797e30f6c..02f4ec1067 100644 --- a/apps/web/pages/api/auth/[...nextauth].ts +++ b/apps/web/pages/api/auth/[...nextauth].ts @@ -2,9 +2,17 @@ import type { NextApiRequest, NextApiResponse } from "next"; import NextAuth from "next-auth"; import { getOptions } from "@calcom/features/auth/lib/next-auth-options"; +import { getTrackingFromCookies } from "@calcom/lib/tracking"; // pass req to NextAuth: https://github.com/nextauthjs/next-auth/discussions/469 const handler = (req: NextApiRequest, res: NextApiResponse) => - NextAuth(req, res, getOptions({ getDubId: () => req.cookies.dub_id || req.cookies.dclid })); + NextAuth( + req, + res, + getOptions({ + getDubId: () => req.cookies.dub_id || req.cookies.dclid, + getTrackingData: () => getTrackingFromCookies(req.cookies), + }) + ); export default handler; diff --git a/packages/app-store/stripepayment/api/subscription.ts b/packages/app-store/stripepayment/api/subscription.ts index 8b9bea5083..585e4cffe2 100644 --- a/packages/app-store/stripepayment/api/subscription.ts +++ b/packages/app-store/stripepayment/api/subscription.ts @@ -5,7 +5,6 @@ import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/li import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import { getTrackingFromCookies } from "@calcom/lib/tracking"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; @@ -40,8 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return; } - const tracking = getTrackingFromCookies(req.cookies); - const return_url = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=${callbackUrl}`; const createSessionParams: Stripe.Checkout.SessionCreateParams = { mode: "subscription", @@ -58,8 +55,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) metadata: { userId: userId.toString(), intentUsername, - ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds.campaignId }), - ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }), }, }; diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 4e0465bae3..373ccd11ab 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -12,7 +12,9 @@ import GoogleProvider from "next-auth/providers/google"; import { updateProfilePhotoGoogle } from "@calcom/app-store/_utils/oauth/updateProfilePhotoGoogle"; import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService"; import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService"; +import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing"; import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository"; +import type { TrackingData } from "@calcom/lib/tracking"; import { DeploymentRepository } from "@calcom/features/ee/deployment/repositories/DeploymentRepository"; import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; @@ -456,9 +458,12 @@ const mapIdentityProvider = (providerName: string) => { export const getOptions = ({ getDubId, + getTrackingData, }: { /** so we can extract the Dub cookie in both pages and app routers */ getDubId: () => string | undefined; + /** Ad tracking data for Stripe customer metadata */ + getTrackingData: () => TrackingData; }): AuthOptions => ({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -596,14 +601,14 @@ export const getOptions = ({ org: profileOrg && !profileOrg.isPlatform ? { - id: profileOrg.id, - name: profileOrg.name, - slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "", - logoUrl: profileOrg.logoUrl, - fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""), - domainSuffix: subdomainSuffix(), - role: orgRole as MembershipRole, // It can't be undefined if we have a profileOrg - } + id: profileOrg.id, + name: profileOrg.name, + slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "", + logoUrl: profileOrg.logoUrl, + fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""), + domainSuffix: subdomainSuffix(), + role: orgRole as MembershipRole, // It can't be undefined if we have a profileOrg + } : null, } as JWT; }; @@ -1066,11 +1071,12 @@ export const getOptions = ({ const { orgUsername, orgId } = await checkIfUserShouldBelongToOrg(idP, user.email); try { + const newUsername = orgId ? slugify(orgUsername) : usernameSlug(user.name); const newUser = await prisma.user.create({ data: { // Slugify the incoming name and append a few random characters to // prevent conflicts for users with the same name. - username: orgId ? slugify(orgUsername) : usernameSlug(user.name), + username: newUsername, emailVerified: new Date(Date.now()), name: user.name, ...(user.image && { avatarUrl: user.image }), @@ -1094,6 +1100,40 @@ export const getOptions = ({ ); await calcomAdapter.linkAccount(linkAccountNewUserData); + waitUntil( + (async () => { + try { + const tracking = getTrackingData(); + const billingService = getBillingProviderService(); + const customer = await billingService.createCustomer({ + email: newUser.email, + metadata: { + email: newUser.email, + username: newUser.username ?? newUsername, + ...(tracking.googleAds?.gclid && { + gclid: tracking.googleAds.gclid, + campaignId: tracking.googleAds.campaignId, + }), + ...(tracking.linkedInAds?.liFatId && { + liFatId: tracking.linkedInAds.liFatId, + linkedInCampaignId: tracking.linkedInAds.campaignId, + }), + }, + }); + await prisma.user.update({ + where: { id: newUser.id }, + data: { + metadata: { + stripeCustomerId: customer.stripeCustomerId, + }, + }, + }); + } catch (err) { + log.error("Failed to create Stripe customer with tracking", err); + } + })() + ); + if (account.twoFactorEnabled) { return loginWithTotp(newUser.email); } else { diff --git a/packages/features/auth/signup/handlers/calcomHandler.ts b/packages/features/auth/signup/handlers/calcomHandler.ts index 0ea62c1c86..6573e4ddd2 100644 --- a/packages/features/auth/signup/handlers/calcomHandler.ts +++ b/packages/features/auth/signup/handlers/calcomHandler.ts @@ -16,6 +16,7 @@ import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import type { CustomNextApiHandler } from "@calcom/lib/server/username"; import { usernameHandler } from "@calcom/lib/server/username"; +import { getTrackingFromCookies } from "@calcom/lib/tracking"; import { prisma } from "@calcom/prisma"; import { CreationSource } from "@calcom/prisma/enums"; import { IdentityProvider } from "@calcom/prisma/enums"; @@ -105,13 +106,24 @@ const handler: CustomNextApiHandler = async (body, usernameStatus) => { username = usernameAndEmailValidation.username; } - // Create the customer in Stripe + // Create the customer in Stripe with ad tracking metadata + const cookieStore = await cookies(); + const cookiesObj = Object.fromEntries(cookieStore.getAll().map((c) => [c.name, c.value])); + const tracking = getTrackingFromCookies(cookiesObj); const customer = await billingService.createCustomer({ email, metadata: { email /* Stripe customer email can be changed, so we add this to keep track of which email was used to signup */, username, + ...(tracking.googleAds?.gclid && { + gclid: tracking.googleAds.gclid, + campaignId: tracking.googleAds.campaignId, + }), + ...(tracking.linkedInAds?.liFatId && { + liFatId: tracking.linkedInAds.liFatId, + linkedInCampaignId: tracking.linkedInAds.campaignId, + }), }, }); diff --git a/packages/lib/tracking/server.ts b/packages/lib/tracking/server.ts index baa6806773..e0aca6f875 100644 --- a/packages/lib/tracking/server.ts +++ b/packages/lib/tracking/server.ts @@ -24,14 +24,14 @@ export function getTrackingFromCookies(cookies?: NextApiRequest["cookies"]): Tra if (process.env.GOOGLE_ADS_ENABLED === "1" && cookies.gclid) { tracking.googleAds = { gclid: cookies.gclid, - ...(cookies.gad_campaignid && { campaignId: cookies.gad_campaignid }), + ...(cookies.gad_campaignId && { campaignId: cookies.gad_campaignId }), }; } if (process.env.LINKEDIN_ADS_ENABLED === "1" && cookies.li_fat_id) { tracking.linkedInAds = { liFatId: cookies.li_fat_id, - ...(cookies.li_campaignid && { campaignId: cookies.li_campaignid }), + ...(cookies.li_campaignId && { campaignId: cookies.li_campaignId }), }; }