From d0d8878f3463b39c7a58f6c0082de36752d0cab0 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:32:39 -0500 Subject: [PATCH] Adds ability to add custom sender names for workflow emails (#6463) Co-authored-by: CarinaWolli --- .env.example | 2 + README.md | 1 + apps/web/public/static/locales/en/common.json | 1 + .../workflows/api/scheduleEmailReminders.ts | 5 +- .../workflows/components/AddActionDialog.tsx | 54 +++++++++++---- .../components/WorkflowDetailsPage.tsx | 14 +++- .../components/WorkflowStepContainer.tsx | 66 +++++++++++-------- .../features/ee/workflows/lib/getOptions.ts | 5 +- .../features/ee/workflows/lib/isSMSAction.ts | 5 ++ .../lib/reminders/emailReminderManager.ts | 13 +++- .../lib/reminders/reminderScheduler.ts | 8 ++- .../features/ee/workflows/pages/workflow.tsx | 18 +++-- packages/lib/constants.ts | 1 + .../trpc/server/routers/viewer/workflows.tsx | 33 +++++++--- turbo.json | 1 + 15 files changed, 158 insertions(+), 69 deletions(-) create mode 100644 packages/features/ee/workflows/lib/isSMSAction.ts diff --git a/.env.example b/.env.example index 14a7a32f42..ebe8537385 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/README.md b/README.md index 8e9743c44e..ae612405e2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 6e8f6b42f1..13b0cce05a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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" } diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index e0ee21024d..c4da9148d1 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -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, diff --git a/packages/features/ee/workflows/components/AddActionDialog.tsx b/packages/features/ee/workflows/components/AddActionDialog.tsx index 0e5872acf3..3acca421e1 100644 --- a/packages/features/ee/workflows/components/AddActionDialog.tsx +++ b/packages/features/ee/workflows/components/AddActionDialog.tsx @@ -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>; - 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({ 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) => {
{ - 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) => { )} - {isSenderIdNeeded && ( + {isSenderIdNeeded ? ( + <> +
+ + +
+ {form.formState.errors && form.formState?.errors?.senderId && ( +

{t("sender_id_error_message")}

+ )} + + ) : (
- + +
)} {form.getValues("action") === WorkflowActions.SMS_ATTENDEE && ( diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index b658e2a364..aa8c93b658 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -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); diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index a498574750..3d6309c0af 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -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 && ( - <> -
- -
- {form.formState.errors.steps && - form.formState?.errors?.steps[step.stepNumber - 1]?.sender && ( -

{t("sender_id_error_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 && (
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, }; }); } diff --git a/packages/features/ee/workflows/lib/isSMSAction.ts b/packages/features/ee/workflows/lib/isSMSAction.ts new file mode 100644 index 0000000000..c781328143 --- /dev/null +++ b/packages/features/ee/workflows/lib/isSMSAction.ts @@ -0,0 +1,5 @@ +import { WorkflowActions } from "@prisma/client"; + +export function isSMSAction(action: WorkflowActions) { + return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER; +} diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 09282e8d7f..9a79bde934 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -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, diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index fde9158da3..769ac11f41 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -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 ); } }); diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index 2af97d9c14..59737a7f83 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -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`, { diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 3bde35f15d..9ee541a153 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -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) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 82d9ee9646..ea1cf18e39 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -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 & { 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( diff --git a/turbo.json b/turbo.json index 5b36ea1130..34e53ecd21 100644 --- a/turbo.json +++ b/turbo.json @@ -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",