import type { Payment } from "@prisma/client"; import type { EventType } from "@prisma/client"; import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"; import type { StripeElementLocale } from "@stripe/stripe-js"; import { useRouter } from "next/navigation"; import type { SyntheticEvent } from "react"; import { useEffect, useState } from "react"; import getStripe from "@calcom/app-store/stripepayment/lib/client"; import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, CheckboxField } from "@calcom/ui"; import type { PaymentPageProps } from "../pages/payment"; type Props = { payment: Omit & { data: Record; }; eventType: { id: number; successRedirectUrl: EventType["successRedirectUrl"]; forwardParamsSuccessRedirect: EventType["forwardParamsSuccessRedirect"]; }; user: { username: string | null; }; location?: string | null; clientSecret: string; booking: PaymentPageProps["booking"]; }; type States = | { status: "idle"; } | { status: "processing"; } | { status: "error"; error: Error; } | { status: "ok"; }; const PaymentForm = (props: Props) => { const { user: { username }, } = props; const { t, i18n } = useLocale(); const router = useRouter(); const searchParams = useCompatSearchParams(); const [state, setState] = useState({ status: "idle" }); const [isCanceling, setIsCanceling] = useState(false); const stripe = useStripe(); const elements = useElements(); const paymentOption = props.payment.paymentOption; const [holdAcknowledged, setHoldAcknowledged] = useState(paymentOption === "HOLD" ? false : true); const bookingSuccessRedirect = useBookingSuccessRedirect(); useEffect(() => { elements?.update({ locale: i18n.language as StripeElementLocale }); }, [elements, i18n.language]); const handleSubmit = async (ev: SyntheticEvent) => { ev.preventDefault(); if (!stripe || !elements || searchParams === null) { return; } setState({ status: "processing" }); let payload; const params: { uid: string; email: string | null; location?: string; payment_intent?: string; payment_intent_client_secret?: string; redirect_status?: string; } = { uid: props.booking.uid, email: searchParams?.get("email"), }; if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) { payload = await stripe.confirmSetup({ elements, redirect: "if_required", }); if (payload.setupIntent) { params.payment_intent = payload.setupIntent.id; params.payment_intent_client_secret = payload.setupIntent.client_secret || undefined; params.redirect_status = payload.setupIntent.status; } } else if (paymentOption === "ON_BOOKING") { payload = await stripe.confirmPayment({ elements, redirect: "if_required", }); if (payload.paymentIntent) { params.payment_intent = payload.paymentIntent.id; params.payment_intent_client_secret = payload.paymentIntent.client_secret || undefined; params.redirect_status = payload.paymentIntent.status; } } if (payload?.error) { setState({ status: "error", error: new Error(`Payment failed: ${payload.error.message}`), }); } else { if (props.location) { if (props.location.includes("integration")) { params.location = t("web_conferencing_details_to_follow"); } else { params.location = props.location; } } return bookingSuccessRedirect({ successRedirectUrl: props.eventType.successRedirectUrl, query: params, booking: props.booking, forwardParamsSuccessRedirect: props.eventType.forwardParamsSuccessRedirect, }); } }; const disableButtons = isCanceling || !holdAcknowledged || ["processing", "error"].includes(state.status); return (
setState({ status: "idle" })} />
{paymentOption === "HOLD" && (
setHoldAcknowledged(e.target.checked)} descriptionClassName="text-info font-semibold" />
)}
{state.status === "error" && (
{state.error.message}
)}
); }; export default function PaymentComponent(props: Props) { const stripePromise = getStripe(props.payment.data.stripe_publishable_key as any); const [theme, setTheme] = useState<"stripe" | "night">("stripe"); useEffect(() => { if (document.documentElement.classList.contains("dark")) { setTheme("night"); } }, []); return ( ); }