diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 51922d13a6..f27a2a36f4 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -299,6 +299,39 @@ const BookingPage = ({ } const bookEvent = (booking: BookingFormValues) => { + const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({ + label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", + value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "", + })); + + // Checking if custom inputs of type Phone number are valid to display error message on UI + if (eventType.customInputs.length) { + let isErrorFound = false; + eventType.customInputs.forEach((customInput) => { + if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) { + const input = bookingCustomInputs.find((i) => i.label === customInput.label); + try { + z.string({ + errorMap: () => ({ + message: `Missing ${customInput.type} customInput: '${customInput.label}'`, + }), + }) + .refine((val) => isValidPhoneNumber(val), { + message: "Phone number is invalid", + }) + .parse(input?.value); + } catch (err) { + isErrorFound = true; + bookingForm.setError(`customInputs.${customInput.id}`, { + type: "custom", + message: "Invalid Phone number", + }); + } + } + }); + if (isErrorFound) return; + } + telemetry.event( top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed, { isTeamBooking: document.URL.includes("team/") } @@ -355,10 +388,7 @@ const BookingPage = ({ attendeeAddress: booking.attendeeAddress, }), metadata, - customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ - label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", - value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "", - })), + customInputs: bookingCustomInputs, hasHashedBookingLink, hashedLink, smsReminderNumber: @@ -386,10 +416,7 @@ const BookingPage = ({ attendeeAddress: booking.attendeeAddress, }), metadata, - customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ - label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", - value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "", - })), + customInputs: bookingCustomInputs, hasHashedBookingLink, hashedLink, smsReminderNumber: @@ -793,6 +820,23 @@ const BookingPage = ({ )} + {input.type === EventTypeCustomInputType.PHONE && ( +
+ + name={`customInputs.${input.id}`} + control={bookingForm.control} + placeholder={t("enter_phone_number")} + id={`customInputs.${input.id}`} + required={input.required} + /> + {bookingForm.formState.errors?.customInputs?.[input.id] && ( +
+ +

{t("invalid_number")}

+
+ )} +
+ )} ))} {!eventType.disableGuests && guestToggle && ( diff --git a/apps/web/components/eventtype/CustomInputTypeForm.tsx b/apps/web/components/eventtype/CustomInputTypeForm.tsx index 1a7d7fee20..240be6091d 100644 --- a/apps/web/components/eventtype/CustomInputTypeForm.tsx +++ b/apps/web/components/eventtype/CustomInputTypeForm.tsx @@ -40,6 +40,7 @@ const CustomInputTypeForm: FC = (props) => { value: EventTypeCustomInputType.RADIO, label: t("radio"), }, + { value: EventTypeCustomInputType.PHONE, label: t("phone_number") }, ]; const { selectedCustomInput } = props; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 880fc4dc24..763e041f92 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -7,6 +7,7 @@ import { WebhookTriggerEvents, } from "@prisma/client"; import async from "async"; +import { isValidPhoneNumber } from "libphonenumber-js"; import { cloneDeep } from "lodash"; import type { NextApiRequest } from "next"; import short from "short-uuid"; @@ -1095,6 +1096,16 @@ function handleCustomInputs( z.literal(true, { errorMap: () => ({ message: `Missing ${etcInput.type} customInput: '${etcInput.label}'` }), }).parse(input?.value); + } else if (etcInput.type === "PHONE") { + z.string({ + errorMap: () => ({ + message: `Missing ${etcInput.type} customInput: '${etcInput.label}'`, + }), + }) + .refine((val) => isValidPhoneNumber(val), { + message: "Phone number is invalid", + }) + .parse(input?.value); } else { // type: NUMBER are also passed as string z.string({ diff --git a/packages/features/ee/package.json b/packages/features/ee/package.json index 3804e0c251..18043f1a38 100644 --- a/packages/features/ee/package.json +++ b/packages/features/ee/package.json @@ -12,6 +12,7 @@ "@hookform/resolvers": "^2.9.7", "@sendgrid/client": "^7.7.0", "@sendgrid/mail": "^7.6.2", + "libphonenumber-js": "^1.10.12", "twilio": "^3.80.1", "zod": "^3.20.2" }, diff --git a/packages/prisma/migrations/20221121115813_/migration.sql b/packages/prisma/migrations/20221121115813_/migration.sql new file mode 100644 index 0000000000..631ff23cbb --- /dev/null +++ b/packages/prisma/migrations/20221121115813_/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "EventTypeCustomInputType" ADD VALUE 'phone'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 20a630b61f..97b8b147e8 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -344,6 +344,7 @@ enum EventTypeCustomInputType { NUMBER @map("number") BOOL @map("bool") RADIO @map("radio") + PHONE @map("phone") } model EventTypeCustomInput {