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:
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user