Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

import { useParams } from "next/navigation";

import dayjs from "@calcom/dayjs";
import { DecoyBookingSuccessCard } from "@calcom/features/bookings/Booker/components/DecoyBookingSuccessCard";
import { useDecoyBooking } from "@calcom/features/bookings/Booker/components/hooks/useDecoyBooking";

export default function BookingSuccessful() {
const params = useParams();

const uid = params?.uid as string;
const bookingData = useDecoyBooking(uid);

if (!bookingData) {
return null;
}

const { booking } = bookingData;

// Format the data for the BookingSuccessCard
const startTime = booking.startTime ? dayjs(booking.startTime) : null;
const endTime = booking.endTime ? dayjs(booking.endTime) : null;
const timeZone = booking.booker?.timeZone || booking.host?.timeZone || dayjs.tz.guess();

const formattedDate = startTime ? startTime.tz(timeZone).format("dddd, MMMM D, YYYY") : "";
const formattedTime = startTime ? startTime.tz(timeZone).format("h:mm A") : "";
const formattedEndTime = endTime ? endTime.tz(timeZone).format("h:mm A") : "";
const formattedTimeZone = timeZone;

const hostName = booking.host?.name || null;
const hostEmail = null; // Email not stored for spam decoy bookings
const attendeeName = booking.booker?.name || null;
const attendeeEmail = booking.booker?.email || null;

return (
<DecoyBookingSuccessCard
title={booking.title || "Booking"}
formattedDate={formattedDate}
formattedTime={formattedTime}
endTime={formattedEndTime}
formattedTimeZone={formattedTimeZone}
hostName={hostName}
hostEmail={hostEmail}
attendeeName={attendeeName}
attendeeEmail={attendeeEmail}
location={booking.location || null}
/>
);
Comment on lines +36 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Clear PII after render and localize fallback title

  • Remove decoy entry after reading to minimize PII retention.
  • Localize the fallback "Booking" string.
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { removeDecoyBooking } from "@calcom/features/bookings/lib/client/decoyBookingStore";
@@
 export default function BookingSuccessful() {
   const params = useParams();
+  const { t } = useLocale();
@@
   const bookingData = useDecoyBooking(uid);
@@
-    <DecoyBookingSuccessCard
-      title={booking.title || "Booking"}
+    {(() => {
+      // Clear decoy data after first render
+      removeDecoyBooking(uid);
+      return (
+        <DecoyBookingSuccessCard
+          title={booking.title || t("booking")}
           formattedDate={formattedDate}
           formattedTime={formattedTime}
           endTime={formattedEndTime}
           formattedTimeZone={formattedTimeZone}
           hostName={hostName}
           hostEmail={hostEmail}
           attendeeName={attendeeName}
           attendeeEmail={attendeeEmail}
           location={booking.location || null}
-    />
+        />
+      );
+    })()}

Run to verify TTL and removal behavior in the decoy store:


🏁 Script executed:

#!/bin/bash
# Inspect decoyBookingStore for TTL enforcement and removal
fd -t f 'decoyBookingStore.ts' | xargs -I{} sh -c 'echo "=== {} ==="; sed -n "1,200p" "{}"'
rg -n 'getDecoyBooking|removeDecoyBooking|timestamp|expiration|24' -C3

Length of output: 284658


🏁 Script executed:

#!/bin/bash
sed -n '1,240p' apps/web/app/\(booking-page-wrapper\)/booking-successful/\[uid\]/page.tsx || true

Length of output: 1759


Clear PII after render and localize fallback title

  • Replace the literal fallback "Booking" with localized text via useLocale (const { t } = useLocale(); title={booking.title || t("booking")}).
  • Delete stored decoy data after reading by calling removeDecoyBooking(uid) in a useEffect once bookingData is present (avoid performing removal during render).
  • File: apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx — update imports and add the useEffect around the existing render logic. Decoy store already enforces a 24h TTL and exposes removeDecoyBooking (verified).
🤖 Prompt for AI Agents
In apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx around
lines 36 to 49, the component uses a hardcoded fallback title and performs no
cleanup of stored decoy PII; update imports to include useLocale and useEffect
(and removeDecoyBooking from your decoy store), call const { t } = useLocale()
and replace title={booking.title || "Booking"} with title={booking.title ||
t("booking")}, and add a useEffect that, when bookingData (or booking) is
present, calls removeDecoyBooking(uid) once to delete stored decoy data (do not
call removal during render).

}
4 changes: 4 additions & 0 deletions apps/web/test/utils/bookingScenario/bookingScenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2001,6 +2001,10 @@ export async function mockCalendarToHaveNoBusySlots(
return await mockCalendar(metadataLookupKey, { ...calendarData, busySlots: [] });
}

export async function mockCalendarToCrashOnGetAvailability(metadataLookupKey: keyof typeof appStoreMetadata) {
return await mockCalendar(metadataLookupKey, { getAvailabilityCrash: true });
}

export async function mockCalendarToCrashOnCreateEvent(metadataLookupKey: keyof typeof appStoreMetadata) {
return await mockCalendar(metadataLookupKey, { creationCrash: true });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui/components/badge";
import { Icon } from "@calcom/ui/components/icon";

export interface DecoyBookingSuccessCardProps {
title: string;
formattedDate: string;
formattedTime: string;
endTime: string;
formattedTimeZone: string;
hostName: string | null;
hostEmail: string | null;
attendeeName: string | null;
attendeeEmail: string | null;
location: string | null;
}

export function DecoyBookingSuccessCard({
title,
formattedDate,
formattedTime,
endTime,
formattedTimeZone,
hostName,
hostEmail,
attendeeName,
attendeeEmail,
location,
}: DecoyBookingSuccessCardProps) {
const { t } = useLocale();

return (
<div className="h-screen">
<main className="mx-auto max-w-3xl">
<div className="overflow-y-auto">
<div className="flex items-end justify-center px-4 pb-20 pt-4 text-center sm:flex sm:p-0">
<div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
<div
className="bg-default dark:bg-muted border-booker border-booker-width inline-block transform overflow-hidden rounded-lg px-8 pb-4 pt-5 text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<Icon name="check" className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
</div>
<div className="mb-8 mt-6 text-center last:mb-0">
<h3 className="text-emphasis text-2xl font-semibold leading-6" id="modal-headline">
{t("meeting_is_scheduled")}
</h3>

<div className="mt-3">
<p className="text-default">{t("emailed_you_and_any_other_attendees")}</p>
</div>

<div className="border-subtle text-default mt-8 grid grid-cols-3 gap-x-4 border-t pt-8 text-left rtl:text-right sm:gap-x-0">
<div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6 last:mb-0">{title}</div>

{formattedDate && (
<>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6 last:mb-0">
{formattedDate}
{formattedTime && (
<>
<br />
{formattedTime}
{endTime && ` - ${endTime}`}
{formattedTimeZone && (
<span className="text-bookinglight"> ({formattedTimeZone})</span>
)}
</>
)}
</div>
</>
)}

<div className="font-medium">{t("who")}</div>
<div className="col-span-2 last:mb-0">
{hostName && (
<div className="mb-3">
<div>
<span className="mr-2">{hostName}</span>
<Badge variant="blue">{t("Host")}</Badge>
</div>
{hostEmail && <p className="text-default">{hostEmail}</p>}
</div>
)}
{attendeeName && (
<div className="mb-3 last:mb-0">
<p>{attendeeName}</p>
{attendeeEmail && <p>{attendeeEmail}</p>}
</div>
)}
</div>

{location && (
<>
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3">{t("web_conferencing_details_to_follow")}</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
}

68 changes: 39 additions & 29 deletions packages/features/bookings/Booker/components/hooks/useBookings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import dayjs from "@calcom/dayjs";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
import { storeDecoyBooking } from "@calcom/features/bookings/lib/client/decoyBookingStore";
import { createBooking } from "@calcom/features/bookings/lib/create-booking";
import { createInstantBooking } from "@calcom/features/bookings/lib/create-instant-booking";
import { createRecurringBooking } from "@calcom/features/bookings/lib/create-recurring-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { getFullName } from "@calcom/features/form-builder/utils";
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
import { useBookingSuccessRedirect } from "../../../lib/bookingSuccessRedirect";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
Expand Down Expand Up @@ -165,13 +168,7 @@ const storeInLocalStorage = ({
localStorage.setItem(STORAGE_KEY, value);
};

export const useBookings = ({
event,
hashedLink,
bookingForm,
metadata,
isBookingDryRun,
}: IUseBookings) => {
export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookingDryRun }: IUseBookings) => {
const router = useRouter();
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
const eventTypeId = useBookerStoreContext((state) => state.eventId);
Expand Down Expand Up @@ -248,7 +245,7 @@ export const useBookings = ({
} else {
showToast(t("something_went_wrong_on_our_end"), "error");
}
} catch (err) {
} catch {
showToast(t("something_went_wrong_on_our_end"), "error");
}
},
Expand All @@ -259,12 +256,6 @@ export const useBookings = ({
mutationFn: createBooking,
onSuccess: (booking) => {
if (booking.isDryRun) {
const validDuration = event.data?.isDynamic
Copy link
Member Author

Choose a reason for hiding this comment

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

Unused variable

? duration || event.data?.length
: duration && event.data?.metadata?.multipleDuration?.includes(duration)
? duration
: event.data?.length;

if (isRescheduling) {
sdkActionManager?.fire(
"dryRunRescheduleBookingSuccessfulV2",
Expand All @@ -286,6 +277,28 @@ export const useBookings = ({
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"));

Expand Down Expand Up @@ -380,9 +393,10 @@ export const useBookings = ({
: event?.data?.forwardParamsSuccessRedirect,
});
},
onError: (err, _, ctx) => {
// 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" });
onError: (err) => {
if (bookerFormErrorRef?.current) {
bookerFormErrorRef.current.scrollIntoView({ behavior: "smooth" });
}

const error = err as Error & {
data: { rescheduleUid: string; startTime: string; attendees: string[] };
Expand Down Expand Up @@ -414,10 +428,11 @@ export const useBookings = ({
updateQueryParam("bookingId", responseData.bookingId);
setExpiryTime(responseData.expires);
},
onError: (err, _, ctx) => {
onError: (err) => {
console.error("Error creating instant 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" });
if (bookerFormErrorRef?.current) {
bookerFormErrorRef.current.scrollIntoView({ behavior: "smooth" });
}
},
});

Expand Down Expand Up @@ -513,15 +528,10 @@ export const useBookings = ({
bookingForm,
hashedLink,
metadata,
handleInstantBooking: (
variables: Parameters<typeof createInstantBookingMutation.mutate>[0]
) => {
handleInstantBooking: (variables: Parameters<typeof createInstantBookingMutation.mutate>[0]) => {
const remaining = getInstantCooldownRemainingMs(eventTypeId);
if (remaining > 0) {
showToast(
t("please_try_again_later_or_book_another_slot"),
"error"
);
showToast(t("please_try_again_later_or_book_another_slot"), "error");
return;
}
createInstantBookingMutation.mutate(variables);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

import { getDecoyBooking } from "@calcom/features/bookings/lib/client/decoyBookingStore";
import type { DecoyBookingData } from "@calcom/features/bookings/lib/client/decoyBookingStore";

/**
* Hook to retrieve and manage decoy booking data from localStorage
* @param uid - The booking uid
* @returns The booking data or null if not found/expired
*/
export function useDecoyBooking(uid: string) {
const router = useRouter();
const [bookingData, setBookingData] = useState<DecoyBookingData | null>(null);

useEffect(() => {
const data = getDecoyBooking(uid);

if (!data) {
router.push("/404");
return;
}

setBookingData(data);
}, [uid, router]);

return bookingData;
}

Loading
Loading