+
{t("action")}
diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
index 04620593c9fb12..cc2e0a4826c217 100644
--- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
+++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
@@ -380,11 +380,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
});
const hasAiAction = hasCalAIAction(steps);
- const hasSMSAction = steps.some((s) => isSMSAction(s.action));
- const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action));
const hasEmailToHostAction = steps.some((s) => s.action === WorkflowActions.EMAIL_HOST);
+ const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action));
- const disallowFormTriggers = hasAiAction || hasSMSAction || hasEmailToHostAction || hasWhatsappAction;
+ const disallowFormTriggers = hasAiAction || hasEmailToHostAction || hasWhatsappAction;
const filteredTriggerOptions = triggerOptions.filter(
(option) => !(isFormTrigger(option.value) && disallowFormTriggers)
diff --git a/packages/features/ee/workflows/lib/constants.ts b/packages/features/ee/workflows/lib/constants.ts
index a9703887b0b3b7..fb937664a84b1a 100644
--- a/packages/features/ee/workflows/lib/constants.ts
+++ b/packages/features/ee/workflows/lib/constants.ts
@@ -108,4 +108,6 @@ export const FORM_TRIGGER_WORKFLOW_EVENTS: WorkflowTriggerEvents[] = [
export const ALLOWED_FORM_WORKFLOW_ACTIONS = [
WorkflowActions.EMAIL_ATTENDEE,
WorkflowActions.EMAIL_ADDRESS,
+ WorkflowActions.SMS_ATTENDEE,
+ WorkflowActions.SMS_NUMBER,
] as const;
diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
index cb1e4c9eae1637..70f22f1c43794a 100644
--- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
@@ -340,6 +340,7 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
});
};
+// sends all immediately, no scheduling needed
const scheduleEmailReminderForForm = async (
args: scheduleEmailReminderArgs & {
formData: FormSubmissionData;
diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
index d1f2609127e705..bc2ecd88eb9e47 100644
--- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
+++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
@@ -114,10 +114,6 @@ const processWorkflowStep = async (
};
if (isSMSAction(step.action)) {
- if (!evt) {
- // SMS action not not yet supported for form triggers
- return;
- }
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleSMSReminder({
@@ -127,7 +123,7 @@ const processWorkflowStep = async (
message: step.reminderBody || "",
sender: step.sender,
isVerificationPending: step.numberVerificationPending,
- evt,
+ ...contextData,
});
} else if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
index 6a037864f4477f..643cb60f2caa5b 100644
--- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
@@ -4,6 +4,7 @@ import {
getSMSMessageWithVariables,
shouldUseTwilio,
} from "@calcom/ee/workflows/lib/reminders/utils";
+import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation";
import { SENDER_ID } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
@@ -24,6 +25,7 @@ import { WorkflowOptOutService } from "../service/workflowOptOutService";
import type { ScheduleReminderArgs } from "./emailReminderManager";
import { scheduleSmsOrFallbackEmail, sendSmsOrFallbackEmail } from "./messageDispatcher";
import * as twilio from "./providers/twilioProvider";
+import type { FormSubmissionData } from "./reminderScheduler";
import smsReminderTemplate from "./templates/smsReminderTemplate";
export enum timeUnitLowerCase {
@@ -76,7 +78,6 @@ export type ScheduleTextReminderAction = Extract<
WorkflowActions,
"SMS_ATTENDEE" | "SMS_NUMBER" | "WHATSAPP_ATTENDEE" | "WHATSAPP_NUMBER"
>;
-
export type ScheduleTextReminderArgs = ScheduleReminderArgs & {
reminderPhone: string | null;
message: string;
@@ -87,43 +88,38 @@ export type ScheduleTextReminderArgs = ScheduleReminderArgs & {
prisma?: PrismaClient;
verifiedAt: Date | null;
};
-export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs & { evt: BookingInfo }) => {
- const {
- evt,
- reminderPhone,
- triggerEvent,
- action,
- timeSpan,
- message = "",
- workflowStepId,
- template,
- sender,
- userId,
- teamId,
- isVerificationPending = false,
- seatReferenceUid,
- verifiedAt,
- } = args;
+export type ScheduleTextReminderArgsWithRequiredFields = Omit<
+ ScheduleTextReminderArgs,
+ "reminderPhone" | "sender"
+> & {
+ reminderPhone: string; // Required, not nullable
+ sender: string; // Required, not nullable
+};
+
+export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
+ const { reminderPhone, sender, verifiedAt, workflowStepId, action, userId, teamId, isVerificationPending } =
+ args;
if (!verifiedAt) {
log.warn(`Workflow step ${workflowStepId} not yet verified`);
return;
}
- if (reminderPhone && (await WorkflowOptOutContactRepository.isOptedOut(reminderPhone))) {
- log.warn(
- `Phone number opted out of SMS workflows`,
- safeStringify({ workflowStep: workflowStepId, eventUid: evt.uid })
- );
+ if (!reminderPhone) {
+ log.warn(`No phone number provided for WhatsApp reminder in workflow step ${workflowStepId}`);
return;
}
- const { startTime, endTime } = evt;
- const uid = evt.uid as string;
- const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase;
- let scheduledDate = null;
+ if (await WorkflowOptOutContactRepository.isOptedOut(reminderPhone)) {
+ log.warn(`Phone number opted out of SMS workflows`, safeStringify({ workflowStep: workflowStepId }));
+ return;
+ }
const senderID = getSenderId(reminderPhone, sender || SENDER_ID);
+ const params: ScheduleTextReminderArgs = {
+ ...args,
+ sender: senderID,
+ };
//SMS_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)
@@ -138,141 +134,224 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs & { evt
if (!!verifiedNumber) return true;
return isVerificationPending;
}
+
const isNumberVerified = await getIsNumberVerified();
+ if (!isNumberVerified) {
+ log.warn(`Phone number not verified`, safeStringify({ reminderPhone, isNumberVerified }));
+ return;
+ }
+
+ if (params.evt) {
+ await scheduleSMSReminderForEvt(
+ params as ScheduleTextReminderArgsWithRequiredFields & { evt: BookingInfo }
+ );
+ } else {
+ await scheduleSMSReminderForForm(
+ params as ScheduleTextReminderArgsWithRequiredFields & {
+ formData: FormSubmissionData;
+ }
+ );
+ }
+};
+
+const scheduleSMSReminderForEvt = async (
+ args: ScheduleTextReminderArgsWithRequiredFields & { evt: BookingInfo }
+) => {
+ const {
+ evt,
+ reminderPhone,
+ triggerEvent,
+ action,
+ timeSpan,
+ message = "",
+ workflowStepId,
+ template,
+ sender,
+ userId,
+ teamId,
+ seatReferenceUid,
+ } = args;
+
+ const { startTime, endTime } = evt;
+ const uid = evt.uid as string;
+ const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase;
+ let scheduledDate = null;
+
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;
}
- if (reminderPhone && isNumberVerified) {
- const useTwilio = shouldUseTwilio(triggerEvent, scheduledDate);
- if (useTwilio) {
- const attendeeToBeUsedInSMS = getAttendeeToBeUsedInSMS(action, evt, reminderPhone);
-
- const name = action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.name : "";
- const attendeeName =
- action === WorkflowActions.SMS_ATTENDEE ? evt.organizer.name : attendeeToBeUsedInSMS.name;
- const timeZone =
- action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.timeZone : evt.organizer.timeZone;
-
- let smsMessage = message;
-
- if (smsMessage) {
- smsMessage = await getSMSMessageWithVariables(smsMessage, evt, attendeeToBeUsedInSMS, action);
- } else if (template === WorkflowTemplates.REMINDER) {
- smsMessage =
- smsReminderTemplate(
- false,
- evt.organizer.language.locale,
- action,
- evt.organizer.timeFormat,
- evt.startTime,
- evt.title,
- timeZone,
- attendeeName,
- name
- ) || message;
- }
+ const useTwilio = shouldUseTwilio(triggerEvent, scheduledDate);
+ if (useTwilio) {
+ const attendeeToBeUsedInSMS = getAttendeeToBeUsedInSMS(action, evt, reminderPhone);
- if (smsMessage.length > 0) {
- const smsMessageWithoutOptOut = smsMessage;
+ const name = action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.name : "";
+ const attendeeName =
+ action === WorkflowActions.SMS_ATTENDEE ? evt.organizer.name : attendeeToBeUsedInSMS.name;
+ const timeZone =
+ action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.timeZone : evt.organizer.timeZone;
- if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
- smsMessage = await WorkflowOptOutService.addOptOutMessage(
- smsMessage,
- evt.organizer.language.locale
- );
- }
- // Allows debugging generated email content without waiting for sendgrid to send emails
- log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage);
-
- if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(triggerEvent)) {
- try {
- await sendSmsOrFallbackEmail({
- twilioData: {
- phoneNumber: reminderPhone,
- body: smsMessage,
- sender: senderID,
- bodyWithoutOptOut: smsMessageWithoutOptOut,
- bookingUid: evt.uid,
- userId,
- teamId,
- },
- fallbackData: isAttendeeAction(action)
- ? {
- email: evt.attendees[0].email,
- t: await getTranslation(evt.attendees[0].language.locale, "common"),
- replyTo: evt.organizer.email,
- }
- : undefined,
- });
- } catch (error) {
- log.error(`Error sending SMS with error ${error}`);
- }
+ let smsMessage = message;
+
+ if (smsMessage) {
+ smsMessage = await getSMSMessageWithVariables(smsMessage, evt, attendeeToBeUsedInSMS, action);
+ } else if (template === WorkflowTemplates.REMINDER) {
+ smsMessage =
+ smsReminderTemplate(
+ false,
+ evt.organizer.language.locale,
+ action,
+ evt.organizer.timeFormat,
+ evt.startTime,
+ evt.title,
+ timeZone,
+ attendeeName,
+ name
+ ) || message;
+ }
+
+ if (smsMessage.trim().length > 0) {
+ const smsMessageWithoutOptOut = await WorkflowOptOutService.addOptOutMessage(
+ smsMessage,
+ evt.organizer.language.locale
+ );
+
+ // Allows debugging generated email content without waiting for sendgrid to send emails
+ log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage);
+
+ if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(triggerEvent)) {
+ try {
+ await sendSmsOrFallbackEmail({
+ twilioData: {
+ phoneNumber: reminderPhone,
+ body: smsMessage,
+ sender,
+ bodyWithoutOptOut: smsMessageWithoutOptOut,
+ bookingUid: evt.uid,
+ userId,
+ teamId,
+ },
+ fallbackData: isAttendeeAction(action)
+ ? {
+ email: evt.attendees[0].email,
+ t: await getTranslation(evt.attendees[0].language.locale, "common"),
+ replyTo: evt.organizer.email,
+ }
+ : undefined,
+ });
+ } catch (error) {
+ log.error(`Error sending SMS with error ${error}`);
}
+ }
+
+ if (
+ (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
+ triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) &&
+ scheduledDate
+ ) {
+ try {
+ // schedule at least 15 minutes in advance and at most 2 hours in advance
+ const scheduledNotification = await scheduleSmsOrFallbackEmail({
+ twilioData: {
+ phoneNumber: reminderPhone,
+ body: smsMessage,
+ scheduledDate: scheduledDate.toDate(),
+ sender,
+ bookingUid: evt.uid,
+ userId,
+ teamId,
+ },
+ fallbackData: isAttendeeAction(action)
+ ? {
+ email: evt.attendees[0].email,
+ t: await getTranslation(evt.attendees[0].language.locale, "common"),
+ replyTo: evt.organizer.email,
+ workflowStepId,
+ }
+ : undefined,
+ });
- if (
- (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
- triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) &&
- scheduledDate
- ) {
- try {
- // schedule at least 15 minutes in advance and at most 2 hours in advance
- const scheduledNotification = await scheduleSmsOrFallbackEmail({
- twilioData: {
- phoneNumber: reminderPhone,
- body: smsMessage,
+ if (scheduledNotification?.sid) {
+ await prisma.workflowReminder.create({
+ data: {
+ bookingUid: uid,
+ workflowStepId: workflowStepId,
+ method: WorkflowMethods.SMS,
scheduledDate: scheduledDate.toDate(),
- sender: senderID,
- bookingUid: evt.uid,
- userId,
- teamId,
+ scheduled: true,
+ referenceId: scheduledNotification.sid,
+ seatReferenceId: seatReferenceUid,
},
- fallbackData: isAttendeeAction(action)
- ? {
- email: evt.attendees[0].email,
- t: await getTranslation(evt.attendees[0].language.locale, "common"),
- replyTo: evt.organizer.email,
- workflowStepId,
- }
- : undefined,
});
-
- if (scheduledNotification?.sid) {
- await prisma.workflowReminder.create({
- data: {
- bookingUid: uid,
- workflowStepId: workflowStepId,
- method: WorkflowMethods.SMS,
- scheduledDate: scheduledDate.toDate(),
- scheduled: true,
- referenceId: scheduledNotification.sid,
- seatReferenceId: seatReferenceUid,
- },
- });
- }
- } catch (error) {
- log.error(`Error scheduling SMS with error ${error}`);
}
+ } catch (error) {
+ log.error(`Error scheduling SMS with error ${error}`);
}
}
- return;
}
+ return;
+ }
+
+ if (!useTwilio && scheduledDate) {
+ // Write to DB and send to CRON if scheduled reminder date is past 2 hours from now
+ await prisma.workflowReminder.create({
+ data: {
+ bookingUid: uid,
+ workflowStepId: workflowStepId,
+ method: WorkflowMethods.SMS,
+ scheduledDate: scheduledDate.toDate(),
+ scheduled: false,
+ seatReferenceId: seatReferenceUid,
+ },
+ });
+ }
+};
+
+// sends all immediately, no scheduling needed
+const scheduleSMSReminderForForm = async (
+ args: ScheduleTextReminderArgsWithRequiredFields & {
+ formData: FormSubmissionData;
+ }
+) => {
+ const { message, triggerEvent, reminderPhone, sender, userId, teamId, action, formData } = args;
+
+ const smsMessage = message;
+
+ if (smsMessage.trim().length > 0) {
+ const smsMessageWithoutOptOut = await WorkflowOptOutService.addOptOutMessage(
+ smsMessage,
+ formData.user.locale
+ );
+
+ // Allows debugging generated email content without waiting for sendgrid to send emails
+ log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage);
+
+ try {
+ const submitterEmail = getSubmitterEmail(formData.responses);
- if (!useTwilio && scheduledDate) {
- // Write to DB and send to CRON if scheduled reminder date is past 2 hours from now
- await prisma.workflowReminder.create({
- data: {
- bookingUid: uid,
- workflowStepId: workflowStepId,
- method: WorkflowMethods.SMS,
- scheduledDate: scheduledDate.toDate(),
- scheduled: false,
- seatReferenceId: seatReferenceUid,
+ await sendSmsOrFallbackEmail({
+ twilioData: {
+ phoneNumber: reminderPhone,
+ body: smsMessage,
+ sender,
+ bodyWithoutOptOut: smsMessageWithoutOptOut,
+ userId,
+ teamId,
},
+ fallbackData:
+ isAttendeeAction(action) && submitterEmail
+ ? {
+ email: submitterEmail,
+ t: await getTranslation(formData.user.locale, "common"),
+ replyTo: formData.user.email,
+ }
+ : undefined,
});
+ } catch (error) {
+ log.error(`Error sending SMS with error ${error}`);
}
}
};
diff --git a/packages/features/ee/workflows/lib/service/workflowOptOutService.ts b/packages/features/ee/workflows/lib/service/workflowOptOutService.ts
index 728588c096f093..59f2bdb6efc107 100644
--- a/packages/features/ee/workflows/lib/service/workflowOptOutService.ts
+++ b/packages/features/ee/workflows/lib/service/workflowOptOutService.ts
@@ -26,7 +26,11 @@ export class WorkflowOptOutService {
}
static async addOptOutMessage(message: string, locale: string) {
- const t = await getTranslation(locale, "common");
- return `${message}\n\n${t("sms_opt_out_message")}`;
+ if (process.env.TWILIO_OPT_OUT_ENABLED === "true") {
+ const t = await getTranslation(locale, "common");
+ return `${message}\n\n${t("sms_opt_out_message")}`;
+ }
+
+ return message;
}
}