import { CalendarIcon, ClockIcon, CreditCardIcon, ExclamationIcon } from "@heroicons/react/solid"; import { EventTypeCustomInputType } from "@prisma/client"; import { useContracts } from "contexts/contractsContext"; import dayjs from "dayjs"; import dynamic from "next/dynamic"; import Head from "next/head"; import { useRouter } from "next/router"; import React, { useEffect, useMemo, useState } from "react"; import { Controller, useForm, useWatch } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; import { v4 as uuidv4 } from "uuid"; import { createPaymentLink } from "@ee/lib/stripe/client"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { ensureArray } from "@lib/ensureArray"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; import { parseZone } from "@lib/parseZone"; import slugify from "@lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { detectBrowserTimeFormat } from "@lib/timeFormat"; import CustomBranding from "@components/CustomBranding"; import { EmailInput, Form } from "@components/form/fields"; import AvatarGroup from "@components/ui/AvatarGroup"; import { Button } from "@components/ui/Button"; import { BookPageProps } from "../../../pages/[user]/book"; import { TeamBookingPageProps } from "../../../pages/team/[slug]/book"; /** These are like 40kb that not every user needs */ const PhoneInput = dynamic(() => import("@components/ui/form/PhoneInput")); type BookingPageProps = BookPageProps | TeamBookingPageProps; type BookingFormValues = { name: string; email: string; notes?: string; locationType?: LocationType; guests?: string[]; phone?: string; customInputs?: { [key: string]: string; }; }; const BookingPage = (props: BookingPageProps) => { const { t, i18n } = useLocale(); const router = useRouter(); const { contracts } = useContracts(); const { eventType } = props; useEffect(() => { if (eventType.metadata.smartContractAddress) { const eventOwner = eventType.users[0]; if (!contracts[(eventType.metadata.smartContractAddress || null) as number]) /* @ts-ignore */ router.replace(`/${eventOwner.username}`); } }, [contracts, eventType.metadata.smartContractAddress, router]); const mutation = useMutation(createBooking, { onSuccess: async (responseData) => { const { attendees, paymentUid } = responseData; if (paymentUid) { return await router.push( createPaymentLink({ paymentUid, date, name: attendees[0].name, absolute: false, }) ); } const location = (function humanReadableLocation(location) { if (!location) { return; } if (location.includes("integration")) { return t("web_conferencing_details_to_follow"); } return location; })(responseData.location); return router.push({ pathname: "/success", query: { date, type: props.eventType.id, user: props.profile.slug, reschedule: !!rescheduleUid, name: attendees[0].name, email: attendees[0].email, location, }, }); }, }); const rescheduleUid = router.query.rescheduleUid as string; const { isReady, Theme } = useTheme(props.profile.theme); const date = asStringOrNull(router.query.date); const [guestToggle, setGuestToggle] = useState(props.booking && props.booking.attendees.length > 1); const eventTypeDetail = { isWeb3Active: false, ...props.eventType }; type Location = { type: LocationType; address?: string }; // it would be nice if Prisma at some point in the future allowed for Json; as of now this is not the case. const locations: Location[] = useMemo( () => (props.eventType.locations as Location[]) || [], [props.eventType.locations] ); useEffect(() => { if (router.query.guest) { setGuestToggle(true); } }, [router.query.guest]); const telemetry = useTelemetry(); const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); // TODO: Move to translations const locationLabels = { [LocationType.InPerson]: t("in_person_meeting"), [LocationType.Phone]: t("phone_call"), [LocationType.GoogleMeet]: "Google Meet", [LocationType.Zoom]: "Zoom Video", [LocationType.Jitsi]: "Jitsi Meet", [LocationType.Daily]: "Daily.co Video", [LocationType.Huddle01]: "Huddle01 Video", [LocationType.Tandem]: "Tandem Video", }; const defaultValues = () => { if (!rescheduleUid) { return { name: (router.query.name as string) || "", email: (router.query.email as string) || "", notes: (router.query.notes as string) || "", guests: ensureArray(router.query.guest) as string[], customInputs: props.eventType.customInputs.reduce( (customInputs, input) => ({ ...customInputs, [input.id]: router.query[slugify(input.label)], }), {} ), }; } if (!props.booking || !props.booking.attendees.length) { return {}; } const primaryAttendee = props.booking.attendees[0]; if (!primaryAttendee) { return {}; } return { name: primaryAttendee.name || "", email: primaryAttendee.email || "", guests: props.booking.attendees.slice(1).map((attendee) => attendee.email), }; }; const bookingForm = useForm({ defaultValues: defaultValues(), }); const selectedLocation = useWatch({ control: bookingForm.control, name: "locationType", defaultValue: ((): LocationType | undefined => { if (router.query.location) { return router.query.location as LocationType; } if (locations.length === 1) { return locations[0]?.type; } })(), }); const getLocationValue = (booking: Pick) => { const { locationType } = booking; switch (locationType) { case LocationType.Phone: { return booking.phone || ""; } case LocationType.InPerson: { return locationInfo(locationType)?.address || ""; } // Catches all other location types, such as Google Meet, Zoom etc. default: return selectedLocation || ""; } }; const parseDate = (date: string | null) => { if (!date) return "No date"; const parsedZone = parseZone(date); if (!parsedZone?.isValid()) return "Invalid date"; const formattedTime = parsedZone?.format(detectBrowserTimeFormat); return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" }); }; const bookEvent = (booking: BookingFormValues) => { telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters()) ); // "metadata" is a reserved key to allow for connecting external users without relying on the email address. // <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type. // @TODO: move to metadata const metadata = Object.keys(router.query) .filter((key) => key.startsWith("metadata")) .reduce( (metadata, key) => ({ ...metadata, [key.substring("metadata[".length, key.length - 1)]: router.query[key], }), {} ); let web3Details; if (eventTypeDetail.metadata.smartContractAddress) { web3Details = { // @ts-ignore userWallet: window.web3.currentProvider.selectedAddress, userSignature: contracts[(eventTypeDetail.metadata.smartContractAddress || null) as number], }; } mutation.mutate({ ...booking, web3Details, start: dayjs(date).format(), end: dayjs(date).add(props.eventType.length, "minute").format(), eventTypeId: props.eventType.id, timeZone: timeZone(), language: i18n.language, rescheduleUid, user: router.query.user, location: getLocationValue(booking.locationType ? booking : { locationType: selectedLocation }), metadata, customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ label: props.eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label, value: booking.customInputs![inputId], })), }); }; return (
{rescheduleUid ? t("booking_reschedule_confirmation", { eventTypeTitle: props.eventType.title, profileName: props.profile.name, }) : t("booking_confirmation", { eventTypeTitle: props.eventType.title, profileName: props.profile.name, })}{" "} | Cal.com
{isReady && (
user.name !== props.profile.name) .map((user) => ({ image: user.avatar || "", alt: user.name || "", })) )} />

{props.profile.name}

{props.eventType.title}

{props.eventType.length} {t("minutes")}

{props.eventType.price > 0 && (

)}

{parseDate(date)}

{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (

{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}

)}

{props.eventType.description}

{locations.length > 1 && (
{t("location")} {locations.map((location, i) => ( ))}
)} {selectedLocation === LocationType.Phone && (
{/* @ts-ignore */}
)} {props.eventType.customInputs .sort((a, b) => a.id - b.id) .map((input) => (
{input.type !== EventTypeCustomInputType.BOOL && ( )} {input.type === EventTypeCustomInputType.TEXTLONG && (