@blocklet/payment-react
Version:
Reusable react components for payment kit v2
530 lines (529 loc) • 20.3 kB
JavaScript
"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)
})]
})]
});
}