diff --git a/apps/web/components/booking/CancelBooking.tsx b/apps/web/components/booking/CancelBooking.tsx index a521515eb5fcf1..738cf566341df0 100644 --- a/apps/web/components/booking/CancelBooking.tsx +++ b/apps/web/components/booking/CancelBooking.tsx @@ -9,7 +9,8 @@ import { useTelemetry } from "@calcom/lib/hooks/useTelemetry"; import { collectPageParameters, telemetryEventTypes } from "@calcom/lib/telemetry"; import type { RecurringEvent } from "@calcom/types/Calendar"; import { Button } from "@calcom/ui/components/button"; -import { Label, Select, TextArea } from "@calcom/ui/components/form"; +import { Dialog, DialogContent, DialogHeader } from "@calcom/ui/components/dialog"; +import { Input, Label, Select, TextArea } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; interface InternalNotePresetsSelectProps { @@ -76,6 +77,8 @@ type Props = { title?: string; uid?: string; id?: number; + userEmail?: string; + attendees?: { email: string }[]; }; profile: { name: string | null; @@ -118,6 +121,9 @@ export default function CancelBooking(props: Props) { const telemetry = useTelemetry(); const [error, setError] = useState(booking ? null : t("booking_already_cancelled")); const [internalNote, setInternalNote] = useState<{ id: number; name: string } | null>(null); + const [showVerificationDialog, setShowVerificationDialog] = useState(false); + const [verificationEmail, setVerificationEmail] = useState(""); + const [verificationError, setVerificationError] = useState(""); const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => { if (node !== null) { @@ -128,6 +134,74 @@ export default function CancelBooking(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const handleCancellation = async () => { + setLoading(true); + + telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters()); + + 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, + }), + 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(); + } else { + setLoading(false); + setError(`${t("error_with_status_code_occured", { status: res.status })} ${t("please_try_again")}`); + } + }; + + const verifyAndCancel = () => { + if (!currentUserEmail) { + setShowVerificationDialog(true); + return; + } + + handleCancellation(); + }; + + const handleVerification = () => { + setVerificationError(""); + if (!verificationEmail) { + setVerificationError(t("email_required")); + return; + } + + if ( + booking.attendees?.some( + (attendee) => attendee.email.toLowerCase() === verificationEmail.toLowerCase() + ) || + verificationEmail.toLowerCase() === booking?.userEmail?.toLowerCase() + ) { + setShowVerificationDialog(false); + handleCancellation(); + } else { + setVerificationError(t("proceed_with_cancellation_error")); + } + }; + return ( <> {error && ( @@ -182,7 +256,7 @@ export default function CancelBooking(props: Props) { {props.isHost ? (
-

+

{t("notify_attendee_cancellation_reason_warning")}

@@ -201,48 +275,7 @@ export default function CancelBooking(props: Props) { props.isHost && (!cancellationReason?.trim() || (props.internalNotePresets.length > 0 && !internalNote?.id)) } - onClick={async () => { - setLoading(true); - - telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters()); - - 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, - }), - 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(); - } else { - setLoading(false); - setError( - `${t("error_with_status_code_occured", { status: res.status })} ${t( - "please_try_again" - )}` - ); - } - }} + onClick={verifyAndCancel} loading={loading}> {props.allRemainingBookings ? t("cancel_all_remaining") : t("cancel_event")} @@ -250,6 +283,34 @@ export default function CancelBooking(props: Props) { )} + + + +
+

{t("proceed_with_cancellation_description")}

+ setVerificationEmail(e.target.value)} + className="mb-2" + /> + {verificationError &&

{verificationError}

} +
+ + +
+
+
+
); } diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index f5372f603b86bf..ab6cd5611455b6 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -185,7 +185,6 @@ export default function Success(props: PageProps) { searchParams?.get("cancelledBy") ?? session?.user?.email ?? undefined; - const defaultRating = isNaN(parsedRating) ? 3 : parsedRating > 5 ? 5 : parsedRating < 1 ? 1 : parsedRating; const [rateValue, setRateValue] = useState(defaultRating); const [isFeedbackSubmitted, setIsFeedbackSubmitted] = useState(false); @@ -859,6 +858,8 @@ export default function Success(props: PageProps) { uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id, + attendees: bookingInfo?.attendees, + userEmail: bookingInfo?.user?.email as string, }} profile={{ name: props.profile.name, slug: props.profile.slug }} recurringEvent={eventType.recurringEvent} diff --git a/packages/lib/server/locales/en/common.json b/packages/lib/server/locales/en/common.json index b9103f0d9a97c5..f81af622d41690 100644 --- a/packages/lib/server/locales/en/common.json +++ b/packages/lib/server/locales/en/common.json @@ -2500,6 +2500,9 @@ "extensive_whitelabeling": "Extensive Whitelabeling", "extensive_whitelabeling_description": "Customize your scheduling experience with your own logo, colors, and more", "unlimited_teams": "Unlimited Teams", + "verify_email": "Verify email", + "proceed_with_cancellation_description" : "Please enter the email address used for this booking to proceed with cancellation", + "proceed_with_cancellation_error" : "Email doesn't match the booking email", "unlimited_teams_description": "Add as many subteams as you need to your organization", "unified_billing": "Unified Billing", "advanced_managed_events": "Advanced Managed Event Types",