Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: skip confirm step followup #19076

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/features/bookings/Booker/Booker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ const BookerComponent = ({
}
};

const skipConfirmStep = useSkipConfirmStep(bookingForm, event?.data?.bookingFields);
const skipConfirmStep = useSkipConfirmStep(bookingForm, bookerState, event?.data?.bookingFields);

// Cloudflare Turnstile Captcha
const shouldRenderCaptcha = !!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export const BookEventForm = ({
/>
</div>
)}
{/* Cloudflare Turnstile Captcha */}
{!isPlatform && (
<div className="text-subtle my-3 w-full text-xs">
<Trans
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { useState, useEffect } from "react";
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { getBookingResponsesSchemaWithOptionalChecks } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { BookerEvent } from "@calcom/features/bookings/types";

import type { BookerEvent } from "../../../types";
import type { BookerState } from "../../types";

const useSkipConfirmStep = (
bookingForm: UseBookingFormReturnType["bookingForm"],
bookerState: BookerState,
bookingFields?: BookerEvent["bookingFields"]
) => {
const bookingFormValues = bookingForm.getValues();
Expand Down Expand Up @@ -34,8 +37,8 @@ const useSkipConfirmStep = (
}
};

checkSkipStep();
}, [bookingFormValues, bookingFields, rescheduleUid]);
bookerState === "selecting_time" && checkSkipStep();
}, [bookingFormValues, bookingFields, rescheduleUid, bookerState]);

return canSkip;
};
Expand Down
144 changes: 80 additions & 64 deletions packages/features/bookings/components/AvailableTimes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { getQueryParam } from "../Booker/utils/query-param";
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";

type Slot = Slots[string][number] & { showConfirmButton?: boolean };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cross-dependency on @features/schedules is a bit odd. Maybe we want to type this directly into the bookings component.


type TOnTimeSelect = (
time: string,
attendees: number,
Expand All @@ -34,10 +36,10 @@ export type AvailableTimesProps = {
slots: IGetAvailableSlots["slots"][string];
showTimeFormatToggle?: boolean;
className?: string;
} & Omit<SlotItemProps, "slot">;
} & Omit<SlotItemProps, "slot" | "handleSlotClick">;

