feat: New workflow action to send Whatsapp message (#8818)

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
jemiluv8 2023-07-11 15:48:44 +00:00 committed by GitHub
parent fdef15712a
commit d58924ecad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 863 additions and 149 deletions

View File

@ -121,6 +121,7 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
TWILIO_WHATSAPP_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=

View File

@ -0,0 +1,23 @@
name: Cron - scheduleWhatsappReminders
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron-scheduleWhatsappReminders:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/workflows/scheduleWhatsappReminders \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail

View File

@ -0,0 +1 @@
export { default } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";

View File

@ -1086,6 +1086,8 @@
"email_attendee_action": "send email to attendees",
"sms_attendee_action": "Send SMS to attendee",
"sms_number_action": "send SMS to a specific number",
"whatsapp_number_action": "send Whatsapp to a specific number",
"whatsapp_attendee_action": "send Whatsapp to attendee",
"workflows": "Workflows",
"new_workflow_btn": "New Workflow",
"add_new_workflow": "Add a new workflow",
@ -1147,8 +1149,10 @@
"specific_issue": "Have a specific issue?",
"browse_our_docs": "browse our docs",
"choose_template": "Choose a template",
"reminder": "Reminder",
"custom": "Custom",
"reminder": "Reminder",
"rescheduled": "Rescheduled",
"completed": "Completed",
"reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}",
"not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.",
"minute_one": "{{count}} minute",
@ -1491,7 +1495,7 @@
"team_url_required": "Must enter a team URL",
"url_taken": "This URL is already taken",
"team_publish": "Publish team",
"number_sms_notifications": "Phone number (SMS notifications)",
"number_text_notifications": "Phone number (Text notifications)",
"attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",

View File

@ -1490,7 +1490,6 @@
"team_url_required": "Vous devez saisir un lien d'équipe",
"url_taken": "Ce lien est déjà pris",
"team_publish": "Publier l'équipe",
"number_sms_notifications": "Numéro de téléphone (notifications par SMS)",
"attendee_email_variable": "Adresse e-mail du participant",
"attendee_email_info": "Adresse e-mail du participant",
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",

View File

@ -23,7 +23,7 @@ export const getSmsReminderNumberField = () =>
({
name: SMS_REMINDER_NUMBER_FIELD,
type: "phone",
defaultLabel: "number_sms_notifications",
defaultLabel: "number_text_notifications",
defaultPlaceholder: "enter_phone_number",
editable: "system",
} as const);
@ -136,7 +136,7 @@ export const ensureBookingInputsHaveSystemFields = ({
const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>;
workflows.forEach((workflow) => {
workflow.workflow.steps.forEach((step) => {
if (step.action === "SMS_ATTENDEE") {
if (step.action === "SMS_ATTENDEE" || step.action === "WHATSAPP_ATTENDEE") {
const workflowId = workflow.workflow.id;
smsNumberSources.push(
getSmsReminderNumberSource({

View File

@ -13,6 +13,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import { deleteScheduledWhatsappReminder } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
@ -655,6 +656,8 @@ async function handler(req: CustomRequest) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
});

View File

@ -41,6 +41,8 @@ import {
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import { deleteScheduledWhatsappReminder } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
@ -1976,6 +1978,8 @@ async function handler(
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
} catch (error) {

View File

@ -0,0 +1,111 @@
/* Schedule any workflow reminder that falls within 7 days for WHATSAPP */
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import * as twilio from "../lib/reminders/smsProviders/twilioProvider";
import { getWhatsappTemplateFunction } from "../lib/actionHelperFunctions";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
res.status(401).json({ message: "Not authenticated" });
return;
}
//delete all scheduled whatsapp reminders where scheduled date is past current date
await prisma.workflowReminder.deleteMany({
where: {
method: WorkflowMethods.WHATSAPP,
scheduledDate: {
lte: dayjs().toISOString(),
},
},
});
//find all unscheduled WHATSAPP reminders
const unscheduledReminders = await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.WHATSAPP,
scheduled: false,
scheduledDate: {
lte: dayjs().add(7, "day").toISOString(),
},
},
include: {
workflowStep: true,
booking: {
include: {
eventType: true,
user: true,
attendees: true,
},
},
},
});
if (!unscheduledReminders.length) res.json({ ok: true });
for (const reminder of unscheduledReminders) {
if (!reminder.workflowStep || !reminder.booking) {
continue;
}
try {
const sendTo =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_NUMBER
? reminder.workflowStep.sendTo
: reminder.booking?.smsReminderNumber;
const userName =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_ATTENDEE
? reminder.booking?.attendees[0].name
: "";
const attendeeName =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_ATTENDEE
? reminder.booking?.user?.name
: reminder.booking?.attendees[0].name;
const timeZone =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_ATTENDEE
? reminder.booking?.attendees[0].timeZone
: reminder.booking?.user?.timeZone;
const templateFunction = getWhatsappTemplateFunction(reminder.workflowStep.template)
const message = templateFunction(
false,
reminder.workflowStep.action,
reminder.booking?.startTime.toISOString() || "",
reminder.booking?.eventType?.title || "",
timeZone || "",
attendeeName || "",
userName
);
if (message?.length && message?.length > 0 && sendTo) {
const scheduledSMS = await twilio.scheduleSMS(sendTo, message, reminder.scheduledDate, "", true);
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: scheduledSMS.sid,
},
});
}
} catch (error) {
console.log(`Error scheduling WHATSAPP with error ${error}`);
}
}
res.status(200).json({ message: "WHATSAPP scheduled" });
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});

View File

@ -5,8 +5,7 @@ import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { SENDER_ID } from "@calcom/lib/constants";
import { SENDER_NAME } from "@calcom/lib/constants";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WorkflowActions } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
@ -94,6 +93,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
setIsPhoneNumberNeeded(true);
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
form.resetField("senderId", { defaultValue: SENDER_ID })
} else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true);
setIsSenderIdNeeded(false);
@ -102,6 +102,11 @@ export const AddActionDialog = (props: IAddActionDialog) => {
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false);
form.resetField("senderId", { defaultValue: SENDER_ID })
} else if (newValue.value === WorkflowActions.WHATSAPP_NUMBER) {
setIsSenderIdNeeded(false);
setIsPhoneNumberNeeded(true);
setIsEmailAddressNeeded(false);
} else {
setIsSenderIdNeeded(false);
setIsEmailAddressNeeded(false);
@ -116,6 +121,20 @@ export const AddActionDialog = (props: IAddActionDialog) => {
if (!actionOptions) return null;
const canRequirePhoneNumber = (workflowStep: string) => {
return (
WorkflowActions.SMS_ATTENDEE === workflowStep ||
WorkflowActions.WHATSAPP_ATTENDEE === workflowStep
)
}
const showSender = (action: string) => {
return !isSenderIdNeeded && !(
WorkflowActions.WHATSAPP_NUMBER === action ||
WorkflowActions.WHATSAPP_ATTENDEE === action
)
}
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent type="creation" title={t("add_action")}>
@ -167,7 +186,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
{isPhoneNumberNeeded && (
<div className="mt-5 space-y-1">
<Label htmlFor="sendTo">{t("phone_number")}</Label>
<div className="mb-5 mt-1">
<div className="mt-1 mb-5">
<Controller
control={form.control}
name="sendTo"
@ -193,7 +212,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<EmailField required label={t("email_address")} {...form.register("sendTo")} />
</div>
)}
{isSenderIdNeeded ? (
{isSenderIdNeeded && (
<>
<div className="mt-5">
<div className="flex">
@ -208,13 +227,14 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p>
)}
</>
) : (
)}
{showSender(form.getValues('action')) && (
<div className="mt-5">
<Label>{t("sender_name")}</Label>
<Input type="text" placeholder={SENDER_NAME} {...form.register(`senderName`)} />
</div>
)}
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (
{canRequirePhoneNumber(form.getValues("action")) && (
<div className="mt-5">
<Controller
name="numberRequired"

View File

@ -89,14 +89,12 @@ const WorkflowListItem = (props: ItemProps) => {
sendTo.add(t("organizer"));
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo.add(t("attendee_name_variable"));
break;
case WorkflowActions.SMS_ATTENDEE:
case WorkflowActions.WHATSAPP_ATTENDEE:
sendTo.add(t("attendee_name_variable"));
break;
case WorkflowActions.SMS_NUMBER:
sendTo.add(step.sendTo || "");
break;
case WorkflowActions.WHATSAPP_NUMBER:
case WorkflowActions.EMAIL_ADDRESS:
sendTo.add(step.sendTo || "");
break;

View File

@ -13,7 +13,7 @@ import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
import { Button, Label, MultiSelectCheckboxes, TextField } from "@calcom/ui";
import { ArrowDown, Trash2 } from "@calcom/ui/components/icon";
import { isSMSAction } from "../lib/actionHelperFunctions";
import { isSMSAction, isWhatsappAction } from "../lib/actionHelperFunctions";
import type { FormValues } from "../pages/workflow";
import { AddActionDialog } from "./AddActionDialog";
import { DeleteDialog } from "./DeleteDialog";
@ -98,7 +98,7 @@ export default function WorkflowDetailsPage(props: Props) {
workflowId: workflowId,
reminderBody: null,
emailSubject: null,
template: WorkflowTemplates.CUSTOM,
template: isWhatsappAction(action) ? WorkflowTemplates.REMINDER : WorkflowTemplates.CUSTOM,
numberRequired: numberRequired || false,
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,

View File

@ -6,8 +6,7 @@ import { Controller } from "react-hook-form";
import "react-phone-number-input/style.css";
import { classNames } from "@calcom/lib";
import { SENDER_ID } from "@calcom/lib/constants";
import { SENDER_NAME } from "@calcom/lib/constants";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { WorkflowTemplates, TimeUnit, WorkflowActions } from "@calcom/prisma/enums";
@ -40,11 +39,12 @@ import {
} from "@calcom/ui";
import { ArrowDown, MoreHorizontal, Trash2, HelpCircle, Info } from "@calcom/ui/components/icon";
import { isAttendeeAction, isSMSAction } from "../lib/actionHelperFunctions";
import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants";
import { isAttendeeAction, isSMSAction, isSMSOrWhatsappAction, isWhatsappAction, getWhatsappTemplateForAction } from "../lib/actionHelperFunctions";
import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions";
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate";
import { whatsappReminderTemplate } from "../lib/reminders/templates/whatsapp";
import type { FormValues } from "../pages/workflow";
import { TimeTimeUnitInput } from "./TimeTimeUnitInput";
@ -71,24 +71,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const [verificationCode, setVerificationCode] = useState("");
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(
step?.action === WorkflowActions.SMS_NUMBER ? true : false
);
const action = step?.action
const requirePhoneNumber = WorkflowActions.SMS_NUMBER === action || WorkflowActions.WHATSAPP_NUMBER === action;
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(requirePhoneNumber);
const [updateTemplate, setUpdateTemplate] = useState(false);
const [firstRender, setFirstRender] = useState(true);
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(
step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE
? true
: false
);
useEffect(() => {
setNumberVerified(
!!step &&
!!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
);
}, [verifiedNumbers.length]);
const senderNeeded = step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE;
const [isSenderIsNeeded, setIsSenderIsNeeded] = useState(senderNeeded);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
@ -112,17 +104,25 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
);
const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
const triggerOptions = getWorkflowTriggerOptions(t);
const templateOptions = getWorkflowTemplateOptions(t);
const templateOptions = getWorkflowTemplateOptions(t, step?.action);
if (step && form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.REMINDER) {
if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
if (isSMSAction(form.getValues(`steps.${step.stepNumber - 1}.action`))) {
const smsBody = smsReminderTemplate(true, form.getValues(`steps.${step.stepNumber - 1}.action`));
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, smsBody);
const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
if (isSMSAction(action)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, smsReminderTemplate(
true,
action
));
} else if (isWhatsappAction(action)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, whatsappReminderTemplate(
true,
action
))
} else {
const reminderBodyTemplate = emailReminderTemplate(
true,
form.getValues(`steps.${step.stepNumber - 1}.action`)
action
).emailBody;
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate);
}
@ -134,6 +134,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
).emailSubject;
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate);
}
} else if (step && isWhatsappAction(step.action)) {
const templateBody = getWhatsappTemplateForAction(step.action, step.template)
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, templateBody)
}
const { ref: emailSubjectFormRef, ...restEmailSubjectForm } = step
@ -148,10 +151,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const refReminderBody = useRef<HTMLTextAreaElement | null>(null);
const [numberVerified, setNumberVerified] = useState(
step &&
!!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
);
const getNumberVerificationStatus = () => !!step && !!verifiedNumbers.find((number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
const [numberVerified, setNumberVerified] = useState(getNumberVerificationStatus());
useEffect(() => setNumberVerified(getNumberVerificationStatus()), [verifiedNumbers.length]);
const addVariableBody = (variable: string) => {
if (step) {
@ -232,17 +236,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
return (
<>
<div className="flex justify-center">
<div className="min-w-80 bg-default border-subtle w-full rounded-md border p-7">
<div className="w-full border rounded-md min-w-80 bg-default border-subtle p-7">
<div className="flex">
<div className="bg-subtle text-default mt-[3px] flex h-5 w-5 items-center justify-center rounded-full p-1 text-xs font-medium ltr:mr-5 rtl:ml-5">
1
</div>
<div>
<div className="text-emphasis text-base font-bold">{t("trigger")}</div>
<div className="text-default text-sm">{t("when_something_happens")}</div>
<div className="text-base font-bold text-emphasis">{t("trigger")}</div>
<div className="text-sm text-default">{t("when_something_happens")}</div>
</div>
</div>
<div className="border-subtle my-7 border-t" />
<div className="border-t border-subtle my-7" />
<Label>{t("when")}</Label>
<Controller
name="trigger"
@ -287,7 +291,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Label>{showTimeSectionAfter ? t("how_long_after") : t("how_long_before")}</Label>
<TimeTimeUnitInput form={form} disabled={props.readOnly} />
{!props.readOnly && (
<div className="mt-1 flex text-gray-500">
<div className="flex mt-1 text-gray-500">
<Info className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_workflow_info_message")}</p>
</div>
@ -301,6 +305,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}
if (step && step.action) {
const templateValue = form.watch(`steps.${step.stepNumber - 1}.template`);
const actionString = t(`${step.action.toLowerCase()}_action`);
const selectedAction = {
@ -311,13 +316,20 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const selectedTemplate = { label: t(`${step.template.toLowerCase()}`), value: step.template };
const canRequirePhoneNumber = (workflowStep: string) => {
return (
WorkflowActions.SMS_ATTENDEE === workflowStep ||
WorkflowActions.WHATSAPP_ATTENDEE === workflowStep
)
}
return (
<>
<div className="my-3 flex justify-center">
<div className="flex justify-center my-3">
<ArrowDown className="text-subtle stroke-[1.5px] text-3xl" />
</div>
<div className="flex justify-center">
<div className="min-w-80 bg-default border-subtle flex w-full rounded-md border p-7">
<div className="flex w-full border rounded-md min-w-80 bg-default border-subtle p-7">
<div className="w-full">
<div className="flex">
<div className="w-full">
@ -326,8 +338,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{step.stepNumber + 1}
</div>
<div>
<div className="text-emphasis text-base font-bold">{t("action")}</div>
<div className="text-default text-sm">{t("action_is_performed")}</div>
<div className="text-base font-bold text-emphasis">{t("action")}</div>
<div className="text-sm text-default">{t("action_is_performed")}</div>
</div>
</div>
</div>
@ -367,7 +379,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
)}
</div>
<div className="border-subtle my-7 border-t" />
<div className="border-t border-subtle my-7" />
<div>
<Label>{t("do_this")}</Label>
<Controller
@ -383,22 +395,35 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);
if (isSMSAction(val.value)) {
setIsSenderIdNeeded(true);
const setNumberRequiredConfigs = (phoneNumberIsNeeded: boolean, senderNeeded = true) => {
setIsSenderIsNeeded(senderNeeded);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(val.value === WorkflowActions.SMS_NUMBER);
setNumberVerified(false);
setIsPhoneNumberNeeded(phoneNumberIsNeeded);
setNumberVerified(getNumberVerificationStatus());
}
if (isSMSAction(val.value)) {
setNumberRequiredConfigs(val.value === WorkflowActions.SMS_NUMBER)
// email action changes to sms action
if (!isSMSAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.sender`, SENDER_ID);
}
setIsEmailSubjectNeeded(false);
} else if (isWhatsappAction(val.value)) {
setNumberRequiredConfigs(val.value === WorkflowActions.WHATSAPP_NUMBER, false);
if (!isWhatsappAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.sender`, "");
}
setIsEmailSubjectNeeded(false);
} else {
setIsPhoneNumberNeeded(false);
setIsSenderIdNeeded(false);
setIsSenderIsNeeded(false);
setIsEmailAddressNeeded(val.value === WorkflowActions.EMAIL_ADDRESS);
setIsEmailSubjectNeeded(true);
}
@ -407,7 +432,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
form.getValues(`steps.${step.stepNumber - 1}.template`) ===
WorkflowTemplates.REMINDER
) {
if (isSMSAction(val.value) === isSMSAction(oldValue)) {
if (isSMSOrWhatsappAction(val.value) === isSMSOrWhatsappAction(oldValue)) {
if (isAttendeeAction(oldValue) !== isAttendeeAction(val.value)) {
const currentReminderBody =
form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) || "";
@ -417,7 +442,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
.replaceAll("{PLACEHOLDER}", "{ATTENDEE}");
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, newReminderBody);
if (!isSMSAction(val.value)) {
if (!isSMSOrWhatsappAction(val.value)) {
const currentEmailSubject =
form.getValues(`steps.${step.stepNumber - 1}.emailSubject`) || "";
const newEmailSubject = isAttendeeAction(val.value)
@ -436,6 +461,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
`steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(true, val.value)
);
} else if (isWhatsappAction(val.value)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
whatsappReminderTemplate(true, val.value)
);
} else {
const emailReminderBody = emailReminderTemplate(true, val.value);
form.setValue(
@ -448,6 +478,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
);
}
}
} else {
const template = isWhatsappAction(val.value) ? "REMINDER" : "CUSTOM";
template && form.setValue(`steps.${step.stepNumber - 1}.template`, template);
}
form.unregister(`steps.${step.stepNumber - 1}.sendTo`);
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
@ -468,7 +501,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
/>
</div>
{isPhoneNumberNeeded && (
<div className="bg-muted mt-2 rounded-md p-4 pt-0">
<div className="p-4 pt-0 mt-2 rounded-md bg-muted">
<Label className="pt-4">{t("custom_phone_number")}</Label>
<div className="block sm:flex">
<Controller
@ -520,7 +553,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
) : (
!props.readOnly && (
<>
<div className="mt-3 flex">
<div className="flex mt-3">
<TextField
className="rounded-r-none border-r-transparent"
placeholder="Verification code"
@ -556,8 +589,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)}
</div>
)}
<div className="bg-muted mt-2 rounded-md p-4 pt-0">
{isSenderIdNeeded ? (
{!isWhatsappAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (<div className="p-4 pt-0 mt-2 rounded-md bg-muted">
{isSenderIsNeeded ? (
<>
<div className="pt-4">
<div className="flex">
@ -592,8 +625,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
</>
)}
</div>
{form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && (
</div>)}
{canRequirePhoneNumber(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div className="mt-2">
<Controller
name={`steps.${step.stepNumber - 1}.numberRequired`}
@ -614,7 +647,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
)}
{isEmailAddressNeeded && (
<div className="bg-muted mt-5 rounded-md p-4">
<div className="p-4 mt-5 rounded-md bg-muted">
<EmailField
required
disabled={props.readOnly}
@ -628,7 +661,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Controller
name={`steps.${step.stepNumber - 1}.template`}
control={form.control}
render={() => {
render={({ field }) => {
return (
<Select
isSearchable={false}
@ -636,47 +669,53 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`)
if (val.value === WorkflowTemplates.REMINDER) {
if (isSMSAction(form.getValues(`steps.${step.stepNumber - 1}.action`))) {
if (isWhatsappAction(action)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(
whatsappReminderTemplate(
true,
form.getValues(`steps.${step.stepNumber - 1}.action`)
action
)
);
} else if (isSMSAction(action)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(true,action)
);
} else {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
emailReminderTemplate(
true,
form.getValues(`steps.${step.stepNumber - 1}.action`)
).emailBody
emailReminderTemplate(true, action).emailBody
);
form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
emailReminderTemplate(
true,
form.getValues(`steps.${step.stepNumber - 1}.action`)
).emailSubject
emailReminderTemplate(true, action).emailSubject
);
}
} else {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, "");
if (isWhatsappAction(action)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, getWhatsappTemplateForAction(action, val.value))
} else {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, "");
}
}
field.onChange(val.value)
form.setValue(`steps.${step.stepNumber - 1}.template`, val.value);
setUpdateTemplate(!updateTemplate);
}
}}
defaultValue={selectedTemplate}
value={selectedTemplate}
options={templateOptions}
/>
);
}}
/>
</div>
<div className="bg-muted mt-2 rounded-md pt-2 md:p-6 md:pt-4">
<div className="pt-2 mt-2 rounded-md bg-muted md:p-6 md:pt-4">
{isEmailSubjectNeeded && (
<div className="mb-6">
<div className="flex items-center">
@ -716,7 +755,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
step.action !== WorkflowActions.SMS_NUMBER ? (
<>
<div className="mb-2 flex items-center pb-[1.5px]">
<Label className="mb-0 flex-none ">
<Label className="flex-none mb-0 ">
{isEmailSubjectNeeded ? t("email_body") : t("text_message")}
</Label>
</div>
@ -730,10 +769,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}}
variables={DYNAMIC_TEXT_VARIABLES}
height="200px"
editable={!props.readOnly}
updateTemplate={updateTemplate}
firstRender={firstRender}
setFirstRender={setFirstRender}
editable={!props.readOnly && !isWhatsappAction(step.action)}
/>
</>
) : (
@ -756,7 +795,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
reminderBodyFormRef?.(e);
refReminderBody.current = e;
}}
className="my-0 h-24"
className="h-24 my-0"
disabled={props.readOnly}
required
{...restReminderBodyForm}
@ -772,7 +811,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{!props.readOnly && (
<div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="text-default mt-2 flex text-sm">
<div className="flex mt-2 text-sm text-default">
<HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-left">{t("using_booking_questions_as_variables")}</p>
</div>
@ -884,23 +923,23 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<DialogContent type="creation" className="sm:max-w-[610px]">
<div className="-m-3 h-[430px] overflow-x-hidden overflow-y-scroll sm:m-0">
<h1 className="w-full text-xl font-semibold ">{t("how_booking_questions_as_variables")}</h1>
<div className="bg-muted-3 mb-6 rounded-md sm:p-4">
<p className="test-sm font-medium">{t("format")}</p>
<ul className="text-emphasis ml-5 mt-2 list-disc">
<div className="mb-6 rounded-md bg-muted-3 sm:p-4">
<p className="font-medium test-sm">{t("format")}</p>
<ul className="mt-2 ml-5 list-disc text-emphasis">
<li>{t("uppercase_for_letters")}</li>
<li>{t("replace_whitespaces_underscores")}</li>
<li>{t("ignore_special_characters_booking_questions")}</li>
</ul>
<div className="mt-4">
<p className="test-sm w-full font-medium">{t("example_1")}</p>
<div className="mt-2 grid grid-cols-12">
<div className="test-sm text-default col-span-5 ltr:mr-2 rtl:ml-2">
<p className="w-full font-medium test-sm">{t("example_1")}</p>
<div className="grid grid-cols-12 mt-2">
<div className="col-span-5 test-sm text-default ltr:mr-2 rtl:ml-2">
{t("booking_question_identifier")}
</div>
<div className="test-sm text-emphasis col-span-7">{t("company_size")}</div>
<div className="test-sm text-default col-span-5 w-full">{t("variable")}</div>
<div className="col-span-7 test-sm text-emphasis">{t("company_size")}</div>
<div className="w-full col-span-5 test-sm text-default">{t("variable")}</div>
<div className="test-sm text-emphasis col-span-7 break-words">
<div className="col-span-7 break-words test-sm text-emphasis">
{" "}
{`{${t("company_size")
.replace(/[^a-zA-Z0-9 ]/g, "")
@ -911,14 +950,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
</div>
<div className="mt-4">
<p className="test-sm w-full font-medium">{t("example_2")}</p>
<div className="mt-2 grid grid-cols-12">
<div className="test-sm text-default col-span-5 ltr:mr-2 rtl:ml-2">
<p className="w-full font-medium test-sm">{t("example_2")}</p>
<div className="grid grid-cols-12 mt-2">
<div className="col-span-5 test-sm text-default ltr:mr-2 rtl:ml-2">
{t("booking_question_identifier")}
</div>
<div className="test-sm text-emphasis col-span-7">{t("what_help_needed")}</div>
<div className="test-sm text-default col-span-5">{t("variable")}</div>
<div className="test-sm text-emphasis col-span-7 break-words">
<div className="col-span-7 test-sm text-emphasis">{t("what_help_needed")}</div>
<div className="col-span-5 test-sm text-default">{t("variable")}</div>
<div className="col-span-7 break-words test-sm text-emphasis">
{" "}
{`{${t("what_help_needed")
.replace(/[^a-zA-Z0-9 ]/g, "")

View File

@ -1,9 +1,55 @@
import { WorkflowActions } from "@calcom/prisma/enums";
import { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@prisma/client";
import { whatsappEventCancelledTemplate, whatsappEventCompletedTemplate, whatsappEventRescheduledTemplate, whatsappReminderTemplate } from "../lib/reminders/templates/whatsapp";
export function isSMSAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
}
export function isAttendeeAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.EMAIL_ATTENDEE;
export function isWhatsappAction(action: WorkflowActions) {
return action === WorkflowActions.WHATSAPP_NUMBER || action === WorkflowActions.WHATSAPP_ATTENDEE;
}
export function isSMSOrWhatsappAction(action: WorkflowActions) {
return isSMSAction(action) || isWhatsappAction(action)
}
export function isAttendeeAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
}
export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): WorkflowTemplates {
switch(trigger) {
case "NEW_EVENT":
case "BEFORE_EVENT":
return WorkflowTemplates.REMINDER
case "AFTER_EVENT":
return WorkflowTemplates.COMPLETED
case "EVENT_CANCELLED":
return WorkflowTemplates.CANCELLED
case "RESCHEDULE_EVENT":
return WorkflowTemplates.RESCHEDULED
default:
return WorkflowTemplates.REMINDER
}
}
export function getWhatsappTemplateFunction(template: WorkflowTemplates): typeof whatsappReminderTemplate {
switch(template) {
case "CANCELLED":
return whatsappEventCancelledTemplate
case "COMPLETED":
return whatsappEventCompletedTemplate
case "RESCHEDULED":
return whatsappEventRescheduledTemplate
case "CUSTOM":
case "REMINDER":
return whatsappReminderTemplate
default:
return whatsappReminderTemplate
}
}
export function getWhatsappTemplateForAction(action: WorkflowActions, template: WorkflowTemplates): string | null {
const templateFunction = getWhatsappTemplateFunction(template)
return templateFunction(true, action)
}

View File

@ -14,11 +14,31 @@ export const WORKFLOW_ACTIONS = [
WorkflowActions.EMAIL_ADDRESS,
WorkflowActions.SMS_ATTENDEE,
WorkflowActions.SMS_NUMBER,
WorkflowActions.WHATSAPP_ATTENDEE,
WorkflowActions.WHATSAPP_NUMBER,
] as const;
export const TIME_UNIT = [TimeUnit.DAY, TimeUnit.HOUR, TimeUnit.MINUTE] as const;
export const WORKFLOW_TEMPLATES = [WorkflowTemplates.CUSTOM, WorkflowTemplates.REMINDER] as const;
export const WORKFLOW_TEMPLATES = [
WorkflowTemplates.CUSTOM,
WorkflowTemplates.REMINDER,
WorkflowTemplates.CANCELLED,
WorkflowTemplates.COMPLETED,
WorkflowTemplates.RESCHEDULED
] as const;
export const BASIC_WORKFLOW_TEMPLATES = [
WorkflowTemplates.CUSTOM,
WorkflowTemplates.REMINDER,
] as const;
export const WHATSAPP_WORKFLOW_TEMPLATES = [
WorkflowTemplates.REMINDER,
WorkflowTemplates.COMPLETED,
WorkflowTemplates.CANCELLED,
WorkflowTemplates.RESCHEDULED
] as const;
export const DYNAMIC_TEXT_VARIABLES = [
"event_name",

View File

@ -3,6 +3,7 @@ import type { WorkflowStep } from "@prisma/client";
import { classNames } from "@calcom/lib";
import { WorkflowActions } from "@calcom/prisma/enums";
import { Zap, Smartphone, Mail, Bell } from "@calcom/ui/components/icon";
import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
export function getActionIcon(steps: WorkflowStep[], className?: string): JSX.Element {
if (steps.length === 0) {
@ -10,7 +11,7 @@ export function getActionIcon(steps: WorkflowStep[], className?: string): JSX.El
}
if (steps.length === 1) {
if (steps[0].action === WorkflowActions.SMS_ATTENDEE || steps[0].action === WorkflowActions.SMS_NUMBER) {
if (isSMSOrWhatsappAction(steps[0].action)) {
return (
<Smartphone
className={classNames(className ? className : "mr-1.5 inline h-3 w-3")}
@ -29,15 +30,9 @@ export function getActionIcon(steps: WorkflowStep[], className?: string): JSX.El
for (const step of steps) {
if (!messageType) {
messageType =
step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER
? "SMS"
: "EMAIL";
messageType = isSMSOrWhatsappAction(step.action) ? "SMS" : "EMAIL";
} else if (messageType !== "MIX") {
const newMessageType =
step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER
? "SMS"
: "EMAIL";
const newMessageType = isSMSOrWhatsappAction(step.action) ? "SMS" : "EMAIL";
if (newMessageType !== messageType) {
messageType = "MIX";
}

View File

@ -2,8 +2,8 @@ import type { TFunction } from "next-i18next";
import { WorkflowActions } from "@calcom/prisma/enums";
import { isSMSAction } from "./actionHelperFunctions";
import { TIME_UNIT, WORKFLOW_ACTIONS, WORKFLOW_TEMPLATES, WORKFLOW_TRIGGER_EVENTS } from "./constants";
import { isSMSOrWhatsappAction, isWhatsappAction } from "./actionHelperFunctions";
import { TIME_UNIT, WHATSAPP_WORKFLOW_TEMPLATES, WORKFLOW_ACTIONS, BASIC_WORKFLOW_TEMPLATES, WORKFLOW_TRIGGER_EVENTS } from "./constants";
export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) {
return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode
@ -13,7 +13,7 @@ export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) {
return {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: action,
needsUpgrade: isSMSAction(action) && !isTeamsPlan,
needsUpgrade: isSMSOrWhatsappAction(action) && !isTeamsPlan,
};
});
}
@ -32,8 +32,9 @@ export function getWorkflowTimeUnitOptions(t: TFunction) {
});
}
export function getWorkflowTemplateOptions(t: TFunction) {
return WORKFLOW_TEMPLATES.map((template) => {
export function getWorkflowTemplateOptions(t: TFunction, action: WorkflowActions | undefined) {
const TEMPLATES = (action && isWhatsappAction(action)) ? WHATSAPP_WORKFLOW_TEMPLATES : BASIC_WORKFLOW_TEMPLATES;
return TEMPLATES.map((template) => {
return { label: t(`${template.toLowerCase()}`), value: template };
});
}) as { label: string; value: any }[];
}

View File

@ -8,6 +8,9 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
import { scheduleEmailReminder } from "./emailReminderManager";
import { scheduleSMSReminder } from "./smsReminderManager";
import { scheduleWhatsappReminder } from "./whatsappReminderManager";
import { isWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
type ExtendedCalendarEvent = CalendarEvent & {
metadata?: { videoCallUrl: string | undefined };
@ -109,6 +112,24 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
step.sender || SENDER_NAME,
hideBranding
);
} else if (isWhatsappAction(step.action)) {
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleWhatsappReminder(
evt,
sendTo,
workflow.trigger,
step.action,
{
time: workflow.time,
timeUnit: workflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
workflow.userId,
workflow.teamId,
step.numberVerificationPending
);
}
}
}
@ -186,6 +207,24 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
step.sender || SENDER_NAME,
hideBranding
);
} else if (isWhatsappAction(step.action)) {
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleWhatsappReminder(
evt,
sendTo,
workflow.trigger,
step.action,
{
time: workflow.time,
timeUnit: workflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
workflow.userId,
workflow.teamId,
step.numberVerificationPending
);
}
}
}

View File

@ -19,27 +19,39 @@ function assertTwilio(twilio: TwilioClient.Twilio | undefined): asserts twilio i
if (!twilio) throw new Error("Twilio credentials are missing from the .env file");
}
export const sendSMS = async (phoneNumber: string, body: string, sender: string) => {
function getDefaultSender(whatsapp = false) {
let defaultSender = process.env.TWILIO_PHONE_NUMBER
if (whatsapp) {
defaultSender = `whatsapp:+${process.env.TWILIO_WHATSAPP_PHONE_NUMBER}`
}
return defaultSender
}
function getSMSNumber(phone: string, whatsapp = false) {
return whatsapp ? `whatsapp:${phone}` : phone;
}
export const sendSMS = async (phoneNumber: string, body: string, sender: string, whatsapp = false) => {
assertTwilio(twilio);
const response = await twilio.messages.create({
body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
to: phoneNumber,
from: sender ? sender : process.env.TWILIO_PHONE_NUMBER,
to: getSMSNumber(phoneNumber, whatsapp),
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
});
return response;
};
export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date, sender: string) => {
export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date, sender: string, whatsapp = false) => {
assertTwilio(twilio);
const response = await twilio.messages.create({
body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
to: phoneNumber,
to: getSMSNumber(phoneNumber, whatsapp),
scheduleType: "fixed",
sendAt: scheduledDate,
from: sender ? sender : process.env.TWILIO_PHONE_NUMBER,
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
});
return response;

View File

@ -0,0 +1,4 @@
export * from "./whatsappEventCancelledTemplate"
export * from "./whatsappEventCompletedTemplate"
export * from "./whatsappEventReminderTemplate"
export * from "./whatsappEventRescheduledTemplate"

View File

@ -0,0 +1,35 @@
import dayjs from "@calcom/dayjs";
import { WorkflowActions } from "@prisma/client";
export const whatsappEventCancelledTemplate = (
isEditingMode: boolean,
action?: WorkflowActions,
startTime?: string,
eventName?: string,
timeZone?: string,
attendee?: string,
name?: string
) => {
let eventDate;
if (isEditingMode) {
eventName = "{EVENT_NAME}";
timeZone = "{TIMEZONE}";
startTime = "{START_TIME_h:mmA}";
eventDate = "{EVENT_DATE_YYYY MMM D}";
attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}";
name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}";
} else {
eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D");
startTime = dayjs(startTime).tz(timeZone).format("h:mmA");
}
const templateOne = `Hi${
name ? ` ${name}` : ``
}, your meeting (*${eventName}*) with ${attendee} on ${eventDate} at ${startTime} ${timeZone} has been canceled.`
//Twilio supports up to 1024 characters for whatsapp template messages
if (templateOne.length <= 1024) return templateOne;
return null;
};

View File

@ -0,0 +1,35 @@
import dayjs from "@calcom/dayjs";
import { WorkflowActions } from "@prisma/client";
export const whatsappEventCompletedTemplate = (
isEditingMode: boolean,
action?: WorkflowActions,
startTime?: string,
eventName?: string,
timeZone?: string,
attendee?: string,
name?: string
) => {
let eventDate;
if (isEditingMode) {
eventName = "{EVENT_NAME}";
timeZone = "{TIMEZONE}";
startTime = "{START_TIME_h:mmA}";
eventDate = "{EVENT_DATE_YYYY MMM D}";
attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}";
name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}";
} else {
eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D");
startTime = dayjs(startTime).tz(timeZone).format("h:mmA");
}
const templateOne = `Hi${
name ? ` ${name}` : ``
}, thank you for attending the event (*${eventName}*) on ${eventDate} at ${startTime} ${timeZone}.`;
//Twilio supports up to 1024 characters for whatsapp template messages
if (templateOne.length <= 1024) return templateOne;
return null;
};

