Adds ability to add custom sender names for workflow emails (#6463)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Carina Wollendorfer 2023-01-18 09:32:39 -05:00 committed by GitHub
parent 59ff6c35ee
commit d0d8878f34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 158 additions and 69 deletions

View File

@ -82,6 +82,7 @@ SEND_FEEDBACK_EMAIL=
# Used for email reminders in workflows and internal sync services
SENDGRID_API_KEY=
SENDGRID_EMAIL=
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Twilio
# Used to send SMS reminders in workflows
@ -89,6 +90,7 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=

View File

@ -425,6 +425,7 @@ following
3. Copy API key to your .env file into the SENDGRID_API_KEY field
4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
6. Add your custom sender name to the .env file into the NEXT_PUBLIC_SENDGRID_SENDER_NAME field (fallback is Cal.com)
### Setting up Twilio for SMS reminders

View File

@ -1510,5 +1510,6 @@
"continue_to_install_google_calendar": "Continue to install Google Calendar",
"install_google_meet": "Install Google Meet",
"install_google_calendar": "Install Google Calendar",
"sender_name": "Sender name",
"no_recordings_found": "No recordings found"
}

View File

@ -152,7 +152,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
await sgMail.send({
to: sendTo,
from: senderEmail,
from: {
email: senderEmail,
name: reminder.workflowStep.sender || "Cal.com",
},
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,

View File

@ -6,6 +6,7 @@ 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@ -17,10 +18,10 @@ import {
DialogFooter,
EmailField,
Form,
Input,
Label,
PhoneInput,
Select,
TextField,
} from "@calcom/ui";
import { WORKFLOW_ACTIONS } from "../lib/constants";
@ -29,7 +30,13 @@ import { onlyLettersNumbersSpaces } from "../pages/workflow";
interface IAddActionDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => void;
addAction: (
action: WorkflowActions,
sendTo?: string,
numberRequired?: boolean,
senderId?: string,
senderName?: string
) => void;
}
interface ISelectActionOption {
@ -41,7 +48,8 @@ type AddActionFormValues = {
action: WorkflowActions;
sendTo?: string;
numberRequired?: boolean;
sender?: string;
senderId?: string;
senderName?: string;
};
export const AddActionDialog = (props: IAddActionDialog) => {
@ -59,17 +67,19 @@ export const AddActionDialog = (props: IAddActionDialog) => {
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
.optional(),
numberRequired: z.boolean().optional(),
sender: z
senderId: z
.string()
.refine((val) => onlyLettersNumbersSpaces(val))
.nullable(),
senderName: z.string().nullable(),
});
const form = useForm<AddActionFormValues>({
mode: "onSubmit",
defaultValues: {
action: WorkflowActions.EMAIL_HOST,
sender: SENDER_ID,
senderId: SENDER_ID,
senderName: SENDER_NAME,
},
resolver: zodResolver(formSchema),
});
@ -111,7 +121,13 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<Form
form={form}
handleSubmit={(values) => {
addAction(values.action, values.sendTo, values.numberRequired, values.sender);
addAction(
values.action,
values.sendTo,
values.numberRequired,
values.senderId,
values.senderName
);
form.unregister("sendTo");
form.unregister("action");
form.unregister("numberRequired");
@ -169,15 +185,25 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<EmailField required label={t("email_address")} {...form.register("sendTo")} />
</div>
)}
{isSenderIdNeeded && (
{isSenderIdNeeded ? (
<>
<div className="mt-5">
<Label>{t("sender_id")}</Label>
<Input
type="text"
placeholder={SENDER_ID}
maxLength={11}
{...form.register(`senderId`)}
/>
</div>
{form.formState.errors && form.formState?.errors?.senderId && (
<p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p>
)}
</>
) : (
<div className="mt-5">
<TextField
label={t("sender_id")}
type="text"
placeholder={SENDER_ID}
maxLength={11}
{...form.register(`sender`)}
/>
<Label>{t("sender_name")}</Label>
<Input type="text" placeholder={SENDER_NAME} {...form.register(`senderName`)} />
</div>
)}
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (

View File

@ -3,12 +3,13 @@ import { useRouter } from "next/router";
import { Dispatch, SetStateAction, useMemo, useState } from "react";
import { Controller, UseFormReturn } from "react-hook-form";
import { SENDER_ID } from "@calcom/lib/constants";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
import { Button, Icon, Label, MultiSelectCheckboxes, TextField } from "@calcom/ui";
import { isSMSAction } from "../lib/isSMSAction";
import type { FormValues } from "../pages/workflow";
import { AddActionDialog } from "./AddActionDialog";
import { DeleteDialog } from "./DeleteDialog";
@ -47,7 +48,13 @@ export default function WorkflowDetailsPage(props: Props) {
[data]
);
const addAction = (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => {
const addAction = (
action: WorkflowActions,
sendTo?: string,
numberRequired?: boolean,
sender?: string,
senderName?: string
) => {
const steps = form.getValues("steps");
const id =
steps?.length > 0
@ -71,7 +78,8 @@ export default function WorkflowDetailsPage(props: Props) {
emailSubject: null,
template: WorkflowTemplates.CUSTOM,
numberRequired: numberRequired || false,
sender: sender || SENDER_ID,
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,
numberVerificationPending: false,
};
steps?.push(step);

View File

@ -11,6 +11,7 @@ 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
@ -37,12 +38,12 @@ import {
TextField,
Editor,
AddVariablesDropdown,
Tooltip,
Input,
} from "@calcom/ui";
import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants";
import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions";
import { translateVariablesToEnglish } from "../lib/variableTranslations";
import { isSMSAction } from "../lib/isSMSAction";
import type { FormValues } from "../pages/workflow";
import { TimeTimeUnitInput } from "./TimeTimeUnitInput";
@ -340,28 +341,24 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
onChange={(val) => {
if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);
const wasSMSAction =
oldValue === WorkflowActions.SMS_ATTENDEE ||
oldValue === WorkflowActions.SMS_NUMBER;
const isSMSAction =
val.value === WorkflowActions.SMS_ATTENDEE ||
val.value === WorkflowActions.SMS_NUMBER;
if (isSMSAction) {
if (isSMSAction(val.value)) {
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(val.value === WorkflowActions.SMS_NUMBER);
setNumberVerified(false);
if (!wasSMSAction) {
if (!isSMSAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.sender`, SENDER_ID);
}
} else {
setIsPhoneNumberNeeded(false);
setIsSenderIdNeeded(false);
setIsEmailAddressNeeded(val.value === WorkflowActions.EMAIL_ADDRESS);
if (wasSMSAction) {
if (isSMSAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.senderName`, SENDER_NAME);
}
}
@ -468,25 +465,38 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)}
</>
)}
{isSenderIdNeeded && (
<>
<div className="pt-4">
<TextField
label={t("sender_id")}
type="text"
placeholder={SENDER_ID}
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>
)}
<div className="mt-2 rounded-md bg-gray-50 p-4 pt-0">
{isSenderIdNeeded ? (
<>
<div className="pt-4">
<Label>{t("sender_id")}</Label>
<Input
type="text"
placeholder={SENDER_ID}
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 className="pt-4">
<Label>{t("sender_name")}</Label>
<Input
type="text"
placeholder={SENDER_NAME}
{...form.register(`steps.${step.stepNumber - 1}.senderName`)}
/>
</div>
</>
)}
</div>
{form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && (
<div className="mt-2">
<Controller

View File

@ -2,18 +2,17 @@ import { WorkflowActions } from "@prisma/client";
import { TFunction } from "next-i18next";
import { TIME_UNIT, WORKFLOW_ACTIONS, WORKFLOW_TEMPLATES, WORKFLOW_TRIGGER_EVENTS } from "./constants";
import { isSMSAction } from "./isSMSAction";
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
.map((action) => {
const actionString = t(`${action.toLowerCase()}_action`);
const isSMSAction = action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
return {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: action,
needsUpgrade: isSMSAction && !isTeamsPlan,
needsUpgrade: isSMSAction(action) && !isTeamsPlan,
};
});
}

View File

@ -0,0 +1,5 @@
import { WorkflowActions } from "@prisma/client";
export function isSMSAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
}

View File

@ -38,7 +38,8 @@ export const scheduleEmailReminder = async (
emailSubject: string,
emailBody: string,
workflowStepId: number,
template: WorkflowTemplates
template: WorkflowTemplates,
sender: string
) => {
if (action === WorkflowActions.EMAIL_ADDRESS) return;
const { startTime, endTime } = evt;
@ -125,7 +126,10 @@ export const scheduleEmailReminder = async (
try {
await sgMail.send({
to: sendTo,
from: senderEmail,
from: {
email: senderEmail,
name: sender,
},
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,
@ -149,7 +153,10 @@ export const scheduleEmailReminder = async (
try {
await sgMail.send({
to: sendTo,
from: senderEmail,
from: {
email: senderEmail,
name: sender,
},
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,

View File

@ -6,7 +6,7 @@ import {
WorkflowTriggerEvents,
} from "@prisma/client";
import { SENDER_ID } from "@calcom/lib/constants";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { scheduleEmailReminder } from "./emailReminderManager";
@ -83,7 +83,8 @@ export const scheduleWorkflowReminders = async (
step.emailSubject || "",
step.reminderBody || "",
step.id,
step.template
step.template,
step.sender || SENDER_NAME
);
}
});
@ -151,7 +152,8 @@ export const sendCancelledReminders = async (
step.emailSubject || "",
step.reminderBody || "",
step.id,
step.template
step.template,
step.sender || SENDER_NAME
);
}
});

View File

@ -15,6 +15,7 @@ import { z } from "zod";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { SENDER_ID } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
@ -25,12 +26,13 @@ import { Alert, Button, Form, showToast } from "@calcom/ui";
import LicenseRequired from "../../common/components/v2/LicenseRequired";
import SkeletonLoader from "../components/SkeletonLoaderEdit";
import WorkflowDetailsPage from "../components/WorkflowDetailsPage";
import { isSMSAction } from "../lib/isSMSAction";
import { getTranslatedText, translateVariablesToEnglish } from "../lib/variableTranslations";
export type FormValues = {
name: string;
activeOn: Option[];
steps: WorkflowStep[];
steps: (WorkflowStep & { senderName: string | null })[];
trigger: WorkflowTriggerEvents;
time?: number;
timeUnit?: TimeUnit;
@ -69,6 +71,7 @@ const formSchema = z.object({
.refine((val) => onlyLettersNumbersSpaces(val))
.optional()
.nullable(),
senderName: z.string().optional().nullable(),
})
.array(),
});
@ -124,7 +127,11 @@ function WorkflowPage() {
//translate dynamic variables into local language
const steps = workflow.steps.map((step) => {
const updatedStep = step;
const updatedStep = {
...step,
senderName: step.sender,
sender: isSMSAction(step.action) ? step.sender : SENDER_ID,
};
if (step.reminderBody) {
updatedStep.reminderBody = getTranslatedText(step.reminderBody || "", {
locale: i18n.language,
@ -181,13 +188,12 @@ function WorkflowPage() {
let isVerified = true;
values.steps.forEach((step) => {
const isSMSAction =
step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER;
const strippedHtml = step.reminderBody?.replace(/<[^>]+>/g, "") || "";
const isBodyEmpty =
step.template === WorkflowTemplates.CUSTOM && !isSMSAction && strippedHtml.length <= 1;
step.template === WorkflowTemplates.CUSTOM &&
!isSMSAction(step.action) &&
strippedHtml.length <= 1;
if (isBodyEmpty) {
form.setError(`steps.${step.stepNumber - 1}.reminderBody`, {

View File

@ -16,6 +16,7 @@ export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "Cal.com";
export const SUPPORT_MAIL_ADDRESS = process.env.NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS || "help@cal.com";
export const COMPANY_NAME = process.env.NEXT_PUBLIC_COMPANY_NAME || "Cal.com, Inc.";
export const SENDER_ID = process.env.NEXT_PUBLIC_SENDER_ID || "Cal";
export const SENDER_NAME = process.env.NEXT_PUBLIC_SENDGRID_SENDER_NAME || "Cal.com";
// This is the URL from which all Cal Links and their assets are served.
// Use website URL to make links shorter(cal.com and not app.cal.com)

View File

@ -18,6 +18,7 @@ import {
TIME_UNIT,
} from "@calcom/features/ee/workflows/lib/constants";
import { getWorkflowActionOptions } from "@calcom/features/ee/workflows/lib/getOptions";
import { isSMSAction } from "@calcom/features/ee/workflows/lib/isSMSAction";
import {
deleteScheduledEmailReminder,
scheduleEmailReminder,
@ -32,16 +33,20 @@ import {
sendVerificationCode,
} from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber";
import { SENDER_ID } from "@calcom/lib/constants";
import { SENDER_NAME } from "@calcom/lib/constants";
// import { getErrorFromUnknown } from "@calcom/lib/errors";
import { getTranslation } from "@calcom/lib/server/i18n";
import { WorkflowStep } from "@calcom/prisma/client";
import { TRPCError } from "@trpc/server";
import { router, authedProcedure, authedRateLimitedProcedure } from "../../trpc";
import { viewerTeamsRouter } from "./teams";
function isSMSAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
function getSender(
step: Pick<WorkflowStep, "action" | "sender"> & { senderName: string | null | undefined }
) {
return isSMSAction(step.action) ? step.sender || SENDER_ID : step.senderName || SENDER_NAME;
}
export const workflowsRouter = router({
@ -166,7 +171,7 @@ export const workflowsRouter = router({
action: WorkflowActions.EMAIL_HOST,
template: WorkflowTemplates.REMINDER,
workflowId: workflow.id,
sender: SENDER_ID,
sender: SENDER_NAME,
numberVerificationPending: false,
},
});
@ -244,6 +249,7 @@ export const workflowsRouter = router({
template: z.enum(WORKFLOW_TEMPLATES),
numberRequired: z.boolean().nullable(),
sender: z.string().optional().nullable(),
senderName: z.string().optional().nullable(),
})
.array(),
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
@ -472,7 +478,8 @@ export const workflowsRouter = router({
step.emailSubject || "",
step.reminderBody || "",
step.id,
step.template
step.template,
step.senderName || SENDER_NAME
);
} else if (step.action === WorkflowActions.SMS_NUMBER) {
await scheduleSMSReminder(
@ -561,7 +568,11 @@ export const workflowsRouter = router({
emailSubject: newStep.template === WorkflowTemplates.CUSTOM ? newStep.emailSubject : null,
template: newStep.template,
numberRequired: newStep.numberRequired,
sender: newStep.sender || SENDER_ID,
sender: getSender({
action: newStep.action,
sender: newStep.sender || null,
senderName: newStep.senderName,
}),
numberVerificationPending: false,
},
});
@ -659,7 +670,8 @@ export const workflowsRouter = router({
newStep.emailSubject || "",
newStep.reminderBody || "",
newStep.id,
newStep.template
newStep.template,
newStep.senderName || SENDER_NAME
);
} else if (newStep.action === WorkflowActions.SMS_NUMBER) {
await scheduleSMSReminder(
@ -702,7 +714,11 @@ export const workflowsRouter = router({
addedSteps.forEach(async (step) => {
if (step) {
const newStep = step;
newStep.sender = step.sender || SENDER_ID;
newStep.sender = getSender({
action: newStep.action,
sender: newStep.sender || null,
senderName: newStep.senderName,
});
const createdStep = await ctx.prisma.workflowStep.create({
data: { ...step, numberVerificationPending: false },
});
@ -776,7 +792,8 @@ export const workflowsRouter = router({
step.emailSubject || "",
step.reminderBody || "",
createdStep.id,
step.template
step.template,
step.senderName || SENDER_NAME
);
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
await scheduleSMSReminder(

View File

@ -225,6 +225,7 @@
"$NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS",
"$NEXT_PUBLIC_COMPANY_NAME",
"$NEXT_PUBLIC_SENDER_ID",
"$NEXT_PUBLIC_SENDGRID_SENDER_NAME",
"$NEXT_PUBLIC_DISABLE_SIGNUP",
"$NEXTAUTH_COOKIE_DOMAIN",
"$NEXTAUTH_SECRET",