type SlotItemProps = {
slot: Slots[string][number];
slot: Slot;
seatsPerTimeSlot?: number | null;
selectedSlots?: string[];
onTimeSelect: TOnTimeSelect;
Expand All @@ -52,6 +54,7 @@ type SlotItemProps = {
skipConfirmStep?: boolean;
shouldRenderCaptcha?: boolean;
watchedCfToken?: string;
handleSlotClick: (slot: Slot, isOverlapping: boolean) => void;
};

const SlotItem = ({
Expand All @@ -68,6 +71,7 @@ const SlotItem = ({
skipConfirmStep,
shouldRenderCaptcha,
watchedCfToken,
handleSlotClick,
}: SlotItemProps) => {
const { t } = useLocale();

Expand Down Expand Up @@ -106,26 +110,6 @@ const SlotItem = ({
offset,
});

const [showConfirm, setShowConfirm] = useState(false);

const onButtonClick = useCallback(() => {
if (!showConfirm && ((overlayCalendarToggled && isOverlapping) || skipConfirmStep)) {
setShowConfirm(true);
return;
}
onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid);
}, [
overlayCalendarToggled,
isOverlapping,
showConfirm,
onTimeSelect,
slot.time,
slot?.attendees,
slot.bookingUid,
seatsPerTimeSlot,
skipConfirmStep,
]);

return (
<AnimatePresence>
<div className="flex gap-2">
Expand All @@ -143,7 +127,7 @@ const SlotItem = ({
data-testid="time"
data-disabled={bookingFull}
data-time={slot.time}
onClick={onButtonClick}
onClick={() => handleSlotClick(slot, isOverlapping)}
className={classNames(
`hover:border-brand-default min-h-9 mb-2 flex h-auto w-full flex-grow flex-col justify-center py-2`,
selectedSlots?.includes(slot.time) && "border-brand-default",
Expand Down Expand Up @@ -176,47 +160,40 @@ const SlotItem = ({
</p>
)}
</Button>
{showConfirm && (
{!!slot.showConfirmButton && (
<HoverCard.Root>
<HoverCard.Trigger asChild>
<m.div initial={{ width: 0 }} animate={{ width: "auto" }} exit={{ width: 0 }}>
{skipConfirmStep ? (
<Button
type="button"
onClick={() =>
onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid)
}
data-testid="skip-confirm-book-button"
disabled={
(!!shouldRenderCaptcha && !watchedCfToken) ||
loadingStates?.creatingBooking ||
loadingStates?.creatingRecurringBooking ||
isVerificationCodeSending ||
loadingStates?.creatingInstantBooking
}
color="primary"
loading={
(selectedTimeslot === slot.time && loadingStates?.creatingBooking) ||
loadingStates?.creatingRecurringBooking ||
isVerificationCodeSending ||
loadingStates?.creatingInstantBooking
}>
{renderConfirmNotVerifyEmailButtonCond
? isPaidEvent
? t("pay_and_book")
: t("confirm")
: t("verify_email_email_button")}
</Button>
) : (
<Button
variant={layout === "column_view" ? "icon" : "button"}
StartIcon={layout === "column_view" ? "chevron-right" : undefined}
onClick={() =>
onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid)
}>
{layout !== "column_view" && t("confirm")}
</Button>
)}
<m.div key={slot.time} initial={{ width: 0 }} animate={{ width: "auto" }} exit={{ width: 0 }}>
<Button
variant={layout === "column_view" ? "icon" : "button"}
StartIcon={layout === "column_view" ? "chevron-right" : undefined}
type="button"
onClick={() =>
onTimeSelect(slot.time, slot?.attendees || 0, seatsPerTimeSlot, slot.bookingUid)
}
data-testid="skip-confirm-book-button"
disabled={
(!!shouldRenderCaptcha && !watchedCfToken) ||
loadingStates?.creatingBooking ||
loadingStates?.creatingRecurringBooking ||
isVerificationCodeSending ||
loadingStates?.creatingInstantBooking
}
color="primary"
loading={
(selectedTimeslot === slot.time && loadingStates?.creatingBooking) ||
loadingStates?.creatingRecurringBooking ||
isVerificationCodeSending ||
loadingStates?.creatingInstantBooking
}>
{layout == "column_view"
? ""
: renderConfirmNotVerifyEmailButtonCond
? isPaidEvent
? t("pay_and_book")
: t("confirm")
: t("verify_email_email_button")}
</Button>
</m.div>
</HoverCard.Trigger>
{isOverlapping && (
Expand All @@ -241,13 +218,42 @@ const SlotItem = ({
};

export const AvailableTimes = ({
slots,
slots: Incomingslots,
showTimeFormatToggle = true,
className,
seatsPerTimeSlot,
skipConfirmStep,
onTimeSelect,
...props
}: AvailableTimesProps) => {
const { t } = useLocale();

const [slots, setSlots] = useState(Incomingslots);
Copy link
Contributor

@Udit-takkar Udit-takkar Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if this could cause some issues in the future as we would have to maintain two different states of slots


const overlayCalendarToggled =
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");

const handleSlotClick = useCallback(
(selectedSlot: Slot, isOverlapping: boolean) => {
if ((overlayCalendarToggled && isOverlapping) || skipConfirmStep) {
setSlots((prevSlots) =>
prevSlots.map((slot) => ({
...slot,
showConfirmButton: slot.time === selectedSlot.time ? !selectedSlot?.showConfirmButton : false,
}))
);
return;
}
onTimeSelect(
selectedSlot.time,
selectedSlot?.attendees || 0,
seatsPerTimeSlot,
selectedSlot.bookingUid
);
},
[overlayCalendarToggled, onTimeSelect, seatsPerTimeSlot, skipConfirmStep]
);

const oooAllDay = slots.every((slot) => slot.away);
if (oooAllDay) {
return <OOOSlot {...slots[0]} />;
Expand All @@ -273,7 +279,17 @@ export const AvailableTimes = ({
{oooBeforeSlots && !oooAfterSlots && <OOOSlot {...slots[0]} />}
{slots.map((slot) => {
if (slot.away) return null;
return <SlotItem key={slot.time} slot={slot} {...props} />;
return (
<SlotItem
key={slot.time}
slot={slot}
{...props}
handleSlotClick={handleSlotClick}
seatsPerTimeSlot={seatsPerTimeSlot}
skipConfirmStep={skipConfirmStep}
onTimeSelect={onTimeSelect}
/>
);
})}
{oooAfterSlots && !oooBeforeSlots && <OOOSlot {...slots[slots.length - 1]} className="pb-0" />}
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/platform/atoms/booker/BookerPlatformWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,8 @@ export const BookerPlatformWrapper = (
onOverlaySwitchStateChange={onOverlaySwitchStateChange}
extraOptions={extraOptions ?? {}}
bookings={{
handleBookEvent: () => {
handleBookEvent();
handleBookEvent: (timeSlot?: string) => {
handleBookEvent(timeSlot);
return;
},
expiryTime: undefined,
Expand Down
21 changes: 15 additions & 6 deletions packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useIsPlatform } from "@calcom/atoms/monorepo";
import { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime";
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
Expand All @@ -7,9 +8,11 @@ import type { BookerEvent } from "@calcom/features/bookings/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RoutingFormSearchParams } from "@calcom/platform-types";
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
import { showToast } from "@calcom/ui";

import type { UseCreateBookingInput } from "./useCreateBooking";

type Callbacks = { onSuccess?: () => void; onError?: (err: any) => void };
type UseHandleBookingProps = {
bookingForm: UseBookingFormReturnType["bookingForm"];
event?: {
Expand All @@ -20,9 +23,9 @@ type UseHandleBookingProps = {
};
metadata: Record<string, string>;
hashedLink?: string | null;
handleBooking: (input: UseCreateBookingInput) => void;
handleInstantBooking: (input: BookingCreateBody) => void;
handleRecBooking: (input: BookingCreateBody[]) => void;
handleBooking: (input: UseCreateBookingInput, callbacks?: Callbacks) => void;
handleInstantBooking: (input: BookingCreateBody, callbacks?: Callbacks) => void;
handleRecBooking: (input: BookingCreateBody[], callbacks?: Callbacks) => void;
locationUrl?: string;
routingFormSearchParams?: RoutingFormSearchParams;
};
Expand All @@ -38,6 +41,7 @@ export const useHandleBookEvent = ({
locationUrl,
routingFormSearchParams,
}: UseHandleBookingProps) => {
const isPlatform = useIsPlatform();
const setFormValues = useBookerStore((state) => state.setFormValues);
const storeTimeSlot = useBookerStore((state) => state.selectedTimeslot);
const duration = useBookerStore((state) => state.selectedDuration);
Expand All @@ -54,10 +58,15 @@ export const useHandleBookEvent = ({
const teamMemberEmail = useBookerStore((state) => state.teamMemberEmail);
const crmOwnerRecordType = useBookerStore((state) => state.crmOwnerRecordType);
const crmAppSlug = useBookerStore((state) => state.crmAppSlug);
const handleError = (err: any) => {
const errorMessage = err?.message ? t(err.message) : t("can_you_try_again");
showToast(errorMessage, "error");
};

const handleBookEvent = (inputTimeSlot?: string) => {
const values = bookingForm.getValues();
const timeslot = inputTimeSlot ?? storeTimeSlot;
const callbacks = inputTimeSlot && !isPlatform ? { onError: handleError } : undefined;
if (timeslot) {
// Clears form values stored in store, so old values won't stick around.
setFormValues({});
Expand Down Expand Up @@ -101,11 +110,11 @@ export const useHandleBookEvent = ({
};

if (isInstantMeeting) {
handleInstantBooking(mapBookingToMutationInput(bookingInput));
handleInstantBooking(mapBookingToMutationInput(bookingInput), callbacks);
} else if (event.data?.recurringEvent?.freq && recurringEventCount && !rescheduleUid) {
handleRecBooking(mapRecurringBookingToMutationInput(bookingInput, recurringEventCount));
handleRecBooking(mapRecurringBookingToMutationInput(bookingInput, recurringEventCount), callbacks);
} else {
handleBooking({ ...mapBookingToMutationInput(bookingInput), locationUrl });
handleBooking({ ...mapBookingToMutationInput(bookingInput), locationUrl }, callbacks);
}
// Clears form values stored in store, so old values won't stick around.
setFormValues({});
Expand Down
Loading