import { CheckIcon } from "@heroicons/react/outline"; import { ArrowLeftIcon, ClockIcon, XIcon } from "@heroicons/react/solid"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import classNames from "classnames"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import timezone from "dayjs/plugin/timezone"; import toArray from "dayjs/plugin/toArray"; import utc from "dayjs/plugin/utc"; import { createEvent } from "ics"; import { 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 RRule from "rrule"; import { SpaceBookingSuccessPage } from "@calcom/app-store/spacebooking/components"; import { sdkActionManager, useEmbedNonStylesConfig, useIsBackgroundTransparent, useIsEmbed, } from "@calcom/embed-core"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { localStorage } from "@calcom/lib/webstorage"; import { RecurringEvent } from "@calcom/types/Calendar"; import Button from "@calcom/ui/Button"; import { EmailInput } from "@calcom/ui/form/fields"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { getEventName } from "@lib/event"; import useTheme from "@lib/hooks/useTheme"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable"; import prisma from "@lib/prisma"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { isBrowserLocale24h } from "@lib/timeFormat"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import CustomBranding from "@components/CustomBranding"; import { HeadSeo } from "@components/seo/head-seo"; import { ssrInit } from "@server/lib/ssr"; dayjs.extend(utc); dayjs.extend(toArray); dayjs.extend(timezone); dayjs.extend(localizedFormat); 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 parsedSuccessUrl = new URL(document.URL); const parsedExternalUrl = new URL(url); /* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type for (let [name, value] of parsedExternalUrl.searchParams.entries()) { parsedSuccessUrl.searchParams.set(name, value); } const urlWithSuccessParams = parsedExternalUrl.origin + parsedExternalUrl.pathname + "?" + parsedSuccessUrl.searchParams.toString() + parsedExternalUrl.hash; const { t } = useLocale(); const timerRef = useRef(null); useEffect(() => { 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, urlWithSuccessParams]); if (!isToastVisible) { return null; } return ( <>

Redirecting to {url} ... {t("you_are_being_redirected", { url, seconds: timeRemaining })}

); } type SuccessProps = inferSSRProps; export default function Success(props: SuccessProps) { const { t } = useLocale(); const router = useRouter(); const { location: _location, name, reschedule } = router.query; const location = Array.isArray(_location) ? _location[0] : _location; const [is24h, setIs24h] = useState(isBrowserLocale24h()); const { data: session } = useSession(); const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date))); const { isReady, Theme } = useTheme(props.profile.theme); const { eventType, bookingInfo } = props; const isBackgroundTransparent = useIsBackgroundTransparent(); const isEmbed = useIsEmbed(); const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const attendeeName = typeof name === "string" ? name : "Nameless"; const eventNameObject = { attendeeName, eventType: props.eventType.title, eventName: (props.dynamicEventName as string) || props.eventType.eventName, host: props.profile.name || "Nameless", t, }; const metadata = props.eventType?.metadata as { giphyThankYouPage: string }; const giphyImage = metadata?.giphyThankYouPage; const eventName = getEventName(eventNameObject); const needsConfirmation = eventType.requiresConfirmation && reschedule != "true"; const telemetry = useTelemetry(); useEffect(() => { telemetry.withJitsu((jitsu) => jitsu.track( top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView, collectPageParameters("/success") ) ); }, [telemetry]); useEffect(() => { const users = eventType.users; // TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add sdkActionManager!.fire("bookingSuccessful", { eventType, date: date.toString(), duration: eventType.length, organizer: { name: users[0].name || "Nameless", email: users[0].email || "Email-less", timeZone: users[0].timeZone, }, confirmed: !needsConfirmation, // TODO: Add payment details }); setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess())); setIs24h(!!localStorage.getItem("timeOption.is24hClock")); }, [eventType, needsConfirmation]); function eventLink(): string { const optional: { location?: string } = {}; if (location) { optional["location"] = location; } const event = createEvent({ start: [ date.toDate().getUTCFullYear(), (date.toDate().getUTCMonth() as number) + 1, date.toDate().getUTCDate(), date.toDate().getUTCHours(), date.toDate().getUTCMinutes(), ], startInputType: "utc", title: eventName, description: props.eventType.description ? props.eventType.description : undefined, /** formatted to required type of description ^ */ duration: { minutes: props.eventType.length }, ...optional, }); if (event.error) { throw event.error; } return encodeURIComponent(event.value ? event.value : false); } function getTitle(): string { const titleSuffix = props.recurringBookings ? "_recurring" : ""; if (needsConfirmation) { if (props.profile.name !== null) { return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, { user: props.profile.name, }); } return t("needs_to_be_confirmed_or_rejected" + titleSuffix); } return t("emailed_you_and_attendees" + titleSuffix); } const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id))); const title = t( `booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}` ); return ( (isReady && ( <>
{isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? ( ) : null}{" "}
{/* SPACE BOOKING APP */} {props.userHasSpaceBooking && ( )} )) || null ); } type RecurringBookingsProps = { isReschedule: boolean; eventType: SuccessProps["eventType"]; recurringBookings: SuccessProps["recurringBookings"]; date: dayjs.Dayjs; is24h: boolean; }; function RecurringBookings({ isReschedule = false, eventType, recurringBookings, date, is24h, }: RecurringBookingsProps) { const [moreEventsVisible, setMoreEventsVisible] = useState(false); const { t } = useLocale(); return !isReschedule && recurringBookings ? ( <> {eventType.recurringEvent?.count && recurringBookings.slice(0, 4).map((dateStr, idx) => (
{dayjs(dateStr).format("MMMM DD, YYYY")}
{dayjs(dateStr).format("LT")} - {dayjs(dateStr).add(eventType.length, "m").format("LT")}{" "} ({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
))} {recurringBookings.length > 4 && ( setMoreEventsVisible(!moreEventsVisible)}> {t("plus_more", { count: recurringBookings.length - 4 })} {eventType.recurringEvent?.count && recurringBookings.slice(4).map((dateStr, idx) => (
{dayjs(dateStr).format("MMMM DD, YYYY")}
{dayjs(dateStr).format("LT")} - {dayjs(dateStr).add(eventType.length, "m").format("LT")}{" "} ({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
))}
)} ) : !eventType.recurringEvent.freq ? ( <> {date.format("MMMM DD, YYYY")}
{date.format("LT")} - {date.add(eventType.length, "m").format("LT")}{" "} ({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()}) ) : null; } const getEventTypesFromDB = async (typeId: number) => { return await prisma.eventType.findUnique({ where: { id: typeId, }, select: { id: true, title: true, description: true, length: true, eventName: true, recurringEvent: true, requiresConfirmation: true, userId: true, successRedirectUrl: true, users: { select: { id: true, name: true, hideBranding: true, plan: true, theme: true, brandColor: true, darkBrandColor: true, email: true, timeZone: true, }, }, team: { select: { name: true, hideBranding: true, }, }, metadata: true, }, }); }; export async function getServerSideProps(context: GetServerSidePropsContext) { const ssr = await ssrInit(context); const typeId = parseInt(asStringOrNull(context.query.type) ?? ""); const recurringEventIdQuery = asStringOrNull(context.query.recur); const typeSlug = asStringOrNull(context.query.eventSlug) ?? "15min"; const dynamicEventName = asStringOrNull(context.query.eventName) ?? ""; const bookingId = parseInt(context.query.bookingId as string); if (isNaN(typeId)) { return { notFound: true, }; } let eventTypeRaw = !typeId ? getDefaultEvent(typeSlug) : await getEventTypesFromDB(typeId); if (!eventTypeRaw) { return { notFound: true, }; } let spaceBookingAvailable = false; let userHasSpaceBooking = false; if (eventTypeRaw.users[0] && eventTypeRaw.users[0].id) { const credential = await prisma.credential.findFirst({ where: { type: "spacebooking_other", userId: eventTypeRaw.users[0].id, }, }); if (credential && credential.type === "spacebooking_other") { userHasSpaceBooking = true; } } if (!eventTypeRaw.users.length && eventTypeRaw.userId) { // TODO we should add `user User` relation on `EventType` so this extra query isn't needed const user = await prisma.user.findUnique({ where: { id: eventTypeRaw.userId, }, select: { id: true, name: true, hideBranding: true, plan: true, theme: true, brandColor: true, darkBrandColor: true, email: true, timeZone: true, }, }); if (user) { eventTypeRaw.users.push(user as any); } } if (!eventTypeRaw.users.length) { return { notFound: true, }; } const eventType = { ...eventTypeRaw, recurringEvent: (eventTypeRaw.recurringEvent || {}) as RecurringEvent, }; const profile = { name: eventType.team?.name || eventType.users[0]?.name || null, email: eventType.team ? null : eventType.users[0].email || null, theme: (!eventType.team?.name && eventType.users[0]?.theme) || null, brandColor: eventType.team ? null : eventType.users[0].brandColor || null, darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null, }; const bookingInfo = await prisma.booking.findUnique({ where: { id: bookingId, }, select: { description: true, user: { select: { name: true, email: true, }, }, attendees: { select: { name: true, email: true, }, }, }, }); let recurringBookings = null; if (recurringEventIdQuery) { // We need to get the dates for the bookings to be able to show them in the UI recurringBookings = await prisma.booking.findMany({ where: { recurringEventId: recurringEventIdQuery, }, select: { startTime: true, }, }); } return { props: { hideBranding: eventType.team ? eventType.team.hideBranding : isBrandingHidden(eventType.users[0]), profile, eventType, recurringBookings: recurringBookings ? recurringBookings.map((obj) => obj.startTime.toString()) : null, trpcState: ssr.dehydrate(), dynamicEventName, userHasSpaceBooking, bookingInfo, }, }; }