UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

579 lines (578 loc) 21.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); module.exports = PaymentSummary; var _jsxRuntime = require("react/jsx-runtime"); var _context = require("@arcblock/ux/lib/Locale/context"); var _iconsMaterial = require("@mui/icons-material"); var _material = require("@mui/material"); var _util = require("@ocap/util"); var _ahooks = require("ahooks"); var _noop = _interopRequireDefault(require("lodash/noop")); var _useBus = _interopRequireDefault(require("use-bus")); var _ExpandMore = _interopRequireDefault(require("@mui/icons-material/ExpandMore")); var _styles = require("@mui/material/styles"); var _status = _interopRequireDefault(require("../components/status")); var _api = _interopRequireDefault(require("../libs/api")); var _util2 = require("../libs/util"); var _amount = _interopRequireDefault(require("./amount")); var _productDonation = _interopRequireDefault(require("./product-donation")); var _productItem = _interopRequireDefault(require("./product-item")); var _livemode = _interopRequireDefault(require("../components/livemode")); var _payment = require("../contexts/payment"); var _mobile = require("../hooks/mobile"); var _loadingButton = _interopRequireDefault(require("../components/loading-button")); var _promotionCode = _interopRequireDefault(require("../components/promotion-code")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const ExpandMore = (0, _styles.styled)(props => { const { expand, ...other } = props; return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.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, skipError = true) { try { const { data } = await _api.default.get(`/api/checkout-sessions/${id}/cross-sell?skipError=${skipError}`); if (!data.error) { return data; } return null; } catch (err) { return null; } } function getStakingSetup(items, currency, billingThreshold = 0) { const staking = { licensed: new _util.BN(0), metered: new _util.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 (0, _util.fromTokenToUnit)(billingThreshold, currency.decimal).toString(); } items.forEach(x => { const price = x.upsell_price || x.price; const unit = (0, _util2.getPriceUintAmountByCurrency)(price, currency); const amount = new _util.BN(unit).mul(new _util.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"; } function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell = _noop.default, onDownsell = _noop.default, onQuantityChange = _noop.default, onApplyCrossSell = _noop.default, onCancelCrossSell = _noop.default, onChangeAmount = _noop.default, checkoutSessionId = "", crossSellBehavior = "", showStaking = false, donationSettings = void 0, action = "", trialEnd = 0, completed = false, checkoutSession = void 0, paymentMethods = [], onPromotionUpdate = _noop.default, showFeatures = false, ...rest }) { const { t, locale } = (0, _context.useLocaleContext)(); const { isMobile } = (0, _mobile.useMobile)(); const { paymentState, ...settings } = (0, _payment.usePaymentContext)(); const [state, setState] = (0, _ahooks.useSetState)({ loading: false, shake: false, expanded: items?.length < 3 }); const { data, runAsync } = (0, _ahooks.useRequest)(skipError => checkoutSessionId ? fetchCrossSell(checkoutSessionId, skipError) : Promise.resolve(null)); const sessionDiscounts = checkoutSession?.discounts || []; const allowPromotionCodes = !!checkoutSession?.allow_promotion_codes; const hasDiscounts = sessionDiscounts?.length > 0; const discountCurrency = paymentMethods && checkoutSession ? (0, _util2.findCurrency)(paymentMethods, hasDiscounts ? checkoutSession?.currency_id || currency.id : currency.id) || settings.settings?.baseCurrency : currency; const headlines = (0, _util2.formatCheckoutHeadlines)(items, discountCurrency, { trialEnd, trialInDays }, locale); const staking = showStaking ? getStakingSetup(items, discountCurrency, billingThreshold) : "0"; const getAppliedPromotionCodes = () => { if (!sessionDiscounts?.length) return []; return sessionDiscounts.map(discount => ({ id: discount.promotion_code || discount.coupon, code: discount.verification_data?.code || "APPLIED", discount_amount: discount.discount_amount })); }; const handlePromotionUpdate = () => { onPromotionUpdate?.(); }; const handleRemovePromotion = async sessionId => { if (paymentState.paying || paymentState.stripePaying) { return; } try { await _api.default.delete(`/api/checkout-sessions/${sessionId}/remove-promotion`); onPromotionUpdate?.(); } catch (err) { console.error("Failed to remove promotion code:", err); } }; const discountAmount = new _util.BN(checkoutSession?.total_details?.amount_discount || "0"); const subtotalAmount = (0, _util.fromUnitToToken)(new _util.BN((0, _util.fromTokenToUnit)(headlines.actualAmount, discountCurrency?.decimal)).add(new _util.BN(staking)).toString(), discountCurrency?.decimal); const totalAmount = (0, _util.fromUnitToToken)(new _util.BN((0, _util.fromTokenToUnit)(subtotalAmount, discountCurrency?.decimal)).sub(discountAmount).toString(), discountCurrency?.decimal); (0, _useBus.default)("error.REQUIRE_CROSS_SELL", () => { setState({ shake: true }); setTimeout(() => { setState({ shake: false }); }, 1e3); }, []); const handleUpsell = async (from, to) => { await onUpsell(from, to); runAsync(false); }; const handleQuantityChange = async (itemId, quantity) => { await onQuantityChange(itemId, quantity); runAsync(false); }; const handleDownsell = async from => { await onDownsell(from); runAsync(false); }; 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 hasSubTotal = +staking > 0 || allowPromotionCodes; const ProductCardList = /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { className: "cko-product-list", sx: { flex: "0 1 auto", overflow: "auto" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { spacing: { xs: 1, sm: 2 }, children: items.map(x => x.price.custom_unit_amount && onChangeAmount && donationSettings ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_productDonation.default, { item: x, settings: donationSettings, onChange: onChangeAmount, currency: discountCurrency }, `${x.price_id}-${discountCurrency.id}`) : /* @__PURE__ */(0, _jsxRuntime.jsx)(_productItem.default, { item: x, items, trialInDays, trialEnd, currency: discountCurrency, onUpsell: handleUpsell, onDownsell: handleDownsell, adjustableQuantity: x.adjustable_quantity, completed, showFeatures, onQuantityChange: handleQuantityChange, children: x.cross_sell && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { alignItems: "center", justifyContent: "space-between", width: 1 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_loadingButton.default, { 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}-${discountCurrency.id}`)) }), data && items.some(x => x.price_id === data.id) === false && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grow, { in: true, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { sx: { mt: 1 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_productItem.default, { item: { quantity: 1, price: data, price_id: data.id, cross_sell: true }, items, trialInDays, currency: discountCurrency, trialEnd, onUpsell: _noop.default, onDownsell: _noop.default, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { alignItems: "center", justifyContent: "space-between", width: 1 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: crossSellBehavior === "required" && /* @__PURE__ */(0, _jsxRuntime.jsx)(_status.default, { label: t("payment.checkout.required"), color: "info", variant: "outlined", sx: { mr: 1 } }) }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_loadingButton.default, { 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__ */(0, _jsxRuntime.jsx)(_material.Fade, { in: true, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { className: "cko-product", direction: "column", ...rest, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { display: "flex", alignItems: "center", mb: 2.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.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__ */(0, _jsxRuntime.jsx)(_livemode.default, {})] }), isMobile && !donationSettings ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { onClick: () => setState({ expanded: !state.expanded }), sx: { justifyContent: "space-between", flexDirection: "row", alignItems: "center", mb: 1.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: t("payment.checkout.productListTotal", { total: items.length }) }), /* @__PURE__ */(0, _jsxRuntime.jsx)(ExpandMore, { expand: state.expanded, "aria-expanded": state.expanded, "aria-label": "show more", children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_ExpandMore.default, {}) })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Collapse, { in: state.expanded || !isMobile, timeout: "auto", unmountOnExit: true, children: ProductCardList })] }) : ProductCardList, /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { mt: 2.5, mb: 2.5 } }), +staking > 0 && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 1, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { justifyContent: "space-between", alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 0.5, sx: { alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.secondary" }, children: t("payment.checkout.paymentRequired") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Tooltip, { title: t("payment.checkout.stakingConfirm"), placement: "top", sx: { maxWidth: "150px" }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.HelpOutline, { fontSize: "small", sx: { color: "text.lighter" } }) })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: headlines.amount })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { justifyContent: "space-between", alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 0.5, sx: { alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.secondary" }, children: t("payment.checkout.staking.title") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Tooltip, { title: t("payment.checkout.staking.tooltip"), placement: "top", sx: { maxWidth: "150px" }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.HelpOutline, { fontSize: "small", sx: { color: "text.lighter" } }) })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { children: [(0, _util2.formatAmount)(staking, discountCurrency.decimal), " ", discountCurrency.symbol] })] })] }), (allowPromotionCodes || hasDiscounts) && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { justifyContent: "space-between", alignItems: "center", ...(+staking > 0 && { borderTop: "1px solid", borderColor: "divider", pt: 1, mt: 1 }) }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { className: "base-label", children: t("common.subtotal") }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { children: [(0, _util2.formatNumber)(subtotalAmount), " ", discountCurrency.symbol] })] }), allowPromotionCodes && !hasDiscounts && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { mt: 1 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_promotionCode.default, { checkoutSessionId: checkoutSession.id, initialAppliedCodes: getAppliedPromotionCodes(), disabled: completed, onUpdate: handlePromotionUpdate, currencyId: currency.id }) }), hasDiscounts && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { children: sessionDiscounts.map(discount => { const promotionCodeInfo = discount.promotion_code_details; const couponInfo = discount.coupon_details; const discountDescription = couponInfo ? (0, _util2.formatCouponTerms)(couponInfo, discountCurrency, locale) : ""; const notSupported = discountDescription === t("payment.checkout.coupon.noDiscount"); return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { justifyContent: "space-between", alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { alignItems: "center", backgroundColor: "grey.100", width: "fit-content", px: 1, py: 1, borderRadius: 1 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { sx: { fontWeight: "medium", display: "flex", alignItems: "center", gap: 0.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.LocalOffer, { sx: { color: "warning.main", fontSize: "small" } }), promotionCodeInfo?.code || discount.verification_data?.code || t("payment.checkout.discount")] }), !completed && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { size: "small", disabled: paymentState.paying || paymentState.stripePaying, onClick: () => handleRemovePromotion(checkoutSessionId), sx: { minWidth: "auto", width: 16, height: 16, color: "text.secondary", "&.Mui-disabled": { color: "text.disabled" } }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.Close, { sx: { fontSize: 14 } }) })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { sx: { color: "text.secondary" }, children: ["-", (0, _util2.formatAmount)(discount.discount_amount || "0", discountCurrency.decimal), " ", discountCurrency.symbol] })] }), discountDescription && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontSize: "small", color: notSupported ? "error.main" : "text.secondary", mt: 0.5 }, children: discountDescription })] }, discount.promotion_code || discount.coupon || `discount-${discount.discount_amount}`); }) }), hasSubTotal && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { my: 1 } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { display: "flex", justifyContent: "space-between", flexDirection: "row", alignItems: "center", width: "100%" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { className: "base-label", children: [t("common.total"), " "] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_amount.default, { amount: `${totalAmount} ${discountCurrency.symbol}`, sx: { fontSize: "16px" } })] }), headlines.then && headlines.showThen && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "div", sx: { fontSize: "0.7875rem", color: "text.lighter", textAlign: "right", margin: "-2px 0 8px" }, children: headlines.then })] }) }); }