diff --git a/packages/atoms/booker/Booker.docs.mdx b/packages/atoms/booker/Booker.docs.mdx index f92c09c954..7355e12376 100644 --- a/packages/atoms/booker/Booker.docs.mdx +++ b/packages/atoms/booker/Booker.docs.mdx @@ -1,6 +1,6 @@ import { Meta, Canvas, ArgsTable } from "@storybook/blocks"; import { Title } from "@calcom/storybook/components"; -import { Booker } from "./Booker"; +import { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper"; import * as BookerStories from "./Booker.stories"; diff --git a/packages/atoms/booker/Booker.stories.tsx b/packages/atoms/booker/Booker.stories.tsx index ab485daa3f..7ab0111f37 100644 --- a/packages/atoms/booker/Booker.stories.tsx +++ b/packages/atoms/booker/Booker.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Booker } from "./Booker"; +import { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper"; const meta: Meta = { component: Booker, diff --git a/packages/atoms/booker/Booker.tsx b/packages/atoms/booker/Booker.tsx deleted file mode 100644 index 28e050bace..0000000000 --- a/packages/atoms/booker/Booker.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { BookerProps } from "@calcom/features/bookings/Booker"; -import { Booker as BookerComponent } from "@calcom/features/bookings/Booker"; - -import type { AtomsGlobalConfigProps } from "../types"; - -type BookerAtomProps = BookerProps & AtomsGlobalConfigProps; - -/** - * @TODO Before we can turn this into a reusable atom - * * Use the webAppUrl coming from AtomsGlobalConfigProps to make url dynamic - * * Find a solution for translations - */ -export const Booker = (props: BookerAtomProps) => ; diff --git a/packages/atoms/booker/export.ts b/packages/atoms/booker/export.ts index 2bb3a2ac04..3c06badbb9 100644 --- a/packages/atoms/booker/export.ts +++ b/packages/atoms/booker/export.ts @@ -1,5 +1,5 @@ /** Export file is only used for building the dist version of this Atom. */ // import "../globals.css"; -export { Booker } from "./Booker"; +export { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper"; export * from "../types"; diff --git a/packages/atoms/booker/index.ts b/packages/atoms/booker/index.ts index 47fb01d585..8f1e66dfa0 100644 --- a/packages/atoms/booker/index.ts +++ b/packages/atoms/booker/index.ts @@ -1 +1 @@ -export { Booker } from "./Booker"; +export { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper"; diff --git a/packages/atoms/booker/wrappers/BookerWebWrapper.tsx b/packages/atoms/booker/wrappers/BookerWebWrapper.tsx new file mode 100644 index 0000000000..3fffb53cbb --- /dev/null +++ b/packages/atoms/booker/wrappers/BookerWebWrapper.tsx @@ -0,0 +1,195 @@ +import { useSession } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; +import { useMemo, useCallback } from "react"; +import { shallow } from "zustand/shallow"; + +import dayjs from "@calcom/dayjs"; +import type { BookerProps } from "@calcom/features/bookings/Booker"; +import { Booker as BookerComponent } from "@calcom/features/bookings/Booker"; +import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout"; +import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; +import { useBookings } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; +import { useCalendars } from "@calcom/features/bookings/Booker/components/hooks/useCalendars"; +import { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useSlots"; +import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode"; +import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail"; +import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; +import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; +import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; +import { BookerLayouts } from "@calcom/prisma/zod-utils"; + +import type { AtomsGlobalConfigProps } from "../../types"; + +type BookerWebWrapperAtomProps = BookerProps & AtomsGlobalConfigProps; + +export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const event = useEvent(); + const bookerLayout = useBookerLayout(event.data); + + const selectedDate = searchParams?.get("date"); + const isRedirect = searchParams?.get("redirected") === "true" || false; + const fromUserNameRedirected = searchParams?.get("username") || ""; + const rescheduleUid = + typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null; + const bookingUid = + typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null; + const date = dayjs(selectedDate).format("YYYY-MM-DD"); + + useInitializeBookerStore({ + ...props, + eventId: event?.data?.id, + rescheduleUid, + bookingUid: bookingUid, + layout: bookerLayout.defaultLayout, + org: props.entity.orgSlug, + }); + + const [bookerState, _] = useBookerStore((state) => [state.state, state.setState], shallow); + const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow); + const { data: session } = useSession(); + const routerQuery = useRouterQuery(); + const hasSession = !!session; + const firstNameQueryParam = searchParams?.get("firstName"); + const lastNameQueryParam = searchParams?.get("lastName"); + const metadata = Object.keys(routerQuery) + .filter((key) => key.startsWith("metadata")) + .reduce( + (metadata, key) => ({ + ...metadata, + [key.substring("metadata[".length, key.length - 1)]: searchParams?.get(key), + }), + {} + ); + const prefillFormParams = useMemo(() => { + return { + name: + searchParams?.get("name") || + (firstNameQueryParam ? `${firstNameQueryParam} ${lastNameQueryParam}` : null), + guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"), + }; + }, [searchParams, firstNameQueryParam, lastNameQueryParam]); + + const bookerForm = useBookingForm({ + event, + sessionEmail: session?.user.email, + sessionUsername: session?.user.username, + sessionName: session?.user.name, + hasSession, + extraOptions: routerQuery, + prefillFormParams, + }); + const bookings = useBookings({ + event, + hashedLink: props.hashedLink, + bookingForm: bookerForm.bookingForm, + metadata: metadata ?? {}, + }); + const calendars = useCalendars({ hasSession }); + const verifyEmail = useVerifyEmail({ + email: bookerForm.formEmail, + name: bookerForm.formName, + requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification, + onVerifyEmail: bookerForm.beforeVerifyEmail, + }); + const slots = useSlots(event); + + const prefetchNextMonth = + (bookerLayout.layout === BookerLayouts.WEEK_VIEW && + !!bookerLayout.extraDays && + dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) || + (bookerLayout.layout === BookerLayouts.COLUMN_VIEW && + dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()); + + const monthCount = + ((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") || + bookerLayout.layout === BookerLayouts.COLUMN_VIEW) && + dayjs(date).add(1, "month").month() !== + dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month() + ? 2 + : undefined; + /** + * Prioritize dateSchedule load + * Component will render but use data already fetched from here, and no duplicate requests will be made + * */ + const schedule = useScheduleForEvent({ + prefetchNextMonth, + username: props.username, + monthCount, + dayCount, + eventSlug: props.eventSlug, + month: props.month, + duration: props.duration, + selectedDate, + }); + + const verifyCode = useVerifyCode({ + onSuccess: () => { + verifyEmail.setVerifiedEmail(bookerForm.formEmail); + verifyEmail.setEmailVerificationModalVisible(false); + bookings.handleBookEvent(); + }, + }); + + // Toggle query param for overlay calendar + const onOverlaySwitchStateChange = useCallback( + (state: boolean) => { + const current = new URLSearchParams(Array.from(searchParams?.entries() ?? [])); + if (state) { + current.set("overlayCalendar", "true"); + localStorage.setItem("overlayCalendarSwitchDefault", "true"); + } else { + current.delete("overlayCalendar"); + localStorage.removeItem("overlayCalendarSwitchDefault"); + } + // cast to string + const value = current.toString(); + const query = value ? `?${value}` : ""; + router.push(`${pathname}${query}`); + }, + [searchParams, pathname, router] + ); + + return ( + { + if (pathname) window.location.href = pathname; + }} + onConnectNowInstantMeeting={() => { + const newPath = `${pathname}?isInstantMeeting=true`; + router.push(newPath); + }} + onOverlayClickNoCalendar={() => { + router.push("/apps/categories/calendar"); + }} + onClickOverlayContinue={() => { + const currentUrl = new URL(window.location.href); + currentUrl.pathname = "/login/"; + currentUrl.searchParams.set("callbackUrl", window.location.pathname); + currentUrl.searchParams.set("overlayCalendar", "true"); + router.push(currentUrl.toString()); + }} + onOverlaySwitchStateChange={onOverlaySwitchStateChange} + sessionUsername={session?.user.username} + isRedirect={isRedirect} + fromUserNameRedirected={fromUserNameRedirected} + rescheduleUid={rescheduleUid} + bookingUid={bookingUid} + hasSession={hasSession} + extraOptions={routerQuery} + bookings={bookings} + calendars={calendars} + slots={slots} + verifyEmail={verifyEmail} + bookerForm={bookerForm} + event={event} + bookerLayout={bookerLayout} + schedule={schedule} + verifyCode={verifyCode} + /> + ); +}; diff --git a/packages/atoms/index.ts b/packages/atoms/index.ts index fc1b9f7f9d..07163614f2 100644 --- a/packages/atoms/index.ts +++ b/packages/atoms/index.ts @@ -1,2 +1,2 @@ -export { Booker } from "./booker/Booker"; +export { Booker } from "./booker"; export { CalProvider } from "./cal-provider/index"; diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index e08a5e3cde..2acb06150c 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -1,8 +1,5 @@ import { LazyMotion, m, AnimatePresence } from "framer-motion"; -import { useSession } from "next-auth/react"; import dynamic from "next/dynamic"; -import { useSearchParams } from "next/navigation"; -import { usePathname, useRouter } from "next/navigation"; import { useEffect, useRef, useMemo } from "react"; import StickyBox from "react-sticky-box"; import { shallow } from "zustand/shallow"; @@ -29,16 +26,9 @@ import { OverlayCalendar } from "./components/OverlayCalendar/OverlayCalendar"; import { RedirectToInstantMeetingModal } from "./components/RedirectToInstantMeetingModal"; import { BookerSection } from "./components/Section"; import { Away, NotFound } from "./components/Unavailable"; -import { useBookerLayout } from "./components/hooks/useBookerLayout"; -import { useBookingForm } from "./components/hooks/useBookingForm"; -import { useBookings } from "./components/hooks/useBookings"; -import { useCalendars } from "./components/hooks/useCalendars"; -import { useSlots } from "./components/hooks/useSlots"; -import { useVerifyEmail } from "./components/hooks/useVerifyEmail"; import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config"; -import { useBookerStore, useInitializeBookerStore } from "./store"; -import type { BookerProps } from "./types"; -import { useEvent, useScheduleForEvent } from "./utils/event"; +import { useBookerStore } from "./store"; +import type { BookerProps, WrappedBookerProps } from "./types"; import { useBrandColors } from "./utils/use-brand-colors"; const loadFramerFeatures = () => import("./framer-features").then((res) => res.default); @@ -53,28 +43,32 @@ const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => const BookerComponent = ({ username, eventSlug, - month, - bookingData, hideBranding = false, - isTeamEvent, entity, - durationConfig, - duration, - hashedLink, isInstantMeeting = false, -}: BookerProps) => { - const router = useRouter(); - const pathname = usePathname(); + onGoBackInstantMeeting, + onConnectNowInstantMeeting, + onOverlayClickNoCalendar, + onClickOverlayContinue, + onOverlaySwitchStateChange, + sessionUsername, + isRedirect, + fromUserNameRedirected, + rescheduleUid, + hasSession, + extraOptions, + bookings, + verifyEmail, + slots, + calendars, + bookerForm, + event, + bookerLayout, + schedule, + verifyCode, +}: BookerProps & WrappedBookerProps) => { const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); const selectedDate = useBookerStore((state) => state.selectedDate); - const [seatedEventData, setSeatedEventData] = useBookerStore( - (state) => [state.seatedEventData, state.setSeatedEventData], - shallow - ); - const event = useEvent(); - const session = useSession(); - const { selectedTimeslot, setSelectedTimeslot } = useSlots(event); - const { shouldShowFormInDialog, hasDarkBackground, @@ -82,47 +76,19 @@ const BookerComponent = ({ columnViewExtraDays, isMobile, layout, - defaultLayout, hideEventTypeDetails, isEmbed, bookerLayouts, - } = useBookerLayout(event.data); + } = bookerLayout; + + const [seatedEventData, setSeatedEventData] = useBookerStore( + (state) => [state.seatedEventData, state.setSeatedEventData], + shallow + ); + const { selectedTimeslot, setSelectedTimeslot } = slots; const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow); - const date = dayjs(selectedDate).format("YYYY-MM-DD"); - - const prefetchNextMonth = - (layout === BookerLayouts.WEEK_VIEW && - !!extraDays && - dayjs(date).month() !== dayjs(date).add(extraDays, "day").month()) || - (layout === BookerLayouts.COLUMN_VIEW && - dayjs(date).month() !== dayjs(date).add(columnViewExtraDays.current, "day").month()); - - const monthCount = - ((layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") || - layout === BookerLayouts.COLUMN_VIEW) && - dayjs(date).add(1, "month").month() !== dayjs(date).add(columnViewExtraDays.current, "day").month() - ? 2 - : undefined; - - const searchParams = useSearchParams(); - - /** - * Prioritize dateSchedule load - * Component will render but use data already fetched from here, and no duplicate requests will be made - * */ - const schedule = useScheduleForEvent({ - prefetchNextMonth, - username, - monthCount, - dayCount, - eventSlug, - month, - duration, - selectedDate: searchParams?.get("date"), - }); - const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter( (slot) => dayjs(selectedDate).diff(slot, "day") <= 0 ); @@ -146,67 +112,24 @@ const BookerComponent = ({ const timeslotsRef = useRef(null); const StickyOnDesktop = isMobile ? "div" : StickyBox; - const rescheduleUid = - typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null; - const bookingUid = - typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null; const { t } = useLocale(); - const isRedirect = searchParams?.get("redirected") === "true" || false; - const fromUserNameRedirected = searchParams?.get("username") || ""; - useBrandColors({ brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR, darkBrandColor: event.data?.profile.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR, theme: event.data?.profile.theme, }); - useInitializeBookerStore({ - username, - eventSlug, - month, - eventId: event?.data?.id, - rescheduleUid, - bookingUid, - bookingData, - layout: defaultLayout, - isTeamEvent, - org: entity.orgSlug, - durationConfig, - isInstantMeeting, - }); - - const { - bookerFormErrorRef, - key, - formEmail, - formName, - bookingForm, - beforeVerifyEmail, - errors: formErrors, - } = useBookingForm({ - event, - }); - - const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates, expiryTime } = useBookings({ - event, - hashedLink, - bookingForm, - }); + const { bookerFormErrorRef, key, formEmail, bookingForm, errors: formErrors } = bookerForm; + const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates, expiryTime } = bookings; const { isEmailVerificationModalVisible, setEmailVerificationModalVisible, handleVerifyEmail, - setVerifiedEmail, renderConfirmNotVerifyEmailButtonCond, - } = useVerifyEmail({ - email: formEmail, - name: formName, - requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification, - onVerifyEmail: beforeVerifyEmail, - }); + } = verifyEmail; const { overlayBusyDates, @@ -214,7 +137,7 @@ const BookerComponent = ({ connectedCalendars, loadingConnectedCalendar, onToggleCalendar, - } = useCalendars(); + } = calendars; useEffect(() => { if (event.isPending) return setBookerState("loading"); @@ -223,76 +146,81 @@ const BookerComponent = ({ return setBookerState("booking"); }, [event, selectedDate, selectedTimeslot, setBookerState]); - const EventBooker = useMemo( - () => - bookerState === "booking" ? ( - { - setSelectedTimeslot(null); - if (seatedEventData.bookingUid) { - setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined }); - } - }} - onSubmit={renderConfirmNotVerifyEmailButtonCond ? handleBookEvent : handleVerifyEmail} - errorRef={bookerFormErrorRef} - errors={{ ...formErrors, ...errors }} - loadingStates={loadingStates} - renderConfirmNotVerifyEmailButtonCond={renderConfirmNotVerifyEmailButtonCond} - bookingForm={bookingForm} - eventQuery={event} - rescheduleUid={rescheduleUid}> - <> - { - setVerifiedEmail(formEmail); - setEmailVerificationModalVisible(false); - handleBookEvent(); - }} - isUserSessionRequiredToVerify={false} - /> - { - // Prevent null on app directory - if (pathname) window.location.href = pathname; - }} - /> - > - - ) : ( - <>> - ), - [ - bookerFormErrorRef, - bookerState, - bookingForm, - errors, - event, - formEmail, - formErrors, - handleBookEvent, - handleVerifyEmail, - hasInstantMeetingTokenExpired, - isEmailVerificationModalVisible, - key, - loadingStates, - pathname, - renderConfirmNotVerifyEmailButtonCond, - rescheduleUid, - seatedEventData, - setEmailVerificationModalVisible, - setSeatedEventData, - setSelectedTimeslot, - setVerifiedEmail, - expiryTime, - ] - ); + const EventBooker = useMemo(() => { + return bookerState === "booking" ? ( + { + setSelectedTimeslot(null); + if (seatedEventData.bookingUid) { + setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined }); + } + }} + onSubmit={renderConfirmNotVerifyEmailButtonCond ? handleBookEvent : handleVerifyEmail} + errorRef={bookerFormErrorRef} + errors={{ ...formErrors, ...errors }} + loadingStates={loadingStates} + renderConfirmNotVerifyEmailButtonCond={renderConfirmNotVerifyEmailButtonCond} + bookingForm={bookingForm} + eventQuery={event} + extraOptions={extraOptions} + rescheduleUid={rescheduleUid}> + <> + + { + onGoBackInstantMeeting(); + }} + /> + > + + ) : ( + <>> + ); + }, [ + bookerFormErrorRef, + bookerState, + bookingForm, + errors, + event, + expiryTime, + extraOptions, + formEmail, + formErrors, + handleBookEvent, + handleVerifyEmail, + hasInstantMeetingTokenExpired, + isEmailVerificationModalVisible, + key, + loadingStates, + onGoBackInstantMeeting, + renderConfirmNotVerifyEmailButtonCond, + rescheduleUid, + seatedEventData, + setEmailVerificationModalVisible, + setSeatedEventData, + setSelectedTimeslot, + verifyCode.error, + verifyCode.isPending, + verifyCode.resetErrors, + verifyCode.setIsPending, + verifyCode.verifyCodeWithSessionNotRequired, + verifyCode.verifyCodeWithSessionRequired, + ]); if (entity.isUnpublished) { return ; @@ -317,8 +245,7 @@ const BookerComponent = ({ { - const newPath = `${pathname}?isInstantMeeting=true`; - router.push(newPath); + onConnectNowInstantMeeting(); }} /> @@ -372,7 +299,7 @@ const BookerComponent = ({ "bg-default dark:bg-muted sticky top-0 z-10" )}> { - router.push("/apps/categories/calendar"); + onOverlayClickNoCalendar(); }} /> > @@ -444,7 +374,12 @@ const BookerComponent = ({ visible={layout === BookerLayouts.WEEK_VIEW} className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l" {...fadeInLeft}> - + { +export const Booker = (props: BookerProps & WrappedBookerProps) => { if (props.isAway) return ; return ( diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index d6f351168a..5eef9b990e 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -7,13 +7,12 @@ import type { FieldError } from "react-hook-form"; import { WEBSITE_URL } from "@calcom/lib/constants"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { Alert, Button, EmptyScreen, Form } from "@calcom/ui"; import { Calendar } from "@calcom/ui/components/icon"; import { useBookerStore } from "../../store"; import type { useEventReturnType } from "../../utils/event"; -import type { useBookingFormReturnType } from "../hooks/useBookingForm"; +import type { UseBookingFormReturnType } from "../hooks/useBookingForm"; import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBookings"; import { BookingFields } from "./BookingFields"; import { FormSkeleton } from "./Skeleton"; @@ -22,11 +21,12 @@ type BookEventFormProps = { onCancel?: () => void; onSubmit: () => void; errorRef: React.RefObject; - errors: useBookingFormReturnType["errors"] & IUseBookingErrors; + errors: UseBookingFormReturnType["errors"] & IUseBookingErrors; loadingStates: IUseBookingLoadingStates; children?: React.ReactNode; - bookingForm: useBookingFormReturnType["bookingForm"]; + bookingForm: UseBookingFormReturnType["bookingForm"]; renderConfirmNotVerifyEmailButtonCond: boolean; + extraOptions: Record; }; export const BookEventForm = ({ @@ -40,12 +40,12 @@ export const BookEventForm = ({ renderConfirmNotVerifyEmailButtonCond, bookingForm, children, + extraOptions, }: Omit & { eventQuery: useEventReturnType; rescheduleUid: string | null; }) => { const eventType = eventQuery.data; - const routerQuery = useRouterQuery(); const setFormValues = useBookerStore((state) => state.setFormValues); const bookingData = useBookerStore((state) => state.bookingData); const timeslot = useBookerStore((state) => state.selectedTimeslot); @@ -76,7 +76,7 @@ export const BookEventForm = ({ ); if (!eventType) { - console.warn("No event type found for event", routerQuery); + console.warn("No event type found for event", extraOptions); return ; } diff --git a/packages/features/bookings/Booker/components/InstantBooking.tsx b/packages/features/bookings/Booker/components/InstantBooking.tsx index 3677993dcf..11db897e5a 100644 --- a/packages/features/bookings/Booker/components/InstantBooking.tsx +++ b/packages/features/bookings/Booker/components/InstantBooking.tsx @@ -1,11 +1,11 @@ +import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { SchedulingType } from "@calcom/prisma/enums"; -import { type RouterOutputs } from "@calcom/trpc"; import { Button, UserAvatarGroupWithOrg } from "@calcom/ui"; interface IInstantBookingProps { onConnectNow: () => void; - event: NonNullable; + event: NonNullable; } export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => { diff --git a/packages/features/bookings/Booker/components/LargeCalendar.tsx b/packages/features/bookings/Booker/components/LargeCalendar.tsx index 6702b27102..66183d13e3 100644 --- a/packages/features/bookings/Booker/components/LargeCalendar.tsx +++ b/packages/features/bookings/Booker/components/LargeCalendar.tsx @@ -7,8 +7,7 @@ import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/week import { localStorage } from "@calcom/lib/webstorage"; import { useBookerStore } from "../store"; -import type { useScheduleForEventReturnType } from "../utils/event"; -import { useEvent } from "../utils/event"; +import type { useEventReturnType, useScheduleForEventReturnType } from "../utils/event"; import { getQueryParam } from "../utils/query-param"; import { useOverlayCalendarStore } from "./OverlayCalendar/store"; @@ -16,10 +15,12 @@ export const LargeCalendar = ({ extraDays, schedule, isLoading, + event, }: { extraDays: number; schedule?: useScheduleForEventReturnType["data"]; isLoading: boolean; + event: useEventReturnType; }) => { const selectedDate = useBookerStore((state) => state.selectedDate); const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); @@ -28,7 +29,6 @@ export const LargeCalendar = ({ const displayOverlay = getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); - const event = useEvent(); const eventDuration = selectedEventDuration || event?.data?.length || 30; const availableSlots = useMemo(() => { diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendar.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendar.tsx index f67323315f..ac7ab7b632 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendar.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendar.tsx @@ -11,7 +11,12 @@ type OverlayCalendarProps = Pick< | "onToggleCalendar" | "loadingConnectedCalendar" | "isOverlayCalendarEnabled" -> & { handleClickNoCalendar: () => void }; +> & { + handleClickNoCalendar: () => void; + hasSession: boolean; + handleClickContinue: () => void; + handleSwitchStateChange: (state: boolean) => void; +}; export const OverlayCalendar = ({ connectedCalendars, @@ -20,6 +25,9 @@ export const OverlayCalendar = ({ isOverlayCalendarEnabled, loadingConnectedCalendar, handleClickNoCalendar, + handleSwitchStateChange, + handleClickContinue, + hasSession, }: OverlayCalendarProps) => { const { handleCloseContinueModal, @@ -31,8 +39,16 @@ export const OverlayCalendar = ({ } = useOverlayCalendar({ connectedCalendars, overlayBusyDates, onToggleCalendar }); return ( <> - - + + void; + onContinue: () => void; } export function OverlayCalendarContinueModal(props: IOverlayCalendarContinueModalProps) { - const router = useRouter(); const { t } = useLocale(); return ( <> @@ -24,12 +23,7 @@ export function OverlayCalendarContinueModal(props: IOverlayCalendarContinueModa { - const currentUrl = new URL(window.location.href); - currentUrl.pathname = "/login/"; - currentUrl.searchParams.set("callbackUrl", window.location.pathname); - currentUrl.searchParams.set("overlayCalendar", "true"); - - router.push(currentUrl.toString()); + props.onContinue(); }} className="gap w-full items-center justify-center font-semibold" StartIcon={CalendarSearch}> diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx index b9d5ef81b9..a795908449 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx @@ -1,10 +1,7 @@ -import { useSession } from "next-auth/react"; -import { useSearchParams, useRouter, usePathname } from "next/navigation"; -import { useCallback, useEffect } from "react"; +import { useEffect } from "react"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { localStorage } from "@calcom/lib/webstorage"; import { Button, Switch } from "@calcom/ui"; import { Settings } from "@calcom/ui/components/icon"; @@ -13,50 +10,29 @@ import { useOverlayCalendarStore } from "./store"; interface OverlayCalendarSwitchProps { enabled?: boolean; + hasSession: boolean; + onStateChange: (state: boolean) => void; } -export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) { +export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: OverlayCalendarSwitchProps) { const { t } = useLocale(); const setContinueWithProvider = useOverlayCalendarStore((state) => state.setContinueWithProviderModal); const setCalendarSettingsOverlay = useOverlayCalendarStore( (state) => state.setCalendarSettingsOverlayModal ); const layout = useBookerStore((state) => state.layout); - const searchParams = useSearchParams(); - const { data: session } = useSession(); - const router = useRouter(); - const pathname = usePathname(); const switchEnabled = enabled; - // Toggle query param for overlay calendar - const toggleOverlayCalendarQueryParam = useCallback( - (state: boolean) => { - const current = new URLSearchParams(Array.from(searchParams?.entries() ?? [])); - if (state) { - current.set("overlayCalendar", "true"); - localStorage.setItem("overlayCalendarSwitchDefault", "true"); - } else { - current.delete("overlayCalendar"); - localStorage.removeItem("overlayCalendarSwitchDefault"); - } - // cast to string - const value = current.toString(); - const query = value ? `?${value}` : ""; - router.push(`${pathname}${query}`); - }, - [searchParams, pathname, router] - ); - /** * If a user is not logged in and the overlay calendar query param is true, * show the continue modal so they can login / create an account */ useEffect(() => { - if (!session && switchEnabled) { - toggleOverlayCalendarQueryParam(false); + if (!hasSession && switchEnabled) { + onStateChange(false); setContinueWithProvider(true); } - }, [session, switchEnabled, setContinueWithProvider, toggleOverlayCalendarQueryParam]); + }, [hasSession, switchEnabled, setContinueWithProvider, onStateChange]); return ( { - if (!session) { + if (!hasSession) { setContinueWithProvider(state); } else { - toggleOverlayCalendarQueryParam(state); + onStateChange(state); } }} /> @@ -83,7 +59,7 @@ export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) { {t("overlay_my_calendar")} - {session && ( + {hasSession && ( ; + export const useBookerLayout = (event: useEventReturnType["data"]) => { const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const isEmbed = useIsEmbed(); diff --git a/packages/features/bookings/Booker/components/hooks/useBookingForm.ts b/packages/features/bookings/Booker/components/hooks/useBookingForm.ts index e3cf47eec0..0b05983038 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookingForm.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookingForm.ts @@ -13,11 +13,28 @@ import { useInitialFormValues } from "./useInitialFormValues"; export interface IUseBookingForm { event: useEventReturnType; + sessionEmail?: string | null; + sessionName?: string | null; + sessionUsername?: string | null; + hasSession: boolean; + extraOptions: Record; + prefillFormParams: { + guests: string[]; + name: string | null; + }; } -export type useBookingFormReturnType = ReturnType; +export type UseBookingFormReturnType = ReturnType; -export const useBookingForm = ({ event }: IUseBookingForm) => { +export const useBookingForm = ({ + event, + sessionEmail, + sessionName, + sessionUsername, + hasSession, + extraOptions, + prefillFormParams, +}: IUseBookingForm) => { const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const bookingData = useBookerStore((state) => state.bookingData); const { t } = useLocale(); @@ -48,6 +65,12 @@ export const useBookingForm = ({ event }: IUseBookingForm) => { eventType: event.data, rescheduleUid, isRescheduling, + email: sessionEmail, + name: sessionName, + username: sessionUsername, + hasSession, + extraOptions, + prefillFormParams, }); const bookingForm = useForm({ diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index a3c6dab869..29410e9a02 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useRef, useState, useEffect } from "react"; import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client"; @@ -18,17 +18,17 @@ import { import { getFullName } from "@calcom/features/form-builder/utils"; import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc"; import { showToast } from "@calcom/ui"; -import type { useBookingFormReturnType } from "./useBookingForm"; +import type { UseBookingFormReturnType } from "./useBookingForm"; export interface IUseBookings { event: useEventReturnType; hashedLink?: string | null; - bookingForm: useBookingFormReturnType["bookingForm"]; + bookingForm: UseBookingFormReturnType["bookingForm"]; + metadata: Record; } export interface IUseBookingLoadingStates { @@ -41,8 +41,9 @@ export interface IUseBookingErrors { hasDataErrors: boolean; dataErrors: unknown; } +export type UseBookingsReturnType = ReturnType; -export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) => { +export const useBookings = ({ event, hashedLink, bookingForm, metadata }: IUseBookings) => { const router = useRouter(); const eventSlug = useBookerStore((state) => state.eventSlug); const setFormValues = useBookerStore((state) => state.setFormValues); @@ -59,8 +60,6 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) => const duration = useBookerStore((state) => state.selectedDuration); const { timezone } = useTimePreferences(); const username = useBookerStore((state) => state.username); - const routerQuery = useRouterQuery(); - const searchParams = useSearchParams(); const isRescheduling = !!rescheduleUid && !!bookingData; @@ -220,15 +219,7 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) => rescheduleUid: rescheduleUid || undefined, bookingUid: (bookingData && bookingData.uid) || seatedEventData?.bookingUid || undefined, username: username || "", - metadata: Object.keys(routerQuery) - .filter((key) => key.startsWith("metadata")) - .reduce( - (metadata, key) => ({ - ...metadata, - [key.substring("metadata[".length, key.length - 1)]: searchParams?.get(key), - }), - {} - ), + metadata: metadata, hashedLink, }; diff --git a/packages/features/bookings/Booker/components/hooks/useCalendars.ts b/packages/features/bookings/Booker/components/hooks/useCalendars.ts index 3da1b89066..b6f90c2c1a 100644 --- a/packages/features/bookings/Booker/components/hooks/useCalendars.ts +++ b/packages/features/bookings/Booker/components/hooks/useCalendars.ts @@ -1,4 +1,3 @@ -import { useSession } from "next-auth/react"; import { useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { shallow } from "zustand/shallow"; @@ -12,7 +11,10 @@ import { useOverlayCalendarStore } from "../OverlayCalendar/store"; import { useLocalSet } from "./useLocalSet"; export type UseCalendarsReturnType = ReturnType; -export const useCalendars = () => { +type UseCalendarsProps = { + hasSession: boolean; +}; +export const useCalendars = ({ hasSession }: UseCalendarsProps) => { const searchParams = useSearchParams(); const selectedDate = useBookerStore((state) => state.selectedDate); const { timezone } = useTimePreferences(); @@ -24,7 +26,6 @@ export const useCalendars = () => { externalId: string; }>("toggledConnectedCalendars", []); const utils = trpc.useContext(); - const { data: session } = useSession(); const [calendarSettingsOverlay] = useOverlayCalendarStore( (state) => [state.calendarSettingsOverlayModal, state.setCalendarSettingsOverlayModal], @@ -42,7 +43,7 @@ export const useCalendars = () => { })), }, { - enabled: !!session && set.size > 0 && switchEnabled, + enabled: hasSession && set.size > 0 && switchEnabled, } ); diff --git a/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts b/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts index 954305b38e..eb4123010c 100644 --- a/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts +++ b/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts @@ -1,5 +1,3 @@ -import { useSession } from "next-auth/react"; -import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import type { z } from "zod"; @@ -7,25 +5,38 @@ import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import type { useEvent } from "@calcom/features/bookings/Booker/utils/event"; import type getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; -import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; export type useInitialFormValuesReturnType = ReturnType; +type UseInitialFormValuesProps = { + eventType: ReturnType["data"]; + rescheduleUid: string | null; + isRescheduling: boolean; + email?: string | null; + name?: string | null; + username?: string | null; + hasSession: boolean; + extraOptions: Record; + prefillFormParams: { + guests: string[]; + name: string | null; + }; +}; + export function useInitialFormValues({ eventType, rescheduleUid, isRescheduling, -}: { - eventType: ReturnType["data"]; - rescheduleUid: string | null; - isRescheduling: boolean; -}) { + email, + name, + username, + hasSession, + extraOptions, + prefillFormParams, +}: UseInitialFormValuesProps) { const [initialValues, setDefaultValues] = useState>({}); const bookingData = useBookerStore((state) => state.bookingData); const formValues = useBookerStore((state) => state.formValues); - const searchParams = useSearchParams(); - const routerQuery = useRouterQuery(); - const session = useSession(); useEffect(() => { (async function () { if (Object.keys(formValues).length) { @@ -41,19 +52,12 @@ export function useInitialFormValues({ view: rescheduleUid ? "reschedule" : "booking", }); - // Routing Forms don't support Split full name(because no Form Builder in there), so user needs to create two fields in there themselves. If they name these fields, `firstName` and `lastName`, we can prefill the Booking Form with them - // Once we support formBuilder in Routing Forms, we should be able to forward JSON form of name field value to Booking Form and prefill it there without having these two query params separately. - const firstNameQueryParam = searchParams?.get("firstName"); - const lastNameQueryParam = searchParams?.get("lastName"); - const parsedQuery = await querySchema.parseAsync({ - ...routerQuery, - name: - searchParams?.get("name") || - (firstNameQueryParam ? `${firstNameQueryParam} ${lastNameQueryParam}` : null), + ...extraOptions, + name: prefillFormParams.name, // `guest` because we need to support legacy URL with `guest` query param support // `guests` because the `name` of the corresponding bookingField is `guests` - guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"), + guests: prefillFormParams.guests, }); const defaultUserValues = { @@ -62,13 +66,13 @@ export function useInitialFormValues({ ? bookingData?.attendees[0].email : !!parsedQuery["email"] ? parsedQuery["email"] - : session.data?.user?.email ?? "", + : email ?? "", name: rescheduleUid && bookingData && bookingData.attendees.length > 0 ? bookingData?.attendees[0].name : !!parsedQuery["name"] ? parsedQuery["name"] - : session.data?.user?.name ?? session.data?.user?.username ?? "", + : name ?? username ?? "", }; if (!isRescheduling) { @@ -115,7 +119,7 @@ export function useInitialFormValues({ }; setDefaultValues(defaults); })(); - // do not add routerQuery as a dependency, it will cause infinite loop + // do not add extraOptions as a dependency, it will cause infinite loop // eslint-disable-next-line react-hooks/exhaustive-deps }, [ eventType?.bookingFields, @@ -123,14 +127,14 @@ export function useInitialFormValues({ isRescheduling, bookingData, rescheduleUid, - searchParams, - session.data?.user?.email, - session.data?.user?.name, - session.data?.user?.username, + email, + name, + username, + prefillFormParams, ]); // When initialValues is available(after doing async schema parsing) or session is available(so that we can prefill logged-in user email and name), we need to reset the form with the initialValues - const key = `${Object.keys(initialValues).length}_${session ? 1 : 0}`; + const key = `${Object.keys(initialValues).length}_${hasSession ? 1 : 0}`; return { initialValues, key }; } diff --git a/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts b/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts index 5208b2fdd5..20043691d6 100644 --- a/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts +++ b/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts @@ -8,6 +8,8 @@ import { useOverlayCalendarStore } from "../OverlayCalendar/store"; import type { UseCalendarsReturnType } from "./useCalendars"; import { useLocalSet } from "./useLocalSet"; +export type UseOverlayCalendarReturnType = ReturnType; + export const useOverlayCalendar = ({ connectedCalendars, overlayBusyDates, diff --git a/packages/features/bookings/Booker/components/hooks/useSlots.ts b/packages/features/bookings/Booker/components/hooks/useSlots.ts index d66cda272f..f578ae9c1a 100644 --- a/packages/features/bookings/Booker/components/hooks/useSlots.ts +++ b/packages/features/bookings/Booker/components/hooks/useSlots.ts @@ -8,6 +8,8 @@ import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/ import { MINUTES_TO_BOOK } from "@calcom/lib/constants"; import { trpc } from "@calcom/trpc"; +export type UseSlotsReturnType = ReturnType; + export const useSlots = (event: useEventReturnType) => { const selectedDuration = useBookerStore((state) => state.selectedDuration); const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( diff --git a/packages/features/bookings/Booker/components/hooks/useVerifyCode.ts b/packages/features/bookings/Booker/components/hooks/useVerifyCode.ts new file mode 100644 index 0000000000..3fb7d37138 --- /dev/null +++ b/packages/features/bookings/Booker/components/hooks/useVerifyCode.ts @@ -0,0 +1,74 @@ +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; + +export type UseVerifyCodeReturnType = ReturnType; + +type UseVerifyCodeProps = { + onSuccess: (isVerified: boolean) => void; +}; + +export const useVerifyCode = ({ onSuccess }: UseVerifyCodeProps) => { + const { t } = useLocale(); + + const [isPending, setIsPending] = useState(false); + const [error, setError] = useState(""); + const [value, setValue] = useState(""); + const [hasVerified, setHasVerified] = useState(false); + + const verifyCodeMutationUserSessionRequired = trpc.viewer.organizations.verifyCode.useMutation({ + onSuccess: (data) => { + setIsPending(false); + onSuccess(data); + }, + onError: (err) => { + setIsPending(false); + setHasVerified(false); + if (err.message === "invalid_code") { + setError(t("code_provided_invalid")); + } + }, + }); + + const verifyCodeMutationUserSessionNotRequired = trpc.viewer.auth.verifyCodeUnAuthenticated.useMutation({ + onSuccess: (data) => { + setIsPending(false); + onSuccess(data); + }, + onError: (err) => { + setIsPending(false); + setHasVerified(false); + if (err.message === "invalid_code") { + setError(t("code_provided_invalid")); + } + }, + }); + + const verifyCodeWithSessionRequired = (code: string, email: string) => { + verifyCodeMutationUserSessionRequired.mutate({ + code, + email, + }); + }; + + const verifyCodeWithSessionNotRequired = (code: string, email: string) => { + verifyCodeMutationUserSessionNotRequired.mutate({ + code, + email, + }); + }; + + return { + verifyCodeWithSessionRequired, + verifyCodeWithSessionNotRequired, + isPending, + setIsPending, + error, + value, + hasVerified, + setValue, + setHasVerified, + resetErrors: () => setError(""), + }; +}; diff --git a/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts b/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts index fd37048468..81bb7ece31 100644 --- a/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts +++ b/packages/features/bookings/Booker/components/hooks/useVerifyEmail.ts @@ -11,7 +11,7 @@ export interface IUseVerifyEmailProps { name?: string | { firstName: string; lastname?: string }; requiresBookerEmailVerification?: boolean; } - +export type UseVerifyEmailReturnType = ReturnType; export const useVerifyEmail = ({ email, name, diff --git a/packages/features/bookings/Booker/types.ts b/packages/features/bookings/Booker/types.ts index cde630fb1c..7644c1c347 100644 --- a/packages/features/bookings/Booker/types.ts +++ b/packages/features/bookings/Booker/types.ts @@ -1,3 +1,14 @@ +import type { UseBookerLayoutType } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout"; +import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; +import type { UseBookingsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; +import type { UseCalendarsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useCalendars"; +import type { UseSlotsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useSlots"; +import type { UseVerifyCodeReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode"; +import type { UseVerifyEmailReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail"; +import type { + useEventReturnType, + useScheduleForEventReturnType, +} from "@calcom/features/bookings/Booker/utils/event"; import type { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { GetBookingType } from "../lib/get-booking"; @@ -75,6 +86,30 @@ export interface BookerProps { isInstantMeeting?: boolean; } +export type WrappedBookerProps = { + sessionUsername?: string | null; + rescheduleUid: string | null; + bookingUid: string | null; + isRedirect: boolean; + fromUserNameRedirected: string; + hasSession: boolean; + onGoBackInstantMeeting: () => void; + onConnectNowInstantMeeting: () => void; + onOverlayClickNoCalendar: () => void; + onClickOverlayContinue: () => void; + onOverlaySwitchStateChange: (state: boolean) => void; + extraOptions: Record; + bookings: UseBookingsReturnType; + slots: UseSlotsReturnType; + calendars: UseCalendarsReturnType; + verifyEmail: UseVerifyEmailReturnType; + bookerForm: UseBookingFormReturnType; + event: useEventReturnType; + schedule: useScheduleForEventReturnType; + bookerLayout: UseBookerLayoutType; + verifyCode: UseVerifyCodeReturnType; +}; + export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking"; export type BookerLayout = BookerLayouts | "mobile"; export type BookerAreas = "calendar" | "timeslots" | "main" | "meta" | "header"; diff --git a/packages/features/bookings/components/VerifyCodeDialog.tsx b/packages/features/bookings/components/VerifyCodeDialog.tsx index 9322c95b85..3036b62d23 100644 --- a/packages/features/bookings/components/VerifyCodeDialog.tsx +++ b/packages/features/bookings/components/VerifyCodeDialog.tsx @@ -3,7 +3,6 @@ import { useState, useEffect, useCallback } from "react"; import useDigitInput from "react-digit-input"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc/react"; import { Button, Dialog, @@ -20,19 +19,26 @@ export const VerifyCodeDialog = ({ isOpenDialog, setIsOpenDialog, email, - onSuccess, isUserSessionRequiredToVerify = true, + verifyCodeWithSessionNotRequired, + verifyCodeWithSessionRequired, + resetErrors, + setIsPending, + isPending, + error, }: { isOpenDialog: boolean; setIsOpenDialog: Dispatch>; email: string; - onSuccess: (isVerified: boolean) => void; isUserSessionRequiredToVerify?: boolean; + verifyCodeWithSessionNotRequired: (code: string, email: string) => void; + verifyCodeWithSessionRequired: (code: string, email: string) => void; + resetErrors: () => void; + isPending: boolean; + setIsPending: (status: boolean) => void; + error: string; }) => { const { t } = useLocale(); - // Not using the mutation isPending flag because after verifying we submit the underlying org creation form - const [isPending, setIsPending] = useState(false); - const [error, setError] = useState(""); const [value, setValue] = useState(""); const [hasVerified, setHasVerified] = useState(false); @@ -42,60 +48,28 @@ export const VerifyCodeDialog = ({ value, onChange: useCallback((value: string) => { // whenever there's a change in the input, we reset the error value. - setError(""); + resetErrors(); setValue(value); }, []), }); - const verifyCodeMutationUserSessionRequired = trpc.viewer.organizations.verifyCode.useMutation({ - onSuccess: (data) => { - setIsPending(false); - onSuccess(data); - }, - onError: (err) => { - setIsPending(false); - setHasVerified(false); - if (err.message === "invalid_code") { - setError(t("code_provided_invalid")); - } - }, - }); - - const verifyCodeMutationUserSessionNotRequired = trpc.viewer.auth.verifyCodeUnAuthenticated.useMutation({ - onSuccess: (data) => { - setIsPending(false); - onSuccess(data); - }, - onError: (err) => { - setIsPending(false); - setHasVerified(false); - if (err.message === "invalid_code") { - setError(t("code_provided_invalid")); - } - }, - }); - const verifyCode = useCallback(() => { - setError(""); + resetErrors(); setIsPending(true); if (isUserSessionRequiredToVerify) { - verifyCodeMutationUserSessionRequired.mutate({ - code: value, - email, - }); + verifyCodeWithSessionRequired(value, email); } else { - verifyCodeMutationUserSessionNotRequired.mutate({ - code: value, - email, - }); + verifyCodeWithSessionNotRequired(value, email); } setHasVerified(true); }, [ - email, + resetErrors, + setIsPending, isUserSessionRequiredToVerify, + verifyCodeWithSessionRequired, value, - verifyCodeMutationUserSessionNotRequired, - verifyCodeMutationUserSessionRequired, + email, + verifyCodeWithSessionNotRequired, ]); useEffect(() => { @@ -116,7 +90,7 @@ export const VerifyCodeDialog = ({ open={isOpenDialog} onOpenChange={(open) => { setValue(""); - setError(""); + resetErrors(); setIsOpenDialog(open); }}> diff --git a/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx b/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx index 5618e14db1..cde15cce2e 100644 --- a/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx +++ b/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; +import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode"; import { VerifyCodeDialog } from "@calcom/features/bookings/components/VerifyCodeDialog"; import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -74,6 +75,18 @@ export const CreateANewOrganizationForm = ({ slug }: { slug?: string }) => { }, }); + const verifyCode = useVerifyCode({ + onSuccess: (isVerified) => { + if (isVerified) { + createOrganizationMutation.mutate({ + ...newOrganizationFormMethods.getValues(), + language: i18n.language, + check: false, + }); + } + }, + }); + return ( <> { isOpenDialog={showVerifyCode} setIsOpenDialog={setShowVerifyCode} email={watchAdminEmail} - onSuccess={(isVerified) => { - if (isVerified) { - createOrganizationMutation.mutate({ - ...newOrganizationFormMethods.getValues(), - language: i18n.language, - check: false, - }); - } - }} + verifyCodeWithSessionNotRequired={verifyCode.verifyCodeWithSessionNotRequired} + verifyCodeWithSessionRequired={verifyCode.verifyCodeWithSessionRequired} + error={verifyCode.error} + resetErrors={verifyCode.resetErrors} + isPending={verifyCode.isPending} + setIsPending={verifyCode.setIsPending} /> > );