UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

591 lines (589 loc) 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _jsxRuntime = require("react/jsx-runtime"); var _react = require("react"); var _material = require("@mui/material"); var _context = require("@arcblock/ux/lib/Locale/context"); var _useBrowser = _interopRequireDefault(require("@arcblock/react-hooks/lib/useBrowser")); var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast")); var _ufo = require("ufo"); var _ahooks = require("ahooks"); var _pWaitFor = _interopRequireDefault(require("p-wait-for")); var _ux = require("@arcblock/ux"); var _iconsMaterial = require("@mui/icons-material"); var _debounce = _interopRequireDefault(require("lodash/debounce")); var _payment = require("../contexts/payment"); var _util = require("../libs/util"); var _subscription = require("../hooks/subscription"); var _api = _interopRequireDefault(require("../libs/api")); var _loadingButton = _interopRequireDefault(require("./loading-button")); var _stripePaymentAction = _interopRequireDefault(require("./stripe-payment-action")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const fetchOverdueInvoices = async params => { if (!params.subscriptionId && !params.customerId) { throw new Error("Either subscriptionId or customerId must be provided"); } let url; if (params.subscriptionId) { url = `/api/subscriptions/${params.subscriptionId}/overdue/invoices`; } else { url = `/api/customers/${params.customerId}/overdue/invoices`; } const res = await _api.default.get(params.authToken ? (0, _ufo.joinURL)(url, `?authToken=${params.authToken}`) : url); return res.data; }; function OverdueInvoicePayment({ subscriptionId = void 0, customerId = void 0, mode = "default", dialogProps = { open: true }, children = void 0, onPaid = () => {}, detailLinkOptions = { enabled: true }, successToast = true, alertMessage = "", authToken = void 0 }) { const { t, locale } = (0, _context.useLocaleContext)(); const browser = (0, _useBrowser.default)(); const inArcSphere = browser?.arcSphere; const theme = (0, _material.useTheme)(); const { connect, session } = (0, _payment.usePaymentContext)(); const [selectCurrencyId, setSelectCurrencyId] = (0, _react.useState)(""); const [payLoading, setPayLoading] = (0, _react.useState)(false); const [dialogOpen, setDialogOpen] = (0, _react.useState)(dialogProps.open || false); const [processedCurrencies, setProcessedCurrencies] = (0, _react.useState)({}); const [paymentStatus, setPaymentStatus] = (0, _react.useState)({}); const [stripePaymentInProgress, setStripePaymentInProgress] = (0, _react.useState)({}); const stripePaymentInProgressRef = (0, _react.useRef)(stripePaymentInProgress); const sourceType = subscriptionId ? "subscription" : "customer"; const effectiveCustomerId = customerId || session?.user?.did; const sourceId = subscriptionId || effectiveCustomerId; const customerIdRef = (0, _react.useRef)(effectiveCustomerId); (0, _react.useEffect)(() => { stripePaymentInProgressRef.current = stripePaymentInProgress; }, [stripePaymentInProgress]); const { data = { summary: {}, invoices: [] }, error, loading, runAsync: refresh } = (0, _ahooks.useRequest)(() => fetchOverdueInvoices({ subscriptionId, customerId: effectiveCustomerId, authToken }), { ready: !!subscriptionId || !!effectiveCustomerId, onSuccess: res => { if (res.customer?.id && res.customer?.id !== customerIdRef.current) { customerIdRef.current = res.customer?.id; } } }); const detailUrl = (0, _react.useMemo)(() => { if (subscriptionId) { return (0, _ufo.joinURL)((0, _util.getPrefix)(), `/customer/subscription/${subscriptionId}`); } if (effectiveCustomerId) { return (0, _ufo.joinURL)((0, _util.getPrefix)(), "/customer/invoice/past-due"); } return ""; }, [subscriptionId, effectiveCustomerId]); const summaryList = (0, _react.useMemo)(() => { if (!data?.summary) { return []; } return Object.values(data.summary); }, [data?.summary]); const checkAndHandleInvoicePaid = async currencyId => { if (stripePaymentInProgressRef.current[currencyId]) { try { const checkData = await fetchOverdueInvoices({ subscriptionId, customerId: effectiveCustomerId, authToken }); const hasRemainingInvoices = checkData.invoices?.some(inv => inv.currency_id === currencyId); if (hasRemainingInvoices) { return; } setStripePaymentInProgress(prev => { const newState = { ...prev }; delete newState[currencyId]; return newState; }); setPaymentStatus(prev => ({ ...prev, [currencyId]: "success" })); } catch (err) { console.error("Error checking Stripe payment completion:", err); return; } } if (successToast) { _Toast.default.close(); _Toast.default.success(t("payment.customer.invoice.paySuccess")); } setPayLoading(false); const res = await refresh(); if (res.invoices?.length === 0) { setDialogOpen(false); onPaid(sourceId, currencyId, sourceType); } }; const debouncedHandleInvoicePaid = (0, _debounce.default)(currencyId => checkAndHandleInvoicePaid(currencyId), 1e3, { leading: false, trailing: true, maxWait: 5e3 }); const isCrossOriginRequest = (0, _util.isCrossOrigin)(); const subscription = (0, _subscription.useSubscription)("events"); const waitForInvoicePaidByCurrency = async currencyId => { let isPaid = false; await (0, _pWaitFor.default)(async () => { const checkData = await fetchOverdueInvoices({ subscriptionId, customerId: effectiveCustomerId, authToken }); const hasRemainingInvoices = checkData.invoices?.some(inv => inv.currency_id === currencyId); isPaid = !hasRemainingInvoices; return isPaid; }, { interval: 2e3, timeout: 3 * 60 * 1e3 }); return isPaid; }; const handleConnected = async (currencyId, isStripe = false) => { if (isCrossOriginRequest || inArcSphere) { try { const paid = await waitForInvoicePaidByCurrency(currencyId); if (paid) { setPaymentStatus(prev => ({ ...prev, [currencyId]: "success" })); const finalRes = await refresh(); if (successToast) { _Toast.default.close(); _Toast.default.success(t("payment.customer.invoice.paySuccess")); } if (finalRes.invoices?.length === 0) { setDialogOpen(false); onPaid(sourceId, currencyId, sourceType); } } } catch (err) { console.error("Check payment status failed:", err); setPaymentStatus(prev => ({ ...prev, [currencyId]: "error" })); } finally { setPayLoading(false); } } else if (isStripe) { setStripePaymentInProgress(prev => ({ ...prev, [currencyId]: true })); setPaymentStatus(prev => ({ ...prev, [currencyId]: "processing" })); } }; (0, _react.useEffect)(() => { if (!subscription || isCrossOriginRequest || inArcSphere) { return void 0; } const handleInvoicePaid = ({ response }) => { const relevantId = subscriptionId || response.customer_id; const uniqueKey = `${relevantId}-${response.currency_id}`; if (subscriptionId && response.subscription_id === subscriptionId || effectiveCustomerId && effectiveCustomerId === response.customer_id || customerIdRef.current && customerIdRef.current === response.customer_id) { if (!processedCurrencies[uniqueKey]) { setProcessedCurrencies(prev => ({ ...prev, [uniqueKey]: 1 })); debouncedHandleInvoicePaid(response.currency_id); } } }; subscription.on("invoice.paid", handleInvoicePaid); return () => { subscription.off("invoice.paid", handleInvoicePaid); }; }, [subscription, subscriptionId, effectiveCustomerId]); const handlePay = item => { const { currency, method } = item; if (method.type === "stripe") { _Toast.default.error(t("payment.subscription.overdue.notSupport")); return; } if (payLoading) { return; } setSelectCurrencyId(currency.id); setPayLoading(true); setPaymentStatus(prev => ({ ...prev, [currency.id]: "idle" })); if (["arcblock", "ethereum", "base"].includes(method.type)) { const extraParams = { currencyId: currency.id }; if (subscriptionId) { extraParams.subscriptionId = subscriptionId; } else if (effectiveCustomerId) { extraParams.customerId = effectiveCustomerId; } connect.open({ locale, containerEl: void 0, saveConnect: false, action: "collect-batch", prefix: (0, _ufo.joinURL)((0, _util.getPrefix)(), "/api/did"), useSocket: !isCrossOriginRequest, extraParams, messages: { scan: t("common.connect.defaultScan"), title: t("payment.customer.invoice.payBatch"), confirm: t("common.connect.confirm") }, onSuccess: () => { connect.close(); handleConnected(currency.id); setPayLoading(false); setPaymentStatus(prev => ({ ...prev, [currency.id]: "success" })); }, onClose: () => { connect.close(); setPayLoading(false); }, onError: err => { _Toast.default.error((0, _util.formatError)(err)); setPaymentStatus(prev => ({ ...prev, [currency.id]: "error" })); setPayLoading(false); } }); } }; const handleClose = () => { setDialogOpen(false); dialogProps.onClose?.(); }; const handleViewDetailClick = e => { if (detailLinkOptions.onClick) { e.preventDefault(); detailLinkOptions.onClick(e); } else if (!detailLinkOptions.enabled) { e.preventDefault(); handleClose(); } }; if (loading) { return null; } const getDetailLinkText = () => { if (detailLinkOptions.title) { return detailLinkOptions.title; } if (subscriptionId) { return t("payment.subscription.overdue.view"); } return t("payment.customer.pastDue.view"); }; const renderPayButton = (item, primaryButton = true, options = { variant: "contained" }) => { const { currency } = item; const inProcess = payLoading && selectCurrencyId === currency.id; const status = paymentStatus[currency.id] || "idle"; if (status === "success") { return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { variant: options?.variant || "contained", size: "small", onClick: () => checkAndHandleInvoicePaid(currency.id), ...(primaryButton ? {} : { color: "success", startIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.CheckCircle, {}) }), children: t("payment.subscription.overdue.paid") }); } if (item.method.type === "stripe") { let buttonText = t("payment.subscription.overdue.payNow"); if (status === "error") { buttonText = t("payment.subscription.overdue.retry"); } else if (status === "processing") { buttonText = t("payment.subscription.overdue.processing"); } return /* @__PURE__ */(0, _jsxRuntime.jsx)(_stripePaymentAction.default, { subscriptionId, customerId: !subscriptionId ? effectiveCustomerId : void 0, currencyId: currency.id, paymentMethod: item.method, onSuccess: () => { handleConnected(currency.id, true); }, onError: () => { setPaymentStatus(prev => ({ ...prev, [currency.id]: "error" })); setStripePaymentInProgress(prev => ({ ...prev, [currency.id]: false })); }, children: (onPay, paying) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_loadingButton.default, { variant: options?.variant || "contained", size: "small", disabled: paying || status === "processing", loading: paying || status === "processing", onClick: onPay, sx: options?.sx, children: buttonText }) }); } return /* @__PURE__ */(0, _jsxRuntime.jsx)(_loadingButton.default, { variant: options?.variant || "contained", size: "small", disabled: inProcess, loading: inProcess, onClick: () => handlePay(item), sx: options?.sx, children: status === "error" ? t("payment.subscription.overdue.retry") : t("payment.subscription.overdue.payNow") }); }; const getMethodText = method => { if (method.name && method.type !== "arcblock") { return ` (${method.name})`; } return ""; }; const getOverdueTitle = () => { if (subscriptionId && data.subscription) { if (summaryList.length === 1) { return t("payment.subscription.overdue.title", { name: data.subscription?.description, count: data.invoices?.length, total: (0, _util.formatAmount)(summaryList[0]?.amount, summaryList[0]?.currency?.decimal), symbol: summaryList[0]?.currency?.symbol, method: getMethodText(summaryList[0]?.method) }); } return t("payment.subscription.overdue.simpleTitle", { name: data.subscription?.description, count: data.invoices?.length }); } if (effectiveCustomerId) { let title = ""; if (summaryList.length === 1) { title = t("payment.customer.overdue.title", { subscriptionCount: data.subscriptionCount || 0, count: data.invoices?.length, total: (0, _util.formatAmount)(summaryList[0]?.amount, summaryList[0]?.currency?.decimal), symbol: summaryList[0]?.currency?.symbol, method: getMethodText(summaryList[0]?.method) }); } else { title = t("payment.customer.overdue.simpleTitle", { subscriptionCount: data.subscriptionCount || 0, count: data.invoices?.length }); } if (alertMessage) { return `${title}${alertMessage}`; } return `${title}${t("payment.customer.overdue.defaultAlert")}`; } return ""; }; const getEmptyStateMessage = () => { if (subscriptionId && data.subscription) { return t("payment.subscription.overdue.empty", { name: data.subscription?.description }); } return t("payment.customer.overdue.empty"); }; if (mode === "custom" && children && typeof children === "function") { return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { children: children(handlePay, { subscription: data?.subscription, summary: data?.summary, invoices: data?.invoices, subscriptionCount: data?.subscriptionCount, detailUrl }) }); } return /* @__PURE__ */(0, _jsxRuntime.jsx)(_ux.Dialog, { PaperProps: { style: { minHeight: "auto" } }, ...(dialogProps || {}), open: dialogOpen, title: dialogProps?.title || t("payment.subscription.overdue.pastDue"), sx: { "& .MuiDialogContent-root": { pt: 0 } }, onClose: handleClose, children: error ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, { severity: "error", children: error.message }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { gap: 1 }, children: [summaryList.length === 0 && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, { severity: "success", children: getEmptyStateMessage() }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { direction: "row", sx: { justifyContent: "flex-end", mt: 2 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { variant: "outlined", color: "primary", onClick: handleClose, sx: { width: "fit-content" }, children: t("common.know") }) })] }), summaryList.length === 1 && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { variant: "body1", sx: { color: "text.secondary" }, children: [getOverdueTitle(), detailLinkOptions.enabled && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)("br", {}), t("payment.subscription.overdue.description"), /* @__PURE__ */(0, _jsxRuntime.jsx)("a", { href: detailUrl, target: "_blank", onClick: handleViewDetailClick, rel: "noreferrer", style: { color: theme.palette.text.link }, children: getDetailLinkText() })] })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { justifyContent: "flex-end", gap: 2, mt: 2 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { variant: "outlined", color: "primary", onClick: handleClose, children: t("common.cancel") }), renderPayButton(summaryList[0])] })] }), summaryList.length > 1 && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { variant: "body1", sx: { color: "text.secondary" }, children: [getOverdueTitle(), detailLinkOptions.enabled && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)("br", {}), t("payment.subscription.overdue.description"), /* @__PURE__ */(0, _jsxRuntime.jsx)("a", { href: detailUrl, target: "_blank", rel: "noreferrer", onClick: handleViewDetailClick, style: { color: theme.palette.text.link }, children: getDetailLinkText() })] })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "body1", sx: { color: "text.secondary" }, children: t("payment.subscription.overdue.list") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { children: summaryList.map(item => /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { justifyContent: "space-between", alignItems: "center", py: 1, px: 0.5, borderBottom: "1px solid", borderColor: "divider", "&:hover": { backgroundColor: () => theme.palette.grey[100] }, mt: 0 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: t("payment.subscription.overdue.total", { total: (0, _util.formatAmount)(item?.amount, item?.currency?.decimal), currency: item?.currency?.symbol, method: getMethodText(item?.method) }) }), renderPayButton(item, false, { variant: "text", sx: { color: "text.link" } })] }, item?.currency?.id)) })] })] }) }); } module.exports = OverdueInvoicePayment;