UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

692 lines (690 loc) 23.5 kB
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; } `;