From be1b9d1a79c5ee49ada82dc2a7d63fe94e05dbf3 Mon Sep 17 00:00:00 2001 From: Somay Chauhan Date: Wed, 29 Jan 2025 21:26:23 +0530 Subject: [PATCH] feat: skip confirm step in booker (#18773) * feat: skip confirm step * better naming * disable on loading * feat: added cloudflare turnstile captcha to booker * Update Booker.tsx * Update AvailableTimeSlots.tsx * made optional to fix type errors * Update Booker.tsx * Update getBookingResponsesSchema.ts * Update Booker.tsx * fixed failing tests * added tests * fix: fixed failing embed tests --- apps/web/playwright/booking-pages.e2e.ts | 17 ++ .../manage-booking-questions.e2e.ts | 2 - packages/embeds/embed-core/playground.ts | 2 - packages/features/bookings/Booker/Booker.tsx | 55 +++++- .../Booker/components/AvailableTimeSlots.tsx | 23 ++- .../BookEventForm/BookEventForm.tsx | 29 +-- .../components/hooks/useSkipConfirmStep.ts | 43 +++++ .../bookings/components/AvailableTimes.tsx | 170 +++++++++++------- .../bookings/lib/getBookingResponsesSchema.ts | 19 +- .../hooks/bookings/useHandleBookEvent.ts | 5 +- 10 files changed, 251 insertions(+), 114 deletions(-) create mode 100644 packages/features/bookings/Booker/components/hooks/useSkipConfirmStep.ts diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index 5d83253fd27b9c..e10bc3fa33ad13 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -401,6 +401,23 @@ test.describe("prefill", () => { await expect(page.locator('[name="email"]')).toHaveValue(testEmail); }); }); + + test("skip confirm step if all fields are prefilled from query params", async ({ page }) => { + await page.goto("/pro/30min"); + const url = new URL(page.url()); + url.searchParams.set("name", testName); + url.searchParams.set("email", testEmail); + url.searchParams.set("guests", "guest1@example.com"); + url.searchParams.set("guests", "guest2@example.com"); + url.searchParams.set("notes", "This is an additional note"); + await page.goto(url.toString()); + await selectFirstAvailableTimeSlotNextMonth(page); + + await expect(page.locator('[data-testid="skip-confirm-book-button"]')).toBeVisible(); + await page.click('[data-testid="skip-confirm-book-button"]'); + + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + }); }); test.describe("Booking on different layouts", () => { diff --git a/apps/web/playwright/manage-booking-questions.e2e.ts b/apps/web/playwright/manage-booking-questions.e2e.ts index 3a040c118be8f4..9443f13f1b7a71 100644 --- a/apps/web/playwright/manage-booking-questions.e2e.ts +++ b/apps/web/playwright/manage-booking-questions.e2e.ts @@ -172,7 +172,6 @@ test.describe("Manage Booking Questions", () => { prefillUrl.searchParams.append("email", "john@example.com"); prefillUrl.searchParams.append("guests", "guest1@example.com"); prefillUrl.searchParams.append("guests", "guest2@example.com"); - prefillUrl.searchParams.append("notes", "This is an additional note"); await page.goto(prefillUrl.toString()); await bookTimeSlot({ page, skipSubmission: true }); await expectSystemFieldsToBeThereOnBookingPage({ @@ -185,7 +184,6 @@ test.describe("Manage Booking Questions", () => { }, email: "john@example.com", guests: ["guest1@example.com", "guest2@example.com"], - notes: "This is an additional note", }, }); }); diff --git a/packages/embeds/embed-core/playground.ts b/packages/embeds/embed-core/playground.ts index 74b51218f8da99..f1f71d85848746 100644 --- a/packages/embeds/embed-core/playground.ts +++ b/packages/embeds/embed-core/playground.ts @@ -63,7 +63,6 @@ if (only === "all" || only === "ns:default") { }, name: "John", email: "johndoe@gmail.com", - notes: "Test Meeting", guests: ["janedoe@example.com", "test@example.com"], theme: "dark", "flag.coep": "true", @@ -454,7 +453,6 @@ if (only === "all" || only == "ns:floatingButton") { "flag.coep": "true", name: "John", email: "johndoe@gmail.com", - notes: "Test Meeting", guests: ["janedoe@example.com", "test@example.com"], ...(theme ? { theme } : {}), }, diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 827d498ef2676c..582a3669deabbd 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -8,9 +8,11 @@ import { shallow } from "zustand/shallow"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo"; import dayjs from "@calcom/dayjs"; +import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep"; import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import classNames from "@calcom/lib/classNames"; +import { CLOUDFLARE_SITE_ID, CLOUDFLARE_USE_TURNSTILE_IN_BOOKER } from "@calcom/lib/constants"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; @@ -33,6 +35,8 @@ import { useBookerStore } from "./store"; import type { BookerProps, WrappedBookerProps } from "./types"; import { isBookingDryRun } from "./utils/isBookingDryRun"; +const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false }); + const loadFramerFeatures = () => import("./framer-features").then((res) => res.default); const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy").then((mod) => mod.default)); const UnpublishedEntity = dynamic(() => @@ -78,6 +82,7 @@ const BookerComponent = ({ const searchParams = useCompatSearchParams(); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); + const selectedDate = useBookerStore((state) => state.selectedDate); const { shouldShowFormInDialog, @@ -128,6 +133,8 @@ const BookerComponent = ({ const { handleBookEvent, errors, loadingStates, expiryTime, instantVideoMeetingUrl } = bookings; + const watchedCfToken = bookingForm.watch("cfToken"); + const { isEmailVerificationModalVisible, setEmailVerificationModalVisible, @@ -153,29 +160,46 @@ const BookerComponent = ({ } }; + const skipConfirmStep = useSkipConfirmStep(bookingForm, event?.data?.bookingFields); + + // Cloudflare Turnstile Captcha + const shouldRenderCaptcha = !!( + !process.env.NEXT_PUBLIC_IS_E2E && + renderCaptcha && + CLOUDFLARE_SITE_ID && + CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1" && + (bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep)) + ); + useEffect(() => { if (event.isPending) return setBookerState("loading"); if (!selectedDate) return setBookerState("selecting_date"); - if (!selectedTimeslot) return setBookerState("selecting_time"); + if (!selectedTimeslot || skipConfirmStep) return setBookerState("selecting_time"); return setBookerState("booking"); - }, [event, selectedDate, selectedTimeslot, setBookerState]); + }, [event, selectedDate, selectedTimeslot, setBookerState, skipConfirmStep]); const slot = getQueryParam("slot"); + useEffect(() => { setSelectedTimeslot(slot || null); }, [slot, setSelectedTimeslot]); + + const onSubmit = (timeSlot?: string) => { + renderConfirmNotVerifyEmailButtonCond ? handleBookEvent(timeSlot) : handleVerifyEmail(); + }; + const EventBooker = useMemo(() => { return bookerState === "booking" ? ( { setSelectedTimeslot(null); if (seatedEventData.bookingUid) { setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined }); } }} - onSubmit={renderConfirmNotVerifyEmailButtonCond ? handleBookEvent : handleVerifyEmail} + onSubmit={() => (renderConfirmNotVerifyEmailButtonCond ? handleBookEvent() : handleVerifyEmail())} errorRef={bookerFormErrorRef} errors={{ ...formErrors, ...errors }} loadingStates={loadingStates} @@ -249,6 +273,8 @@ const BookerComponent = ({ verifyCode?.verifyCodeWithSessionNotRequired, verifyCode?.verifyCodeWithSessionRequired, isPlatform, + shouldRenderCaptcha, + isVerificationCodeSending, ]); /** @@ -442,6 +468,13 @@ const BookerComponent = ({ seatsPerTimeSlot={event.data?.seatsPerTimeSlot} showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount} event={event} + loadingStates={loadingStates} + renderConfirmNotVerifyEmailButtonCond={renderConfirmNotVerifyEmailButtonCond} + isVerificationCodeSending={isVerificationCodeSending} + onSubmit={onSubmit} + skipConfirmStep={skipConfirmStep} + shouldRenderCaptcha={shouldRenderCaptcha} + watchedCfToken={watchedCfToken} /> @@ -472,7 +505,19 @@ const BookerComponent = ({ /> )} - {!hideBranding && (!isPlatform || isPlatformBookerEmbed) && ( + + {shouldRenderCaptcha && ( +
+ { + bookingForm.setValue("cfToken", token); + }} + /> +
+ )} + + {!hideBranding && (!isPlatform || isPlatformBookerEmbed) && !shouldRenderCaptcha && ( | null; + data?: Pick | null; }; customClassNames?: { availableTimeSlotsContainer?: string; @@ -29,6 +30,13 @@ type AvailableTimeSlotsProps = { availableTimeSlotsTimeFormatToggle?: string; availableTimes?: string; }; + loadingStates: IUseBookingLoadingStates; + isVerificationCodeSending: boolean; + renderConfirmNotVerifyEmailButtonCond: boolean; + onSubmit: (timeSlot?: string) => void; + skipConfirmStep: boolean; + shouldRenderCaptcha?: boolean; + watchedCfToken?: string; }; /** @@ -38,15 +46,17 @@ type AvailableTimeSlotsProps = { * will also fetch the next `extraDays` days and show multiple days * in columns next to each other. */ + export const AvailableTimeSlots = ({ extraDays, limitHeight, - seatsPerTimeSlot, showAvailableSeatsCount, schedule, isLoading, - event, customClassNames, + skipConfirmStep, + onSubmit, + ...props }: AvailableTimeSlotsProps) => { const selectedDate = useBookerStore((state) => state.selectedDate); const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); @@ -71,6 +81,9 @@ export const AvailableTimeSlots = ({ showAvailableSeatsCount, }); } + if (skipConfirmStep) { + onSubmit(time); + } return; }; @@ -135,9 +148,9 @@ export const AvailableTimeSlots = ({ showTimeFormatToggle={!isColumnView} onTimeSelect={onTimeSelect} slots={slots.slots} - seatsPerTimeSlot={seatsPerTimeSlot} showAvailableSeatsCount={showAvailableSeatsCount} - event={event} + skipConfirmStep={skipConfirmStep} + {...props} /> ))} diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index 4714aa72da91d9..60ab42fe282f71 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -1,18 +1,12 @@ import type { TFunction } from "next-i18next"; import { Trans } from "next-i18next"; -import dynamic from "next/dynamic"; import Link from "next/link"; import { useMemo, useState } from "react"; import type { FieldError } from "react-hook-form"; import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo"; import type { BookerEvent } from "@calcom/features/bookings/types"; -import { - WEBSITE_PRIVACY_POLICY_URL, - WEBSITE_TERMS_URL, - CLOUDFLARE_SITE_ID, - CLOUDFLARE_USE_TURNSTILE_IN_BOOKER, -} from "@calcom/lib/constants"; +import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants"; import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Button, EmptyScreen, Form } from "@calcom/ui"; @@ -23,8 +17,6 @@ import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBo import { BookingFields } from "./BookingFields"; import { FormSkeleton } from "./Skeleton"; -const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false }); - type BookEventFormProps = { onCancel?: () => void; onSubmit: () => void; @@ -37,7 +29,7 @@ type BookEventFormProps = { extraOptions: Record; isPlatform?: boolean; isVerificationCodeSending: boolean; - renderCaptcha?: boolean; + shouldRenderCaptcha?: boolean; }; export const BookEventForm = ({ @@ -54,7 +46,7 @@ export const BookEventForm = ({ extraOptions, isVerificationCodeSending, isPlatform = false, - renderCaptcha, + shouldRenderCaptcha, }: Omit & { eventQuery: { isError: boolean; @@ -71,13 +63,6 @@ export const BookEventForm = ({ const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); - // Cloudflare Turnstile Captcha - const shouldRenderCaptcha = - !process.env.NEXT_PUBLIC_IS_E2E && - renderCaptcha && - CLOUDFLARE_SITE_ID && - CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1"; - const [responseVercelIdHeader] = useState(null); const { t } = useLocale(); @@ -140,14 +125,6 @@ export const BookEventForm = ({ )} {/* Cloudflare Turnstile Captcha */} - {shouldRenderCaptcha ? ( - { - bookingForm.setValue("cfToken", token); - }} - /> - ) : null} {!isPlatform && (
{ + const bookingFormValues = bookingForm.getValues(); + + const [canSkip, setCanSkip] = useState(false); + const rescheduleUid = useBookerStore((state) => state.rescheduleUid); + + useEffect(() => { + const checkSkipStep = async () => { + if (!bookingFields) { + setCanSkip(false); + return; + } + + try { + const responseSchema = getBookingResponsesSchemaWithOptionalChecks({ + bookingFields, + view: rescheduleUid ? "reschedule" : "booking", + }); + const responseSafeParse = await responseSchema.safeParseAsync(bookingFormValues.responses); + + setCanSkip(responseSafeParse.success); + } catch (error) { + setCanSkip(false); + } + }; + + checkSkipStep(); + }, [bookingFormValues, bookingFields, rescheduleUid]); + + return canSkip; +}; + +export default useSkipConfirmStep; diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index 61cb28ec0e5af8..238f7dd739affd 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -1,15 +1,17 @@ // We do not need to worry about importing framer-motion here as it is lazy imported in Booker. import * as HoverCard from "@radix-ui/react-hover-card"; import { AnimatePresence, m } from "framer-motion"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useIsPlatform } from "@calcom/atoms/monorepo"; import type { IOutOfOfficeData } from "@calcom/core/getUserAvailability"; import dayjs from "@calcom/dayjs"; import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots"; +import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; import type { BookerEvent } from "@calcom/features/bookings/types"; import type { Slots } from "@calcom/features/schedules"; import { classNames } from "@calcom/lib"; +import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { localStorage } from "@calcom/lib/webstorage"; import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; @@ -28,18 +30,28 @@ type TOnTimeSelect = ( bookingUid?: string ) => void; -type AvailableTimesProps = { +export type AvailableTimesProps = { slots: IGetAvailableSlots["slots"][string]; - onTimeSelect: TOnTimeSelect; - seatsPerTimeSlot?: number | null; - showAvailableSeatsCount?: boolean | null; showTimeFormatToggle?: boolean; className?: string; +} & Omit; + +type SlotItemProps = { + slot: Slots[string][number]; + seatsPerTimeSlot?: number | null; selectedSlots?: string[]; + onTimeSelect: TOnTimeSelect; + showAvailableSeatsCount?: boolean | null; event: { - data?: Pick | null; + data?: Pick | null; }; customClassNames?: string; + loadingStates?: IUseBookingLoadingStates; + isVerificationCodeSending?: boolean; + renderConfirmNotVerifyEmailButtonCond?: boolean; + skipConfirmStep?: boolean; + shouldRenderCaptcha?: boolean; + watchedCfToken?: string; }; const SlotItem = ({ @@ -50,25 +62,29 @@ const SlotItem = ({ showAvailableSeatsCount, event, customClassNames, -}: { - slot: Slots[string][number]; - seatsPerTimeSlot?: number | null; - selectedSlots?: string[]; - onTimeSelect: TOnTimeSelect; - showAvailableSeatsCount?: boolean | null; - event: { - data?: Pick | null; - }; - customClassNames?: string; -}) => { + loadingStates, + renderConfirmNotVerifyEmailButtonCond, + isVerificationCodeSending, + skipConfirmStep, + shouldRenderCaptcha, + watchedCfToken, +}: SlotItemProps) => { const { t } = useLocale(); + const { data: eventData } = event; + + const isPaidEvent = useMemo(() => { + if (!eventData?.price) return false; + const paymentAppData = getPaymentAppData(eventData); + return eventData?.price > 0 && !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; + }, [eventData]); + const overlayCalendarToggled = getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); + const { timeFormat, timezone } = useBookerTime(); const bookingData = useBookerStore((state) => state.bookingData); const layout = useBookerStore((state) => state.layout); - const { data: eventData } = event; const hasTimeSlots = !!seatsPerTimeSlot; const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone); @@ -82,35 +98,32 @@ const SlotItem = ({ const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60; + const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); + const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({ start: computedDateWithUsersTimezone, selectedDuration: eventData?.length ?? 0, offset, }); - const [overlapConfirm, setOverlapConfirm] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); const onButtonClick = useCallback(() => { - if (!overlayCalendarToggled || (isOverlapping && overlapConfirm)) { - onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid); + if (!showConfirm && ((overlayCalendarToggled && isOverlapping) || skipConfirmStep)) { + setShowConfirm(true); return; } - - if (isOverlapping) { - setOverlapConfirm(true); - return; - } - onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid); }, [ overlayCalendarToggled, isOverlapping, - overlapConfirm, + showConfirm, onTimeSelect, slot.time, slot?.attendees, slot.bookingUid, seatsPerTimeSlot, + skipConfirmStep, ]); return ( @@ -118,7 +131,15 @@ const SlotItem = ({
- {overlapConfirm && isOverlapping && ( + {showConfirm && ( - + {skipConfirmStep ? ( + + ) : ( + + )} - - -
-
-

Busy

+ {isOverlapping && ( + + +
+
+

Busy

+
+

+ {overlappingTimeStart} - {overlappingTimeEnd} +

-

- {overlappingTimeStart} - {overlappingTimeEnd} -

-
- - + + + )} )}
@@ -190,14 +242,9 @@ const SlotItem = ({ export const AvailableTimes = ({ slots, - onTimeSelect, - seatsPerTimeSlot, - showAvailableSeatsCount, showTimeFormatToggle = true, className, - selectedSlots, - event, - customClassNames, + ...props }: AvailableTimesProps) => { const { t } = useLocale(); @@ -226,18 +273,7 @@ export const AvailableTimes = ({ {oooBeforeSlots && !oooAfterSlots && } {slots.map((slot) => { if (slot.away) return null; - return ( - - ); + return ; })} {oooAfterSlots && !oooBeforeSlots && }
diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index 9961ffb8df26b5..24fff7905c51ac 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -34,6 +34,12 @@ export default function getBookingResponsesSchema({ bookingFields, view }: Commo return preprocess({ schema, bookingFields, isPartialSchema: false, view }); } +// Should be used when we want to check if the optional fields are entered and valid as well +export function getBookingResponsesSchemaWithOptionalChecks({ bookingFields, view }: CommonParams) { + const schema = bookingResponses.and(z.record(z.any())); + return preprocess({ schema, bookingFields, isPartialSchema: false, view, checkOptional: true }); +} + // TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder // It allows anyone using FormBuilder to get the same preprocessing automatically function preprocess({ @@ -41,12 +47,14 @@ function preprocess({ bookingFields, isPartialSchema, view: currentView, + checkOptional = false, }: CommonParams & { schema: T; // It is useful when we want to prefill the responses with the partial values. Partial can be in 2 ways // - Not all required fields are need to be provided for prefill. // - Even a field response itself can be partial so the content isn't validated e.g. a field with type="phone" can be given a partial phone number(e.g. Specifying the country code like +91) isPartialSchema: boolean; + checkOptional?: boolean; }): z.ZodType, z.infer, z.infer> { const preprocessed = z.preprocess( (responses) => { @@ -149,8 +157,11 @@ function preprocess({ if (bookingField.hideWhenJustOneOption) { hidden = hidden || numOptions <= 1; } + let isRequired = false; // If the field is hidden, then it can never be required - const isRequired = hidden ? false : isFieldApplicableToCurrentView ? bookingField.required : false; + if (!hidden && isFieldApplicableToCurrentView) { + isRequired = checkOptional || !!bookingField.required; + } if ((isPartialSchema || !isRequired) && value === undefined) { continue; @@ -162,7 +173,7 @@ function preprocess({ } if (bookingField.type === "email") { - if (!bookingField.hidden && bookingField.required) { + if (!bookingField.hidden && checkOptional ? true : bookingField.required) { // Email RegExp to validate if the input is a valid email if (!emailSchema.safeParse(value).success) { ctx.addIssue({ @@ -285,9 +296,7 @@ function preprocess({ const typeOfOptionInput = optionField?.type; if ( // Either the field is required or there is a radio selected, we need to check if the optionInput is required or not. - (isRequired || value?.value) && - optionField?.required && - !optionValue + (isRequired || value?.value) && checkOptional ? true : optionField?.required && !optionValue ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("error_required_field") }); return; diff --git a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts index 3bef4e4dc78c51..830773fbf5a470 100644 --- a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts +++ b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts @@ -39,7 +39,7 @@ export const useHandleBookEvent = ({ routingFormSearchParams, }: UseHandleBookingProps) => { const setFormValues = useBookerStore((state) => state.setFormValues); - const timeslot = useBookerStore((state) => state.selectedTimeslot); + const storeTimeSlot = useBookerStore((state) => state.selectedTimeslot); const duration = useBookerStore((state) => state.selectedDuration); const { timezone } = useBookerTime(); const rescheduleUid = useBookerStore((state) => state.rescheduleUid); @@ -55,8 +55,9 @@ export const useHandleBookEvent = ({ const crmOwnerRecordType = useBookerStore((state) => state.crmOwnerRecordType); const crmAppSlug = useBookerStore((state) => state.crmAppSlug); - const handleBookEvent = () => { + const handleBookEvent = (inputTimeSlot?: string) => { const values = bookingForm.getValues(); + const timeslot = inputTimeSlot ?? storeTimeSlot; if (timeslot) { // Clears form values stored in store, so old values won't stick around. setFormValues({});