@blocklet/payment-react 1.25.10 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/checkout-v2/checkout-v2.d.ts +2 -0
- package/es/checkout-v2/checkout-v2.js +121 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
- package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/es/checkout-v2/components/left/billing-toggle.js +118 -0
- package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
- package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/es/checkout-v2/components/left/product-item-card.js +571 -0
- package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/es/checkout-v2/components/left/promotion-input.js +178 -0
- package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
- package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/es/checkout-v2/components/left/trial-info.js +48 -0
- package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/es/checkout-v2/components/right/currency-grid.js +48 -0
- package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/es/checkout-v2/components/right/customer-info-card.js +156 -0
- package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/es/checkout-v2/components/right/status-feedback.js +17 -0
- package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/es/checkout-v2/components/right/submit-button.js +29 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
- package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
- package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/es/checkout-v2/components/shared/total-display.js +84 -0
- package/es/checkout-v2/index.d.ts +2 -0
- package/es/checkout-v2/index.js +1 -0
- package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/es/checkout-v2/layouts/checkout-layout.js +226 -0
- package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/composite-panel.js +423 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.js +611 -0
- package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/es/checkout-v2/panels/left/scenario-router.js +19 -0
- package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/es/checkout-v2/panels/right/payment-panel.js +644 -0
- package/es/checkout-v2/types.d.ts +15 -0
- package/es/checkout-v2/types.js +0 -0
- package/es/checkout-v2/utils/format.d.ts +59 -0
- package/es/checkout-v2/utils/format.js +125 -0
- package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/es/checkout-v2/utils/scenario-detector.js +17 -0
- package/es/checkout-v2/views/error-view.d.ts +7 -0
- package/es/checkout-v2/views/error-view.js +269 -0
- package/es/checkout-v2/views/loading-view.d.ts +5 -0
- package/es/checkout-v2/views/loading-view.js +158 -0
- package/es/checkout-v2/views/success-view.d.ts +29 -0
- package/es/checkout-v2/views/success-view.js +614 -0
- package/es/components/phone-field.d.ts +14 -0
- package/es/components/phone-field.js +96 -0
- package/es/index.d.ts +3 -1
- package/es/index.js +3 -1
- package/es/locales/en.js +45 -6
- package/es/locales/zh.js +45 -6
- package/es/payment/form/index.js +10 -1
- package/lib/checkout-v2/checkout-v2.d.ts +2 -0
- package/lib/checkout-v2/checkout-v2.js +151 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
- package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
- package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
- package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/lib/checkout-v2/components/left/product-item-card.js +738 -0
- package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/lib/checkout-v2/components/left/promotion-input.js +220 -0
- package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
- package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/lib/checkout-v2/components/left/trial-info.js +82 -0
- package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/lib/checkout-v2/components/right/currency-grid.js +96 -0
- package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
- package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/lib/checkout-v2/components/right/status-feedback.js +30 -0
- package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/lib/checkout-v2/components/right/submit-button.js +35 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
- package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
- package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/lib/checkout-v2/components/shared/total-display.js +154 -0
- package/lib/checkout-v2/index.d.ts +2 -0
- package/lib/checkout-v2/index.js +13 -0
- package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
- package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.js +795 -0
- package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
- package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
- package/lib/checkout-v2/types.d.ts +15 -0
- package/lib/checkout-v2/types.js +1 -0
- package/lib/checkout-v2/utils/format.d.ts +59 -0
- package/lib/checkout-v2/utils/format.js +158 -0
- package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/lib/checkout-v2/utils/scenario-detector.js +23 -0
- package/lib/checkout-v2/views/error-view.d.ts +7 -0
- package/lib/checkout-v2/views/error-view.js +321 -0
- package/lib/checkout-v2/views/loading-view.d.ts +5 -0
- package/lib/checkout-v2/views/loading-view.js +168 -0
- package/lib/checkout-v2/views/success-view.d.ts +29 -0
- package/lib/checkout-v2/views/success-view.js +735 -0
- package/lib/components/phone-field.d.ts +14 -0
- package/lib/components/phone-field.js +130 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.js +8 -0
- package/lib/locales/en.js +45 -6
- package/lib/locales/zh.js +45 -6
- package/lib/payment/form/index.js +10 -1
- package/package.json +4 -3
- package/src/checkout-v2/checkout-v2.tsx +155 -0
- package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
- package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
- package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
- package/src/checkout-v2/components/left/product-item-card.tsx +642 -0
- package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
- package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
- package/src/checkout-v2/components/left/trial-info.tsx +63 -0
- package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
- package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
- package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
- package/src/checkout-v2/components/right/submit-button.tsx +37 -0
- package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
- package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
- package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
- package/src/checkout-v2/components/shared/total-display.tsx +112 -0
- package/src/checkout-v2/index.ts +2 -0
- package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
- package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
- package/src/checkout-v2/panels/left/credit-topup-panel.tsx +677 -0
- package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
- package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
- package/src/checkout-v2/types.ts +18 -0
- package/src/checkout-v2/utils/format.ts +205 -0
- package/src/checkout-v2/utils/scenario-detector.ts +30 -0
- package/src/checkout-v2/views/error-view.tsx +293 -0
- package/src/checkout-v2/views/loading-view.tsx +162 -0
- package/src/checkout-v2/views/success-view.tsx +770 -0
- package/src/components/phone-field.tsx +119 -0
- package/src/index.ts +3 -0
- package/src/locales/en.tsx +45 -4
- package/src/locales/zh.tsx +43 -4
- package/src/payment/form/index.tsx +16 -1
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
3
|
+
import CheckIcon from '@mui/icons-material/Check';
|
|
4
|
+
import LocalOfferIcon from '@mui/icons-material/LocalOffer';
|
|
5
|
+
import RemoveIcon from '@mui/icons-material/Remove';
|
|
6
|
+
import ShoppingCartCheckoutIcon from '@mui/icons-material/ShoppingCartCheckout';
|
|
7
|
+
import {
|
|
8
|
+
Avatar,
|
|
9
|
+
Box,
|
|
10
|
+
Chip,
|
|
11
|
+
Collapse,
|
|
12
|
+
IconButton,
|
|
13
|
+
Skeleton,
|
|
14
|
+
Stack,
|
|
15
|
+
Switch,
|
|
16
|
+
TextField,
|
|
17
|
+
Typography,
|
|
18
|
+
useMediaQuery,
|
|
19
|
+
useTheme,
|
|
20
|
+
} from '@mui/material';
|
|
21
|
+
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
22
|
+
import type { TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
|
|
23
|
+
import { getPriceUnitAmountByCurrency } from '@blocklet/payment-react-headless';
|
|
24
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
25
|
+
import { INTERVAL_LOCALE_KEY, formatDynamicUnitPrice, formatTokenAmount, formatTrialText } from '../../utils/format';
|
|
26
|
+
|
|
27
|
+
interface ProductItemCardProps {
|
|
28
|
+
item: TLineItemExpanded & { adjustable_quantity?: { enabled: boolean; minimum?: number; maximum?: number } };
|
|
29
|
+
currency: TPaymentCurrency | null;
|
|
30
|
+
discounts: any[];
|
|
31
|
+
exchangeRate: string | null;
|
|
32
|
+
onQuantityChange: (itemId: string, qty: number) => Promise<void>;
|
|
33
|
+
onUpsell: (fromId: string, toId: string) => Promise<void>;
|
|
34
|
+
onDownsell: (priceId: string) => Promise<void>;
|
|
35
|
+
trialActive: boolean;
|
|
36
|
+
trialDays: number;
|
|
37
|
+
t: (key: string, params?: any) => string;
|
|
38
|
+
recommended?: boolean;
|
|
39
|
+
hideUpsell?: boolean;
|
|
40
|
+
isRateLoading?: boolean;
|
|
41
|
+
showFeatures?: boolean;
|
|
42
|
+
children?: React.ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function ProductItemCard({
|
|
46
|
+
item,
|
|
47
|
+
currency,
|
|
48
|
+
discounts,
|
|
49
|
+
exchangeRate,
|
|
50
|
+
onQuantityChange,
|
|
51
|
+
onUpsell,
|
|
52
|
+
onDownsell,
|
|
53
|
+
trialActive,
|
|
54
|
+
trialDays,
|
|
55
|
+
t,
|
|
56
|
+
recommended = false,
|
|
57
|
+
hideUpsell = false,
|
|
58
|
+
isRateLoading = false,
|
|
59
|
+
showFeatures = true,
|
|
60
|
+
children = undefined,
|
|
61
|
+
}: ProductItemCardProps) {
|
|
62
|
+
const activePrice: any = (item as any).upsell_price || item.price;
|
|
63
|
+
const product = activePrice?.product;
|
|
64
|
+
const name = product?.name || 'Item';
|
|
65
|
+
const logo = product?.images?.[0] || '';
|
|
66
|
+
const features: Array<{ name: string; icon?: string }> = product?.features || [];
|
|
67
|
+
const recurring = activePrice?.recurring;
|
|
68
|
+
|
|
69
|
+
const quantity = item.quantity || 1;
|
|
70
|
+
const isMetered = recurring?.usage_type === 'metered';
|
|
71
|
+
const metered = isMetered ? ` ${t('common.metered')}` : '';
|
|
72
|
+
|
|
73
|
+
const perUnitFormatted = formatDynamicUnitPrice(activePrice, currency, exchangeRate);
|
|
74
|
+
|
|
75
|
+
// Item type badge: just "SUBSCRIPTION" or "ONE-TIME" (interval is shown with price)
|
|
76
|
+
const isSubscription = !!recurring;
|
|
77
|
+
const typeBadgeText = isSubscription
|
|
78
|
+
? t('payment.checkout.typeBadge.subscription')
|
|
79
|
+
: t('payment.checkout.typeBadge.oneTime');
|
|
80
|
+
|
|
81
|
+
// Billing interval suffix for price display: "/ month"
|
|
82
|
+
const priceIntervalSuffix = recurring?.interval ? ` / ${t(`common.${recurring.interval}`)}` : '';
|
|
83
|
+
|
|
84
|
+
// Subtitle: only quantity breakdown (interval is now in the type badge)
|
|
85
|
+
const subtitleText = (() => {
|
|
86
|
+
if (quantity > 1 && perUnitFormatted && currency) {
|
|
87
|
+
return `${quantity} × ${perUnitFormatted} ${currency.symbol}`;
|
|
88
|
+
}
|
|
89
|
+
if (isMetered) return metered.trim();
|
|
90
|
+
return '';
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
// Item total
|
|
94
|
+
const itemTotal = (() => {
|
|
95
|
+
// custom_amount is backend-quoted in a specific currency — only use when quote_currency_id is present and matches
|
|
96
|
+
// (when quote_currency_id is absent, custom_amount denomination is unknown — skip to base_amount path)
|
|
97
|
+
const quoteCurrencyId = (item as any).quote_currency_id as string | undefined;
|
|
98
|
+
if ((item as any).custom_amount && quoteCurrencyId && quoteCurrencyId === currency?.id) {
|
|
99
|
+
return formatTokenAmount((item as any).custom_amount, currency);
|
|
100
|
+
}
|
|
101
|
+
if (activePrice?.pricing_type === 'dynamic' && exchangeRate && activePrice.base_amount) {
|
|
102
|
+
const rate = Number(exchangeRate);
|
|
103
|
+
if (rate > 0 && Number.isFinite(rate)) {
|
|
104
|
+
const baseUsd = Number(activePrice.base_amount);
|
|
105
|
+
if (baseUsd > 0 && Number.isFinite(baseUsd)) {
|
|
106
|
+
const tokenAmount = (baseUsd * quantity) / rate;
|
|
107
|
+
const abs = Math.abs(tokenAmount);
|
|
108
|
+
const precision = abs > 0 && abs < 0.01 ? 6 : 2;
|
|
109
|
+
return (
|
|
110
|
+
tokenAmount
|
|
111
|
+
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
112
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
113
|
+
.replace(/\.$/, '') || '0'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Fiat/Stripe fallback: use base_amount when no exchange rate
|
|
119
|
+
// (unit_amount may be in crypto denomination, not fiat cents)
|
|
120
|
+
if (!exchangeRate && activePrice?.base_amount != null) {
|
|
121
|
+
const baseUsd = Number(activePrice.base_amount);
|
|
122
|
+
if (baseUsd >= 0 && Number.isFinite(baseUsd)) {
|
|
123
|
+
const total = baseUsd * quantity;
|
|
124
|
+
return total.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Fallback: look up unit_amount from currency_options for the selected currency
|
|
128
|
+
if (activePrice && currency) {
|
|
129
|
+
const unitAmount = getPriceUnitAmountByCurrency(activePrice, currency);
|
|
130
|
+
if (unitAmount && unitAmount !== '0') {
|
|
131
|
+
const totalUnits = BigInt(unitAmount) * BigInt(quantity);
|
|
132
|
+
return formatTokenAmount(totalUnits.toString(), currency);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return '0';
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
// Per-item discount
|
|
139
|
+
const discount = discounts?.[0];
|
|
140
|
+
const discountCode =
|
|
141
|
+
discount?.promotion_code_details?.code || discount?.verification_data?.code || discount?.promotion_code || '';
|
|
142
|
+
const perItemDiscount = (() => {
|
|
143
|
+
if (!discountCode || !discount) return null;
|
|
144
|
+
const couponDetails = discount?.coupon_details;
|
|
145
|
+
if (couponDetails?.percent_off > 0) {
|
|
146
|
+
const numericTotal = parseFloat(String(itemTotal).replace(/,/g, ''));
|
|
147
|
+
if (!Number.isNaN(numericTotal) && numericTotal > 0) {
|
|
148
|
+
const discAmount = (numericTotal * couponDetails.percent_off) / 100;
|
|
149
|
+
const abs = Math.abs(discAmount);
|
|
150
|
+
const precision = abs > 0 && abs < 0.01 ? 6 : 2;
|
|
151
|
+
return `${
|
|
152
|
+
discAmount
|
|
153
|
+
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
154
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
155
|
+
.replace(/\.$/, '') || '0'
|
|
156
|
+
} ${currency?.symbol || ''}`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if ((item as any).discount_amounts?.length > 0 && currency) {
|
|
160
|
+
return `${formatTokenAmount((item as any).discount_amounts[0].amount, currency)} ${currency.symbol}`;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
})();
|
|
164
|
+
|
|
165
|
+
// Quantity controls
|
|
166
|
+
const adjustable = item.adjustable_quantity?.enabled;
|
|
167
|
+
const min = item.adjustable_quantity?.minimum || 1;
|
|
168
|
+
const max = item.adjustable_quantity?.maximum;
|
|
169
|
+
|
|
170
|
+
const [qtyInput, setQtyInput] = useState(String(quantity));
|
|
171
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
172
|
+
const theme = useTheme();
|
|
173
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
174
|
+
const [featuresOpen, setFeaturesOpen] = useState(!isMobile);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!isEditing) setQtyInput(String(quantity));
|
|
177
|
+
}, [quantity, isEditing]);
|
|
178
|
+
|
|
179
|
+
// Upsell info
|
|
180
|
+
const canUpsell = !!(item.price as any)?.upsell?.upsells_to;
|
|
181
|
+
const isUpselled = !!(item as any).upsell_price;
|
|
182
|
+
const upsellTo = (item.price as any)?.upsell?.upsells_to;
|
|
183
|
+
|
|
184
|
+
let savingsPercent = 0;
|
|
185
|
+
if (canUpsell && upsellTo) {
|
|
186
|
+
const fromAmount = parseFloat((item.price as any)?.base_amount || (item.price as any)?.unit_amount || '0');
|
|
187
|
+
const toAmount = parseFloat(upsellTo?.base_amount || upsellTo?.unit_amount || '0');
|
|
188
|
+
const fromInterval = (item.price as any)?.recurring?.interval;
|
|
189
|
+
const toInterval = upsellTo?.recurring?.interval;
|
|
190
|
+
if (fromAmount > 0 && toAmount > 0 && fromInterval && toInterval) {
|
|
191
|
+
const monthsMap: Record<string, number> = { day: 365, week: 52, month: 12, year: 1 };
|
|
192
|
+
const fromYearly = fromAmount * (monthsMap[fromInterval] || 1);
|
|
193
|
+
const toYearly = toAmount * (monthsMap[toInterval] || 1);
|
|
194
|
+
if (fromYearly > toYearly) {
|
|
195
|
+
savingsPercent = Math.round(((fromYearly - toYearly) / fromYearly) * 100);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Upsell price display
|
|
201
|
+
const upsellInterval = upsellTo?.recurring?.interval;
|
|
202
|
+
const upsellPrice = (() => {
|
|
203
|
+
if (!upsellTo) return '';
|
|
204
|
+
const slashText = upsellInterval ? t('common.slash', { interval: t(`common.${upsellInterval}`) }) : '';
|
|
205
|
+
if (upsellTo.pricing_type === 'dynamic' && upsellTo.base_amount && exchangeRate) {
|
|
206
|
+
const rate = Number(exchangeRate);
|
|
207
|
+
if (rate > 0 && Number.isFinite(rate)) {
|
|
208
|
+
const baseUsd = parseFloat(upsellTo.base_amount);
|
|
209
|
+
if (baseUsd > 0 && Number.isFinite(baseUsd)) {
|
|
210
|
+
const tokenAmount = baseUsd / rate;
|
|
211
|
+
const abs = Math.abs(tokenAmount);
|
|
212
|
+
const precision = abs > 0 && abs < 0.01 ? 6 : 2;
|
|
213
|
+
const formatted =
|
|
214
|
+
tokenAmount
|
|
215
|
+
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
216
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
217
|
+
.replace(/\.$/, '') || '0';
|
|
218
|
+
return `${formatted} ${currency?.symbol || ''} ${slashText}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (upsellTo.pricing_type === 'dynamic' && upsellTo.base_amount && upsellTo.base_currency === 'USD') {
|
|
223
|
+
const baseUsd = parseFloat(upsellTo.base_amount);
|
|
224
|
+
const formattedUsd = baseUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
225
|
+
return `$${formattedUsd} ${slashText}`;
|
|
226
|
+
}
|
|
227
|
+
const upsellUnitFormatted = formatDynamicUnitPrice(upsellTo, currency, exchangeRate);
|
|
228
|
+
return upsellUnitFormatted ? `${upsellUnitFormatted} ${currency?.symbol || ''} ${slashText}` : '';
|
|
229
|
+
})();
|
|
230
|
+
|
|
231
|
+
// Downsell price display
|
|
232
|
+
const originalInterval = (item.price as any)?.recurring?.interval;
|
|
233
|
+
const downsellPrice = (() => {
|
|
234
|
+
if (!item.price || !isUpselled) return '';
|
|
235
|
+
const originalPrice: any = item.price;
|
|
236
|
+
const originalSlash = originalInterval ? t('common.slash', { interval: t(`common.${originalInterval}`) }) : '';
|
|
237
|
+
if (originalPrice.pricing_type === 'dynamic' && originalPrice.base_amount && exchangeRate) {
|
|
238
|
+
const rate = Number(exchangeRate);
|
|
239
|
+
if (rate > 0 && Number.isFinite(rate)) {
|
|
240
|
+
const baseUsd = parseFloat(originalPrice.base_amount);
|
|
241
|
+
if (baseUsd > 0 && Number.isFinite(baseUsd)) {
|
|
242
|
+
const tokenAmount = baseUsd / rate;
|
|
243
|
+
const abs = Math.abs(tokenAmount);
|
|
244
|
+
const precision = abs > 0 && abs < 0.01 ? 6 : 2;
|
|
245
|
+
const formatted =
|
|
246
|
+
tokenAmount
|
|
247
|
+
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
248
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
249
|
+
.replace(/\.$/, '') || '0';
|
|
250
|
+
return `${formatted} ${currency?.symbol || ''} ${originalSlash}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
originalPrice.pricing_type === 'dynamic' &&
|
|
256
|
+
originalPrice.base_amount &&
|
|
257
|
+
originalPrice.base_currency === 'USD'
|
|
258
|
+
) {
|
|
259
|
+
const baseUsd = parseFloat(originalPrice.base_amount);
|
|
260
|
+
const formattedUsd = baseUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
261
|
+
return `$${formattedUsd} ${originalSlash}`;
|
|
262
|
+
}
|
|
263
|
+
const unitFormatted = formatDynamicUnitPrice(originalPrice, currency, exchangeRate);
|
|
264
|
+
return unitFormatted ? `${unitFormatted} ${currency?.symbol || ''} ${originalSlash}` : '';
|
|
265
|
+
})();
|
|
266
|
+
|
|
267
|
+
// Upsell toggle label
|
|
268
|
+
const upsellToggleLabel = (() => {
|
|
269
|
+
if (isUpselled) {
|
|
270
|
+
const recurringLabel = originalInterval ? t(INTERVAL_LOCALE_KEY[originalInterval] || '') : '';
|
|
271
|
+
return t('payment.checkout.upsell.revert', { recurring: recurringLabel });
|
|
272
|
+
}
|
|
273
|
+
const recurringLabel = upsellInterval ? t(INTERVAL_LOCALE_KEY[upsellInterval] || '') : '';
|
|
274
|
+
return t('payment.checkout.upsell.save', { recurring: recurringLabel });
|
|
275
|
+
})();
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<Box sx={{ position: 'relative' }}>
|
|
279
|
+
{/* Recommended badge — top-right */}
|
|
280
|
+
{recommended && (
|
|
281
|
+
<Chip
|
|
282
|
+
label="RECOMMENDED"
|
|
283
|
+
size="small"
|
|
284
|
+
sx={{
|
|
285
|
+
position: 'absolute',
|
|
286
|
+
top: 0,
|
|
287
|
+
right: 40,
|
|
288
|
+
transform: 'translateY(-50%)',
|
|
289
|
+
zIndex: 1,
|
|
290
|
+
height: 22,
|
|
291
|
+
fontSize: 9,
|
|
292
|
+
fontWeight: 900,
|
|
293
|
+
letterSpacing: '0.12em',
|
|
294
|
+
bgcolor: 'primary.main',
|
|
295
|
+
color: '#fff',
|
|
296
|
+
boxShadow: '0 4px 12px rgba(45,124,243,0.2)',
|
|
297
|
+
'& .MuiChip-label': { px: 1.5 },
|
|
298
|
+
}}
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
<Box
|
|
303
|
+
sx={{
|
|
304
|
+
p: { xs: 2, md: 3 },
|
|
305
|
+
bgcolor: 'background.paper',
|
|
306
|
+
borderRadius: { xs: '16px', md: '24px' },
|
|
307
|
+
border: '1px solid',
|
|
308
|
+
borderColor: 'divider',
|
|
309
|
+
boxShadow: (th) =>
|
|
310
|
+
th.palette.mode === 'dark' ? '0 12px 40px -8px rgba(0,0,0,0.3)' : '0 12px 40px -8px rgba(0,0,0,0.06)',
|
|
311
|
+
transition: 'all 0.3s ease',
|
|
312
|
+
...(canUpsell && !hideUpsell ? { borderRadius: { xs: '16px 16px 0 0', md: '24px 24px 0 0' } } : {}),
|
|
313
|
+
'&:hover': {
|
|
314
|
+
borderColor: (th) => (th.palette.mode === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(45,124,243,0.15)'),
|
|
315
|
+
},
|
|
316
|
+
}}>
|
|
317
|
+
<Stack direction="row" spacing={{ xs: 1.5, md: 2.5 }} sx={{ alignItems: 'center', width: '100%' }}>
|
|
318
|
+
{/* Product avatar */}
|
|
319
|
+
{logo ? (
|
|
320
|
+
<Avatar
|
|
321
|
+
src={logo}
|
|
322
|
+
alt={name}
|
|
323
|
+
variant="rounded"
|
|
324
|
+
sx={{
|
|
325
|
+
width: { xs: 44, md: 64 },
|
|
326
|
+
height: { xs: 44, md: 64 },
|
|
327
|
+
borderRadius: { xs: '12px', md: '16px' },
|
|
328
|
+
flexShrink: 0,
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
) : (
|
|
332
|
+
<Avatar
|
|
333
|
+
variant="rounded"
|
|
334
|
+
sx={{
|
|
335
|
+
width: { xs: 44, md: 64 },
|
|
336
|
+
height: { xs: 44, md: 64 },
|
|
337
|
+
borderRadius: { xs: '12px', md: '16px' },
|
|
338
|
+
bgcolor: (th) => (th.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : '#eff6ff'),
|
|
339
|
+
flexShrink: 0,
|
|
340
|
+
}}>
|
|
341
|
+
<ShoppingCartCheckoutIcon sx={{ fontSize: { xs: 22, md: 28 }, color: 'primary.main', opacity: 0.45 }} />
|
|
342
|
+
</Avatar>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
|
346
|
+
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 0.25 }}>
|
|
347
|
+
<Box sx={{ minWidth: 0 }}>
|
|
348
|
+
<Typography
|
|
349
|
+
sx={{ color: 'text.primary', fontWeight: 800, fontSize: { xs: 15, md: 18 }, lineHeight: 1.3 }}>
|
|
350
|
+
{name}
|
|
351
|
+
</Typography>
|
|
352
|
+
{/* Item type badge: subscription/one-time + interval */}
|
|
353
|
+
<Typography
|
|
354
|
+
component="span"
|
|
355
|
+
sx={{
|
|
356
|
+
display: 'inline-block',
|
|
357
|
+
mt: 0.5,
|
|
358
|
+
fontSize: 10,
|
|
359
|
+
fontWeight: 700,
|
|
360
|
+
letterSpacing: '0.08em',
|
|
361
|
+
lineHeight: 1,
|
|
362
|
+
textTransform: 'uppercase',
|
|
363
|
+
color: 'primary.main',
|
|
364
|
+
bgcolor: (th) =>
|
|
365
|
+
th.palette.mode === 'dark' ? `${th.palette.primary.main}1A` : `${th.palette.primary.main}0D`,
|
|
366
|
+
px: 0.75,
|
|
367
|
+
py: 0.4,
|
|
368
|
+
borderRadius: '3px',
|
|
369
|
+
}}>
|
|
370
|
+
{typeBadgeText}
|
|
371
|
+
</Typography>
|
|
372
|
+
{subtitleText && (
|
|
373
|
+
<Typography sx={{ color: 'text.disabled', fontSize: 13, fontWeight: 500, mt: 0.25 }}>
|
|
374
|
+
{subtitleText}
|
|
375
|
+
</Typography>
|
|
376
|
+
)}
|
|
377
|
+
</Box>
|
|
378
|
+
<Stack alignItems="flex-end" sx={{ flexShrink: 0, ml: 1.5 }}>
|
|
379
|
+
{/* eslint-disable-next-line no-nested-ternary */}
|
|
380
|
+
{trialActive && isSubscription ? (
|
|
381
|
+
<Typography
|
|
382
|
+
sx={{ fontWeight: 800, color: 'text.primary', whiteSpace: 'nowrap', fontSize: { xs: 15, md: 18 } }}>
|
|
383
|
+
{formatTrialText(t, trialDays, recurring?.interval || 'day')}
|
|
384
|
+
</Typography>
|
|
385
|
+
) : isRateLoading ? (
|
|
386
|
+
<Skeleton variant="text" width={100} height={28} />
|
|
387
|
+
) : (
|
|
388
|
+
<>
|
|
389
|
+
<Typography
|
|
390
|
+
sx={{
|
|
391
|
+
fontWeight: 800,
|
|
392
|
+
color: 'text.primary',
|
|
393
|
+
whiteSpace: 'nowrap',
|
|
394
|
+
fontSize: { xs: 15, md: 18 },
|
|
395
|
+
transition: 'opacity 0.3s ease',
|
|
396
|
+
}}>
|
|
397
|
+
{itemTotal} {currency?.symbol}
|
|
398
|
+
{priceIntervalSuffix}
|
|
399
|
+
</Typography>
|
|
400
|
+
{exchangeRate && activePrice?.base_amount && (
|
|
401
|
+
<Typography sx={{ fontSize: 12, color: 'text.disabled', fontWeight: 600, lineHeight: 1 }}>
|
|
402
|
+
≈ ${(Number(activePrice.base_amount) * quantity).toFixed(2)}
|
|
403
|
+
</Typography>
|
|
404
|
+
)}
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
</Stack>
|
|
408
|
+
</Stack>
|
|
409
|
+
</Box>
|
|
410
|
+
</Stack>
|
|
411
|
+
|
|
412
|
+
{/* Product features — collapsible, default expanded */}
|
|
413
|
+
{showFeatures && features.length > 0 && (
|
|
414
|
+
<Box
|
|
415
|
+
sx={{
|
|
416
|
+
mt: { xs: 2, md: 2.5 },
|
|
417
|
+
pt: { xs: 2, md: 2.5 },
|
|
418
|
+
borderTop: '1px solid',
|
|
419
|
+
borderColor: (th) => (th.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.100'),
|
|
420
|
+
}}>
|
|
421
|
+
<Stack
|
|
422
|
+
direction="row"
|
|
423
|
+
alignItems="center"
|
|
424
|
+
justifyContent="space-between"
|
|
425
|
+
onClick={() => setFeaturesOpen((v) => !v)}
|
|
426
|
+
sx={{ cursor: 'pointer', userSelect: 'none', mb: featuresOpen ? 1.25 : 0 }}>
|
|
427
|
+
<Typography sx={{ fontSize: 14, fontWeight: 700, color: 'text.primary' }}>
|
|
428
|
+
{t('payment.checkout.planFeatures')}
|
|
429
|
+
</Typography>
|
|
430
|
+
<KeyboardArrowUpIcon
|
|
431
|
+
sx={{
|
|
432
|
+
fontSize: 20,
|
|
433
|
+
color: 'text.secondary',
|
|
434
|
+
transition: 'transform 0.2s ease',
|
|
435
|
+
transform: featuresOpen ? 'rotate(0deg)' : 'rotate(180deg)',
|
|
436
|
+
}}
|
|
437
|
+
/>
|
|
438
|
+
</Stack>
|
|
439
|
+
<Collapse in={featuresOpen}>
|
|
440
|
+
<Stack spacing={1.25}>
|
|
441
|
+
{features.map((feature) => (
|
|
442
|
+
<Stack key={feature.name} direction="row" spacing={1.5} alignItems="center">
|
|
443
|
+
<Box
|
|
444
|
+
sx={{
|
|
445
|
+
width: 20,
|
|
446
|
+
height: 20,
|
|
447
|
+
borderRadius: '50%',
|
|
448
|
+
bgcolor: (th) =>
|
|
449
|
+
th.palette.mode === 'dark' ? 'rgba(16,185,129,0.15)' : 'rgba(16,185,129,0.1)',
|
|
450
|
+
display: 'flex',
|
|
451
|
+
alignItems: 'center',
|
|
452
|
+
justifyContent: 'center',
|
|
453
|
+
flexShrink: 0,
|
|
454
|
+
}}>
|
|
455
|
+
<CheckIcon sx={{ fontSize: 14, color: 'success.main' }} />
|
|
456
|
+
</Box>
|
|
457
|
+
<Typography sx={{ fontSize: 14, fontWeight: 500, color: 'text.secondary' }}>
|
|
458
|
+
{feature.name}
|
|
459
|
+
</Typography>
|
|
460
|
+
</Stack>
|
|
461
|
+
))}
|
|
462
|
+
</Stack>
|
|
463
|
+
</Collapse>
|
|
464
|
+
</Box>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{/* Discount chip */}
|
|
468
|
+
{discountCode && perItemDiscount && (
|
|
469
|
+
<Box sx={{ mt: 1.5 }}>
|
|
470
|
+
<Chip
|
|
471
|
+
icon={<LocalOfferIcon sx={{ color: 'warning.main', fontSize: 'small' }} />}
|
|
472
|
+
label={`${discountCode} (-${perItemDiscount})`}
|
|
473
|
+
size="small"
|
|
474
|
+
sx={{ height: 22, borderRadius: '6px', '& .MuiChip-label': { fontSize: 12 } }}
|
|
475
|
+
/>
|
|
476
|
+
</Box>
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{/* Quantity controls */}
|
|
480
|
+
{adjustable && (
|
|
481
|
+
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 2 }}>
|
|
482
|
+
<Typography sx={{ color: 'text.secondary', minWidth: 'fit-content', fontSize: 13, fontWeight: 500 }}>
|
|
483
|
+
{t('common.quantity')}
|
|
484
|
+
</Typography>
|
|
485
|
+
<IconButton
|
|
486
|
+
size="small"
|
|
487
|
+
onClick={() => onQuantityChange(item.price_id, quantity - 1)}
|
|
488
|
+
disabled={quantity <= min}
|
|
489
|
+
sx={{
|
|
490
|
+
width: 36,
|
|
491
|
+
height: 36,
|
|
492
|
+
border: '1px solid',
|
|
493
|
+
borderColor: 'divider',
|
|
494
|
+
borderRadius: '50%',
|
|
495
|
+
'&:hover': { bgcolor: 'action.hover' },
|
|
496
|
+
}}>
|
|
497
|
+
<RemoveIcon sx={{ fontSize: 20 }} />
|
|
498
|
+
</IconButton>
|
|
499
|
+
<TextField
|
|
500
|
+
value={qtyInput}
|
|
501
|
+
size="small"
|
|
502
|
+
type="number"
|
|
503
|
+
slotProps={{
|
|
504
|
+
htmlInput: {
|
|
505
|
+
style: {
|
|
506
|
+
textAlign: 'center',
|
|
507
|
+
padding: '6px 4px',
|
|
508
|
+
fontWeight: 700,
|
|
509
|
+
fontSize: 16,
|
|
510
|
+
MozAppearance: 'textfield',
|
|
511
|
+
} as React.CSSProperties,
|
|
512
|
+
min,
|
|
513
|
+
max: max || undefined,
|
|
514
|
+
},
|
|
515
|
+
}}
|
|
516
|
+
sx={{
|
|
517
|
+
minWidth: 64,
|
|
518
|
+
'& input': { textAlign: 'center' },
|
|
519
|
+
'& .MuiOutlinedInput-root': { borderRadius: '12px' },
|
|
520
|
+
'& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
|
|
521
|
+
WebkitAppearance: 'none',
|
|
522
|
+
margin: 0,
|
|
523
|
+
},
|
|
524
|
+
'& input[type=number]': {
|
|
525
|
+
MozAppearance: 'textfield',
|
|
526
|
+
},
|
|
527
|
+
}}
|
|
528
|
+
onFocus={() => setIsEditing(true)}
|
|
529
|
+
onChange={(e) => setQtyInput(e.target.value)}
|
|
530
|
+
onKeyDown={(e) => {
|
|
531
|
+
if (e.key === 'Enter') {
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
e.stopPropagation();
|
|
534
|
+
(e.target as HTMLInputElement).blur();
|
|
535
|
+
}
|
|
536
|
+
}}
|
|
537
|
+
onBlur={() => {
|
|
538
|
+
setIsEditing(false);
|
|
539
|
+
const v = parseInt(qtyInput, 10);
|
|
540
|
+
if (!Number.isNaN(v) && v >= min && (!max || v <= max) && v !== quantity) {
|
|
541
|
+
onQuantityChange(item.price_id, v);
|
|
542
|
+
} else {
|
|
543
|
+
setQtyInput(String(quantity));
|
|
544
|
+
}
|
|
545
|
+
}}
|
|
546
|
+
/>
|
|
547
|
+
<IconButton
|
|
548
|
+
size="small"
|
|
549
|
+
onClick={() => onQuantityChange(item.price_id, quantity + 1)}
|
|
550
|
+
disabled={max ? quantity >= max : false}
|
|
551
|
+
sx={{
|
|
552
|
+
width: 36,
|
|
553
|
+
height: 36,
|
|
554
|
+
border: '1px solid',
|
|
555
|
+
borderColor: 'divider',
|
|
556
|
+
borderRadius: '50%',
|
|
557
|
+
'&:hover': { bgcolor: 'action.hover' },
|
|
558
|
+
}}>
|
|
559
|
+
<AddIcon sx={{ fontSize: 20 }} />
|
|
560
|
+
</IconButton>
|
|
561
|
+
</Stack>
|
|
562
|
+
)}
|
|
563
|
+
|
|
564
|
+
{children}
|
|
565
|
+
</Box>
|
|
566
|
+
|
|
567
|
+
{/* Upsell toggle strip — seamless with card (hidden when promoted to top-level) */}
|
|
568
|
+
{canUpsell && !hideUpsell && (
|
|
569
|
+
<Box
|
|
570
|
+
sx={{
|
|
571
|
+
px: { xs: 2, md: 3 },
|
|
572
|
+
py: 1.5,
|
|
573
|
+
bgcolor: 'background.paper',
|
|
574
|
+
border: '1px solid',
|
|
575
|
+
borderTop: 0,
|
|
576
|
+
borderColor: 'divider',
|
|
577
|
+
borderRadius: { xs: '0 0 16px 16px', md: '0 0 24px 24px' },
|
|
578
|
+
boxShadow: (th) =>
|
|
579
|
+
th.palette.mode === 'dark' ? '0 12px 40px -8px rgba(0,0,0,0.3)' : '0 12px 40px -8px rgba(0,0,0,0.06)',
|
|
580
|
+
}}>
|
|
581
|
+
{/* Subtle divider */}
|
|
582
|
+
<Box sx={{ borderTop: '1px solid', borderColor: 'divider', mb: 1.5 }} />
|
|
583
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
|
584
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
585
|
+
<Switch
|
|
586
|
+
checked={isUpselled}
|
|
587
|
+
onChange={async () => {
|
|
588
|
+
try {
|
|
589
|
+
if (isUpselled) {
|
|
590
|
+
await onDownsell((item as any).upsell_price?.id || (item.price as any).id);
|
|
591
|
+
} else {
|
|
592
|
+
await onUpsell(item.price_id, upsellTo.id);
|
|
593
|
+
}
|
|
594
|
+
} catch (err: any) {
|
|
595
|
+
Toast.error(err?.response?.data?.error || err?.message || 'Failed');
|
|
596
|
+
}
|
|
597
|
+
}}
|
|
598
|
+
size="small"
|
|
599
|
+
sx={{
|
|
600
|
+
'& .MuiSwitch-switchBase.Mui-checked': { color: '#12b886' },
|
|
601
|
+
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#12b886' },
|
|
602
|
+
}}
|
|
603
|
+
/>
|
|
604
|
+
<Typography
|
|
605
|
+
sx={{ fontSize: 13, color: 'text.secondary', cursor: 'pointer', fontWeight: 600 }}
|
|
606
|
+
onClick={async () => {
|
|
607
|
+
try {
|
|
608
|
+
if (isUpselled) await onDownsell((item as any).upsell_price?.id || (item.price as any).id);
|
|
609
|
+
else await onUpsell(item.price_id, upsellTo.id);
|
|
610
|
+
} catch (err: any) {
|
|
611
|
+
Toast.error(err?.response?.data?.error || err?.message || 'Failed');
|
|
612
|
+
}
|
|
613
|
+
}}>
|
|
614
|
+
{upsellToggleLabel}
|
|
615
|
+
</Typography>
|
|
616
|
+
{!isUpselled && savingsPercent > 0 && (
|
|
617
|
+
<Chip
|
|
618
|
+
label={t('payment.checkout.upsell.off', { saving: savingsPercent })}
|
|
619
|
+
size="small"
|
|
620
|
+
sx={{
|
|
621
|
+
height: 22,
|
|
622
|
+
fontSize: 11,
|
|
623
|
+
fontWeight: 700,
|
|
624
|
+
bgcolor: (th) => (th.palette.mode === 'dark' ? 'rgba(18,184,134,0.1)' : '#ebfef5'),
|
|
625
|
+
color: '#12b886',
|
|
626
|
+
border: '1px solid',
|
|
627
|
+
borderColor: (th) => (th.palette.mode === 'dark' ? 'rgba(18,184,134,0.2)' : '#d3f9e8'),
|
|
628
|
+
borderRadius: '9999px',
|
|
629
|
+
'& .MuiChip-label': { px: 1 },
|
|
630
|
+
}}
|
|
631
|
+
/>
|
|
632
|
+
)}
|
|
633
|
+
</Stack>
|
|
634
|
+
<Typography sx={{ fontSize: 13, color: 'text.primary', whiteSpace: 'nowrap', fontWeight: 700 }}>
|
|
635
|
+
{isUpselled ? downsellPrice : upsellPrice}
|
|
636
|
+
</Typography>
|
|
637
|
+
</Stack>
|
|
638
|
+
</Box>
|
|
639
|
+
)}
|
|
640
|
+
</Box>
|
|
641
|
+
);
|
|
642
|
+
}
|