* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main: - Rebrand Cal.com to Cal.diy across the entire codebase - Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions - Switch license from AGPL-3.0 to MIT - Remove docs/ directory (migrated to Nextra at cal.diy) - Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc. - Clean up .env.example for self-hosted Cal.diy - Update Docker image references to calcom/cal.diy - Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork - Add PR welcome bot for Cal.diy contributors - Fix API v2 breaking changes oasdiff ignore entries - Replace Blacksmith CI runners with default GitHub Actions 3893 files changed, 20789 insertions(+), 411020 deletions(-) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rip out org related comments in api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
442 lines
14 KiB
TypeScript
442 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
|
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
|
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/hooks/useBookingForm";
|
|
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
|
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
|
|
import { storeDecoyBooking } from "@calcom/features/bookings/lib/client/decoyBookingStore";
|
|
import { createBooking } from "@calcom/features/bookings/lib/create-booking";
|
|
import { createRecurringBooking } from "@calcom/features/bookings/lib/create-recurring-booking";
|
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
|
import type { BookerEvent, BookingResponse } from "@calcom/features/bookings/types";
|
|
import { getFullName } from "@calcom/features/form-builder/utils";
|
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { useRouter } from "next/navigation";
|
|
import { useRef } from "react";
|
|
import { shallow } from "zustand/shallow";
|
|
|
|
export interface IUseBookings {
|
|
event: {
|
|
data?:
|
|
| (Pick<
|
|
BookerEvent,
|
|
| "id"
|
|
| "slug"
|
|
| "subsetOfHosts"
|
|
| "requiresConfirmation"
|
|
| "isDynamic"
|
|
| "metadata"
|
|
| "forwardParamsSuccessRedirect"
|
|
| "successRedirectUrl"
|
|
| "length"
|
|
| "recurringEvent"
|
|
| "schedulingType"
|
|
> & {
|
|
subsetOfUsers: Pick<
|
|
BookerEvent["subsetOfUsers"][number],
|
|
"name" | "username" | "avatarUrl" | "weekStart" | "profile" | "bookerUrl"
|
|
>[];
|
|
})
|
|
| null;
|
|
};
|
|
hashedLink?: string | null;
|
|
bookingForm: UseBookingFormReturnType["bookingForm"];
|
|
metadata: Record<string, string>;
|
|
teamMemberEmail?: string | null;
|
|
isBookingDryRun?: boolean;
|
|
}
|
|
|
|
const getBaseBookingEventPayload = (booking: {
|
|
title?: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
eventTypeId?: number | null;
|
|
status?: BookingStatus;
|
|
paymentRequired: boolean;
|
|
isRecurring: boolean;
|
|
videoCallUrl?: string;
|
|
}) => {
|
|
return {
|
|
title: booking.title,
|
|
startTime: booking.startTime,
|
|
endTime: booking.endTime,
|
|
eventTypeId: booking.eventTypeId,
|
|
status: booking.status,
|
|
paymentRequired: booking.paymentRequired,
|
|
isRecurring: booking.isRecurring,
|
|
videoCallUrl: booking.videoCallUrl,
|
|
};
|
|
};
|
|
|
|
const getBookingSuccessfulEventPayload = (booking: {
|
|
title?: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
eventTypeId?: number | null;
|
|
status?: BookingStatus;
|
|
paymentRequired: boolean;
|
|
uid?: string;
|
|
isRecurring: boolean;
|
|
videoCallUrl?: string;
|
|
}) => {
|
|
return {
|
|
uid: booking.uid,
|
|
...getBaseBookingEventPayload(booking),
|
|
};
|
|
};
|
|
|
|
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
|
|
|
export const getDryRunBookingSuccessfulEventPayload = getBaseBookingEventPayload;
|
|
|
|
export const getDryRunRescheduleBookingSuccessfulEventPayload = getDryRunBookingSuccessfulEventPayload;
|
|
export interface IUseBookingLoadingStates {
|
|
creatingBooking: boolean;
|
|
creatingRecurringBooking: boolean;
|
|
}
|
|
|
|
export interface IUseBookingErrors {
|
|
hasDataErrors: boolean;
|
|
dataErrors: unknown;
|
|
}
|
|
export type UseBookingsReturnType = ReturnType<typeof useBookings>;
|
|
|
|
export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookingDryRun }: IUseBookings) => {
|
|
const router = useRouter();
|
|
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
|
|
const eventTypeId = useBookerStoreContext((state) => state.eventId);
|
|
|
|
const [rescheduleUid, setRescheduleUid] = useBookerStoreContext(
|
|
(state) => [state.rescheduleUid, state.setRescheduleUid],
|
|
shallow
|
|
);
|
|
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
|
|
const [bookingData, setBookingData] = useBookerStoreContext(
|
|
(state) => [state.bookingData, state.setBookingData],
|
|
shallow
|
|
);
|
|
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
|
const { t } = useLocale();
|
|
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
|
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
|
|
|
|
const duration = useBookerStoreContext((state) => state.selectedDuration);
|
|
|
|
const isRescheduling = !!rescheduleUid && !!bookingData;
|
|
|
|
const bookingUid = getQueryParam("bookingUid") ?? "";
|
|
|
|
const createBookingMutation = useMutation({
|
|
mutationFn: createBooking,
|
|
onSuccess: (booking) => {
|
|
if (booking.isDryRun) {
|
|
if (isRescheduling) {
|
|
sdkActionManager?.fire(
|
|
"dryRunRescheduleBookingSuccessfulV2",
|
|
getDryRunRescheduleBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: false,
|
|
})
|
|
);
|
|
} else {
|
|
sdkActionManager?.fire(
|
|
"dryRunBookingSuccessfulV2",
|
|
getDryRunBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: false,
|
|
})
|
|
);
|
|
}
|
|
|
|
router.push("/booking/dry-run-successful");
|
|
return;
|
|
}
|
|
|
|
if ("isShortCircuitedBooking" in booking && booking.isShortCircuitedBooking) {
|
|
if (!booking.uid) {
|
|
console.error("Decoy booking missing uid");
|
|
return;
|
|
}
|
|
|
|
const bookingData = {
|
|
uid: booking.uid,
|
|
title: booking.title ?? null,
|
|
startTime: booking.startTime,
|
|
endTime: booking.endTime,
|
|
booker: booking.attendees?.[0] ?? null,
|
|
host: booking.user ?? null,
|
|
location: booking.location ?? null,
|
|
};
|
|
|
|
storeDecoyBooking(bookingData);
|
|
router.push(`/booking-successful/${booking.uid}`);
|
|
return;
|
|
}
|
|
|
|
const { uid, paymentUid } = booking;
|
|
const fullName = getFullName(bookingForm.getValues("responses.name"));
|
|
|
|
const users = event.data?.subsetOfHosts?.length
|
|
? event.data?.subsetOfHosts.map((host) => host.user)
|
|
: event.data?.subsetOfUsers;
|
|
|
|
const validDuration = event.data?.isDynamic
|
|
? duration || event.data?.length
|
|
: duration && event.data?.metadata?.multipleDuration?.includes(duration)
|
|
? duration
|
|
: event.data?.length;
|
|
|
|
if (isRescheduling) {
|
|
sdkActionManager?.fire("rescheduleBookingSuccessful", {
|
|
booking: booking,
|
|
eventType: event.data,
|
|
date: booking?.startTime?.toString() || "",
|
|
duration: validDuration,
|
|
organizer: {
|
|
name: users?.[0]?.name || "Nameless",
|
|
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
|
timeZone: booking.user?.timeZone || "Europe/London",
|
|
},
|
|
confirmed: !(booking.status === BookingStatus.PENDING && event.data?.requiresConfirmation),
|
|
});
|
|
sdkActionManager?.fire(
|
|
"rescheduleBookingSuccessfulV2",
|
|
getRescheduleBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: false,
|
|
})
|
|
);
|
|
} else {
|
|
sdkActionManager?.fire("bookingSuccessful", {
|
|
booking: booking,
|
|
eventType: event.data,
|
|
date: booking?.startTime?.toString() || "",
|
|
duration: validDuration,
|
|
organizer: {
|
|
name: users?.[0]?.name || "Nameless",
|
|
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
|
timeZone: booking.user?.timeZone || "Europe/London",
|
|
},
|
|
confirmed: !(booking.status === BookingStatus.PENDING && event.data?.requiresConfirmation),
|
|
});
|
|
|
|
sdkActionManager?.fire(
|
|
"bookingSuccessfulV2",
|
|
getBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: false,
|
|
})
|
|
);
|
|
}
|
|
|
|
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 booking ? (booking.seatReferenceUid as string) : null,
|
|
formerTime:
|
|
isRescheduling && bookingData?.startTime ? dayjs(bookingData.startTime).toString() : undefined,
|
|
rescheduledBy, // ensure further reschedules performed on the success page are recorded correctly
|
|
};
|
|
|
|
bookingSuccessRedirect({
|
|
successRedirectUrl: event?.data?.successRedirectUrl || "",
|
|
query,
|
|
booking: booking as unknown as Pick<
|
|
BookingResponse,
|
|
| "uid"
|
|
| "title"
|
|
| "description"
|
|
| "startTime"
|
|
| "endTime"
|
|
| "location"
|
|
| "attendees"
|
|
| "user"
|
|
| "responses"
|
|
>,
|
|
forwardParamsSuccessRedirect:
|
|
event?.data?.forwardParamsSuccessRedirect === undefined
|
|
? true
|
|
: event?.data?.forwardParamsSuccessRedirect,
|
|
});
|
|
},
|
|
onError: (err) => {
|
|
if (bookerFormErrorRef?.current) {
|
|
bookerFormErrorRef.current.scrollIntoView({ behavior: "smooth" });
|
|
}
|
|
|
|
const error = err as Error & {
|
|
data: { rescheduleUid: string; startTime: string; attendees: string[]; seatUid?: string };
|
|
traceId?: string;
|
|
};
|
|
|
|
if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
|
|
setRescheduleUid(error.data?.seatUid ?? error.data?.rescheduleUid);
|
|
setBookingData({
|
|
uid: error.data?.rescheduleUid,
|
|
startTime: error.data?.startTime,
|
|
attendees: error.data?.attendees,
|
|
} as unknown as GetBookingType);
|
|
}
|
|
},
|
|
});
|
|
|
|
const createRecurringBookingMutation = useMutation({
|
|
mutationFn: createRecurringBooking,
|
|
onSuccess: async (bookings) => {
|
|
const booking = bookings[0] || {};
|
|
|
|
if (booking.isDryRun) {
|
|
if (isRescheduling) {
|
|
sdkActionManager?.fire("dryRunRescheduleBookingSuccessfulV2", {
|
|
...getDryRunRescheduleBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: true,
|
|
}),
|
|
allBookings: bookings.map((booking) => ({
|
|
startTime: booking.startTime,
|
|
endTime: booking.endTime,
|
|
})),
|
|
});
|
|
} else {
|
|
sdkActionManager?.fire("dryRunBookingSuccessfulV2", {
|
|
...getDryRunBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: true,
|
|
}),
|
|
allBookings: bookings.map((booking) => ({
|
|
startTime: booking.startTime,
|
|
endTime: booking.endTime,
|
|
})),
|
|
});
|
|
}
|
|
|
|
router.push("/booking/dry-run-successful");
|
|
return;
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
if (isRescheduling) {
|
|
// NOTE: It is recommended to define the event payload in the argument itself to provide a better type safety.
|
|
sdkActionManager?.fire("rescheduleBookingSuccessfulV2", {
|
|
...getRescheduleBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: true,
|
|
}),
|
|
allBookings: bookings.map((booking) => ({
|
|
startTime: booking.startTime,
|
|
endTime: booking.endTime,
|
|
})),
|
|
});
|
|
} else {
|
|
sdkActionManager?.fire("bookingSuccessfulV2", {
|
|
...getBookingSuccessfulEventPayload({
|
|
...booking,
|
|
isRecurring: true,
|
|
}),
|
|
allBookings: bookings.map((booking) => ({
|
|
startTime: booking.startTime,
|
|
endTime: booking.endTime,
|
|
})),
|
|
});
|
|
}
|
|
|
|
bookingSuccessRedirect({
|
|
successRedirectUrl: event?.data?.successRedirectUrl || "",
|
|
query,
|
|
booking: booking as unknown as Pick<
|
|
BookingResponse,
|
|
| "uid"
|
|
| "title"
|
|
| "description"
|
|
| "startTime"
|
|
| "endTime"
|
|
| "location"
|
|
| "attendees"
|
|
| "user"
|
|
| "responses"
|
|
>,
|
|
forwardParamsSuccessRedirect:
|
|
event?.data?.forwardParamsSuccessRedirect === undefined
|
|
? true
|
|
: event?.data?.forwardParamsSuccessRedirect,
|
|
});
|
|
},
|
|
onError: (err, _, ctx) => {
|
|
console.error("Error creating recurring booking", err);
|
|
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- It is only called when user takes an action in embed
|
|
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
},
|
|
});
|
|
|
|
const handleBookEvent = useHandleBookEvent({
|
|
event,
|
|
bookingForm,
|
|
hashedLink,
|
|
metadata,
|
|
handleRecBooking: createRecurringBookingMutation.mutate,
|
|
handleBooking: createBookingMutation.mutate,
|
|
isBookingDryRun,
|
|
});
|
|
|
|
const errors = {
|
|
hasDataErrors: Boolean(createBookingMutation.isError || createRecurringBookingMutation.isError),
|
|
dataErrors: createBookingMutation.error || createRecurringBookingMutation.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,
|
|
};
|
|
|
|
return {
|
|
handleBookEvent,
|
|
bookingForm,
|
|
bookerFormErrorRef,
|
|
errors,
|
|
loadingStates,
|
|
};
|
|
};
|