UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

926 lines (925 loc) 37.1 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { useLocaleContext } from "@arcblock/ux/lib/Locale/context"; import { Box, Stack, Typography, IconButton, TextField, Alert, Chip } from "@mui/material"; import { Add, Remove, LocalOffer } from "@mui/icons-material"; import { useEffect, useMemo, useState } from "react"; import { useRequest } from "ahooks"; import { BN, fromTokenToUnit, fromUnitToToken } from "@ocap/util"; import LoadingAmount from "../components/loading-amount.js"; import Status from "../components/status.js"; import Switch from "../components/switch-button.js"; import { findCurrency, formatLineItemPricing, formatNumber, formatPrice, formatQuantityInventory, formatRecurring, formatUpsellSaving, formatAmount, formatCreditForCheckout, formatCreditAmount, formatBNStr, getUsdAmountFromBaseAmount, formatUsdAmount, formatDynamicPrice } from "../libs/util.js"; import ProductCard from "./product-card.js"; import dayjs from "../libs/dayjs.js"; import { usePaymentContext } from "../contexts/payment.js"; const getRecommendedQuantityFromUrl = (priceId) => { try { const urlParams = new URLSearchParams(window.location.search); const recommendedQuantity = urlParams.get(`qty_${priceId}`) || urlParams.get("qty"); return recommendedQuantity ? Math.max(1, parseInt(recommendedQuantity, 10)) : void 0; } catch { return void 0; } }; const getUserQuantityPreference = (userDid, priceId) => { try { const key = `quantity_preference_${userDid}_${priceId}`; const stored = localStorage.getItem(key); return stored ? Math.max(1, parseInt(stored, 10)) : void 0; } catch { return void 0; } }; const saveUserQuantityPreference = (userDid, priceId, quantity) => { try { const key = `quantity_preference_${userDid}_${priceId}`; localStorage.setItem(key, quantity.toString()); } catch { } }; export default function ProductItem({ item, items, trialInDays, trialEnd = 0, currency, mode = "normal", children = null, onUpsell, onDownsell, completed = false, adjustableQuantity = { enabled: false }, onQuantityChange = () => { }, showFeatures = false, exchangeRate = null, isStripePayment = false, isPriceLocked = false, isRateLoading = false, discounts = [], calculatedDiscountAmount = null }) { const { t, locale } = useLocaleContext(); const { settings, setPayable, session, api } = usePaymentContext(); const pricingSource = item.upsell_price || item.price; const isDynamicPricing = pricingSource?.pricing_type === "dynamic"; const pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays, exchangeRate }, locale); const saving = formatUpsellSaving(items, currency); const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : ""; const canUpsell = mode === "normal" && items.length === 1; const isTrial = trialInDays > 0 || trialEnd > dayjs().unix(); const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config; const creditAmount = isCreditProduct && item.price.metadata?.credit_config?.credit_amount ? Number(item.price.metadata.credit_config.credit_amount) : 0; const creditCurrency = isCreditProduct && item.price.metadata?.credit_config?.currency_id ? findCurrency(settings.paymentMethods, item.price.metadata.credit_config.currency_id) : null; const validDuration = item.price.metadata?.credit_config?.valid_duration_value; const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || "days"; const scheduleConfig = item.price.metadata?.credit_config?.schedule; const hasSchedule = scheduleConfig?.enabled && scheduleConfig?.delivery_mode && scheduleConfig.delivery_mode !== "invoice"; const userDid = session?.user?.did; const { data: pendingAmount } = useRequest( async () => { if (!isCreditProduct || !userDid) return null; try { const { data } = await api.get("/api/meter-events/pending-amount", { params: { customer_id: userDid, currency_id: creditCurrency?.id } }); return data?.[creditCurrency?.id || ""]; } catch (error) { console.warn("Failed to fetch pending amount:", error); return null; } }, { refreshDeps: [isCreditProduct, userDid, creditCurrency?.id] } ); const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal"; const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1); const quantityAvailable = Math.min( item.price?.quantity_limit_per_checkout ?? Infinity, item.price?.quantity_available ?? Infinity ); const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity; const getMinQuantityForPending = useMemo(() => { if (!isCreditProduct || !pendingAmount) return null; const pendingAmountBN = new BN(pendingAmount || "0"); if (!pendingAmountBN.gt(new BN(0))) return null; if (!creditAmount || creditAmount <= 0) return null; const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2); if (!creditAmountBN || creditAmountBN.isZero()) return null; return Math.ceil(pendingAmountBN.mul(new BN(100)).div(creditAmountBN).toNumber() / 100); }, [isCreditProduct, pendingAmount, creditAmount, creditCurrency?.decimal]); const initialQuantity = useMemo(() => { const urlQuantity = getRecommendedQuantityFromUrl(item.price.id); if (urlQuantity && urlQuantity > 0) { if (canAdjustQuantity && getMinQuantityForPending) { return Math.max(urlQuantity, getMinQuantityForPending, minQuantity); } return urlQuantity; } if (userDid) { const preferredQuantity = getUserQuantityPreference(userDid, item.price.id); if (preferredQuantity && preferredQuantity > 0) { if (canAdjustQuantity && getMinQuantityForPending) { return Math.max(preferredQuantity, getMinQuantityForPending, minQuantity); } return preferredQuantity; } } let baseQuantity = item.quantity; if (canAdjustQuantity && getMinQuantityForPending) { baseQuantity = Math.max(baseQuantity, getMinQuantityForPending, minQuantity); } return baseQuantity; }, [item.quantity, item.price.id, userDid, getMinQuantityForPending, canAdjustQuantity, minQuantity]); const [localQuantity, setLocalQuantity] = useState(initialQuantity); const localQuantityNum = localQuantity || 0; useEffect(() => { if (initialQuantity && initialQuantity > 0) { if (initialQuantity !== localQuantity) { setLocalQuantity(initialQuantity); } if (initialQuantity !== item.quantity) { onQuantityChange(item.price_id, initialQuantity); } if (isCreditProduct && pendingAmount && getMinQuantityForPending) { setPayable(initialQuantity >= getMinQuantityForPending); } else { setPayable(true); } } }, [initialQuantity, isCreditProduct, pendingAmount, getMinQuantityForPending]); const handleQuantityChange = (newQuantity) => { if (!newQuantity) { setLocalQuantity(void 0); setPayable(false); return; } if (newQuantity >= minQuantity && newQuantity <= maxQuantity) { if (formatQuantityInventory(item.price, newQuantity, locale)) { return; } if (isCreditProduct && pendingAmount && getMinQuantityForPending) { if (newQuantity < getMinQuantityForPending) { setPayable(false); setLocalQuantity(newQuantity); onQuantityChange(item.price_id, newQuantity); return; } } setPayable(true); setLocalQuantity(newQuantity); onQuantityChange(item.price_id, newQuantity); if (userDid && newQuantity > 0) { saveUserQuantityPreference(userDid, item.price?.id, newQuantity); } } }; const handleQuantityIncrease = () => { if (localQuantityNum < maxQuantity) { handleQuantityChange(localQuantityNum + 1); } }; const handleQuantityDecrease = () => { if (localQuantityNum > minQuantity) { handleQuantityChange(localQuantityNum - 1); } }; const handleQuantityInputChange = (event) => { const value = parseInt(event.target.value || "0", 10); if (!Number.isNaN(value)) { handleQuantityChange(value); } }; const formatCreditInfo = () => { if (!isCreditProduct) return null; const totalCreditStr = formatNumber(creditAmount * (localQuantity || 0)); const currencySymbol = creditCurrency?.symbol || "Credits"; const formattedTotalCredit = formatCreditForCheckout(totalCreditStr, currencySymbol, locale); const hasPendingAmount = pendingAmount && new BN(pendingAmount || "0").gt(new BN(0)); const isRecurring = item.price?.type === "recurring"; const hasExpiry = validDuration && validDuration > 0; const buildBaseParams = () => ({ amount: formattedTotalCredit, ...hasExpiry && { duration: validDuration, unit: t(`common.${validDurationUnit}`) }, ...isRecurring && { period: formatRecurring(item.price?.recurring, true, "per", locale) } }); const buildPendingParams = (pendingBN, availableAmount) => ({ amount: formatCreditForCheckout( formatBNStr(pendingBN.toString(), creditCurrency?.decimal || 2), currencySymbol, locale ), totalAmount: formattedTotalCredit, ...availableAmount && { availableAmount: formatCreditForCheckout( formatBNStr(availableAmount, creditCurrency?.decimal || 2), currencySymbol, locale ) }, ...hasExpiry && { duration: validDuration, unit: t(`common.${validDurationUnit}`) }, ...isRecurring && { period: formatRecurring(item.price?.recurring, true, "per", locale) } }); const getLocaleKey = (category, type2) => { const suffix = hasExpiry ? "WithExpiry" : ""; return `payment.checkout.credit.${category}.${type2}${suffix}`; }; if (!hasPendingAmount) { const type2 = isRecurring ? "recurring" : "oneTime"; return t(getLocaleKey("normal", type2), buildBaseParams()); } const pendingAmountBN = new BN(pendingAmount || "0"); const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2); const minQuantityNeeded = getMinQuantityForPending || 0; const currentPurchaseCreditBN = creditAmountBN.mul(new BN(localQuantity || 0)); const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString(); if (!new BN(actualAvailable).gt(new BN(0))) { return t("payment.checkout.credit.pending.notEnough", { amount: formatCreditForCheckout( formatBNStr(pendingAmountBN.toString(), creditCurrency?.decimal || 2), currencySymbol, locale ), quantity: formatNumber(minQuantityNeeded) }); } const type = isRecurring ? "recurringEnough" : "oneTimeEnough"; return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable)); }; const quantityForUsd = Number.isFinite(localQuantity) ? Number(localQuantity) : item.quantity || 0; const dynamicTokenDisplay = useMemo(() => { if (isStripePayment || !currency) { return null; } const baseAmount = pricingSource?.base_amount; const unitAmount = pricingSource?.unit_amount; if (isDynamicPricing && baseAmount !== void 0 && baseAmount !== null) { if (isPriceLocked) { const quotedAmount = item?.quoted_amount; if (quotedAmount) { const totalBN = new BN(quotedAmount); const tokenValue = fromUnitToToken(totalBN, currency.decimal); return `${formatDynamicPrice(tokenValue, true, 6)} ${currency.symbol}`; } } if (!exchangeRate) { return null; } const rate = Number(exchangeRate); if (rate <= 0 || Number.isNaN(rate)) { return null; } const usdAmount = Number(baseAmount); const tokenAmount = usdAmount / rate; const totalTokens = tokenAmount * (quantityForUsd || 1); return `${formatDynamicPrice(totalTokens, true, 6)} ${currency.symbol}`; } if (!isDynamicPricing && exchangeRate && unitAmount) { const rate = Number(exchangeRate); if (rate <= 0 || Number.isNaN(rate)) { return null; } const usdAmount = Number(unitAmount) / 100; const tokenAmount = usdAmount / rate; const totalTokens = tokenAmount * (quantityForUsd || 1); return `${formatDynamicPrice(totalTokens, true, 6)} ${currency.symbol}`; } return null; }, [isStripePayment, isDynamicPricing, currency, pricingSource, item, exchangeRate, quantityForUsd, isPriceLocked]); const formatScheduleInfo = () => { if (!hasSchedule || !scheduleConfig) return null; const totalCredit = creditAmount * (localQuantity || 0); const currencySymbol = creditCurrency?.symbol || "Credits"; const intervalUnit = scheduleConfig.interval_unit; const intervalValue = scheduleConfig.interval_value; let amountPerGrant; if (scheduleConfig.amount_per_grant) { amountPerGrant = Number(scheduleConfig.amount_per_grant) * (localQuantity || 1); } else { const billingIntervalUnit = item.price.recurring?.interval || "month"; const billingIntervalCount = item.price.recurring?.interval_count || 1; const unitToHours = { hour: 1, day: 24, week: 168, month: 720 // ~30 days }; const billingHours = unitToHours[billingIntervalUnit] * billingIntervalCount; const scheduleHours = unitToHours[intervalUnit] * intervalValue; const grantsPerPeriod = Math.floor(billingHours / scheduleHours); amountPerGrant = grantsPerPeriod > 0 ? totalCredit / grantsPerPeriod : totalCredit; } const formattedAmount = formatCreditAmount(formatNumber(amountPerGrant), currencySymbol); const intervalDisplay = intervalValue === 1 ? t(`common.${intervalUnit}`) : ` ${intervalValue} ${t(`common.${intervalUnit}s`)} `; const expireWithNext = scheduleConfig.expire_with_next_grant; if (expireWithNext) { return t("payment.checkout.credit.schedule.withRefresh", { amount: formattedAmount, interval: intervalDisplay }); } return t("payment.checkout.credit.schedule.periodic", { amount: formattedAmount, interval: intervalDisplay }); }; const primaryText = useMemo(() => { const price = item.upsell_price || item.price || {}; const isRecurring = price?.type === "recurring" && price?.recurring; if (isTrial) { return pricing.primary; } let displayAmount; if (!isStripePayment && dynamicTokenDisplay) { displayAmount = dynamicTokenDisplay; } else if (isDynamicPricing && !isStripePayment) { const baseAmount = pricingSource?.base_amount; if (baseAmount !== void 0 && baseAmount !== null) { const usdValue = Number(baseAmount); displayAmount = `\u2248 $${usdValue.toFixed(2)}`; } else { displayAmount = pricing.primary; } } else { displayAmount = pricing.primary; } if (isRecurring && price?.recurring?.usage_type !== "metered") { return `${displayAmount} ${price.recurring ? formatRecurring(price.recurring, false, "slash", locale) : ""}`; } return displayAmount; }, [isTrial, pricing, item, locale, dynamicTokenDisplay, isDynamicPricing, pricingSource, isStripePayment]); const usdReference = useMemo(() => { if (!currency || !isDynamicPricing || isStripePayment) { return null; } const baseAmount = pricingSource?.base_amount; const hasBaseAmount = baseAmount !== void 0 && baseAmount !== null; if (hasBaseAmount) { return getUsdAmountFromBaseAmount(baseAmount, quantityForUsd); } return null; }, [currency, pricingSource, quantityForUsd, isDynamicPricing, isStripePayment]); const usdReferenceDisplay = useMemo(() => formatUsdAmount(usdReference, locale), [usdReference, locale]); const upsellTokenDisplay = useMemo(() => { const upsellPrice = item.price?.upsell?.upsells_to; if (!upsellPrice) { return null; } if (isStripePayment && currency) { const baseAmount2 = upsellPrice?.base_amount; const unitAmount2 = upsellPrice?.unit_amount; let usdAmount2; if (baseAmount2 !== void 0 && baseAmount2 !== null) { usdAmount2 = Number(baseAmount2); } else if (unitAmount2) { usdAmount2 = Number(unitAmount2) / 100; } else { return null; } const recurring2 = upsellPrice?.recurring ? ` ${formatRecurring(upsellPrice.recurring, false, "slash", locale)}` : ""; return `${usdAmount2} ${currency.symbol}${recurring2}`; } if (!currency || !exchangeRate) { return null; } const rate = Number(exchangeRate); if (rate <= 0 || Number.isNaN(rate)) { return null; } const baseAmount = upsellPrice?.base_amount; const unitAmount = upsellPrice?.unit_amount; let usdAmount; if (baseAmount !== void 0 && baseAmount !== null) { usdAmount = Number(baseAmount); } else if (unitAmount) { usdAmount = Number(unitAmount) / 100; } else { return null; } const tokenAmount = usdAmount / rate; const recurring = upsellPrice?.recurring ? ` ${formatRecurring(upsellPrice.recurring, false, "slash", locale)}` : ""; return `${formatDynamicPrice(tokenAmount, true, 6)} ${currency.symbol}${recurring}`; }, [isStripePayment, currency, exchangeRate, item.price?.upsell?.upsells_to, locale]); const downsellTokenDisplay = useMemo(() => { if (!item.upsell_price_id || !item.price) { return null; } const originalPrice = item.price; if (isStripePayment && currency) { const baseAmount2 = originalPrice?.base_amount; const unitAmount2 = originalPrice?.unit_amount; let usdAmount2; if (baseAmount2 !== void 0 && baseAmount2 !== null) { usdAmount2 = Number(baseAmount2); } else if (unitAmount2) { usdAmount2 = Number(unitAmount2) / 100; } else { return null; } const recurring2 = originalPrice?.recurring ? ` ${formatRecurring(originalPrice.recurring, false, "slash", locale)}` : ""; return `${usdAmount2} ${currency.symbol}${recurring2}`; } if (!currency || !exchangeRate) { return null; } const rate = Number(exchangeRate); if (rate <= 0 || Number.isNaN(rate)) { return null; } const baseAmount = originalPrice?.base_amount; const unitAmount = originalPrice?.unit_amount; let usdAmount; if (baseAmount !== void 0 && baseAmount !== null) { usdAmount = Number(baseAmount); } else if (unitAmount) { usdAmount = Number(unitAmount) / 100; } else { return null; } const tokenAmount = usdAmount / rate; const recurring = originalPrice?.recurring ? ` ${formatRecurring(originalPrice.recurring, false, "slash", locale)}` : ""; return `${formatDynamicPrice(tokenAmount, true, 6)} ${currency.symbol}${recurring}`; }, [isStripePayment, currency, exchangeRate, item.price, item.upsell_price_id, locale]); const displayItemDiscountAmount = useMemo(() => { if (isTrial && item.price?.type === "recurring" || item.price?.recurring?.usage_type === "metered") { return null; } if (!discounts?.length) { return null; } const couponDetails = discounts[0]?.coupon_details; if (isStripePayment && couponDetails) { if (!item.discountable) { return null; } const isDynamic = pricingSource?.pricing_type === "dynamic"; const baseAmount = pricingSource?.base_amount; const unitAmount = pricingSource?.unit_amount; let amountCents; if (isDynamic && baseAmount !== void 0 && baseAmount !== null) { amountCents = Number(baseAmount) * 100; } else if (unitAmount) { amountCents = Number(unitAmount); } else { return null; } const quantity = localQuantity || item.quantity || 1; const itemSubtotalCents = amountCents * quantity; if (couponDetails.percent_off && couponDetails.percent_off > 0) { const discountCents = Math.round(itemSubtotalCents * couponDetails.percent_off / 100); return discountCents.toString(); } if (couponDetails.amount_off && items.length === 1 && calculatedDiscountAmount) { return calculatedDiscountAmount; } return null; } if (isDynamicPricing && exchangeRate && couponDetails) { const rateNum = Number(exchangeRate); if (rateNum <= 0) { return null; } if (!item.discountable) { return null; } const baseAmount = pricingSource?.base_amount; if (!baseAmount) { return null; } const quantity = localQuantity || item.quantity || 1; const itemTokenAmount = Number(baseAmount) * quantity / rateNum; if (couponDetails.percent_off && couponDetails.percent_off > 0) { const discountAmount = itemTokenAmount * couponDetails.percent_off / 100; const discountAmountUnit = fromTokenToUnit( discountAmount.toFixed(currency.decimal || 8), currency.decimal || 8 ); return discountAmountUnit.toString(); } if (couponDetails.amount_off && items.length === 1 && calculatedDiscountAmount) { return calculatedDiscountAmount; } } if (items.length === 1 && calculatedDiscountAmount) { return calculatedDiscountAmount; } return null; }, [ isTrial, discounts, isStripePayment, isDynamicPricing, exchangeRate, item, pricingSource, localQuantity, currency.decimal, items.length, calculatedDiscountAmount ]); const discountCodeDisplay = useMemo(() => { if (!discounts?.length) return null; const discount = discounts[0]; return discount.promotion_code_details?.code || discount.verification_data?.code || "DISCOUNT"; }, [discounts]); const quantityInventoryError = formatQuantityInventory(item.price, localQuantityNum, locale); const features = item.price?.product?.features || []; return /* @__PURE__ */ jsxs( Stack, { direction: "column", spacing: 1, className: "product-item", sx: { alignItems: "flex-start", width: "100%" }, children: [ /* @__PURE__ */ jsxs( Stack, { direction: "column", className: "product-item-content", sx: { alignItems: "flex-start", width: "100%" }, children: [ /* @__PURE__ */ jsxs( Stack, { direction: "row", spacing: 0.5, sx: { alignItems: "center", flexWrap: "wrap", justifyContent: "space-between", width: "100%" }, children: [ /* @__PURE__ */ jsx( ProductCard, { logo: item.price?.product?.images[0], name: item.price?.product?.name, extra: /* @__PURE__ */ jsx( Box, { sx: { display: "flex", alignItems: "center" }, children: item.price?.type === "recurring" && item.price?.recurring ? [pricing.quantity, t("common.billed", { rule: `${formatRecurring(item.upsell_price?.recurring || item.price?.recurring, true, "per", locale)} ${metered}` })].filter(Boolean).join(", ") : pricing.quantity } ) } ), /* @__PURE__ */ jsxs( Stack, { direction: "column", sx: { alignItems: "flex-end", flex: 1 }, children: [ /* @__PURE__ */ jsx( LoadingAmount, { value: primaryText, loading: isRateLoading, skeletonWidth: 80, height: 20, sx: { color: "text.primary", fontWeight: 500 } } ), isDynamicPricing && !isStripePayment && !isTrial && usdReferenceDisplay && /* @__PURE__ */ jsxs(Typography, { sx: { fontSize: "0.74375rem", color: "text.lighter" }, children: [ "\u2248 $", usdReferenceDisplay ] }), pricing.secondary && !isTrial && /* @__PURE__ */ jsx(Typography, { sx: { fontSize: "0.74375rem", color: "text.lighter" }, children: pricing.secondary }) ] } ) ] } ), !(isTrial && item.price?.type === "recurring" || item.price?.recurring?.usage_type === "metered") && (displayItemDiscountAmount || item.discount_amounts && item.discount_amounts.length > 0) && /* @__PURE__ */ jsx( Stack, { direction: "row", spacing: 1, sx: { mt: 1, alignItems: "center", opacity: isRateLoading ? 0 : 1, transition: "opacity 300ms ease-in-out" }, children: /* @__PURE__ */ jsx( Chip, { icon: /* @__PURE__ */ jsx(LocalOffer, { sx: { fontSize: "0.8rem !important" } }), label: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [ /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "0.75rem", fontWeight: "medium" }, children: discountCodeDisplay || "DISCOUNT" }), /* @__PURE__ */ jsxs(Typography, { component: "span", sx: { fontSize: "0.75rem" }, children: [ "(-", formatAmount( displayItemDiscountAmount || item.discount_amounts?.[0]?.amount || "0", currency.decimal ), " ", currency.symbol, ")" ] }) ] }), size: "small", variant: "filled", sx: { height: 20, "& .MuiChip-icon": { color: "warning.main" }, "& .MuiChip-label": { px: 1 } } } ) } ), showFeatures && features.length > 0 && /* @__PURE__ */ jsx( Box, { sx: { display: "flex", alignItems: "center", justifyContent: "flex-start", flexWrap: "wrap", gap: 1.5, width: "100%", mt: 1.5 }, children: features.map((feature) => /* @__PURE__ */ jsxs( Box, { sx: { display: "flex", alignItems: "center", justifyContent: "center", gap: 0.5, px: 1, py: 0.5, bgcolor: "action.hover", borderRadius: 1, border: "1px solid", borderColor: "divider" }, children: [ /* @__PURE__ */ jsx( Box, { sx: { bgcolor: "success.main", width: 4, height: 4, borderRadius: "50%" } } ), /* @__PURE__ */ jsx( Typography, { component: "span", variant: "caption", sx: { color: "text.primary", fontSize: "0.75rem", fontWeight: 500, textTransform: "capitalize", whiteSpace: "nowrap" }, children: feature.name } ) ] }, feature.name )) } ), quantityInventoryError ? /* @__PURE__ */ jsx( Status, { label: quantityInventoryError, variant: "outlined", sx: { mt: 1, borderColor: "chip.error.border", backgroundColor: "chip.error.background", color: "chip.error.text" } } ) : null, canAdjustQuantity && !completed && /* @__PURE__ */ jsx(Box, { sx: { mt: 1, p: 1 }, children: /* @__PURE__ */ jsxs( Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, children: [ /* @__PURE__ */ jsxs( Typography, { variant: "body2", sx: { color: "text.secondary", minWidth: "fit-content" }, children: [ t("common.quantity"), ":" ] } ), /* @__PURE__ */ jsx( IconButton, { size: "small", onClick: handleQuantityDecrease, disabled: localQuantityNum <= minQuantity, sx: { minWidth: 32, width: 32, height: 32 }, children: /* @__PURE__ */ jsx(Remove, { fontSize: "small" }) } ), /* @__PURE__ */ jsx( TextField, { size: "small", value: localQuantity, onChange: handleQuantityInputChange, type: "number", slotProps: { htmlInput: { min: minQuantity, max: maxQuantity, style: { textAlign: "center", padding: "4px", minWidth: 80 } } } } ), /* @__PURE__ */ jsx( IconButton, { size: "small", onClick: handleQuantityIncrease, disabled: localQuantityNum >= maxQuantity, sx: { minWidth: 32, width: 32, height: 32 }, children: /* @__PURE__ */ jsx(Add, { fontSize: "small" }) } ) ] } ) }), isCreditProduct && /* @__PURE__ */ jsx(Alert, { severity: "info", sx: { mt: 1, fontSize: "0.875rem" }, icon: false, children: hasSchedule ? /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "inherit" }, children: formatScheduleInfo() }) : /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "inherit" }, children: formatCreditInfo() }) }), children ] } ), canUpsell && !item.upsell_price_id && item.price?.upsell?.upsells_to && /* @__PURE__ */ jsxs( Stack, { direction: "row", className: "product-item-upsell", sx: { alignItems: "center", justifyContent: "space-between", width: "100%" }, children: [ /* @__PURE__ */ jsxs( Typography, { component: "label", htmlFor: "upsell-switch", sx: { fontSize: 12, cursor: "pointer", color: "text.secondary" }, children: [ /* @__PURE__ */ jsx( Switch, { id: "upsell-switch", sx: { mr: 1 }, variant: "success", checked: false, onChange: () => onUpsell(item.price_id, item.price?.upsell?.upsells_to_id) } ), t("payment.checkout.upsell.save", { recurring: formatRecurring( item.price?.upsell?.upsells_to?.recurring, true, "per", locale ) }), /* @__PURE__ */ jsx( Status, { label: t("payment.checkout.upsell.off", { saving }), variant: "outlined", sx: { ml: 1, borderColor: "chip.warning.border", backgroundColor: "chip.warning.background", color: "chip.warning.text" } } ) ] } ), /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 12 }, children: upsellTokenDisplay || formatPrice(item.price?.upsell?.upsells_to, currency, item.price?.product?.unit_label, 1, true, locale) }) ] } ), canUpsell && item.upsell_price_id && /* @__PURE__ */ jsxs( Stack, { direction: "row", className: "product-item-upsell", sx: { alignItems: "center", justifyContent: "space-between", width: "100%" }, children: [ /* @__PURE__ */ jsxs( Typography, { component: "label", htmlFor: "upsell-switch", sx: { fontSize: 12, cursor: "pointer", color: "text.secondary" }, children: [ /* @__PURE__ */ jsx( Switch, { id: "upsell-switch", sx: { mr: 1 }, variant: "success", checked: true, onChange: () => onDownsell(item.upsell_price_id) } ), t("payment.checkout.upsell.revert", { recurring: t(`common.${formatRecurring(item.price?.recurring)}`) }) ] } ), /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 12 }, children: downsellTokenDisplay || formatPrice(item.price, currency, item.price?.product?.unit_label, 1, true, locale) }) ] } ) ] } ); }