Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5566e77
feat: Add cancelledBy input to cancellation page
iharsh02 Jan 20, 2025
9d50856
fix: Update email verification error message
iharsh02 Jan 20, 2025
5425092
Merge branch 'main' into fix-cal-5045
iharsh02 Jan 22, 2025
922c4c4
Merge branch 'main' into fix-cal-5045
iharsh02 Jan 24, 2025
f5d2a4f
Merge branch 'main' into fix-cal-5045
iharsh02 Feb 19, 2025
80b8840
Merge branch 'main' into fix-cal-5045
iharsh02 Feb 19, 2025
9a7c5fb
Merge branch 'main' into fix-cal-5045
anikdhabal Feb 25, 2025
300467a
Update safe-parse.ts
anikdhabal Feb 25, 2025
27eda6c
Merge branch 'main' into fix-cal-5045
anikdhabal Feb 25, 2025
121e1c7
Merge branch 'main' into fix-cal-5045
kart1ka Jul 5, 2025
f551126
Merge branch 'main' into fix-cal-5045
kart1ka Jul 5, 2025
8e541ac
fix: check attendees and booking owner email
kart1ka Jul 5, 2025
02a90b9
Merge branch 'main' into fix-cal-5045
anikdhabal Jul 8, 2025
1bc263c
fix: minor ui
kart1ka Jul 9, 2025
abfe1ca
Update CancelBooking.tsx
anikdhabal Jul 14, 2025
93e3b72
Update common.json
anikdhabal Jul 14, 2025
c3a435a
Merge branch 'main' into fix-cal-5045
kart1ka Jul 14, 2025
5d4589c
Merge branch 'main' into fix-cal-5045
iharsh02 Jul 15, 2025
b98e770
fix: resolve booking-seats.e2e failing test
iharsh02 Jul 16, 2025
e03902b
Merge branch 'main' into fix-cal-5045
iharsh02 Jul 16, 2025
ced377d
Update apps/web/components/booking/CancelBooking.tsx
anikdhabal Jul 16, 2025
d67ae00
Merge branch 'main' into fix-cal-5045
iharsh02 Jul 23, 2025
ab65181
feat: obfuscate sensitive user information in booking details
iharsh02 Jul 23, 2025
eb98e9e
Merge branch 'main' to fix-cal-5045
iharsh02 Jul 25, 2025
90ccd6c
chore
iharsh02 Jul 25, 2025
a6c06e1
Merge branch 'main' to fix-cal-5045
iharsh02 Jul 25, 2025
d177570
feat : Add email input dialog to /booking/uid page
iharsh02 Jul 25, 2025
54dbe9f
fix : booking-seats.e2e test
iharsh02 Jul 26, 2025
da1c051
Merge branch 'main' into fix-cal-5045
iharsh02 Jul 26, 2025
290ad23
fix : linter and coderabbit issue
iharsh02 Jul 26, 2025
672504e
fix : linting and coderabbit issue
iharsh02 Jul 26, 2025
7636778
fix: linting issue
iharsh02 Jul 26, 2025
293d7dd
Merge branch 'main' into fix-cal-5045
iharsh02 Jul 31, 2025
b7c17ca
fix : booking page being displayed before the dialog box opens
iharsh02 Jul 31, 2025
fc002a1
Merge branch 'main' into fix-cal-5045
iharsh02 Jul 31, 2025
2ce0960
fix : failing e2e test
iharsh02 Jul 31, 2025
fb0ac70
Merge branch 'main' into fix-cal-5045
kart1ka Aug 11, 2025
a92bb7a
Merge branch 'main' into fix-cal-5045
kart1ka Aug 11, 2025
8dc4cf2
Merge branch 'main' into fix-cal-5045
kart1ka Aug 26, 2025
3edb21b
Merge branch 'main' into fix-cal-5045
kart1ka Sep 1, 2025
0be3e55
Merge branch 'main' into fix-cal-5045
kart1ka Sep 4, 2025
c8176f5
Merge branch 'main' into fix-cal-5045
kart1ka Sep 10, 2025
11785f2
Merge upstream/main into fix-cal-5045 - resolve conflicts for email v…
devin-ai-integration[bot] Jan 14, 2026
84b351d
Merge branch 'main' into fix-cal-5045
keithwillcode Jan 14, 2026
b31cfd4
feat: implement server-side email verification for booking page
devin-ai-integration[bot] Jan 14, 2026
832b6dc
Merge branch 'main' into fix-cal-5045
keithwillcode Jan 14, 2026
74772c4
refactor: move prisma query to BookingRepository
devin-ai-integration[bot] Jan 14, 2026
7d153b0
fix: address review feedback for email verification feature
devin-ai-integration[bot] Feb 5, 2026
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
127 changes: 71 additions & 56 deletions apps/web/components/booking/CancelBooking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type Props = {
currency: string;
appId: string | null;
} | null;
userEmail?: string;
attendees?: { email: string }[];
};
profile: {
name: string | null;
Expand Down Expand Up @@ -177,6 +179,74 @@ export default function CancelBooking(props: Props) {

const isRenderedAsCancelDialog = props.renderContext === "dialog";

const handleCancellation = async () => {
setLoading(true);

try {
// telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());

const response = await fetch("/api/csrf?sameSite=none", { cache: "no-store" });
if (!response.ok) {
throw new Error(`Failed to fetch CSRF token: ${response.status}`);
}
const { csrfToken } = await response.json();

const res = await fetch("/api/cancel", {
body: JSON.stringify({
uid: booking?.uid,
cancellationReason: cancellationReason,
allRemainingBookings,
// @NOTE: very important this shouldn't cancel with number ID use uid instead
seatReferenceUid,
cancelledBy: currentUserEmail,
internalNote: internalNote,
csrfToken,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});

const bookingWithCancellationReason = {
...(bookingCancelledEventProps.booking as object),
cancellationReason,
} as unknown;

if (res.ok) {
// tested by apps/web/playwright/booking-pages.e2e.ts
sdkActionManager?.fire("bookingCancelled", {
...bookingCancelledEventProps,
booking: bookingWithCancellationReason,
});
refreshData();
if (props.onCanceled) {
props.onCanceled();
}
} else {
const data = await res.json();
const errorMessage =
data.message ||
`${t("error_with_status_code_occured", { status: res.status })} ${t("please_try_again")}`;

if (props.showErrorAsToast) {
showToast(errorMessage, "error");
} else {
setError(errorMessage);
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : t("please_try_again");
if (props.showErrorAsToast) {
showToast(errorMessage, "error");
} else {
setError(errorMessage);
}
} finally {
setLoading(false);
}
};

return (
<>
{error && !props.showErrorAsToast && (
Expand Down Expand Up @@ -268,62 +338,7 @@ export default function CancelBooking(props: Props) {
<Button
data-testid="confirm_cancel"
disabled={hostMissingCancellationReason || cancellationNoShowFeeNotAcknowledged}
onClick={async () => {
setLoading(true);

// telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());

const response = await fetch("/api/csrf?sameSite=none", { cache: "no-store" });
const { csrfToken } = await response.json();

const res = await fetch("/api/cancel", {
body: JSON.stringify({
uid: booking?.uid,
cancellationReason: cancellationReason,
allRemainingBookings,
// @NOTE: very important this shouldn't cancel with number ID use uid instead
seatReferenceUid,
cancelledBy: currentUserEmail,
internalNote: internalNote,
csrfToken,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});

const bookingWithCancellationReason = {
...(bookingCancelledEventProps.booking as object),
cancellationReason,
} as unknown;

if (res.status >= 200 && res.status < 300) {
// tested by apps/web/playwright/booking-pages.e2e.ts
sdkActionManager?.fire("bookingCancelled", {
...bookingCancelledEventProps,
booking: bookingWithCancellationReason,
});
refreshData();
if (props.onCanceled) {
props.onCanceled();
}
} else {
const data = await res.json();
setLoading(false);
const errorMessage =
data.message ||
`${t("error_with_status_code_occured", { status: res.status })} ${t(
"please_try_again"
)}`;

if (props.showErrorAsToast) {
showToast(errorMessage, "error");
} else {
setError(errorMessage);
}
}
}}
onClick={handleCancellation}
loading={loading}>
{props.allRemainingBookings ? t("cancel_all_remaining") : t("cancel_event")}
</Button>
Expand Down
138 changes: 135 additions & 3 deletions apps/web/modules/bookings/views/bookings-single-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import classNames from "classnames";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { Fragment, useEffect, useState } from "react";
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
import { Toaster } from "sonner";
import { z } from "zod";

Expand Down Expand Up @@ -49,8 +49,10 @@ import { Alert } from "@calcom/ui/components/alert";
import { Avatar } from "@calcom/ui/components/avatar";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent, DialogHeader } from "@calcom/ui/components/dialog";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { EmailInput, TextArea } from "@calcom/ui/components/form";
import { Input } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { useCalcomTheme } from "@calcom/ui/styles";
Expand Down Expand Up @@ -82,6 +84,10 @@ const querySchema = z.object({
redirect_status: z.string().optional(),
});

const verificationSchema = z.object({
email: z.string().min(1, "Email is required").email("Enter a valid email address"),
});

const useBrandColors = ({
brandColor,
darkBrandColor,
Expand Down Expand Up @@ -162,7 +168,7 @@ export default function Success(props: PageProps) {
const [is24h, setIs24h] = useState(
props?.userTimeFormat ? props.userTimeFormat === 24 : isBrowserLocale24h()
);
const { data: session } = useSession();
const { data: session, status: sessionStatus } = useSession();
const isHost = props.isLoggedInUserHost;

const [showUtmParams, setShowUtmParams] = useState(false);
Expand Down Expand Up @@ -195,12 +201,17 @@ export default function Success(props: PageProps) {
const currentUserEmail =
searchParams?.get("rescheduledBy") ??
searchParams?.get("cancelledBy") ??
searchParams?.get("email") ??
session?.user?.email ??
undefined;

const defaultRating = validateRating(rating);
const [rateValue, setRateValue] = useState<number>(defaultRating);
const [isFeedbackSubmitted, setIsFeedbackSubmitted] = useState(false);
const [showVerificationDialog, setShowVerificationDialog] = useState<boolean>(false);
const [verificationEmail, setVerificationEmail] = useState<string>("");
const [verificationError, setVerificationError] = useState<string>("");
const [isVerifying, setIsVerifying] = useState<boolean>(false);

const mutation = trpc.viewer.public.submitRating.useMutation({
onSuccess: async () => {
Expand Down Expand Up @@ -450,6 +461,87 @@ export default function Success(props: PageProps) {
return isRecurringBooking ? t("meeting_is_scheduled_recurring") : t("meeting_is_scheduled");
})();

const emailParam = useMemo(() => {
return searchParams.get("email");
}, [searchParams]);

const [isEmailVerified, setIsEmailVerified] = useState<boolean>(false);
const [lastVerifiedEmail, setLastVerifiedEmail] = useState<string | null>(null);

const { data: emailVerificationResult, isLoading: isVerifyingEmailParam } =
trpc.viewer.public.verifyBookingEmail.useQuery(
{ bookingUid: bookingInfo.uid, email: emailParam ?? "" },
{ enabled: !!emailParam && !session }
);

useEffect(() => {
if (emailVerificationResult?.isValid && emailParam) {
setIsEmailVerified(true);
setLastVerifiedEmail(emailParam);
}
}, [emailVerificationResult, emailParam]);

useEffect(() => {
if (emailParam !== lastVerifiedEmail) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: Manual email verification is immediately cleared by the new emailParam !== lastVerifiedEmail effect because lastVerifiedEmail is never updated in the manual success path, causing booking info to disappear until revalidation succeeds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/modules/bookings/views/bookings-single-view.tsx, line 485:

<comment>Manual email verification is immediately cleared by the new `emailParam !== lastVerifiedEmail` effect because `lastVerifiedEmail` is never updated in the manual success path, causing booking info to disappear until revalidation succeeds.</comment>

<file context>
@@ -474,10 +475,17 @@ export default function Success(props: PageProps) {
+  }, [emailVerificationResult, emailParam]);
+
+  useEffect(() => {
+    if (emailParam !== lastVerifiedEmail) {
+      setIsEmailVerified(false);
+    }
</file context>
Fix with Cubic

setIsEmailVerified(false);
}
}, [emailParam, lastVerifiedEmail]);

useEffect(() => {
if (sessionStatus === "loading" || isVerifyingEmailParam) return;

const needsVerification = !session && (!emailParam || !emailVerificationResult?.isValid);

setShowVerificationDialog((prev) => {
if (prev === needsVerification) return prev;
return needsVerification;
});
}, [session, sessionStatus, emailParam, emailVerificationResult, isVerifyingEmailParam]);

const updateSearchParams = useCallback(
(email: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set("email", email);
router.replace(`?${params.toString()}`, { scroll: false });
},
[router, searchParams]
);

const verifyEmailMutation = trpc.viewer.public.verifyBookingEmail.useQuery(
{ bookingUid: bookingInfo.uid, email: verificationEmail },
{ enabled: false }
);

const handleVerification = async () => {
setVerificationError("");

const parsed = verificationSchema.safeParse({ email: verificationEmail });
if (!parsed.success) {
setVerificationError(parsed.error.errors[0].message);
return;
}

setIsVerifying(true);
try {
const result = await verifyEmailMutation.refetch();
if (result.data?.isValid) {
setIsEmailVerified(true);
updateSearchParams(verificationEmail);
setShowVerificationDialog(false);
} else {
setVerificationError(t("verification_email_error"));
}
} finally {
setIsVerifying(false);
}
};

const showBookingInfo = useMemo(() => {
if (session) return true;
if (!emailParam) return false;
return isEmailVerified || emailVerificationResult?.isValid;
}, [session, emailParam, isEmailVerified, emailVerificationResult]);

return (
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
{!isEmbed && !isFeedbackMode && (
Expand Down Expand Up @@ -478,7 +570,8 @@ export default function Success(props: PageProps) {
<BookingPageTagManager
eventType={{ ...eventType, metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata) }}
/>
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
{showBookingInfo ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

P1: Email-gated booking details are only hidden in the UI; SSR still delivers full booking data to unauthenticated users

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/modules/bookings/views/bookings-single-view.tsx, line 565:

<comment>Email-gated booking details are only hidden in the UI; SSR still delivers full booking data to unauthenticated users</comment>

<file context>
@@ -478,7 +562,8 @@ export default function Success(props: PageProps) {
         eventType={{ ...eventType, metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata) }}
       />
-      <main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
+      {showBookingInfo ? (
+        <main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
         <div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
</file context>

<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
<div
className={classNames(
Expand Down Expand Up @@ -1112,6 +1205,45 @@ export default function Success(props: PageProps) {
</div>
</div>
</main>
) : (
<Dialog
open={showVerificationDialog}
onOpenChange={() => {
setShowVerificationDialog;
}}
data-testid="verify-email-dialog">
<DialogContent
onPointerDownOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}>
<DialogHeader title={t("verify_email")} />
<div className="space-y-4 py-4">
<p className="text-default text-sm">{t("verification_email_dialog_description")}</p>
<Input
data-testid="verify-email-input"
type="email"
placeholder={t("verification_email_input_placeholder")}
value={verificationEmail}
onChange={(e) => setVerificationEmail(e.target.value)}
className="mb-2"
/>
{verificationError && (
<p data-testid="verify-email-error" className="text-error text-sm">
{verificationError}
</p>
)}
<div className="flex justify-end space-x-2">
<Button
onClick={handleVerification}
disabled={isVerifying}
loading={isVerifying}
data-testid="verify-email-trigger">
{t("verify")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)}
<Toaster position="bottom-right" />
</div>
);
Expand Down
Loading
Loading