UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

530 lines (529 loc) 20.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); module.exports = CompositePanel; var _jsxRuntime = require("react/jsx-runtime"); var _react = require("react"); var _ExpandMore = _interopRequireDefault(require("@mui/icons-material/ExpandMore")); var _TrendingDown = _interopRequireDefault(require("@mui/icons-material/TrendingDown")); var _material = require("@mui/material"); var _context = require("@arcblock/ux/lib/Locale/context"); var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast")); var _paymentReactHeadless = require("@blocklet/payment-react-headless"); var _mobile = require("../../../hooks/mobile"); var _format = require("../../utils/format"); var _productItemCard = _interopRequireDefault(require("../../components/left/product-item-card")); var _billingToggle = _interopRequireDefault(require("../../components/left/billing-toggle")); var _crossSellCard = _interopRequireDefault(require("../../components/left/cross-sell-card")); var _exchangeRateFooter = _interopRequireDefault(require("../../components/shared/exchange-rate-footer")); var _trialInfo = _interopRequireDefault(require("../../components/left/trial-info")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function CompositePanel() { const { t } = (0, _context.useLocaleContext)(); const { session } = (0, _paymentReactHeadless.useSessionContext)(); const { currency, isStripe, switching: currencySwitching } = (0, _paymentReactHeadless.usePaymentMethodContext)(); const { livemode } = (0, _paymentReactHeadless.useCheckoutStatus)(); const { product, pageInfo } = (0, _paymentReactHeadless.useProduct)(); const lineItems = (0, _paymentReactHeadless.useLineItems)(); const billingInterval = (0, _paymentReactHeadless.useBillingInterval)(); const pricing = (0, _paymentReactHeadless.usePricingFeature)(); const rate = (0, _paymentReactHeadless.useExchangeRate)(); const slippage = (0, _paymentReactHeadless.useSlippage)(); const { isMobile } = (0, _mobile.useMobile)(); const mode = session?.mode || "payment"; const discounts = session?.discounts || []; const appName = session?.app_name || session?.payment_link?.app_name || ""; const appLogo = session?.app_logo || session?.payment_link?.app_logo || ""; const showItemsCollapse = isMobile || lineItems.items.length >= 4; const [itemsExpanded, setItemsExpanded] = (0, _react.useState)(isMobile ? lineItems.items.length <= 1 : lineItems.items.length < 4); const crossSellNotAdded = lineItems.crossSellItem && !lineItems.items.some(i => i.price_id === lineItems.crossSellItem?.id); const nonCrossSellItems = lineItems.items.filter(i => !i.cross_sell); const itemsWithUpsell = lineItems.items.filter(i => i.price?.upsell?.upsells_to); const upsellPrimaryItem = itemsWithUpsell.length === 1 ? itemsWithUpsell[0] : null; const upsellTarget = upsellPrimaryItem ? upsellPrimaryItem.price?.upsell?.upsells_to : null; const canUpsell = nonCrossSellItems.length <= 1; const hasTopUpsell = canUpsell && !!upsellPrimaryItem && ["subscription", "setup"].includes(mode); const isUpselled = !!upsellPrimaryItem?.upsell_price; const [upsellSwitching, setUpsellSwitching] = (0, _react.useState)(false); const [pendingUpsell, setPendingUpsell] = (0, _react.useState)(null); const visualIsUpselled = pendingUpsell !== null ? pendingUpsell : isUpselled; const currentInterval = hasTopUpsell ? upsellPrimaryItem.price?.recurring?.interval : null; const upsellInterval = hasTopUpsell ? upsellTarget?.recurring?.interval : null; let upsellSavings = 0; if (hasTopUpsell && currentInterval && upsellInterval) { const fromAmt = parseFloat(upsellPrimaryItem.price?.base_amount || upsellPrimaryItem.price?.unit_amount || "0"); const toAmt = parseFloat(upsellTarget?.base_amount || upsellTarget?.unit_amount || "0"); const yearMap = { day: 365, week: 52, month: 12, year: 1 }; const fromY = fromAmt * (yearMap[currentInterval] || 1); const toY = toAmt * (yearMap[upsellInterval] || 1); if (fromY > toY && fromY > 0) upsellSavings = Math.round((fromY - toY) / fromY * 100); } const prevRateStatusRef = (0, _react.useRef)(rate.status); (0, _react.useEffect)(() => { if (prevRateStatusRef.current !== "unavailable" && rate.status === "unavailable" && !isStripe) { _Toast.default.error(t("payment.dynamicPricing.unavailable.message")); } prevRateStatusRef.current = rate.status; }, [rate.status, isStripe, t]); const headerMeta = (0, _format.getSessionHeaderMeta)(t, session, product, lineItems.items); const isMultiItem = lineItems.items.length > 1; const activeSx = { bgcolor: "primary.main", color: theme => (0, _format.primaryContrastColor)(theme), boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)" }; const inactiveSx = { color: "text.secondary", "&:hover": { color: "text.primary" } }; const capsuleBtnSx = active => ({ px: 3.5, py: 1, borderRadius: "9999px", cursor: "pointer", transition: "all 0.3s ease", userSelect: "none", ...(active ? activeSx : inactiveSx) }); return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { display: "flex", flexDirection: "column", flex: 1 }, children: [!isMobile && (appName || appLogo) && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { mb: 5 }, children: [appLogo && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Avatar, { src: appLogo, alt: appName, sx: { width: 48, height: 48, borderRadius: "16px", bgcolor: "background.paper", boxShadow: 1, border: "1px solid", borderColor: "divider" } }), appName && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontWeight: 600, fontSize: 15, color: "text.primary" }, children: appName })] }), !isMobile && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { flexGrow: 1, flexShrink: 1, flexBasis: 0 } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { flexShrink: 0 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { mb: { xs: 3, md: 4 } }, children: [isMobile && (appName || appLogo) && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { mb: 1 }, children: [appLogo && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Avatar, { src: appLogo, alt: appName, sx: { width: 24, height: 24, borderRadius: "6px" } }), appName && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontWeight: 600, fontSize: 13, color: "text.secondary" }, children: appName })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { mb: 1.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "span", sx: { fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", lineHeight: 1, textTransform: "uppercase", color: "primary.main", bgcolor: theme => theme.palette.mode === "dark" ? `${theme.palette.primary.main}1A` : `${theme.palette.primary.main}0D`, px: 1, py: 0.5, borderRadius: "4px" }, children: headerMeta.badgeLabel }), pricing.trial.active && pricing.trial.days > 0 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "span", sx: { fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", lineHeight: 1, textTransform: "uppercase", color: "primary.main", bgcolor: theme => theme.palette.mode === "dark" ? `${theme.palette.primary.main}1A` : `${theme.palette.primary.main}0D`, px: 1, py: 0.5, borderRadius: "4px" }, children: (0, _format.formatTrialText)(t, pricing.trial.days, pricing.trial.afterTrialInterval || "day") }), !livemode && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "span", sx: { fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", lineHeight: 1, textTransform: "uppercase", color: theme => theme.palette.mode === "dark" ? theme.palette.grey[500] : theme.palette.grey[400], bgcolor: theme => theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : theme.palette.grey[100], px: 1, py: 0.5, borderRadius: "4px" }, children: t("common.livemode") })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontWeight: 800, fontSize: { xs: 24, md: 36 }, lineHeight: 1.1, letterSpacing: "-0.03em", color: "text.primary", mb: 0.75 }, children: isMultiItem ? (0, _format.tSafe)(t, "payment.checkout.orderSummary", "Order Summary") : headerMeta.title }), (isMultiItem || headerMeta.subtitle) && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.secondary", fontSize: { xs: 14, md: 16 }, fontWeight: 500, lineHeight: 1.5 }, children: isMultiItem ? (0, _format.tSafe)(t, "payment.checkout.orderSummarySubtitle", "Items included in this purchase") : headerMeta.subtitle }), hasTopUpsell && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", spacing: 2, sx: { mt: 2.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", sx: { bgcolor: "background.paper", borderRadius: "9999px", border: "1px solid", borderColor: "divider", p: "4px", display: "inline-flex", boxShadow: theme => theme.palette.mode === "dark" ? "0 1px 2px 0 rgba(0,0,0,0.3)" : "0 1px 2px 0 rgba(0,0,0,0.05)" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { onClick: async () => { if (isUpselled && !upsellSwitching) { setPendingUpsell(false); setUpsellSwitching(true); try { await lineItems.downsell(upsellPrimaryItem.upsell_price?.id || upsellPrimaryItem.price_id); } catch (err) { setPendingUpsell(null); _Toast.default.error(err?.response?.data?.error || err?.message || "Failed"); } finally { setUpsellSwitching(false); setPendingUpsell(null); } } }, sx: capsuleBtnSx(!visualIsUpselled), children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "span", sx: { fontSize: 14, fontWeight: 700, color: "inherit", lineHeight: 1 }, children: t(_format.INTERVAL_LOCALE_KEY[currentInterval] || "") }) }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { onClick: async () => { if (!isUpselled && !upsellSwitching) { setPendingUpsell(true); setUpsellSwitching(true); try { await lineItems.upsell(upsellPrimaryItem.price_id, upsellTarget.id); } catch (err) { setPendingUpsell(null); _Toast.default.error(err?.response?.data?.error || err?.message || "Failed"); } finally { setUpsellSwitching(false); setPendingUpsell(null); } } }, sx: capsuleBtnSx(visualIsUpselled), children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "span", sx: { fontSize: 14, fontWeight: 700, color: "inherit", lineHeight: 1 }, children: t(_format.INTERVAL_LOCALE_KEY[upsellInterval] || "") }) })] }), upsellSavings > 0 && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", spacing: 0.75, sx: { px: 1.5, py: 0.75, bgcolor: theme => theme.palette.mode === "dark" ? "rgba(18,184,134,0.1)" : "#ebfef5", color: "#12b886", fontSize: 11, fontWeight: 700, borderRadius: "9999px", border: "1px solid", borderColor: theme => theme.palette.mode === "dark" ? "rgba(18,184,134,0.2)" : "#d3f9e8", textTransform: "uppercase", letterSpacing: "0.05em" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_TrendingDown.default, { sx: { fontSize: 14 } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { component: "span", sx: { fontSize: "inherit", fontWeight: "inherit", color: "inherit", lineHeight: 1 }, children: ["SAVE ", upsellSavings, "%"] })] })] })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_billingToggle.default, { billingInterval })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { flexGrow: 0, flexShrink: 1, flexBasis: "auto", minHeight: { md: 0 }, overflowY: { md: "auto" }, "&::-webkit-scrollbar": { display: "none" }, scrollbarWidth: "none" }, children: [showItemsCollapse && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", onClick: () => setItemsExpanded(!itemsExpanded), sx: { cursor: "pointer", mb: 1.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontSize: 14, fontWeight: 600 }, children: t("payment.checkout.productListTotal", { total: lineItems.items.length }) }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_ExpandMore.default, { sx: { transform: itemsExpanded ? "rotate(180deg)" : "rotate(0deg)", transition: "0.3s" } })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Collapse, { in: itemsExpanded || !showItemsCollapse, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { spacing: 2, sx: { mb: 2 }, children: lineItems.items.map(item => /* @__PURE__ */(0, _jsxRuntime.jsx)(_productItemCard.default, { item, currency, discounts, exchangeRate: rate.value, onQuantityChange: lineItems.updateQuantity, onUpsell: lineItems.upsell, onDownsell: lineItems.downsell, trialActive: pricing.trial.active, trialDays: pricing.trial.days, hideUpsell: hasTopUpsell || !canUpsell, isRateLoading: currencySwitching || rate.hasDynamicPricing && rate.status === "loading", showFeatures: pageInfo?.showProductFeatures ?? false, t, children: item.cross_sell && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { size: "small", color: "error", variant: "text", onClick: lineItems.removeCrossSell, sx: { mt: 1, ml: -0.5, textTransform: "none", fontSize: 12 }, children: t("payment.checkout.cross_sell.remove") }) }, item.id)) }), crossSellNotAdded && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { mb: 2 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_crossSellCard.default, { crossSellItem: lineItems.crossSellItem, currency, exchangeRate: rate.value, crossSellRequired: lineItems.crossSellRequired, onAdd: lineItems.addCrossSell }) })] })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { flexShrink: 0 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_trialInfo.default, { trial: { active: pricing.trial.active, days: pricing.trial.days, afterTrialPrice: pricing.trial.afterTrialPrice, afterTrialInterval: pricing.trial.afterTrialInterval }, mode, items: lineItems.items }) }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { flexGrow: 1, flexShrink: 1, flexBasis: 0 } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { flexShrink: 0 }, children: [rate.hasDynamicPricing && rate.status === "unavailable" && !isStripe && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", alignItems: "center", spacing: 0.75, sx: { mb: 2 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontSize: 13, color: "text.secondary", fontWeight: 500 }, children: t("payment.dynamicPricing.unavailable.title") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { component: "span", onClick: rate.refresh, sx: { fontSize: 13, color: "primary.main", fontWeight: 600, cursor: "pointer", "&:hover": { textDecoration: "underline" } }, children: t("payment.dynamicPricing.unavailable.retry") })] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_exchangeRateFooter.default, { hasDynamicPricing: rate.hasDynamicPricing, rate: { value: rate.value, display: rate.display, provider: rate.provider, providerDisplay: rate.providerDisplay, fetchedAt: rate.fetchedAt, status: rate.status }, slippage: { percent: slippage.percent, set: slippage.set }, currencySymbol: currency?.symbol || "", isSubscription: ["subscription", "setup"].includes(mode) })] })] }); }