@blocklet/payment-react
Version:
Reusable react components for payment kit v2
692 lines (690 loc) • 23.5 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
import Toast from "@arcblock/ux/lib/Toast";
import { OpenInNewOutlined } from "@mui/icons-material";
import { Box, Button, CircularProgress, Stack, Typography, Tooltip, Avatar } from "@mui/material";
import { styled } from "@mui/system";
import { useInfiniteScroll, useRequest, useSetState } from "ahooks";
import React, { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import debounce from "lodash/debounce";
import Status from "../../components/status.js";
import { usePaymentContext } from "../../contexts/payment.js";
import { useSubscription } from "../../hooks/subscription.js";
import api from "../../libs/api.js";
import StripePaymentAction from "../../components/stripe-payment-action.js";
import {
formatBNStr,
formatError,
formatToDate,
formatToDatetime,
getInvoiceDescriptionAndReason,
getInvoiceStatusColor,
getTxLink,
isCrossOrigin
} from "../../libs/util.js";
import Table from "../../components/table.js";
import { createLink, handleNavigation } from "../../libs/navigation.js";
const groupByDate = (items) => {
const grouped = {};
items.forEach((item) => {
const date = new Date(item.created_at).toLocaleDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date]?.push(item);
});
return grouped;
};
const fetchData = (params = {}) => {
const search = new URLSearchParams();
Object.keys(params).forEach((key) => {
if (params[key]) {
search.set(key, String(params[key]));
}
});
return api.get(`/api/invoices?${search.toString()}`).then((res) => res.data);
};
const getInvoiceLink = (invoice, action) => {
if (invoice.id.startsWith("in_")) {
const path = `/customer/invoice/${invoice.id}${invoice.status === "uncollectible" && action ? `?action=${action}` : ""}`;
return {
connect: invoice.status === "uncollectible",
link: createLink(path)
};
}
return {
connect: false,
link: createLink(getTxLink(invoice.paymentMethod, invoice.metadata?.payment_details).link, true)
};
};
const linkStyle = {
cursor: "pointer"
};
const InvoiceTable = React.memo((props) => {
const {
pageSize,
target,
action,
onPay,
status,
customer_id,
currency_id,
subscription_id,
include_staking,
include_return_staking,
include_recovered_from,
onTableDataChange,
relatedSubscription
} = props;
const listKey = "invoice-table";
const { t, locale } = useLocaleContext();
const navigate = useNavigate();
const [search, setSearch] = useState({
pageSize: pageSize || 10,
page: 1
});
const {
loading,
data = { list: [], count: 0 },
refresh
} = useRequest(
() => fetchData({
...search,
status,
customer_id,
currency_id,
subscription_id,
include_staking,
include_return_staking,
include_recovered_from,
ignore_zero: true
}),
{
refreshDeps: [search, status, customer_id, currency_id, subscription_id, include_staking, include_recovered_from]
}
);
const prevData = useRef(data);
useEffect(() => {
if (onTableDataChange) {
onTableDataChange(data, prevData.current);
prevData.current = data;
}
}, [data]);
const subscription = useSubscription("events");
const debouncedHandleInvoicePaid = debounce(
async () => {
Toast.close();
Toast.success(t("payment.customer.invoice.paySuccess"));
await refresh();
},
1e3,
{
leading: false,
trailing: true,
maxWait: 5e3
}
);
useEffect(() => {
if (subscription && customer_id) {
subscription.on("invoice.paid", ({ response }) => {
if (response.customer_id === customer_id) {
debouncedHandleInvoicePaid();
}
});
}
}, [subscription]);
const handleLinkClick = (e, invoice) => {
const { link } = getInvoiceLink(invoice, action);
handleNavigation(e, link, navigate, { target: link.external ? "_blank" : target });
};
const handleRelatedSubscriptionClick = (e, invoice) => {
if (invoice.subscription_id) {
handleNavigation(e, createLink(`/customer/subscription/${invoice.subscription_id}`), navigate);
}
};
const columns = [
{
label: t("common.amount"),
name: "total",
width: 80,
align: "right",
options: {
customBodyRenderLite: (_, index) => {
const invoice = data?.list[index];
const isVoid = invoice.status === "void";
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: /* @__PURE__ */ jsxs(Typography, { sx: isVoid ? { textDecoration: "line-through" } : {}, children: [
formatBNStr(invoice.total, invoice.paymentCurrency.decimal),
"\xA0",
invoice.paymentCurrency.symbol
] }) });
}
}
},
{
label: t("common.paymentMethod"),
name: "paymentMethod",
options: {
customBodyRenderLite: (_, index) => {
const invoice = data?.list[index];
return /* @__PURE__ */ jsxs(
Typography,
{
sx: { display: "flex", alignItems: "center", whiteSpace: "nowrap" },
onClick: (e) => handleLinkClick(e, invoice),
children: [
/* @__PURE__ */ jsx(Avatar, { src: invoice.paymentMethod.logo, sx: { width: 18, height: 18, mr: 1 } }),
invoice.paymentMethod.name
]
}
);
}
}
},
{
label: t("common.type"),
name: "billing_reason",
options: {
customBodyRenderLite: (_, index) => {
const invoice = data.list[index];
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: /* @__PURE__ */ jsx(Status, { label: getInvoiceDescriptionAndReason(invoice, locale)?.type }) });
}
}
},
{
label: t("payment.customer.invoice.invoiceNumber"),
name: "number",
options: {
customBodyRenderLite: (_, index) => {
const invoice = data?.list[index];
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: invoice?.number });
}
}
},
...relatedSubscription ? [
{
label: t("common.relatedSubscription"),
name: "subscription",
options: {
customBodyRenderLite: (_, index) => {
const invoice = data?.list[index];
return invoice.subscription_id ? /* @__PURE__ */ jsx(
Box,
{
onClick: (e) => handleRelatedSubscriptionClick(e, invoice),
sx: { color: "text.link", cursor: "pointer" },
children: invoice.subscription?.description
}
) : /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: { ...linkStyle, color: "text.lighter" }, children: t("common.none") });
}
}
}
] : [],
{
label: t("common.updatedAt"),
name: "name",
options: {
customBodyRenderLite: (val, index) => {
const invoice = data?.list[index];
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: formatToDate(
invoice.created_at,
locale,
relatedSubscription ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD HH:mm:ss"
) });
}
}
},
{
label: t("common.description"),
name: "",
options: {
sort: false,
customBodyRenderLite: (val, index) => {
const invoice = data?.list[index];
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: getInvoiceDescriptionAndReason(invoice, locale)?.description || invoice.id });
}
}
},
{
label: t("common.status"),
name: "status",
options: {
customBodyRenderLite: (val, index) => {
const invoice = data?.list[index];
const hidePay = invoice.billing_reason === "overdraft-protection";
const { connect } = getInvoiceLink(invoice, action);
const isVoid = invoice.status === "void";
if (action && !hidePay) {
if (connect) {
if (invoice.paymentMethod?.type === "stripe") {
return /* @__PURE__ */ jsx(
StripePaymentAction,
{
invoice,
paymentMethod: invoice.paymentMethod,
onSuccess: () => {
refresh();
},
children: (handlePay, paying) => /* @__PURE__ */ jsx(
Button,
{
variant: "text",
size: "small",
sx: { color: "text.link" },
disabled: paying,
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
handlePay();
},
children: paying ? t("payment.checkout.processing") : t("payment.customer.invoice.pay")
}
)
}
);
}
return /* @__PURE__ */ jsx(Button, { variant: "text", size: "small", onClick: () => onPay(invoice.id), sx: { color: "text.link" }, children: t("payment.customer.invoice.pay") });
}
return /* @__PURE__ */ jsx(
Button,
{
component: "a",
variant: "text",
size: "small",
onClick: (e) => handleLinkClick(e, invoice),
sx: { color: "text.link" },
rel: "noreferrer",
children: t("payment.customer.invoice.pay")
}
);
}
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: isVoid ? /* @__PURE__ */ jsx(Tooltip, { title: t("payment.customer.invoice.noPaymentRequired"), arrow: true, placement: "top", children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(Status, { label: invoice.status, color: getInvoiceStatusColor(invoice.status) }) }) }) : /* @__PURE__ */ jsx(Status, { label: invoice.status, color: getInvoiceStatusColor(invoice.status) }) });
}
}
}
];
const onTableChange = ({ page, rowsPerPage }) => {
if (search.pageSize !== rowsPerPage) {
setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
} else if (search.page !== page + 1) {
setSearch((x) => ({ ...x, page: page + 1 }));
}
};
return /* @__PURE__ */ jsx(InvoiceTableRoot, { children: /* @__PURE__ */ jsx(
Table,
{
hasRowLink: true,
durable: `__${listKey}__`,
durableKeys: ["page", "rowsPerPage", "searchText"],
data: data.list,
columns,
options: {
count: data.count,
page: search.page - 1,
rowsPerPage: search.pageSize
},
loading,
onChange: onTableChange,
toolbar: false,
sx: { mt: 2 },
showMobile: false,
mobileTDFlexDirection: "row",
emptyNodeText: t("payment.customer.invoice.emptyList")
}
) });
});
const InvoiceTableRoot = styled(Box)`
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
> div {
width: fit-content;
flex: inherit;
font-size: 14px;
}
}
.invoice-summary {
padding-right: 20px;
}
}
`;
const InvoiceList = React.memo((props) => {
const {
customer_id,
subscription_id,
include_recovered_from,
currency_id,
include_staking,
status,
pageSize,
target,
action,
onPay,
onTableDataChange
} = props;
const size = pageSize || 10;
const subscription = useSubscription("events");
const { t, locale } = useLocaleContext();
const navigate = useNavigate();
const { data, loadMore, loadingMore, loading, reloadAsync } = useInfiniteScroll(
(d) => {
const page = d ? Math.ceil(d.list.length / size) + 1 : 1;
return fetchData({
page,
pageSize: size,
status,
customer_id,
currency_id,
subscription_id,
include_staking,
include_recovered_from,
ignore_zero: true
});
},
{
reloadDeps: [customer_id, subscription_id, status, include_staking, include_recovered_from]
}
);
const prevData = useRef(data);
useEffect(() => {
if (onTableDataChange) {
onTableDataChange(data, prevData.current);
prevData.current = data;
}
}, [data]);
const debouncedHandleInvoicePaid = debounce(
async () => {
Toast.close();
Toast.success(t("payment.customer.invoice.paySuccess"));
await reloadAsync();
},
1e3,
{
leading: false,
trailing: true,
maxWait: 5e3
}
);
useEffect(() => {
if (subscription && customer_id) {
subscription.on("invoice.paid", ({ response }) => {
if (response.customer_id === customer_id) {
debouncedHandleInvoicePaid();
}
});
}
}, [subscription]);
if (loading || !data) {
return /* @__PURE__ */ jsx(CircularProgress, {});
}
if (data && data.list.length === 0) {
if (data.subscription && ["active", "trialing"].includes(data.subscription.status)) {
return /* @__PURE__ */ jsx(
Typography,
{
sx: {
color: "text.secondary",
my: 0.5
},
children: t("payment.customer.invoice.next", { date: formatToDatetime(data.subscription.current_period_end * 1e3) })
}
);
}
return /* @__PURE__ */ jsx(
Typography,
{
sx: {
color: "text.secondary",
my: 0.5
},
children: t("payment.customer.invoice.empty")
}
);
}
const hasMore = data && data.list.length < data.count;
const grouped = groupByDate(data.list);
const handleLinkClick = (e, link) => {
handleNavigation(e, link, navigate, { target: link.external ? "_blank" : target });
};
return /* @__PURE__ */ jsxs(Root, { direction: "column", gap: 1, sx: { mt: 1 }, children: [
Object.entries(grouped).map(([date, invoices]) => /* @__PURE__ */ jsxs(Box, { children: [
/* @__PURE__ */ jsx(Typography, { sx: { fontWeight: "bold", color: "text.secondary", mt: 2, mb: 1 }, children: date }),
invoices.map((invoice) => {
const { link, connect } = getInvoiceLink(invoice, action);
const isVoid = invoice.status === "void";
return /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
sx: {
gap: {
xs: 0.5,
sm: 1.5,
md: 3
},
alignItems: "center",
flexWrap: "nowrap",
my: 1
},
children: [
/* @__PURE__ */ jsx(
Box,
{
sx: {
flex: 2
},
children: /* @__PURE__ */ jsx(
"a",
{
href: link.url,
target: link.external ? "_blank" : target,
rel: "noreferrer",
onClick: (e) => handleLinkClick(e, link),
children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center"
},
children: [
/* @__PURE__ */ jsx(Typography, { component: "span", children: invoice.number }),
link.external && /* @__PURE__ */ jsx(
OpenInNewOutlined,
{
fontSize: "small",
sx: {
color: "text.secondary",
display: { xs: "none", md: "inline-flex" }
}
}
)
]
}
)
}
)
}
),
/* @__PURE__ */ jsx(
Box,
{
sx: {
flex: 1,
textAlign: "right"
},
children: /* @__PURE__ */ jsxs(Typography, { sx: isVoid ? { textDecoration: "line-through" } : {}, children: [
formatBNStr(invoice.total, invoice.paymentCurrency.decimal),
"\xA0",
invoice.paymentCurrency.symbol
] })
}
),
/* @__PURE__ */ jsx(
Box,
{
sx: {
flex: 1,
textAlign: "right"
},
children: /* @__PURE__ */ jsx(Typography, { children: formatToDate(invoice.created_at, locale, "HH:mm:ss") })
}
),
!action && /* @__PURE__ */ jsx(
Box,
{
className: "invoice-description",
sx: {
flex: 2,
textAlign: "right",
display: { xs: "none", lg: "inline-flex" }
},
children: /* @__PURE__ */ jsx(Typography, { children: invoice.description || invoice.id })
}
),
/* @__PURE__ */ jsx(
Box,
{
sx: {
flex: 1,
textAlign: "right"
},
children: action ? connect ? invoice.paymentMethod?.type === "stripe" ? /* @__PURE__ */ jsx(
StripePaymentAction,
{
invoice,
paymentMethod: invoice.paymentMethod,
onSuccess: async () => {
await reloadAsync();
},
children: (handlePay, paying) => /* @__PURE__ */ jsx(
Button,
{
variant: "contained",
color: "primary",
size: "small",
sx: { whiteSpace: "nowrap" },
disabled: paying,
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
handlePay();
},
children: paying ? t("payment.checkout.processing") : t("payment.customer.invoice.pay")
}
)
}
) : /* @__PURE__ */ jsx(
Button,
{
variant: "contained",
color: "primary",
size: "small",
onClick: () => onPay(invoice.id),
sx: { whiteSpace: "nowrap" },
children: t("payment.customer.invoice.pay")
}
) : /* @__PURE__ */ jsx(
Button,
{
component: "a",
variant: "contained",
size: "small",
onClick: (e) => handleLinkClick(e, link),
sx: { whiteSpace: "nowrap" },
rel: "noreferrer",
children: t("payment.customer.invoice.pay")
}
) : isVoid ? /* @__PURE__ */ jsx(Tooltip, { title: t("payment.customer.invoice.noPaymentRequired"), arrow: true, placement: "top", children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(Status, { label: invoice.status, color: getInvoiceStatusColor(invoice.status) }) }) }) : /* @__PURE__ */ jsx(Status, { label: invoice.status, color: getInvoiceStatusColor(invoice.status) })
}
)
]
},
invoice.id
);
})
] }, date)),
/* @__PURE__ */ jsxs(Box, { children: [
hasMore && /* @__PURE__ */ jsx(Button, { variant: "text", type: "button", color: "inherit", onClick: loadMore, disabled: loadingMore, children: loadingMore ? t("common.loadingMore", { resource: t("payment.customer.invoices") }) : t("common.loadMore", { resource: t("payment.customer.invoices") }) }),
!hasMore && data.count > size && /* @__PURE__ */ jsx(
Typography,
{
sx: {
color: "text.secondary"
},
children: t("common.noMore", { resource: t("payment.customer.invoices") })
}
)
] })
] });
});
export default function CustomerInvoiceList(rawProps) {
const props = Object.assign(
{
customer_id: "",
subscription_id: "",
currency_id: "",
include_staking: false,
include_recovered_from: false,
status: "open,paid,uncollectible",
pageSize: 10,
target: "_self",
action: "",
type: "list",
onTableDataChange: () => {
},
relatedSubscription: false
},
rawProps
);
const { action, type } = props;
const { t, locale } = useLocaleContext();
const { connect } = usePaymentContext();
const [state, setState] = useSetState({ paying: "" });
const onPay = (invoiceId) => {
if (state.paying) {
return;
}
setState({ paying: invoiceId });
connect.open({
action: "collect",
saveConnect: false,
locale,
useSocket: isCrossOrigin() === false,
messages: {
scan: "",
title: t(`payment.customer.invoice.${action || "pay"}`),
success: t(`payment.customer.invoice.${action || "pay"}Success`),
error: t(`payment.customer.invoice.${action || "pay"}Error`),
confirm: ""
},
extraParams: { invoiceId, action },
onSuccess: () => {
connect.close();
setState({ paying: "" });
},
onClose: () => {
connect.close();
setState({ paying: "" });
},
onError: (err) => {
setState({ paying: "" });
Toast.error(formatError(err));
}
});
};
if (type === "table") {
return /* @__PURE__ */ jsx(InvoiceTable, { ...props, onPay });
}
return /* @__PURE__ */ jsx(InvoiceList, { ...props, onPay });
}
const Root = styled(Stack)`
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
svg.MuiSvgIcon-root {
display: none !important;
}
}
a.MuiButton-root {
text-decoration: none !important;
}
`;