UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

400 lines (399 loc) 13.7 kB
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 } ) ] }) }); }