UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

312 lines (311 loc) 9.7 kB
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 } ) } ) } ); }