Skip to content

Commit f9b294c

Browse files
fix: email and phone field validation (calcom#24432)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
1 parent e5f14c9 commit f9b294c

5 files changed

Lines changed: 239 additions & 4 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3788,6 +3788,10 @@
37883788
"configure_agent_to_handle_incoming_calls": "Configure agent to handle incoming calls",
37893789
"incoming_calls": "Incoming Calls",
37903790
"outgoing_calls": "Outgoing Calls",
3791+
"booking_fields_email_and_phone_both_hidden": "Both Email and Attendee Phone Number cannot be hidden",
3792+
"booking_fields_email_or_phone_required": "At least Email or Attendee Phone Number must be a required field",
3793+
"booking_fields_phone_required_when_email_hidden": "Attendee Phone Number must be required when Email is hidden",
3794+
"booking_fields_email_required_when_phone_hidden": "Email must be required when Attendee Phone Number is hidden",
37913795
"inbound_agent_setup_success": "Inbound agent setup successful",
37923796
"inbound_agent_configured": "Inbound agent configured",
37933797
"setup_inbound_agent": "Set up Inbound Agent",

packages/features/bookings/lib/getBookingResponsesSchema.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,66 @@ describe("getBookingResponsesSchema", () => {
176176
);
177177
});
178178

179+
test(`hidden required email field should not be validated`, async () => {
180+
const schema = getBookingResponsesSchema({
181+
bookingFields: [
182+
{
183+
name: "name",
184+
type: "name",
185+
required: true,
186+
},
187+
{
188+
name: "email",
189+
type: "email",
190+
required: true,
191+
hidden: true,
192+
},
193+
{
194+
name: "attendeePhoneNumber",
195+
type: "phone",
196+
required: true,
197+
},
198+
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
199+
view: "ALL_VIEWS",
200+
});
201+
const parsedResponses = await schema.safeParseAsync({
202+
name: "John",
203+
email: "",
204+
attendeePhoneNumber: "+919999999999",
205+
});
206+
expect(parsedResponses.success).toBe(true);
207+
});
208+
209+
test(`hidden required phone field should not be validated`, async () => {
210+
const schema = getBookingResponsesSchema({
211+
bookingFields: [
212+
{
213+
name: "name",
214+
type: "name",
215+
required: true,
216+
},
217+
{
218+
name: "email",
219+
type: "email",
220+
required: true,
221+
},
222+
{
223+
name: "attendeePhoneNumber",
224+
type: "phone",
225+
required: true,
226+
hidden: true,
227+
},
228+
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
229+
view: "ALL_VIEWS",
230+
});
231+
const parsedResponses = await schema.safeParseAsync({
232+
name: "John",
233+
email: "john@example.com",
234+
attendeePhoneNumber: "",
235+
});
236+
expect(parsedResponses.success).toBe(true);
237+
});
238+
179239
test(`firstName is required and lastName is optional by default`, async () => {
180240
const schema = getBookingResponsesSchema({
181241
bookingFields: [

packages/features/bookings/lib/getBookingResponsesSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ function preprocess<T extends z.ZodType>({
174174
}
175175

176176
if (bookingField.type === "email") {
177-
if (!bookingField.hidden && checkOptional ? true : bookingField.required) {
177+
if (!bookingField.hidden && (checkOptional || bookingField.required)) {
178178
// Email RegExp to validate if the input is a valid email
179179
if (!emailSchema.safeParse(value).success) {
180180
ctx.addIssue({

packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
77
import { TRPCError } from "@trpc/server";
88

99
import type { authedProcedure } from "../../../procedures/authedProcedure";
10-
import { createEventPbacProcedure } from "../util";
10+
import { createEventPbacProcedure, ensureEmailOrPhoneNumberIsPresent } from "../util";
1111

1212
// Mock dependencies
1313
vi.mock("@calcom/features/pbac/services/permission-check.service");
@@ -513,4 +513,163 @@ describe("createEventPbacProcedure", () => {
513513
);
514514
});
515515
});
516+
517+
describe("ensureEmailOrPhoneNumberIsPresent", () => {
518+
it("should throw error when both email and phone are hidden", () => {
519+
const fields = [
520+
{
521+
name: "email",
522+
type: "email" as const,
523+
required: true,
524+
hidden: true,
525+
},
526+
{
527+
name: "attendeePhoneNumber",
528+
type: "phone" as const,
529+
required: true,
530+
hidden: true,
531+
},
532+
];
533+
534+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
535+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
536+
expect.objectContaining({
537+
code: "BAD_REQUEST",
538+
message: "booking_fields_email_and_phone_both_hidden",
539+
})
540+
);
541+
});
542+
543+
it("should throw error when neither email nor phone is required", () => {
544+
const fields = [
545+
{
546+
name: "email",
547+
type: "email" as const,
548+
required: false,
549+
hidden: false,
550+
},
551+
{
552+
name: "attendeePhoneNumber",
553+
type: "phone" as const,
554+
required: false,
555+
hidden: false,
556+
},
557+
];
558+
559+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
560+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
561+
expect.objectContaining({
562+
code: "BAD_REQUEST",
563+
message: "booking_fields_email_or_phone_required",
564+
})
565+
);
566+
});
567+
568+
it("should throw error when email is hidden and phone is not required", () => {
569+
const fields = [
570+
{
571+
name: "email",
572+
type: "email" as const,
573+
required: true,
574+
hidden: true,
575+
},
576+
{
577+
name: "attendeePhoneNumber",
578+
type: "phone" as const,
579+
required: false,
580+
hidden: false,
581+
},
582+
];
583+
584+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
585+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
586+
expect.objectContaining({
587+
code: "BAD_REQUEST",
588+
message: "booking_fields_phone_required_when_email_hidden",
589+
})
590+
);
591+
});
592+
593+
it("should throw error when phone is hidden and email is not required", () => {
594+
const fields = [
595+
{
596+
name: "email",
597+
type: "email" as const,
598+
required: false,
599+
hidden: false,
600+
},
601+
{
602+
name: "attendeePhoneNumber",
603+
type: "phone" as const,
604+
required: true,
605+
hidden: true,
606+
},
607+
];
608+
609+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
610+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
611+
expect.objectContaining({
612+
code: "BAD_REQUEST",
613+
message: "booking_fields_email_required_when_phone_hidden",
614+
})
615+
);
616+
});
617+
618+
it("should pass when email is visible and required while phone is hidden", () => {
619+
const fields = [
620+
{
621+
name: "email",
622+
type: "email" as const,
623+
required: true,
624+
hidden: false,
625+
},
626+
{
627+
name: "attendeePhoneNumber",
628+
type: "phone" as const,
629+
required: false,
630+
hidden: true,
631+
},
632+
];
633+
634+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
635+
});
636+
637+
it("should pass when phone is visible and required while email is hidden", () => {
638+
const fields = [
639+
{
640+
name: "email",
641+
type: "email" as const,
642+
required: false,
643+
hidden: true,
644+
},
645+
{
646+
name: "attendeePhoneNumber",
647+
type: "phone" as const,
648+
required: true,
649+
hidden: false,
650+
},
651+
];
652+
653+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
654+
});
655+
656+
it("should pass when both email and phone are visible and required", () => {
657+
const fields = [
658+
{
659+
name: "email",
660+
type: "email" as const,
661+
required: true,
662+
hidden: false,
663+
},
664+
{
665+
name: "attendeePhoneNumber",
666+
type: "phone" as const,
667+
required: true,
668+
hidden: false,
669+
},
670+
];
671+
672+
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
673+
});
674+
});
516675
});

packages/trpc/server/routers/viewer/eventTypes/util.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,25 @@ export function ensureEmailOrPhoneNumberIsPresent(fields: TUpdateInputSchema["bo
274274
if (emailField?.hidden && attendeePhoneNumberField?.hidden) {
275275
throw new TRPCError({
276276
code: "BAD_REQUEST",
277-
message: `Both Email and Attendee Phone Number cannot be hidden`,
277+
message: "booking_fields_email_and_phone_both_hidden",
278278
});
279279
}
280280
if (!emailField?.required && !attendeePhoneNumberField?.required) {
281281
throw new TRPCError({
282282
code: "BAD_REQUEST",
283-
message: `At least Email or Attendee Phone Number need to be required field.`,
283+
message: "booking_fields_email_or_phone_required",
284+
});
285+
}
286+
if (emailField?.hidden && !attendeePhoneNumberField?.required) {
287+
throw new TRPCError({
288+
code: "BAD_REQUEST",
289+
message: "booking_fields_phone_required_when_email_hidden",
290+
});
291+
}
292+
if (attendeePhoneNumberField?.hidden && !emailField?.required) {
293+
throw new TRPCError({
294+
code: "BAD_REQUEST",
295+
message: "booking_fields_email_required_when_phone_hidden",
284296
});
285297
}
286298
}

0 commit comments

Comments
 (0)