Add alphanumeric sender ID to SMS workflow actions (#5471)
* add sender id * add sender to twilio from * added missing sender * add migration * fix design of add action dialog * add cal as sender when creating new workflow * fix type errors Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>pr/5217
parent
ef3e7fae20
commit
54f4e665a3
|
@ -1361,6 +1361,8 @@
|
||||||
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
|
"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": "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.",
|
"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_routing_form": "Test Routing Form",
|
||||||
"test_preview": "Test Preview",
|
"test_preview": "Test Preview",
|
||||||
"route_to": "Route to",
|
"route_to": "Route to",
|
||||||
|
|
|
@ -105,7 +105,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (message?.length && message?.length > 0 && sendTo) {
|
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({
|
await prisma.workflowReminder.update({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -6,17 +6,18 @@ import { Controller, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
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 PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, Select } from "@calcom/ui/v2";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, Select } from "@calcom/ui/v2";
|
||||||
|
|
||||||
import { WORKFLOW_ACTIONS } from "../../lib/constants";
|
import { WORKFLOW_ACTIONS } from "../../lib/constants";
|
||||||
import { getWorkflowActionOptions } from "../../lib/getOptions";
|
import { getWorkflowActionOptions } from "../../lib/getOptions";
|
||||||
|
import { onlyLettersNumbersSpaces } from "../../pages/v2/workflow";
|
||||||
|
|
||||||
interface IAddActionDialog {
|
interface IAddActionDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||||
addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean) => void;
|
addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => void;
|
||||||
isFreeUser: boolean;
|
isFreeUser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ type AddActionFormValues = {
|
||||||
action: WorkflowActions;
|
action: WorkflowActions;
|
||||||
sendTo?: string;
|
sendTo?: string;
|
||||||
numberRequired?: boolean;
|
numberRequired?: boolean;
|
||||||
|
sender?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanUpActionsForFreeUser = (actions: ISelectActionOption[]) => {
|
const cleanUpActionsForFreeUser = (actions: ISelectActionOption[]) => {
|
||||||
|
@ -41,6 +43,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { isOpenDialog, setIsOpenDialog, addAction, isFreeUser } = props;
|
const { isOpenDialog, setIsOpenDialog, addAction, isFreeUser } = props;
|
||||||
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
|
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
|
||||||
|
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(false);
|
||||||
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);
|
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);
|
||||||
const workflowActions = getWorkflowActionOptions(t);
|
const workflowActions = getWorkflowActionOptions(t);
|
||||||
const actionOptions = isFreeUser ? cleanUpActionsForFreeUser(workflowActions) : workflowActions;
|
const actionOptions = isFreeUser ? cleanUpActionsForFreeUser(workflowActions) : workflowActions;
|
||||||
|
@ -52,12 +55,17 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
|
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
|
||||||
.optional(),
|
.optional(),
|
||||||
numberRequired: z.boolean().optional(),
|
numberRequired: z.boolean().optional(),
|
||||||
|
sender: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => onlyLettersNumbersSpaces(val))
|
||||||
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<AddActionFormValues>({
|
const form = useForm<AddActionFormValues>({
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
action: WorkflowActions.EMAIL_HOST,
|
action: WorkflowActions.EMAIL_HOST,
|
||||||
|
sender: "Cal",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
});
|
});
|
||||||
|
@ -67,11 +75,18 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
form.setValue("action", newValue.value);
|
form.setValue("action", newValue.value);
|
||||||
if (newValue.value === WorkflowActions.SMS_NUMBER) {
|
if (newValue.value === WorkflowActions.SMS_NUMBER) {
|
||||||
setIsPhoneNumberNeeded(true);
|
setIsPhoneNumberNeeded(true);
|
||||||
|
setIsSenderIdNeeded(true);
|
||||||
setIsEmailAddressNeeded(false);
|
setIsEmailAddressNeeded(false);
|
||||||
} else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) {
|
} else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) {
|
||||||
setIsEmailAddressNeeded(true);
|
setIsEmailAddressNeeded(true);
|
||||||
|
setIsSenderIdNeeded(false);
|
||||||
|
setIsPhoneNumberNeeded(false);
|
||||||
|
} else if (newValue.value === WorkflowActions.SMS_ATTENDEE) {
|
||||||
|
setIsSenderIdNeeded(true);
|
||||||
|
setIsEmailAddressNeeded(false);
|
||||||
setIsPhoneNumberNeeded(false);
|
setIsPhoneNumberNeeded(false);
|
||||||
} else {
|
} else {
|
||||||
|
setIsSenderIdNeeded(false);
|
||||||
setIsEmailAddressNeeded(false);
|
setIsEmailAddressNeeded(false);
|
||||||
setIsPhoneNumberNeeded(false);
|
setIsPhoneNumberNeeded(false);
|
||||||
}
|
}
|
||||||
|
@ -90,13 +105,14 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
handleSubmit={(values) => {
|
handleSubmit={(values) => {
|
||||||
addAction(values.action, values.sendTo, values.numberRequired);
|
addAction(values.action, values.sendTo, values.numberRequired, values.sender);
|
||||||
form.unregister("sendTo");
|
form.unregister("sendTo");
|
||||||
form.unregister("action");
|
form.unregister("action");
|
||||||
form.unregister("numberRequired");
|
form.unregister("numberRequired");
|
||||||
setIsOpenDialog(false);
|
setIsOpenDialog(false);
|
||||||
setIsPhoneNumberNeeded(false);
|
setIsPhoneNumberNeeded(false);
|
||||||
setIsEmailAddressNeeded(false);
|
setIsEmailAddressNeeded(false);
|
||||||
|
setIsSenderIdNeeded(false);
|
||||||
}}>
|
}}>
|
||||||
<div className="mt-5 space-y-1">
|
<div className="mt-5 space-y-1">
|
||||||
<Label htmlFor="label">{t("action")}:</Label>
|
<Label htmlFor="label">{t("action")}:</Label>
|
||||||
|
@ -119,25 +135,10 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
<p className="mt-1 text-sm text-red-500">{form.formState.errors.action.message}</p>
|
<p className="mt-1 text-sm text-red-500">{form.formState.errors.action.message}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (
|
|
||||||
<div className="mt-5">
|
|
||||||
<Controller
|
|
||||||
name="numberRequired"
|
|
||||||
control={form.control}
|
|
||||||
render={() => (
|
|
||||||
<Checkbox
|
|
||||||
defaultChecked={form.getValues("numberRequired") || false}
|
|
||||||
description={t("make_phone_number_required")}
|
|
||||||
onChange={(e) => form.setValue("numberRequired", e.target.checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isPhoneNumberNeeded && (
|
{isPhoneNumberNeeded && (
|
||||||
<div className="mt-5 space-y-1">
|
<div className="mt-5 space-y-1">
|
||||||
<Label htmlFor="sendTo">{t("phone_number")}</Label>
|
<Label htmlFor="sendTo">{t("phone_number")}</Label>
|
||||||
<div className="mt-1">
|
<div className="mt-1 mb-5">
|
||||||
<PhoneInput<AddActionFormValues>
|
<PhoneInput<AddActionFormValues>
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="sendTo"
|
name="sendTo"
|
||||||
|
@ -157,6 +158,32 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
<EmailField required label={t("email_address")} {...form.register("sendTo")} />
|
<EmailField required label={t("email_address")} {...form.register("sendTo")} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isSenderIdNeeded && (
|
||||||
|
<div className="mt-5">
|
||||||
|
<TextField
|
||||||
|
label={t("sender_id")}
|
||||||
|
type="text"
|
||||||
|
placeholder="Cal"
|
||||||
|
maxLength={11}
|
||||||
|
{...form.register(`sender`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (
|
||||||
|
<div className="mt-5">
|
||||||
|
<Controller
|
||||||
|
name="numberRequired"
|
||||||
|
control={form.control}
|
||||||
|
render={() => (
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked={form.getValues("numberRequired") || false}
|
||||||
|
description={t("make_phone_number_required")}
|
||||||
|
onChange={(e) => form.setValue("numberRequired", e.target.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button
|
||||||
|
@ -168,6 +195,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
form.unregister("numberRequired");
|
form.unregister("numberRequired");
|
||||||
setIsPhoneNumberNeeded(false);
|
setIsPhoneNumberNeeded(false);
|
||||||
setIsEmailAddressNeeded(false);
|
setIsEmailAddressNeeded(false);
|
||||||
|
setIsSenderIdNeeded(false);
|
||||||
}}>
|
}}>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||||
[data]
|
[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 steps = form.getValues("steps");
|
||||||
const id =
|
const id =
|
||||||
steps?.length > 0
|
steps?.length > 0
|
||||||
|
@ -75,6 +75,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||||
emailSubject: null,
|
emailSubject: null,
|
||||||
template: WorkflowTemplates.CUSTOM,
|
template: WorkflowTemplates.CUSTOM,
|
||||||
numberRequired: numberRequired || false,
|
numberRequired: numberRequired || false,
|
||||||
|
sender: sender || "Cal",
|
||||||
};
|
};
|
||||||
steps?.push(step);
|
steps?.push(step);
|
||||||
form.setValue("steps", steps);
|
form.setValue("steps", steps);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger }
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Button } from "@calcom/ui/components";
|
import { Button } from "@calcom/ui/components";
|
||||||
import { Checkbox } 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 PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||||
import { DialogClose, DialogContent } from "@calcom/ui/v2";
|
import { DialogClose, DialogContent } from "@calcom/ui/v2";
|
||||||
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
|
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
|
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(
|
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(
|
||||||
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
|
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
|
||||||
);
|
);
|
||||||
|
@ -279,13 +285,20 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (val.value === WorkflowActions.SMS_NUMBER) {
|
if (val.value === WorkflowActions.SMS_NUMBER) {
|
||||||
setIsPhoneNumberNeeded(true);
|
setIsPhoneNumberNeeded(true);
|
||||||
|
setIsSenderIdNeeded(true);
|
||||||
setIsEmailAddressNeeded(false);
|
setIsEmailAddressNeeded(false);
|
||||||
} else if (val.value === WorkflowActions.EMAIL_ADDRESS) {
|
} else if (val.value === WorkflowActions.EMAIL_ADDRESS) {
|
||||||
setIsEmailAddressNeeded(true);
|
setIsEmailAddressNeeded(true);
|
||||||
setIsPhoneNumberNeeded(false);
|
setIsPhoneNumberNeeded(false);
|
||||||
|
setIsSenderIdNeeded(false);
|
||||||
|
} else if (val.value === WorkflowActions.SMS_ATTENDEE) {
|
||||||
|
setIsSenderIdNeeded(true);
|
||||||
|
setIsEmailAddressNeeded(false);
|
||||||
|
setIsPhoneNumberNeeded(false);
|
||||||
} else {
|
} else {
|
||||||
setIsEmailAddressNeeded(false);
|
setIsEmailAddressNeeded(false);
|
||||||
setIsPhoneNumberNeeded(false);
|
setIsPhoneNumberNeeded(false);
|
||||||
|
setIsSenderIdNeeded(false);
|
||||||
}
|
}
|
||||||
form.unregister(`steps.${step.stepNumber - 1}.sendTo`);
|
form.unregister(`steps.${step.stepNumber - 1}.sendTo`);
|
||||||
form.clearErrors(`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 && (
|
|
||||||
<div className="mt-5">
|
|
||||||
<Controller
|
|
||||||
name={`steps.${step.stepNumber - 1}.numberRequired`}
|
|
||||||
control={form.control}
|
|
||||||
render={() => (
|
|
||||||
<Checkbox
|
|
||||||
defaultChecked={
|
|
||||||
form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false
|
|
||||||
}
|
|
||||||
description={t("make_phone_number_required")}
|
|
||||||
onChange={(e) =>
|
|
||||||
form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{isPhoneNumberNeeded && (
|
{(isPhoneNumberNeeded || isSenderIdNeeded) && (
|
||||||
<div className="mt-5 rounded-md bg-gray-50 p-4">
|
<div className="mt-2 rounded-md bg-gray-50 p-4 pt-0">
|
||||||
<Label>{t("custom_phone_number")}</Label>
|
{isPhoneNumberNeeded && (
|
||||||
<PhoneInput<FormValues>
|
<>
|
||||||
|
<Label className="pt-4">{t("custom_phone_number")}</Label>
|
||||||
|
<PhoneInput<FormValues>
|
||||||
|
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 && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">
|
||||||
|
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isSenderIdNeeded && (
|
||||||
|
<>
|
||||||
|
<div className="pt-4">
|
||||||
|
<TextField
|
||||||
|
label={t("sender_id")}
|
||||||
|
type="text"
|
||||||
|
placeholder="Cal"
|
||||||
|
maxLength={11}
|
||||||
|
{...form.register(`steps.${step.stepNumber - 1}.sender`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{form.formState.errors.steps &&
|
||||||
|
form.formState?.errors?.steps[step.stepNumber - 1]?.sender && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Controller
|
||||||
|
name={`steps.${step.stepNumber - 1}.numberRequired`}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`steps.${step.stepNumber - 1}.sendTo`}
|
render={() => (
|
||||||
placeholder={t("phone_number")}
|
<Checkbox
|
||||||
id={`steps.${step.stepNumber - 1}.sendTo`}
|
defaultChecked={
|
||||||
className="w-full rounded-md"
|
form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false
|
||||||
required
|
}
|
||||||
/>
|
description={t("make_phone_number_required")}
|
||||||
{form.formState.errors.steps &&
|
onChange={(e) =>
|
||||||
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
|
form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked)
|
||||||
<p className="mt-1 text-sm text-red-500">
|
}
|
||||||
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
|
/>
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isEmailAddressNeeded && (
|
{isEmailAddressNeeded && (
|
||||||
|
@ -408,7 +442,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
/>
|
/>
|
||||||
{form.formState.errors.steps &&
|
{form.formState.errors.steps &&
|
||||||
form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && (
|
form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && (
|
||||||
<p className="mt-1 text-sm text-red-500">
|
<p className="mt-1 text-xs text-red-500">
|
||||||
{form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""}
|
{form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -433,7 +467,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
/>
|
/>
|
||||||
{form.formState.errors.steps &&
|
{form.formState.errors.steps &&
|
||||||
form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody && (
|
form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody && (
|
||||||
<p className="mt-1 text-sm text-red-500">
|
<p className="mt-1 text-xs text-red-500">
|
||||||
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
|
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -497,6 +531,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
emailSubject,
|
emailSubject,
|
||||||
reminderBody,
|
reminderBody,
|
||||||
template: step.template,
|
template: step.template,
|
||||||
|
sender: step.sender || "Cal",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const isNumberValid =
|
const isNumberValid =
|
||||||
|
|
|
@ -47,7 +47,8 @@ export const scheduleWorkflowReminders = async (
|
||||||
},
|
},
|
||||||
step.reminderBody || "",
|
step.reminderBody || "",
|
||||||
step.id,
|
step.id,
|
||||||
step.template
|
step.template,
|
||||||
|
step.sender || "Cal"
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||||
|
@ -115,7 +116,8 @@ export const sendCancelledReminders = async (
|
||||||
},
|
},
|
||||||
step.reminderBody || "",
|
step.reminderBody || "",
|
||||||
step.id,
|
step.id,
|
||||||
step.template
|
step.template,
|
||||||
|
step.sender || "Cal"
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||||
|
|
|
@ -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");
|
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);
|
assertTwilio(twilio);
|
||||||
const response = await twilio.messages.create({
|
const response = await twilio.messages.create({
|
||||||
body: body,
|
body: body,
|
||||||
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
|
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
|
||||||
to: phoneNumber,
|
to: phoneNumber,
|
||||||
|
from: sender,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
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);
|
assertTwilio(twilio);
|
||||||
const response = await twilio.messages.create({
|
const response = await twilio.messages.create({
|
||||||
body: body,
|
body: body,
|
||||||
|
@ -38,6 +39,7 @@ export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDa
|
||||||
to: phoneNumber,
|
to: phoneNumber,
|
||||||
scheduleType: "fixed",
|
scheduleType: "fixed",
|
||||||
sendAt: scheduledDate,
|
sendAt: scheduledDate,
|
||||||
|
from: sender,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -48,7 +48,8 @@ export const scheduleSMSReminder = async (
|
||||||
},
|
},
|
||||||
message: string,
|
message: string,
|
||||||
workflowStepId: number,
|
workflowStepId: number,
|
||||||
template: WorkflowTemplates
|
template: WorkflowTemplates,
|
||||||
|
sender: string
|
||||||
) => {
|
) => {
|
||||||
const { startTime, endTime } = evt;
|
const { startTime, endTime } = evt;
|
||||||
const uid = evt.uid as string;
|
const uid = evt.uid as string;
|
||||||
|
@ -97,7 +98,7 @@ export const scheduleSMSReminder = async (
|
||||||
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
|
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await twilio.sendSMS(reminderPhone, message);
|
await twilio.sendSMS(reminderPhone, message, sender);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error sending SMS with error ${error}`);
|
console.log(`Error sending SMS with error ${error}`);
|
||||||
}
|
}
|
||||||
|
@ -112,7 +113,12 @@ export const scheduleSMSReminder = async (
|
||||||
!scheduledDate.isAfter(currentDate.add(7, "day"))
|
!scheduledDate.isAfter(currentDate.add(7, "day"))
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const scheduledSMS = await twilio.scheduleSMS(reminderPhone, message, scheduledDate.toDate());
|
const scheduledSMS = await twilio.scheduleSMS(
|
||||||
|
reminderPhone,
|
||||||
|
message,
|
||||||
|
scheduledDate.toDate(),
|
||||||
|
sender
|
||||||
|
);
|
||||||
|
|
||||||
await prisma.workflowReminder.create({
|
await prisma.workflowReminder.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -38,6 +38,13 @@ export type FormValues = {
|
||||||
timeUnit?: TimeUnit;
|
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({
|
const formSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
activeOn: z.object({ value: z.string(), label: z.string() }).array(),
|
activeOn: z.object({ value: z.string(), label: z.string() }).array(),
|
||||||
|
@ -58,6 +65,11 @@ const formSchema = z.object({
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
|
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
|
||||||
.nullable(),
|
.nullable(),
|
||||||
|
sender: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => onlyLettersNumbersSpaces(val))
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "WorkflowStep" ADD COLUMN "sender" TEXT;
|
|
@ -573,6 +573,7 @@ model WorkflowStep {
|
||||||
template WorkflowTemplates @default(REMINDER)
|
template WorkflowTemplates @default(REMINDER)
|
||||||
workflowReminders WorkflowReminder[]
|
workflowReminders WorkflowReminder[]
|
||||||
numberRequired Boolean?
|
numberRequired Boolean?
|
||||||
|
sender String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Workflow {
|
model Workflow {
|
||||||
|
|
|
@ -160,6 +160,7 @@ export const workflowsRouter = router({
|
||||||
action: WorkflowActions.EMAIL_HOST,
|
action: WorkflowActions.EMAIL_HOST,
|
||||||
template: WorkflowTemplates.REMINDER,
|
template: WorkflowTemplates.REMINDER,
|
||||||
workflowId: workflow.id,
|
workflowId: workflow.id,
|
||||||
|
sender: "Cal",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { workflow };
|
return { workflow };
|
||||||
|
@ -235,6 +236,7 @@ export const workflowsRouter = router({
|
||||||
emailSubject: z.string().optional().nullable(),
|
emailSubject: z.string().optional().nullable(),
|
||||||
template: z.enum(WORKFLOW_TEMPLATES),
|
template: z.enum(WORKFLOW_TEMPLATES),
|
||||||
numberRequired: z.boolean().nullable(),
|
numberRequired: z.boolean().nullable(),
|
||||||
|
sender: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
|
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
|
||||||
|
@ -472,7 +474,8 @@ export const workflowsRouter = router({
|
||||||
},
|
},
|
||||||
step.reminderBody || "",
|
step.reminderBody || "",
|
||||||
step.id,
|
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,
|
emailSubject: newStep.template === WorkflowTemplates.CUSTOM ? newStep.emailSubject : null,
|
||||||
template: newStep.template,
|
template: newStep.template,
|
||||||
numberRequired: newStep.numberRequired,
|
numberRequired: newStep.numberRequired,
|
||||||
|
sender: newStep.sender || "Cal",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//cancel all reminders of step and create new ones (not for newEventTypes)
|
//cancel all reminders of step and create new ones (not for newEventTypes)
|
||||||
|
@ -651,7 +655,8 @@ export const workflowsRouter = router({
|
||||||
},
|
},
|
||||||
newStep.reminderBody || "",
|
newStep.reminderBody || "",
|
||||||
newStep.id || 0,
|
newStep.id || 0,
|
||||||
newStep.template
|
newStep.template,
|
||||||
|
newStep.sender || "Cal"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -677,6 +682,8 @@ export const workflowsRouter = router({
|
||||||
});
|
});
|
||||||
addedSteps.forEach(async (step) => {
|
addedSteps.forEach(async (step) => {
|
||||||
if (step) {
|
if (step) {
|
||||||
|
const newStep = step;
|
||||||
|
newStep.sender = step.sender || "Cal";
|
||||||
const createdStep = await ctx.prisma.workflowStep.create({
|
const createdStep = await ctx.prisma.workflowStep.create({
|
||||||
data: step,
|
data: step,
|
||||||
});
|
});
|
||||||
|
@ -764,7 +771,8 @@ export const workflowsRouter = router({
|
||||||
},
|
},
|
||||||
step.reminderBody || "",
|
step.reminderBody || "",
|
||||||
createdStep.id,
|
createdStep.id,
|
||||||
step.template
|
step.template,
|
||||||
|
step.sender || "Cal"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -812,10 +820,11 @@ export const workflowsRouter = router({
|
||||||
reminderBody: z.string(),
|
reminderBody: z.string(),
|
||||||
template: z.enum(WORKFLOW_TEMPLATES),
|
template: z.enum(WORKFLOW_TEMPLATES),
|
||||||
sendTo: z.string().optional(),
|
sendTo: z.string().optional(),
|
||||||
|
sender: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { action, emailSubject, reminderBody, template, sendTo } = input;
|
const { action, emailSubject, reminderBody, template, sendTo, sender } = input;
|
||||||
try {
|
try {
|
||||||
const booking = await ctx.prisma.booking.findFirst({
|
const booking = await ctx.prisma.booking.findFirst({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
|
@ -899,7 +908,8 @@ export const workflowsRouter = router({
|
||||||
{ time: null, timeUnit: null },
|
{ time: null, timeUnit: null },
|
||||||
reminderBody,
|
reminderBody,
|
||||||
0,
|
0,
|
||||||
template
|
template,
|
||||||
|
sender || "Cal"
|
||||||
);
|
);
|
||||||
return { message: "Notification sent" };
|
return { message: "Notification sent" };
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,6 +183,9 @@
|
||||||
"$CLOSECOM_API_KEY",
|
"$CLOSECOM_API_KEY",
|
||||||
"$SENDGRID_API_KEY",
|
"$SENDGRID_API_KEY",
|
||||||
"$SENDGRID_EMAIL",
|
"$SENDGRID_EMAIL",
|
||||||
|
"$TWILIO_TOKEN",
|
||||||
|
"$TWILIO_SID",
|
||||||
|
"$TWILIO_MESSAGING_SID",
|
||||||
"$CRON_API_KEY",
|
"$CRON_API_KEY",
|
||||||
"$DAILY_API_KEY",
|
"$DAILY_API_KEY",
|
||||||
"$DAILY_SCALE_PLAN",
|
"$DAILY_SCALE_PLAN",
|
||||||
|
|
Loading…
Reference in New Issue