@blocklet/payment-react 1.18.18 → 1.18.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 +23 -13
- package/es/components/livemode.js +2 -2
- package/es/hooks/keyboard.d.ts +44 -0
- package/es/hooks/keyboard.js +86 -0
- package/es/index.d.ts +1 -0
- package/es/index.js +1 -0
- package/es/locales/en.js +3 -2
- package/es/locales/zh.js +2 -1
- package/es/payment/donation-form.js +51 -14
- package/es/payment/form/index.js +11 -0
- package/es/payment/product-donation.js +223 -115
- package/lib/components/livemode.js +2 -2
- package/lib/hooks/keyboard.d.ts +44 -0
- package/lib/hooks/keyboard.js +85 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +12 -0
- package/lib/locales/en.js +3 -2
- package/lib/locales/zh.js +2 -1
- package/lib/payment/donation-form.js +42 -7
- package/lib/payment/form/index.js +11 -0
- package/lib/payment/product-donation.js +148 -40
- package/package.json +6 -6
- package/src/components/livemode.tsx +2 -2
- package/src/hooks/keyboard.ts +159 -0
- package/src/index.ts +1 -0
- package/src/locales/en.tsx +3 -2
- package/src/locales/zh.tsx +2 -1
- package/src/payment/donation-form.tsx +44 -11
- package/src/payment/form/index.tsx +20 -0
- package/src/payment/product-donation.tsx +180 -72
|
@@ -7,6 +7,15 @@ import { useEffect, useRef } from 'react';
|
|
|
7
7
|
import { formatAmountPrecisionLimit } from '../libs/util';
|
|
8
8
|
import { usePaymentContext } from '../contexts/payment';
|
|
9
9
|
import { usePreventWheel } from '../hooks/scroll';
|
|
10
|
+
import { useTabNavigation } from '../hooks/keyboard';
|
|
11
|
+
|
|
12
|
+
// LocalStorage key base for preset selection
|
|
13
|
+
const DONATION_PRESET_KEY_BASE = 'payment-donation-preset';
|
|
14
|
+
const DONATION_CUSTOM_AMOUNT_KEY_BASE = 'payment-donation-custom-amount';
|
|
15
|
+
|
|
16
|
+
const formatAmount = (amount: string | number): string => {
|
|
17
|
+
return String(amount);
|
|
18
|
+
};
|
|
10
19
|
|
|
11
20
|
export default function ProductDonation({
|
|
12
21
|
item,
|
|
@@ -20,32 +29,129 @@ export default function ProductDonation({
|
|
|
20
29
|
currency: TPaymentCurrency;
|
|
21
30
|
}) {
|
|
22
31
|
const { t, locale } = useLocaleContext();
|
|
23
|
-
const { setPayable } = usePaymentContext();
|
|
32
|
+
const { setPayable, session } = usePaymentContext();
|
|
24
33
|
usePreventWheel();
|
|
25
|
-
const presets = settings?.amount?.presets || [];
|
|
26
|
-
|
|
34
|
+
const presets = (settings?.amount?.presets || []).map(formatAmount);
|
|
35
|
+
|
|
36
|
+
const getUserStorageKey = (base: string) => {
|
|
37
|
+
const userDid = session?.user?.did;
|
|
38
|
+
return userDid ? `${base}:${userDid}` : base;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getSavedCustomAmount = () => {
|
|
42
|
+
try {
|
|
43
|
+
return localStorage.getItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE)) || '';
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn('Failed to access localStorage', e);
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getDefaultPreset = () => {
|
|
51
|
+
if (settings?.amount?.preset) {
|
|
52
|
+
return formatAmount(settings.amount.preset);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const savedPreset = localStorage.getItem(getUserStorageKey(DONATION_PRESET_KEY_BASE));
|
|
57
|
+
if (savedPreset) {
|
|
58
|
+
if (presets.includes(formatAmount(savedPreset))) {
|
|
59
|
+
return formatAmount(savedPreset);
|
|
60
|
+
}
|
|
61
|
+
if (savedPreset === 'custom' && supportCustom) {
|
|
62
|
+
return 'custom';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.warn('Failed to access localStorage', e);
|
|
67
|
+
}
|
|
68
|
+
if (presets.length > 0) {
|
|
69
|
+
const middleIndex = Math.floor(presets.length / 2);
|
|
70
|
+
return presets[middleIndex] || presets[0];
|
|
71
|
+
}
|
|
72
|
+
return '0';
|
|
73
|
+
};
|
|
74
|
+
|
|
27
75
|
const supportPreset = presets.length > 0;
|
|
28
76
|
const supportCustom = !!settings?.amount?.custom;
|
|
77
|
+
const defaultPreset = getDefaultPreset();
|
|
78
|
+
const defaultCustomAmount = defaultPreset === 'custom' ? getSavedCustomAmount() : '';
|
|
79
|
+
|
|
29
80
|
const [state, setState] = useSetState({
|
|
30
|
-
selected:
|
|
31
|
-
input:
|
|
32
|
-
custom: !supportPreset,
|
|
81
|
+
selected: defaultPreset === 'custom' ? '' : defaultPreset,
|
|
82
|
+
input: defaultCustomAmount,
|
|
83
|
+
custom: !supportPreset || defaultPreset === 'custom',
|
|
33
84
|
error: '',
|
|
34
85
|
});
|
|
86
|
+
|
|
35
87
|
const customInputRef = useRef<HTMLInputElement>(null);
|
|
88
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
89
|
+
|
|
90
|
+
const handleSelect = (amount: string) => {
|
|
91
|
+
setPayable(true);
|
|
92
|
+
setState({ selected: formatAmount(amount), custom: false, error: '' });
|
|
93
|
+
onChange({ priceId: item.price_id, amount: formatAmount(amount) });
|
|
94
|
+
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), formatAmount(amount));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleCustomSelect = () => {
|
|
98
|
+
setState({ custom: true, selected: '', error: '' });
|
|
99
|
+
const savedCustomAmount = getSavedCustomAmount();
|
|
100
|
+
if (savedCustomAmount) {
|
|
101
|
+
setState({ input: savedCustomAmount });
|
|
102
|
+
onChange({ priceId: item.price_id, amount: savedCustomAmount });
|
|
103
|
+
setPayable(true);
|
|
104
|
+
} else if (!state.input) {
|
|
105
|
+
setPayable(false);
|
|
106
|
+
}
|
|
107
|
+
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), 'custom');
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleTabSelect = (selectedItem: string | 'custom') => {
|
|
111
|
+
if (selectedItem === 'custom') {
|
|
112
|
+
handleCustomSelect();
|
|
113
|
+
} else {
|
|
114
|
+
handleSelect(selectedItem as string);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// 使用useTabNavigation进行键盘导航
|
|
119
|
+
const { handleKeyDown } = useTabNavigation(presets, handleTabSelect, {
|
|
120
|
+
includeCustom: supportCustom,
|
|
121
|
+
currentValue: state.custom ? undefined : state.selected,
|
|
122
|
+
isCustomSelected: state.custom,
|
|
123
|
+
enabled: true,
|
|
124
|
+
selector: '.tab-navigable-card button',
|
|
125
|
+
containerRef,
|
|
126
|
+
});
|
|
36
127
|
|
|
37
|
-
// Set default amount
|
|
38
128
|
useEffect(() => {
|
|
39
129
|
if (settings.amount.preset) {
|
|
40
130
|
setState({ selected: settings.amount.preset, custom: false });
|
|
41
131
|
onChange({ priceId: item.price_id, amount: settings.amount.preset });
|
|
42
132
|
} else if (settings.amount.presets && settings.amount.presets.length > 0) {
|
|
43
|
-
|
|
44
|
-
|
|
133
|
+
const isCustom = defaultPreset === 'custom';
|
|
134
|
+
setState({
|
|
135
|
+
selected: isCustom ? '' : defaultPreset,
|
|
136
|
+
custom: isCustom,
|
|
137
|
+
input: isCustom ? getSavedCustomAmount() : '',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!isCustom) {
|
|
141
|
+
onChange({ priceId: item.price_id, amount: defaultPreset });
|
|
142
|
+
} else if (defaultCustomAmount) {
|
|
143
|
+
onChange({ priceId: item.price_id, amount: defaultCustomAmount });
|
|
144
|
+
setPayable(true);
|
|
145
|
+
} else {
|
|
146
|
+
setPayable(false);
|
|
147
|
+
}
|
|
45
148
|
}
|
|
46
149
|
}, [settings.amount.preset, settings.amount.presets]); // eslint-disable-line
|
|
47
150
|
|
|
48
151
|
useEffect(() => {
|
|
152
|
+
if (containerRef.current) {
|
|
153
|
+
containerRef.current.focus();
|
|
154
|
+
}
|
|
49
155
|
if (state.custom) {
|
|
50
156
|
setTimeout(() => {
|
|
51
157
|
customInputRef.current?.focus();
|
|
@@ -53,12 +159,6 @@ export default function ProductDonation({
|
|
|
53
159
|
}
|
|
54
160
|
}, [state.custom]);
|
|
55
161
|
|
|
56
|
-
const handleSelect = (amount: string) => {
|
|
57
|
-
setPayable(true);
|
|
58
|
-
setState({ selected: amount, custom: false, error: '' });
|
|
59
|
-
onChange({ priceId: item.price_id, amount });
|
|
60
|
-
};
|
|
61
|
-
|
|
62
162
|
const handleInput = (event: any) => {
|
|
63
163
|
const { value } = event.target;
|
|
64
164
|
const min = parseFloat(settings.amount.minimum || '0');
|
|
@@ -78,69 +178,75 @@ export default function ProductDonation({
|
|
|
78
178
|
}
|
|
79
179
|
setPayable(true);
|
|
80
180
|
setState({ error: '', input: value });
|
|
81
|
-
onChange({ priceId: item.price_id, amount: value });
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const handleCustomSelect = () => {
|
|
85
|
-
setState({ custom: true, error: '' });
|
|
86
|
-
if (!state.input) {
|
|
87
|
-
setPayable(false);
|
|
88
|
-
}
|
|
181
|
+
onChange({ priceId: item.price_id, amount: formatAmount(value) });
|
|
182
|
+
localStorage.setItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE), formatAmount(value));
|
|
89
183
|
};
|
|
90
184
|
|
|
91
185
|
return (
|
|
92
|
-
<Box
|
|
186
|
+
<Box
|
|
187
|
+
ref={containerRef}
|
|
188
|
+
display="flex"
|
|
189
|
+
flexDirection="column"
|
|
190
|
+
alignItems="flex-start"
|
|
191
|
+
gap={1.5}
|
|
192
|
+
onKeyDown={handleKeyDown}
|
|
193
|
+
tabIndex={0}
|
|
194
|
+
sx={{ outline: 'none' }}>
|
|
93
195
|
{supportPreset && (
|
|
94
196
|
<Grid container spacing={2}>
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
197
|
+
{presets.map((amount) => (
|
|
198
|
+
<Grid item xs={6} sm={3} key={amount}>
|
|
199
|
+
<Card
|
|
200
|
+
variant="outlined"
|
|
201
|
+
className="tab-navigable-card"
|
|
202
|
+
sx={{
|
|
203
|
+
minWidth: 115,
|
|
204
|
+
textAlign: 'center',
|
|
205
|
+
transition: 'all 0.3s',
|
|
206
|
+
cursor: 'pointer',
|
|
207
|
+
'&:hover': {
|
|
208
|
+
transform: 'translateY(-4px)',
|
|
209
|
+
boxShadow: 3,
|
|
210
|
+
},
|
|
211
|
+
'.MuiCardActionArea-focusHighlight': {
|
|
212
|
+
backgroundColor: 'transparent',
|
|
213
|
+
},
|
|
214
|
+
height: '42px',
|
|
215
|
+
...(formatAmount(state.selected) === formatAmount(amount) && !state.custom
|
|
216
|
+
? { borderColor: 'primary.main', borderWidth: 1 }
|
|
217
|
+
: {}),
|
|
218
|
+
}}>
|
|
219
|
+
<CardActionArea
|
|
220
|
+
onClick={() => handleSelect(amount)}
|
|
221
|
+
tabIndex={0}
|
|
222
|
+
aria-selected={formatAmount(state.selected) === formatAmount(amount) && !state.custom}>
|
|
223
|
+
<Stack
|
|
224
|
+
direction="row"
|
|
225
|
+
sx={{ py: 1.5, px: 1.5 }}
|
|
226
|
+
spacing={0.5}
|
|
227
|
+
alignItems="center"
|
|
228
|
+
justifyContent="center">
|
|
229
|
+
<Avatar src={currency?.logo} sx={{ width: 16, height: 16, mr: 0.5 }} alt={currency?.symbol} />
|
|
230
|
+
<Typography
|
|
231
|
+
component="strong"
|
|
232
|
+
lineHeight={1}
|
|
233
|
+
variant="h3"
|
|
234
|
+
sx={{ fontVariantNumeric: 'tabular-nums', fontWeight: 400 }}>
|
|
235
|
+
{amount}
|
|
236
|
+
</Typography>
|
|
237
|
+
<Typography lineHeight={1} fontSize={14} color="text.secondary">
|
|
238
|
+
{currency?.symbol}
|
|
239
|
+
</Typography>
|
|
240
|
+
</Stack>
|
|
241
|
+
</CardActionArea>
|
|
242
|
+
</Card>
|
|
243
|
+
</Grid>
|
|
244
|
+
))}
|
|
139
245
|
{supportCustom && (
|
|
140
246
|
<Grid item xs={6} sm={3} key="custom">
|
|
141
247
|
<Card
|
|
142
|
-
key="custom"
|
|
143
248
|
variant="outlined"
|
|
249
|
+
className="tab-navigable-card"
|
|
144
250
|
sx={{
|
|
145
251
|
textAlign: 'center',
|
|
146
252
|
transition: 'all 0.3s',
|
|
@@ -149,10 +255,13 @@ export default function ProductDonation({
|
|
|
149
255
|
transform: 'translateY(-4px)',
|
|
150
256
|
boxShadow: 3,
|
|
151
257
|
},
|
|
258
|
+
'.MuiCardActionArea-focusHighlight': {
|
|
259
|
+
backgroundColor: 'transparent',
|
|
260
|
+
},
|
|
152
261
|
height: '42px',
|
|
153
262
|
...(state.custom ? { borderColor: 'primary.main', borderWidth: 1 } : {}),
|
|
154
263
|
}}>
|
|
155
|
-
<CardActionArea onClick={
|
|
264
|
+
<CardActionArea onClick={handleCustomSelect} tabIndex={0} aria-selected={state.custom}>
|
|
156
265
|
<Stack
|
|
157
266
|
direction="row"
|
|
158
267
|
sx={{ py: 1.5, px: 1.5 }}
|
|
@@ -179,7 +288,6 @@ export default function ProductDonation({
|
|
|
179
288
|
error={!!state.error}
|
|
180
289
|
helperText={state.error}
|
|
181
290
|
inputRef={customInputRef}
|
|
182
|
-
// eslint-disable-next-line react/jsx-no-duplicate-props
|
|
183
291
|
InputProps={{
|
|
184
292
|
endAdornment: (
|
|
185
293
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ ml: 1 }}>
|
|
@@ -190,7 +298,7 @@ export default function ProductDonation({
|
|
|
190
298
|
autoComplete: 'off',
|
|
191
299
|
}}
|
|
192
300
|
sx={{
|
|
193
|
-
mt:
|
|
301
|
+
mt: defaultPreset !== '0' ? 0 : 1,
|
|
194
302
|
}}
|
|
195
303
|
/>
|
|
196
304
|
)}
|