UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

657 lines (656 loc) 25.7 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 _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast")); 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 _react = require("react"); var _status = _interopRequireDefault(require("../components/status")); var _api = _interopRequireDefault(require("../libs/api")); var _util2 = require("../libs/util"); 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 _dynamicPricing = require("../hooks/dynamic-pricing"); var _loadingButton = _interopRequireDefault(require("../components/loading-button")); var _dynamicPricingUnavailable = _interopRequireDefault(require("../components/dynamic-pricing-unavailable")); var _promotionSection = _interopRequireDefault(require("./summary-section/promotion-section")); var _totalSection = _interopRequireDefault(require("./summary-section/total-section")); 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, paymentIntent = void 0, paymentMethods = [], onPromotionUpdate = _noop.default, showFeatures = false, rateUnavailable = false, isRateLoading = false, // eslint-disable-next-line @typescript-eslint/no-unused-vars rateError: _rateError = void 0, // Technical errors are logged but not displayed to users onQuoteExpired = void 0, onRefreshRate = void 0, onSlippageChange = void 0, slippageConfig: slippageConfigProp = void 0, liveRate = void 0, liveQuoteSnapshot = void 0, isStripePayment = false, isSubscription: isSubscriptionProp = void 0, ...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 slippageConfig = slippageConfigProp ?? checkoutSession?.metadata?.slippage; const { hasDynamicPricing, isPriceLocked, quoteMeta, rateInfo, quoteLockedAt, currentSlippagePercent, rateDisplay, calculatedTokenAmount, calculatedDiscountAmount, calculateUsdDisplay, buildQuoteDetailRows } = (0, _dynamicPricing.useDynamicPricing)({ items, currency: discountCurrency, liveRate, liveQuoteSnapshot, checkoutSession, paymentIntent, locale, isStripePayment, isSubscription: isSubscriptionProp, slippageConfig, trialInDays, trialEnd, discounts: sessionDiscounts }); const headlines = (0, _util2.formatCheckoutHeadlines)(items, discountCurrency, { trialEnd, trialInDays }, locale, { exchangeRate: rateInfo.exchangeRate }); const staking = showStaking ? getStakingSetup(items, discountCurrency, billingThreshold) : "0"; const effectiveHasDynamicPricing = hasDynamicPricing && !isStripePayment; const hasRecurringItems = items.some(x => (x.upsell_price || x.price)?.type === "recurring"); const isTrialScenario = headlines.actualAmount === "0" && hasRecurringItems; const headlineAmountDisplay = (0, _react.useMemo)(() => { if (!effectiveHasDynamicPricing) { return headlines.amount; } if (isTrialScenario || !headlines.amount.includes(discountCurrency.symbol)) { return headlines.amount; } if (calculatedTokenAmount) { const displayAmount = (0, _util.fromUnitToToken)(calculatedTokenAmount, discountCurrency?.decimal); const formatted2 = (0, _util2.formatDynamicPrice)(displayAmount, true, 6); return `${formatted2} ${discountCurrency.symbol}`; } const formatted = (0, _util2.formatDynamicPrice)(headlines.actualAmount, true, 6); return `${formatted} ${discountCurrency.symbol}`; }, [headlines.amount, headlines.actualAmount, discountCurrency.symbol, discountCurrency?.decimal, effectiveHasDynamicPricing, calculatedTokenAmount, isTrialScenario]); const discountAmount = (0, _react.useMemo)(() => new _util.BN(checkoutSession?.total_details?.amount_discount || "0"), [checkoutSession?.total_details?.amount_discount]); const subtotalAmountUnit = new _util.BN((0, _util.fromTokenToUnit)(headlines.actualAmount, discountCurrency?.decimal)).add(new _util.BN(staking)).toString(); const subtotalAmount = (0, _util.fromUnitToToken)(subtotalAmountUnit, discountCurrency?.decimal); const totalAmountUnit = new _util.BN(subtotalAmountUnit).sub(discountAmount).toString(); const totalAmountValue = (0, _util.fromUnitToToken)(totalAmountUnit, discountCurrency?.decimal); const subtotalDisplay = (0, _react.useMemo)(() => { if (effectiveHasDynamicPricing && calculatedTokenAmount && !isTrialScenario) { const dynamicSubtotalUnit = new _util.BN(calculatedTokenAmount).add(new _util.BN(staking)).toString(); const displayAmount = (0, _util.fromUnitToToken)(dynamicSubtotalUnit, discountCurrency?.decimal); return (0, _util2.formatDynamicPrice)(displayAmount, true, 6); } return (0, _util2.formatDynamicPrice)(subtotalAmount, effectiveHasDynamicPricing, 6); }, [effectiveHasDynamicPricing, calculatedTokenAmount, staking, discountCurrency?.decimal, subtotalAmount, isTrialScenario]); const totalAmountDisplay = (0, _react.useMemo)(() => { if (effectiveHasDynamicPricing && calculatedTokenAmount && !isTrialScenario) { const effectiveDiscount = calculatedDiscountAmount ? new _util.BN(calculatedDiscountAmount) : discountAmount; const dynamicTotalUnit = new _util.BN(calculatedTokenAmount).add(new _util.BN(staking)).sub(effectiveDiscount).toString(); const displayAmount = (0, _util.fromUnitToToken)(dynamicTotalUnit, discountCurrency?.decimal); const numericValue = Number(displayAmount); if (Number.isFinite(numericValue) && numericValue >= 0) { return (0, _util2.formatDynamicPrice)(displayAmount, true, 6); } } if (isStripePayment && calculatedDiscountAmount && !isTrialScenario) { const effectiveDiscount = new _util.BN(calculatedDiscountAmount); const adjustedTotalUnit = new _util.BN(subtotalAmountUnit).sub(effectiveDiscount).toString(); const displayAmount = (0, _util.fromUnitToToken)(adjustedTotalUnit, discountCurrency?.decimal); const numericValue = Number(displayAmount); if (Number.isFinite(numericValue) && numericValue >= 0) { return (0, _util2.formatDynamicPrice)(displayAmount, false, 6); } } return (0, _util2.formatDynamicPrice)(totalAmountValue, effectiveHasDynamicPricing, 6); }, [effectiveHasDynamicPricing, calculatedTokenAmount, staking, discountAmount, calculatedDiscountAmount, discountCurrency?.decimal, totalAmountValue, isTrialScenario, isStripePayment, subtotalAmountUnit]); const totalAmountText = totalAmountDisplay === "\u2014" ? "\u2014" : `${totalAmountDisplay} ${discountCurrency.symbol}`; const totalUsdDisplay = (0, _react.useMemo)(() => { if (effectiveHasDynamicPricing && calculatedTokenAmount && !isTrialScenario) { const effectiveDiscount = calculatedDiscountAmount ? new _util.BN(calculatedDiscountAmount) : discountAmount; const dynamicTotalUnit = new _util.BN(calculatedTokenAmount).add(new _util.BN(staking)).sub(effectiveDiscount).toString(); const dynamicTotalToken = (0, _util.fromUnitToToken)(dynamicTotalUnit, discountCurrency?.decimal); return calculateUsdDisplay(dynamicTotalToken); } return calculateUsdDisplay(totalAmountValue); }, [effectiveHasDynamicPricing, calculatedTokenAmount, staking, discountAmount, calculatedDiscountAmount, discountCurrency?.decimal, totalAmountValue, calculateUsdDisplay, isTrialScenario]); const quoteDetailRows = buildQuoteDetailRows(t); const isSubscription = isSubscriptionProp ?? (checkoutSession?.mode === "subscription" || checkoutSession?.mode === "setup"); 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 expiredHandledRef = (0, _react.useRef)(false); (0, _react.useEffect)(() => { if (completed || expiredHandledRef.current) { return; } if (!liveQuoteSnapshot?.expires_at && !quoteMeta?.expiresAt) { return; } const currentTime = Math.floor(Date.now() / 1e3); const effectiveExpiresAt = liveQuoteSnapshot?.expires_at ?? quoteMeta?.expiresAt; const quoteRemaining = effectiveExpiresAt ? Math.max(0, effectiveExpiresAt - currentTime) : 0; const lockRemaining = quoteLockedAt ? Math.max(0, quoteLockedAt + 180 - currentTime) : 0; const hasExpiry = !!effectiveExpiresAt; const lockActive = lockRemaining > 0; if (hasExpiry && !lockActive && quoteRemaining <= 0) { expiredHandledRef.current = true; onQuoteExpired?.(); } }, [liveQuoteSnapshot?.expires_at, quoteMeta?.expiresAt, quoteLockedAt, completed, onQuoteExpired]); const handleSlippageChange = async newSlippageConfig => { if (!onSlippageChange) { return; } if (!checkoutSessionId) { onSlippageChange(newSlippageConfig); return; } try { await _api.default.put(`/api/checkout-sessions/${checkoutSessionId}/slippage`, { slippage_config: newSlippageConfig }); onSlippageChange(newSlippageConfig); if (onQuoteExpired) { await onQuoteExpired(true); } } catch (err) { console.error("Failed to update slippage", err); _Toast.default.error(err.response?.data?.error || (0, _util2.formatError)(err)); } }; (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, exchangeRate: rateInfo.exchangeRate, isStripePayment, isPriceLocked, isRateLoading, onUpsell: handleUpsell, onDownsell: handleDownsell, adjustableQuantity: x.adjustable_quantity, completed, showFeatures, onQuantityChange: handleQuantityChange, discounts: sessionDiscounts, calculatedDiscountAmount: calculatedDiscountAmount || (isStripePayment ? discountAmount.toString() : null), 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, exchangeRate: rateInfo.exchangeRate, isStripePayment, isPriceLocked, isRateLoading, 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") })] }) }) }) })] }); if (!discountCurrency || !items?.length) { return null; } 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, {})] }), effectiveHasDynamicPricing && rateUnavailable && /* @__PURE__ */(0, _jsxRuntime.jsx)(_dynamicPricingUnavailable.default, { sx: { mb: 2 }, onRetry: onRefreshRate }), 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: headlineAmountDisplay })] }), /* @__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: [subtotalDisplay, " ", discountCurrency.symbol] })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_promotionSection.default, { checkoutSessionId: checkoutSession?.id || checkoutSessionId, currency: discountCurrency, currencyId: currency.id, discounts: sessionDiscounts, allowPromotionCodes, completed, disabled: paymentState.paying || paymentState.stripePaying, onPromotionUpdate: handlePromotionUpdate, onRemovePromotion: handleRemovePromotion, calculatedDiscountAmount: calculatedDiscountAmount || (isStripePayment ? discountAmount.toString() : null), isRateLoading }), hasSubTotal && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { my: 1 } }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_totalSection.default, { totalAmountText, totalUsdDisplay, currency: discountCurrency, hasDynamicPricing, rateDisplay, rateInfo, quoteDetailRows, currentSlippagePercent, slippageConfig, isPriceLocked, isSubscription, completed, onSlippageChange: onSlippageChange ? handleSlippageChange : void 0, isStripePayment, isRateLoading, thenInfo: headlines.thenValue && headlines.showThen ? headlines.thenValue : void 0 })] }) }); }