fix: disable custom templates for free plan (#18635)

* add upgrade badge

* adjust update handler

* code clean up

* clean up template code

* only handle email templates

* fixes

* code clean up

* fix workflow template zod type

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Carina Wollendorfer
2025-01-15 15:25:14 +00:00
committed by GitHub
co-authored by CarinaWolli Alex van Andel
parent f137aee436
commit 36dd3e9e6c
7 changed files with 114 additions and 17 deletions
@@ -12,7 +12,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
import { Button, Icon, Label, MultiSelectCheckboxes, TextField, CheckboxField, InfoBadge } from "@calcom/ui";
import { isSMSAction, isWhatsappAction } from "../lib/actionHelperFunctions";
import { isSMSAction } from "../lib/actionHelperFunctions";
import type { FormValues } from "../pages/workflow";
import { AddActionDialog } from "./AddActionDialog";
import { DeleteDialog } from "./DeleteDialog";
@@ -83,7 +83,7 @@ export default function WorkflowDetailsPage(props: Props) {
workflowId: workflowId,
reminderBody: null,
emailSubject: null,
template: isWhatsappAction(action) ? WorkflowTemplates.REMINDER : WorkflowTemplates.CUSTOM,
template: WorkflowTemplates.REMINDER,
numberRequired: numberRequired || false,
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,
@@ -8,6 +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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
@@ -84,6 +85,8 @@ 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 },
@@ -129,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);
const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasPaidPlan);
if (step && form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.REMINDER) {
if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
@@ -370,7 +373,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
needsTeamsUpgrade: false,
};
const selectedTemplate = { label: t(`${step.template.toLowerCase()}`), value: step.template };
const selectedTemplate = {
label: t(`${step.template.toLowerCase()}`),
value: step.template,
needsTeamsUpgrade: false,
};
const canRequirePhoneNumber = (workflowStep: string) => {
return (
@@ -879,6 +886,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
defaultValue={selectedTemplate}
value={selectedTemplate}
options={templateOptions}
isOptionDisabled={(option: {
label: string;
value: any;
needsTeamsUpgrade: boolean;
}) => option.needsTeamsUpgrade}
/>
);
}}
@@ -906,7 +918,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refEmailSubject.current = e;
}}
rows={1}
disabled={props.readOnly}
disabled={props.readOnly || !hasPaidPlan}
className="my-0 focus:ring-transparent"
required
{...restEmailSubjectForm}
@@ -939,7 +951,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
updateTemplate={updateTemplate}
firstRender={firstRender}
setFirstRender={setFirstRender}
editable={!props.readOnly && !isWhatsappAction(step.action)}
editable={!props.readOnly && !isWhatsappAction(step.action) && hasPaidPlan}
excludedToolbarItems={
!isSMSAction(step.action) ? [] : ["blockType", "bold", "italic", "link"]
}
@@ -30,6 +30,14 @@ export function isSMSOrWhatsappAction(action: WorkflowActions) {
return isSMSAction(action) || isWhatsappAction(action);
}
export function isEmailAction(action: WorkflowActions) {
return (
action === WorkflowActions.EMAIL_ADDRESS ||
action === WorkflowActions.EMAIL_ATTENDEE ||
action === WorkflowActions.EMAIL_HOST
);
}
export function isAttendeeAction(action: WorkflowActions) {
return (
action === WorkflowActions.SMS_ATTENDEE ||
@@ -1,7 +1,7 @@
import type { TFunction } from "next-i18next";
import type { WorkflowActions } from "@calcom/prisma/enums";
import { WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { isSMSOrWhatsappAction, isWhatsappAction, isEmailToAttendeeAction } from "./actionHelperFunctions";
import {
@@ -39,7 +39,11 @@ export function getWorkflowTriggerOptions(t: TFunction) {
});
}
export function getWorkflowTemplateOptions(t: TFunction, action: WorkflowActions | undefined) {
export function getWorkflowTemplateOptions(
t: TFunction,
action: WorkflowActions | undefined,
hasPaidPlan: boolean
) {
const TEMPLATES =
action && isWhatsappAction(action)
? WHATSAPP_WORKFLOW_TEMPLATES
@@ -47,6 +51,10 @@ export function getWorkflowTemplateOptions(t: TFunction, action: WorkflowActions
? ATTENDEE_WORKFLOW_TEMPLATES
: BASIC_WORKFLOW_TEMPLATES;
return TEMPLATES.map((template) => {
return { label: t(`${template.toLowerCase()}`), value: template };
}) as { label: string; value: any }[];
return {
label: t(`${template.toLowerCase()}`),
value: template,
needsTeamsUpgrade: !hasPaidPlan && template == WorkflowTemplates.CUSTOM,
};
}) as { label: string; value: any; needsTeamsUpgrade: boolean }[];
}
@@ -1,9 +1,12 @@
import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import {
isEmailAction,
isSMSOrWhatsappAction,
} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import type { PrismaClient } from "@calcom/prisma";
import { WorkflowActions } from "@calcom/prisma/enums";
import { WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
@@ -20,6 +23,7 @@ import {
verifyEmailSender,
removeSmsReminderFieldForEventTypes,
isStepEdited,
getEmailTemplateText,
} from "./util";
type UpdateOptions = {
@@ -333,8 +337,34 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
});
} else if (isStepEdited(oldStep, newStep)) {
// check if step that require team plan already existed before
if (!hasPaidPlan && !isSMSOrWhatsappAction(oldStep.action) && isSMSOrWhatsappAction(newStep.action)) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
if (!hasPaidPlan) {
const isChangingToSMSOrWhatsapp =
!isSMSOrWhatsappAction(oldStep.action) && isSMSOrWhatsappAction(newStep.action);
const isChangingToCustomTemplate =
newStep.template === WorkflowTemplates.CUSTOM && oldStep.template !== WorkflowTemplates.CUSTOM;
if (isChangingToSMSOrWhatsapp || isChangingToCustomTemplate) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
}
//if email body or subject was changed, change to predefined template
if (newStep.emailSubject !== oldStep.emailSubject || newStep.reminderBody !== oldStep.reminderBody) {
// already existing custom templates can't be updated
if (newStep.template === WorkflowTemplates.CUSTOM) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
}
if (isEmailAction(newStep.action)) {
// on free plans always use predefined templates
const { emailBody, emailSubject } = getEmailTemplateText(newStep.template, {
locale: ctx.user.locale,
action: newStep.action,
timeFormat: ctx.user.timeFormat,
});
newStep = { ...newStep, reminderBody: emailBody, emailSubject };
}
}
}
// update step
@@ -388,8 +418,19 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
steps
.filter((step) => step.id <= 0)
.map(async (newStep) => {
if (isSMSOrWhatsappAction(newStep.action) && !hasPaidPlan) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
if (!hasPaidPlan) {
if (isSMSOrWhatsappAction(newStep.action) || newStep.template === WorkflowTemplates.CUSTOM) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
}
// on free plans always use predefined templates
const { emailBody, emailSubject } = getEmailTemplateText(newStep.template, {
locale: ctx.user.locale,
action: newStep.action,
timeFormat: ctx.user.timeFormat,
});
newStep = { ...newStep, reminderBody: emailBody, emailSubject };
}
if (newStep.action === WorkflowActions.EMAIL_ADDRESS) {
@@ -1,9 +1,9 @@
import { z } from "zod";
import {
WORKFLOW_TEMPLATES,
TIME_UNIT,
WORKFLOW_ACTIONS,
WORKFLOW_TEMPLATES,
WORKFLOW_TRIGGER_EVENTS,
} from "@calcom/features/ee/workflows/lib/constants";
@@ -5,6 +5,8 @@ import { isSMSOrWhatsappAction } from "@calcom/ee/workflows/lib/actionHelperFunc
import { getAllWorkflows } from "@calcom/ee/workflows/lib/getAllWorkflows";
import { scheduleEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
import emailRatingTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailRatingTemplate";
import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate";
import { scheduleWhatsappReminder } from "@calcom/ee/workflows/lib/reminders/whatsappReminderManager";
import type { Workflow as WorkflowType } from "@calcom/ee/workflows/lib/types";
import { SMS_REMINDER_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField";
@@ -23,6 +25,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import type { Prisma, WorkflowStep } from "@calcom/prisma/client";
import type { TimeUnit } from "@calcom/prisma/enums";
import { WorkflowTemplates } from "@calcom/prisma/enums";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus, MembershipRole, WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -848,3 +851,28 @@ export const getEventTypeWorkflows = async (
return workflows.map((workflow) => ({ workflow }));
};
export function getEmailTemplateText(
template: WorkflowTemplates,
params: { locale: string; action: WorkflowActions; timeFormat: number | null }
) {
const { locale, action } = params;
const timeFormat = getTimeFormatStringFromUserTimeFormat(params.timeFormat);
let { emailBody, emailSubject } = emailReminderTemplate(true, locale, action, timeFormat);
if (template === WorkflowTemplates.RATING) {
const ratingTemplate = emailRatingTemplate({
isEditingMode: true,
locale,
action,
timeFormat,
});
emailBody = ratingTemplate.emailBody;
emailSubject = ratingTemplate.emailSubject;
}
return { emailBody, emailSubject };
}