Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Disable custom templates for trialing teams #18748

Merged
merged 9 commits into from
Jan 20, 2025
1 change: 1 addition & 0 deletions packages/features/ee/billing/billing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface BillingService {
subscriptionItemId: string;
membershipCount: number;
}): Promise<void>;
checkIfTeamHasActivePlan(subscriptionId: string): Promise<boolean>;
}
6 changes: 6 additions & 0 deletions packages/features/ee/billing/stripe-billling-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ export class StripeBillingService implements BillingService {
const checkoutSession = await this.stripe.checkout.sessions.retrieve(paymentId);
return checkoutSession.payment_status === "paid";
}
async checkIfTeamHasActivePlan(subscriptionId: string) {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
if (!subscription || !subscription.status) return false;

return subscription.status === "active";
}
}
6 changes: 6 additions & 0 deletions packages/features/ee/billing/teams/internal-team-billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,10 @@ export class InternalTeamBilling implements TeamBilling {
paymentRequired: false,
};
}
/** Used to check if the current team plan is active */
async checkIfTeamHasActivePlan() {
const { subscriptionId } = this.team.metadata;
if (!subscriptionId) return false;
return billing.checkIfTeamHasActivePlan(subscriptionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import "react-phone-number-input/style.css";

import { classNames } from "@calcom/lib";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import useHasPaidPlan from "@calcom/lib/hooks/useHasPaidPlan";
import { useHasActiveTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
Expand Down Expand Up @@ -85,14 +85,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const { t, i18n } = useLocale();
const utils = trpc.useUtils();

const { hasPaidPlan } = useHasPaidPlan();

const { step, form, reload, setReload, teamId } = props;
const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
{ teamId },
{ enabled: !!teamId }
);

const { hasActiveTeamPlan } = useHasActiveTeamPlan(teamId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joeauyeung Now we only allow custom templates on team workflows but not on User workflows for user's that are part of a team or org


const { data: _verifiedEmails } = trpc.viewer.workflows.getVerifiedEmails.useQuery({ teamId });

const timeFormat = getTimeFormatStringFromUserTimeFormat(props.user.timeFormat);
Expand Down Expand Up @@ -132,7 +132,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {

const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
const triggerOptions = getWorkflowTriggerOptions(t);
const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasPaidPlan);
const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasActiveTeamPlan);

if (step && form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.REMINDER) {
if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
Expand Down Expand Up @@ -918,7 +918,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refEmailSubject.current = e;
}}
rows={1}
disabled={props.readOnly || !hasPaidPlan}
disabled={props.readOnly || !hasActiveTeamPlan}
className="my-0 focus:ring-transparent"
required
{...restEmailSubjectForm}
Expand Down Expand Up @@ -951,7 +951,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
updateTemplate={updateTemplate}
firstRender={firstRender}
setFirstRender={setFirstRender}
editable={!props.readOnly && !isWhatsappAction(step.action) && hasPaidPlan}
editable={!props.readOnly && !isWhatsappAction(step.action) && hasActiveTeamPlan}
excludedToolbarItems={
!isSMSAction(step.action) ? [] : ["blockType", "bold", "italic", "link"]
}
Expand Down
8 changes: 8 additions & 0 deletions packages/lib/hooks/useHasPaidPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ export function useHasEnterprisePlan() {
return { isPending, hasTeamPlan: hasTeamPlan?.hasTeamPlan };
}

export function useHasActiveTeamPlan(teamId?: number) {
if (IS_SELF_HOSTED) return { isPending: false, hasActiveTeamPlan: true };

const { data, isPending } = trpc.viewer.teams.hasActiveTeamPlan.useQuery({ teamId });

return { isPending, hasActiveTeamPlan: !!data };
}

export default useHasPaidPlan;
8 changes: 8 additions & 0 deletions packages/trpc/server/routers/viewer/teams/_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ZGetSchema } from "./get.schema";
import { ZGetMemberAvailabilityInputSchema } from "./getMemberAvailability.schema";
import { ZGetMembershipbyUserInputSchema } from "./getMembershipbyUser.schema";
import { ZGetUserConnectedAppsInputSchema } from "./getUserConnectedApps.schema";
import { ZHasActiveTeamPlanSchema } from "./hasActiveTeamPlan.schema";
import { ZHasEditPermissionForUserSchema } from "./hasEditPermissionForUser.schema";
import { ZInviteMemberInputSchema } from "./inviteMember/inviteMember.schema";
import { ZInviteMemberByTokenSchemaInputSchema } from "./inviteMemberByToken.schema";
Expand Down Expand Up @@ -222,4 +223,11 @@ export const viewerTeamsRouter = router({
);
return handler(opts);
}),
hasActiveTeamPlan: authedProcedure.input(ZHasActiveTeamPlanSchema).query(async (opts) => {
const handler = await importHandler(
namespaced("hasActiveTeamPlan"),
() => import("./hasActiveTeamPlan.handler")
);
return handler(opts);
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { InternalTeamBilling } from "@calcom/ee/billing/teams/internal-team-billing";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

import type { THasActiveTeamPlanSchema } from "./hasActiveTeamPlan.schema";

type HasActiveTeamPlanOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: THasActiveTeamPlanSchema;
};

export const hasActiveTeamPlanHandler = async ({ input, ctx }: HasActiveTeamPlanOptions) => {
if (IS_SELF_HOSTED) return true;

if (!input.teamId) return false;

const userId = ctx.user.id;

// Check if the user is a member of the requested team
const team = await prisma.team.findFirst({
where: {
id: input.teamId,
members: {
some: {
userId: userId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also add accepted: true

accepted: true,
},
},
},
});
if (!team) return false;

// Get the current team's subscription
const teamBillingService = new InternalTeamBilling(team);
return await teamBillingService.checkIfTeamHasActivePlan();
};

export default hasActiveTeamPlanHandler;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const ZHasActiveTeamPlanSchema = z.object({
teamId: z.number().optional(),
});

export type THasActiveTeamPlanSchema = z.infer<typeof ZHasActiveTeamPlanSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

import { TRPCError } from "@trpc/server";

import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
import hasActiveTeamPlanHandler from "../teams/hasActiveTeamPlan.handler";
import type { TUpdateInputSchema } from "./update.schema";
import {
getSender,
Expand Down Expand Up @@ -82,8 +82,10 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {

let isTeamsPlan = false;
if (!isCurrentUsernamePremium) {
const { hasTeamPlan } = await hasTeamPlanHandler({ ctx });
isTeamsPlan = !!hasTeamPlan;
isTeamsPlan = await hasActiveTeamPlanHandler({
ctx,
input: { teamId: userWorkflow?.teamId ?? undefined },
});
}
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;

Expand Down
Loading