Files
calendar/apps/web/modules/bookings/components/BookerWebWrapper.tsx
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d3e7554835 refactor: move WebWrapper files from packages/platform to apps/web/modules (#26629)
* refactor: move WebWrapper files from packages/platform to apps/web/modules

Move the following WebWrapper files to their appropriate locations in apps/web/modules:
- EventTypeWebWrapper and related tab wrappers to apps/web/modules/event-types/components/wrappers/
- BookerWebWrapper to apps/web/modules/bookings/components/
- ConferencingAppsViewWebWrapper to apps/web/modules/apps/components/
- SelectedCalendarsSettingsWebWrapper to apps/web/modules/calendars/components/
- AddMembersWithSwitchWebWrapper to apps/web/modules/event-types/components/

This reduces the number of files in packages/platform that import from @calcom/web,
improving the separation between platform and web-specific code.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: move web-specific hooks to apps/web and update imports

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* add back comments

* fix

* fix

* fix

* fix

* fix: correct relative import paths in moved WebWrapper files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* final

* fix: use package alias for event-type hooks instead of deeply nested relative paths

- Move sortHosts function to @calcom/lib/bookings/hostGroupUtils.ts for shared access
- Add exports for useEventTypeForm, useHandleRouteChange, useTabsNavigations to @calcom/atoms package.json
- Update EventTypeWebWrapper.tsx to use @calcom/atoms package alias instead of ../../../../../packages/... paths
- Update useEventTypeForm.ts to import sortHosts from @calcom/lib instead of @calcom/web
- Re-export sortHosts from HostEditDialogs.tsx for backward compatibility

This addresses the Cubic AI review feedback about fragile deeply nested relative paths that bypass proper package resolution.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-10 12:55:39 +00:00

275 lines
9.9 KiB
TypeScript

"use client";
import { useSession } from "next-auth/react";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useMemo, useCallback, useEffect } from "react";
import React from "react";
import { shallow } from "zustand/shallow";
import {
sdkActionManager,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { useBookerEmbedEvents } from "@calcom/embed-core/src/embed-iframe/react-hooks";
import type { BookerProps } from "@calcom/features/bookings/Booker";
import {
BookerStoreProvider,
useInitializeBookerStoreContext,
useBookerStoreContext,
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
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 { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { localStorage } from "@calcom/lib/webstorage";
import { Booker as BookerComponent } from "./Booker";
export type BookerWebWrapperAtomProps = BookerProps & {
eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>;
};
const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const clientFetchedEvent = useEvent({
disabled: !!props.eventData,
fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink,
});
const event = props.eventData
? {
data: props.eventData,
isSuccess: true,
isError: false,
isPending: false,
}
: clientFetchedEvent;
const bookerLayout = useBookerLayout(event.data?.profile?.bookerLayouts);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
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 rescheduledBy =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduledBy") : null;
const bookingUid =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null;
const timezone = searchParams?.get("cal.tz") || null;
useEffect(() => {
// This event isn't processed by BookingPageTagManager because BookingPageTagManager hasn't loaded when it is fired. I think we should have a queue in fire method to handle this.
sdkActionManager?.fire("navigatedToBooker", {});
}, []);
useInitializeBookerStore({
...props,
eventId: props.entity.eventTypeId ?? event?.data?.id,
rescheduleUid,
rescheduledBy,
bookingUid: bookingUid,
layout: bookerLayout.isMobile ? "mobile" : bookerLayout.defaultLayout,
org: props.entity.orgSlug,
timezone,
});
useInitializeBookerStoreContext({
...props,
eventId: props.entity.eventTypeId ?? event?.data?.id,
rescheduleUid,
rescheduledBy,
bookingUid: bookingUid,
layout: bookerLayout.isMobile ? "mobile" : bookerLayout.defaultLayout,
org: props.entity.orgSlug,
timezone,
});
const [dayCount] = useBookerStoreContext((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: event.data,
sessionEmail: session?.user.email,
sessionUsername: session?.user.username,
sessionName: session?.user.name,
hasSession,
extraOptions: routerQuery,
prefillFormParams,
});
const calendars = useCalendars({ hasSession });
const verifyEmail = useVerifyEmail({
email: bookerForm.formEmail,
name: bookerForm.formName,
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
onVerifyEmail: bookerForm.beforeVerifyEmail,
});
const slots = useSlots(event?.data ? { id: event.data.id, length: event.data.length } : null);
const isEmbed = useIsEmbed();
/**
* Prioritize dateSchedule load
* Component will render but use data already fetched from here, and no duplicate requests will be made
* */
const schedule = useScheduleForEvent({
eventId: props.entity.eventTypeId ?? event.data?.id,
username: props.username,
dayCount,
eventSlug: props.eventSlug,
month: props.month,
duration: props.duration,
selectedDate,
teamMemberEmail: props.teamMemberEmail,
fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink,
isTeamEvent: props.isTeamEvent ?? !!event.data?.team,
useApiV2: props.useApiV2,
bookerLayout,
...(props.entity.orgSlug ? { orgSlug: props.entity.orgSlug } : {}),
});
const bookings = useBookings({
event,
hashedLink: props.hashedLink,
bookingForm: bookerForm.bookingForm,
metadata: metadata ?? {},
teamMemberEmail: props.teamMemberEmail,
});
useBookerEmbedEvents({
eventId: event.data?.id,
eventSlug: event.data?.slug,
schedule,
});
const verifyCode = useVerifyCode({
onSuccess: () => {
if (!bookerForm.formEmail) return;
verifyEmail.setVerifiedEmail(bookerForm.formEmail);
verifyEmail.setEmailVerificationModalVisible(false);
bookings.handleBookEvent();
},
});
// Toggle query param for overlay calendar
const onOverlaySwitchStateChange = useCallback(
(state: boolean) => {
const url = new URL(window.location.href);
if (state) {
url.searchParams.set("overlayCalendar", "true");
localStorage.setItem("overlayCalendarSwitchDefault", "true");
} else {
url.searchParams.delete("overlayCalendar");
localStorage.removeItem("overlayCalendarSwitchDefault");
}
router.push(`${url.pathname}${url.search}`);
},
[router]
);
useBrandColors({
brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
darkBrandColor: event.data?.profile.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
theme: event.data?.profile.theme,
});
const areInstantMeetingParametersSet = Boolean(
event.data?.instantMeetingParameters &&
searchParams &&
event.data.instantMeetingParameters?.every?.((param) =>
Array.from(searchParams.values()).includes(param)
)
);
useEffect(() => {
if (hasSession) onOverlaySwitchStateChange(true);
}, [hasSession]);
return (
<BookerComponent
{...props}
onGoBackInstantMeeting={() => {
if (pathname) window.location.href = pathname;
}}
onConnectNowInstantMeeting={() => {
const newPath = `${pathname}?isInstantMeeting=true`;
if (isEmbed) {
const fullUrl = `${new URL(document.URL).origin}/${newPath}`;
window.open(fullUrl, "_blank", "noopener,noreferrer");
} else {
router.push(newPath);
}
}}
onOverlayClickNoCalendar={() => {
router.push("/apps/categories/calendar");
}}
onClickOverlayContinue={() => {
const newUrl = new URL(`${WEBAPP_URL}/login`);
newUrl.searchParams.set("callbackUrl", window.location.pathname);
newUrl.searchParams.set("overlayCalendar", "true");
router.push(newUrl.toString());
}}
onOverlaySwitchStateChange={onOverlaySwitchStateChange}
sessionUsername={session?.user.username}
isRedirect={isRedirect}
fromUserNameRedirected={fromUserNameRedirected}
rescheduleUid={rescheduleUid}
rescheduledBy={rescheduledBy}
bookingUid={bookingUid}
hasSession={hasSession}
hasValidLicense={session?.hasValidLicense ?? false}
extraOptions={routerQuery}
bookings={bookings}
calendars={calendars}
slots={slots}
verifyEmail={verifyEmail}
bookerForm={bookerForm}
event={event}
bookerLayout={bookerLayout}
schedule={schedule}
verifyCode={verifyCode}
isPlatform={false}
areInstantMeetingParametersSet={areInstantMeetingParametersSet}
userLocale={session?.user.locale}
renderCaptcha
/>
);
};
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
return (
<BookerStoreProvider>
<BookerWebWrapperComponent {...props} />
</BookerStoreProvider>
);
};