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>10070-meeting-booking-link-long-event-description-text-wrapping-is-odd
parent
fdef15712a
commit
d58924ecad
|
@ -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=
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
export { default } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
|
|
@ -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...",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 }),
|
||||
});
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, "")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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 }[];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./whatsappEventCancelledTemplate"
|
||||
export * from "./whatsappEventCompletedTemplate"
|
||||
export * from "./whatsappEventReminderTemplate"
|
||||
export * from "./whatsappEventRescheduledTemplate"
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue