@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,27 @@
1
+ import { Typography } from '@mui/material';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+
4
+ interface SubscriptionDisclaimerProps {
5
+ mode: string;
6
+ subscription: {
7
+ confirmMessage: string;
8
+ showStake: boolean;
9
+ } | null;
10
+ staking: string | null;
11
+ appName: string;
12
+ }
13
+
14
+ export default function SubscriptionDisclaimer({ mode, subscription, staking, appName }: SubscriptionDisclaimerProps) {
15
+ const { t } = useLocaleContext();
16
+
17
+ if (!['subscription', 'setup'].includes(mode) || !subscription) return null;
18
+
19
+ return (
20
+ <Typography sx={{ mt: 2.5, color: 'text.secondary', fontSize: '0.7875rem', lineHeight: '0.9625rem' }}>
21
+ {subscription.confirmMessage ||
22
+ (subscription.showStake && staking
23
+ ? t('payment.checkout.confirm.withStake', { payee: appName || 'New Payment Kit' })
24
+ : t('payment.checkout.confirm.withoutStake', { payee: appName || 'New Payment Kit' }))}
25
+ </Typography>
26
+ );
27
+ }
@@ -0,0 +1,221 @@
1
+ import { useEffect, useState } from 'react';
2
+ import TuneIcon from '@mui/icons-material/Tune';
3
+ import { Box, Dialog, DialogContent, DialogTitle, Fade, Stack, Tooltip, Typography, keyframes } from '@mui/material';
4
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
+ import SlippageConfig from '../../../components/slippage-config';
6
+ import { whiteTooltipSx } from '../../utils/format';
7
+
8
+ const ping = keyframes`
9
+ 75%, 100% { transform: scale(2); opacity: 0; }
10
+ `;
11
+
12
+ function formatDateTime(ts: number | null): string {
13
+ if (!ts) return '—';
14
+ const d = new Date(ts);
15
+ const pad = (n: number) => String(n).padStart(2, '0');
16
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
17
+ }
18
+
19
+ interface ExchangeRateFooterProps {
20
+ hasDynamicPricing: boolean;
21
+ rate: {
22
+ value: string | null;
23
+ display: string | null;
24
+ provider: string | null;
25
+ providerDisplay: string | null;
26
+ fetchedAt: number | null;
27
+ status: 'loading' | 'available' | 'unavailable';
28
+ };
29
+ slippage: {
30
+ percent: number;
31
+ set: (config: { mode: string; percent: number; base_currency?: string }) => Promise<void>;
32
+ };
33
+ currencySymbol: string;
34
+ isSubscription: boolean;
35
+ }
36
+
37
+ export default function ExchangeRateFooter({
38
+ hasDynamicPricing,
39
+ rate,
40
+ slippage,
41
+ currencySymbol,
42
+ isSubscription,
43
+ }: ExchangeRateFooterProps) {
44
+ const { t } = useLocaleContext();
45
+ const [dialogOpen, setDialogOpen] = useState(false);
46
+ const [pendingConfig, setPendingConfig] = useState<{ mode: 'percent' | 'rate'; percent: number } | null>(null);
47
+ const [submitting, setSubmitting] = useState(false);
48
+
49
+ // Optimistic local slippage — sync from prop, update immediately on save
50
+ const [localSlippage, setLocalSlippage] = useState(slippage?.percent ?? 0.5);
51
+ useEffect(() => {
52
+ setLocalSlippage(slippage?.percent ?? 0.5);
53
+ }, [slippage?.percent]);
54
+
55
+ if (!hasDynamicPricing || !rate.value || rate.status === 'unavailable') return null;
56
+
57
+ // Use pre-formatted display (e.g. "$0.19") — no redundant "USD" suffix
58
+ const rateDisplay = rate.display || `$${Number(rate.value).toFixed(2)}`;
59
+ const showSlippage = isSubscription && typeof slippage?.set === 'function';
60
+
61
+ const handleOpenDialog = () => {
62
+ setPendingConfig({ mode: 'percent', percent: localSlippage });
63
+ setDialogOpen(true);
64
+ };
65
+
66
+ const handleCloseDialog = () => {
67
+ setDialogOpen(false);
68
+ setPendingConfig(null);
69
+ };
70
+
71
+ const handleSlippageChange = (value: number) => {
72
+ setPendingConfig((prev) => (prev ? { ...prev, percent: value } : { mode: 'percent' as const, percent: value }));
73
+ };
74
+
75
+ const handleConfigChange = (config: { mode: 'percent' | 'rate'; percent: number }) => {
76
+ setPendingConfig(config);
77
+ };
78
+
79
+ const handleSubmit = async () => {
80
+ if (!pendingConfig || !slippage?.set) return;
81
+ setSubmitting(true);
82
+ try {
83
+ // Optimistic update — show new value immediately
84
+ setLocalSlippage(pendingConfig.percent);
85
+ await slippage.set({ ...pendingConfig, base_currency: 'USD' });
86
+ setDialogOpen(false);
87
+ setPendingConfig(null);
88
+ } catch (err) {
89
+ // Revert on error
90
+ setLocalSlippage(slippage?.percent ?? 0.5);
91
+ console.error('Failed to update slippage', err);
92
+ } finally {
93
+ setSubmitting(false);
94
+ }
95
+ };
96
+
97
+ const labelSx = {
98
+ fontSize: 11,
99
+ fontWeight: 700,
100
+ color: 'primary.main',
101
+ letterSpacing: '0.02em',
102
+ };
103
+
104
+ // Hover tooltip content: full rate + provider + update time
105
+ const providerName = rate.providerDisplay || rate.provider || '—';
106
+ const updatedAt = formatDateTime(rate.fetchedAt);
107
+ const fullRate = `$${rate.value}`;
108
+
109
+ const tooltipSx = whiteTooltipSx;
110
+
111
+ const rowSx = { fontSize: 12, color: 'text.secondary', lineHeight: 1.4 };
112
+ const valSx = { fontSize: 12, fontWeight: 600, color: 'text.primary', lineHeight: 1.4 };
113
+
114
+ const tooltipContent = (
115
+ <Stack spacing={0.75}>
116
+ {/* Full precision rate */}
117
+ <Typography sx={{ fontSize: 13, fontWeight: 700, color: 'text.primary', mb: 0.25 }}>
118
+ 1 {currencySymbol} = {fullRate}
119
+ </Typography>
120
+ <Box sx={{ borderTop: '1px solid', borderColor: 'divider', pt: 0.75 }}>
121
+ <Stack spacing={0.5}>
122
+ <Stack direction="row" justifyContent="space-between" spacing={2}>
123
+ <Typography sx={rowSx}>{t('payment.checkout.quote.detailProvider')}</Typography>
124
+ <Typography sx={valSx}>{providerName}</Typography>
125
+ </Stack>
126
+ <Stack direction="row" justifyContent="space-between" spacing={2}>
127
+ <Typography sx={rowSx}>{t('payment.checkout.quote.detailUpdatedAt')}</Typography>
128
+ <Typography sx={valSx}>{updatedAt}</Typography>
129
+ </Stack>
130
+ </Stack>
131
+ </Box>
132
+ </Stack>
133
+ );
134
+
135
+ // Slippage tooltip
136
+ const slippageTooltip = t('payment.checkout.quote.slippage.tooltip');
137
+
138
+ return (
139
+ <>
140
+ <Box sx={{ width: '100%', pt: 3, pb: 1, borderTop: '1px solid', borderColor: 'divider' }}>
141
+ <Fade in timeout={300}>
142
+ <Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
143
+ {/* Rate with hover tooltip showing full rate + provider + update time */}
144
+ <Tooltip title={tooltipContent} placement="top" arrow slotProps={{ popper: { sx: tooltipSx } }}>
145
+ <Stack direction="row" alignItems="center" spacing={1} sx={{ cursor: 'default' }}>
146
+ {/* Ping dot */}
147
+ <Box sx={{ position: 'relative', width: 8, height: 8, flexShrink: 0 }}>
148
+ <Box
149
+ sx={{
150
+ position: 'absolute',
151
+ inset: 0,
152
+ borderRadius: '50%',
153
+ bgcolor: '#60a5fa',
154
+ opacity: 0.6,
155
+ animation: `${ping} 1s cubic-bezier(0, 0, 0.2, 1) infinite`,
156
+ }}
157
+ />
158
+ <Box
159
+ sx={{
160
+ position: 'relative',
161
+ width: 8,
162
+ height: 8,
163
+ borderRadius: '50%',
164
+ bgcolor: 'primary.main',
165
+ }}
166
+ />
167
+ </Box>
168
+ <Typography sx={labelSx}>
169
+ 1 {currencySymbol} ≈ {rateDisplay}
170
+ </Typography>
171
+ </Stack>
172
+ </Tooltip>
173
+
174
+ {/* Slippage with tooltip + click to configure */}
175
+ {showSlippage && (
176
+ <Tooltip title={slippageTooltip} placement="top" arrow slotProps={{ popper: { sx: whiteTooltipSx } }}>
177
+ <Stack
178
+ direction="row"
179
+ alignItems="center"
180
+ spacing={0.75}
181
+ onClick={handleOpenDialog}
182
+ sx={{
183
+ cursor: 'pointer',
184
+ '&:hover': { opacity: 1 },
185
+ '&:hover .tune-icon': { transform: 'rotate(90deg)' },
186
+ transition: 'opacity 0.2s',
187
+ }}>
188
+ <TuneIcon
189
+ className="tune-icon"
190
+ sx={{ fontSize: 16, color: 'primary.main', transition: 'transform 0.5s' }}
191
+ />
192
+ <Typography sx={labelSx}>
193
+ {t('payment.checkout.quote.detailSlippage')} {localSlippage}%
194
+ </Typography>
195
+ </Stack>
196
+ </Tooltip>
197
+ )}
198
+ </Stack>
199
+ </Fade>
200
+ </Box>
201
+
202
+ <Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
203
+ <DialogTitle>{t('payment.checkout.quote.slippage.title')}</DialogTitle>
204
+ <DialogContent>
205
+ <SlippageConfig
206
+ value={pendingConfig?.percent ?? localSlippage}
207
+ onChange={handleSlippageChange}
208
+ config={pendingConfig || { mode: 'percent' as const, percent: localSlippage }}
209
+ onConfigChange={handleConfigChange}
210
+ exchangeRate={rate.value ? String(rate.value) : null}
211
+ baseCurrency="USD"
212
+ disabled={submitting}
213
+ sx={{ mt: 1 }}
214
+ onCancel={handleCloseDialog}
215
+ onSave={handleSubmit}
216
+ />
217
+ </DialogContent>
218
+ </Dialog>
219
+ </>
220
+ );
221
+ }
@@ -0,0 +1,51 @@
1
+ import { Stack, Typography } from '@mui/material';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { tSafe } from '../../utils/format';
4
+
5
+ interface ScenarioBadgeProps {
6
+ livemode: boolean;
7
+ label?: string;
8
+ }
9
+
10
+ export default function ScenarioBadge({ livemode, label = undefined }: ScenarioBadgeProps) {
11
+ const { t } = useLocaleContext();
12
+ return (
13
+ <Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.5 }}>
14
+ <Typography
15
+ component="span"
16
+ sx={{
17
+ fontSize: 10,
18
+ fontWeight: 700,
19
+ letterSpacing: '0.1em',
20
+ lineHeight: 1,
21
+ textTransform: 'uppercase',
22
+ color: 'primary.main',
23
+ bgcolor: (theme) =>
24
+ theme.palette.mode === 'dark' ? `${theme.palette.primary.main}1A` : `${theme.palette.primary.main}0D`,
25
+ px: 1,
26
+ py: 0.5,
27
+ borderRadius: '4px',
28
+ }}>
29
+ {label || tSafe(t, 'payment.checkout.orderSummary', 'Order Summary')}
30
+ </Typography>
31
+ {!livemode && (
32
+ <Typography
33
+ component="span"
34
+ sx={{
35
+ fontSize: 10,
36
+ fontWeight: 700,
37
+ letterSpacing: '0.1em',
38
+ lineHeight: 1,
39
+ textTransform: 'uppercase',
40
+ color: (theme) => (theme.palette.mode === 'dark' ? theme.palette.grey[500] : theme.palette.grey[400]),
41
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : theme.palette.grey[100]),
42
+ px: 1,
43
+ py: 0.5,
44
+ borderRadius: '4px',
45
+ }}>
46
+ {t('common.livemode')}
47
+ </Typography>
48
+ )}
49
+ </Stack>
50
+ );
51
+ }
@@ -0,0 +1,112 @@
1
+ import { Box, Stack, Typography } from '@mui/material';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { tSafe } from '../../utils/format';
4
+
5
+ interface TotalDisplayProps {
6
+ label?: string;
7
+ total: string;
8
+ usdEquivalent?: string | null;
9
+ }
10
+
11
+ // Split a price string like "1,234.56 TBA" into parts for display
12
+ function splitPrice(total: string): { prefix: string; integer: string; decimal: string; suffix: string } {
13
+ if (!total) return { prefix: '', integer: '0', decimal: '', suffix: '' };
14
+ const trimmed = total.trim();
15
+
16
+ // Handle "$X.XX" format
17
+ if (trimmed.startsWith('$')) {
18
+ const numPart = trimmed.slice(1).trim();
19
+ const dotIdx = numPart.indexOf('.');
20
+ if (dotIdx >= 0) {
21
+ return {
22
+ prefix: '$',
23
+ integer: numPart.slice(0, dotIdx),
24
+ decimal: `.${numPart.slice(dotIdx + 1)}`,
25
+ suffix: '',
26
+ };
27
+ }
28
+ return { prefix: '$', integer: numPart, decimal: '', suffix: '' };
29
+ }
30
+
31
+ // Handle "123.45 TBA" or "0 TBA" format
32
+ const parts = trimmed.split(/\s+/);
33
+ const numStr = parts[0] || '0';
34
+ const unit = parts.slice(1).join(' ');
35
+ const dotIdx = numStr.indexOf('.');
36
+ if (dotIdx >= 0) {
37
+ return {
38
+ prefix: '',
39
+ integer: numStr.slice(0, dotIdx),
40
+ decimal: `.${numStr.slice(dotIdx + 1)}`,
41
+ suffix: unit,
42
+ };
43
+ }
44
+ return { prefix: '', integer: numStr, decimal: '', suffix: unit };
45
+ }
46
+
47
+ export default function TotalDisplay({ label = undefined, total, usdEquivalent = undefined }: TotalDisplayProps) {
48
+ const { t } = useLocaleContext();
49
+ const { prefix, integer, decimal, suffix } = splitPrice(total);
50
+
51
+ return (
52
+ <Box sx={{ pt: 4, borderTop: '1px solid', borderColor: 'divider' }}>
53
+ <Stack direction="row" justifyContent="space-between" alignItems="flex-end">
54
+ {/* Left: label + promo link */}
55
+ <Stack spacing={0.5} sx={{ pb: 1 }}>
56
+ <Typography
57
+ sx={{
58
+ fontSize: 12,
59
+ fontWeight: 700,
60
+ letterSpacing: '0.02em',
61
+ color: 'text.disabled',
62
+ }}>
63
+ {label || tSafe(t, 'payment.checkout.totalDueToday', 'Total due today')}
64
+ </Typography>
65
+ </Stack>
66
+
67
+ {/* Right: big price */}
68
+ <Box sx={{ display: 'flex', alignItems: 'baseline', lineHeight: 1 }}>
69
+ {/* Currency symbol / prefix */}
70
+ {prefix && (
71
+ <Typography component="span" sx={{ fontSize: 36, fontWeight: 500, color: 'text.primary', opacity: 0.4 }}>
72
+ {prefix}
73
+ </Typography>
74
+ )}
75
+ {/* Integer part — extra large */}
76
+ <Typography
77
+ component="span"
78
+ sx={{
79
+ fontSize: { xs: 54, md: 72 },
80
+ fontWeight: 800,
81
+ lineHeight: 1,
82
+ letterSpacing: '-0.04em',
83
+ color: 'text.primary',
84
+ }}>
85
+ {integer}
86
+ </Typography>
87
+ {/* Decimal part — lighter */}
88
+ {decimal && (
89
+ <Typography component="span" sx={{ fontSize: 36, fontWeight: 300, color: 'text.primary', opacity: 0.3 }}>
90
+ {decimal}
91
+ </Typography>
92
+ )}
93
+ {/* Currency unit */}
94
+ {suffix && (
95
+ <Typography
96
+ component="span"
97
+ sx={{ fontSize: 20, fontWeight: 700, color: 'text.disabled', ml: 1, textTransform: 'uppercase' }}>
98
+ {suffix}
99
+ </Typography>
100
+ )}
101
+ </Box>
102
+ </Stack>
103
+
104
+ {/* USD equivalent */}
105
+ {usdEquivalent && (
106
+ <Typography sx={{ fontSize: 12, color: 'text.disabled', textAlign: 'right', mt: 0.5, fontWeight: 500 }}>
107
+ ≈ {usdEquivalent}
108
+ </Typography>
109
+ )}
110
+ </Box>
111
+ );
112
+ }
@@ -0,0 +1,2 @@
1
+ export { default as CheckoutV2 } from './checkout-v2';
2
+ export type { CheckoutV2Props, CheckoutScenario } from './types';
@@ -0,0 +1,232 @@
1
+ import { Box } from '@mui/material';
2
+ import { alpha, useTheme } from '@mui/material/styles';
3
+ import Header from '@blocklet/ui-react/lib/Header';
4
+ import { useMobile } from '../../hooks/mobile';
5
+
6
+ const hiddenScrollbar = {
7
+ overflowY: 'auto',
8
+ '&::-webkit-scrollbar': { display: 'none' },
9
+ scrollbarWidth: 'none',
10
+ } as const;
11
+
12
+ // Mobile: whole container fades in
13
+ const mobileFadeIn = {
14
+ '@keyframes mobileFadeIn': {
15
+ from: { opacity: 0 },
16
+ to: { opacity: 1 },
17
+ },
18
+ animation: { xs: 'mobileFadeIn 0.4s ease both', md: 'none' },
19
+ } as const;
20
+
21
+ // Desktop: left content fades in with slight upward motion
22
+ const fadeIn = {
23
+ '@keyframes fadeIn': {
24
+ from: { opacity: 0, transform: 'translateY(12px)' },
25
+ to: { opacity: 1, transform: 'translateY(0)' },
26
+ },
27
+ animation: { xs: 'none', md: 'fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both' },
28
+ } as const;
29
+
30
+ // Desktop: right panel slides in from the right
31
+ const slideInFromRight = {
32
+ '@keyframes slideInRight': {
33
+ from: { transform: 'translateX(100%)' },
34
+ to: { transform: 'translateX(0)' },
35
+ },
36
+ animation: { xs: 'none', md: 'slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards' },
37
+ } as const;
38
+
39
+ interface CheckoutLayoutProps {
40
+ left: React.ReactNode;
41
+ right: React.ReactNode;
42
+ mode?: string;
43
+ }
44
+
45
+ export default function CheckoutLayout({ left, right, mode = 'inline' }: CheckoutLayoutProps) {
46
+ const isFullScreen = mode === 'standalone';
47
+ const { isMobile } = useMobile();
48
+ const theme = useTheme();
49
+ const isDark = theme.palette.mode === 'dark';
50
+
51
+ const hideLeft = left === null;
52
+
53
+ // Mobile fixed bottom bar for submit button
54
+ const mobileSubmitBarSx: Record<string, unknown> = {};
55
+ if (isMobile && !hideLeft) {
56
+ mobileSubmitBarSx['& .cko-v2-submit-btn'] = {
57
+ position: 'fixed',
58
+ bottom: 0,
59
+ left: 0,
60
+ right: 0,
61
+ zIndex: 999,
62
+ bgcolor: isDark ? 'rgba(30,30,30,0.82)' : 'background.paper',
63
+ backdropFilter: isDark ? 'blur(12px)' : 'none',
64
+ p: 1.5,
65
+ borderTop: '1px solid',
66
+ borderColor: 'divider',
67
+ boxShadow: isDark ? '0 -4px 20px rgba(0,0,0,0.4)' : '0 -2px 10px rgba(0,0,0,0.08)',
68
+ };
69
+ }
70
+
71
+ if (!isFullScreen) {
72
+ // Card (inline) mode
73
+ return (
74
+ <Box
75
+ sx={{
76
+ display: 'flex',
77
+ flexDirection: { xs: 'column', md: 'row' },
78
+ width: '100%',
79
+ minHeight: { md: 640 },
80
+ maxWidth: 1120,
81
+ mx: 'auto',
82
+ borderRadius: '16px',
83
+ overflow: 'hidden',
84
+ boxShadow: 1,
85
+ border: 1,
86
+ borderColor: 'divider',
87
+ borderLeft: '4px solid',
88
+ borderLeftColor: 'primary.main',
89
+ // Same bg as left panel so slide-in reveals white over gradient
90
+ bgcolor: (t) => (t.palette.mode === 'dark' ? 'background.default' : '#f8faff'),
91
+ ...mobileFadeIn,
92
+ }}>
93
+ {!hideLeft && (
94
+ <Box
95
+ sx={{
96
+ flex: 1,
97
+ bgcolor: (t) => (t.palette.mode === 'dark' ? 'background.default' : '#f8faff'),
98
+ p: { xs: 3, md: 5 },
99
+ pt: { xs: 3, md: 4 },
100
+ display: 'flex',
101
+ flexDirection: 'column',
102
+ ...fadeIn,
103
+ }}>
104
+ {left}
105
+ </Box>
106
+ )}
107
+ <Box
108
+ sx={{
109
+ width: hideLeft ? '100%' : { xs: '100%', md: 480 },
110
+ flexShrink: 0,
111
+ bgcolor: 'background.paper',
112
+ p: { xs: 3, md: 4 },
113
+ pt: { xs: 3, md: 4 },
114
+ display: 'flex',
115
+ flexDirection: 'column',
116
+ overflow: { md: 'hidden' },
117
+ ...(hideLeft ? {} : slideInFromRight),
118
+ }}>
119
+ {right}
120
+ </Box>
121
+ </Box>
122
+ );
123
+ }
124
+
125
+ // Standalone (full-screen) mode — backgrounds fill edge-to-edge, content constrained
126
+ // Mobile: gray page bg with white card sections; Desktop: gradient bg with slide-in
127
+ const mobileBg = isDark ? theme.palette.background.default : theme.palette.grey[100];
128
+ const desktopBg = isDark ? theme.palette.background.default : '#f8faff';
129
+
130
+ return (
131
+ <Box
132
+ sx={{
133
+ width: '100%',
134
+ height: { xs: 'auto', md: '100vh' },
135
+ minHeight: { xs: '100vh' },
136
+ overflow: { md: 'hidden' },
137
+ display: 'flex',
138
+ flexDirection: { xs: 'column', md: 'row' },
139
+ position: 'relative',
140
+ // Mobile: solid gray bg; Desktop: gradient over light blue
141
+ background: isMobile
142
+ ? mobileBg
143
+ : `linear-gradient(160deg, ${alpha(theme.palette.primary.main, 0.03)} 0%, ${alpha(theme.palette.primary.main, 0.07)} 50%, ${alpha(theme.palette.primary.main, 0.04)} 100%)`,
144
+ bgcolor: desktopBg,
145
+ ...mobileFadeIn,
146
+ }}>
147
+ {/* Header — absolute positioned at top, transparent background, shows logo */}
148
+ <Header
149
+ sx={{
150
+ position: 'absolute',
151
+ top: 20,
152
+ left: 0,
153
+ right: 0,
154
+ zIndex: 10,
155
+ background: 'transparent',
156
+ '& .header-container': { height: 'auto' },
157
+ }}
158
+ hideNavMenu
159
+ brand={null}
160
+ description={null}
161
+ addons={(buildIns: any) => {
162
+ const addons = buildIns.filter((addon: any) =>
163
+ ['locale-selector', 'theme-mode-toggle', 'session-user'].includes(addon.key)
164
+ );
165
+ return addons;
166
+ }}
167
+ />
168
+
169
+ {/* Left panel — hidden when left is null (e.g. mobile success) */}
170
+ {!hideLeft && (
171
+ <Box
172
+ sx={{
173
+ width: { xs: '100%', md: '50%' },
174
+ height: { xs: 'auto', md: '100vh' },
175
+ display: 'flex',
176
+ justifyContent: { md: 'center' },
177
+ ...(isMobile ? { bgcolor: 'background.paper', mt: 9 } : {}),
178
+ ...hiddenScrollbar,
179
+ }}>
180
+ <Box
181
+ sx={{
182
+ width: '100%',
183
+ maxWidth: { md: 640 },
184
+ p: { xs: 3, md: 5 },
185
+ pt: { xs: 3, md: 10 },
186
+ display: 'flex',
187
+ flexDirection: 'column',
188
+ flex: 1,
189
+ ...fadeIn,
190
+ }}>
191
+ {left}
192
+ </Box>
193
+ </Box>
194
+ )}
195
+
196
+ {/* Right panel — full width when left is hidden */}
197
+ <Box
198
+ sx={{
199
+ width: hideLeft ? '100%' : { xs: '100%', md: '50%' },
200
+ height: hideLeft ? '100vh' : { xs: 'auto', md: '100vh' },
201
+ bgcolor: 'background.paper',
202
+ boxShadow: hideLeft ? 'none' : { md: '-4px 0 16px rgba(0,0,0,0.04)' },
203
+ position: 'relative',
204
+ display: 'flex',
205
+ flexDirection: 'column',
206
+ ...(hideLeft ? {} : slideInFromRight),
207
+ ...(isMobile && !hideLeft ? { pb: '180px', mt: 2 } : {}),
208
+ // Mobile: scroll entire panel; Desktop: let PaymentPanel handle internal scroll
209
+ overflowY: { xs: 'auto', md: 'hidden' },
210
+ '&::-webkit-scrollbar': { display: 'none' },
211
+ scrollbarWidth: 'none',
212
+ ...mobileSubmitBarSx,
213
+ }}>
214
+ <Box
215
+ sx={{
216
+ width: '100%',
217
+ maxWidth: { md: 600 },
218
+ mx: 'auto',
219
+ p: { xs: 3, md: 4 },
220
+ pt: hideLeft ? { xs: 10, md: '10vh' } : { xs: 3, md: '10vh' },
221
+ display: 'flex',
222
+ flexDirection: 'column',
223
+ flex: 1,
224
+ minHeight: 0,
225
+ justifyContent: hideLeft ? 'center' : undefined,
226
+ }}>
227
+ {right}
228
+ </Box>
229
+ </Box>
230
+ </Box>
231
+ );
232
+ }