-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: Add async spam check and decoy booking response #24326
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
Changes from all commits
bd229ca
753737b
c773994
a267c18
a1dd9e5
2fb5a86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainClear PII after render and localize fallback title
+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' -C3Length of output: 284658 🏁 Script executed: #!/bin/bash
sed -n '1,240p' apps/web/app/\(booking-page-wrapper\)/booking-successful/\[uid\]/page.tsx || trueLength of output: 1759 Clear PII after render and localize fallback title
🤖 Prompt for AI Agents |
||
| } | ||
| 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"> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <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> | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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); | ||
|
|
@@ -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"); | ||
| } | ||
| }, | ||
|
|
@@ -259,12 +256,6 @@ export const useBookings = ({ | |
| mutationFn: createBooking, | ||
| onSuccess: (booking) => { | ||
| if (booking.isDryRun) { | ||
| const validDuration = event.data?.isDynamic | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
|
@@ -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; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const { uid, paymentUid } = booking; | ||
| const fullName = getFullName(bookingForm.getValues("responses.name")); | ||
|
|
||
|
|
@@ -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[] }; | ||
|
|
@@ -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" }); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
|
|
@@ -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); | ||
|
|
||
| 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; | ||
| } | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.