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

feat: skip confirm step in booker #18773

Merged
merged 22 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
203b38c
feat: skip confirm step
SomayChauhan Jan 20, 2025
9290e43
better naming
SomayChauhan Jan 21, 2025
025c387
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 21, 2025
48fd326
disable on loading
SomayChauhan Jan 21, 2025
5adc9c4
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 21, 2025
90a5c77
feat: added cloudflare turnstile captcha to booker
SomayChauhan Jan 23, 2025
aad9bf3
Update Booker.tsx
SomayChauhan Jan 23, 2025
b4307a4
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 23, 2025
e42d019
Update AvailableTimeSlots.tsx
SomayChauhan Jan 23, 2025
9b015d1
made optional to fix type errors
SomayChauhan Jan 23, 2025
b36f507
Update Booker.tsx
SomayChauhan Jan 23, 2025
0d59d80
Update getBookingResponsesSchema.ts
SomayChauhan Jan 23, 2025
1793c4b
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 24, 2025
7d4165a
Update Booker.tsx
SomayChauhan Jan 24, 2025
fcc6933
fixed failing tests
SomayChauhan Jan 24, 2025
56334d0
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 24, 2025
8617e57
added tests
SomayChauhan Jan 24, 2025
0eb75ca
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 24, 2025
704407c
fix: fixed failing embed tests
SomayChauhan Jan 24, 2025
41963f9
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 24, 2025
f069838
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 27, 2025
8ba3d54
Merge branch 'main' into feat/skip-confirm-step
SomayChauhan Jan 28, 2025
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
17 changes: 17 additions & 0 deletions apps/web/playwright/booking-pages.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,23 @@ test.describe("prefill", () => {
await expect(page.locator('[name="email"]')).toHaveValue(testEmail);
});
});

test("skip confirm step if all fields are prefilled from query params", async ({ page }) => {
await page.goto("/pro/30min");
const url = new URL(page.url());
url.searchParams.set("name", testName);
url.searchParams.set("email", testEmail);
url.searchParams.set("guests", "[email protected]");
url.searchParams.set("guests", "[email protected]");
url.searchParams.set("notes", "This is an additional note");
await page.goto(url.toString());
await selectFirstAvailableTimeSlotNextMonth(page);

await expect(page.locator('[data-testid="skip-confirm-book-button"]')).toBeVisible();
await page.click('[data-testid="skip-confirm-book-button"]');

await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
});

test.describe("Booking on different layouts", () => {
Expand Down
2 changes: 0 additions & 2 deletions apps/web/playwright/manage-booking-questions.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ test.describe("Manage Booking Questions", () => {
prefillUrl.searchParams.append("email", "[email protected]");
prefillUrl.searchParams.append("guests", "[email protected]");
prefillUrl.searchParams.append("guests", "[email protected]");
prefillUrl.searchParams.append("notes", "This is an additional note");
await page.goto(prefillUrl.toString());
await bookTimeSlot({ page, skipSubmission: true });
await expectSystemFieldsToBeThereOnBookingPage({
Expand All @@ -185,7 +184,6 @@ test.describe("Manage Booking Questions", () => {
},
email: "[email protected]",
guests: ["[email protected]", "[email protected]"],
notes: "This is an additional note",
},
});
});
Expand Down
2 changes: 0 additions & 2 deletions packages/embeds/embed-core/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ if (only === "all" || only === "ns:default") {
},
name: "John",
email: "[email protected]",
notes: "Test Meeting",
guests: ["[email protected]", "[email protected]"],
theme: "dark",
"flag.coep": "true",
Expand Down Expand Up @@ -454,7 +453,6 @@ if (only === "all" || only == "ns:floatingButton") {
"flag.coep": "true",
name: "John",
email: "[email protected]",
notes: "Test Meeting",
guests: ["[email protected]", "[email protected]"],
...(theme ? { theme } : {}),
},
Expand Down
55 changes: 50 additions & 5 deletions packages/features/bookings/Booker/Booker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { shallow } from "zustand/shallow";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo";
import dayjs from "@calcom/dayjs";
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import { CLOUDFLARE_SITE_ID, CLOUDFLARE_USE_TURNSTILE_IN_BOOKER } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { BookerLayouts } from "@calcom/prisma/zod-utils";

Expand All @@ -33,6 +35,8 @@ import { useBookerStore } from "./store";
import type { BookerProps, WrappedBookerProps } from "./types";
import { isBookingDryRun } from "./utils/isBookingDryRun";

const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false });

