diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 4a70b27a3a..3683e26aa8 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -115,8 +115,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
diff --git a/apps/web/components/PageWrapperAppDir.tsx b/apps/web/components/PageWrapperAppDir.tsx
index 64c6d11155..86cdc7fffc 100644
--- a/apps/web/components/PageWrapperAppDir.tsx
+++ b/apps/web/components/PageWrapperAppDir.tsx
@@ -46,7 +46,6 @@ function PageWrapper(props: PageWrapperProps) {
id="page-status"
// It is strictly not necessary to disable, but in a future update of react/no-danger this will error.
// And we don't want it to error here anyways
- // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
{props.requiresLicense ? (
diff --git a/apps/web/server/lib/auth/sso/[provider]/getServerSideProps.tsx b/apps/web/server/lib/auth/sso/[provider]/getServerSideProps.tsx
index 8dc8a6395d..095c3f22d3 100644
--- a/apps/web/server/lib/auth/sso/[provider]/getServerSideProps.tsx
+++ b/apps/web/server/lib/auth/sso/[provider]/getServerSideProps.tsx
@@ -9,6 +9,7 @@ import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
import { checkUsername } from "@calcom/features/profile/lib/checkUsername";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
+import { getTrackingFromCookies, type TrackingData } from "@calcom/lib/tracking";
import { prisma } from "@calcom/prisma";
import { z } from "zod";
@@ -20,9 +21,9 @@ const Params = z.object({
export const getServerSideProps = async ({ req, query }: GetServerSidePropsContext) => {
- const {
- provider: providerParam,
- email: emailParam,
+ const {
+ provider: providerParam,
+ email: emailParam,
username: usernameParam,
} = Params.parse(query);
@@ -40,11 +41,13 @@ export const getServerSideProps = async ({ req, query }: GetServerSidePropsConte
if (usernameParam && session.user.email) {
const availability = await checkUsername(usernameParam, currentOrgDomain);
if (availability.available && availability.premium && IS_PREMIUM_USERNAME_ENABLED) {
+ const tracking = getTrackingFromCookies(req.cookies);
const stripePremiumUrl = await getStripePremiumUsernameUrl({
userId: session.user.id.toString(),
userEmail: session.user.email,
username: usernameParam,
successDestination,
+ tracking,
});
if (stripePremiumUrl) {
return {
@@ -112,6 +115,7 @@ type GetStripePremiumUsernameUrl = {
userEmail: string;
username: string;
successDestination: string;
+ tracking?: TrackingData;
};
const getStripePremiumUsernameUrl = async ({
@@ -119,6 +123,7 @@ const getStripePremiumUsernameUrl = async ({
userEmail,
username,
successDestination,
+ tracking,
}: GetStripePremiumUsernameUrl): Promise => {
// @TODO: probably want to check if stripe user email already exists? or not
const customer = await stripe.customers.create({
@@ -143,6 +148,8 @@ const getStripePremiumUsernameUrl = async ({
allow_promotion_codes: true,
metadata: {
dubCustomerId: userId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
+ ...(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/app-store/stripepayment/api/subscription.ts b/packages/app-store/stripepayment/api/subscription.ts
index 07914b8535..8b9bea5083 100644
--- a/packages/app-store/stripepayment/api/subscription.ts
+++ b/packages/app-store/stripepayment/api/subscription.ts
@@ -5,6 +5,7 @@ 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";
@@ -39,6 +40,8 @@ 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",
@@ -52,6 +55,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
customer: customerId,
success_url: return_url,
cancel_url: return_url,
+ 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 }),
+ },
};
const checkPremiumResult = await checkPremiumUsername(intentUsername);
diff --git a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts
index 67c1010a32..6751e5fc17 100644
--- a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts
+++ b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts
@@ -1,5 +1,7 @@
import type { Logger } from "tslog";
+import type { TrackingData } from "@calcom/lib/tracking";
+
import type { RetellAIPhoneServiceProviderTypeMap } from "../providers/retellAI";
/**
@@ -243,6 +245,7 @@ export interface AIPhoneServiceProvider;
/**
diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts
index 44294ebf16..23d6010d44 100644
--- a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts
+++ b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts
@@ -1,3 +1,5 @@
+import type { TrackingData } from "@calcom/lib/tracking";
+
import type {
AIPhoneServiceProvider,
AIPhoneServiceProviderType,
@@ -169,6 +171,7 @@ export class RetellAIPhoneServiceProvider
teamId?: number;
agentId?: string | null;
workflowId?: string;
+ tracking?: TrackingData;
}): Promise<{ url: string; message: string }> {
return await this.service.generatePhoneNumberCheckoutSession(params);
}
diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts
index 7df281dcb6..ec66f004df 100644
--- a/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts
+++ b/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts
@@ -1,3 +1,5 @@
+import type { TrackingData } from "@calcom/lib/tracking";
+
import type {
AIPhoneServiceUpdateModelParams,
AIPhoneServiceCreatePhoneNumberParams,
@@ -265,6 +267,7 @@ export class RetellAIService {
teamId?: number;
agentId?: string | null;
workflowId?: string;
+ tracking?: TrackingData;
}) {
return this.billingService.generatePhoneNumberCheckoutSession(params);
}
diff --git a/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts b/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts
index f71b3fb775..abb25f5783 100644
--- a/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts
+++ b/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts
@@ -7,6 +7,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL, IS_PRODUCTION } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
+import type { TrackingData } from "@calcom/lib/tracking";
import { PhoneNumberSubscriptionStatus } from "@calcom/prisma/enums";
import type { PhoneNumberRepositoryInterface } from "../../interfaces/PhoneNumberRepositoryInterface";
@@ -25,18 +26,20 @@ export class BillingService {
phoneNumberRepository: PhoneNumberRepositoryInterface;
retellRepository: RetellAIRepository;
}
- ) {}
+ ) { }
async generatePhoneNumberCheckoutSession({
userId,
teamId,
agentId,
workflowId,
+ tracking,
}: {
userId: number;
teamId?: number;
agentId?: string | null;
workflowId?: string;
+ tracking?: TrackingData;
}) {
const phoneNumberPriceId = getPhoneNumberMonthlyPriceId();
@@ -80,6 +83,8 @@ export class BillingService {
agentId: agentId || "",
workflowId: workflowId || "",
type: CHECKOUT_SESSION_TYPES.PHONE_NUMBER_SUBSCRIPTION,
+ ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds.campaignId }),
+ ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }),
},
subscription_data: {
metadata: {
@@ -114,14 +119,14 @@ export class BillingService {
// Find phone number with proper team authorization
const phoneNumber = teamId
? await this.deps.phoneNumberRepository.findByIdWithTeamAccess({
- id: phoneNumberId,
- teamId,
- userId,
- })
+ id: phoneNumberId,
+ teamId,
+ userId,
+ })
: await this.deps.phoneNumberRepository.findByIdAndUserId({
- id: phoneNumberId,
- userId,
- });
+ id: phoneNumberId,
+ userId,
+ });
if (!phoneNumber) {
throw new HttpError({
diff --git a/packages/features/ee/billing/api/webhook/_checkout.session.completed.ts b/packages/features/ee/billing/api/webhook/_checkout.session.completed.ts
index 32b9908858..a8a0c64d2e 100644
--- a/packages/features/ee/billing/api/webhook/_checkout.session.completed.ts
+++ b/packages/features/ee/billing/api/webhook/_checkout.session.completed.ts
@@ -1,5 +1,6 @@
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
import stripe from "@calcom/features/ee/payments/server/stripe";
+import logger from "@calcom/lib/logger";
import { PrismaAgentRepository } from "@calcom/lib/server/repository/PrismaAgentRepository";
import { PrismaPhoneNumberRepository } from "@calcom/lib/server/repository/PrismaPhoneNumberRepository";
import { CreditsRepository } from "@calcom/lib/server/repository/credits";
@@ -10,9 +11,36 @@ import { CHECKOUT_SESSION_TYPES } from "../../constants";
import type { SWHMap } from "./__handler";
import { HttpCode } from "./__handler";
+const log = logger.getSubLogger({ prefix: ["checkout.session.completed"] });
+
const handler = async (data: SWHMap["checkout.session.completed"]["data"]) => {
const session = data.object;
+ // Store ad tracking data in Stripe customer metadata for Zapier tracking
+ if (session.customer && session.metadata) {
+ try {
+ const trackingMetadata = {
+ gclid: session.metadata?.gclid,
+ campaignId: session.metadata?.campaignId,
+ liFatId: session.metadata?.liFatId,
+ linkedInCampaignId: session.metadata?.linkedInCampaignId,
+ };
+
+ const cleanedMetadata = Object.fromEntries(
+ Object.entries(trackingMetadata).filter(([_, value]) => value)
+ );
+
+ if (Object.keys(cleanedMetadata).length > 0) {
+ const customerId = typeof session.customer === "string" ? session.customer : session.customer.id;
+ await stripe.customers.update(customerId, {
+ metadata: cleanedMetadata,
+ });
+ }
+ } catch (error) {
+ log.error("Failed to update Stripe customer metadata with ad tracking data", { error });
+ }
+ }
+
if (session.metadata?.type === CHECKOUT_SESSION_TYPES.PHONE_NUMBER_SUBSCRIPTION) {
return await handleCalAIPhoneNumberSubscription(session);
}
diff --git a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx
index fb6550d266..0f754af683 100644
--- a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx
+++ b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx
@@ -1,7 +1,6 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useLayoutEffect, useState } from "react";
@@ -32,7 +31,6 @@ import { ImageUploader } from "@calcom/ui/components/image-uploader";
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
import { revalidateTeamDataCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
-import { revalidateTeamsList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/teams/actions";
import { subdomainSuffix } from "../../../organizations/lib/orgDomains";
@@ -54,7 +52,6 @@ const OtherTeamProfileView = () => {
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useUtils();
- const session = useSession();
const [firstRender, setFirstRender] = useState(true);
useLayoutEffect(() => {
@@ -133,19 +130,6 @@ const OtherTeamProfileView = () => {
},
});
- const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({
- async onSuccess() {
- await utils.viewer.teams.get.invalidate();
- await utils.viewer.teams.list.invalidate();
- revalidateTeamsList();
- await utils.viewer.eventTypes.invalidate();
- showToast(t("success"), "success");
- },
- async onError(err) {
- showToast(err.message, "error");
- },
- });
-
const publishMutation = trpc.viewer.teams.publish.useMutation({
async onSuccess(data: { url?: string }) {
if (data.url) {
@@ -161,14 +145,6 @@ const OtherTeamProfileView = () => {
if (team?.id) deleteTeamMutation.mutate({ teamId: team.id });
}
- function leaveTeam() {
- if (team?.id && session.data)
- removeMemberMutation.mutate({
- teamIds: [team.id],
- memberIds: [session.data.user.id],
- });
- }
-
if (!team) return null;
return (
@@ -291,7 +267,6 @@ const OtherTeamProfileView = () => {
>
diff --git a/packages/features/ee/teams/lib/payments.ts b/packages/features/ee/teams/lib/payments.ts
index 1c6129a049..834532ff98 100644
--- a/packages/features/ee/teams/lib/payments.ts
+++ b/packages/features/ee/teams/lib/payments.ts
@@ -17,6 +17,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import { BillingPeriod } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
+import { TrackingData } from "@calcom/lib/tracking";
const log = logger.getSubLogger({ prefix: ["teams/lib/payments"] });
const teamPaymentMetadataSchema = z.object({
@@ -51,11 +52,13 @@ export const generateTeamCheckoutSession = async ({
teamSlug,
userId,
isOnboarding,
+ tracking,
}: {
teamName: string;
teamSlug: string;
userId: number;
isOnboarding?: boolean;
+ tracking?: TrackingData;
}) => {
const [customer, dubCustomer] = await Promise.all([
getStripeCustomerIdFromUserId(userId),
@@ -66,15 +69,15 @@ export const generateTeamCheckoutSession = async ({
mode: "subscription",
...(dubCustomer?.discount?.couponId
? {
- discounts: [
- {
- coupon:
- process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId
- ? dubCustomer.discount.couponTestId
- : dubCustomer.discount.couponId,
- },
- ],
- }
+ discounts: [
+ {
+ coupon:
+ process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId
+ ? dubCustomer.discount.couponTestId
+ : dubCustomer.discount.couponId,
+ },
+ ],
+ }
: { allow_promotion_codes: true }),
success_url: `${WEBAPP_URL}/api/teams/create?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${WEBAPP_URL}/settings/my-account/profile`,
@@ -102,6 +105,8 @@ export const generateTeamCheckoutSession = async ({
userId,
dubCustomerId: userId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
...(isOnboarding !== undefined && { isOnboarding: isOnboarding.toString() }),
+ ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds?.campaignId }),
+ ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }),
},
});
return session;
@@ -127,6 +132,7 @@ export const purchaseTeamOrOrgSubscription = async (input: {
isOrg?: boolean;
pricePerSeat: number | null;
billingPeriod?: BillingPeriod;
+ tracking?: TrackingData;
}) => {
const {
teamId,
@@ -136,6 +142,7 @@ export const purchaseTeamOrOrgSubscription = async (input: {
isOrg,
pricePerSeat,
billingPeriod = BillingPeriod.MONTHLY,
+ tracking,
} = input;
const { url } = await checkIfTeamPaymentRequired({ teamId });
if (url) return { url };
@@ -193,6 +200,8 @@ export const purchaseTeamOrOrgSubscription = async (input: {
},
metadata: {
teamId,
+ ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds.campaignId }),
+ ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }),
},
subscription_data: {
metadata: {
diff --git a/packages/lib/cookie.ts b/packages/lib/cookie.ts
index 454a0192e0..de428541eb 100644
--- a/packages/lib/cookie.ts
+++ b/packages/lib/cookie.ts
@@ -7,3 +7,10 @@ export const getCookie = (name: string) => {
.find((cookie) => cookie.trim().startsWith(`${name}=`))
?.split("=")[1];
};
+
+export const setCookie = (name: string, value: string, options: string) => {
+ if (typeof document === "undefined") {
+ return;
+ }
+ document.cookie = `${name}=${value}; ${options}`;
+};
diff --git a/packages/lib/tracking/index.ts b/packages/lib/tracking/index.ts
new file mode 100644
index 0000000000..b01b1fabdf
--- /dev/null
+++ b/packages/lib/tracking/index.ts
@@ -0,0 +1,7 @@
+export {
+ getTrackingFromCookies,
+ type TrackingData,
+ type GoogleAdsTrackingData,
+ type LinkedInAdsTrackingData,
+} from "./server";
+
diff --git a/packages/lib/tracking/server.ts b/packages/lib/tracking/server.ts
new file mode 100644
index 0000000000..baa6806773
--- /dev/null
+++ b/packages/lib/tracking/server.ts
@@ -0,0 +1,39 @@
+import type { NextApiRequest } from "next";
+
+export type GoogleAdsTrackingData = {
+ gclid: string;
+ campaignId?: string;
+};
+
+export type LinkedInAdsTrackingData = {
+ liFatId: string;
+ campaignId?: string;
+};
+
+export type TrackingData = {
+ googleAds?: GoogleAdsTrackingData;
+ linkedInAds?: LinkedInAdsTrackingData;
+};
+
+
+export function getTrackingFromCookies(cookies?: NextApiRequest["cookies"]): TrackingData {
+ const tracking: TrackingData = {};
+
+ if (!cookies) return tracking;
+
+ if (process.env.GOOGLE_ADS_ENABLED === "1" && cookies.gclid) {
+ tracking.googleAds = {
+ gclid: cookies.gclid,
+ ...(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 }),
+ };
+ }
+
+ return tracking;
+}
diff --git a/packages/trpc/server/routers/viewer/organizations/_router.tsx b/packages/trpc/server/routers/viewer/organizations/_router.tsx
index 1e18bf49a0..dc5652c19f 100644
--- a/packages/trpc/server/routers/viewer/organizations/_router.tsx
+++ b/packages/trpc/server/routers/viewer/organizations/_router.tsx
@@ -1,3 +1,4 @@
+import type { NextApiRequest } from "next";
import { createPhoneCallSchema } from "@calcom/features/calAIPhone/zod-utils";
import { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
@@ -77,9 +78,9 @@ export const viewerOrganizationsRouter = router({
const { default: handler } = await import("./checkIfOrgNeedsUpgrade.handler");
return handler(opts);
}),
- publish: authedProcedure.mutation(async (opts) => {
+ publish: authedProcedure.mutation(async ({ ctx }) => {
const { default: handler } = await import("./publish.handler");
- return handler(opts);
+ return handler({ ctx: { ...ctx, req: ctx.req as NextApiRequest } });
}),
setPassword: authedProcedure.input(ZSetPasswordSchema).mutation(async (opts) => {
const { default: handler } = await import("./setPassword.handler");
diff --git a/packages/trpc/server/routers/viewer/organizations/publish.handler.ts b/packages/trpc/server/routers/viewer/organizations/publish.handler.ts
index fb6abecc4c..afa43ac3e5 100644
--- a/packages/trpc/server/routers/viewer/organizations/publish.handler.ts
+++ b/packages/trpc/server/routers/viewer/organizations/publish.handler.ts
@@ -1,7 +1,10 @@
+import type { NextApiRequest } from "next";
+
import { getRequestedSlugError } from "@calcom/app-store/stripepayment/lib/team-billing";
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
+import { getTrackingFromCookies } from "@calcom/lib/tracking";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
@@ -13,6 +16,7 @@ import type { TrpcSessionUser } from "../../../types";
type PublishOptions = {
ctx: {
user: NonNullable;
+ req?: NextApiRequest;
};
};
@@ -51,6 +55,8 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
// Since this is an ORG we need to make sure ORG members are scyned with the team. Every time a user is added to the TEAM, we need to add them to the ORG
if (IS_TEAM_BILLING_ENABLED) {
+ const tracking = getTrackingFromCookies(ctx.req?.cookies);
+
const checkoutSession = await purchaseTeamOrOrgSubscription({
teamId: prevTeam.id,
seatsUsed: prevTeam.members.length,
@@ -61,6 +67,7 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
isOrg: true,
pricePerSeat: metadata.data?.orgPricePerSeat ?? null,
billingPeriod: metadata.data?.billingPeriod ?? undefined,
+ tracking,
});
if (!checkoutSession.url)
@@ -79,10 +86,9 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
}
const { requestedSlug, ...newMetadata } = metadata.data;
- let updatedTeam: Awaited>;
try {
- updatedTeam = await prisma.team.update({
+ await prisma.team.update({
where: { id: orgId },
data: {
slug: requestedSlug,
diff --git a/packages/trpc/server/routers/viewer/phoneNumber/_router.ts b/packages/trpc/server/routers/viewer/phoneNumber/_router.ts
index 556767fd00..faa515af8d 100644
--- a/packages/trpc/server/routers/viewer/phoneNumber/_router.ts
+++ b/packages/trpc/server/routers/viewer/phoneNumber/_router.ts
@@ -1,3 +1,5 @@
+import type { NextApiRequest } from "next";
+
import authedProcedure from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { ZBuyInputSchema } from "./buy.schema";
@@ -21,7 +23,7 @@ export const phoneNumberRouter = router({
const { buyHandler } = await import("./buy.handler");
return buyHandler({
- ctx,
+ ctx: { ...ctx, req: ctx.req as NextApiRequest },
input,
});
}),
diff --git a/packages/trpc/server/routers/viewer/phoneNumber/buy.handler.ts b/packages/trpc/server/routers/viewer/phoneNumber/buy.handler.ts
index 5d37674d4f..426cc63156 100644
--- a/packages/trpc/server/routers/viewer/phoneNumber/buy.handler.ts
+++ b/packages/trpc/server/routers/viewer/phoneNumber/buy.handler.ts
@@ -1,4 +1,7 @@
+import type { NextApiRequest } from "next";
+
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
+import { getTrackingFromCookies } from "@calcom/lib/tracking";
import { TRPCError } from "@trpc/server";
@@ -8,6 +11,7 @@ import type { TBuyInputSchema } from "./buy.schema";
type BuyHandlerOptions = {
ctx: {
user: NonNullable;
+ req?: NextApiRequest;
};
input: TBuyInputSchema;
};
@@ -15,12 +19,14 @@ type BuyHandlerOptions = {
export const buyHandler = async ({ ctx, input }: BuyHandlerOptions) => {
const userId = ctx.user.id;
const aiService = createDefaultAIPhoneServiceProvider();
+ const tracking = getTrackingFromCookies(ctx.req?.cookies);
const checkoutSession = await aiService.generatePhoneNumberCheckoutSession({
userId,
teamId: input?.teamId ?? undefined,
agentId: input.agentId,
workflowId: input.workflowId,
+ tracking,
});
if (checkoutSession) {
diff --git a/packages/trpc/server/routers/viewer/teams/_router.tsx b/packages/trpc/server/routers/viewer/teams/_router.tsx
index b199fc5a54..b0dce5f91a 100644
--- a/packages/trpc/server/routers/viewer/teams/_router.tsx
+++ b/packages/trpc/server/routers/viewer/teams/_router.tsx
@@ -1,3 +1,5 @@
+import type { NextApiRequest } from "next";
+
import authedProcedure from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { ZAcceptOrLeaveInputSchema } from "./acceptOrLeave.schema";
@@ -34,9 +36,6 @@ import { ZUpdateInputSchema } from "./update.schema";
import { ZUpdateInternalNotesPresetsInputSchema } from "./updateInternalNotesPresets.schema";
import { ZUpdateMembershipInputSchema } from "./updateMembership.schema";
-const NAMESPACE = "teams";
-const namespaced = (s: string) => `${NAMESPACE}.${s}`;
-
export const viewerTeamsRouter = router({
// Retrieves team by id
get: authedProcedure.input(ZGetSchema).query(async (opts) => {
@@ -53,9 +52,9 @@ export const viewerTeamsRouter = router({
const { default: handler } = await import("./list.handler");
return handler(opts);
}),
- create: authedProcedure.input(ZCreateInputSchema).mutation(async (opts) => {
+ create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
const { default: handler } = await import("./create.handler");
- return handler(opts);
+ return handler({ ctx: { ...ctx, req: ctx.req as NextApiRequest }, input });
}),
// Allows team owner to update team metadata
update: authedProcedure.input(ZUpdateInputSchema).mutation(async (opts) => {
diff --git a/packages/trpc/server/routers/viewer/teams/create.handler.ts b/packages/trpc/server/routers/viewer/teams/create.handler.ts
index 735f10e493..df2150a1e1 100644
--- a/packages/trpc/server/routers/viewer/teams/create.handler.ts
+++ b/packages/trpc/server/routers/viewer/teams/create.handler.ts
@@ -1,8 +1,12 @@
+import type { NextApiRequest } from "next";
+
import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { uploadLogo } from "@calcom/lib/server/avatar";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
+import { getTrackingFromCookies } from "@calcom/lib/tracking";
+import type { TrackingData } from "@calcom/lib/tracking";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -14,6 +18,7 @@ import type { TCreateInputSchema } from "./create.schema";
type CreateOptions = {
ctx: {
user: NonNullable;
+ req?: NextApiRequest;
};
input: TCreateInputSchema;
};
@@ -23,11 +28,13 @@ const generateCheckoutSession = async ({
teamName,
userId,
isOnboarding,
+ tracking,
}: {
teamSlug: string;
teamName: string;
userId: number;
isOnboarding?: boolean;
+ tracking?: TrackingData;
}) => {
if (!IS_TEAM_BILLING_ENABLED) {
console.info("Team billing is disabled, not generating a checkout session.");
@@ -39,6 +46,7 @@ const generateCheckoutSession = async ({
teamName,
userId,
isOnboarding,
+ tracking,
});
if (!checkoutSession.url)
@@ -79,11 +87,14 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
// If the user is not a part of an org, then make them pay before creating the team
if (!isOrgChildTeam) {
+ const tracking = getTrackingFromCookies(ctx.req?.cookies);
+
const checkoutSession = await generateCheckoutSession({
teamSlug: slug,
teamName: name,
userId: user.id,
isOnboarding,
+ tracking,
});
// If there is a checkout session, return it. Otherwise, it means it's disabled.
diff --git a/packages/trpc/server/routers/viewer/teams/publish.handler.ts b/packages/trpc/server/routers/viewer/teams/publish.handler.ts
index 979e576d23..f16e79a85d 100644
--- a/packages/trpc/server/routers/viewer/teams/publish.handler.ts
+++ b/packages/trpc/server/routers/viewer/teams/publish.handler.ts
@@ -1,11 +1,8 @@
-import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
-import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
+import { WEBAPP_URL } from "@calcom/lib/constants";
import { Redirect } from "@calcom/lib/redirect";
-import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
-import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -19,39 +16,6 @@ type PublishOptions = {
input: TPublishInputSchema;
};
-const parseMetadataOrThrow = (metadata: Prisma.JsonValue) => {
- const parsedMetadata = teamMetadataSchema.safeParse(metadata);
-
- if (!parsedMetadata.success || !parsedMetadata.data)
- throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid team metadata" });
- return parsedMetadata.data;
-};
-
-const generateCheckoutSession = async ({
- teamId,
- seats,
- userId,
-}: {
- teamId: number;
- seats: number;
- userId: number;
-}) => {
- if (!IS_TEAM_BILLING_ENABLED) return;
-
- const checkoutSession = await purchaseTeamOrOrgSubscription({
- teamId,
- seatsUsed: seats,
- userId,
- pricePerSeat: null,
- });
- if (!checkoutSession.url)
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "Failed retrieving a checkout session URL.",
- });
- return { url: checkoutSession.url, message: "Payment required to publish team" };
-};
-
async function checkPermissions({ ctx, input }: PublishOptions) {
const { profile } = ctx.user;
const permissionCheckService = new PermissionCheckService();
diff --git a/turbo.json b/turbo.json
index 80a0ecd74c..44790bbc4c 100644
--- a/turbo.json
+++ b/turbo.json
@@ -284,6 +284,9 @@
"ENTERPRISE_SLUGS",
"PLATFORM_ENTERPRISE_SLUGS",
"USE_POOL",
+ "STRIPE_ORG_PRODUCT_ID",
+ "GOOGLE_ADS_ENABLED",
+ "LINKEDIN_ADS_ENABLED",
"SEED_PLATFORM_OAUTH_CLIENT_ID",
"SEED_PLATFORM_OAUTH_CLIENT_SECRET"
],