View File

@ -0,0 +1,36 @@
import { WorkflowActions } from "@prisma/client";
import dayjs from "@calcom/dayjs";
export const whatsappReminderTemplate = (
isEditingMode: boolean,
action?: WorkflowActions,
startTime?: string,
eventName?: string,
timeZone?: string,
attendee?: string,
name?: string
) => {
let eventDate;
if (isEditingMode) {
eventName = "{EVENT_NAME}";
timeZone = "{TIMEZONE}";
startTime = "{START_TIME_h:mmA}";
eventDate = "{EVENT_DATE_YYYY MMM D}";
attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}";
name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}";
} else {
eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D");
startTime = dayjs(startTime).tz(timeZone).format("h:mmA");
}
const templateOne = `Hi${
name ? ` ${name}` : ``
}, this is a reminder that your meeting (*${eventName}*) with ${attendee} is on ${eventDate} at ${startTime} ${timeZone}.`;
//Twilio supports up to 1024 characters for whatsapp template messages
if (templateOne.length <= 1024) return templateOne;
return null;
};

View File

@ -0,0 +1,36 @@
import { WorkflowActions } from "@prisma/client";
import dayjs from "@calcom/dayjs";
export const whatsappEventRescheduledTemplate = (
isEditingMode: boolean,
action?: WorkflowActions,
startTime?: string,
eventName?: string,
timeZone?: string,
attendee?: string,
name?: string
) => {
let eventDate;
if (isEditingMode) {
eventName = "{EVENT_NAME}";
timeZone = "{TIMEZONE}";
startTime = "{START_TIME_h:mmA}";
eventDate = "{EVENT_DATE_YYYY MMM D}";
attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}";
name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}";
} else {
eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D");
startTime = dayjs(startTime).tz(timeZone).format("h:mmA");
}
const templateOne = `Hi${
name ? ` ${name}` : ``
}, your meeting (*${eventName}*) with ${attendee} on ${eventDate} at ${startTime} ${timeZone} has been rescheduled.`;
//Twilio supports up to 1024 characters for whatsapp template messages
if (templateOne.length <= 1024) return templateOne;
return null;
};

