From a1fc6668d54b994a299a6507e99e3d8d48f50f5b Mon Sep 17 00:00:00 2001 From: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:27:32 +0530 Subject: [PATCH] feat: ability to require an email (#18057) * feat: ability to require an email * fix: requireEmails empty bug * Update apps/web/public/static/locales/en/common.json * Update apps/web/public/static/locales/en/common.json * add tests --------- Co-authored-by: Keith Williams --- apps/web/public/static/locales/en/common.json | 2 + .../lib/getBookingResponsesSchema.test.ts | 78 +++++++++++++++++++ .../bookings/lib/getBookingResponsesSchema.ts | 12 +++ .../features/form-builder/FormBuilder.tsx | 25 +++++- packages/features/form-builder/schema.ts | 6 +- 5 files changed, 119 insertions(+), 4 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 7330fb2f5d..5401779406 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2846,7 +2846,9 @@ "rr_distribution_method_balanced_title": "Load balancing", "rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.", "exclude_emails_that_contain": "Exclude emails that contain ...", + "require_emails_that_contain": "Require emails that contain ...", "exclude_emails_match_found_error_message": "Please enter a valid work email address", + "require_emails_no_match_found_error_message": "You are not allowed to book with this email domain. Please contact the owner of this link.", "search_columns": "Search columns", "search_options": "Search options", "disable_org_url_label": "Disable public organization profile and redirect", diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index 6ac5b47962..8160b584ed 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -12,6 +12,7 @@ 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 CUSTOM_EMAIL_REQUIRED_ERROR_MSG = "require_emails_no_match_found_error_message"; const ZOD_REQUIRED_FIELD_ERROR_MSG = "Required"; describe("getBookingResponsesSchema", () => { @@ -1318,3 +1319,80 @@ describe("excluded email/domain validation", () => { }); }); }); + +describe("require email/domain validation", () => { + test("should fail if the requried email/domain is not present", async () => { + const requiredEmails = "gmail.com, user@hotmail.com, @yahoo.com"; + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + requireEmails: requiredEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "test@test.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_REQUIRED_ERROR_MSG}`, + }) + ); + }); + + test("should pass if the required email/domain is present", async () => { + const requiredEmails = "gmail.com, user@hotmail.com, @yahoo.com"; + + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + requireEmails: requiredEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "test@gmail.com", + }); + + expect(parsedResponses.success).toBe(true); + + if (!parsedResponses.success) { + throw new Error("Should not reach here"); + } + + expect(parsedResponses.data).toEqual({ + name: "test", + email: "test@gmail.com", + }); + }); +}); diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index 493118f946..760fb6b79d 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -183,6 +183,18 @@ function preprocess({ message: m("exclude_emails_match_found_error_message"), }); } + const requiredEmails = + bookingField.requireEmails + ?.split(",") + .map((domain) => domain.trim()) + .filter(Boolean) || []; + const requiredEmailsMatch = requiredEmails.find((email) => bookerEmail.includes(email)); + if (requiredEmails.length > 0 && !requiredEmailsMatch) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: m("require_emails_no_match_found_error_message"), + }); + } } continue; diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index 1cf9e90e37..6a17630bfd 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -33,7 +33,7 @@ import { import { fieldTypesConfigMap } from "./fieldTypes"; import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml"; -import { type fieldsSchema, excludeEmailSchema } from "./schema"; +import { type fieldsSchema, excludeOrRequireEmailSchema } from "./schema"; import { getFieldIdentifier } from "./utils/getFieldIdentifier"; import { getConfig as getVariantsConfig } from "./utils/variantsConfig"; @@ -574,13 +574,34 @@ function FieldEditDialog({ ) : null} + {formFieldType === "email" && ( + { + try { + excludeOrRequireEmailSchema.parse(e.target.value); + fieldForm.clearErrors("requireEmails"); + } catch (err) { + if (err instanceof ZodError) { + fieldForm.setError("requireEmails", { + message: err.errors[0]?.message || "Invalid input", + }); + } + } + }} + label={t("require_emails_that_contain")} + placeholder="gmail.com, hotmail.com, ..." + /> + )} + {formFieldType === "email" && ( { try { - excludeEmailSchema.parse(e.target.value); + excludeOrRequireEmailSchema.parse(e.target.value); fieldForm.clearErrors("excludeEmails"); } catch (err) { if (err instanceof ZodError) { diff --git a/packages/features/form-builder/schema.ts b/packages/features/form-builder/schema.ts index 07b2059e02..9856d37533 100644 --- a/packages/features/form-builder/schema.ts +++ b/packages/features/form-builder/schema.ts @@ -36,7 +36,7 @@ export const EditableSchema = z.enum([ "user-readonly", // All fields are readOnly. ]); -export const excludeEmailSchema = z.string().superRefine((val, ctx) => { +export const excludeOrRequireEmailSchema = z.string().superRefine((val, ctx) => { const allDomains = val.split(",").map((dom) => dom.trim()); const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/; @@ -121,7 +121,9 @@ const baseFieldSchema = z.object({ maxLength: z.number().optional(), // Emails that needs to be excluded - excludeEmails: excludeEmailSchema.optional(), + excludeEmails: excludeOrRequireEmailSchema.optional(), + // Emails that need to be required + requireEmails: excludeOrRequireEmailSchema.optional(), }); export const variantsConfigSchema = z.object({