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 <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Keith Williams
parent
351ab0790f
commit
a1fc6668d5
@@ -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",
|
||||
|
||||
@@ -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<typeof eventTypeBookingFields> & 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<typeof eventTypeBookingFields> & 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,6 +183,18 @@ function preprocess<T extends z.ZodType>({
|
||||
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;
|
||||
|
||||
@@ -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({
|
||||
<FieldWithLengthCheckSupport containerClassName="mt-6" fieldForm={fieldForm} />
|
||||
) : null}
|
||||
|
||||
{formFieldType === "email" && (
|
||||
<InputField
|
||||
{...fieldForm.register("requireEmails")}
|
||||
containerClassName="mt-6"
|
||||
onChange={(e) => {
|
||||
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" && (
|
||||
<InputField
|
||||
{...fieldForm.register("excludeEmails")}
|
||||
containerClassName="mt-6"
|
||||
onChange={(e) => {
|
||||
try {
|
||||
excludeEmailSchema.parse(e.target.value);
|
||||
excludeOrRequireEmailSchema.parse(e.target.value);
|
||||
fieldForm.clearErrors("excludeEmails");
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user