View File

@ -0,0 +1,141 @@
import type { TimeUnit } from "@prisma/client";
import { WorkflowTriggerEvents, WorkflowTemplates, WorkflowActions, WorkflowMethods } from "@prisma/client";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import logger from "@calcom/lib/logger";
import { BookingInfo, deleteScheduledSMSReminder, timeUnitLowerCase } from "./smsReminderManager";
import * as twilio from "./smsProviders/twilioProvider";
import { whatsappEventCancelledTemplate, whatsappEventCompletedTemplate, whatsappEventRescheduledTemplate, whatsappReminderTemplate } from "./templates/whatsapp";
const log = logger.getChildLogger({ prefix: ["[whatsappReminderManager]"] });
export const scheduleWhatsappReminder = async (
evt: BookingInfo,
reminderPhone: string | null,
triggerEvent: WorkflowTriggerEvents,
action: WorkflowActions,
timeSpan: {
time: number | null;
timeUnit: TimeUnit | null;
},
message: string,
workflowStepId: number,
template: WorkflowTemplates,
userId?: number | null,
teamId?: number | null,
isVerificationPending = false
) => {
const { startTime, endTime } = evt;
const uid = evt.uid as string;
const currentDate = dayjs();
const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase;
let scheduledDate = null;
//WHATSAPP_ATTENDEE action does not need to be verified
//isVerificationPending is from all already existing workflows (once they edit their workflow, they will also have to verify the number)
async function getIsNumberVerified() {
if (action === WorkflowActions.WHATSAPP_ATTENDEE) return true;
const verifiedNumber = await prisma.verifiedNumber.findFirst({
where: {
OR: [{ userId }, { teamId }],
phoneNumber: reminderPhone || "",
},
});
if (!!verifiedNumber) return true;
return isVerificationPending;
}
const isNumberVerified = await getIsNumberVerified();
if (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT) {
scheduledDate = timeSpan.time && timeUnit ? dayjs(startTime).subtract(timeSpan.time, timeUnit) : null;
} else if (triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) {
scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null;
}
const name = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].name : evt.organizer.name;
const attendeeName = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.organizer.name : evt.attendees[0].name;
const timeZone =
action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].timeZone : evt.organizer.timeZone;
switch(template) {
case WorkflowTemplates.REMINDER:
message = whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message;
break;
case WorkflowTemplates.CANCELLED:
message = whatsappEventCancelledTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message;
break
case WorkflowTemplates.RESCHEDULED:
message = whatsappEventRescheduledTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message;
break;
case WorkflowTemplates.COMPLETED:
message = whatsappEventCompletedTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message;
break
default:
message = whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message;
}
// Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages
log.debug(`Sending Whatsapp for trigger ${triggerEvent}`, message);
if (message.length > 0 && reminderPhone && isNumberVerified) {
//send WHATSAPP when event is booked/cancelled/rescheduled
if (
triggerEvent === WorkflowTriggerEvents.NEW_EVENT ||
triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED ||
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) {
try {
await twilio.sendSMS(reminderPhone, message, "", true);
} catch (error) {
console.log(`Error sending WHATSAPP with error ${error}`);
}
} else if (
(triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) &&
scheduledDate
) {
// Can only schedule at least 60 minutes in advance and at most 7 days in advance
if (
currentDate.isBefore(scheduledDate.subtract(1, "hour")) &&
!scheduledDate.isAfter(currentDate.add(7, "day"))
) {
try {
const scheduledWHATSAPP = await twilio.scheduleSMS(
reminderPhone,
message,
scheduledDate.toDate(),
"",
true
);
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId: workflowStepId,
method: WorkflowMethods.WHATSAPP,
scheduledDate: scheduledDate.toDate(),
scheduled: true,
referenceId: scheduledWHATSAPP.sid,
},
});
} catch (error) {
console.log(`Error scheduling WHATSAPP with error ${error}`);
}
} else if (scheduledDate.isAfter(currentDate.add(7, "day"))) {
// Write to DB and send to CRON if scheduled reminder date is past 7 days
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId: workflowStepId,
method: WorkflowMethods.WHATSAPP,
scheduledDate: scheduledDate.toDate(),
scheduled: false,
},
});
}
}
}
};
export const deleteScheduledWhatsappReminder = deleteScheduledSMSReminder;

