import type { TFunction } from "i18next"; import Link from "next/link"; import { useMemo, useState } from "react"; import type { FieldError } from "react-hook-form"; import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; import type { BookerEvent } from "@calcom/features/bookings/types"; import ServerTrans from "@calcom/lib/components/ServerTrans"; import { APP_NAME, WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { TimeFormat } from "@calcom/lib/timeFormat"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { Form } from "@calcom/ui/components/form"; import { formatEventFromTime } from "@calcom/features/bookings/Booker/utils/dates"; import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime"; import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/hooks/useBookingForm"; import type { IUseBookingErrors, IUseBookingLoadingStates } from "../../hooks/useBookings"; import { BookingFields } from "./BookingFields"; import { FormSkeleton } from "./Skeleton"; type BookEventFormProps = { onCancel?: () => void; onSubmit: () => void; errorRef: React.RefObject; errors: UseBookingFormReturnType["errors"] & IUseBookingErrors; loadingStates: IUseBookingLoadingStates; children?: React.ReactNode; bookingForm: UseBookingFormReturnType["bookingForm"]; renderConfirmNotVerifyEmailButtonCond: boolean; extraOptions: Record; isPlatform?: boolean; isVerificationCodeSending: boolean; isTimeslotUnavailable: boolean; shouldRenderCaptcha?: boolean; confirmButtonDisabled?: boolean; classNames?: { confirmButton?: string; backButton?: string; }; timeslot: string | null; }; export const BookEventForm = ({ onCancel, eventQuery, onSubmit, errorRef, errors, loadingStates, renderConfirmNotVerifyEmailButtonCond, bookingForm, children, extraOptions, isVerificationCodeSending, isPlatform = false, isTimeslotUnavailable, shouldRenderCaptcha, confirmButtonDisabled, classNames, timeslot, }: Omit & { eventQuery: { isError: boolean; isPending: boolean; data?: Pick | null; }; }) => { const eventType = eventQuery.data; const setFormValues = useBookerStoreContext((state) => state.setFormValues); const bookingData = useBookerStoreContext((state) => state.bookingData); const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid); const username = useBookerStoreContext((state) => state.username); const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); const { timeFormat, timezone } = useBookerTime(); const [responseVercelIdHeader] = useState(null); const { t, i18n } = useLocale(); const isPaidEvent = useMemo(() => { if (!eventType?.price) return false; const paymentAppData = getPaymentAppData(eventType); return eventType?.price > 0 && !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; }, [eventType]); const paymentCurrency = useMemo(() => { if (!eventType) return "USD"; return getPaymentAppData(eventType)?.currency || "USD"; }, [eventType]); if (eventQuery.isError) return ; if (eventQuery.isPending || !eventQuery.data) return ; if (!timeslot) return ( ); if (!eventType) { console.warn("No event type found for event", extraOptions); return ; } const watchedCfToken = bookingForm.watch("cfToken"); return (
{ // Form data is saved in store. This way when user navigates back to // still change the timeslot, and comes back to the form, all their values // still exist. This gets cleared when the form is submitted. const values = bookingForm.getValues(); setFormValues(values); }} form={bookingForm} handleSubmit={onSubmit} noValidate> -1)} fields={eventType.bookingFields} locations={eventType.locations} rescheduleUid={rescheduleUid || undefined} bookingData={bookingData} isPaidEvent={isPaidEvent} paymentCurrency={paymentCurrency} /> {errors.hasFormErrors || errors.hasDataErrors ? (
) : isTimeslotUnavailable ? (
Please select a new time , ]} /> } />
) : null} {!isPlatform && (
Terms , Privacy Policy. , ]} />
)} {isPlatformBookerEmbed && (
{t("proceeding_agreement")}{" "} {t("terms")} {" "} {t("and")}{" "} {t("privacy_policy")} .
)}
{isInstantMeeting ? ( ) : ( <> {!!onCancel && ( )} )}
{children}
); }; const getError = ({ globalError, dataError, t, responseVercelIdHeader, timeFormat, timezone, language, }: { globalError: FieldError | undefined; // It feels like an implementation detail to reimplement the types of useMutation here. // Since they don't matter for this function, I'd rather disable them then giving you // the cognitive overload of thinking to update them here when anything changes. // eslint-disable-next-line @typescript-eslint/no-explicit-any dataError: any; t: TFunction; responseVercelIdHeader: string | null; timeFormat: TimeFormat; timezone: string; language: string; }) => { if (globalError) return globalError?.message; const error = dataError; let date = ""; let count = 0; if (error.message === ErrorCode.BookerLimitExceededReschedule) { const formattedDate = formatEventFromTime({ date: error.data.startTime, timeFormat, timeZone: timezone, language, }); date = `${formattedDate.date} ${formattedDate.time}`; } if (error.message === ErrorCode.BookerLimitExceeded && error.data?.count) { count = error.data.count; } const messageKey = error.message === ErrorCode.BookerLimitExceeded ? "booker_upcoming_limit_reached" : error.message; return error?.message ? ( <> {responseVercelIdHeader ?? ""} {t(messageKey, { date, count })} {error.data?.traceId && (
{t("trace_reference_id")}: {error.data.traceId}
)} ) : ( <>{t("can_you_try_again")} ); };