fix: Disable custom templates for trialing teams (#18748)

* Create `checkIfTeamHasActivePlan` method on `stripe-billing-service`

* Add `checkIfTeamPlanIsActive` to `InternalTeamBilling`

* Create `hasActiveTeamPlan` trpc endpoint

* Check for active team plan when updating workflows

* Create `useHasActiveTeamPlan` hook

* Use `useHasActiveTeamPlan` on workflow edit page

* Type fixes

* Type fix

* Add `accepted` to team query
This commit is contained in:
Joe Au-Yeung
2025-01-20 14:48:33 +00:00
committed by GitHub
parent 7131a1f015
commit d6306dcd22
9 changed files with 88 additions and 9 deletions
@@ -7,4 +7,5 @@ export interface BillingService {
subscriptionItemId: string;
membershipCount: number;
}): Promise<void>;
checkIfTeamHasActivePlan(subscriptionId: string): Promise<boolean>;
}
@@ -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";
}
}
@@ -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);
}
}
@@ -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";
@@ -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);
const { data: _verifiedEmails } = trpc.viewer.workflows.getVerifiedEmails.useQuery({ teamId });
const timeFormat = getTimeFormatStringFromUserTimeFormat(props.user.timeFormat);
@@ -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`)) {
@@ -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}
@@ -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"]
}
+8
View File
@@ -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;
@@ -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";
@@ -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);
}),
});
@@ -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,
accepted: true,
},
},
},
});
if (!team) return false;
// Get the current team's subscription
const teamBillingService = new InternalTeamBilling(team);
return await teamBillingService.checkIfTeamHasActivePlan();
};
export default hasActiveTeamPlanHandler;
@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZHasActiveTeamPlanSchema = z.object({
teamId: z.number().optional(),
});
export type THasActiveTeamPlanSchema = z.infer<typeof ZHasActiveTeamPlanSchema>;
@@ -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,
@@ -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;