const loadFramerFeatures = () => import("./framer-features").then((res) => res.default);
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy").then((mod) => mod.default));
const UnpublishedEntity = dynamic(() =>
Expand Down Expand Up @@ -77,6 +81,7 @@ const BookerComponent = ({
const searchParams = useCompatSearchParams();
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);

const selectedDate = useBookerStore((state) => state.selectedDate);
const {
shouldShowFormInDialog,
Expand Down Expand Up @@ -127,6 +132,8 @@ const BookerComponent = ({

const { handleBookEvent, errors, loadingStates, expiryTime, instantVideoMeetingUrl } = bookings;

const watchedCfToken = bookingForm.watch("cfToken");

const {
isEmailVerificationModalVisible,
setEmailVerificationModalVisible,
Expand All @@ -152,29 +159,46 @@ const BookerComponent = ({
}
};

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

// Cloudflare Turnstile Captcha
const shouldRenderCaptcha = !!(
!process.env.NEXT_PUBLIC_IS_E2E &&
renderCaptcha &&
CLOUDFLARE_SITE_ID &&
CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1" &&
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
);

useEffect(() => {
if (event.isPending) return setBookerState("loading");
if (!selectedDate) return setBookerState("selecting_date");
if (!selectedTimeslot) return setBookerState("selecting_time");
if (!selectedTimeslot || skipConfirmStep) return setBookerState("selecting_time");
return setBookerState("booking");
}, [event, selectedDate, selectedTimeslot, setBookerState]);
}, [event, selectedDate, selectedTimeslot, setBookerState, skipConfirmStep]);

const slot = getQueryParam("slot");

useEffect(() => {
setSelectedTimeslot(slot || null);
}, [slot, setSelectedTimeslot]);

const onSubmit = (timeSlot?: string) => {
renderConfirmNotVerifyEmailButtonCond ? handleBookEvent(timeSlot) : handleVerifyEmail();
};

const EventBooker = useMemo(() => {
return bookerState === "booking" ? (
<BookEventForm
key={key}
renderCaptcha={renderCaptcha}
shouldRenderCaptcha={shouldRenderCaptcha}
onCancel={() => {
setSelectedTimeslot(null);
if (seatedEventData.bookingUid) {
setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined });
}
}}
onSubmit={renderConfirmNotVerifyEmailButtonCond ? handleBookEvent : handleVerifyEmail}
onSubmit={() => (renderConfirmNotVerifyEmailButtonCond ? handleBookEvent() : handleVerifyEmail())}
errorRef={bookerFormErrorRef}
errors={{ ...formErrors, ...errors }}
loadingStates={loadingStates}
Expand Down Expand Up @@ -248,6 +272,8 @@ const BookerComponent = ({
verifyCode?.verifyCodeWithSessionNotRequired,
verifyCode?.verifyCodeWithSessionRequired,
isPlatform,
shouldRenderCaptcha,
isVerificationCodeSending,
]);

/**
Expand Down Expand Up @@ -440,6 +466,13 @@ const BookerComponent = ({
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
event={event}
loadingStates={loadingStates}
renderConfirmNotVerifyEmailButtonCond={renderConfirmNotVerifyEmailButtonCond}
isVerificationCodeSending={isVerificationCodeSending}
onSubmit={onSubmit}
skipConfirmStep={skipConfirmStep}
shouldRenderCaptcha={shouldRenderCaptcha}
watchedCfToken={watchedCfToken}
/>
</BookerSection>
</AnimatePresence>
Expand Down Expand Up @@ -470,7 +503,19 @@ const BookerComponent = ({
/>
</div>
)}
{!hideBranding && (!isPlatform || isPlatformBookerEmbed) && (

{shouldRenderCaptcha && (
<div className="mb-6 mt-auto pt-6">
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
bookingForm.setValue("cfToken", token);
}}
/>
</div>
)}

{!hideBranding && (!isPlatform || isPlatformBookerEmbed) && !shouldRenderCaptcha && (
<m.span
key="logo"
className={classNames(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useRef } from "react";

import dayjs from "@calcom/dayjs";
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
Expand All @@ -20,7 +21,7 @@ type AvailableTimeSlotsProps = {
seatsPerTimeSlot?: number | null;
showAvailableSeatsCount?: boolean | null;
event: {
data?: Pick<BookerEvent, "length"> | null;
data?: Pick<BookerEvent, "length" | "bookingFields" | "price" | "currency" | "metadata"> | null;
};
customClassNames?: {
availableTimeSlotsContainer?: string;
Expand All @@ -29,6 +30,13 @@ type AvailableTimeSlotsProps = {
availableTimeSlotsTimeFormatToggle?: string;
availableTimes?: string;
};
loadingStates: IUseBookingLoadingStates;
isVerificationCodeSending: boolean;
renderConfirmNotVerifyEmailButtonCond: boolean;
onSubmit: (timeSlot?: string) => void;
skipConfirmStep: boolean;
shouldRenderCaptcha?: boolean;
watchedCfToken?: string;
};

/**
Expand All @@ -38,15 +46,17 @@ type AvailableTimeSlotsProps = {
* will also fetch the next `extraDays` days and show multiple days
* in columns next to each other.
*/

export const AvailableTimeSlots = ({
extraDays,
limitHeight,
seatsPerTimeSlot,
showAvailableSeatsCount,
schedule,
isLoading,
event,
customClassNames,
skipConfirmStep,
onSubmit,
...props
}: AvailableTimeSlotsProps) => {
const selectedDate = useBookerStore((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
Expand All @@ -71,6 +81,9 @@ export const AvailableTimeSlots = ({
showAvailableSeatsCount,
});
}
if (skipConfirmStep) {
onSubmit(time);
}
return;
};

Expand Down Expand Up @@ -135,9 +148,9 @@ export const AvailableTimeSlots = ({
showTimeFormatToggle={!isColumnView}
onTimeSelect={onTimeSelect}
slots={slots.slots}
seatsPerTimeSlot={seatsPerTimeSlot}
showAvailableSeatsCount={showAvailableSeatsCount}
event={event}
skipConfirmStep={skipConfirmStep}
{...props}
/>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useMemo, useState } from "react";
import type { FieldError } from "react-hook-form";

import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo";
import type { BookerEvent } from "@calcom/features/bookings/types";
import {
WEBSITE_PRIVACY_POLICY_URL,
WEBSITE_TERMS_URL,
CLOUDFLARE_SITE_ID,
CLOUDFLARE_USE_TURNSTILE_IN_BOOKER,
} from "@calcom/lib/constants";
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, EmptyScreen, Form } from "@calcom/ui";
Expand All @@ -23,8 +17,6 @@ import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBo
import { BookingFields } from "./BookingFields";
import { FormSkeleton } from "./Skeleton";

const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false });

type BookEventFormProps = {
onCancel?: () => void;
onSubmit: () => void;
Expand All @@ -37,7 +29,7 @@ type BookEventFormProps = {
extraOptions: Record<string, string | string[]>;
isPlatform?: boolean;
isVerificationCodeSending: boolean;
renderCaptcha?: boolean;
shouldRenderCaptcha?: boolean;
};

export const BookEventForm = ({
Expand All @@ -54,7 +46,7 @@ export const BookEventForm = ({
extraOptions,
isVerificationCodeSending,
isPlatform = false,
renderCaptcha,
shouldRenderCaptcha,
}: Omit<BookEventFormProps, "event"> & {
eventQuery: {
isError: boolean;
Expand All @@ -71,13 +63,6 @@ export const BookEventForm = ({
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();

// Cloudflare Turnstile Captcha
const shouldRenderCaptcha =
!process.env.NEXT_PUBLIC_IS_E2E &&
renderCaptcha &&
CLOUDFLARE_SITE_ID &&
CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1";

const [responseVercelIdHeader] = useState<string | null>(null);
const { t } = useLocale();

Expand Down Expand Up @@ -140,14 +125,6 @@ export const BookEventForm = ({
</div>
)}
{/* Cloudflare Turnstile Captcha */}
{shouldRenderCaptcha ? (
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
bookingForm.setValue("cfToken", token);
}}
/>
) : null}
{!isPlatform && (
<div className="text-subtle my-3 w-full text-xs">
<Trans
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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";

const useSkipConfirmStep = (
bookingForm: UseBookingFormReturnType["bookingForm"],
bookingFields?: BookerEvent["bookingFields"]
) => {
const bookingFormValues = bookingForm.getValues();

const [canSkip, setCanSkip] = useState(false);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);

useEffect(() => {
const checkSkipStep = async () => {
if (!bookingFields) {
setCanSkip(false);
return;
}

try {
const responseSchema = getBookingResponsesSchemaWithOptionalChecks({
bookingFields,
view: rescheduleUid ? "reschedule" : "booking",
});
const responseSafeParse = await responseSchema.safeParseAsync(bookingFormValues.responses);

setCanSkip(responseSafeParse.success);
} catch (error) {
setCanSkip(false);
}
};

checkSkipStep();
}, [bookingFormValues, bookingFields, rescheduleUid]);

return canSkip;
};

export default useSkipConfirmStep;
Loading
Loading