@blocklet/payment-react 1.25.10 → 1.26.0
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 +615 -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 +799 -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 +634 -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 +681 -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 +204 -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,134 @@
|
|
|
1
|
+
import { Divider, Stack, Typography } from '@mui/material';
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import {
|
|
4
|
+
useSessionContext,
|
|
5
|
+
usePaymentMethodContext,
|
|
6
|
+
useSubmitFeature,
|
|
7
|
+
useCustomerFormFeature,
|
|
8
|
+
} from '@blocklet/payment-react-headless';
|
|
9
|
+
import StripeForm from '../../../payment/form/stripe';
|
|
10
|
+
import ConfirmDialog from '../../../components/confirm';
|
|
11
|
+
import PriceChangeConfirm from '../../../components/price-change-confirm';
|
|
12
|
+
import { formatTokenAmount } from '../../utils/format';
|
|
13
|
+
|
|
14
|
+
function getRedirectUrl(session: any): string | undefined {
|
|
15
|
+
try {
|
|
16
|
+
const params = new URLSearchParams(window.location.search);
|
|
17
|
+
const redirect = params.get('redirect');
|
|
18
|
+
if (redirect) return decodeURIComponent(redirect);
|
|
19
|
+
} catch {
|
|
20
|
+
// ignore
|
|
21
|
+
}
|
|
22
|
+
if (session?.success_url) return session.success_url;
|
|
23
|
+
if (session?.payment_link?.after_completion?.redirect?.url) {
|
|
24
|
+
return session.payment_link.after_completion.redirect.url;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function CheckoutDialogs() {
|
|
30
|
+
const { t } = useLocaleContext();
|
|
31
|
+
const { session } = useSessionContext();
|
|
32
|
+
const { currency, stripe } = usePaymentMethodContext();
|
|
33
|
+
const submit = useSubmitFeature();
|
|
34
|
+
const form = useCustomerFormFeature();
|
|
35
|
+
const mode = session?.mode || 'payment';
|
|
36
|
+
|
|
37
|
+
// Stripe dialog
|
|
38
|
+
const stripeContext = submit.context?.type === 'stripe' ? submit.context : null;
|
|
39
|
+
const showStripeDialog = submit.status === 'waiting_stripe' && stripeContext?.clientSecret;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{/* Stripe Payment Dialog */}
|
|
44
|
+
{showStripeDialog && (
|
|
45
|
+
<StripeForm
|
|
46
|
+
clientSecret={stripeContext!.clientSecret}
|
|
47
|
+
intentType={stripeContext!.intentType || 'payment_intent'}
|
|
48
|
+
publicKey={stripe?.publishableKey || ''}
|
|
49
|
+
mode={mode}
|
|
50
|
+
customer={
|
|
51
|
+
{
|
|
52
|
+
name: form.values.customer_name,
|
|
53
|
+
email: form.values.customer_email,
|
|
54
|
+
phone: form.values.customer_phone,
|
|
55
|
+
address: form.values.billing_address,
|
|
56
|
+
} as any
|
|
57
|
+
}
|
|
58
|
+
returnUrl={getRedirectUrl(session)}
|
|
59
|
+
onConfirm={submit.stripeConfirm}
|
|
60
|
+
onCancel={submit.stripeCancel}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* Price Change Confirmation Dialog — reuse V1's PriceChangeConfirm component */}
|
|
65
|
+
{submit.status === 'confirming_price' && submit.context?.type === 'price_change' && (
|
|
66
|
+
<PriceChangeConfirm
|
|
67
|
+
open
|
|
68
|
+
changePercent={submit.context.changePercent}
|
|
69
|
+
onConfirm={submit.confirm}
|
|
70
|
+
onCancel={submit.cancel}
|
|
71
|
+
loading={false}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* Fast Pay Confirmation Dialog */}
|
|
76
|
+
{submit.status === 'confirming_fast_pay' && submit.context?.type === 'fast_pay' && (
|
|
77
|
+
<ConfirmDialog
|
|
78
|
+
onConfirm={submit.confirm}
|
|
79
|
+
onCancel={submit.cancel}
|
|
80
|
+
title={
|
|
81
|
+
submit.context.payType === 'credit'
|
|
82
|
+
? t('payment.checkout.fastPay.credit.title')
|
|
83
|
+
: t('payment.checkout.fastPay.title')
|
|
84
|
+
}
|
|
85
|
+
message={
|
|
86
|
+
submit.context.payType === 'credit' ? (
|
|
87
|
+
<Typography>
|
|
88
|
+
{t('payment.checkout.fastPay.credit.meteringSubscriptionMessage', {
|
|
89
|
+
available: `${formatTokenAmount(submit.context.amount || '0', currency)} ${currency?.symbol || ''}`,
|
|
90
|
+
})}
|
|
91
|
+
</Typography>
|
|
92
|
+
) : (
|
|
93
|
+
<Stack>
|
|
94
|
+
<Typography>{t('payment.checkout.fastPay.autoPaymentReason')}</Typography>
|
|
95
|
+
<Divider sx={{ mt: 1.5, mb: 1.5 }} />
|
|
96
|
+
<Stack spacing={1}>
|
|
97
|
+
<Stack sx={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
98
|
+
<Typography sx={{ color: 'text.primary', whiteSpace: 'nowrap' }}>
|
|
99
|
+
{t('payment.checkout.fastPay.payer')}
|
|
100
|
+
</Typography>
|
|
101
|
+
<Typography sx={{ color: 'text.secondary', fontSize: 14 }}>
|
|
102
|
+
{submit.context.payer
|
|
103
|
+
? `${submit.context.payer.slice(0, 10)}...${submit.context.payer.slice(-6)}`
|
|
104
|
+
: ''}
|
|
105
|
+
</Typography>
|
|
106
|
+
</Stack>
|
|
107
|
+
<Stack sx={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
108
|
+
<Typography sx={{ color: 'text.primary' }}>{t('payment.checkout.fastPay.amount')}</Typography>
|
|
109
|
+
<Typography>
|
|
110
|
+
{formatTokenAmount(submit.context.amount || '0', currency)} {currency?.symbol || ''}
|
|
111
|
+
</Typography>
|
|
112
|
+
</Stack>
|
|
113
|
+
</Stack>
|
|
114
|
+
</Stack>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
loading={false}
|
|
118
|
+
color="primary"
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Credit Insufficient Dialog (matches V1 ConfirmDialog) */}
|
|
123
|
+
{submit.status === 'credit_insufficient' && submit.context?.type === 'credit_insufficient' && (
|
|
124
|
+
<ConfirmDialog
|
|
125
|
+
onConfirm={submit.cancel}
|
|
126
|
+
onCancel={submit.cancel}
|
|
127
|
+
title={t('payment.checkout.fastPay.credit.insufficientTitle')}
|
|
128
|
+
message={<Typography>{t('payment.checkout.fastPay.credit.insufficientMessage')}</Typography>}
|
|
129
|
+
confirm={t('common.confirm')}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
|
|
2
|
+
import { Box, CircularProgress, Stack, Typography } from '@mui/material';
|
|
3
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
+
import type { BillingIntervalData } from '@blocklet/payment-react-headless';
|
|
5
|
+
import { INTERVAL_LOCALE_KEY } from '../../utils/format';
|
|
6
|
+
|
|
7
|
+
interface BillingToggleProps {
|
|
8
|
+
billingInterval: BillingIntervalData | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function BillingToggle({ billingInterval }: BillingToggleProps) {
|
|
12
|
+
const { t } = useLocaleContext();
|
|
13
|
+
|
|
14
|
+
if (!billingInterval?.available?.length || billingInterval.available.length <= 1) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const best = billingInterval.available.reduce((a: any, b: any) =>
|
|
19
|
+
Number(b.savings || 0) > Number(a.savings || 0) ? b : a
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Stack direction="row" alignItems="center" spacing={2} sx={{ mb: 3 }}>
|
|
24
|
+
{/* Capsule container — design: bg-white p-1 rounded-full border-slate-200 shadow-sm */}
|
|
25
|
+
<Stack
|
|
26
|
+
direction="row"
|
|
27
|
+
alignItems="center"
|
|
28
|
+
sx={{
|
|
29
|
+
bgcolor: 'background.paper',
|
|
30
|
+
borderRadius: '9999px',
|
|
31
|
+
border: '1px solid',
|
|
32
|
+
borderColor: 'divider',
|
|
33
|
+
p: '4px',
|
|
34
|
+
display: 'inline-flex',
|
|
35
|
+
boxShadow: (theme) =>
|
|
36
|
+
theme.palette.mode === 'dark' ? '0 1px 2px 0 rgba(0,0,0,0.3)' : '0 1px 2px 0 rgba(0,0,0,0.05)',
|
|
37
|
+
}}>
|
|
38
|
+
{billingInterval.available.map((option) => {
|
|
39
|
+
const isSelected = option.interval === billingInterval.current;
|
|
40
|
+
const selectedSx = {
|
|
41
|
+
bgcolor: (th: any) => (th.palette.mode === 'dark' ? 'rgba(156,106,222,0.25)' : 'rgba(156,106,222,0.15)'),
|
|
42
|
+
color: 'primary.main',
|
|
43
|
+
backdropFilter: 'blur(8px)',
|
|
44
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
45
|
+
boxShadow: (th: any) =>
|
|
46
|
+
th.palette.mode === 'dark'
|
|
47
|
+
? '0 2px 8px rgba(156,106,222,0.2), inset 0 1px 0 rgba(255,255,255,0.06)'
|
|
48
|
+
: '0 2px 8px rgba(156,106,222,0.15), inset 0 1px 0 rgba(255,255,255,0.5)',
|
|
49
|
+
border: '1px solid',
|
|
50
|
+
borderColor: (th: any) => (th.palette.mode === 'dark' ? 'rgba(156,106,222,0.3)' : 'rgba(156,106,222,0.2)'),
|
|
51
|
+
};
|
|
52
|
+
const unselectedSx = {
|
|
53
|
+
color: 'text.secondary',
|
|
54
|
+
border: '1px solid transparent',
|
|
55
|
+
'&:hover': { color: 'text.primary' },
|
|
56
|
+
};
|
|
57
|
+
return (
|
|
58
|
+
<Box
|
|
59
|
+
key={option.interval}
|
|
60
|
+
onClick={() => {
|
|
61
|
+
if (option.interval !== billingInterval.current && !billingInterval.switching) {
|
|
62
|
+
billingInterval.switch(option.interval);
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
sx={{
|
|
66
|
+
px: 3.5,
|
|
67
|
+
py: 1,
|
|
68
|
+
borderRadius: '9999px',
|
|
69
|
+
cursor: 'pointer',
|
|
70
|
+
transition: 'all 0.3s ease',
|
|
71
|
+
userSelect: 'none',
|
|
72
|
+
...(isSelected ? selectedSx : unselectedSx),
|
|
73
|
+
}}>
|
|
74
|
+
{billingInterval.switching && isSelected ? (
|
|
75
|
+
<CircularProgress size={14} color="inherit" sx={{ my: '1px' }} />
|
|
76
|
+
) : (
|
|
77
|
+
<Typography
|
|
78
|
+
component="span"
|
|
79
|
+
sx={{
|
|
80
|
+
fontSize: 14,
|
|
81
|
+
fontWeight: 700,
|
|
82
|
+
color: 'inherit',
|
|
83
|
+
lineHeight: 1,
|
|
84
|
+
}}>
|
|
85
|
+
{t(INTERVAL_LOCALE_KEY[option.interval] || option.interval)}
|
|
86
|
+
</Typography>
|
|
87
|
+
)}
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</Stack>
|
|
92
|
+
|
|
93
|
+
{/* SAVE badge — design: bg-[#ebfef5] text-[#12b886] border-[#d3f9e8] trending_down icon */}
|
|
94
|
+
{best.savings && Number(best.savings) > 0 && (
|
|
95
|
+
<Stack
|
|
96
|
+
direction="row"
|
|
97
|
+
alignItems="center"
|
|
98
|
+
spacing={0.75}
|
|
99
|
+
sx={{
|
|
100
|
+
px: 1.5,
|
|
101
|
+
py: 0.75,
|
|
102
|
+
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(18,184,134,0.1)' : '#ebfef5'),
|
|
103
|
+
color: '#12b886',
|
|
104
|
+
fontSize: 11,
|
|
105
|
+
fontWeight: 700,
|
|
106
|
+
borderRadius: '9999px',
|
|
107
|
+
border: '1px solid',
|
|
108
|
+
borderColor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(18,184,134,0.2)' : '#d3f9e8'),
|
|
109
|
+
textTransform: 'uppercase',
|
|
110
|
+
letterSpacing: '0.05em',
|
|
111
|
+
}}>
|
|
112
|
+
<TrendingDownIcon sx={{ fontSize: 14 }} />
|
|
113
|
+
<Typography
|
|
114
|
+
component="span"
|
|
115
|
+
sx={{ fontSize: 'inherit', fontWeight: 'inherit', color: 'inherit', lineHeight: 1 }}>
|
|
116
|
+
SAVE {best.savings}%
|
|
117
|
+
</Typography>
|
|
118
|
+
</Stack>
|
|
119
|
+
)}
|
|
120
|
+
</Stack>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
|
|
2
|
+
import ShoppingCartCheckoutIcon from '@mui/icons-material/ShoppingCartCheckout';
|
|
3
|
+
import { Avatar, Box, Button, Chip, Stack, Typography } from '@mui/material';
|
|
4
|
+
import type { TPaymentCurrency, TPrice } from '@blocklet/payment-types';
|
|
5
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
+
import { formatDynamicUnitPrice, tSafe, INTERVAL_LOCALE_KEY } from '../../utils/format';
|
|
7
|
+
|
|
8
|
+
interface CrossSellCardProps {
|
|
9
|
+
crossSellItem: TPrice;
|
|
10
|
+
currency: TPaymentCurrency | null;
|
|
11
|
+
exchangeRate: string | null;
|
|
12
|
+
crossSellRequired: boolean;
|
|
13
|
+
onAdd: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function CrossSellCard({
|
|
17
|
+
crossSellItem,
|
|
18
|
+
currency,
|
|
19
|
+
exchangeRate,
|
|
20
|
+
crossSellRequired,
|
|
21
|
+
onAdd,
|
|
22
|
+
}: CrossSellCardProps) {
|
|
23
|
+
const { t } = useLocaleContext();
|
|
24
|
+
if (!crossSellItem) return null;
|
|
25
|
+
|
|
26
|
+
const { product } = crossSellItem as any;
|
|
27
|
+
const productImage = product?.images?.[0] || '';
|
|
28
|
+
const productName = product?.name || t('payment.checkout.cross_sell.add');
|
|
29
|
+
const { recurring } = crossSellItem as any;
|
|
30
|
+
const intervalKey = recurring?.interval ? INTERVAL_LOCALE_KEY[recurring.interval] : null;
|
|
31
|
+
// Show interval label (e.g. "monthly") instead of description when description repeats title
|
|
32
|
+
const rawDescription = product?.description || '';
|
|
33
|
+
const subtitle = (() => {
|
|
34
|
+
if (rawDescription && rawDescription !== productName) return rawDescription;
|
|
35
|
+
return intervalKey ? t(intervalKey) : '';
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box sx={{ position: 'relative' }}>
|
|
40
|
+
{/* Recommended badge — top-right, matching product-item-card */}
|
|
41
|
+
{crossSellRequired && (
|
|
42
|
+
<Chip
|
|
43
|
+
label={tSafe(t, 'payment.checkout.cross_sell.recommended', 'RECOMMENDED')}
|
|
44
|
+
size="small"
|
|
45
|
+
sx={{
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: 0,
|
|
48
|
+
right: 40,
|
|
49
|
+
transform: 'translateY(-50%)',
|
|
50
|
+
zIndex: 1,
|
|
51
|
+
height: 22,
|
|
52
|
+
fontSize: 9,
|
|
53
|
+
fontWeight: 900,
|
|
54
|
+
letterSpacing: '0.12em',
|
|
55
|
+
bgcolor: 'primary.main',
|
|
56
|
+
color: '#fff',
|
|
57
|
+
boxShadow: '0 4px 12px rgba(45,124,243,0.2)',
|
|
58
|
+
'& .MuiChip-label': { px: 1.5 },
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
p: { xs: 2, md: 3 },
|
|
66
|
+
bgcolor: 'background.paper',
|
|
67
|
+
borderRadius: { xs: '16px', md: '24px' },
|
|
68
|
+
border: '2px dashed',
|
|
69
|
+
borderColor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.15)' : 'rgba(45,124,243,0.25)'),
|
|
70
|
+
boxShadow: (theme) =>
|
|
71
|
+
theme.palette.mode === 'dark' ? '0 12px 40px -8px rgba(0,0,0,0.3)' : '0 12px 40px -8px rgba(0,0,0,0.06)',
|
|
72
|
+
transition: 'all 0.3s ease',
|
|
73
|
+
cursor: 'pointer',
|
|
74
|
+
'&:hover': {
|
|
75
|
+
borderColor: 'primary.main',
|
|
76
|
+
boxShadow: '0 12px 40px -8px rgba(0,0,0,0.1)',
|
|
77
|
+
},
|
|
78
|
+
}}
|
|
79
|
+
onClick={onAdd}>
|
|
80
|
+
<Stack direction="row" spacing={{ xs: 1.5, md: 2.5 }} sx={{ alignItems: 'center', width: '100%' }}>
|
|
81
|
+
{/* Product avatar */}
|
|
82
|
+
{productImage ? (
|
|
83
|
+
<Avatar
|
|
84
|
+
src={productImage}
|
|
85
|
+
variant="rounded"
|
|
86
|
+
sx={{
|
|
87
|
+
width: { xs: 44, md: 64 },
|
|
88
|
+
height: { xs: 44, md: 64 },
|
|
89
|
+
borderRadius: { xs: '12px', md: '16px' },
|
|
90
|
+
flexShrink: 0,
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<Avatar
|
|
95
|
+
variant="rounded"
|
|
96
|
+
sx={{
|
|
97
|
+
width: { xs: 44, md: 64 },
|
|
98
|
+
height: { xs: 44, md: 64 },
|
|
99
|
+
borderRadius: { xs: '12px', md: '16px' },
|
|
100
|
+
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : '#eff6ff'),
|
|
101
|
+
flexShrink: 0,
|
|
102
|
+
}}>
|
|
103
|
+
<ShoppingCartCheckoutIcon sx={{ fontSize: { xs: 22, md: 28 }, color: 'primary.main', opacity: 0.45 }} />
|
|
104
|
+
</Avatar>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
108
|
+
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 0.25 }}>
|
|
109
|
+
<Typography
|
|
110
|
+
sx={{ fontWeight: 800, fontSize: { xs: 15, md: 18 }, color: 'text.primary', lineHeight: 1.3 }}>
|
|
111
|
+
{productName}
|
|
112
|
+
</Typography>
|
|
113
|
+
<Stack alignItems="flex-end" sx={{ flexShrink: 0, ml: { xs: 1, md: 2 } }}>
|
|
114
|
+
<Typography
|
|
115
|
+
sx={{ fontWeight: 800, color: 'text.primary', whiteSpace: 'nowrap', fontSize: { xs: 15, md: 18 } }}>
|
|
116
|
+
{formatDynamicUnitPrice(crossSellItem as any, currency, exchangeRate)} {currency?.symbol}
|
|
117
|
+
</Typography>
|
|
118
|
+
{exchangeRate && (crossSellItem as any).base_amount && (
|
|
119
|
+
<Typography sx={{ fontSize: 10, color: 'text.disabled', fontWeight: 700, lineHeight: 1 }}>
|
|
120
|
+
≈ ${Number((crossSellItem as any).base_amount || 0).toFixed(2)}
|
|
121
|
+
</Typography>
|
|
122
|
+
)}
|
|
123
|
+
</Stack>
|
|
124
|
+
</Stack>
|
|
125
|
+
{subtitle && (
|
|
126
|
+
<Typography
|
|
127
|
+
sx={{ fontSize: { xs: 12, md: 14 }, color: 'text.secondary', fontWeight: 500, lineHeight: 1.4 }}
|
|
128
|
+
noWrap>
|
|
129
|
+
{subtitle}
|
|
130
|
+
</Typography>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
<Box sx={{ pt: { xs: 1.5, md: 2 } }}>
|
|
134
|
+
<Button
|
|
135
|
+
size="small"
|
|
136
|
+
variant="outlined"
|
|
137
|
+
startIcon={<AddShoppingCartIcon sx={{ fontSize: '14px !important' }} />}
|
|
138
|
+
sx={{
|
|
139
|
+
textTransform: 'none',
|
|
140
|
+
fontSize: { xs: 11, md: 12 },
|
|
141
|
+
fontWeight: 700,
|
|
142
|
+
borderRadius: '8px',
|
|
143
|
+
border: '1px solid',
|
|
144
|
+
borderColor: 'divider',
|
|
145
|
+
color: 'primary.main',
|
|
146
|
+
bgcolor: 'background.paper',
|
|
147
|
+
boxShadow: 1,
|
|
148
|
+
px: 1.5,
|
|
149
|
+
py: 0.5,
|
|
150
|
+
transition: 'all 0.2s',
|
|
151
|
+
'&:hover': {
|
|
152
|
+
bgcolor: 'primary.main',
|
|
153
|
+
color: '#fff',
|
|
154
|
+
borderColor: 'primary.main',
|
|
155
|
+
},
|
|
156
|
+
'&:active': { transform: 'scale(0.95)' },
|
|
157
|
+
}}
|
|
158
|
+
onClick={(e) => {
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
onAdd();
|
|
161
|
+
}}>
|
|
162
|
+
{tSafe(t, 'payment.checkout.cross_sell.addToOrder', 'Add to order')}
|
|
163
|
+
</Button>
|
|
164
|
+
</Box>
|
|
165
|
+
</Box>
|
|
166
|
+
</Stack>
|
|
167
|
+
</Box>
|
|
168
|
+
</Box>
|
|
169
|
+
);
|
|
170
|
+
}
|