297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
import { useMutation } from "@tanstack/react-query";
|
|
import { useRouter } from "next/navigation";
|
|
import { useRef, useState, useEffect } from "react";
|
|
|
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
|
import { useHandleBookEvent } from "@calcom/atoms/monorepo";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
|
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
|
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
|
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
|
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
|
import { getFullName } from "@calcom/features/form-builder/utils";
|
|
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
|
import { trpc } from "@calcom/trpc";
|
|
import { showToast } from "@calcom/ui";
|
|
|
|
import type { UseBookingFormReturnType } from "./useBookingForm";
|
|
|
|
export interface IUseBookings {
|
|
event: {
|
|
data?:
|
|
| (Pick<
|
|
BookerEvent,
|
|
| "id"
|
|
| "slug"
|
|
| "hosts"
|
|
| "requiresConfirmation"
|
|
| "isDynamic"
|
|
| "metadata"
|
|
| "forwardParamsSuccessRedirect"
|
|
| "successRedirectUrl"
|
|
| "length"
|
|
| "recurringEvent"
|
|
| "schedulingType"
|
|
> & {
|
|
users: Pick<
|
|
BookerEvent["users"][number],
|
|
"name" | "username" | "avatarUrl" | "weekStart" | "profile" | "bookerUrl"
|
|
>[];
|
|
})
|
|
| null;
|
|
};
|
|
hashedLink?: string | null;
|
|
bookingForm: UseBookingFormReturnType["bookingForm"];
|
|
metadata: Record<string, string>;
|
|
teamMemberEmail?: string;
|
|
}
|
|
|
|
export interface IUseBookingLoadingStates {
|
|
creatingBooking: boolean;
|
|
creatingRecurringBooking: boolean;
|
|
creatingInstantBooking: boolean;
|
|
}
|
|
|
|
export interface IUseBookingErrors {
|
|
hasDataErrors: boolean;
|
|
dataErrors: unknown;
|
|
}
|
|
export type UseBookingsReturnType = ReturnType<typeof useBookings>;
|
|
|
|
export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemberEmail }: IUseBookings) => {
|
|
const router = useRouter();
|
|
const eventSlug = useBookerStore((state) => state.eventSlug);
|
|
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
|
const bookingData = useBookerStore((state) => state.bookingData);
|
|
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
|
const { t } = useLocale();
|
|
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
|
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
|
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
|
|
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
|
|
const duration = useBookerStore((state) => state.selectedDuration);
|
|
|
|
const isRescheduling = !!rescheduleUid && !!bookingData;
|
|
|
|
const bookingId = parseInt(getQueryParam("bookingId") || "0");
|
|
|
|
const _instantBooking = trpc.viewer.bookings.getInstantBookingLocation.useQuery(
|
|
{
|
|
bookingId: bookingId,
|
|
},
|
|
{
|
|
enabled: !!bookingId,
|
|
refetchInterval: 2000,
|
|
refetchIntervalInBackground: true,
|
|
}
|
|
);
|
|
useEffect(
|
|
function refactorMeWithoutEffect() {
|
|
const data = _instantBooking.data;
|
|
|
|
if (!data || !data.booking) return;
|
|
try {
|
|
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
|
|
data.booking?.metadata || {}
|
|
)?.videoCallUrl;
|
|
|
|
if (locationVideoCallUrl) {
|
|
setInstantVideoMeetingUrl(locationVideoCallUrl);
|
|
} else {
|
|
showToast(t("something_went_wrong_on_our_end"), "error");
|
|
}
|
|
} catch (err) {
|
|
showToast(t("something_went_wrong_on_our_end"), "error");
|
|
}
|
|
},
|
|
[_instantBooking.data]
|
|
);
|
|
|
|
const createBookingMutation = useMutation({
|
|
mutationFn: createBooking,
|
|
onSuccess: (responseData) => {
|
|
const { uid, paymentUid } = responseData;
|
|
const fullName = getFullName(bookingForm.getValues("responses.name"));
|
|
|
|
const users = !!event.data?.hosts?.length
|
|
? event.data?.hosts.map((host) => host.user)
|
|
: event.data?.users;
|
|
|
|
const validDuration = event.data?.isDynamic
|
|
? duration || event.data?.length
|
|
: duration && event.data?.metadata?.multipleDuration?.includes(duration)
|
|
? duration
|
|
: event.data?.length;
|
|
const eventPayload = {
|
|
uid: responseData.uid,
|
|
title: responseData.title,
|
|
startTime: responseData.startTime,
|
|
endTime: responseData.endTime,
|
|
eventTypeId: responseData.eventTypeId,
|
|
status: responseData.status,
|
|
paymentRequired: responseData.paymentRequired,
|
|
};
|
|
if (isRescheduling) {
|
|
sdkActionManager?.fire("rescheduleBookingSuccessful", {
|
|
booking: responseData,
|
|
eventType: event.data,
|
|
date: responseData?.startTime?.toString() || "",
|
|
duration: validDuration,
|
|
organizer: {
|
|
name: users?.[0]?.name || "Nameless",
|
|
email: responseData?.userPrimaryEmail || responseData.user?.email || "Email-less",
|
|
timeZone: responseData.user?.timeZone || "Europe/London",
|
|
},
|
|
confirmed: !(responseData.status === BookingStatus.PENDING && event.data?.requiresConfirmation),
|
|
});
|
|
sdkActionManager?.fire("rescheduleBookingSuccessfulV2", eventPayload);
|
|
} else {
|
|
sdkActionManager?.fire("bookingSuccessful", {
|
|
booking: responseData,
|
|
eventType: event.data,
|
|
date: responseData?.startTime?.toString() || "",
|
|
duration: validDuration,
|
|
organizer: {
|
|
name: users?.[0]?.name || "Nameless",
|
|
email: responseData?.userPrimaryEmail || responseData.user?.email || "Email-less",
|
|
timeZone: responseData.user?.timeZone || "Europe/London",
|
|
},
|
|
confirmed: !(responseData.status === BookingStatus.PENDING && event.data?.requiresConfirmation),
|
|
});
|
|
|
|
sdkActionManager?.fire("bookingSuccessfulV2", eventPayload);
|
|
}
|
|
|
|
if (paymentUid) {
|
|
router.push(
|
|
createPaymentLink({
|
|
paymentUid,
|
|
date: timeslot,
|
|
name: fullName,
|
|
email: bookingForm.getValues("responses.email"),
|
|
absolute: false,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!uid) {
|
|
console.error("No uid returned from createBookingMutation");
|
|
return;
|
|
}
|
|
|
|
const query = {
|
|
isSuccessBookingPage: true,
|
|
email: bookingForm.getValues("responses.email"),
|
|
eventTypeSlug: eventSlug,
|
|
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
|
|
formerTime:
|
|
isRescheduling && bookingData?.startTime ? dayjs(bookingData.startTime).toString() : undefined,
|
|
};
|
|
|
|
bookingSuccessRedirect({
|
|
successRedirectUrl: event?.data?.successRedirectUrl || "",
|
|
query,
|
|
booking: responseData,
|
|
forwardParamsSuccessRedirect:
|
|
event?.data?.forwardParamsSuccessRedirect === undefined
|
|
? true
|
|
: event?.data?.forwardParamsSuccessRedirect,
|
|
});
|
|
},
|
|
onError: (err, _, ctx) => {
|
|
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
},
|
|
});
|
|
|
|
const createInstantBookingMutation = useMutation({
|
|
mutationFn: createInstantBooking,
|
|
onSuccess: (responseData) => {
|
|
updateQueryParam("bookingId", responseData.bookingId);
|
|
setExpiryTime(responseData.expires);
|
|
},
|
|
onError: (err, _, ctx) => {
|
|
console.error("Error creating instant booking", err);
|
|
|
|
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
},
|
|
});
|
|
|
|
const createRecurringBookingMutation = useMutation({
|
|
mutationFn: createRecurringBooking,
|
|
onSuccess: async (responseData) => {
|
|
const booking = responseData[0] || {};
|
|
const { uid } = booking;
|
|
|
|
if (!uid) {
|
|
console.error("No uid returned from createRecurringBookingMutation");
|
|
return;
|
|
}
|
|
|
|
const query = {
|
|
isSuccessBookingPage: true,
|
|
allRemainingBookings: true,
|
|
email: bookingForm.getValues("responses.email"),
|
|
eventTypeSlug: eventSlug,
|
|
formerTime:
|
|
isRescheduling && bookingData?.startTime ? dayjs(bookingData.startTime).toString() : undefined,
|
|
};
|
|
|
|
bookingSuccessRedirect({
|
|
successRedirectUrl: event?.data?.successRedirectUrl || "",
|
|
query,
|
|
booking,
|
|
forwardParamsSuccessRedirect:
|
|
event?.data?.forwardParamsSuccessRedirect === undefined
|
|
? true
|
|
: event?.data?.forwardParamsSuccessRedirect,
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleBookEvent = useHandleBookEvent({
|
|
event,
|
|
bookingForm,
|
|
hashedLink,
|
|
metadata,
|
|
teamMemberEmail,
|
|
handleInstantBooking: createInstantBookingMutation.mutate,
|
|
handleRecBooking: createRecurringBookingMutation.mutate,
|
|
handleBooking: createBookingMutation.mutate,
|
|
});
|
|
|
|
const errors = {
|
|
hasDataErrors: Boolean(
|
|
createBookingMutation.isError ||
|
|
createRecurringBookingMutation.isError ||
|
|
createInstantBookingMutation.isError
|
|
),
|
|
dataErrors:
|
|
createBookingMutation.error ||
|
|
createRecurringBookingMutation.error ||
|
|
createInstantBookingMutation.error,
|
|
};
|
|
|
|
// A redirect is triggered on mutation success, so keep the loading state while it is happening.
|
|
const loadingStates = {
|
|
creatingBooking: createBookingMutation.isPending || createBookingMutation.isSuccess,
|
|
creatingRecurringBooking:
|
|
createRecurringBookingMutation.isPending || createRecurringBookingMutation.isSuccess,
|
|
creatingInstantBooking: createInstantBookingMutation.isPending,
|
|
};
|
|
|
|
return {
|
|
handleBookEvent,
|
|
expiryTime: instantMeetingTokenExpiryTime,
|
|
bookingForm,
|
|
bookerFormErrorRef,
|
|
errors,
|
|
loadingStates,
|
|
instantVideoMeetingUrl,
|
|
};
|
|
};
|