Skip to content

Commit

Permalink
fix: disable custom templates for free plan (#18635)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Jan 15, 2025
1 parent f137aee commit 36dd3e9
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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`)) {
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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}
/>
);
}}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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"]
}
Expand Down
8 changes: 8 additions & 0 deletions packages/features/ee/workflows/lib/actionHelperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
16 changes: 12 additions & 4 deletions packages/features/ee/workflows/lib/getOptions.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -39,14 +39,22 @@ 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
: action && isEmailToAttendeeAction(action)
? 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 }[];
}
53 changes: 47 additions & 6 deletions packages/trpc/server/routers/viewer/workflows/update.handler.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -20,6 +23,7 @@ import {
verifyEmailSender,
removeSmsReminderFieldForEventTypes,
isStepEdited,
getEmailTemplateText,
} from "./util";

type UpdateOptions = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
28 changes: 28 additions & 0 deletions packages/trpc/server/routers/viewer/workflows/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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 };
}

0 comments on commit 36dd3e9

Please sign in to comment.