@blocklet/payment-react 1.25.10 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/checkout-v2/checkout-v2.d.ts +2 -0
- package/es/checkout-v2/checkout-v2.js +121 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
- package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/es/checkout-v2/components/left/billing-toggle.js +118 -0
- package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
- package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/es/checkout-v2/components/left/product-item-card.js +571 -0
- package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/es/checkout-v2/components/left/promotion-input.js +178 -0
- package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
- package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/es/checkout-v2/components/left/trial-info.js +48 -0
- package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/es/checkout-v2/components/right/currency-grid.js +48 -0
- package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/es/checkout-v2/components/right/customer-info-card.js +156 -0
- package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/es/checkout-v2/components/right/status-feedback.js +17 -0
- package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/es/checkout-v2/components/right/submit-button.js +29 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
- package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
- package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/es/checkout-v2/components/shared/total-display.js +84 -0
- package/es/checkout-v2/index.d.ts +2 -0
- package/es/checkout-v2/index.js +1 -0
- package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/es/checkout-v2/layouts/checkout-layout.js +226 -0
- package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/composite-panel.js +423 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.js +611 -0
- package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/es/checkout-v2/panels/left/scenario-router.js +19 -0
- package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/es/checkout-v2/panels/right/payment-panel.js +644 -0
- package/es/checkout-v2/types.d.ts +15 -0
- package/es/checkout-v2/types.js +0 -0
- package/es/checkout-v2/utils/format.d.ts +59 -0
- package/es/checkout-v2/utils/format.js +125 -0
- package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/es/checkout-v2/utils/scenario-detector.js +17 -0
- package/es/checkout-v2/views/error-view.d.ts +7 -0
- package/es/checkout-v2/views/error-view.js +269 -0
- package/es/checkout-v2/views/loading-view.d.ts +5 -0
- package/es/checkout-v2/views/loading-view.js +158 -0
- package/es/checkout-v2/views/success-view.d.ts +29 -0
- package/es/checkout-v2/views/success-view.js +614 -0
- package/es/components/phone-field.d.ts +14 -0
- package/es/components/phone-field.js +96 -0
- package/es/index.d.ts +3 -1
- package/es/index.js +3 -1
- package/es/locales/en.js +45 -6
- package/es/locales/zh.js +45 -6
- package/es/payment/form/index.js +10 -1
- package/lib/checkout-v2/checkout-v2.d.ts +2 -0
- package/lib/checkout-v2/checkout-v2.js +151 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
- package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
- package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
- package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/lib/checkout-v2/components/left/product-item-card.js +738 -0
- package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/lib/checkout-v2/components/left/promotion-input.js +220 -0
- package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
- package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/lib/checkout-v2/components/left/trial-info.js +82 -0
- package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/lib/checkout-v2/components/right/currency-grid.js +96 -0
- package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
- package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/lib/checkout-v2/components/right/status-feedback.js +30 -0
- package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/lib/checkout-v2/components/right/submit-button.js +35 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
- package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
- package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/lib/checkout-v2/components/shared/total-display.js +154 -0
- package/lib/checkout-v2/index.d.ts +2 -0
- package/lib/checkout-v2/index.js +13 -0
- package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
- package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.js +795 -0
- package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
- package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
- package/lib/checkout-v2/types.d.ts +15 -0
- package/lib/checkout-v2/types.js +1 -0
- package/lib/checkout-v2/utils/format.d.ts +59 -0
- package/lib/checkout-v2/utils/format.js +158 -0
- package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/lib/checkout-v2/utils/scenario-detector.js +23 -0
- package/lib/checkout-v2/views/error-view.d.ts +7 -0
- package/lib/checkout-v2/views/error-view.js +321 -0
- package/lib/checkout-v2/views/loading-view.d.ts +5 -0
- package/lib/checkout-v2/views/loading-view.js +168 -0
- package/lib/checkout-v2/views/success-view.d.ts +29 -0
- package/lib/checkout-v2/views/success-view.js +735 -0
- package/lib/components/phone-field.d.ts +14 -0
- package/lib/components/phone-field.js +130 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.js +8 -0
- package/lib/locales/en.js +45 -6
- package/lib/locales/zh.js +45 -6
- package/lib/payment/form/index.js +10 -1
- package/package.json +4 -3
- package/src/checkout-v2/checkout-v2.tsx +155 -0
- package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
- package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
- package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
- package/src/checkout-v2/components/left/product-item-card.tsx +642 -0
- package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
- package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
- package/src/checkout-v2/components/left/trial-info.tsx +63 -0
- package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
- package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
- package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
- package/src/checkout-v2/components/right/submit-button.tsx +37 -0
- package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
- package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
- package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
- package/src/checkout-v2/components/shared/total-display.tsx +112 -0
- package/src/checkout-v2/index.ts +2 -0
- package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
- package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
- package/src/checkout-v2/panels/left/credit-topup-panel.tsx +677 -0
- package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
- package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
- package/src/checkout-v2/types.ts +18 -0
- package/src/checkout-v2/utils/format.ts +205 -0
- package/src/checkout-v2/utils/scenario-detector.ts +30 -0
- package/src/checkout-v2/views/error-view.tsx +293 -0
- package/src/checkout-v2/views/loading-view.tsx +162 -0
- package/src/checkout-v2/views/success-view.tsx +770 -0
- package/src/components/phone-field.tsx +119 -0
- package/src/index.ts +3 -0
- package/src/locales/en.tsx +45 -4
- package/src/locales/zh.tsx +43 -4
- package/src/payment/form/index.tsx +16 -1
|
@@ -0,0 +1,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
|
+
}
|