@blocklet/payment-react
Version:
Reusable react components for payment kit v2
400 lines (399 loc) • 13.7 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
import { HelpOutline } from "@mui/icons-material";
import { Box, Divider, Fade, Grow, Stack, Tooltip, Typography, Collapse, IconButton } from "@mui/material";
import { BN, fromTokenToUnit, fromUnitToToken } from "@ocap/util";
import { useRequest, useSetState } from "ahooks";
import noop from "lodash/noop";
import useBus from "use-bus";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { styled } from "@mui/material/styles";
import Status from "../components/status.js";
import api from "../libs/api.js";
import { formatAmount, formatCheckoutHeadlines, getPriceUintAmountByCurrency } from "../libs/util.js";
import PaymentAmount from "./amount.js";
import ProductDonation from "./product-donation.js";
import ProductItem from "./product-item.js";
import Livemode from "../components/livemode.js";
import { usePaymentContext } from "../contexts/payment.js";
import { useMobile } from "../hooks/mobile.js";
import LoadingButton from "../components/loading-button.js";
const ExpandMore = styled((props) => {
const { expand, ...other } = props;
return /* @__PURE__ */ jsx(IconButton, { ...other });
})(({ theme, expand }) => ({
transform: !expand ? "rotate(0deg)" : "rotate(180deg)",
marginLeft: "auto",
transition: theme.transitions.create("transform", {
duration: theme.transitions.duration.shortest
})
}));
async function fetchCrossSell(id) {
try {
const { data } = await api.get(`/api/checkout-sessions/${id}/cross-sell`);
if (!data.error) {
return data;
}
return null;
} catch (err) {
return null;
}
}
function getStakingSetup(items, currency, billingThreshold = 0) {
const staking = {
licensed: new BN(0),
metered: new BN(0)
};
const recurringItems = items.map((x) => x.upsell_price || x.price).filter((x) => x.type === "recurring" && x.recurring);
if (recurringItems.length > 0) {
if (+billingThreshold) {
return fromTokenToUnit(billingThreshold, currency.decimal).toString();
}
items.forEach((x) => {
const price = x.upsell_price || x.price;
const unit = getPriceUintAmountByCurrency(price, currency);
const amount = new BN(unit).mul(new BN(x.quantity));
if (price.type === "recurring" && price.recurring) {
if (price.recurring.usage_type === "licensed") {
staking.licensed = staking.licensed.add(amount);
}
if (price.recurring.usage_type === "metered") {
staking.metered = staking.metered.add(amount);
}
}
});
return staking.licensed.add(staking.metered).toString();
}
return "0";
}
export default function PaymentSummary({
items,
currency,
trialInDays,
billingThreshold,
onUpsell = noop,
onDownsell = noop,
onQuantityChange = noop,
onApplyCrossSell = noop,
onCancelCrossSell = noop,
onChangeAmount = noop,
checkoutSessionId = "",
crossSellBehavior = "",
showStaking = false,
donationSettings = void 0,
action = "",
trialEnd = 0,
completed = false,
...rest
}) {
const { t, locale } = useLocaleContext();
const { isMobile } = useMobile();
const settings = usePaymentContext();
const [state, setState] = useSetState({ loading: false, shake: false, expanded: items?.length < 3 });
const { data, runAsync } = useRequest(
() => checkoutSessionId ? fetchCrossSell(checkoutSessionId) : Promise.resolve(null)
);
const headlines = formatCheckoutHeadlines(items, currency, { trialEnd, trialInDays }, locale);
const staking = showStaking ? getStakingSetup(items, currency, billingThreshold) : "0";
const totalAmount = fromUnitToToken(
new BN(fromTokenToUnit(headlines.actualAmount, currency?.decimal)).add(new BN(staking)).toString(),
currency?.decimal
);
useBus(
"error.REQUIRE_CROSS_SELL",
() => {
setState({ shake: true });
setTimeout(() => {
setState({ shake: false });
}, 1e3);
},
[]
);
const handleUpsell = async (from, to) => {
await onUpsell(from, to);
runAsync();
};
const handleQuantityChange = async (itemId, quantity) => {
await onQuantityChange(itemId, quantity);
runAsync();
};
const handleDownsell = async (from) => {
await onDownsell(from);
runAsync();
};
const handleApplyCrossSell = async () => {
if (data) {
try {
setState({ loading: true });
await onApplyCrossSell(data.id);
} catch (err) {
console.error(err);
} finally {
setState({ loading: false });
}
}
};
const handleCancelCrossSell = async () => {
try {
setState({ loading: true });
await onCancelCrossSell();
} catch (err) {
console.error(err);
} finally {
setState({ loading: false });
}
};
const ProductCardList = /* @__PURE__ */ jsxs(
Stack,
{
className: "cko-product-list",
sx: {
flex: "0 1 auto",
overflow: "auto"
},
children: [
/* @__PURE__ */ jsx(Stack, { spacing: { xs: 1, sm: 2 }, children: items.map(
(x) => x.price.custom_unit_amount && onChangeAmount && donationSettings ? /* @__PURE__ */ jsx(
ProductDonation,
{
item: x,
settings: donationSettings,
onChange: onChangeAmount,
currency
},
`${x.price_id}-${currency.id}`
) : /* @__PURE__ */ jsx(
ProductItem,
{
item: x,
items,
trialInDays,
trialEnd,
currency,
onUpsell: handleUpsell,
onDownsell: handleDownsell,
adjustableQuantity: x.adjustable_quantity,
completed,
onQuantityChange: handleQuantityChange,
children: x.cross_sell && /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
sx: {
alignItems: "center",
justifyContent: "space-between",
width: 1
},
children: [
/* @__PURE__ */ jsx(Typography, {}),
/* @__PURE__ */ jsx(
LoadingButton,
{
size: "small",
loadingPosition: "end",
endIcon: null,
color: "error",
variant: "text",
loading: state.loading,
onClick: handleCancelCrossSell,
children: t("payment.checkout.cross_sell.remove")
}
)
]
}
)
},
`${x.price_id}-${currency.id}`
)
) }),
data && items.some((x) => x.price_id === data.id) === false && /* @__PURE__ */ jsx(Grow, { in: true, children: /* @__PURE__ */ jsx(
Stack,
{
sx: {
mt: 1
},
children: /* @__PURE__ */ jsx(
ProductItem,
{
item: { quantity: 1, price: data, price_id: data.id, cross_sell: true },
items,
trialInDays,
currency,
trialEnd,
onUpsell: noop,
onDownsell: noop,
children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
sx: {
alignItems: "center",
justifyContent: "space-between",
width: 1
},
children: [
/* @__PURE__ */ jsx(Typography, { children: crossSellBehavior === "required" && /* @__PURE__ */ jsx(Status, { label: t("payment.checkout.required"), color: "info", variant: "outlined", sx: { mr: 1 } }) }),
/* @__PURE__ */ jsx(
LoadingButton,
{
size: "small",
loadingPosition: "end",
endIcon: null,
color: crossSellBehavior === "required" ? "info" : "info",
variant: crossSellBehavior === "required" ? "text" : "text",
loading: state.loading,
onClick: handleApplyCrossSell,
children: t("payment.checkout.cross_sell.add")
}
)
]
}
)
}
)
}
) })
]
}
);
return /* @__PURE__ */ jsx(Fade, { in: true, children: /* @__PURE__ */ jsxs(Stack, { className: "cko-product", direction: "column", ...rest, children: [
/* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
alignItems: "center",
mb: 2.5
},
children: [
/* @__PURE__ */ jsx(
Typography,
{
title: t("payment.checkout.orderSummary"),
sx: {
color: "text.primary",
fontSize: {
xs: "18px",
md: "24px"
},
fontWeight: "700",
lineHeight: "32px"
},
children: action || t("payment.checkout.orderSummary")
}
),
!settings.livemode && /* @__PURE__ */ jsx(Livemode, {})
]
}
),
isMobile && !donationSettings ? /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs(
Stack,
{
onClick: () => setState({ expanded: !state.expanded }),
sx: {
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
mb: 1.5
},
children: [
/* @__PURE__ */ jsx(Typography, { children: t("payment.checkout.productListTotal", { total: items.length }) }),
/* @__PURE__ */ jsx(ExpandMore, { expand: state.expanded, "aria-expanded": state.expanded, "aria-label": "show more", children: /* @__PURE__ */ jsx(ExpandMoreIcon, {}) })
]
}
),
/* @__PURE__ */ jsx(Collapse, { in: state.expanded || !isMobile, timeout: "auto", unmountOnExit: true, children: ProductCardList })
] }) : ProductCardList,
/* @__PURE__ */ jsx(Divider, { sx: { mt: 2.5, mb: 2.5 } }),
staking > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 1,
sx: {
justifyContent: "space-between",
alignItems: "center"
},
children: [
/* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center"
},
children: [
/* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary" }, children: t("payment.checkout.paymentRequired") }),
/* @__PURE__ */ jsx(Tooltip, { title: t("payment.checkout.stakingConfirm"), placement: "top", sx: { maxWidth: "150px" }, children: /* @__PURE__ */ jsx(HelpOutline, { fontSize: "small", sx: { color: "text.lighter" } }) })
]
}
),
/* @__PURE__ */ jsx(Typography, { children: headlines.amount })
]
}
),
/* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 1,
sx: {
justifyContent: "space-between",
alignItems: "center"
},
children: [
/* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center"
},
children: [
/* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary" }, children: t("payment.checkout.staking.title") }),
/* @__PURE__ */ jsx(Tooltip, { title: t("payment.checkout.staking.tooltip"), placement: "top", sx: { maxWidth: "150px" }, children: /* @__PURE__ */ jsx(HelpOutline, { fontSize: "small", sx: { color: "text.lighter" } }) })
]
}
),
/* @__PURE__ */ jsxs(Typography, { children: [
formatAmount(staking, currency.decimal),
" ",
currency.symbol
] })
]
}
)
] }),
/* @__PURE__ */ jsxs(
Stack,
{
sx: {
display: "flex",
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
width: "100%"
},
children: [
/* @__PURE__ */ jsxs(Box, { className: "base-label", children: [
t("common.total"),
" "
] }),
/* @__PURE__ */ jsx(PaymentAmount, { amount: `${totalAmount} ${currency.symbol}`, sx: { fontSize: "16px" } })
]
}
),
headlines.then && headlines.showThen && /* @__PURE__ */ jsx(
Typography,
{
component: "div",
sx: { fontSize: "0.7875rem", color: "text.lighter", textAlign: "right", margin: "-2px 0 8px" },
children: headlines.then
}
)
] }) });
}