@blocklet/payment-react
Version:
Reusable react components for payment kit v2
591 lines (589 loc) • 20.4 kB
JavaScript
;
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;