@blocklet/payment-react 1.19.18 → 1.19.20
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/README.md +313 -0
- package/es/checkout/form.js +18 -2
- package/es/components/auto-topup/index.d.ts +14 -0
- package/es/components/auto-topup/index.js +417 -0
- package/es/components/auto-topup/modal.d.ts +35 -0
- package/es/components/auto-topup/modal.js +734 -0
- package/es/components/auto-topup/product-card.d.ts +13 -0
- package/es/components/auto-topup/product-card.js +173 -0
- package/es/components/collapse.d.ts +13 -0
- package/es/components/collapse.js +76 -0
- package/es/components/input.d.ts +2 -1
- package/es/components/input.js +64 -13
- package/es/components/label.d.ts +2 -1
- package/es/components/label.js +2 -1
- package/es/index.d.ts +4 -1
- package/es/index.js +7 -1
- package/es/libs/util.js +2 -1
- package/es/locales/en.js +56 -0
- package/es/locales/zh.js +56 -0
- package/es/payment/form/index.js +17 -1
- package/es/payment/product-item.js +17 -10
- package/lib/checkout/form.js +18 -2
- package/lib/components/auto-topup/index.d.ts +14 -0
- package/lib/components/auto-topup/index.js +451 -0
- package/lib/components/auto-topup/modal.d.ts +35 -0
- package/lib/components/auto-topup/modal.js +803 -0
- package/lib/components/auto-topup/product-card.d.ts +13 -0
- package/lib/components/auto-topup/product-card.js +149 -0
- package/lib/components/collapse.d.ts +13 -0
- package/lib/components/collapse.js +74 -0
- package/lib/components/input.d.ts +2 -1
- package/lib/components/input.js +66 -24
- package/lib/components/label.d.ts +2 -1
- package/lib/components/label.js +3 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +24 -0
- package/lib/libs/util.js +2 -1
- package/lib/locales/en.js +56 -0
- package/lib/locales/zh.js +56 -0
- package/lib/payment/form/index.js +17 -1
- package/lib/payment/product-item.js +18 -10
- package/package.json +9 -9
- package/src/checkout/form.tsx +21 -2
- package/src/components/auto-topup/index.tsx +449 -0
- package/src/components/auto-topup/modal.tsx +773 -0
- package/src/components/auto-topup/product-card.tsx +156 -0
- package/src/components/collapse.tsx +82 -0
- package/src/components/input.tsx +71 -22
- package/src/components/label.tsx +8 -2
- package/src/index.ts +7 -0
- package/src/libs/util.ts +1 -0
- package/src/locales/en.tsx +59 -0
- package/src/locales/zh.tsx +57 -0
- package/src/payment/form/index.tsx +19 -1
- package/src/payment/product-item.tsx +18 -11
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Stack, Typography, TextField, Card } from '@mui/material';
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
6
|
+
import ProductCard from '../../payment/product-card';
|
|
7
|
+
import { formatPrice, formatNumber } from '../../libs/util';
|
|
8
|
+
|
|
9
|
+
interface AutoTopupProductCardProps {
|
|
10
|
+
product: any;
|
|
11
|
+
price: any;
|
|
12
|
+
currency: TPaymentCurrency;
|
|
13
|
+
quantity: number;
|
|
14
|
+
onQuantityChange: (quantity: number) => void;
|
|
15
|
+
maxQuantity?: number;
|
|
16
|
+
minQuantity?: number;
|
|
17
|
+
creditCurrency: TPaymentCurrency;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function AutoTopupProductCard({
|
|
21
|
+
product,
|
|
22
|
+
price,
|
|
23
|
+
currency,
|
|
24
|
+
quantity,
|
|
25
|
+
onQuantityChange,
|
|
26
|
+
maxQuantity = 99,
|
|
27
|
+
minQuantity = 1,
|
|
28
|
+
creditCurrency,
|
|
29
|
+
}: AutoTopupProductCardProps) {
|
|
30
|
+
const { t } = useLocaleContext();
|
|
31
|
+
const [localQuantity, setLocalQuantity] = useState<number | undefined>(quantity);
|
|
32
|
+
const localQuantityNum = Number(localQuantity) || 0;
|
|
33
|
+
|
|
34
|
+
const handleQuantityChange = (newQuantity: number) => {
|
|
35
|
+
if (!newQuantity) {
|
|
36
|
+
setLocalQuantity(undefined);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
|
|
40
|
+
setLocalQuantity(newQuantity);
|
|
41
|
+
onQuantityChange(newQuantity);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleQuantityInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
46
|
+
const value = parseInt(event.target.value || '0', 10);
|
|
47
|
+
if (!Number.isNaN(value)) {
|
|
48
|
+
handleQuantityChange(value);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const creditUnitAmount = Number(price.metadata?.credit_config?.credit_amount || 0);
|
|
53
|
+
return (
|
|
54
|
+
<Card variant="outlined" sx={{ p: 2 }}>
|
|
55
|
+
<Stack
|
|
56
|
+
direction="row"
|
|
57
|
+
spacing={2}
|
|
58
|
+
sx={{
|
|
59
|
+
flexDirection: {
|
|
60
|
+
xs: 'column',
|
|
61
|
+
sm: 'row',
|
|
62
|
+
},
|
|
63
|
+
alignItems: {
|
|
64
|
+
xs: 'flex-start',
|
|
65
|
+
sm: 'center',
|
|
66
|
+
},
|
|
67
|
+
justifyContent: {
|
|
68
|
+
xs: 'flex-start',
|
|
69
|
+
sm: 'space-between',
|
|
70
|
+
},
|
|
71
|
+
}}>
|
|
72
|
+
<Stack
|
|
73
|
+
direction="row"
|
|
74
|
+
spacing={2}
|
|
75
|
+
sx={{
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
flex: 1,
|
|
78
|
+
}}>
|
|
79
|
+
<ProductCard
|
|
80
|
+
name={product?.name || ''}
|
|
81
|
+
description={t('payment.autoTopup.creditsIncluded', {
|
|
82
|
+
name: creditCurrency?.name,
|
|
83
|
+
num: formatNumber(creditUnitAmount * localQuantityNum),
|
|
84
|
+
})}
|
|
85
|
+
logo={product?.images?.[0] as string}
|
|
86
|
+
size={40}
|
|
87
|
+
/>
|
|
88
|
+
</Stack>
|
|
89
|
+
<Stack
|
|
90
|
+
direction="row"
|
|
91
|
+
spacing={1}
|
|
92
|
+
sx={{
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
alignSelf: {
|
|
95
|
+
xs: 'flex-end',
|
|
96
|
+
sm: 'center',
|
|
97
|
+
},
|
|
98
|
+
}}>
|
|
99
|
+
<Typography
|
|
100
|
+
variant="body2"
|
|
101
|
+
sx={{
|
|
102
|
+
color: 'text.secondary',
|
|
103
|
+
minWidth: 'fit-content',
|
|
104
|
+
}}>
|
|
105
|
+
{t('common.quantity')}:
|
|
106
|
+
</Typography>
|
|
107
|
+
<TextField
|
|
108
|
+
size="small"
|
|
109
|
+
value={localQuantity}
|
|
110
|
+
onChange={handleQuantityInputChange}
|
|
111
|
+
type="number"
|
|
112
|
+
sx={{
|
|
113
|
+
'& .MuiInputBase-root': {
|
|
114
|
+
height: 32,
|
|
115
|
+
},
|
|
116
|
+
}}
|
|
117
|
+
slotProps={{
|
|
118
|
+
htmlInput: {
|
|
119
|
+
min: 0,
|
|
120
|
+
max: maxQuantity,
|
|
121
|
+
style: { textAlign: 'center', padding: '4px 8px' },
|
|
122
|
+
},
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
</Stack>
|
|
126
|
+
</Stack>
|
|
127
|
+
{/* 充值金额显示 */}
|
|
128
|
+
<Stack
|
|
129
|
+
direction="row"
|
|
130
|
+
sx={{
|
|
131
|
+
justifyContent: 'space-between',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
mt: 2,
|
|
134
|
+
pt: 2,
|
|
135
|
+
borderTop: '1px solid',
|
|
136
|
+
borderColor: 'divider',
|
|
137
|
+
}}>
|
|
138
|
+
<Typography
|
|
139
|
+
variant="body2"
|
|
140
|
+
sx={{
|
|
141
|
+
color: 'text.secondary',
|
|
142
|
+
}}>
|
|
143
|
+
{t('payment.autoTopup.rechargeAmount')}
|
|
144
|
+
</Typography>
|
|
145
|
+
<Typography
|
|
146
|
+
variant="h6"
|
|
147
|
+
sx={{
|
|
148
|
+
fontWeight: 600,
|
|
149
|
+
color: 'text.primary',
|
|
150
|
+
}}>
|
|
151
|
+
{formatPrice(price, currency, product?.unit_label, localQuantity, true)}
|
|
152
|
+
</Typography>
|
|
153
|
+
</Stack>
|
|
154
|
+
</Card>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import { ExpandLessOutlined, ExpandMoreOutlined } from '@mui/icons-material';
|
|
3
|
+
import { Box, Collapse, Stack } from '@mui/material';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
trigger: string | ((expanded: boolean) => React.ReactNode) | React.ReactNode;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
expanded?: boolean;
|
|
10
|
+
addons?: React.ReactNode;
|
|
11
|
+
style?: Record<string, any>;
|
|
12
|
+
value?: string;
|
|
13
|
+
onChange?: (value: string, expanded: boolean) => void;
|
|
14
|
+
lazy?: boolean;
|
|
15
|
+
card?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function IconCollapse(rawProps: Props) {
|
|
19
|
+
const props = Object.assign(
|
|
20
|
+
{
|
|
21
|
+
value: '',
|
|
22
|
+
onChange: () => {},
|
|
23
|
+
children: null,
|
|
24
|
+
expanded: false,
|
|
25
|
+
addons: null,
|
|
26
|
+
style: {},
|
|
27
|
+
lazy: true,
|
|
28
|
+
card: false,
|
|
29
|
+
},
|
|
30
|
+
rawProps
|
|
31
|
+
);
|
|
32
|
+
const [expanded, setExpanded] = useState(props.expanded || false);
|
|
33
|
+
const toggleExpanded = () => {
|
|
34
|
+
const newExpanded = !expanded;
|
|
35
|
+
setExpanded(newExpanded);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setExpanded(props.expanded || false);
|
|
40
|
+
}, [props.expanded]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<Stack
|
|
45
|
+
direction="row"
|
|
46
|
+
onClick={(e) => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
props.onChange?.(props.value || '', !expanded);
|
|
49
|
+
toggleExpanded();
|
|
50
|
+
}}
|
|
51
|
+
sx={{
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'space-between',
|
|
54
|
+
width: 1,
|
|
55
|
+
cursor: 'pointer',
|
|
56
|
+
fontWeight: 500,
|
|
57
|
+
color: 'text.primary',
|
|
58
|
+
'& :hover': { color: 'primary.main' },
|
|
59
|
+
...(props.card && {
|
|
60
|
+
borderRadius: 1,
|
|
61
|
+
padding: 1,
|
|
62
|
+
pl: 2,
|
|
63
|
+
backgroundColor: 'grey.100',
|
|
64
|
+
}),
|
|
65
|
+
...props.style,
|
|
66
|
+
}}>
|
|
67
|
+
<Box>{typeof props.trigger === 'function' ? props.trigger(expanded) : props.trigger}</Box>
|
|
68
|
+
<Stack
|
|
69
|
+
direction="row"
|
|
70
|
+
spacing={2}
|
|
71
|
+
sx={{
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
}}>
|
|
74
|
+
{props.addons} {expanded ? <ExpandLessOutlined /> : <ExpandMoreOutlined />}
|
|
75
|
+
</Stack>
|
|
76
|
+
</Stack>
|
|
77
|
+
<Collapse in={expanded} sx={{ width: '100%' }}>
|
|
78
|
+
{expanded || props.lazy ? props.children : null}
|
|
79
|
+
</Collapse>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
package/src/components/input.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { type ReactNode, useImperativeHandle, useRef } from 'react';
|
|
2
|
-
import { Box, InputAdornment, TextField, Typography } from '@mui/material';
|
|
2
|
+
import { Box, InputAdornment, TextField, Typography, Stack, useMediaQuery, useTheme } from '@mui/material';
|
|
3
3
|
import get from 'lodash/get';
|
|
4
4
|
import { Controller, useFormContext } from 'react-hook-form';
|
|
5
5
|
import type { TextFieldProps } from '@mui/material';
|
|
@@ -16,6 +16,7 @@ type InputProps = TextFieldProps & {
|
|
|
16
16
|
required?: boolean;
|
|
17
17
|
tooltip?: ReactNode | string;
|
|
18
18
|
description?: ReactNode | string;
|
|
19
|
+
layout?: 'vertical' | 'horizontal';
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
function FormInputError({ error }: { error: string }) {
|
|
@@ -40,17 +41,46 @@ export default function FormInput({
|
|
|
40
41
|
required = false,
|
|
41
42
|
tooltip = '',
|
|
42
43
|
description = '',
|
|
44
|
+
layout = 'vertical',
|
|
45
|
+
slotProps,
|
|
43
46
|
...rest
|
|
44
47
|
}: InputProps & {
|
|
45
48
|
ref?: React.RefObject<HTMLInputElement | null>;
|
|
46
49
|
inputProps?: TextFieldProps['inputProps'];
|
|
47
50
|
}) {
|
|
48
51
|
const { control, formState } = useFormContext();
|
|
52
|
+
const theme = useTheme();
|
|
53
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
49
54
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
55
|
+
|
|
50
56
|
useImperativeHandle(ref, () => {
|
|
51
57
|
return inputRef.current as HTMLInputElement;
|
|
52
58
|
});
|
|
59
|
+
|
|
53
60
|
const error = get(formState.errors, name)?.message as string;
|
|
61
|
+
const isHorizontal = layout === 'horizontal' && !isMobile;
|
|
62
|
+
|
|
63
|
+
const mergedSlotProps = {
|
|
64
|
+
htmlInput: { ...inputProps, ...slotProps?.htmlInput },
|
|
65
|
+
input: Object.assign(
|
|
66
|
+
rest.InputProps || {},
|
|
67
|
+
slotProps?.input || {},
|
|
68
|
+
errorPosition === 'right' && error ? { endAdornment: <FormInputError error={error} /> } : {}
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const renderLabel = () => {
|
|
73
|
+
if (!label) return null;
|
|
74
|
+
return (
|
|
75
|
+
<FormLabel
|
|
76
|
+
required={required}
|
|
77
|
+
tooltip={tooltip}
|
|
78
|
+
description={description}
|
|
79
|
+
boxSx={isHorizontal ? { width: 'fit-content', whiteSpace: 'nowrap', minWidth: 'fit-content' } : undefined}>
|
|
80
|
+
{label}
|
|
81
|
+
</FormLabel>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
54
84
|
|
|
55
85
|
return (
|
|
56
86
|
<Controller
|
|
@@ -59,28 +89,47 @@ export default function FormInput({
|
|
|
59
89
|
rules={rules}
|
|
60
90
|
render={({ field }) => (
|
|
61
91
|
<Box sx={{ width: '100%', ...wrapperStyle }}>
|
|
62
|
-
{
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
{isHorizontal ? (
|
|
93
|
+
<Stack
|
|
94
|
+
direction="row"
|
|
95
|
+
spacing={2}
|
|
96
|
+
sx={{
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'space-between',
|
|
99
|
+
}}>
|
|
100
|
+
{renderLabel()}
|
|
101
|
+
<TextField
|
|
102
|
+
fullWidth={false}
|
|
103
|
+
error={!!error}
|
|
104
|
+
helperText={errorPosition === 'bottom' && error ? error : ''}
|
|
105
|
+
placeholder={placeholder}
|
|
106
|
+
size="small"
|
|
107
|
+
{...field}
|
|
108
|
+
{...rest}
|
|
109
|
+
inputRef={inputRef}
|
|
110
|
+
slotProps={mergedSlotProps}
|
|
111
|
+
sx={{
|
|
112
|
+
flex: 1,
|
|
113
|
+
...rest.sx,
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
</Stack>
|
|
117
|
+
) : (
|
|
118
|
+
<>
|
|
119
|
+
{renderLabel()}
|
|
120
|
+
<TextField
|
|
121
|
+
fullWidth
|
|
122
|
+
error={!!error}
|
|
123
|
+
helperText={errorPosition === 'bottom' && error ? error : ''}
|
|
124
|
+
placeholder={placeholder}
|
|
125
|
+
size="small"
|
|
126
|
+
{...field}
|
|
127
|
+
{...rest}
|
|
128
|
+
inputRef={inputRef}
|
|
129
|
+
slotProps={mergedSlotProps}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
66
132
|
)}
|
|
67
|
-
<TextField
|
|
68
|
-
fullWidth
|
|
69
|
-
error={!!get(formState.errors, name)}
|
|
70
|
-
helperText={errorPosition === 'bottom' && error ? error : ''}
|
|
71
|
-
placeholder={placeholder}
|
|
72
|
-
size="small"
|
|
73
|
-
{...field}
|
|
74
|
-
{...rest}
|
|
75
|
-
inputRef={inputRef}
|
|
76
|
-
slotProps={{
|
|
77
|
-
htmlInput: inputProps,
|
|
78
|
-
input: Object.assign(
|
|
79
|
-
rest.InputProps || {},
|
|
80
|
-
errorPosition === 'right' && error ? { endAdornment: <FormInputError error={error} /> } : {}
|
|
81
|
-
),
|
|
82
|
-
}}
|
|
83
|
-
/>
|
|
84
133
|
</Box>
|
|
85
134
|
)}
|
|
86
135
|
/>
|
package/src/components/label.tsx
CHANGED
|
@@ -8,10 +8,16 @@ export default function CustomFormLabel({
|
|
|
8
8
|
required = false,
|
|
9
9
|
tooltip = '',
|
|
10
10
|
description = '',
|
|
11
|
+
boxSx = {},
|
|
11
12
|
...props
|
|
12
|
-
}: FormLabelProps & {
|
|
13
|
+
}: FormLabelProps & {
|
|
14
|
+
required?: boolean;
|
|
15
|
+
tooltip?: ReactNode | string;
|
|
16
|
+
description?: ReactNode | string;
|
|
17
|
+
boxSx?: React.CSSProperties;
|
|
18
|
+
}) {
|
|
13
19
|
return (
|
|
14
|
-
<Box sx={{ mb: 1, width: '100%' }}>
|
|
20
|
+
<Box sx={{ mb: 1, width: '100%', ...boxSx }}>
|
|
15
21
|
<FormLabel
|
|
16
22
|
{...props}
|
|
17
23
|
sx={{
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,9 @@ import PaymentBeneficiaries from './components/payment-beneficiaries';
|
|
|
36
36
|
import LoadingButton from './components/loading-button';
|
|
37
37
|
import ResumeSubscription from './components/resume-subscription';
|
|
38
38
|
import DateRangePicker from './components/date-range-picker';
|
|
39
|
+
import AutoTopupModal from './components/auto-topup/modal';
|
|
40
|
+
import AutoTopup from './components/auto-topup';
|
|
41
|
+
import Collapse from './components/collapse';
|
|
39
42
|
|
|
40
43
|
export { PaymentThemeProvider } from './theme';
|
|
41
44
|
|
|
@@ -50,6 +53,7 @@ export * from './hooks/mobile';
|
|
|
50
53
|
export * from './hooks/table';
|
|
51
54
|
export * from './hooks/scroll';
|
|
52
55
|
export * from './hooks/keyboard';
|
|
56
|
+
|
|
53
57
|
export * from './libs/validator';
|
|
54
58
|
|
|
55
59
|
export { translations, createTranslator } from './locales';
|
|
@@ -95,4 +99,7 @@ export {
|
|
|
95
99
|
CreditTransactionsList,
|
|
96
100
|
DateRangePicker,
|
|
97
101
|
CreditStatusChip,
|
|
102
|
+
AutoTopupModal,
|
|
103
|
+
AutoTopup,
|
|
104
|
+
Collapse,
|
|
98
105
|
};
|
package/src/libs/util.ts
CHANGED
|
@@ -1188,6 +1188,7 @@ export function getInvoiceDescriptionAndReason(invoice: TInvoiceExpanded, locale
|
|
|
1188
1188
|
recharge: t('payment.invoice.reason.recharge', locale),
|
|
1189
1189
|
stake_overdraft_protection: t('payment.invoice.reason.stake', locale),
|
|
1190
1190
|
overdraft_protection: t('payment.invoice.reason.fee', locale),
|
|
1191
|
+
auto_recharge: t('payment.invoice.reason.recharge', locale),
|
|
1191
1192
|
};
|
|
1192
1193
|
let invoiceType = t('payment.invoice.reason.payment', locale);
|
|
1193
1194
|
if (reason.includes('stake') || reason.includes('recharge') || reason === 'overdraft_protection') {
|
package/src/locales/en.tsx
CHANGED
|
@@ -31,6 +31,8 @@ export default flat({
|
|
|
31
31
|
removed: 'Resource removed',
|
|
32
32
|
confirm: 'Confirm',
|
|
33
33
|
clear: 'Clear',
|
|
34
|
+
show: 'Show',
|
|
35
|
+
hide: 'Hide',
|
|
34
36
|
selectTimeRange: 'Select time range',
|
|
35
37
|
startDate: 'Start date',
|
|
36
38
|
endDate: 'End date',
|
|
@@ -263,6 +265,63 @@ export default flat({
|
|
|
263
265
|
},
|
|
264
266
|
},
|
|
265
267
|
},
|
|
268
|
+
autoTopup: {
|
|
269
|
+
title: 'Auto Top-Up',
|
|
270
|
+
enableLabel: 'Auto Top-Up',
|
|
271
|
+
purchaseConfig: 'Credit Purchase Configuration',
|
|
272
|
+
triggerThreshold: 'When credits are below',
|
|
273
|
+
thresholdPlaceholder: 'Enter threshold amount',
|
|
274
|
+
thresholdDescription: 'Credits remaining to trigger auto top-up',
|
|
275
|
+
thresholdMinError: 'Threshold must be greater than 0',
|
|
276
|
+
thresholdFormatError: 'Please enter a valid number',
|
|
277
|
+
creditsIncluded: '{num} {name} included',
|
|
278
|
+
purchaseBelow: 'Purchase this package',
|
|
279
|
+
perPackage: 'per package',
|
|
280
|
+
quantity: 'Quantity',
|
|
281
|
+
selectPaymentMethod: 'Select payment method',
|
|
282
|
+
paymentInformation: 'Payment Information',
|
|
283
|
+
saveConfiguration: 'Save Configuration',
|
|
284
|
+
saveSuccess: 'Auto top-up configuration saved successfully',
|
|
285
|
+
disableSuccess: 'Auto top-up disabled successfully',
|
|
286
|
+
authorizationRequired: 'Payment authorization is required to complete the setup',
|
|
287
|
+
cryptoAuthorizationNote: 'You will need to authorize payments from your wallet to enable automatic top-ups',
|
|
288
|
+
tip: 'With auto top-up enabled, the system automatically buys the specified credit package when your balance drops below the threshold.',
|
|
289
|
+
authTitle: 'Auto Top-Up Authorization',
|
|
290
|
+
authTip: 'Please complete the authorization process to enable automatic top-up',
|
|
291
|
+
rechargeAmount: 'Top-up Amount per Time',
|
|
292
|
+
currentPaymentMethod: 'Current Payment Method',
|
|
293
|
+
changePaymentMethod: 'Change Payment Method',
|
|
294
|
+
keepCurrent: 'Keep Current',
|
|
295
|
+
changePaymentMethodTip: 'You will be redirected to set up a new payment method when saving.',
|
|
296
|
+
noPaymentMethodSetup: 'No payment method configured. Please set up a payment method first.',
|
|
297
|
+
addFunds: 'Add Funds',
|
|
298
|
+
advanced: 'Advanced Options',
|
|
299
|
+
enabled: 'Enabled',
|
|
300
|
+
disabled: 'Disabled',
|
|
301
|
+
notConfigured: 'Auto Top-Up not configured',
|
|
302
|
+
setup: 'Enable Auto Top-Up',
|
|
303
|
+
ruleDisplay: 'When balance < {threshold}, top-up {amount} to get {credits} credits',
|
|
304
|
+
activeDescription:
|
|
305
|
+
'Auto top-up is enabled. When Credits drop below {threshold}, the system will automatically purchase {credits} Credits for {amount}',
|
|
306
|
+
activeDescriptionWithCredits:
|
|
307
|
+
'Auto top-up is enabled. When Credits drop below {threshold}, the system will automatically purchase {credits}.',
|
|
308
|
+
purchaseAmount: 'Purchase Amount',
|
|
309
|
+
paymentMethod: 'Payment Method',
|
|
310
|
+
walletBalance: 'Wallet Balance',
|
|
311
|
+
paymentAddress: 'Payment Address',
|
|
312
|
+
inactiveDescription:
|
|
313
|
+
'Enable auto top-up to automatically renew when {name} are insufficient, effectively avoiding service interruptions.',
|
|
314
|
+
showDetails: 'Show Method',
|
|
315
|
+
hideDetails: 'Hide Method',
|
|
316
|
+
dailyLimits: {
|
|
317
|
+
maxAmount: 'Daily Top-up Amount Limit',
|
|
318
|
+
maxAmountPlaceholder: '0 means unlimited',
|
|
319
|
+
maxAmountDescription: 'The maximum amount of top-up you can make each day, 0 means unlimited',
|
|
320
|
+
maxAttempts: 'Daily Top-up Attempts Limit',
|
|
321
|
+
maxAttemptsPlaceholder: '0 means unlimited',
|
|
322
|
+
maxAttemptsDescription: 'The maximum number of top-up attempts you can make each day, 0 means unlimited',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
266
325
|
customer: {
|
|
267
326
|
payments: 'Payment History',
|
|
268
327
|
invoices: 'Invoice History',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -34,6 +34,8 @@ export default flat({
|
|
|
34
34
|
confirm: '确认',
|
|
35
35
|
cancel: '取消',
|
|
36
36
|
clear: '清空',
|
|
37
|
+
show: '显示',
|
|
38
|
+
hide: '隐藏',
|
|
37
39
|
selectTimeRange: '选择时间范围',
|
|
38
40
|
startDate: '开始日期',
|
|
39
41
|
endDate: '结束日期',
|
|
@@ -257,6 +259,61 @@ export default flat({
|
|
|
257
259
|
},
|
|
258
260
|
},
|
|
259
261
|
},
|
|
262
|
+
autoTopup: {
|
|
263
|
+
title: '自动充值',
|
|
264
|
+
enableLabel: '自动充值',
|
|
265
|
+
purchaseConfig: '购买配置',
|
|
266
|
+
triggerThreshold: '当额度低于',
|
|
267
|
+
thresholdPlaceholder: '输入触发阈值',
|
|
268
|
+
thresholdDescription: '触发自动充值的剩余额度',
|
|
269
|
+
thresholdMinError: '阈值必须大于0',
|
|
270
|
+
thresholdFormatError: '请输入有效数字',
|
|
271
|
+
creditsIncluded: '包含 {num} {name}',
|
|
272
|
+
purchaseBelow: '购买此套餐',
|
|
273
|
+
perPackage: '每包',
|
|
274
|
+
quantity: '数量',
|
|
275
|
+
selectPaymentMethod: '选择支付方式',
|
|
276
|
+
paymentInformation: '支付信息',
|
|
277
|
+
saveConfiguration: '保存配置',
|
|
278
|
+
saveSuccess: '自动充值配置保存成功',
|
|
279
|
+
disableSuccess: '已关闭自动充值',
|
|
280
|
+
stripeSetupRequired: '需要信用卡授权以进行自动支付',
|
|
281
|
+
cryptoAuthorizationNote: '您需要从钱包授权支付以启用自动充值',
|
|
282
|
+
tip: '启用自动充值后,当您的额度低于阈值时,系统将自动购买指定数量的额度套餐。',
|
|
283
|
+
authTitle: '自动充值授权',
|
|
284
|
+
authTip: '请完成授权流程以启用自动充值',
|
|
285
|
+
rechargeAmount: '每次充值金额',
|
|
286
|
+
currentPaymentMethod: '当前支付方式',
|
|
287
|
+
changePaymentMethod: '更换支付方式',
|
|
288
|
+
keepCurrent: '保持当前',
|
|
289
|
+
changePaymentMethodTip: '保存时将引导您设置新的支付方式。',
|
|
290
|
+
noPaymentMethodSetup: '未配置支付方式,请先设置支付方式。',
|
|
291
|
+
addFunds: '充值',
|
|
292
|
+
advanced: '高级选项',
|
|
293
|
+
enabled: '已启用',
|
|
294
|
+
disabled: '已禁用',
|
|
295
|
+
notConfigured: '尚未配置自动充值',
|
|
296
|
+
setup: '去启用',
|
|
297
|
+
ruleDisplay: '额度低于{threshold}时,充值{amount}可获得{credits}额度',
|
|
298
|
+
activeDescription:
|
|
299
|
+
'自动充值已启用,当 Credits 低于 {threshold} 时,系统会自动充值购买 {credits} Credits,支付金额为 {amount}',
|
|
300
|
+
activeDescriptionWithCredits: '自动充值已启用,当 Credits 低于 {threshold} 时,系统会自动购买 {credits} 额度。',
|
|
301
|
+
purchaseAmount: '购买所需金额',
|
|
302
|
+
paymentMethod: '支付方式',
|
|
303
|
+
walletBalance: '钱包余额',
|
|
304
|
+
paymentAddress: '支付地址',
|
|
305
|
+
inactiveDescription: '开启自动充值,即可在 {name} 不足时自动续费,有效避免服务中断。',
|
|
306
|
+
showDetails: '展开支付信息',
|
|
307
|
+
hideDetails: '收起',
|
|
308
|
+
dailyLimits: {
|
|
309
|
+
maxAmount: '每日充值金额上限',
|
|
310
|
+
maxAmountPlaceholder: '0表示无限制',
|
|
311
|
+
maxAmountDescription: '每日可充值的最大总金额,0 表示无限制',
|
|
312
|
+
maxAttempts: '每日充值次数上限',
|
|
313
|
+
maxAttemptsPlaceholder: '0表示无限制',
|
|
314
|
+
maxAttemptsDescription: '每日可充值的最大次数,0 表示无限制',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
260
317
|
customer: {
|
|
261
318
|
payments: '支付历史',
|
|
262
319
|
invoices: '账单历史',
|
|
@@ -15,9 +15,11 @@ import { dispatch } from 'use-bus';
|
|
|
15
15
|
import isEmail from 'validator/es/lib/isEmail';
|
|
16
16
|
import { fromUnitToToken } from '@ocap/util';
|
|
17
17
|
import DID from '@arcblock/ux/lib/DID';
|
|
18
|
+
import { PayFailedEvent } from '@arcblock/ux/lib/withTracker/action/pay';
|
|
18
19
|
|
|
19
20
|
import isEmpty from 'lodash/isEmpty';
|
|
20
21
|
import { HelpOutline, OpenInNew } from '@mui/icons-material';
|
|
22
|
+
import { ReactGA } from '@arcblock/ux/lib/withTracker';
|
|
21
23
|
import FormInput from '../../components/input';
|
|
22
24
|
import FormLabel from '../../components/label';
|
|
23
25
|
import { usePaymentContext } from '../../contexts/payment';
|
|
@@ -185,6 +187,7 @@ export default function PaymentForm({
|
|
|
185
187
|
trigger,
|
|
186
188
|
} = useFormContext();
|
|
187
189
|
const errorRef = useRef<HTMLDivElement | null>(null);
|
|
190
|
+
const processingRef = useRef(false);
|
|
188
191
|
const quantityInventoryStatus = useMemo(() => {
|
|
189
192
|
let status = true;
|
|
190
193
|
for (const item of checkoutSession.line_items) {
|
|
@@ -369,6 +372,10 @@ export default function PaymentForm({
|
|
|
369
372
|
}, [session?.user, method, checkoutSession]);
|
|
370
373
|
|
|
371
374
|
const handleConnected = async () => {
|
|
375
|
+
if (processingRef.current) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
processingRef.current = true;
|
|
372
379
|
setState({ paying: true });
|
|
373
380
|
try {
|
|
374
381
|
const result = await waitForCheckoutComplete(checkoutSession.id);
|
|
@@ -377,9 +384,20 @@ export default function PaymentForm({
|
|
|
377
384
|
onPaid(result);
|
|
378
385
|
}
|
|
379
386
|
} catch (err) {
|
|
380
|
-
|
|
387
|
+
const errorMessage = formatError(err);
|
|
388
|
+
const payFailedEvent: PayFailedEvent = {
|
|
389
|
+
action: 'payFailed',
|
|
390
|
+
// @ts-ignore 后续升级的话就会报错了,移除这个 lint 即可
|
|
391
|
+
mode: checkoutSession.mode,
|
|
392
|
+
errorMessage,
|
|
393
|
+
success: false,
|
|
394
|
+
};
|
|
395
|
+
ReactGA.event(payFailedEvent.action, payFailedEvent);
|
|
396
|
+
|
|
397
|
+
Toast.error(errorMessage);
|
|
381
398
|
} finally {
|
|
382
399
|
setState({ paying: false });
|
|
400
|
+
processingRef.current = false;
|
|
383
401
|
}
|
|
384
402
|
};
|
|
385
403
|
|