diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 6ecbb0c3bb..dfccb51525 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2770,5 +2770,7 @@ "salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account", "salesforce_option": "Salesforce Option", "lookup_field_name": "Lookup Field Name", + "exclude_emails_that_contain" : "Exclude emails that contain ...", + "exclude_emails_match_found_error_message" : "Please enter a valid work email address", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index 0e5d21503c..6ac5b47962 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -11,6 +11,7 @@ const CUSTOM_REQUIRED_FIELD_ERROR_MSG = "error_required_field"; const CUSTOM_PHONE_VALIDATION_ERROR_MSG = "invalid_number"; const CUSTOM_EMAIL_VALIDATION_ERROR_MSG = "email_validation_error"; const CUSTOM_URL_VALIDATION_ERROR_MSG = "url_validation_error"; +const CUSTOM_EMAIL_EXCLUDED_ERROR_MSG = "exclude_emails_match_found_error_message"; const ZOD_REQUIRED_FIELD_ERROR_MSG = "Required"; describe("getBookingResponsesSchema", () => { @@ -1241,3 +1242,79 @@ describe("getBookingResponsesPartialSchema - Prefill validation", () => { ); }); }); + +describe("excluded email/domain validation", () => { + test("should fail if the email is present in excluded emails", async () => { + const excludedEmails = "spammer@cal.com, hotmail.com, yahoo, @gmail.com"; + + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + excludeEmails: excludedEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "harry@gmail.com", + }); + expect(parsedResponses.success).toBe(false); + + if (parsedResponses.success) { + throw new Error("Should not reach here"); + } + expect(parsedResponses.error.issues[0]).toEqual( + expect.objectContaining({ + code: "custom", + message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`, + }) + ); + }); + + test("should pass if the email is not present in excluded emails", async () => { + const excludedEmails = "spammer@cal.com, hotmail.com, yahoo, @gmail.com"; + + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + excludeEmails: excludedEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "harry@workmail.com", + }); + + expect(parsedResponses.success).toBe(true); + + if (!parsedResponses.success) { + throw new Error("Should not reach here"); + } + + expect(parsedResponses.data).toEqual({ + name: "test", + email: "harry@workmail.com", + }); + }); +}); diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index 3045bc1977..493118f946 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -162,13 +162,29 @@ function preprocess({ } if (bookingField.type === "email") { - // Email RegExp to validate if the input is a valid email - if (!bookingField.hidden && !emailSchema.safeParse(value).success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: m("email_validation_error"), - }); + if (!bookingField.hidden) { + // Email RegExp to validate if the input is a valid email + if (!emailSchema.safeParse(value).success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: m("email_validation_error"), + }); + } + + // validate the excluded emails + const bookerEmail = value; + const excludedEmails = + bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || []; + + const match = excludedEmails.find((email) => bookerEmail.includes(email)); + if (match) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: m("exclude_emails_match_found_error_message"), + }); + } } + continue; } diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index 0abe51b487..bd40f54411 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -1,4 +1,5 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import type { SubmitHandler, UseFormReturn } from "react-hook-form"; import { Controller, useFieldArray, useForm, useFormContext } from "react-hook-form"; @@ -32,7 +33,7 @@ import { import { fieldTypesConfigMap } from "./fieldTypes"; import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml"; -import type { fieldsSchema } from "./schema"; +import { fieldSchema, type fieldsSchema } from "./schema"; import { getFieldIdentifier } from "./utils/getFieldIdentifier"; import { getConfig as getVariantsConfig } from "./utils/variantsConfig"; @@ -459,7 +460,7 @@ function FieldEditDialog({ const { t } = useLocale(); const fieldForm = useForm({ defaultValues: dialog.data || {}, - // resolver: zodResolver(fieldSchema), + resolver: zodResolver(fieldSchema), }); const formFieldType = fieldForm.getValues("type"); @@ -573,6 +574,15 @@ function FieldEditDialog({ ) : null} + {fieldType.value === "email" && fieldForm.getValues("name") === "email" && ( + + )} + { + const allDomains = val.split(",").map((dom) => dom.trim()); + + const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/; + + /* + Valid patterns - [ example, example.anything, anyone@example.anything ] + Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ] + */ + + const isValid = !allDomains.some((domain) => !regex.test(domain)); + + if (!isValid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Enter valid domain or email", + }); + } + }) + .optional(), }); export const variantsConfigSchema = z.object({