@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.
Files changed (160) hide show
  1. package/es/checkout-v2/checkout-v2.d.ts +2 -0
  2. package/es/checkout-v2/checkout-v2.js +121 -0
  3. package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
  4. package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
  5. package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
  6. package/es/checkout-v2/components/left/billing-toggle.js +118 -0
  7. package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
  8. package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
  9. package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
  10. package/es/checkout-v2/components/left/product-item-card.js +571 -0
  11. package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
  12. package/es/checkout-v2/components/left/promotion-input.js +178 -0
  13. package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
  14. package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
  15. package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
  16. package/es/checkout-v2/components/left/trial-info.js +48 -0
  17. package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
  18. package/es/checkout-v2/components/right/currency-grid.js +48 -0
  19. package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
  20. package/es/checkout-v2/components/right/customer-info-card.js +156 -0
  21. package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
  22. package/es/checkout-v2/components/right/status-feedback.js +17 -0
  23. package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
  24. package/es/checkout-v2/components/right/submit-button.js +29 -0
  25. package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
  26. package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
  27. package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
  28. package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
  29. package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
  30. package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
  31. package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
  32. package/es/checkout-v2/components/shared/total-display.js +84 -0
  33. package/es/checkout-v2/index.d.ts +2 -0
  34. package/es/checkout-v2/index.js +1 -0
  35. package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
  36. package/es/checkout-v2/layouts/checkout-layout.js +226 -0
  37. package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
  38. package/es/checkout-v2/panels/left/composite-panel.js +423 -0
  39. package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
  40. package/es/checkout-v2/panels/left/credit-topup-panel.js +615 -0
  41. package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
  42. package/es/checkout-v2/panels/left/scenario-router.js +19 -0
  43. package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
  44. package/es/checkout-v2/panels/right/payment-panel.js +644 -0
  45. package/es/checkout-v2/types.d.ts +15 -0
  46. package/es/checkout-v2/types.js +0 -0
  47. package/es/checkout-v2/utils/format.d.ts +59 -0
  48. package/es/checkout-v2/utils/format.js +125 -0
  49. package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
  50. package/es/checkout-v2/utils/scenario-detector.js +17 -0
  51. package/es/checkout-v2/views/error-view.d.ts +7 -0
  52. package/es/checkout-v2/views/error-view.js +269 -0
  53. package/es/checkout-v2/views/loading-view.d.ts +5 -0
  54. package/es/checkout-v2/views/loading-view.js +158 -0
  55. package/es/checkout-v2/views/success-view.d.ts +29 -0
  56. package/es/checkout-v2/views/success-view.js +614 -0
  57. package/es/components/phone-field.d.ts +14 -0
  58. package/es/components/phone-field.js +96 -0
  59. package/es/index.d.ts +3 -1
  60. package/es/index.js +3 -1
  61. package/es/locales/en.js +45 -6
  62. package/es/locales/zh.js +45 -6
  63. package/es/payment/form/index.js +10 -1
  64. package/lib/checkout-v2/checkout-v2.d.ts +2 -0
  65. package/lib/checkout-v2/checkout-v2.js +151 -0
  66. package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
  67. package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
  68. package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
  69. package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
  70. package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
  71. package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
  72. package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
  73. package/lib/checkout-v2/components/left/product-item-card.js +738 -0
  74. package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
  75. package/lib/checkout-v2/components/left/promotion-input.js +220 -0
  76. package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
  77. package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
  78. package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
  79. package/lib/checkout-v2/components/left/trial-info.js +82 -0
  80. package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
  81. package/lib/checkout-v2/components/right/currency-grid.js +96 -0
  82. package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
  83. package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
  84. package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
  85. package/lib/checkout-v2/components/right/status-feedback.js +30 -0
  86. package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
  87. package/lib/checkout-v2/components/right/submit-button.js +35 -0
  88. package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
  89. package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
  90. package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
  91. package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
  92. package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
  93. package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
  94. package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
  95. package/lib/checkout-v2/components/shared/total-display.js +154 -0
  96. package/lib/checkout-v2/index.d.ts +2 -0
  97. package/lib/checkout-v2/index.js +13 -0
  98. package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
  99. package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
  100. package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
  101. package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
  102. package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
  103. package/lib/checkout-v2/panels/left/credit-topup-panel.js +799 -0
  104. package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
  105. package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
  106. package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
  107. package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
  108. package/lib/checkout-v2/types.d.ts +15 -0
  109. package/lib/checkout-v2/types.js +1 -0
  110. package/lib/checkout-v2/utils/format.d.ts +59 -0
  111. package/lib/checkout-v2/utils/format.js +158 -0
  112. package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
  113. package/lib/checkout-v2/utils/scenario-detector.js +23 -0
  114. package/lib/checkout-v2/views/error-view.d.ts +7 -0
  115. package/lib/checkout-v2/views/error-view.js +321 -0
  116. package/lib/checkout-v2/views/loading-view.d.ts +5 -0
  117. package/lib/checkout-v2/views/loading-view.js +168 -0
  118. package/lib/checkout-v2/views/success-view.d.ts +29 -0
  119. package/lib/checkout-v2/views/success-view.js +735 -0
  120. package/lib/components/phone-field.d.ts +14 -0
  121. package/lib/components/phone-field.js +130 -0
  122. package/lib/index.d.ts +3 -1
  123. package/lib/index.js +8 -0
  124. package/lib/locales/en.js +45 -6
  125. package/lib/locales/zh.js +45 -6
  126. package/lib/payment/form/index.js +10 -1
  127. package/package.json +4 -3
  128. package/src/checkout-v2/checkout-v2.tsx +155 -0
  129. package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
  130. package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
  131. package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
  132. package/src/checkout-v2/components/left/product-item-card.tsx +634 -0
  133. package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
  134. package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
  135. package/src/checkout-v2/components/left/trial-info.tsx +63 -0
  136. package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
  137. package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
  138. package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
  139. package/src/checkout-v2/components/right/submit-button.tsx +37 -0
  140. package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
  141. package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
  142. package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
  143. package/src/checkout-v2/components/shared/total-display.tsx +112 -0
  144. package/src/checkout-v2/index.ts +2 -0
  145. package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
  146. package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
  147. package/src/checkout-v2/panels/left/credit-topup-panel.tsx +681 -0
  148. package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
  149. package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
  150. package/src/checkout-v2/types.ts +18 -0
  151. package/src/checkout-v2/utils/format.ts +204 -0
  152. package/src/checkout-v2/utils/scenario-detector.ts +30 -0
  153. package/src/checkout-v2/views/error-view.tsx +293 -0
  154. package/src/checkout-v2/views/loading-view.tsx +162 -0
  155. package/src/checkout-v2/views/success-view.tsx +770 -0
  156. package/src/components/phone-field.tsx +119 -0
  157. package/src/index.ts +3 -0
  158. package/src/locales/en.tsx +45 -4
  159. package/src/locales/zh.tsx +43 -4
  160. 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
+ }