@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,207 @@
1
+ import { useState } from 'react';
2
+ import AddIcon from '@mui/icons-material/Add';
3
+ import CloseIcon from '@mui/icons-material/Close';
4
+ import LocalOfferIcon from '@mui/icons-material/LocalOffer';
5
+ import {
6
+ Alert,
7
+ Box,
8
+ Button,
9
+ CircularProgress,
10
+ IconButton,
11
+ InputAdornment,
12
+ Stack,
13
+ TextField,
14
+ Typography,
15
+ } from '@mui/material';
16
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
17
+
18
+ interface PromotionInputProps {
19
+ promotion: {
20
+ applied: boolean;
21
+ code: string | null;
22
+ active: boolean;
23
+ inactiveReason: string | null;
24
+ apply: (code: string) => Promise<{ success: boolean; error?: string }>;
25
+ remove: () => Promise<void>;
26
+ };
27
+ discounts: any[];
28
+ discountAmount: string | null;
29
+ /** Start with input field visible (skip the "Add promotion code" button) */
30
+ initialShowInput?: boolean;
31
+ }
32
+
33
+ export default function PromotionInput({
34
+ promotion,
35
+ discounts,
36
+ discountAmount,
37
+ initialShowInput = false,
38
+ }: PromotionInputProps) {
39
+ const { t } = useLocaleContext();
40
+ const [showInput, setShowInput] = useState(false);
41
+ const [code, setCode] = useState('');
42
+ const [applying, setApplying] = useState(false);
43
+ const [error, setError] = useState('');
44
+
45
+ // When initialShowInput is true, always show the input (e.g. inside a drawer)
46
+ const effectiveShowInput = initialShowInput || showInput;
47
+
48
+ const handleApply = async () => {
49
+ if (!code.trim()) return;
50
+ setApplying(true);
51
+ setError('');
52
+ const result = await promotion.apply(code.trim());
53
+ if (!result.success) {
54
+ setError(result.error || 'Invalid code');
55
+ } else {
56
+ setCode('');
57
+ setShowInput(false);
58
+ }
59
+ setApplying(false);
60
+ };
61
+
62
+ const handleKeyPress = (event: React.KeyboardEvent) => {
63
+ if (event.key === 'Enter' && !applying && code.trim()) {
64
+ handleApply();
65
+ }
66
+ };
67
+
68
+ // Display applied discounts
69
+ if (discounts?.length > 0) {
70
+ return (
71
+ <Box>
72
+ {discounts.map((disc: any, i: number) => {
73
+ const discCode =
74
+ disc.promotion_code_details?.code || disc.verification_data?.code || disc.promotion_code || '';
75
+ const coupon = disc.coupon_details || {};
76
+ const couponOff =
77
+ coupon.percent_off > 0
78
+ ? t('payment.checkout.coupon.percentage', { percent: coupon.percent_off })
79
+ : `${coupon.percent_off || 0}%`;
80
+ let description = '';
81
+ if (coupon.duration === 'repeating' && coupon.duration_in_months) {
82
+ const months = coupon.duration_in_months;
83
+ description = `${couponOff} for ${months} month${months > 1 ? 's' : ''}`;
84
+ } else if (coupon.duration === 'forever') {
85
+ description = t('payment.checkout.coupon.terms.forever', { couponOff });
86
+ } else if (coupon.duration === 'once') {
87
+ description = t('payment.checkout.coupon.terms.once', { couponOff });
88
+ }
89
+ return (
90
+ <Stack
91
+ key={disc.promotion_code || disc.coupon || i}
92
+ direction="row"
93
+ justifyContent="space-between"
94
+ alignItems="center">
95
+ <Stack
96
+ direction="row"
97
+ alignItems="center"
98
+ spacing={0.5}
99
+ sx={{
100
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(18,184,134,0.1)' : '#ebfef5'),
101
+ px: 1.5,
102
+ py: 0.5,
103
+ borderRadius: '8px',
104
+ border: '1px solid',
105
+ borderColor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(18,184,134,0.2)' : '#d3f9e8'),
106
+ }}>
107
+ <LocalOfferIcon sx={{ color: '#12b886', fontSize: 14 }} />
108
+ <Typography sx={{ fontWeight: 700, fontSize: 13, color: '#12b886' }}>{discCode}</Typography>
109
+ {description && (
110
+ <Typography sx={{ fontSize: 12, color: '#12b886', fontWeight: 500, opacity: 0.8 }}>
111
+ · {description}
112
+ </Typography>
113
+ )}
114
+ <IconButton size="small" onClick={promotion.remove} sx={{ width: 18, height: 18, ml: 0.25 }}>
115
+ <CloseIcon sx={{ fontSize: 12, color: '#12b886' }} />
116
+ </IconButton>
117
+ </Stack>
118
+ <Typography sx={{ color: 'text.primary', fontWeight: 600, fontSize: 14 }}>
119
+ -{discountAmount || '0'}
120
+ </Typography>
121
+ </Stack>
122
+ );
123
+ })}
124
+ </Box>
125
+ );
126
+ }
127
+
128
+ // Add promo code — consistent height: button and input share same container height
129
+ if (!promotion.active) return null;
130
+
131
+ return (
132
+ <Box sx={{ minHeight: 36 }}>
133
+ {effectiveShowInput ? (
134
+ <Box
135
+ onBlur={(e) => {
136
+ // Don't collapse if initialShowInput is forced (e.g. inside a drawer)
137
+ if (initialShowInput) return;
138
+ if (!e.currentTarget.contains(e.relatedTarget as Node) && !code.trim()) {
139
+ setShowInput(false);
140
+ }
141
+ }}>
142
+ <TextField
143
+ fullWidth
144
+ size="small"
145
+ value={code}
146
+ onChange={(e) => setCode(e.target.value)}
147
+ onKeyPress={handleKeyPress}
148
+ placeholder={t('payment.checkout.promotion.placeholder')}
149
+ disabled={applying}
150
+ autoFocus
151
+ slotProps={{
152
+ input: {
153
+ endAdornment: (
154
+ <InputAdornment position="end">
155
+ <Button
156
+ size="small"
157
+ onClick={handleApply}
158
+ disabled={!code.trim() || applying}
159
+ variant="text"
160
+ sx={{
161
+ color: 'primary.main',
162
+ fontSize: 13,
163
+ textTransform: 'none',
164
+ minWidth: 'auto',
165
+ fontWeight: 600,
166
+ }}>
167
+ {applying ? <CircularProgress size={16} /> : t('payment.checkout.promotion.apply')}
168
+ </Button>
169
+ </InputAdornment>
170
+ ),
171
+ },
172
+ }}
173
+ sx={{
174
+ '& .MuiOutlinedInput-root': { pr: 1, borderRadius: '8px', height: 36 },
175
+ '& .MuiOutlinedInput-input': { py: '6px', fontSize: 13 },
176
+ }}
177
+ />
178
+ {error && (
179
+ <Alert severity="error" sx={{ mt: 0.5, py: 0, fontSize: 12, borderRadius: '6px' }}>
180
+ {error}
181
+ </Alert>
182
+ )}
183
+ </Box>
184
+ ) : (
185
+ <Button
186
+ onClick={() => setShowInput(true)}
187
+ startIcon={<AddIcon sx={{ fontSize: 18 }} />}
188
+ variant="text"
189
+ sx={{
190
+ fontWeight: 600,
191
+ fontSize: 13,
192
+ textTransform: 'none',
193
+ justifyContent: 'flex-start',
194
+ p: 0,
195
+ height: 36,
196
+ color: 'primary.main',
197
+ '&:hover': {
198
+ backgroundColor: 'transparent',
199
+ textDecoration: 'underline',
200
+ },
201
+ }}>
202
+ {t('payment.checkout.promotion.add_code')}
203
+ </Button>
204
+ )}
205
+ </Box>
206
+ );
207
+ }
@@ -0,0 +1,57 @@
1
+ import HelpOutline from '@mui/icons-material/HelpOutline';
2
+ import { Stack, Tooltip, Typography } from '@mui/material';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { formatTrialText, whiteTooltipSx } from '../../utils/format';
5
+
6
+ interface StakingBreakdownProps {
7
+ staking: string;
8
+ paymentAmount: string;
9
+ trialActive: boolean;
10
+ trialDays: number;
11
+ afterTrialInterval: string | null;
12
+ }
13
+
14
+ export default function StakingBreakdown({
15
+ staking,
16
+ paymentAmount,
17
+ trialActive,
18
+ trialDays,
19
+ afterTrialInterval,
20
+ }: StakingBreakdownProps) {
21
+ const { t } = useLocaleContext();
22
+
23
+ return (
24
+ <Stack spacing={1} sx={{ mb: 1 }}>
25
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
26
+ <Stack direction="row" spacing={0.5} alignItems="center">
27
+ <Typography sx={{ color: 'text.secondary', fontSize: 14 }}>
28
+ {t('payment.checkout.paymentRequired')}
29
+ </Typography>
30
+ <Tooltip
31
+ title={t('payment.checkout.stakingConfirm')}
32
+ placement="top"
33
+ arrow
34
+ slotProps={{ popper: { sx: whiteTooltipSx } }}>
35
+ <HelpOutline sx={{ fontSize: 16, color: 'text.disabled' }} />
36
+ </Tooltip>
37
+ </Stack>
38
+ <Typography>
39
+ {trialActive ? formatTrialText(t, trialDays, afterTrialInterval || 'day') : paymentAmount}
40
+ </Typography>
41
+ </Stack>
42
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
43
+ <Stack direction="row" spacing={0.5} alignItems="center">
44
+ <Typography sx={{ color: 'text.secondary', fontSize: 14 }}>{t('payment.checkout.staking.title')}</Typography>
45
+ <Tooltip
46
+ title={t('payment.checkout.staking.tooltip')}
47
+ placement="top"
48
+ arrow
49
+ slotProps={{ popper: { sx: whiteTooltipSx } }}>
50
+ <HelpOutline sx={{ fontSize: 16, color: 'text.disabled' }} />
51
+ </Tooltip>
52
+ </Stack>
53
+ <Typography>{staking}</Typography>
54
+ </Stack>
55
+ </Stack>
56
+ );
57
+ }
@@ -0,0 +1,63 @@
1
+ import { Stack, Typography } from '@mui/material';
2
+ import type { TLineItemExpanded } from '@blocklet/payment-types';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { INTERVAL_LOCALE_KEY } from '../../utils/format';
5
+
6
+ interface TrialInfoProps {
7
+ trial: {
8
+ active: boolean;
9
+ days: number;
10
+ afterTrialPrice: string | null;
11
+ afterTrialInterval: string | null;
12
+ };
13
+ mode: string;
14
+ items: TLineItemExpanded[];
15
+ }
16
+
17
+ export default function TrialInfo({ trial, mode, items }: TrialInfoProps) {
18
+ const { t } = useLocaleContext();
19
+
20
+ // Trial: "Then X/month after trial"
21
+ if (trial.active && trial.afterTrialPrice) {
22
+ return (
23
+ <Stack
24
+ direction="row"
25
+ justifyContent="space-between"
26
+ alignItems="center"
27
+ sx={{ borderTop: '1px solid', borderColor: 'divider', pt: 1, mt: 1 }}>
28
+ <Typography sx={{ color: 'text.secondary', fontSize: 14 }}>{t('common.nextCharge')}</Typography>
29
+ <Typography sx={{ fontSize: 16, color: 'text.secondary' }}>
30
+ {trial.afterTrialPrice}
31
+ {trial.afterTrialInterval ? ` ${t(INTERVAL_LOCALE_KEY[trial.afterTrialInterval] || '')}` : ''}
32
+ </Typography>
33
+ </Stack>
34
+ );
35
+ }
36
+
37
+ // Metered next charge (non-trial)
38
+ if (!trial.active && ['subscription', 'setup'].includes(mode)) {
39
+ const meteredItem = items.find(
40
+ (item: any) => ((item as any).upsell_price || item.price)?.recurring?.usage_type === 'metered'
41
+ );
42
+ if (!meteredItem) return null;
43
+ const meteredInterval = ((meteredItem as any).upsell_price || meteredItem.price)?.recurring?.interval;
44
+ if (!meteredInterval) return null;
45
+ const recurringText = t('common.per', { interval: t(`common.${meteredInterval}`) });
46
+ return (
47
+ <Stack
48
+ direction="row"
49
+ justifyContent="space-between"
50
+ alignItems="center"
51
+ sx={{ borderTop: '1px solid', borderColor: 'divider', pt: 1, mt: 1 }}>
52
+ <Typography sx={{ color: 'text.secondary', fontSize: 14, fontWeight: 600 }}>
53
+ {t('common.nextCharge')}
54
+ </Typography>
55
+ <Typography sx={{ fontSize: 16, color: 'text.secondary' }}>
56
+ {t('payment.checkout.metered', { recurring: recurringText })}
57
+ </Typography>
58
+ </Stack>
59
+ );
60
+ }
61
+
62
+ return null;
63
+ }
@@ -0,0 +1,59 @@
1
+ import { Avatar, Card, Radio, Stack, Typography } from '@mui/material';
2
+ import { styled } from '@mui/material/styles';
3
+ import type { TPaymentCurrency } from '@blocklet/payment-types';
4
+
5
+ const CurrencyRoot = styled('section')`
6
+ display: grid;
7
+ width: 100%;
8
+ gap: 12px;
9
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
10
+
11
+ .cko-payment-card {
12
+ position: relative;
13
+ border: 1px solid ${({ theme }) => theme.palette.primary.main};
14
+ padding: 4px 8px;
15
+ cursor: pointer;
16
+ background: ${({ theme }) => theme.palette.grey[50]};
17
+ }
18
+
19
+ .cko-payment-card-unselect {
20
+ border: 1px solid ${({ theme }) => theme.palette.divider};
21
+ padding: 4px 8px;
22
+ cursor: pointer;
23
+ background: ${({ theme }) => theme.palette.grey[50]};
24
+ }
25
+ `;
26
+
27
+ interface CurrencyGridProps {
28
+ currencies: TPaymentCurrency[];
29
+ selectedId: string | undefined;
30
+ onSelect: (id: string) => Promise<void>;
31
+ }
32
+
33
+ export default function CurrencyGrid({ currencies, selectedId, onSelect }: CurrencyGridProps) {
34
+ if (!currencies?.length) return null;
35
+ return (
36
+ <CurrencyRoot style={{ display: currencies.length > 1 ? 'grid' : 'block' }}>
37
+ {currencies.map((cur) => {
38
+ const selected = cur.id === selectedId;
39
+ const methodName = (cur as any).method?.name || cur.name || '';
40
+ return (
41
+ <Card
42
+ key={cur.id}
43
+ variant="outlined"
44
+ onClick={() => onSelect(cur.id)}
45
+ className={selected ? 'cko-payment-card' : 'cko-payment-card-unselect'}>
46
+ <Stack direction="row" sx={{ alignItems: 'center', position: 'relative' }}>
47
+ <Avatar src={cur.logo} alt={cur.name} sx={{ width: 40, height: 40, mr: '12px' }} />
48
+ <div>
49
+ <Typography sx={{ fontSize: 16, color: 'text.primary', fontWeight: 500 }}>{cur.symbol}</Typography>
50
+ <Typography sx={{ color: 'text.secondary', fontSize: 14 }}>{methodName}</Typography>
51
+ </div>
52
+ <Radio checked={selected} sx={{ position: 'absolute', right: 0 }} />
53
+ </Stack>
54
+ </Card>
55
+ );
56
+ })}
57
+ </CurrencyRoot>
58
+ );
59
+ }
@@ -0,0 +1,214 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { Box, Button, InputBase, InputAdornment, Stack, Typography } from '@mui/material';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import CountrySelect from '../../../components/country-select';
5
+ import PhoneField from '../../../components/phone-field';
6
+ import { countryCodeToFlag } from '../../utils/format';
7
+
8
+ interface FieldConfig {
9
+ name: string;
10
+ type: string;
11
+ required: boolean;
12
+ }
13
+
14
+ interface CustomerInfoCardProps {
15
+ form: {
16
+ fields: FieldConfig[];
17
+ values: Record<string, any>;
18
+ onChange: (field: string, value: string | boolean | Record<string, string>) => void;
19
+ errors: Partial<Record<string, string>>;
20
+ validateField: (field: string) => Promise<void>;
21
+ };
22
+ isLoggedIn: boolean;
23
+ }
24
+
25
+ const fieldLabelMap = (t: (key: string) => string) =>
26
+ ({
27
+ customer_name: t('payment.checkout.customer.name'),
28
+ customer_email: t('payment.checkout.customer.email'),
29
+ customer_phone: t('payment.checkout.customer.phone'),
30
+ 'billing_address.country': t('payment.checkout.billing.country'),
31
+ 'billing_address.state': t('payment.checkout.billing.state'),
32
+ 'billing_address.city': t('payment.checkout.billing.city'),
33
+ 'billing_address.line1': t('payment.checkout.billing.line1'),
34
+ 'billing_address.line2': t('payment.checkout.billing.line2'),
35
+ 'billing_address.postal_code': t('payment.checkout.billing.postal_code'),
36
+ }) as Record<string, string>;
37
+
38
+ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardProps) {
39
+ const { t } = useLocaleContext();
40
+ const labels = fieldLabelMap(t);
41
+
42
+ // Default to confirmed view if required fields have values
43
+ const hasRequiredData = !!(form.values.customer_name && form.values.customer_email);
44
+ const [showEditForm, setShowEditForm] = useState(!hasRequiredData);
45
+ const autoConfirmedRef = useRef(false);
46
+
47
+ // When data arrives (e.g. prefetch), auto-switch to confirmed if valid
48
+ useEffect(() => {
49
+ if (!autoConfirmedRef.current && form.values.customer_name && form.values.customer_email) {
50
+ autoConfirmedRef.current = true;
51
+ setShowEditForm(false);
52
+ }
53
+ }, [form.values.customer_name, form.values.customer_email]);
54
+
55
+ if (!isLoggedIn) return null;
56
+
57
+ // Summary view
58
+ if (!showEditForm) {
59
+ return (
60
+ <Box sx={{ mt: 2 }}>
61
+ {/* Header — outside the card */}
62
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
63
+ <Typography sx={{ fontSize: 13, fontWeight: 600, color: 'text.secondary' }}>
64
+ {t('payment.checkout.customerInfo')}
65
+ </Typography>
66
+ <Button
67
+ size="small"
68
+ variant="text"
69
+ onClick={() => setShowEditForm(true)}
70
+ sx={{ minWidth: 0, fontSize: 13, fontWeight: 600, p: 0 }}>
71
+ {t('common.edit')}
72
+ </Button>
73
+ </Stack>
74
+ {/* Card body */}
75
+ <Stack
76
+ spacing={0.5}
77
+ sx={{
78
+ p: 2,
79
+ backgroundColor: 'background.paper',
80
+ borderRadius: 1,
81
+ border: '1px solid',
82
+ borderColor: 'divider',
83
+ }}>
84
+ {form.values.customer_name && (
85
+ <Typography variant="body2" sx={{ color: 'text.primary', fontWeight: 600, fontSize: '0.9375rem' }}>
86
+ {form.values.customer_name}
87
+ </Typography>
88
+ )}
89
+ {form.values.customer_email && (
90
+ <Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.8125rem' }}>
91
+ {form.values.customer_email}
92
+ </Typography>
93
+ )}
94
+ {form.fields.some((f) => f.name === 'customer_phone') && form.values.customer_phone && (
95
+ <Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.8125rem' }}>
96
+ {form.values.customer_phone}
97
+ </Typography>
98
+ )}
99
+ {(form.values.billing_address?.country ||
100
+ form.values.billing_address?.state ||
101
+ form.values.billing_address?.postal_code) && (
102
+ <Stack direction="row" alignItems="center" spacing={0.75}>
103
+ {form.values.billing_address?.country && (
104
+ <Box component="span" sx={{ fontSize: 14, lineHeight: 1 }}>
105
+ {countryCodeToFlag(form.values.billing_address.country)}
106
+ </Box>
107
+ )}
108
+ <Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.8125rem' }}>
109
+ {form.values.billing_address?.state || ''}
110
+ {form.values.billing_address?.postal_code
111
+ ? ` [ ${t('payment.checkout.billing.postal_code')}: ${form.values.billing_address.postal_code} ]`
112
+ : ''}
113
+ </Typography>
114
+ </Stack>
115
+ )}
116
+ </Stack>
117
+ </Box>
118
+ );
119
+ }
120
+
121
+ // Edit form — v1 style: bold label above, grey background input
122
+ return (
123
+ <Box sx={{ mt: 2 }}>
124
+ {/* Header — outside the card */}
125
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
126
+ <Typography sx={{ fontSize: 13, fontWeight: 600, color: 'text.secondary' }}>
127
+ {t('payment.checkout.customerInfo')}
128
+ </Typography>
129
+ <Button
130
+ size="small"
131
+ variant="text"
132
+ onClick={() => setShowEditForm(false)}
133
+ sx={{ minWidth: 0, fontSize: 13, fontWeight: 600, p: 0 }}>
134
+ {t('common.confirm')}
135
+ </Button>
136
+ </Stack>
137
+ {/* Card body */}
138
+ <Stack
139
+ spacing={0}
140
+ sx={{
141
+ p: 2,
142
+ backgroundColor: 'background.paper',
143
+ borderRadius: 1,
144
+ border: '1px solid',
145
+ borderColor: 'divider',
146
+ }}>
147
+ {form.fields
148
+ .filter((f) => f.name !== 'billing_address.country')
149
+ .map((field) => {
150
+ const { name } = field;
151
+ const label = labels[name] || name;
152
+ const value = name.includes('.')
153
+ ? name.split('.').reduce((o: any, k) => o?.[k], form.values)
154
+ : form.values[name];
155
+ const isPostalCode = name === 'billing_address.postal_code';
156
+ const isPhone = name === 'customer_phone';
157
+
158
+ // Phone field — with country flag + dial code selector
159
+ if (isPhone) {
160
+ return (
161
+ <PhoneField
162
+ key={name}
163
+ value={value || ''}
164
+ country={form.values.billing_address?.country || ''}
165
+ onChange={(phone) => form.onChange('customer_phone', phone)}
166
+ onCountryChange={(c) => form.onChange('billing_address.country', c)}
167
+ onBlur={() => form.validateField(name)}
168
+ label={label}
169
+ error={form.errors[name]}
170
+ />
171
+ );
172
+ }
173
+
174
+ return (
175
+ <Box key={name} sx={{ mb: 1.5 }}>
176
+ <Typography sx={{ fontSize: 13, fontWeight: 600, color: 'text.primary', mb: 0.5 }}>{label}</Typography>
177
+ <InputBase
178
+ fullWidth
179
+ value={value || ''}
180
+ onChange={(e) => form.onChange(name, e.target.value)}
181
+ onBlur={() => form.validateField(name)}
182
+ startAdornment={
183
+ isPostalCode ? (
184
+ <InputAdornment position="start" sx={{ mr: 0.5, ml: -0.5 }}>
185
+ <CountrySelect
186
+ value={form.values.billing_address?.country || ''}
187
+ onChange={(v) => form.onChange('billing_address.country', v)}
188
+ sx={{
189
+ '.MuiOutlinedInput-notchedOutline': { borderColor: 'transparent !important' },
190
+ '& .MuiSelect-select': { py: 0, pr: '20px !important' },
191
+ }}
192
+ />
193
+ </InputAdornment>
194
+ ) : undefined
195
+ }
196
+ sx={{
197
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.50'),
198
+ borderRadius: '8px',
199
+ px: 1.5,
200
+ py: 0.75,
201
+ fontSize: 14,
202
+ '& .MuiInputBase-input': { p: 0 },
203
+ }}
204
+ />
205
+ {form.errors[name] && (
206
+ <Typography sx={{ fontSize: 12, color: 'error.main', mt: 0.25 }}>{form.errors[name]}</Typography>
207
+ )}
208
+ </Box>
209
+ );
210
+ })}
211
+ </Stack>
212
+ </Box>
213
+ );
214
+ }
@@ -0,0 +1,35 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+
5
+ interface StatusFeedbackProps {
6
+ status: string;
7
+ context: any;
8
+ onReset: () => void;
9
+ }
10
+
11
+ export default function StatusFeedback({ status, context, onReset }: StatusFeedbackProps) {
12
+ const { t } = useLocaleContext();
13
+ const prevStatusRef = useRef(status);
14
+
15
+ // Show toast for generic error states, then reset to idle
16
+ // Special codes (CUSTOMER_LIMITED, PRICE_CHANGED, etc.) are handled by parent
17
+ useEffect(() => {
18
+ if (status === prevStatusRef.current) return;
19
+ prevStatusRef.current = status;
20
+
21
+ if (status === 'failed' && context?.type === 'error') {
22
+ // CUSTOMER_LIMITED is handled by parent (overdue invoice dialog)
23
+ if (context.code === 'CUSTOMER_LIMITED') return;
24
+
25
+ Toast.error(context.message || 'Payment failed');
26
+ onReset();
27
+ }
28
+
29
+ // credit_insufficient is handled by CheckoutDialogs (ConfirmDialog), not Toast
30
+ }, [status, context, t, onReset]);
31
+
32
+ // Stripe dialog and DID Connect are handled by CheckoutDialogs and payment-panel respectively
33
+ // No inline Alerts — matches V1 behavior (Toast for errors, dialogs for interaction)
34
+ return null;
35
+ }
@@ -0,0 +1,37 @@
1
+ import { Button, CircularProgress } from '@mui/material';
2
+
3
+ interface SubmitButtonProps {
4
+ canSubmit: boolean;
5
+ isProcessing: boolean;
6
+ isWaitingStripe: boolean;
7
+ label: string;
8
+ processingLabel: string;
9
+ onSubmit: () => Promise<void>;
10
+ }
11
+
12
+ export default function SubmitButton({
13
+ canSubmit,
14
+ isProcessing,
15
+ isWaitingStripe,
16
+ label,
17
+ processingLabel,
18
+ onSubmit,
19
+ }: SubmitButtonProps) {
20
+ return (
21
+ <Button
22
+ variant="contained"
23
+ size="large"
24
+ fullWidth
25
+ disabled={!canSubmit || isWaitingStripe}
26
+ onClick={onSubmit}
27
+ startIcon={isProcessing ? <CircularProgress size={20} color="inherit" /> : null}
28
+ sx={{
29
+ py: 1.5,
30
+ fontSize: '1.3rem',
31
+ fontWeight: 600,
32
+ textTransform: 'none',
33
+ }}>
34
+ {isProcessing ? processingLabel : label}
35
+ </Button>
36
+ );
37
+ }