@blocklet/payment-react
Version:
Reusable react components for payment kit v2
312 lines (311 loc) • 9.7 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import Center from "@arcblock/ux/lib/Center";
import Dialog from "@arcblock/ux/lib/Dialog";
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
import { CircularProgress, Typography, useTheme } from "@mui/material";
import { styled } from "@mui/system";
import { useSetState } from "ahooks";
import { useEffect, useCallback } from "react";
import { useMobile } from "../../../hooks/mobile.js";
import LoadingButton from "../../../components/loading-button.js";
const { Elements, PaymentElement, useElements, useStripe, loadStripe, LinkAuthenticationElement } = globalThis.__STRIPE_COMPONENTS__;
const PaymentElementContainer = styled("div")`
width: 100%;
opacity: 0;
transition: opacity 300ms ease;
&.visible {
opacity: 1;
}
`;
function StripeCheckoutForm({
clientSecret,
intentType,
customer,
mode,
onConfirm,
returnUrl = "",
submitButtonText = ""
}) {
const stripe = useStripe();
const elements = useElements();
const { t } = useLocaleContext();
const theme = useTheme();
const [state, setState] = useSetState({
message: "",
confirming: false,
loaded: false,
showBillingForm: false,
isTransitioning: false,
paymentMethod: "card"
});
const handlePaymentMethodChange = (event) => {
const method = event.value?.type;
const needsBillingInfo = method === "google_pay" || method === "apple_pay";
const shouldShowForm = needsBillingInfo && !isCompleteBillingAddress(customer.address);
if (shouldShowForm && !state.showBillingForm) {
setState({ isTransitioning: true });
setTimeout(() => {
setState({
isTransitioning: false,
paymentMethod: method,
showBillingForm: true
});
}, 300);
} else {
setState({
showBillingForm: false,
paymentMethod: method,
isTransitioning: false
});
}
};
const isCompleteBillingAddress = (address) => {
return address && address.line1 && address.city && address.state && address.postal_code && address.country;
};
useEffect(() => {
if (!stripe) {
return;
}
if (!clientSecret) {
return;
}
const method = intentType === "payment_intent" ? "retrievePaymentIntent" : "retrieveSetupIntent";
stripe[method](clientSecret).then(({ paymentIntent, setupIntent }) => {
const intent = paymentIntent || setupIntent;
switch (intent?.status) {
case "succeeded":
setState({ message: t("paymentCredit.preparePayMessage.succeeded") });
break;
case "processing":
setState({ message: t("paymentCredit.preparePayMessage.processing") });
break;
case "requires_payment_method":
// 忽略该状态
default:
break;
}
});
}, [stripe, clientSecret]);
const handleSubmit = useCallback(
async (e) => {
e.preventDefault();
if (!stripe || !elements) {
return;
}
try {
setState({ confirming: true, message: "" });
const method = intentType === "payment_intent" ? "confirmPayment" : "confirmSetup";
const { error: submitError } = await elements.submit();
if (submitError) {
setState({ confirming: false });
return;
}
const { error, paymentIntent, setupIntent } = await stripe[method]({
elements,
redirect: "if_required",
confirmParams: {
return_url: returnUrl || window.location.href,
...!state.showBillingForm ? {
payment_method_data: {
billing_details: {
name: customer.name,
phone: customer.phone,
email: customer.email,
address: {
...customer.address || {},
country: customer.address?.country || "us",
line1: customer.address?.line1 || "",
line2: customer.address?.line2 || "",
city: customer.address?.city || "",
state: customer.address?.state || "",
postal_code: customer.address?.postal_code || "00000"
}
}
}
} : {}
}
});
const intent = paymentIntent || setupIntent;
if (intent?.status === "canceled" || intent?.status === "requires_payment_method") {
setState({ confirming: false });
return;
}
setState({ confirming: false });
if (error) {
if (error.type === "validation_error") {
return;
}
setState({ message: error.message });
return;
}
onConfirm();
} catch (err) {
console.error(err);
setState({ confirming: false, message: err.message });
}
},
[customer, intentType, stripe, state.showBillingForm, returnUrl]
// eslint-disable-line
);
return /* @__PURE__ */ jsxs(Content, { onSubmit: handleSubmit, children: [
(!state.paymentMethod || ["link", "card"].includes(state.paymentMethod)) && /* @__PURE__ */ jsx(
LinkAuthenticationElement,
{
options: {
defaultValues: {
email: customer.email
}
}
}
),
/* @__PURE__ */ jsx(PaymentElementContainer, { className: !state.isTransitioning ? "visible" : "", children: /* @__PURE__ */ jsx(
PaymentElement,
{
options: {
layout: "auto",
fields: {
billingDetails: state.showBillingForm ? "auto" : "never"
},
readOnly: state.confirming,
defaultValues: {
billingDetails: {
name: customer.name,
phone: customer.phone,
email: customer.email,
address: customer.address
}
},
appearance: {
theme: theme.palette.mode,
variables: {
colorPrimary: theme.palette.primary.main,
colorBackground: theme.palette.background.paper,
colorText: theme.palette.text.primary,
colorDanger: theme.palette.error.main,
borderRadius: "4px"
}
}
},
onChange: handlePaymentMethodChange,
onReady: () => setState({ loaded: true })
}
) }),
(!stripe || !elements || !state.loaded) && /* @__PURE__ */ jsx(Center, { relative: "parent", children: /* @__PURE__ */ jsx(CircularProgress, {}) }),
stripe && elements && state.loaded && /* @__PURE__ */ jsx(
LoadingButton,
{
fullWidth: true,
sx: { mt: 2, mb: 1, borderRadius: 0, fontSize: "0.875rem" },
type: "submit",
disabled: state.confirming || !state.loaded,
loading: state.confirming,
variant: "contained",
color: "primary",
size: "large",
children: submitButtonText || t("payment.checkout.continue", { action: t(`payment.checkout.${mode}`) })
}
),
state.message && /* @__PURE__ */ jsx(Typography, { sx: { mt: 1, color: "error.main" }, children: state.message })
] });
}
const Content = styled("form")`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
min-height: 320px;
`;
export default function StripeCheckout({
clientSecret,
intentType,
publicKey,
mode,
customer,
onConfirm,
onCancel,
returnUrl = "",
title = "",
submitButtonText = ""
}) {
const stripePromise = loadStripe(publicKey);
const { isMobile } = useMobile();
const { t, locale } = useLocaleContext();
const theme = useTheme();
const [state, setState] = useSetState({
open: true,
closable: true
});
const handleClose = (_, reason) => {
if (reason === "backdropClick") {
return;
}
setState({ open: false });
onCancel();
};
return /* @__PURE__ */ jsx(
Dialog,
{
title: title || t("payment.checkout.cardPay", { action: t(`payment.checkout.${mode}`) }),
showCloseButton: state.closable,
open: state.open,
onClose: handleClose,
disableEscapeKeyDown: true,
sx: {
".StripeElement": {
minWidth: isMobile ? "100%" : "500px",
py: 1
},
form: {
justifyContent: "flex-start"
},
".StripeElement--focus": {
borderColor: theme.palette.primary.main
},
".StripeElement--invalid": {
borderColor: theme.palette.error.main
},
".StripeElement--complete": {
borderColor: theme.palette.success.main
}
},
PaperProps: {
style: {
minWidth: isMobile ? "100%" : "500px"
}
},
children: /* @__PURE__ */ jsx(
Elements,
{
options: {
clientSecret,
locale: locale === "zh" ? "zh-CN" : "en",
appearance: {
theme: theme.palette.mode,
variables: {
colorPrimary: theme.palette.primary.main,
colorBackground: theme.palette.background.paper,
colorText: theme.palette.text.primary,
colorDanger: theme.palette.error.main
}
}
},
stripe: stripePromise,
children: /* @__PURE__ */ jsx(
StripeCheckoutForm,
{
clientSecret,
intentType,
mode,
customer,
onConfirm,
returnUrl,
submitButtonText
}
)
}
)
}
);
}