diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 4baccf7a35..eb7cc34535 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -118,7 +118,7 @@ function BookingListItem(booking: BookingItemProps) { label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"), /* When cancelling we need to let the UI and the API know if the intention is to cancel all remaining bookings or just that booking instance. */ - href: `/success?uid=${booking.uid}&cancel=true${ + href: `/booking/${booking.uid}?cancel=true${ isTabRecurring && isRecurring ? "&allRemainingBookings=true" : "" }`, icon: Icon.FiX, @@ -195,11 +195,9 @@ function BookingListItem(booking: BookingItemProps) { const onClickTableData = () => { router.push({ - pathname: "/success", + pathname: `/booking/${booking.uid}`, query: { - uid: booking.uid, allRemainingBookings: isTabRecurring, - listingStatus: booking.listingStatus, email: booking.attendees[0] ? booking.attendees[0].email : undefined, }, }); diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 5404f51b55..d15a239b77 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -136,9 +136,8 @@ const BookingPage = ({ } return router.push({ - pathname: "/success", + pathname: `/booking/${uid}`, query: { - uid, isSuccessBookingPage: true, email: bookingForm.getValues("email"), eventTypeSlug: eventType.slug, @@ -152,9 +151,8 @@ const BookingPage = ({ const { uid } = responseData[0] || {}; return router.push({ - pathname: "/success", + pathname: `/booking/${uid}`, query: { - uid, allRemainingBookings: true, email: bookingForm.getValues("email"), eventTypeSlug: eventType.slug, diff --git a/apps/web/components/dialog/RescheduleDialog.tsx b/apps/web/components/dialog/RescheduleDialog.tsx index 3f6b20d59a..a81df1531c 100644 --- a/apps/web/components/dialog/RescheduleDialog.tsx +++ b/apps/web/components/dialog/RescheduleDialog.tsx @@ -47,7 +47,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
-

{t("reschedule_modal_description")}

+

{t("reschedule_modal_description")}

{t("reason_for_reschedule_request")} (Optional) diff --git a/apps/web/lib/hooks/usePublicPage.ts b/apps/web/lib/hooks/usePublicPage.ts index 90c88c90c1..d10ae13a25 100644 --- a/apps/web/lib/hooks/usePublicPage.ts +++ b/apps/web/lib/hooks/usePublicPage.ts @@ -2,7 +2,7 @@ import { useRouter } from "next/router"; export default function usePublicPage() { const router = useRouter(); - const isPublicPage = ["/[user]", "/success", "/cancel", "/reschedule"].find((route) => + const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) => router.pathname.startsWith(route) ); return isPublicPage; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 302798ffef..902d6289e4 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -149,6 +149,21 @@ const nextConfig = { source: "/router", destination: "/apps/routing-forms/router", }, + { + source: "/success/:path*", + has: [ + { + type: "query", + key: "uid", + value: "(?.*)", + }, + ], + destination: "/booking/:uid/:path*", + }, + { + source: "/cancel/:path*", + destination: "/booking/:path*", + }, /* TODO: have these files being served from another deployment or CDN { source: "/embed/embed.js", destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?, diff --git a/apps/web/pages/404.tsx b/apps/web/pages/404.tsx index 3dd1d8428f..952a9c5ffc 100644 --- a/apps/web/pages/404.tsx +++ b/apps/web/pages/404.tsx @@ -38,7 +38,7 @@ export default function Custom404() { setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`); }, [username]); - const isSuccessPage = router.asPath.startsWith("/success"); + const isSuccessPage = router.asPath.startsWith("/booking"); const isSubpage = router.asPath.includes("/", 2) || isSuccessPage; const isSignup = router.asPath.startsWith("/signup"); const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com"; diff --git a/apps/web/pages/success.tsx b/apps/web/pages/booking/[uid].tsx similarity index 99% rename from apps/web/pages/success.tsx rename to apps/web/pages/booking/[uid].tsx index d02aa7d3db..b2f1b57211 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -147,6 +147,7 @@ const querySchema = z.object({ uid: z.string(), allRemainingBookings: stringToBoolean, cancel: stringToBoolean, + changes: stringToBoolean, reschedule: stringToBoolean, isSuccessBookingPage: z.string().optional(), }); @@ -159,9 +160,10 @@ export default function Success(props: SuccessProps) { allRemainingBookings, isSuccessBookingPage, cancel: isCancellationMode, + changes, } = querySchema.parse(router.query); - if (isCancellationMode && typeof window !== "undefined") { + if ((isCancellationMode || changes) && typeof window !== "undefined") { window.scrollTo(0, document.body.scrollHeight); } const location: ReturnType = Array.isArray(props.bookingInfo.location) @@ -221,7 +223,7 @@ export default function Success(props: SuccessProps) { useEffect(() => { if (top !== window) { //page_view will be collected automatically by _middleware.ts - telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/success")); + telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking")); } }, [telemetry]); diff --git a/apps/web/pages/cancel/[uid].tsx b/apps/web/pages/cancel/[uid].tsx deleted file mode 100644 index 3162d06336..0000000000 --- a/apps/web/pages/cancel/[uid].tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { GetServerSidePropsContext } from "next"; -import z from "zod"; - -const querySchema = z.object({ - uid: z.string(), - allRemainingBookings: z - .string() - .optional() - .transform((val) => (val ? JSON.parse(val) : false)), -}); - -export default function Type() { - return <>; -} - -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const { allRemainingBookings, uid } = querySchema.parse(context.query); - return { - redirect: { - permanent: false, - destination: `/success?uid=${uid}&allRemainingBookings=${allRemainingBookings}&cancel=true`, - }, - }; -}; diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index fbfab86433..cd247bc3f3 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -93,7 +93,7 @@ test.describe("pro user", () => { await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.waitForNavigation({ url(url) { - return url.pathname === "/success"; + return url.pathname.startsWith("/booking"); }, }); }); @@ -108,10 +108,9 @@ test.describe("pro user", () => { await page.locator('[data-testid="cancel"]').first().click(); await page.waitForNavigation({ url: (url) => { - return url.pathname.startsWith("/success"); + return url.pathname.startsWith("/booking"); }, }); - // --- fill form await page.locator('[data-testid="cancel"]').click(); const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText(); diff --git a/apps/web/playwright/dynamic-booking-pages.e2e.ts b/apps/web/playwright/dynamic-booking-pages.e2e.ts index bf4e7e0f58..148f755edc 100644 --- a/apps/web/playwright/dynamic-booking-pages.e2e.ts +++ b/apps/web/playwright/dynamic-booking-pages.e2e.ts @@ -37,7 +37,7 @@ test("dynamic booking", async ({ page, users }) => { await page.locator('[data-testid="confirm-reschedule-button"]').click(); await page.waitForNavigation({ url(url) { - return url.pathname === "/success"; + return url.pathname.startsWith("/booking"); }, }); await expect(page.locator("[data-testid=success-page]")).toBeVisible(); @@ -48,10 +48,9 @@ test("dynamic booking", async ({ page, users }) => { await page.locator('[data-testid="cancel"]').first().click(); await page.waitForNavigation({ url: (url) => { - return url.pathname.startsWith("/success"); + return url.pathname.startsWith("/booking"); }, }); - // --- fill form await page.locator('[data-testid="cancel"]').click(); const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText(); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index b55b45c0c0..ae898ff789 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -112,7 +112,7 @@ async function bookEventOnThisPage(page: Page) { // Make sure we're navigated to the success page await page.waitForNavigation({ url(url) { - return url.pathname.endsWith("/success"); + return url.pathname.startsWith("/booking"); }, }); await expect(page.locator("[data-testid=success-page]")).toBeVisible(); diff --git a/apps/web/playwright/reschedule.e2e.ts b/apps/web/playwright/reschedule.e2e.ts index 49edcefceb..7ad35676df 100644 --- a/apps/web/playwright/reschedule.e2e.ts +++ b/apps/web/playwright/reschedule.e2e.ts @@ -150,7 +150,7 @@ test.describe("Reschedule Tests", async () => { await page.locator('[data-testid="confirm-reschedule-button"]').click(); - await expect(page).toHaveURL(/.*success/); + await expect(page).toHaveURL(/.*booking/); await payment.delete(); }); @@ -168,7 +168,7 @@ test.describe("Reschedule Tests", async () => { await page.locator('[data-testid="confirm-reschedule-button"]').click(); - await expect(page).toHaveURL(/.*success/); + await expect(page).toHaveURL(/.*booking/); const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } }); expect(newBooking).not.toBeNull(); @@ -189,7 +189,7 @@ test.describe("Reschedule Tests", async () => { await page.locator('[data-testid="confirm-reschedule-button"]').click(); - await expect(page).toHaveURL(/.*success/); + await expect(page).toHaveURL(/.*booking/); const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } }); expect(newBooking).not.toBeNull(); diff --git a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts index a30131d4f4..f8366c6d4e 100644 --- a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts +++ b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts @@ -1,7 +1,7 @@ import { createEvent, DateArray, Person } from "ics"; import dayjs from "@calcom/dayjs"; -import { getCancelLink } from "@calcom/lib/CalEventParser"; +import { getManageLink } from "@calcom/lib/CalEventParser"; import type { CalendarEvent } from "@calcom/types/Calendar"; import { renderEmail } from ".."; @@ -89,7 +89,7 @@ ${this.t("request_reschedule_subtitle", { })}, ${this.getWhen()} ${this.t("need_to_reschedule_or_cancel")} -${getCancelLink(this.calEvent)} +${getManageLink(this.calEvent)} `.replace(/(<([^>]+)>)/gi, ""); } } diff --git a/packages/features/ee/payments/components/Payment.tsx b/packages/features/ee/payments/components/Payment.tsx index 8d15682d45..39cd3bcd49 100644 --- a/packages/features/ee/payments/components/Payment.tsx +++ b/packages/features/ee/payments/components/Payment.tsx @@ -81,9 +81,7 @@ export default function PaymentComponent(props: Props) { error: new Error(`Payment failed: ${payload.error.message}`), }); } else { - const params: { [k: string]: any } = { - uid: props.bookingUid, - }; + const params: { [k: string]: any } = {}; if (props.location) { if (props.location.includes("integration")) { @@ -94,7 +92,7 @@ export default function PaymentComponent(props: Props) { } const query = stringify(params); - const successUrl = `/success?${query}`; + const successUrl = `/booking/${props.bookingUid}?${query}`; await router.push(successUrl); } diff --git a/packages/lib/CalEventParser.ts b/packages/lib/CalEventParser.ts index 358a1a8698..fe867db412 100644 --- a/packages/lib/CalEventParser.ts +++ b/packages/lib/CalEventParser.ts @@ -123,21 +123,20 @@ export const getProviderName = (calEvent: CalendarEvent): string => { return ""; }; -export const getManageLink = (calEvent: CalendarEvent) => { - return ` -${calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} -${getCancelLink(calEvent)} - `; -}; - export const getUid = (calEvent: CalendarEvent): string => { return calEvent.uid ?? translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); }; +export const getManageLink = (calEvent: CalendarEvent) => { + return ` +${calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} +${WEBAPP_URL + "/booking/" + getUid(calEvent) + "?changes=true"} + `; +}; + export const getCancelLink = (calEvent: CalendarEvent): string => { return ( - WEBAPP_URL + - `/success?uid=${getUid(calEvent)}&cancel=true&allRemainingBookings=${!!calEvent.recurringEvent}` + WEBAPP_URL + `/booking/${getUid(calEvent)}?cancel=true&allRemainingBookings=${!!calEvent.recurringEvent}` ); };