Improvement: Do immediate redirect for "Redirect on Booking" (#7947)
Co-authored-by: Alex van Andel <me@alexvanandel.com>pull/8016/head^2
parent
11789c725a
commit
1758288948
|
@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
|
|||
import { getEventName } from "@calcom/core/event";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
setRedirectUrlVisible(e);
|
||||
onChange(e ? value : "");
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||
<div className="lg:-ml-2">
|
||||
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||
<div className="lg:-ml-2 lg:-mb-2">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label={t("redirect_success_booking")}
|
||||
labelSrOnly
|
||||
placeholder={t("external_redirect_url")}
|
||||
|
@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
defaultValue={eventType.successRedirectUrl || ""}
|
||||
{...formMethods.register("successRedirectUrl")}
|
||||
/>
|
||||
<div className="mt-2 flex">
|
||||
<Checkbox
|
||||
description={t("disable_success_page")}
|
||||
// Disable if it's not Self Hosted or if the redirect url is not set
|
||||
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
||||
{...formMethods.register("metadata.disableSuccessPage")}
|
||||
/>
|
||||
{/*TODO: Extract it out into a component when used more than once*/}
|
||||
{!IS_SELF_HOSTED && (
|
||||
<Link href="https://cal.com/pricing" target="_blank">
|
||||
<Badge variant="orange" className="ml-2">
|
||||
Platform Only
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"p-1 text-sm text-orange-600",
|
||||
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
|
||||
)}>
|
||||
{t("redirect_url_warning")}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RRule } from "rrule";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
|||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
function redirectToExternalUrl(url: string) {
|
||||
window.parent.location.href = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to external URL with query params from current URL.
|
||||
* Query Params and Hash Fragment if present in external URL are kept intact.
|
||||
*/
|
||||
function RedirectionToast({ url }: { url: string }) {
|
||||
const [timeRemaining, setTimeRemaining] = useState(10);
|
||||
const [isToastVisible, setIsToastVisible] = useState(true);
|
||||
|
||||
const { t } = useLocale();
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const router = useRouter();
|
||||
const { cancel: isCancellationMode } = querySchema.parse(router.query);
|
||||
const urlWithSuccessParamsRef = useRef<string | null>();
|
||||
if (isCancellationMode && timerRef.current) {
|
||||
setIsToastVisible(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isToastVisible && timerRef.current) {
|
||||
window.clearInterval(timerRef.current);
|
||||
}
|
||||
}, [isToastVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const parsedExternalUrl = new URL(url);
|
||||
|
||||
const parsedSuccessUrl = new URL(document.URL);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
|
||||
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
|
||||
parsedSuccessUrl.searchParams.set(name, value);
|
||||
}
|
||||
|
||||
const urlWithSuccessParams =
|
||||
parsedExternalUrl.origin +
|
||||
parsedExternalUrl.pathname +
|
||||
"?" +
|
||||
parsedSuccessUrl.searchParams.toString() +
|
||||
parsedExternalUrl.hash;
|
||||
urlWithSuccessParamsRef.current = urlWithSuccessParams;
|
||||
|
||||
timerRef.current = window.setInterval(() => {
|
||||
if (timeRemaining > 0) {
|
||||
setTimeRemaining((timeRemaining) => {
|
||||
return timeRemaining - 1;
|
||||
});
|
||||
} else {
|
||||
redirectToExternalUrl(urlWithSuccessParams);
|
||||
window.clearInterval(timerRef.current as number);
|
||||
}
|
||||
}, 1000);
|
||||
return () => {
|
||||
window.clearInterval(timerRef.current as number);
|
||||
};
|
||||
}, [timeRemaining, url]);
|
||||
|
||||
if (!isToastVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-[60] pb-2 sm:pb-5">
|
||||
<div className="mx-auto w-full sm:max-w-7xl sm:px-2 lg:px-8">
|
||||
<div className="border border-green-600 bg-green-500 p-2 sm:p-3">
|
||||
<div className="flex flex-wrap items-center justify-between">
|
||||
<div className="flex w-0 flex-1 items-center">
|
||||
<p className="truncate font-medium text-white sm:mx-3">
|
||||
<span className="md:hidden">Redirecting to {url} ...</span>
|
||||
<span className="hidden md:inline">
|
||||
{t("you_are_being_redirected", { url, seconds: timeRemaining })}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (urlWithSuccessParamsRef.current) {
|
||||
redirectToExternalUrl(urlWithSuccessParamsRef.current);
|
||||
}
|
||||
}}
|
||||
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
|
||||
{t("continue")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsToastVisible(false);
|
||||
}}
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<FiX className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
const stringToBoolean = z
|
||||
|
@ -176,13 +68,14 @@ const stringToBoolean = z
|
|||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
allRemainingBookings: stringToBoolean,
|
||||
email: z.string().optional(),
|
||||
eventTypeSlug: z.string().optional(),
|
||||
cancel: stringToBoolean,
|
||||
allRemainingBookings: stringToBoolean,
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: stringToBoolean,
|
||||
formerTime: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
});
|
||||
|
||||
|
@ -404,9 +297,6 @@ export default function Success(props: SuccessProps) {
|
|||
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
|
||||
<RedirectionToast url={eventType.successRedirectUrl} />
|
||||
) : null}{" "}
|
||||
<div
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "text-center" : "",
|
||||
|
@ -981,13 +871,6 @@ const getEventTypesFromDB = async (id: number) => {
|
|||
};
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
uid: z.string(),
|
||||
email: z.string().optional(),
|
||||
eventTypeSlug: z.string().optional(),
|
||||
cancel: stringToBoolean,
|
||||
});
|
||||
|
||||
const handleSeatsEventTypeOnBooking = (
|
||||
eventType: {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
|
@ -1017,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
|
|||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const ssr = await ssrInit(context);
|
||||
const parsedQuery = schema.safeParse(context.query);
|
||||
const parsedQuery = querySchema.safeParse(context.query);
|
||||
if (!parsedQuery.success) return { notFound: true };
|
||||
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
|
||||
const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
|
||||
|
||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||
where: {
|
||||
|
@ -1116,7 +999,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||
};
|
||||
|
||||
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
|
||||
if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: eventType.successRedirectUrl,
|
||||
|
|
|
@ -1689,6 +1689,7 @@
|
|||
"events_rescheduled": "Events Rescheduled",
|
||||
"from_last_period": "from last period",
|
||||
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
|
||||
"analytics_for_organisation": "Insights for {{organisationName}}",
|
||||
"subtitle_analytics": "Learn more about your team's activity",
|
||||
"event_trends": "Event Trends",
|
||||
|
|
Loading…
Reference in New Issue