chore: booker web wrapper (#13676)

* chore: booker web wrapper

* fixup! chore: booker web wrapper

* fixup! fixup! chore: booker web wrapper

* fixup! fixup! fixup! chore: booker web wrapper

* fixup! fixup! fixup! fixup! chore: booker web wrapper

* fix: CreateANewOrganizationForm verify dialog

* fix: EventBooker not rerendering when it should

* fix: init booker store properly

* fixup! fix: init booker store properly

* fix: useMemo EventBooker

* fix: remove unnecessary file
This commit is contained in:
Morgan authored and GitHub committed 2024-02-16 11:38:12 +00:00
1 parent 3c9e9329b0
commit 3dd312b88b
26 files changed
+591 -370

No files matched your search

+1 -1
View File
@@ -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";
<Meta of={BookerStories} />
+1 -1
View File
@@ -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<typeof Booker> = {
component: Booker,
-13
View File
@@ -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) => <BookerComponent {...props} />;
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -1 +1 @@
export { Booker } from "./Booker";
export { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper";
@@ -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 (
<BookerComponent
{...props}
onGoBackInstantMeeting={() => {
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}
/>
);
};
+1 -1
View File
@@ -1,2 +1,2 @@
export { Booker } from "./booker/Booker";
export { Booker } from "./booker";
export { CalProvider } from "./cal-provider/index";
+122 -187
View File
@@ -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<HTMLDivElement>(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" ? (
<BookEventForm
key={key}
onCancel={() => {
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}>
<>
<VerifyCodeDialog
isOpenDialog={isEmailVerificationModalVisible}
setIsOpenDialog={setEmailVerificationModalVisible}
email={formEmail}
onSuccess={() => {
setVerifiedEmail(formEmail);
setEmailVerificationModalVisible(false);
handleBookEvent();
}}
isUserSessionRequiredToVerify={false}
/>
<RedirectToInstantMeetingModal
expiryTime={expiryTime}
hasInstantMeetingTokenExpired={hasInstantMeetingTokenExpired}
bookingId={parseInt(getQueryParam("bookingId") || "0")}
onGoBack={() => {
// Prevent null on app directory
if (pathname) window.location.href = pathname;
}}
/>
</>
</BookEventForm>
) : (
<></>
),
[
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" ? (
<BookEventForm
key={key}
onCancel={() => {
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}>
<>
<VerifyCodeDialog
isOpenDialog={isEmailVerificationModalVisible}
setIsOpenDialog={setEmailVerificationModalVisible}
email={formEmail}
isUserSessionRequiredToVerify={false}
verifyCodeWithSessionNotRequired={verifyCode.verifyCodeWithSessionNotRequired}
verifyCodeWithSessionRequired={verifyCode.verifyCodeWithSessionRequired}
error={verifyCode.error}
resetErrors={verifyCode.resetErrors}
isPending={verifyCode.isPending}
setIsPending={verifyCode.setIsPending}
/>
<RedirectToInstantMeetingModal
expiryTime={expiryTime}
hasInstantMeetingTokenExpired={hasInstantMeetingTokenExpired}
bookingId={parseInt(getQueryParam("bookingId") || "0")}
onGoBack={() => {
onGoBackInstantMeeting();
}}
/>
</>
</BookEventForm>
) : (
<></>
);
}, [
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 <UnpublishedEntity {...entity} />;
@@ -317,8 +245,7 @@ const BookerComponent = ({
<InstantBooking
event={event.data}
onConnectNow={() => {
const newPath = `${pathname}?isInstantMeeting=true`;
router.push(newPath);
onConnectNowInstantMeeting();
}}
/>
</div>
@@ -372,7 +299,7 @@ const BookerComponent = ({
"bg-default dark:bg-muted sticky top-0 z-10"
)}>
<Header
isMyLink={Boolean(username === session?.data?.user.username)}
isMyLink={Boolean(username === sessionUsername)}
eventSlug={eventSlug}
enabledLayouts={bookerLayouts.enabledLayouts}
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
@@ -389,8 +316,11 @@ const BookerComponent = ({
loadingConnectedCalendar={loadingConnectedCalendar}
overlayBusyDates={overlayBusyDates}
onToggleCalendar={onToggleCalendar}
hasSession={hasSession}
handleClickContinue={onClickOverlayContinue}
handleSwitchStateChange={onOverlaySwitchStateChange}
handleClickNoCalendar={() => {
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}>
<LargeCalendar extraDays={extraDays} schedule={schedule.data} isLoading={schedule.isPending} />
<LargeCalendar
extraDays={extraDays}
schedule={schedule.data}
isLoading={schedule.isPending}
event={event}
/>
</BookerSection>
<BookerSection
@@ -505,7 +440,7 @@ const BookerComponent = ({
);
};
export const Booker = (props: BookerProps) => {
export const Booker = (props: BookerProps & WrappedBookerProps) => {
if (props.isAway) return <Away />;
return (
@@ -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<HTMLDivElement>;
errors: useBookingFormReturnType["errors"] & IUseBookingErrors;
errors: UseBookingFormReturnType["errors"] & IUseBookingErrors;
loadingStates: IUseBookingLoadingStates;
children?: React.ReactNode;
bookingForm: useBookingFormReturnType["bookingForm"];
bookingForm: UseBookingFormReturnType["bookingForm"];
renderConfirmNotVerifyEmailButtonCond: boolean;
extraOptions: Record<string, string | string[]>;
};
export const BookEventForm = ({
@@ -40,12 +40,12 @@ export const BookEventForm = ({
renderConfirmNotVerifyEmailButtonCond,
bookingForm,
children,
extraOptions,
}: Omit<BookEventFormProps, "event"> & {
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 <Alert severity="warning" message={t("error_booking_event")} />;
}
@@ -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<RouterOutputs["viewer"]["public"]["event"]>;
event: NonNullable<useEventReturnType["data"]>;
}
export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => {
@@ -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(() => {
@@ -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 (
<>
<OverlayCalendarSwitch enabled={isOverlayCalendarEnabled} />
<OverlayCalendarContinueModal open={isOpenOverlayContinueModal} onClose={handleCloseContinueModal} />
<OverlayCalendarSwitch
enabled={isOverlayCalendarEnabled}
hasSession={hasSession}
onStateChange={handleSwitchStateChange}
/>
<OverlayCalendarContinueModal
open={isOpenOverlayContinueModal}
onClose={handleCloseContinueModal}
onContinue={handleClickContinue}
/>
<OverlayCalendarSettingsModal
connectedCalendars={connectedCalendars}
open={isOpenOverlaySettingsModal}
@@ -1,5 +1,4 @@
import { CalendarSearch } from "lucide-react";
import { useRouter } from "next/navigation";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -8,10 +7,10 @@ import { Button, Dialog, DialogContent, DialogFooter } from "@calcom/ui";
interface IOverlayCalendarContinueModalProps {
open?: boolean;
onClose?: (state: boolean) => void;
onContinue: () => void;
}
export function OverlayCalendarContinueModal(props: IOverlayCalendarContinueModalProps) {
const router = useRouter();
const { t } = useLocale();
return (
<>
@@ -24,12 +23,7 @@ export function OverlayCalendarContinueModal(props: IOverlayCalendarContinueModa
<Button
data-testid="overlay-calendar-continue-button"
onClick={() => {
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}>
@@ -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 (
<div
@@ -70,10 +46,10 @@ export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) {
checked={switchEnabled}
id="overlayCalendar"
onCheckedChange={(state) => {
if (!session) {
if (!hasSession) {
setContinueWithProvider(state);
} else {
toggleOverlayCalendarQueryParam(state);
onStateChange(state);
}
}}
/>
@@ -83,7 +59,7 @@ export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) {
{t("overlay_my_calendar")}
</label>
</div>
{session && (
{hasSession && (
<Button
size="base"
data-testid="overlay-calendar-settings-button"
@@ -13,6 +13,8 @@ import type { useEventReturnType } from "../../utils/event";
import { validateLayout } from "../../utils/layout";
import { getQueryParam } from "../../utils/query-param";
export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
export const useBookerLayout = (event: useEventReturnType["data"]) => {
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
const isEmbed = useIsEmbed();
@@ -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<string, string | string[]>;
prefillFormParams: {
guests: string[];
name: string | null;
};
}
export type useBookingFormReturnType = ReturnType<typeof useBookingForm>;
export type UseBookingFormReturnType = ReturnType<typeof useBookingForm>;
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<BookingFormValues>({
@@ -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<string, string>;
}
export interface IUseBookingLoadingStates {
@@ -41,8 +41,9 @@ export interface IUseBookingErrors {
hasDataErrors: boolean;
dataErrors: unknown;
}
export type UseBookingsReturnType = ReturnType<typeof useBookings>;
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,
};
@@ -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<typeof useCalendars>;
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,
}
);
@@ -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<typeof useInitialFormValues>;
type UseInitialFormValuesProps = {
eventType: ReturnType<typeof useEvent>["data"];
rescheduleUid: string | null;
isRescheduling: boolean;
email?: string | null;
name?: string | null;
username?: string | null;
hasSession: boolean;
extraOptions: Record<string, string | string[]>;
prefillFormParams: {
guests: string[];
name: string | null;
};
};
export function useInitialFormValues({
eventType,
rescheduleUid,
isRescheduling,
}: {
eventType: ReturnType<typeof useEvent>["data"];
rescheduleUid: string | null;
isRescheduling: boolean;
}) {
email,
name,
username,
hasSession,
extraOptions,
prefillFormParams,
}: UseInitialFormValuesProps) {
const [initialValues, setDefaultValues] = useState<Record<string, unknown>>({});
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 };
}
@@ -8,6 +8,8 @@ import { useOverlayCalendarStore } from "../OverlayCalendar/store";
import type { UseCalendarsReturnType } from "./useCalendars";
import { useLocalSet } from "./useLocalSet";
export type UseOverlayCalendarReturnType = ReturnType<typeof useOverlayCalendar>;
export const useOverlayCalendar = ({
connectedCalendars,
overlayBusyDates,
@@ -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<typeof useSlots>;
export const useSlots = (event: useEventReturnType) => {
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore(
@@ -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<typeof useVerifyCode>;
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(""),
};
};
@@ -11,7 +11,7 @@ export interface IUseVerifyEmailProps {
name?: string | { firstName: string; lastname?: string };
requiresBookerEmailVerification?: boolean;
}
export type UseVerifyEmailReturnType = ReturnType<typeof useVerifyEmail>;
export const useVerifyEmail = ({
email,
name,
@@ -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<string, string | string[]>;
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";
@@ -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<SetStateAction<boolean>>;
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);
}}>
<DialogContent className="sm:max-w-md">
@@ -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 (
<>
<Form
@@ -203,15 +216,12 @@ export const CreateANewOrganizationForm = ({ slug }: { slug?: string }) => {
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}
/>
</>
);