@blocklet/payment-react 1.26.1 → 1.26.3
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/components/dialogs/checkout-dialogs.js +2 -0
- package/es/checkout-v2/components/left/cross-sell-card.js +3 -3
- package/es/checkout-v2/components/left/product-item-card.js +9 -3
- package/es/checkout-v2/components/left/promotion-input.d.ts +4 -1
- package/es/checkout-v2/components/left/promotion-input.js +8 -13
- package/es/checkout-v2/components/right/customer-info-card.d.ts +2 -0
- package/es/checkout-v2/components/right/customer-info-card.js +22 -14
- package/es/checkout-v2/components/right/status-feedback.js +1 -1
- package/es/checkout-v2/components/right/submit-button.js +3 -1
- package/es/checkout-v2/layouts/checkout-layout.js +13 -3
- package/es/checkout-v2/panels/left/composite-panel.js +27 -6
- package/es/checkout-v2/panels/right/payment-panel.js +40 -9
- package/es/checkout-v2/utils/format.d.ts +1 -1
- package/es/checkout-v2/utils/format.js +1 -0
- package/es/checkout-v2/views/error-view.d.ts +1 -1
- package/es/checkout-v2/views/error-view.js +9 -0
- package/es/checkout-v2/views/success-view.js +3 -1
- package/es/components/over-due-invoice-payment.js +5 -3
- package/es/components/service-suspended-dialog.d.ts +4 -0
- package/es/components/service-suspended-dialog.js +61 -0
- package/es/libs/util.d.ts +8 -0
- package/es/libs/util.js +3 -0
- package/es/locales/en.js +4 -0
- package/es/locales/zh.js +4 -0
- package/es/payment/form/index.js +17 -0
- package/es/payment/index.js +15 -4
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +4 -0
- package/lib/checkout-v2/components/left/cross-sell-card.js +2 -2
- package/lib/checkout-v2/components/left/product-item-card.js +9 -2
- package/lib/checkout-v2/components/left/promotion-input.d.ts +4 -1
- package/lib/checkout-v2/components/left/promotion-input.js +12 -19
- package/lib/checkout-v2/components/right/customer-info-card.d.ts +2 -0
- package/lib/checkout-v2/components/right/customer-info-card.js +19 -13
- package/lib/checkout-v2/components/right/status-feedback.js +1 -1
- package/lib/checkout-v2/components/right/submit-button.js +3 -1
- package/lib/checkout-v2/layouts/checkout-layout.js +28 -5
- package/lib/checkout-v2/panels/left/composite-panel.js +20 -5
- package/lib/checkout-v2/panels/right/payment-panel.js +46 -7
- package/lib/checkout-v2/utils/format.d.ts +1 -1
- package/lib/checkout-v2/utils/format.js +7 -0
- package/lib/checkout-v2/views/error-view.d.ts +1 -1
- package/lib/checkout-v2/views/error-view.js +9 -0
- package/lib/checkout-v2/views/success-view.js +2 -0
- package/lib/components/over-due-invoice-payment.js +12 -2
- package/lib/components/service-suspended-dialog.d.ts +4 -0
- package/lib/components/service-suspended-dialog.js +97 -0
- package/lib/libs/util.d.ts +8 -0
- package/lib/libs/util.js +4 -0
- package/lib/locales/en.js +4 -0
- package/lib/locales/zh.js +4 -0
- package/lib/payment/form/index.js +23 -0
- package/lib/payment/index.js +15 -4
- package/package.json +4 -4
- package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +4 -0
- package/src/checkout-v2/components/left/cross-sell-card.tsx +3 -3
- package/src/checkout-v2/components/left/product-item-card.tsx +18 -8
- package/src/checkout-v2/components/left/promotion-input.tsx +17 -17
- package/src/checkout-v2/components/right/customer-info-card.tsx +29 -16
- package/src/checkout-v2/components/right/status-feedback.tsx +2 -2
- package/src/checkout-v2/components/right/submit-button.tsx +2 -0
- package/src/checkout-v2/layouts/checkout-layout.tsx +25 -10
- package/src/checkout-v2/panels/left/composite-panel.tsx +28 -6
- package/src/checkout-v2/panels/right/payment-panel.tsx +32 -5
- package/src/checkout-v2/utils/format.ts +2 -0
- package/src/checkout-v2/views/error-view.tsx +11 -1
- package/src/checkout-v2/views/success-view.tsx +3 -1
- package/src/components/over-due-invoice-payment.tsx +6 -3
- package/src/components/service-suspended-dialog.tsx +64 -0
- package/src/libs/util.ts +7 -0
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +4 -0
- package/src/payment/form/index.tsx +20 -0
- package/src/payment/index.tsx +26 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import { Box, Button, InputBase, InputAdornment, Stack, Typography } from '@mui/material';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import CountrySelect from '../../../components/country-select';
|
|
@@ -17,7 +17,9 @@ interface CustomerInfoCardProps {
|
|
|
17
17
|
values: Record<string, any>;
|
|
18
18
|
onChange: (field: string, value: string | boolean | Record<string, string>) => void;
|
|
19
19
|
errors: Partial<Record<string, string>>;
|
|
20
|
+
checkValid: () => Promise<boolean>;
|
|
20
21
|
validateField: (field: string) => Promise<void>;
|
|
22
|
+
prefetched: boolean;
|
|
21
23
|
};
|
|
22
24
|
isLoggedIn: boolean;
|
|
23
25
|
}
|
|
@@ -39,20 +41,31 @@ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardP
|
|
|
39
41
|
const { t } = useLocaleContext();
|
|
40
42
|
const labels = fieldLabelMap(t);
|
|
41
43
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
44
|
+
// Don't render until first validation completes — avoids flash.
|
|
45
|
+
// Valid → show collapsed summary; invalid → show expanded form.
|
|
46
|
+
// After first check, only manual "Edit"/"Confirm" toggles.
|
|
47
|
+
const [showEditForm, setShowEditForm] = useState(false);
|
|
48
|
+
const [ready, setReady] = useState(false);
|
|
49
|
+
const checkedRef = useRef(false);
|
|
46
50
|
|
|
47
|
-
// When data arrives (e.g. prefetch), auto-switch to confirmed if valid
|
|
48
51
|
useEffect(() => {
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
if (checkedRef.current) return;
|
|
53
|
+
// Wait for data to arrive before making a decision
|
|
54
|
+
if (!form.prefetched && !form.values.customer_name && !form.values.customer_email) return;
|
|
55
|
+
checkedRef.current = true;
|
|
56
|
+
form.checkValid().then((valid) => {
|
|
57
|
+
setShowEditForm(!valid);
|
|
58
|
+
setReady(true);
|
|
59
|
+
});
|
|
60
|
+
}, [form.prefetched]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
// Wrap onChange to delegate to parent form
|
|
63
|
+
const handleChange: typeof form.onChange = useCallback(
|
|
64
|
+
(field, value) => form.onChange(field, value),
|
|
65
|
+
[form.onChange] // eslint-disable-line react-hooks/exhaustive-deps
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!isLoggedIn || !ready) return null;
|
|
56
69
|
|
|
57
70
|
// Summary view
|
|
58
71
|
if (!showEditForm) {
|
|
@@ -162,8 +175,8 @@ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardP
|
|
|
162
175
|
key={name}
|
|
163
176
|
value={value || ''}
|
|
164
177
|
country={form.values.billing_address?.country || ''}
|
|
165
|
-
onChange={(phone) =>
|
|
166
|
-
onCountryChange={(c) =>
|
|
178
|
+
onChange={(phone) => handleChange('customer_phone', phone)}
|
|
179
|
+
onCountryChange={(c) => handleChange('billing_address.country', c)}
|
|
167
180
|
onBlur={() => form.validateField(name)}
|
|
168
181
|
label={label}
|
|
169
182
|
error={form.errors[name]}
|
|
@@ -177,14 +190,14 @@ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardP
|
|
|
177
190
|
<InputBase
|
|
178
191
|
fullWidth
|
|
179
192
|
value={value || ''}
|
|
180
|
-
onChange={(e) =>
|
|
193
|
+
onChange={(e) => handleChange(name, e.target.value)}
|
|
181
194
|
onBlur={() => form.validateField(name)}
|
|
182
195
|
startAdornment={
|
|
183
196
|
isPostalCode ? (
|
|
184
197
|
<InputAdornment position="start" sx={{ mr: 0.5, ml: -0.5 }}>
|
|
185
198
|
<CountrySelect
|
|
186
199
|
value={form.values.billing_address?.country || ''}
|
|
187
|
-
onChange={(v) =>
|
|
200
|
+
onChange={(v) => handleChange('billing_address.country', v)}
|
|
188
201
|
sx={{
|
|
189
202
|
'.MuiOutlinedInput-notchedOutline': { borderColor: 'transparent !important' },
|
|
190
203
|
'& .MuiSelect-select': { py: 0, pr: '20px !important' },
|
|
@@ -19,8 +19,8 @@ export default function StatusFeedback({ status, context, onReset }: StatusFeedb
|
|
|
19
19
|
prevStatusRef.current = status;
|
|
20
20
|
|
|
21
21
|
if (status === 'failed' && context?.type === 'error') {
|
|
22
|
-
// CUSTOMER_LIMITED
|
|
23
|
-
if (context.code === 'CUSTOMER_LIMITED') return;
|
|
22
|
+
// CUSTOMER_LIMITED / STOP_ACCEPTING_ORDERS are handled by CheckoutDialogs
|
|
23
|
+
if (context.code === 'CUSTOMER_LIMITED' || context.code === 'STOP_ACCEPTING_ORDERS') return;
|
|
24
24
|
|
|
25
25
|
Toast.error(context.message || 'Payment failed');
|
|
26
26
|
onReset();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, CircularProgress } from '@mui/material';
|
|
2
|
+
import { primaryContrastColor } from '../../utils/format';
|
|
2
3
|
|
|
3
4
|
interface SubmitButtonProps {
|
|
4
5
|
canSubmit: boolean;
|
|
@@ -30,6 +31,7 @@ export default function SubmitButton({
|
|
|
30
31
|
fontSize: '1.3rem',
|
|
31
32
|
fontWeight: 600,
|
|
32
33
|
textTransform: 'none',
|
|
34
|
+
color: (theme) => primaryContrastColor(theme),
|
|
33
35
|
}}>
|
|
34
36
|
{isProcessing ? processingLabel : label}
|
|
35
37
|
</Button>
|
|
@@ -27,14 +27,26 @@ const fadeIn = {
|
|
|
27
27
|
animation: { xs: 'none', md: 'fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both' },
|
|
28
28
|
} as const;
|
|
29
29
|
|
|
30
|
-
// Desktop: right panel slides in from
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
// Desktop: right panel slides in from right (non-Safari) or fades in (Safari)
|
|
31
|
+
// Safari has flexbox reflow bugs with translateX(100%), so we use a subtle fade instead
|
|
32
|
+
const isSafari =
|
|
33
|
+
typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
|
|
34
|
+
|
|
35
|
+
const slideInFromRight = isSafari
|
|
36
|
+
? ({
|
|
37
|
+
'@keyframes panelFadeIn': {
|
|
38
|
+
from: { opacity: 0, transform: 'translateX(24px)' },
|
|
39
|
+
to: { opacity: 1, transform: 'none' },
|
|
40
|
+
},
|
|
41
|
+
animation: { xs: 'none', md: 'panelFadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' },
|
|
42
|
+
} as const)
|
|
43
|
+
: ({
|
|
44
|
+
'@keyframes slideInRight': {
|
|
45
|
+
from: { transform: 'translateX(100%)' },
|
|
46
|
+
to: { transform: 'translateX(0)' },
|
|
47
|
+
},
|
|
48
|
+
animation: { xs: 'none', md: 'slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards' },
|
|
49
|
+
} as const);
|
|
38
50
|
|
|
39
51
|
interface CheckoutLayoutProps {
|
|
40
52
|
left: React.ReactNode;
|
|
@@ -94,6 +106,7 @@ export default function CheckoutLayout({ left, right, mode = 'inline' }: Checkou
|
|
|
94
106
|
<Box
|
|
95
107
|
sx={{
|
|
96
108
|
flex: 1,
|
|
109
|
+
minWidth: 0,
|
|
97
110
|
bgcolor: (t) => (t.palette.mode === 'dark' ? 'background.default' : '#f8faff'),
|
|
98
111
|
p: { xs: 3, md: 5 },
|
|
99
112
|
pt: { xs: 3, md: 4 },
|
|
@@ -170,7 +183,8 @@ export default function CheckoutLayout({ left, right, mode = 'inline' }: Checkou
|
|
|
170
183
|
{!hideLeft && (
|
|
171
184
|
<Box
|
|
172
185
|
sx={{
|
|
173
|
-
|
|
186
|
+
flex: { xs: 'none', md: '0 0 50%' },
|
|
187
|
+
width: { xs: '100%' },
|
|
174
188
|
height: { xs: 'auto', md: '100vh' },
|
|
175
189
|
display: 'flex',
|
|
176
190
|
justifyContent: { md: 'center' },
|
|
@@ -196,7 +210,8 @@ export default function CheckoutLayout({ left, right, mode = 'inline' }: Checkou
|
|
|
196
210
|
{/* Right panel — full width when left is hidden */}
|
|
197
211
|
<Box
|
|
198
212
|
sx={{
|
|
199
|
-
|
|
213
|
+
flex: hideLeft ? 'none' : { xs: 'none', md: '0 0 50%' },
|
|
214
|
+
width: hideLeft ? '100%' : { xs: '100%' },
|
|
200
215
|
height: hideLeft ? '100vh' : { xs: 'auto', md: '100vh' },
|
|
201
216
|
bgcolor: 'background.paper',
|
|
202
217
|
boxShadow: hideLeft ? 'none' : { md: '-4px 0 16px rgba(0,0,0,0.04)' },
|
|
@@ -17,7 +17,13 @@ import {
|
|
|
17
17
|
} from '@blocklet/payment-react-headless';
|
|
18
18
|
|
|
19
19
|
import { useMobile } from '../../../hooks/mobile';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
INTERVAL_LOCALE_KEY,
|
|
22
|
+
formatTrialText,
|
|
23
|
+
getSessionHeaderMeta,
|
|
24
|
+
tSafe,
|
|
25
|
+
primaryContrastColor,
|
|
26
|
+
} from '../../utils/format';
|
|
21
27
|
import ProductItemCard from '../../components/left/product-item-card';
|
|
22
28
|
import BillingToggle from '../../components/left/billing-toggle';
|
|
23
29
|
import CrossSellCard from '../../components/left/cross-sell-card';
|
|
@@ -60,6 +66,10 @@ export default function CompositePanel() {
|
|
|
60
66
|
const canUpsell = nonCrossSellItems.length <= 1;
|
|
61
67
|
const hasTopUpsell = canUpsell && !!upsellPrimaryItem && ['subscription', 'setup'].includes(mode);
|
|
62
68
|
const isUpselled = !!(upsellPrimaryItem as any)?.upsell_price;
|
|
69
|
+
const [upsellSwitching, setUpsellSwitching] = useState(false);
|
|
70
|
+
// Optimistic: track which tab the user clicked so highlight switches immediately
|
|
71
|
+
const [pendingUpsell, setPendingUpsell] = useState<boolean | null>(null);
|
|
72
|
+
const visualIsUpselled = pendingUpsell !== null ? pendingUpsell : isUpselled;
|
|
63
73
|
|
|
64
74
|
// Intervals for capsule toggle
|
|
65
75
|
const currentInterval = hasTopUpsell ? (upsellPrimaryItem!.price as any)?.recurring?.interval : null;
|
|
@@ -94,7 +104,7 @@ export default function CompositePanel() {
|
|
|
94
104
|
// Capsule button sx helper
|
|
95
105
|
const activeSx = {
|
|
96
106
|
bgcolor: 'primary.main',
|
|
97
|
-
color:
|
|
107
|
+
color: (theme: any) => primaryContrastColor(theme),
|
|
98
108
|
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
|
|
99
109
|
};
|
|
100
110
|
const inactiveSx = {
|
|
@@ -260,17 +270,23 @@ export default function CompositePanel() {
|
|
|
260
270
|
{/* Current interval */}
|
|
261
271
|
<Box
|
|
262
272
|
onClick={async () => {
|
|
263
|
-
if (isUpselled) {
|
|
273
|
+
if (isUpselled && !upsellSwitching) {
|
|
274
|
+
setPendingUpsell(false);
|
|
275
|
+
setUpsellSwitching(true);
|
|
264
276
|
try {
|
|
265
277
|
await lineItems.downsell(
|
|
266
278
|
(upsellPrimaryItem as any).upsell_price?.id || upsellPrimaryItem!.price_id
|
|
267
279
|
);
|
|
268
280
|
} catch (err: any) {
|
|
281
|
+
setPendingUpsell(null);
|
|
269
282
|
Toast.error(err?.response?.data?.error || err?.message || 'Failed');
|
|
283
|
+
} finally {
|
|
284
|
+
setUpsellSwitching(false);
|
|
285
|
+
setPendingUpsell(null);
|
|
270
286
|
}
|
|
271
287
|
}
|
|
272
288
|
}}
|
|
273
|
-
sx={capsuleBtnSx(!
|
|
289
|
+
sx={capsuleBtnSx(!visualIsUpselled)}>
|
|
274
290
|
<Typography component="span" sx={{ fontSize: 14, fontWeight: 700, color: 'inherit', lineHeight: 1 }}>
|
|
275
291
|
{t(INTERVAL_LOCALE_KEY[currentInterval!] || '')}
|
|
276
292
|
</Typography>
|
|
@@ -278,15 +294,21 @@ export default function CompositePanel() {
|
|
|
278
294
|
{/* Upsell interval */}
|
|
279
295
|
<Box
|
|
280
296
|
onClick={async () => {
|
|
281
|
-
if (!isUpselled) {
|
|
297
|
+
if (!isUpselled && !upsellSwitching) {
|
|
298
|
+
setPendingUpsell(true);
|
|
299
|
+
setUpsellSwitching(true);
|
|
282
300
|
try {
|
|
283
301
|
await lineItems.upsell(upsellPrimaryItem!.price_id, upsellTarget.id);
|
|
284
302
|
} catch (err: any) {
|
|
303
|
+
setPendingUpsell(null);
|
|
285
304
|
Toast.error(err?.response?.data?.error || err?.message || 'Failed');
|
|
305
|
+
} finally {
|
|
306
|
+
setUpsellSwitching(false);
|
|
307
|
+
setPendingUpsell(null);
|
|
286
308
|
}
|
|
287
309
|
}
|
|
288
310
|
}}
|
|
289
|
-
sx={capsuleBtnSx(
|
|
311
|
+
sx={capsuleBtnSx(visualIsUpselled)}>
|
|
290
312
|
<Typography component="span" sx={{ fontSize: 14, fontWeight: 700, color: 'inherit', lineHeight: 1 }}>
|
|
291
313
|
{t(INTERVAL_LOCALE_KEY[upsellInterval!] || '')}
|
|
292
314
|
</Typography>
|
|
@@ -39,10 +39,10 @@ import {
|
|
|
39
39
|
import { joinURL } from 'ufo';
|
|
40
40
|
import { usePaymentContext } from '../../../contexts/payment';
|
|
41
41
|
import { useMobile } from '../../../hooks/mobile';
|
|
42
|
-
import { getPrefix } from '../../../libs/util';
|
|
42
|
+
import { getPrefix, getStatementDescriptor } from '../../../libs/util';
|
|
43
43
|
import OverdueInvoicePayment from '../../../components/over-due-invoice-payment';
|
|
44
44
|
|
|
45
|
-
import { tSafe, whiteTooltipSx } from '../../utils/format';
|
|
45
|
+
import { tSafe, whiteTooltipSx, primaryContrastColor } from '../../utils/format';
|
|
46
46
|
import CustomerInfoCard from '../../components/right/customer-info-card';
|
|
47
47
|
import SubscriptionDisclaimer from '../../components/right/subscription-disclaimer';
|
|
48
48
|
import StatusFeedback from '../../components/right/status-feedback';
|
|
@@ -459,6 +459,8 @@ export default function PaymentPanel() {
|
|
|
459
459
|
}}
|
|
460
460
|
discounts={discounts}
|
|
461
461
|
discountAmount={pricing.discount}
|
|
462
|
+
currency={currency}
|
|
463
|
+
isAmountLoading={isAmountLoading}
|
|
462
464
|
/>
|
|
463
465
|
</>
|
|
464
466
|
)}
|
|
@@ -564,15 +566,39 @@ export default function PaymentPanel() {
|
|
|
564
566
|
disabled={!canSubmit || submit.status === 'waiting_stripe'}
|
|
565
567
|
onClick={handleAction}
|
|
566
568
|
startIcon={isProcessing ? <CircularProgress size={20} color="inherit" /> : null}
|
|
567
|
-
endIcon={!isProcessing ? <ArrowForwardIcon /> : undefined}
|
|
568
569
|
sx={{
|
|
569
570
|
py: 1.5,
|
|
570
571
|
fontSize: '1.1rem',
|
|
571
572
|
fontWeight: 600,
|
|
572
573
|
textTransform: 'none',
|
|
573
574
|
borderRadius: '12px',
|
|
575
|
+
color: (theme) => primaryContrastColor(theme),
|
|
576
|
+
position: 'relative',
|
|
577
|
+
overflow: 'hidden',
|
|
578
|
+
'&:hover': { bgcolor: 'primary.main' },
|
|
579
|
+
'&:hover .arrow-icon': { transform: 'translateX(4px)' },
|
|
580
|
+
'&:hover .shine-layer': { transform: 'translateX(100%)' },
|
|
574
581
|
}}>
|
|
575
|
-
{
|
|
582
|
+
<Box component="span" sx={{ position: 'relative', zIndex: 1 }}>
|
|
583
|
+
{isProcessing ? `${t('payment.checkout.processing')}...` : buttonLabel}
|
|
584
|
+
</Box>
|
|
585
|
+
{!isProcessing && (
|
|
586
|
+
<ArrowForwardIcon
|
|
587
|
+
className="arrow-icon"
|
|
588
|
+
sx={{ ml: 1, position: 'relative', zIndex: 1, transition: 'transform 0.2s ease' }}
|
|
589
|
+
/>
|
|
590
|
+
)}
|
|
591
|
+
<Box
|
|
592
|
+
className="shine-layer"
|
|
593
|
+
sx={{
|
|
594
|
+
position: 'absolute',
|
|
595
|
+
inset: 0,
|
|
596
|
+
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent)',
|
|
597
|
+
transform: 'translateX(-100%)',
|
|
598
|
+
transition: 'transform 0.7s ease',
|
|
599
|
+
pointerEvents: 'none',
|
|
600
|
+
}}
|
|
601
|
+
/>
|
|
576
602
|
</Button>
|
|
577
603
|
|
|
578
604
|
{/* Mobile: SSL footer inside fixed bar, below button */}
|
|
@@ -632,6 +658,7 @@ export default function PaymentPanel() {
|
|
|
632
658
|
}}
|
|
633
659
|
discounts={discounts}
|
|
634
660
|
discountAmount={pricing.discount}
|
|
661
|
+
currency={currency}
|
|
635
662
|
/>
|
|
636
663
|
</Drawer>
|
|
637
664
|
)}
|
|
@@ -642,7 +669,7 @@ export default function PaymentPanel() {
|
|
|
642
669
|
mode={mode}
|
|
643
670
|
subscription={subscription}
|
|
644
671
|
staking={pricing.staking}
|
|
645
|
-
appName={(session?.
|
|
672
|
+
appName={getStatementDescriptor(session?.line_items || [])}
|
|
646
673
|
/>
|
|
647
674
|
|
|
648
675
|
{!isMobile && (
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fromUnitToToken } from '@ocap/util';
|
|
2
2
|
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
3
3
|
|
|
4
|
+
export { primaryContrastColor } from '../../libs/util';
|
|
5
|
+
|
|
4
6
|
// Interval key → locale key mapping
|
|
5
7
|
export const INTERVAL_LOCALE_KEY: Record<string, string> = {
|
|
6
8
|
day: 'common.daily',
|
|
@@ -3,10 +3,11 @@ import { alpha, useTheme } from '@mui/material/styles';
|
|
|
3
3
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
4
4
|
import Header from '@blocklet/ui-react/lib/Header';
|
|
5
5
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
+
import { primaryContrastColor } from '../utils/format';
|
|
6
7
|
|
|
7
8
|
interface ErrorViewProps {
|
|
8
9
|
error: string;
|
|
9
|
-
errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
|
|
10
|
+
errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
|
|
10
11
|
mode?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -127,6 +128,14 @@ function getErrorConfig(
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
if (errorCode === 'STOP_ACCEPTING_ORDERS') {
|
|
132
|
+
return {
|
|
133
|
+
title: t('payment.checkout.stopAcceptingOrders.title'),
|
|
134
|
+
description: t('payment.checkout.stopAcceptingOrders.description'),
|
|
135
|
+
color: '#f59e0b',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
return {
|
|
131
140
|
title: t('payment.checkout.error.title'),
|
|
132
141
|
description: error,
|
|
@@ -191,6 +200,7 @@ function ErrorContent({ error, errorCode = undefined }: { error: string; errorCo
|
|
|
191
200
|
fontWeight: 600,
|
|
192
201
|
fontSize: 16,
|
|
193
202
|
letterSpacing: '0.02em',
|
|
203
|
+
color: (th) => primaryContrastColor(th),
|
|
194
204
|
boxShadow: `0 8px 32px -4px ${alpha(primaryColor, 0.3)}`,
|
|
195
205
|
'&:hover': {
|
|
196
206
|
boxShadow: `0 12px 40px -4px ${alpha(primaryColor, 0.4)}`,
|
|
@@ -23,7 +23,7 @@ import type { TCheckoutSessionExpanded } from '@blocklet/payment-types';
|
|
|
23
23
|
import { usePaymentMethodContext } from '@blocklet/payment-react-headless';
|
|
24
24
|
|
|
25
25
|
import { getPrefix } from '../../libs/util';
|
|
26
|
-
import { formatTokenAmount } from '../utils/format';
|
|
26
|
+
import { formatTokenAmount, primaryContrastColor } from '../utils/format';
|
|
27
27
|
|
|
28
28
|
// ── Animations ──
|
|
29
29
|
|
|
@@ -573,6 +573,7 @@ function SubscriptionLinks({
|
|
|
573
573
|
fontWeight: 700,
|
|
574
574
|
fontSize: { xs: 16, md: 17 },
|
|
575
575
|
letterSpacing: '0.02em',
|
|
576
|
+
color: (theme) => primaryContrastColor(theme),
|
|
576
577
|
boxShadow: '0 8px 24px -4px rgba(59,130,246,0.25)',
|
|
577
578
|
'&:hover': {
|
|
578
579
|
boxShadow: '0 12px 28px -4px rgba(59,130,246,0.35)',
|
|
@@ -616,6 +617,7 @@ function InvoiceLink({
|
|
|
616
617
|
fontWeight: 700,
|
|
617
618
|
fontSize: { xs: 16, md: 17 },
|
|
618
619
|
letterSpacing: '0.02em',
|
|
620
|
+
color: (theme) => primaryContrastColor(theme),
|
|
619
621
|
boxShadow: '0 8px 24px -4px rgba(59,130,246,0.25)',
|
|
620
622
|
'&:hover': {
|
|
621
623
|
boxShadow: '0 12px 28px -4px rgba(59,130,246,0.35)',
|
|
@@ -19,7 +19,7 @@ import Dialog from '@arcblock/ux/lib/Dialog/dialog';
|
|
|
19
19
|
import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
|
|
20
20
|
import debounce from 'lodash/debounce';
|
|
21
21
|
import { usePaymentContext } from '../contexts/payment';
|
|
22
|
-
import { formatAmount, formatError, getPrefix, isCrossOrigin } from '../libs/util';
|
|
22
|
+
import { formatAmount, formatError, getPrefix, isCrossOrigin, primaryContrastColor } from '../libs/util';
|
|
23
23
|
import { useSubscription } from '../hooks/subscription';
|
|
24
24
|
import api from '../libs/api';
|
|
25
25
|
import LoadingButton from './loading-button';
|
|
@@ -397,6 +397,8 @@ function OverdueInvoicePayment({
|
|
|
397
397
|
const { currency } = item;
|
|
398
398
|
const inProcess = payLoading && selectCurrencyId === currency.id;
|
|
399
399
|
const status = paymentStatus[currency.id] || 'idle';
|
|
400
|
+
const containedColorSx =
|
|
401
|
+
(options?.variant || 'contained') === 'contained' ? { color: (th: any) => primaryContrastColor(th) } : {};
|
|
400
402
|
|
|
401
403
|
if (status === 'success') {
|
|
402
404
|
return (
|
|
@@ -404,6 +406,7 @@ function OverdueInvoicePayment({
|
|
|
404
406
|
variant={options?.variant || 'contained'}
|
|
405
407
|
size="small"
|
|
406
408
|
onClick={() => checkAndHandleInvoicePaid(currency.id)}
|
|
409
|
+
sx={containedColorSx}
|
|
407
410
|
{...(primaryButton
|
|
408
411
|
? {}
|
|
409
412
|
: {
|
|
@@ -442,7 +445,7 @@ function OverdueInvoicePayment({
|
|
|
442
445
|
disabled={paying || status === 'processing'}
|
|
443
446
|
loading={paying || status === 'processing'}
|
|
444
447
|
onClick={onPay}
|
|
445
|
-
sx={options?.sx}>
|
|
448
|
+
sx={{ ...containedColorSx, ...((options?.sx || {}) as any) }}>
|
|
446
449
|
{buttonText}
|
|
447
450
|
</LoadingButton>
|
|
448
451
|
)}
|
|
@@ -456,7 +459,7 @@ function OverdueInvoicePayment({
|
|
|
456
459
|
disabled={inProcess}
|
|
457
460
|
loading={inProcess}
|
|
458
461
|
onClick={() => handlePay(item)}
|
|
459
|
-
sx={options?.sx}>
|
|
462
|
+
sx={{ ...containedColorSx, ...((options?.sx || {}) as any) }}>
|
|
460
463
|
{status === 'error' ? t('payment.subscription.overdue.retry') : t('payment.subscription.overdue.payNow')}
|
|
461
464
|
</LoadingButton>
|
|
462
465
|
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Box, Button, Dialog, DialogContent, Stack, Typography } from '@mui/material';
|
|
2
|
+
import { alpha } from '@mui/material/styles';
|
|
3
|
+
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
|
|
4
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
5
|
+
|
|
6
|
+
export default function ServiceSuspendedDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
7
|
+
const { t } = useLocaleContext();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Dialog
|
|
11
|
+
open={open}
|
|
12
|
+
onClose={onClose}
|
|
13
|
+
PaperProps={{
|
|
14
|
+
sx: {
|
|
15
|
+
borderRadius: 3,
|
|
16
|
+
maxWidth: 400,
|
|
17
|
+
mx: 'auto',
|
|
18
|
+
overflow: 'hidden',
|
|
19
|
+
},
|
|
20
|
+
}}>
|
|
21
|
+
<DialogContent sx={{ p: 0 }}>
|
|
22
|
+
<Stack alignItems="center" sx={{ pt: 4, pb: 3, px: 4, textAlign: 'center' }}>
|
|
23
|
+
<Box
|
|
24
|
+
sx={{
|
|
25
|
+
width: 64,
|
|
26
|
+
height: 64,
|
|
27
|
+
borderRadius: '50%',
|
|
28
|
+
display: 'flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
justifyContent: 'center',
|
|
31
|
+
bgcolor: (theme) => alpha(theme.palette.warning.main, 0.1),
|
|
32
|
+
mb: 2.5,
|
|
33
|
+
}}>
|
|
34
|
+
<PauseCircleOutlineIcon sx={{ fontSize: 36, color: 'warning.main' }} />
|
|
35
|
+
</Box>
|
|
36
|
+
|
|
37
|
+
<Typography sx={{ fontWeight: 700, fontSize: 18, mb: 1, color: 'text.primary' }}>
|
|
38
|
+
{t('payment.checkout.stopAcceptingOrders.title')}
|
|
39
|
+
</Typography>
|
|
40
|
+
|
|
41
|
+
<Typography sx={{ color: 'text.secondary', fontSize: 14, lineHeight: 1.6 }}>
|
|
42
|
+
{t('payment.checkout.stopAcceptingOrders.description')}
|
|
43
|
+
</Typography>
|
|
44
|
+
</Stack>
|
|
45
|
+
|
|
46
|
+
<Box sx={{ px: 4, pb: 3 }}>
|
|
47
|
+
<Button
|
|
48
|
+
fullWidth
|
|
49
|
+
variant="contained"
|
|
50
|
+
disableElevation
|
|
51
|
+
onClick={onClose}
|
|
52
|
+
sx={{
|
|
53
|
+
borderRadius: 2,
|
|
54
|
+
textTransform: 'none',
|
|
55
|
+
fontWeight: 600,
|
|
56
|
+
py: 1,
|
|
57
|
+
}}>
|
|
58
|
+
{t('common.know')}
|
|
59
|
+
</Button>
|
|
60
|
+
</Box>
|
|
61
|
+
</DialogContent>
|
|
62
|
+
</Dialog>
|
|
63
|
+
);
|
|
64
|
+
}
|
package/src/libs/util.ts
CHANGED
|
@@ -1823,3 +1823,10 @@ export function formatLinkWithLocale(url: string, locale?: string) {
|
|
|
1823
1823
|
return `${url}${separator}locale=${locale}`;
|
|
1824
1824
|
}
|
|
1825
1825
|
}
|
|
1826
|
+
|
|
1827
|
+
// Compute text color that contrasts with primary.main, works in both light and dark mode
|
|
1828
|
+
export function primaryContrastColor(theme: {
|
|
1829
|
+
palette: { primary: { main: string }; getContrastText: (bg: string) => string };
|
|
1830
|
+
}): string {
|
|
1831
|
+
return theme.palette.getContrastText(theme.palette.primary.main);
|
|
1832
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -396,6 +396,10 @@ export default flat({
|
|
|
396
396
|
title: 'Nothing to show here',
|
|
397
397
|
description: 'It seems this checkout session is not configured properly',
|
|
398
398
|
},
|
|
399
|
+
stopAcceptingOrders: {
|
|
400
|
+
title: 'Service Suspended',
|
|
401
|
+
description: 'New order placement is temporarily unavailable due to a system-level service suspension.',
|
|
402
|
+
},
|
|
399
403
|
error: {
|
|
400
404
|
title: 'Something went wrong',
|
|
401
405
|
},
|
package/src/locales/zh.tsx
CHANGED
|
@@ -61,6 +61,7 @@ import LoadingButton from '../../components/loading-button';
|
|
|
61
61
|
import OverdueInvoicePayment from '../../components/over-due-invoice-payment';
|
|
62
62
|
import { saveCurrencyPreference } from '../../libs/currency';
|
|
63
63
|
import ConfirmDialog from '../../components/confirm';
|
|
64
|
+
import ServiceSuspendedDialog from '../../components/service-suspended-dialog';
|
|
64
65
|
import PriceChangeConfirm from '../../components/price-change-confirm';
|
|
65
66
|
import { getFieldValidation, validatePostalCode } from '../../libs/validator';
|
|
66
67
|
|
|
@@ -267,6 +268,7 @@ export default function PaymentForm({
|
|
|
267
268
|
};
|
|
268
269
|
customer?: TCustomer;
|
|
269
270
|
customerLimited?: boolean;
|
|
271
|
+
serviceSuspended?: boolean;
|
|
270
272
|
stripePaying: boolean;
|
|
271
273
|
fastCheckoutInfo: FastCheckoutInfo | null;
|
|
272
274
|
creditInsufficientInfo: {
|
|
@@ -289,6 +291,7 @@ export default function PaymentForm({
|
|
|
289
291
|
stripeContext: undefined,
|
|
290
292
|
customer,
|
|
291
293
|
customerLimited: false,
|
|
294
|
+
serviceSuspended: false,
|
|
292
295
|
stripePaying: false,
|
|
293
296
|
fastCheckoutInfo: null,
|
|
294
297
|
creditInsufficientInfo: null,
|
|
@@ -1198,6 +1201,11 @@ export default function PaymentForm({
|
|
|
1198
1201
|
shouldToast = false;
|
|
1199
1202
|
setState({ customerLimited: true });
|
|
1200
1203
|
}
|
|
1204
|
+
|
|
1205
|
+
if (errorCode === 'STOP_ACCEPTING_ORDERS') {
|
|
1206
|
+
shouldToast = false;
|
|
1207
|
+
setState({ serviceSuspended: true });
|
|
1208
|
+
}
|
|
1201
1209
|
}
|
|
1202
1210
|
if (shouldToast) {
|
|
1203
1211
|
Toast.error(formatError(err));
|
|
@@ -1474,6 +1482,9 @@ export default function PaymentForm({
|
|
|
1474
1482
|
}}
|
|
1475
1483
|
/>
|
|
1476
1484
|
)}
|
|
1485
|
+
{state.serviceSuspended && (
|
|
1486
|
+
<ServiceSuspendedDialog open onClose={() => setState({ serviceSuspended: false })} />
|
|
1487
|
+
)}
|
|
1477
1488
|
{FastCheckoutConfirmDialog}
|
|
1478
1489
|
{CreditInsufficientDialog}
|
|
1479
1490
|
{PriceUpdatedDialog}
|
|
@@ -1748,6 +1759,15 @@ export default function PaymentForm({
|
|
|
1748
1759
|
}}
|
|
1749
1760
|
/>
|
|
1750
1761
|
)}
|
|
1762
|
+
{state.serviceSuspended && (
|
|
1763
|
+
<ConfirmDialog
|
|
1764
|
+
onConfirm={() => setState({ serviceSuspended: false })}
|
|
1765
|
+
onCancel={() => setState({ serviceSuspended: false })}
|
|
1766
|
+
title={t('payment.checkout.stopAcceptingOrders.title')}
|
|
1767
|
+
message={t('payment.checkout.stopAcceptingOrders.description')}
|
|
1768
|
+
confirm={t('common.confirm')}
|
|
1769
|
+
/>
|
|
1770
|
+
)}
|
|
1751
1771
|
{FastCheckoutConfirmDialog}
|
|
1752
1772
|
{CreditInsufficientDialog}
|
|
1753
1773
|
{PriceUpdatedDialog}
|