Change Stripe `<CardElement />` to `<PaymentElement />` (#8268)
* Use Stripe PaymentElement * Only show needs confirmation, if the booking needs it * Clean up * Type fix * More type fixespull/8035/head
parent
5339a489f5
commit
f00b112792
|
@ -177,7 +177,7 @@ export default function Success(props: SuccessProps) {
|
|||
// - Event Type has require confirmation option enabled always
|
||||
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||
// - It's a paid event and payment is pending.
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
|
||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||
|
||||
const isCancelled =
|
||||
|
|
|
@ -1,37 +1,20 @@
|
|||
import type { Payment } from "@prisma/client";
|
||||
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||
import type { StripeCardElementChangeEvent, StripeElementLocale } from "@stripe/stripe-js";
|
||||
import { useElements, useStripe, PaymentElement, Elements } from "@stripe/react-stripe-js";
|
||||
import type stripejs from "@stripe/stripe-js";
|
||||
import type { StripeElementLocale } from "@stripe/stripe-js";
|
||||
import { useRouter } from "next/router";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import type { StripePaymentData, StripeSetupIntentData } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Checkbox } from "@calcom/ui";
|
||||
|
||||
import type { EventType } from ".prisma/client";
|
||||
|
||||
const CARD_OPTIONS: stripejs.StripeCardElementOptions = {
|
||||
iconStyle: "solid" as const,
|
||||
classes: {
|
||||
base: "block p-2 w-full border-solid border-2 border-default rounded-md dark:bg-black dark:text-inverted dark:border-black focus-within:ring-black focus-within:border-black text-sm",
|
||||
},
|
||||
style: {
|
||||
base: {
|
||||
color: "#666",
|
||||
iconColor: "#666",
|
||||
fontFamily: "ui-sans-serif, system-ui",
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "16px",
|
||||
"::placeholder": {
|
||||
color: "#888888",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
payment: Omit<Payment, "id" | "fee" | "success" | "refunded" | "externalId" | "data"> & {
|
||||
data: StripePaymentData | StripeSetupIntentData;
|
||||
|
@ -49,7 +32,7 @@ type States =
|
|||
| { status: "error"; error: Error }
|
||||
| { status: "ok" };
|
||||
|
||||
export default function PaymentComponent(props: Props) {
|
||||
const PaymentForm = (props: Props) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const [state, setState] = useState<States>({ status: "idle" });
|
||||
|
@ -62,20 +45,10 @@ export default function PaymentComponent(props: Props) {
|
|||
elements?.update({ locale: i18n.language as StripeElementLocale });
|
||||
}, [elements, i18n.language]);
|
||||
|
||||
const handleChange = async (event: StripeCardElementChangeEvent) => {
|
||||
// Listen for changes in the CardElement
|
||||
// and display any errors as the customer types their card details
|
||||
setState({ status: "idle" });
|
||||
if (event.error)
|
||||
setState({ status: "error", error: new Error(event.error?.message || t("missing_card_fields")) });
|
||||
};
|
||||
|
||||
const handleSubmit = async (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!stripe || !elements || !router.isReady) return;
|
||||
const card = elements.getElement(CardElement);
|
||||
if (!card) return;
|
||||
setState({ status: "processing" });
|
||||
|
||||
let payload;
|
||||
|
@ -85,16 +58,18 @@ export default function PaymentComponent(props: Props) {
|
|||
};
|
||||
if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) {
|
||||
const setupIntentData = props.payment.data as unknown as StripeSetupIntentData;
|
||||
payload = await stripe.confirmCardSetup(setupIntentData.setupIntent.client_secret!, {
|
||||
payment_method: {
|
||||
card,
|
||||
payload = await stripe.confirmSetup({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${CAL_URL}/booking/${props.bookingUid}`,
|
||||
},
|
||||
});
|
||||
} else if (paymentOption === "ON_BOOKING") {
|
||||
const paymentData = props.payment.data as unknown as StripePaymentData;
|
||||
payload = await stripe.confirmCardPayment(paymentData.client_secret!, {
|
||||
payment_method: {
|
||||
card,
|
||||
payload = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${CAL_URL}/booking/${props.bookingUid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -120,15 +95,12 @@ export default function PaymentComponent(props: Props) {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="payment-form" className="bg-subtle mt-4 rounded-md p-6" onSubmit={handleSubmit}>
|
||||
<p className="font-semibold">{t("card_details")}</p>
|
||||
<CardElement
|
||||
className="my-5 bg-white p-2"
|
||||
id="card-element"
|
||||
options={CARD_OPTIONS}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div>
|
||||
<PaymentElement onChange={() => setState({ status: "idle" })} />
|
||||
</div>
|
||||
{paymentOption === "HOLD" && (
|
||||
<div className="bg-info mt-2 mb-5 rounded-md p-3">
|
||||
<Checkbox
|
||||
|
@ -149,12 +121,11 @@ export default function PaymentComponent(props: Props) {
|
|||
<span id="button-text">{t("cancel")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={!holdAcknowledged || ["processing", "error"].includes(state.status)}
|
||||
loading={state.status === "processing"}
|
||||
id="submit"
|
||||
className="border-subtle border">
|
||||
color="secondary">
|
||||
<span id="button-text">
|
||||
{state.status === "processing" ? (
|
||||
<div className="spinner" id="spinner" />
|
||||
|
@ -173,4 +144,47 @@ export default function PaymentComponent(props: Props) {
|
|||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ELEMENT_STYLES: stripejs.Appearance = {
|
||||
theme: "none",
|
||||
};
|
||||
|
||||
const ELEMENT_STYLES_DARK: stripejs.Appearance = {
|
||||
theme: "night",
|
||||
variables: {
|
||||
colorText: "#d6d6d6",
|
||||
fontWeightNormal: "600",
|
||||
borderRadius: "6px",
|
||||
colorBackground: "#101010",
|
||||
colorPrimary: "#d6d6d6",
|
||||
},
|
||||
};
|
||||
|
||||
export default function PaymentComponent(props: Props) {
|
||||
const stripePromise = getStripe((props.payment.data as StripePaymentData).stripe_publishable_key);
|
||||
const paymentOption = props.payment.paymentOption;
|
||||
const [darkMode, setDarkMode] = useState<boolean>(false);
|
||||
let clientSecret: string | null;
|
||||
|
||||
useEffect(() => {
|
||||
setDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
}, []);
|
||||
|
||||
if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) {
|
||||
clientSecret = props.payment.data.setupIntent.client_secret;
|
||||
} else if (!("setupIntent" in props.payment.data)) {
|
||||
clientSecret = props.payment.data.client_secret;
|
||||
}
|
||||
|
||||
return (
|
||||
<Elements
|
||||
stripe={stripePromise}
|
||||
options={{
|
||||
clientSecret: clientSecret!,
|
||||
appearance: darkMode ? ELEMENT_STYLES_DARK : ELEMENT_STYLES,
|
||||
}}>
|
||||
<PaymentForm {...props} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import classNames from "classnames";
|
||||
import Head from "next/head";
|
||||
import type { FC } from "react";
|
||||
|
@ -6,8 +5,6 @@ import { useEffect, useState } from "react";
|
|||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import type { StripePaymentData } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
@ -65,9 +62,9 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="mx-auto max-w-3xl py-24">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto scroll-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<div className="inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
@ -126,17 +123,14 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
|||
<div className="text-default mt-4 text-center dark:text-gray-300">{t("paid")}</div>
|
||||
)}
|
||||
{props.payment.appId === "stripe" && !props.payment.success && (
|
||||
<Elements
|
||||
stripe={getStripe((props.payment.data as StripePaymentData).stripe_publishable_key)}>
|
||||
<PaymentComponent
|
||||
payment={props.payment}
|
||||
eventType={props.eventType}
|
||||
user={props.user}
|
||||
location={props.booking.location}
|
||||
bookingId={props.booking.id}
|
||||
bookingUid={props.booking.uid}
|
||||
/>
|
||||
</Elements>
|
||||
<PaymentComponent
|
||||
payment={props.payment}
|
||||
eventType={props.eventType}
|
||||
user={props.user}
|
||||
location={props.booking.location}
|
||||
bookingId={props.booking.id}
|
||||
bookingUid={props.booking.uid}
|
||||
/>
|
||||
)}
|
||||
{props.payment.refunded && (
|
||||
<div className="text-default mt-4 text-center dark:text-gray-300">{t("refunded")}</div>
|
||||
|
|
Loading…
Reference in New Issue