diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 22706a4b0f..c7eb6c3740 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1361,6 +1361,8 @@ "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", "choose_common_schedule_team_event": "Choose a common schedule", "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", + "sender_id": "Sender ID", + "sender_id_error_message":"Only letters, numbers and spaces allowed (max. 11 characters)", "test_routing_form": "Test Routing Form", "test_preview": "Test Preview", "route_to": "Route to", diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index c49ae06f20..fbee89cb48 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -105,7 +105,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { break; } if (message?.length && message?.length > 0 && sendTo) { - const scheduledSMS = await twilio.scheduleSMS(sendTo, message, reminder.scheduledDate); + const scheduledSMS = await twilio.scheduleSMS( + sendTo, + message, + reminder.scheduledDate, + reminder.workflowStep.sender || "Cal" + ); await prisma.workflowReminder.update({ where: { diff --git a/packages/features/ee/workflows/components/v2/AddActionDialog.tsx b/packages/features/ee/workflows/components/v2/AddActionDialog.tsx index e4ddd58f1b..c4db677a95 100644 --- a/packages/features/ee/workflows/components/v2/AddActionDialog.tsx +++ b/packages/features/ee/workflows/components/v2/AddActionDialog.tsx @@ -6,17 +6,18 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Button, Checkbox, EmailField, Form, Label } from "@calcom/ui/components"; +import { Button, Checkbox, EmailField, Form, Label, TextField } from "@calcom/ui/components"; import PhoneInput from "@calcom/ui/form/PhoneInputLazy"; import { Dialog, DialogClose, DialogContent, DialogFooter, Select } from "@calcom/ui/v2"; import { WORKFLOW_ACTIONS } from "../../lib/constants"; import { getWorkflowActionOptions } from "../../lib/getOptions"; +import { onlyLettersNumbersSpaces } from "../../pages/v2/workflow"; interface IAddActionDialog { isOpenDialog: boolean; setIsOpenDialog: Dispatch>; - addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean) => void; + addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => void; isFreeUser: boolean; } @@ -29,6 +30,7 @@ type AddActionFormValues = { action: WorkflowActions; sendTo?: string; numberRequired?: boolean; + sender?: string; }; const cleanUpActionsForFreeUser = (actions: ISelectActionOption[]) => { @@ -41,6 +43,7 @@ export const AddActionDialog = (props: IAddActionDialog) => { const { t } = useLocale(); const { isOpenDialog, setIsOpenDialog, addAction, isFreeUser } = props; const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false); + const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(false); const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false); const workflowActions = getWorkflowActionOptions(t); const actionOptions = isFreeUser ? cleanUpActionsForFreeUser(workflowActions) : workflowActions; @@ -52,12 +55,17 @@ export const AddActionDialog = (props: IAddActionDialog) => { .refine((val) => isValidPhoneNumber(val) || val.includes("@")) .optional(), numberRequired: z.boolean().optional(), + sender: z + .string() + .refine((val) => onlyLettersNumbersSpaces(val)) + .nullable(), }); const form = useForm({ mode: "onSubmit", defaultValues: { action: WorkflowActions.EMAIL_HOST, + sender: "Cal", }, resolver: zodResolver(formSchema), }); @@ -67,11 +75,18 @@ export const AddActionDialog = (props: IAddActionDialog) => { form.setValue("action", newValue.value); if (newValue.value === WorkflowActions.SMS_NUMBER) { setIsPhoneNumberNeeded(true); + setIsSenderIdNeeded(true); setIsEmailAddressNeeded(false); } else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) { setIsEmailAddressNeeded(true); + setIsSenderIdNeeded(false); + setIsPhoneNumberNeeded(false); + } else if (newValue.value === WorkflowActions.SMS_ATTENDEE) { + setIsSenderIdNeeded(true); + setIsEmailAddressNeeded(false); setIsPhoneNumberNeeded(false); } else { + setIsSenderIdNeeded(false); setIsEmailAddressNeeded(false); setIsPhoneNumberNeeded(false); } @@ -90,13 +105,14 @@ export const AddActionDialog = (props: IAddActionDialog) => {
{ - addAction(values.action, values.sendTo, values.numberRequired); + addAction(values.action, values.sendTo, values.numberRequired, values.sender); form.unregister("sendTo"); form.unregister("action"); form.unregister("numberRequired"); setIsOpenDialog(false); setIsPhoneNumberNeeded(false); setIsEmailAddressNeeded(false); + setIsSenderIdNeeded(false); }}>
@@ -119,25 +135,10 @@ export const AddActionDialog = (props: IAddActionDialog) => {

{form.formState.errors.action.message}

)}
- {form.getValues("action") === WorkflowActions.SMS_ATTENDEE && ( -
- ( - form.setValue("numberRequired", e.target.checked)} - /> - )} - /> -
- )} {isPhoneNumberNeeded && (
-
+
control={form.control} name="sendTo" @@ -157,6 +158,32 @@ export const AddActionDialog = (props: IAddActionDialog) => {
)} + {isSenderIdNeeded && ( +
+ +
+ )} + {form.getValues("action") === WorkflowActions.SMS_ATTENDEE && ( +
+ ( + form.setValue("numberRequired", e.target.checked)} + /> + )} + /> +
+ )} diff --git a/packages/features/ee/workflows/components/v2/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/v2/WorkflowDetailsPage.tsx index be500567db..8d79b9a54a 100644 --- a/packages/features/ee/workflows/components/v2/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/v2/WorkflowDetailsPage.tsx @@ -51,7 +51,7 @@ export default function WorkflowDetailsPage(props: Props) { [data] ); - const addAction = (action: WorkflowActions, sendTo?: string, numberRequired?: boolean) => { + const addAction = (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => { const steps = form.getValues("steps"); const id = steps?.length > 0 @@ -75,6 +75,7 @@ export default function WorkflowDetailsPage(props: Props) { emailSubject: null, template: WorkflowTemplates.CUSTOM, numberRequired: numberRequired || false, + sender: sender || "Cal", }; steps?.push(step); form.setValue("steps", steps); diff --git a/packages/features/ee/workflows/components/v2/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/v2/WorkflowStepContainer.tsx index 3a31d394ba..2a11a2810e 100644 --- a/packages/features/ee/workflows/components/v2/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/v2/WorkflowStepContainer.tsx @@ -17,7 +17,7 @@ import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } import { Icon } from "@calcom/ui/Icon"; import { Button } from "@calcom/ui/components"; import { Checkbox } from "@calcom/ui/components"; -import { EmailField, Label, TextArea } from "@calcom/ui/components/form"; +import { EmailField, Label, TextArea, TextField } from "@calcom/ui/components/form"; import PhoneInput from "@calcom/ui/form/PhoneInputLazy"; import { DialogClose, DialogContent } from "@calcom/ui/v2"; import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent"; @@ -52,6 +52,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { step?.action === WorkflowActions.SMS_NUMBER ? true : false ); + const [isSenderIdNeeded, setIsSenderIdNeeded] = useState( + step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE + ? true + : false + ); + const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState( step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false ); @@ -279,13 +285,20 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (val) { if (val.value === WorkflowActions.SMS_NUMBER) { setIsPhoneNumberNeeded(true); + setIsSenderIdNeeded(true); setIsEmailAddressNeeded(false); } else if (val.value === WorkflowActions.EMAIL_ADDRESS) { setIsEmailAddressNeeded(true); setIsPhoneNumberNeeded(false); + setIsSenderIdNeeded(false); + } else if (val.value === WorkflowActions.SMS_ATTENDEE) { + setIsSenderIdNeeded(true); + setIsEmailAddressNeeded(false); + setIsPhoneNumberNeeded(false); } else { setIsEmailAddressNeeded(false); setIsPhoneNumberNeeded(false); + setIsSenderIdNeeded(false); } form.unregister(`steps.${step.stepNumber - 1}.sendTo`); form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`); @@ -315,43 +328,64 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { ); }} /> - {form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && ( -
- ( - - form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked) - } - /> - )} - /> -
- )}
- {isPhoneNumberNeeded && ( -
- - + {(isPhoneNumberNeeded || isSenderIdNeeded) && ( +
+ {isPhoneNumberNeeded && ( + <> + + + control={form.control} + name={`steps.${step.stepNumber - 1}.sendTo`} + placeholder={t("phone_number")} + id={`steps.${step.stepNumber - 1}.sendTo`} + className="w-full rounded-md" + required + /> + {form.formState.errors.steps && + form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( +

+ {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""} +

+ )} + + )} + {isSenderIdNeeded && ( + <> +
+ +
+ {form.formState.errors.steps && + form.formState?.errors?.steps[step.stepNumber - 1]?.sender && ( +

{t("sender_id_error_message")}

+ )} + + )} +
+ )} + {form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && ( +
+ - {form.formState.errors.steps && - form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( -

- {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""} -

+ render={() => ( + + form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked) + } + /> )} + />
)} {isEmailAddressNeeded && ( @@ -408,7 +442,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { /> {form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && ( -

+

{form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""}

)} @@ -433,7 +467,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { /> {form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody && ( -

+

{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}

)} @@ -497,6 +531,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { emailSubject, reminderBody, template: step.template, + sender: step.sender || "Cal", }); } else { const isNumberValid = diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 48b2736574..49d26f6ff2 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -47,7 +47,8 @@ export const scheduleWorkflowReminders = async ( }, step.reminderBody || "", step.id, - step.template + step.template, + step.sender || "Cal" ); } else if ( step.action === WorkflowActions.EMAIL_ATTENDEE || @@ -115,7 +116,8 @@ export const sendCancelledReminders = async ( }, step.reminderBody || "", step.id, - step.template + step.template, + step.sender || "Cal" ); } else if ( step.action === WorkflowActions.EMAIL_ATTENDEE || diff --git a/packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts b/packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts index 4eab4cde6e..420c6159d5 100644 --- a/packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts +++ b/packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts @@ -19,18 +19,19 @@ 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) => { +export const sendSMS = async (phoneNumber: string, body: string, sender: string) => { assertTwilio(twilio); const response = await twilio.messages.create({ body: body, messagingServiceSid: process.env.TWILIO_MESSAGING_SID, to: phoneNumber, + from: sender, }); return response; }; -export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date) => { +export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date, sender: string) => { assertTwilio(twilio); const response = await twilio.messages.create({ body: body, @@ -38,6 +39,7 @@ export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDa to: phoneNumber, scheduleType: "fixed", sendAt: scheduledDate, + from: sender, }); return response; diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 4ef0f3e4ee..ecc60d0bad 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -48,7 +48,8 @@ export const scheduleSMSReminder = async ( }, message: string, workflowStepId: number, - template: WorkflowTemplates + template: WorkflowTemplates, + sender: string ) => { const { startTime, endTime } = evt; const uid = evt.uid as string; @@ -97,7 +98,7 @@ export const scheduleSMSReminder = async ( triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT ) { try { - await twilio.sendSMS(reminderPhone, message); + await twilio.sendSMS(reminderPhone, message, sender); } catch (error) { console.log(`Error sending SMS with error ${error}`); } @@ -112,7 +113,12 @@ export const scheduleSMSReminder = async ( !scheduledDate.isAfter(currentDate.add(7, "day")) ) { try { - const scheduledSMS = await twilio.scheduleSMS(reminderPhone, message, scheduledDate.toDate()); + const scheduledSMS = await twilio.scheduleSMS( + reminderPhone, + message, + scheduledDate.toDate(), + sender + ); await prisma.workflowReminder.create({ data: { diff --git a/packages/features/ee/workflows/pages/v2/workflow.tsx b/packages/features/ee/workflows/pages/v2/workflow.tsx index 8cd4f089ce..c8aac94470 100644 --- a/packages/features/ee/workflows/pages/v2/workflow.tsx +++ b/packages/features/ee/workflows/pages/v2/workflow.tsx @@ -38,6 +38,13 @@ export type FormValues = { timeUnit?: TimeUnit; }; +export function onlyLettersNumbersSpaces(str: string) { + if (str.length <= 11 && /^[A-Za-z0-9\s]*$/.test(str)) { + return true; + } + return false; +} + const formSchema = z.object({ name: z.string(), activeOn: z.object({ value: z.string(), label: z.string() }).array(), @@ -58,6 +65,11 @@ const formSchema = z.object({ .string() .refine((val) => isValidPhoneNumber(val) || val.includes("@")) .nullable(), + sender: z + .string() + .refine((val) => onlyLettersNumbersSpaces(val)) + .optional() + .nullable(), }) .array(), }); diff --git a/packages/prisma/migrations/20221110164757_add_sender_to_workflow_step/migration.sql b/packages/prisma/migrations/20221110164757_add_sender_to_workflow_step/migration.sql new file mode 100644 index 0000000000..e055d39866 --- /dev/null +++ b/packages/prisma/migrations/20221110164757_add_sender_to_workflow_step/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "WorkflowStep" ADD COLUMN "sender" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 9dda13a96b..44b6cedbe4 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -573,6 +573,7 @@ model WorkflowStep { template WorkflowTemplates @default(REMINDER) workflowReminders WorkflowReminder[] numberRequired Boolean? + sender String? } model Workflow { diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 65e808e0d3..ac6a0d5726 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -160,6 +160,7 @@ export const workflowsRouter = router({ action: WorkflowActions.EMAIL_HOST, template: WorkflowTemplates.REMINDER, workflowId: workflow.id, + sender: "Cal", }, }); return { workflow }; @@ -235,6 +236,7 @@ export const workflowsRouter = router({ emailSubject: z.string().optional().nullable(), template: z.enum(WORKFLOW_TEMPLATES), numberRequired: z.boolean().nullable(), + sender: z.string().optional().nullable(), }) .array(), trigger: z.enum(WORKFLOW_TRIGGER_EVENTS), @@ -472,7 +474,8 @@ export const workflowsRouter = router({ }, step.reminderBody || "", step.id, - step.template + step.template, + step.sender || "Cal" ); } }); @@ -541,6 +544,7 @@ export const workflowsRouter = router({ emailSubject: newStep.template === WorkflowTemplates.CUSTOM ? newStep.emailSubject : null, template: newStep.template, numberRequired: newStep.numberRequired, + sender: newStep.sender || "Cal", }, }); //cancel all reminders of step and create new ones (not for newEventTypes) @@ -651,7 +655,8 @@ export const workflowsRouter = router({ }, newStep.reminderBody || "", newStep.id || 0, - newStep.template + newStep.template, + newStep.sender || "Cal" ); } }); @@ -677,6 +682,8 @@ export const workflowsRouter = router({ }); addedSteps.forEach(async (step) => { if (step) { + const newStep = step; + newStep.sender = step.sender || "Cal"; const createdStep = await ctx.prisma.workflowStep.create({ data: step, }); @@ -764,7 +771,8 @@ export const workflowsRouter = router({ }, step.reminderBody || "", createdStep.id, - step.template + step.template, + step.sender || "Cal" ); } }); @@ -812,10 +820,11 @@ export const workflowsRouter = router({ reminderBody: z.string(), template: z.enum(WORKFLOW_TEMPLATES), sendTo: z.string().optional(), + sender: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { - const { action, emailSubject, reminderBody, template, sendTo } = input; + const { action, emailSubject, reminderBody, template, sendTo, sender } = input; try { const booking = await ctx.prisma.booking.findFirst({ orderBy: { @@ -899,7 +908,8 @@ export const workflowsRouter = router({ { time: null, timeUnit: null }, reminderBody, 0, - template + template, + sender || "Cal" ); return { message: "Notification sent" }; } diff --git a/turbo.json b/turbo.json index e1dbd2ae1d..eb7071d799 100644 --- a/turbo.json +++ b/turbo.json @@ -183,6 +183,9 @@ "$CLOSECOM_API_KEY", "$SENDGRID_API_KEY", "$SENDGRID_EMAIL", + "$TWILIO_TOKEN", + "$TWILIO_SID", + "$TWILIO_MESSAGING_SID", "$CRON_API_KEY", "$DAILY_API_KEY", "$DAILY_SCALE_PLAN",