View File

@ -22,7 +22,7 @@ import { Alert, Button, Form, showToast, Badge } from "@calcom/ui";
import LicenseRequired from "../../common/components/LicenseRequired";
import SkeletonLoader from "../components/SkeletonLoaderEdit";
import WorkflowDetailsPage from "../components/WorkflowDetailsPage";
import { isSMSAction } from "../lib/actionHelperFunctions";
import { isSMSAction, isSMSOrWhatsappAction } from "../lib/actionHelperFunctions";
import { getTranslatedText, translateVariablesToEnglish } from "../lib/variableTranslations";
export type FormValues = {
@ -202,7 +202,7 @@ function WorkflowPage() {
values.steps.forEach((step) => {
const strippedHtml = step.reminderBody?.replace(/<[^>]+>/g, "") || "";
const isBodyEmpty = !isSMSAction(step.action) && strippedHtml.length <= 1;
const isBodyEmpty = !isSMSOrWhatsappAction(step.action) && strippedHtml.length <= 1;
if (isBodyEmpty) {
form.setError(`steps.${step.stepNumber - 1}.reminderBody`, {
@ -221,7 +221,7 @@ function WorkflowPage() {
//check if phone number is verified
if (
step.action === WorkflowActions.SMS_NUMBER &&
(step.action === WorkflowActions.SMS_NUMBER || step.action === WorkflowActions.WHATSAPP_NUMBER) &&
!verifiedNumbers?.find((verifiedNumber) => verifiedNumber.phoneNumber === step.sendTo)
) {
isVerified = false;

View File

@ -0,0 +1,16 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "WorkflowActions" ADD VALUE 'WHATSAPP_ATTENDEE';
ALTER TYPE "WorkflowActions" ADD VALUE 'WHATSAPP_NUMBER';
ALTER TYPE "WorkflowMethods" ADD VALUE 'WHATSAPP';
ALTER TYPE "WorkflowTemplates" ADD VALUE 'CANCELLED';
ALTER TYPE "WorkflowTemplates" ADD VALUE 'RESCHEDULED';
ALTER TYPE "WorkflowTemplates" ADD VALUE 'COMPLETED';

View File

@ -709,6 +709,8 @@ enum WorkflowActions {
SMS_ATTENDEE
SMS_NUMBER
EMAIL_ADDRESS
WHATSAPP_ATTENDEE
WHATSAPP_NUMBER
}
model WorkflowStep {
@ -793,11 +795,15 @@ model WorkflowReminder {
enum WorkflowTemplates {
REMINDER
CUSTOM
CANCELLED
RESCHEDULED
COMPLETED
}
enum WorkflowMethods {
EMAIL
SMS
WHATSAPP
}
model BookingSeat {

View File

@ -9,6 +9,7 @@ import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
import { deleteScheduledWhatsappReminder } from "@calcom/ee/workflows/lib/reminders/whatsappReminderManager";
import { sendRequestRescheduleEmail } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
@ -135,6 +136,8 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});

View File

@ -6,6 +6,10 @@ import {
deleteScheduledSMSReminder,
scheduleSMSReminder,
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import {
deleteScheduledWhatsappReminder,
scheduleWhatsappReminder,
} from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/client";
@ -87,7 +91,6 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
eventTypeId,
},
});
if (isActive) {
// disable workflow for this event type & delete all reminders
const remindersToDelete = await prisma.workflowReminder.findMany({
@ -115,6 +118,8 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
@ -221,6 +226,22 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
booking.userId,
eventTypeWorkflow.teamId
);
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) {
await scheduleWhatsappReminder(
bookingInfo,
step.sendTo,
eventTypeWorkflow.trigger,
step.action,
{
time: eventTypeWorkflow.time,
timeUnit: eventTypeWorkflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
booking.userId,
eventTypeWorkflow.teamId
);
}
}
}
@ -233,14 +254,12 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
},
].concat(userEventType.children.map((ch) => ({ workflowId, eventTypeId: ch.id }))),
});
const requiresAttendeeNumber = (action: WorkflowActions) =>
action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
if (
eventTypeWorkflow.steps.some((step) => {
return step.action === WorkflowActions.SMS_ATTENDEE;
})
) {
if (eventTypeWorkflow.steps.some((step) => requiresAttendeeNumber(step.action))) {
const isSmsReminderNumberRequired = eventTypeWorkflow.steps.some((step) => {
return step.action === WorkflowActions.SMS_ATTENDEE && step.numberRequired;
return requiresAttendeeNumber(step.action) && step.numberRequired;
});
[eventTypeId].concat(userEventType.children.map((ch) => ch.id)).map(async (evTyId) => {
await upsertSmsReminderFieldForBooking({

View File

@ -1,6 +1,6 @@
import type { Prisma } from "@prisma/client";
import { isSMSAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import {
deleteScheduledEmailReminder,
scheduleEmailReminder,
@ -9,6 +9,10 @@ import {
deleteScheduledSMSReminder,
scheduleSMSReminder,
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import {
deleteScheduledWhatsappReminder,
scheduleWhatsappReminder,
} from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import type { PrismaClient } from "@calcom/prisma/client";
@ -209,6 +213,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
@ -251,7 +257,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
});
steps.forEach(async (step) => {
if (step.action !== WorkflowActions.SMS_ATTENDEE) {
if (step.action !== WorkflowActions.SMS_ATTENDEE && step.action !== WorkflowActions.WHATSAPP_ATTENDEE) {
//as we do not have attendees phone number (user is notified about that when setting this action)
bookingsForReminders.forEach(async (booking) => {
const bookingInfo = {
@ -330,6 +336,22 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
user.id,
userWorkflow.teamId
);
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER) {
await scheduleWhatsappReminder(
bookingInfo,
step.sendTo || "",
trigger,
step.action,
{
time,
timeUnit,
},
step.reminderBody || "",
step.id || 0,
step.template,
user.id,
userWorkflow.teamId
);
}
});
}
@ -377,6 +399,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
}
@ -388,20 +412,21 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//step was edited
} else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) {
if (!hasPaidPlan && !isSMSAction(oldStep.action) && isSMSAction(newStep.action)) {
if (!hasPaidPlan && !isSMSOrWhatsappAction(oldStep.action) && isSMSOrWhatsappAction(newStep.action)) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const requiresSender =
newStep.action === WorkflowActions.SMS_NUMBER || newStep.action === WorkflowActions.WHATSAPP_NUMBER;
await ctx.prisma.workflowStep.update({
where: {
id: oldStep.id,
},
data: {
action: newStep.action,
sendTo:
newStep.action === WorkflowActions.SMS_NUMBER /*||
sendTo: requiresSender /*||
newStep.action === WorkflowActions.EMAIL_ADDRESS*/
? newStep.sendTo
: null,
? newStep.sendTo
: null,
stepNumber: newStep.stepNumber,
workflowId: newStep.workflowId,
reminderBody: newStep.reminderBody,
@ -533,6 +558,22 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
user.id,
userWorkflow.teamId
);
} else if (newStep.action === WorkflowActions.WHATSAPP_NUMBER) {
await scheduleWhatsappReminder(
bookingInfo,
newStep.sendTo || "",
trigger,
newStep.action,
{
time,
timeUnit,
},
newStep.reminderBody || "",
newStep.id || 0,
newStep.template,
user.id,
userWorkflow.teamId
);
}
});
}
@ -541,7 +582,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//added steps
const addedSteps = steps.map((s) => {
if (s.id <= 0) {
if (isSMSAction(s.action) && !hasPaidPlan) {
if (isSMSOrWhatsappAction(s.action) && !hasPaidPlan) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const { id: _stepId, ...stepToAdd } = s;
@ -569,7 +610,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
if (
(trigger === WorkflowTriggerEvents.BEFORE_EVENT || trigger === WorkflowTriggerEvents.AFTER_EVENT) &&
eventTypesToCreateReminders &&
step.action !== WorkflowActions.SMS_ATTENDEE
step.action !== WorkflowActions.SMS_ATTENDEE && step.action !== WorkflowActions.WHATSAPP_ATTENDEE
) {
const bookingsForReminders = await ctx.prisma.booking.findMany({
where: {
@ -663,6 +704,22 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
user.id,
userWorkflow.teamId
);
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) {
await scheduleWhatsappReminder(
bookingInfo,
step.sendTo,
trigger,
step.action,
{
time,
timeUnit,
},
step.reminderBody || "",
createdStep.id,
step.template,
user.id,
userWorkflow.teamId
);
}
}
}
@ -711,7 +768,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
// Remove or add booking field for sms reminder number
const smsReminderNumberNeeded =
activeOn.length && steps.some((step) => step.action === WorkflowActions.SMS_ATTENDEE);
activeOn.length &&
steps.some(
(step) =>
step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.WHATSAPP_ATTENDEE
);
for (const removedEventType of removedEventTypes) {
await removeSmsReminderFieldForBooking({
@ -725,7 +786,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
await upsertSmsReminderFieldForBooking({
workflowId: id,
isSmsReminderNumberRequired: steps.some(
(s) => s.action === WorkflowActions.SMS_ATTENDEE && s.numberRequired
(s) =>
(s.action === WorkflowActions.SMS_ATTENDEE || s.action === WorkflowActions.WHATSAPP_ATTENDEE) &&
s.numberRequired
),
eventTypeId,
});

View File

@ -1,6 +1,6 @@
import type { Workflow } from "@prisma/client";
import { isSMSAction } from "@calcom/ee/workflows/lib/actionHelperFunctions";
import { isSMSOrWhatsappAction } from "@calcom/ee/workflows/lib/actionHelperFunctions";
import {
getSmsReminderNumberField,
getSmsReminderNumberSource,
@ -15,7 +15,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
export function getSender(
step: Pick<WorkflowStep, "action" | "sender"> & { senderName: string | null | undefined }
) {
return isSMSAction(step.action) ? step.sender || SENDER_ID : step.senderName || SENDER_NAME;
return isSMSOrWhatsappAction(step.action) ? step.sender || SENDER_ID : step.senderName || SENDER_NAME;
}
export async function isAuthorized(

View File

@ -73,6 +73,10 @@ const useDefaultCountry = () => {
refetchOnReconnect: false,
retry: false,
onSuccess: (data) => {
if (!data?.countryCode) {
return;
}
isSupportedCountry(data?.countryCode)
? setDefaultCountry(data.countryCode.toLowerCase())
: setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us");