@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.
@@ -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
- const preset = settings?.amount?.preset || presets?.[0] || '0';
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: preset,
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
- setState({ selected: settings.amount.presets[0], custom: false });
44
- onChange({ priceId: item.price_id, amount: settings.amount.presets[0] });
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 display="flex" flexDirection="column" alignItems="flex-start" gap={1.5}>
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
- {settings.amount.presets &&
96
- settings.amount.presets.length > 0 &&
97
- settings.amount.presets.map((amount) => (
98
- <Grid item xs={6} sm={3} key={amount}>
99
- <Card
100
- key={amount}
101
- variant="outlined"
102
- sx={{
103
- minWidth: 115,
104
- textAlign: 'center',
105
- transition: 'all 0.3s',
106
- cursor: 'pointer',
107
- '&:hover': {
108
- transform: 'translateY(-4px)',
109
- boxShadow: 3,
110
- },
111
- height: '42px',
112
- ...(state.selected === amount && !state.custom
113
- ? { borderColor: 'primary.main', borderWidth: 1 }
114
- : {}),
115
- }}>
116
- <CardActionArea onClick={() => handleSelect(amount)}>
117
- <Stack
118
- direction="row"
119
- sx={{ py: 1.5, px: 1.5 }}
120
- spacing={0.5}
121
- alignItems="center"
122
- justifyContent="center">
123
- <Avatar src={currency?.logo} sx={{ width: 16, height: 16, mr: 0.5 }} alt={currency?.symbol} />
124
- <Typography
125
- component="strong"
126
- lineHeight={1}
127
- variant="h3"
128
- sx={{ fontVariantNumeric: 'tabular-nums', fontWeight: 400 }}>
129
- {amount}
130
- </Typography>
131
- <Typography lineHeight={1} fontSize={14} color="text.secondary">
132
- {currency?.symbol}
133
- </Typography>
134
- </Stack>
135
- </CardActionArea>
136
- </Card>
137
- </Grid>
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={() => handleCustomSelect()}>
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: preset !== '0' ? 0 : 1,
301
+ mt: defaultPreset !== '0' ? 0 : 1,
194
302
  }}
195
303
  />
196
304
  )}