@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
|
@@ -409,6 +409,17 @@ function PaymentForm({
|
|
|
409
409
|
stripePaying: false
|
|
410
410
|
});
|
|
411
411
|
};
|
|
412
|
+
(0, _react.useEffect)(() => {
|
|
413
|
+
const handleKeyDown = e => {
|
|
414
|
+
if (e.key === "Enter" && !state.submitting && !state.paying && !state.stripePaying && quantityInventoryStatus && payable) {
|
|
415
|
+
onAction();
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
419
|
+
return () => {
|
|
420
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
421
|
+
};
|
|
422
|
+
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, payable]);
|
|
412
423
|
if (onlyShowBtn) {
|
|
413
424
|
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
414
425
|
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
@@ -12,6 +12,12 @@ var _react = require("react");
|
|
|
12
12
|
var _util = require("../libs/util");
|
|
13
13
|
var _payment = require("../contexts/payment");
|
|
14
14
|
var _scroll = require("../hooks/scroll");
|
|
15
|
+
var _keyboard = require("../hooks/keyboard");
|
|
16
|
+
const DONATION_PRESET_KEY_BASE = "payment-donation-preset";
|
|
17
|
+
const DONATION_CUSTOM_AMOUNT_KEY_BASE = "payment-donation-custom-amount";
|
|
18
|
+
const formatAmount = amount => {
|
|
19
|
+
return String(amount);
|
|
20
|
+
};
|
|
15
21
|
function ProductDonation({
|
|
16
22
|
item,
|
|
17
23
|
settings,
|
|
@@ -23,20 +29,109 @@ function ProductDonation({
|
|
|
23
29
|
locale
|
|
24
30
|
} = (0, _context.useLocaleContext)();
|
|
25
31
|
const {
|
|
26
|
-
setPayable
|
|
32
|
+
setPayable,
|
|
33
|
+
session
|
|
27
34
|
} = (0, _payment.usePaymentContext)();
|
|
28
35
|
(0, _scroll.usePreventWheel)();
|
|
29
|
-
const presets = settings?.amount?.presets || [];
|
|
30
|
-
const
|
|
36
|
+
const presets = (settings?.amount?.presets || []).map(formatAmount);
|
|
37
|
+
const getUserStorageKey = base => {
|
|
38
|
+
const userDid = session?.user?.did;
|
|
39
|
+
return userDid ? `${base}:${userDid}` : base;
|
|
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
|
+
const getDefaultPreset = () => {
|
|
50
|
+
if (settings?.amount?.preset) {
|
|
51
|
+
return formatAmount(settings.amount.preset);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const savedPreset = localStorage.getItem(getUserStorageKey(DONATION_PRESET_KEY_BASE));
|
|
55
|
+
if (savedPreset) {
|
|
56
|
+
if (presets.includes(formatAmount(savedPreset))) {
|
|
57
|
+
return formatAmount(savedPreset);
|
|
58
|
+
}
|
|
59
|
+
if (savedPreset === "custom" && supportCustom) {
|
|
60
|
+
return "custom";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.warn("Failed to access localStorage", e);
|
|
65
|
+
}
|
|
66
|
+
if (presets.length > 0) {
|
|
67
|
+
const middleIndex = Math.floor(presets.length / 2);
|
|
68
|
+
return presets[middleIndex] || presets[0];
|
|
69
|
+
}
|
|
70
|
+
return "0";
|
|
71
|
+
};
|
|
31
72
|
const supportPreset = presets.length > 0;
|
|
32
73
|
const supportCustom = !!settings?.amount?.custom;
|
|
74
|
+
const defaultPreset = getDefaultPreset();
|
|
75
|
+
const defaultCustomAmount = defaultPreset === "custom" ? getSavedCustomAmount() : "";
|
|
33
76
|
const [state, setState] = (0, _ahooks.useSetState)({
|
|
34
|
-
selected:
|
|
35
|
-
input:
|
|
36
|
-
custom: !supportPreset,
|
|
77
|
+
selected: defaultPreset === "custom" ? "" : defaultPreset,
|
|
78
|
+
input: defaultCustomAmount,
|
|
79
|
+
custom: !supportPreset || defaultPreset === "custom",
|
|
37
80
|
error: ""
|
|
38
81
|
});
|
|
39
82
|
const customInputRef = (0, _react.useRef)(null);
|
|
83
|
+
const containerRef = (0, _react.useRef)(null);
|
|
84
|
+
const handleSelect = amount => {
|
|
85
|
+
setPayable(true);
|
|
86
|
+
setState({
|
|
87
|
+
selected: formatAmount(amount),
|
|
88
|
+
custom: false,
|
|
89
|
+
error: ""
|
|
90
|
+
});
|
|
91
|
+
onChange({
|
|
92
|
+
priceId: item.price_id,
|
|
93
|
+
amount: formatAmount(amount)
|
|
94
|
+
});
|
|
95
|
+
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), formatAmount(amount));
|
|
96
|
+
};
|
|
97
|
+
const handleCustomSelect = () => {
|
|
98
|
+
setState({
|
|
99
|
+
custom: true,
|
|
100
|
+
selected: "",
|
|
101
|
+
error: ""
|
|
102
|
+
});
|
|
103
|
+
const savedCustomAmount = getSavedCustomAmount();
|
|
104
|
+
if (savedCustomAmount) {
|
|
105
|
+
setState({
|
|
106
|
+
input: savedCustomAmount
|
|
107
|
+
});
|
|
108
|
+
onChange({
|
|
109
|
+
priceId: item.price_id,
|
|
110
|
+
amount: savedCustomAmount
|
|
111
|
+
});
|
|
112
|
+
setPayable(true);
|
|
113
|
+
} else if (!state.input) {
|
|
114
|
+
setPayable(false);
|
|
115
|
+
}
|
|
116
|
+
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), "custom");
|
|
117
|
+
};
|
|
118
|
+
const handleTabSelect = selectedItem => {
|
|
119
|
+
if (selectedItem === "custom") {
|
|
120
|
+
handleCustomSelect();
|
|
121
|
+
} else {
|
|
122
|
+
handleSelect(selectedItem);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const {
|
|
126
|
+
handleKeyDown
|
|
127
|
+
} = (0, _keyboard.useTabNavigation)(presets, handleTabSelect, {
|
|
128
|
+
includeCustom: supportCustom,
|
|
129
|
+
currentValue: state.custom ? void 0 : state.selected,
|
|
130
|
+
isCustomSelected: state.custom,
|
|
131
|
+
enabled: true,
|
|
132
|
+
selector: ".tab-navigable-card button",
|
|
133
|
+
containerRef
|
|
134
|
+
});
|
|
40
135
|
(0, _react.useEffect)(() => {
|
|
41
136
|
if (settings.amount.preset) {
|
|
42
137
|
setState({
|
|
@@ -48,35 +143,38 @@ function ProductDonation({
|
|
|
48
143
|
amount: settings.amount.preset
|
|
49
144
|
});
|
|
50
145
|
} else if (settings.amount.presets && settings.amount.presets.length > 0) {
|
|
146
|
+
const isCustom = defaultPreset === "custom";
|
|
51
147
|
setState({
|
|
52
|
-
selected:
|
|
53
|
-
custom:
|
|
54
|
-
|
|
55
|
-
onChange({
|
|
56
|
-
priceId: item.price_id,
|
|
57
|
-
amount: settings.amount.presets[0]
|
|
148
|
+
selected: isCustom ? "" : defaultPreset,
|
|
149
|
+
custom: isCustom,
|
|
150
|
+
input: isCustom ? getSavedCustomAmount() : ""
|
|
58
151
|
});
|
|
152
|
+
if (!isCustom) {
|
|
153
|
+
onChange({
|
|
154
|
+
priceId: item.price_id,
|
|
155
|
+
amount: defaultPreset
|
|
156
|
+
});
|
|
157
|
+
} else if (defaultCustomAmount) {
|
|
158
|
+
onChange({
|
|
159
|
+
priceId: item.price_id,
|
|
160
|
+
amount: defaultCustomAmount
|
|
161
|
+
});
|
|
162
|
+
setPayable(true);
|
|
163
|
+
} else {
|
|
164
|
+
setPayable(false);
|
|
165
|
+
}
|
|
59
166
|
}
|
|
60
167
|
}, [settings.amount.preset, settings.amount.presets]);
|
|
61
168
|
(0, _react.useEffect)(() => {
|
|
169
|
+
if (containerRef.current) {
|
|
170
|
+
containerRef.current.focus();
|
|
171
|
+
}
|
|
62
172
|
if (state.custom) {
|
|
63
173
|
setTimeout(() => {
|
|
64
174
|
customInputRef.current?.focus();
|
|
65
175
|
}, 0);
|
|
66
176
|
}
|
|
67
177
|
}, [state.custom]);
|
|
68
|
-
const handleSelect = amount => {
|
|
69
|
-
setPayable(true);
|
|
70
|
-
setState({
|
|
71
|
-
selected: amount,
|
|
72
|
-
custom: false,
|
|
73
|
-
error: ""
|
|
74
|
-
});
|
|
75
|
-
onChange({
|
|
76
|
-
priceId: item.price_id,
|
|
77
|
-
amount
|
|
78
|
-
});
|
|
79
|
-
};
|
|
80
178
|
const handleInput = event => {
|
|
81
179
|
const {
|
|
82
180
|
value
|
|
@@ -110,32 +208,31 @@ function ProductDonation({
|
|
|
110
208
|
});
|
|
111
209
|
onChange({
|
|
112
210
|
priceId: item.price_id,
|
|
113
|
-
amount: value
|
|
211
|
+
amount: formatAmount(value)
|
|
114
212
|
});
|
|
115
|
-
|
|
116
|
-
const handleCustomSelect = () => {
|
|
117
|
-
setState({
|
|
118
|
-
custom: true,
|
|
119
|
-
error: ""
|
|
120
|
-
});
|
|
121
|
-
if (!state.input) {
|
|
122
|
-
setPayable(false);
|
|
123
|
-
}
|
|
213
|
+
localStorage.setItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE), formatAmount(value));
|
|
124
214
|
};
|
|
125
215
|
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
|
|
216
|
+
ref: containerRef,
|
|
126
217
|
display: "flex",
|
|
127
218
|
flexDirection: "column",
|
|
128
219
|
alignItems: "flex-start",
|
|
129
220
|
gap: 1.5,
|
|
221
|
+
onKeyDown: handleKeyDown,
|
|
222
|
+
tabIndex: 0,
|
|
223
|
+
sx: {
|
|
224
|
+
outline: "none"
|
|
225
|
+
},
|
|
130
226
|
children: [supportPreset && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Grid, {
|
|
131
227
|
container: true,
|
|
132
228
|
spacing: 2,
|
|
133
|
-
children: [
|
|
229
|
+
children: [presets.map(amount => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grid, {
|
|
134
230
|
item: true,
|
|
135
231
|
xs: 6,
|
|
136
232
|
sm: 3,
|
|
137
233
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Card, {
|
|
138
234
|
variant: "outlined",
|
|
235
|
+
className: "tab-navigable-card",
|
|
139
236
|
sx: {
|
|
140
237
|
minWidth: 115,
|
|
141
238
|
textAlign: "center",
|
|
@@ -145,14 +242,19 @@ function ProductDonation({
|
|
|
145
242
|
transform: "translateY(-4px)",
|
|
146
243
|
boxShadow: 3
|
|
147
244
|
},
|
|
245
|
+
".MuiCardActionArea-focusHighlight": {
|
|
246
|
+
backgroundColor: "transparent"
|
|
247
|
+
},
|
|
148
248
|
height: "42px",
|
|
149
|
-
...(state.selected === amount && !state.custom ? {
|
|
249
|
+
...(formatAmount(state.selected) === formatAmount(amount) && !state.custom ? {
|
|
150
250
|
borderColor: "primary.main",
|
|
151
251
|
borderWidth: 1
|
|
152
252
|
} : {})
|
|
153
253
|
},
|
|
154
254
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CardActionArea, {
|
|
155
255
|
onClick: () => handleSelect(amount),
|
|
256
|
+
tabIndex: 0,
|
|
257
|
+
"aria-selected": formatAmount(state.selected) === formatAmount(amount) && !state.custom,
|
|
156
258
|
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
157
259
|
direction: "row",
|
|
158
260
|
sx: {
|
|
@@ -187,13 +289,14 @@ function ProductDonation({
|
|
|
187
289
|
})]
|
|
188
290
|
})
|
|
189
291
|
})
|
|
190
|
-
}
|
|
292
|
+
})
|
|
191
293
|
}, amount)), supportCustom && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grid, {
|
|
192
294
|
item: true,
|
|
193
295
|
xs: 6,
|
|
194
296
|
sm: 3,
|
|
195
297
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Card, {
|
|
196
298
|
variant: "outlined",
|
|
299
|
+
className: "tab-navigable-card",
|
|
197
300
|
sx: {
|
|
198
301
|
textAlign: "center",
|
|
199
302
|
transition: "all 0.3s",
|
|
@@ -202,6 +305,9 @@ function ProductDonation({
|
|
|
202
305
|
transform: "translateY(-4px)",
|
|
203
306
|
boxShadow: 3
|
|
204
307
|
},
|
|
308
|
+
".MuiCardActionArea-focusHighlight": {
|
|
309
|
+
backgroundColor: "transparent"
|
|
310
|
+
},
|
|
205
311
|
height: "42px",
|
|
206
312
|
...(state.custom ? {
|
|
207
313
|
borderColor: "primary.main",
|
|
@@ -209,7 +315,9 @@ function ProductDonation({
|
|
|
209
315
|
} : {})
|
|
210
316
|
},
|
|
211
317
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CardActionArea, {
|
|
212
|
-
onClick:
|
|
318
|
+
onClick: handleCustomSelect,
|
|
319
|
+
tabIndex: 0,
|
|
320
|
+
"aria-selected": state.custom,
|
|
213
321
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
|
|
214
322
|
direction: "row",
|
|
215
323
|
sx: {
|
|
@@ -229,7 +337,7 @@ function ProductDonation({
|
|
|
229
337
|
})
|
|
230
338
|
})
|
|
231
339
|
})
|
|
232
|
-
}
|
|
340
|
+
})
|
|
233
341
|
}, "custom")]
|
|
234
342
|
}), state.custom && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.TextField, {
|
|
235
343
|
type: "number",
|
|
@@ -262,7 +370,7 @@ function ProductDonation({
|
|
|
262
370
|
autoComplete: "off"
|
|
263
371
|
},
|
|
264
372
|
sx: {
|
|
265
|
-
mt:
|
|
373
|
+
mt: defaultPreset !== "0" ? 0 : 1
|
|
266
374
|
}
|
|
267
375
|
})]
|
|
268
376
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.20",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@arcblock/did-connect": "^2.12.
|
|
58
|
-
"@arcblock/ux": "^2.12.
|
|
57
|
+
"@arcblock/did-connect": "^2.12.36",
|
|
58
|
+
"@arcblock/ux": "^2.12.36",
|
|
59
59
|
"@arcblock/ws": "^1.19.15",
|
|
60
|
-
"@blocklet/ui-react": "^2.12.
|
|
60
|
+
"@blocklet/ui-react": "^2.12.36",
|
|
61
61
|
"@mui/icons-material": "^5.16.6",
|
|
62
62
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
63
63
|
"@mui/material": "^5.16.6",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@babel/core": "^7.25.2",
|
|
94
94
|
"@babel/preset-env": "^7.25.2",
|
|
95
95
|
"@babel/preset-react": "^7.24.7",
|
|
96
|
-
"@blocklet/payment-types": "1.18.
|
|
96
|
+
"@blocklet/payment-types": "1.18.20",
|
|
97
97
|
"@storybook/addon-essentials": "^7.6.20",
|
|
98
98
|
"@storybook/addon-interactions": "^7.6.20",
|
|
99
99
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -124,5 +124,5 @@
|
|
|
124
124
|
"vite-plugin-babel": "^1.2.0",
|
|
125
125
|
"vite-plugin-node-polyfills": "^0.21.0"
|
|
126
126
|
},
|
|
127
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "f71a0013f704d58e4c04ebfcc5a35480a0942bcc"
|
|
128
128
|
}
|
|
@@ -16,8 +16,8 @@ export default function Livemode({ color, backgroundColor, sx }: Props) {
|
|
|
16
16
|
color="warning"
|
|
17
17
|
sx={{
|
|
18
18
|
ml: 2,
|
|
19
|
-
height:
|
|
20
|
-
lineHeight:
|
|
19
|
+
height: '18px',
|
|
20
|
+
lineHeight: '18px',
|
|
21
21
|
textTransform: 'uppercase',
|
|
22
22
|
fontSize: '0.7rem',
|
|
23
23
|
fontWeight: 'bold',
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, KeyboardEvent, useRef, RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
type TabNavigationOptions<T> = {
|
|
4
|
+
/** whether to include custom option as the last item */
|
|
5
|
+
includeCustom?: boolean;
|
|
6
|
+
/** the value or index of the current selected item */
|
|
7
|
+
currentValue?: T | number;
|
|
8
|
+
/** whether the current selected item is custom */
|
|
9
|
+
isCustomSelected?: boolean;
|
|
10
|
+
/** a function to compare values, used to determine the current selected item */
|
|
11
|
+
compareValue?: (item: T, value: any) => boolean;
|
|
12
|
+
/** whether to allow Tab key navigation */
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/** a selector to find navigable elements */
|
|
15
|
+
selector?: string;
|
|
16
|
+
/** an element container reference, limiting the query DOM range to improve performance */
|
|
17
|
+
containerRef?: RefObject<HTMLElement>;
|
|
18
|
+
/** the type of the current value, can be 'index' or 'value' */
|
|
19
|
+
valueType?: 'index' | 'value';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tab key navigation hook - implement Tab key circular navigation between a set of options
|
|
24
|
+
*
|
|
25
|
+
* @param items an array of options, can be a simple type (string, number) array or an object array
|
|
26
|
+
* @param onSelect callback when an item is selected
|
|
27
|
+
* @param options configuration options
|
|
28
|
+
* @returns an object containing the event handler and control functions
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // simple string array
|
|
32
|
+
* const { handleKeyDown } = useTabNavigation(['10', '20', '50'], handleSelect);
|
|
33
|
+
*
|
|
34
|
+
* // object array
|
|
35
|
+
* const { handleKeyDown } = useTabNavigation(
|
|
36
|
+
* [{id: 1, name: 'A'}, {id: 2, name: 'B'}],
|
|
37
|
+
* handleSelect,
|
|
38
|
+
* { compareValue: (item, value) => item.id === value.id }
|
|
39
|
+
* );
|
|
40
|
+
*/
|
|
41
|
+
export const useTabNavigation = <T>(
|
|
42
|
+
items: T[],
|
|
43
|
+
onSelect: (item: T | 'custom', index: number) => void,
|
|
44
|
+
options?: TabNavigationOptions<T>
|
|
45
|
+
) => {
|
|
46
|
+
const {
|
|
47
|
+
valueType = 'value',
|
|
48
|
+
includeCustom = false,
|
|
49
|
+
currentValue,
|
|
50
|
+
isCustomSelected = false,
|
|
51
|
+
compareValue = (item: T, value: any) => item === value,
|
|
52
|
+
enabled = true,
|
|
53
|
+
selector = '.tab-navigable-card button',
|
|
54
|
+
containerRef,
|
|
55
|
+
} = options || {};
|
|
56
|
+
|
|
57
|
+
const hasTabbed = useRef(false);
|
|
58
|
+
|
|
59
|
+
const findNavigableElements = useCallback(() => {
|
|
60
|
+
if (containerRef?.current) {
|
|
61
|
+
return containerRef.current.querySelectorAll(selector);
|
|
62
|
+
}
|
|
63
|
+
return document.querySelectorAll(selector);
|
|
64
|
+
}, [containerRef, selector]);
|
|
65
|
+
|
|
66
|
+
// get current index
|
|
67
|
+
const determineCurrentIndex = useCallback(() => {
|
|
68
|
+
const allOptions = includeCustom ? [...items, 'custom' as any] : items;
|
|
69
|
+
if (allOptions.length === 0) return -1;
|
|
70
|
+
|
|
71
|
+
// if not tabbed, determine start point by currentValue
|
|
72
|
+
if (!hasTabbed.current) {
|
|
73
|
+
if (isCustomSelected && includeCustom) {
|
|
74
|
+
return items.length; // current selected custom option
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (currentValue !== undefined) {
|
|
78
|
+
if (valueType === 'index' && typeof currentValue === 'number') {
|
|
79
|
+
// if currentValue is index
|
|
80
|
+
return currentValue >= 0 && currentValue < items.length ? currentValue : -1;
|
|
81
|
+
}
|
|
82
|
+
// if currentValue is value, find matched item
|
|
83
|
+
return items.findIndex((item) => compareValue(item, currentValue));
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// if tabbed, find current focused element
|
|
87
|
+
const focusedElement = document.activeElement;
|
|
88
|
+
const navigableElements = findNavigableElements();
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < navigableElements.length; i++) {
|
|
91
|
+
if (navigableElements[i] === focusedElement) {
|
|
92
|
+
return i;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return -1;
|
|
98
|
+
}, [items, includeCustom, isCustomSelected, currentValue, valueType, compareValue, findNavigableElements]);
|
|
99
|
+
|
|
100
|
+
// get next index
|
|
101
|
+
const getNextIndex = useCallback(
|
|
102
|
+
(currentIndex: number, isShiftKey: boolean) => {
|
|
103
|
+
const totalOptions = includeCustom ? items.length + 1 : items.length;
|
|
104
|
+
|
|
105
|
+
if (currentIndex === -1) {
|
|
106
|
+
return 0; // no current selected item, start from first item
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isShiftKey) {
|
|
110
|
+
// Shift+Tab forward
|
|
111
|
+
return currentIndex === 0 ? totalOptions - 1 : currentIndex - 1;
|
|
112
|
+
}
|
|
113
|
+
// Tab backward
|
|
114
|
+
return currentIndex === totalOptions - 1 ? 0 : currentIndex + 1;
|
|
115
|
+
},
|
|
116
|
+
[items, includeCustom]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const handleKeyDown = useCallback(
|
|
120
|
+
(e: KeyboardEvent) => {
|
|
121
|
+
// if navigation is disabled or not Tab key, do not handle event
|
|
122
|
+
if (!enabled || e.key !== 'Tab') return;
|
|
123
|
+
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
e.stopPropagation();
|
|
126
|
+
|
|
127
|
+
// determine current index and next index
|
|
128
|
+
const currentIndex = determineCurrentIndex();
|
|
129
|
+
const nextIndex = getNextIndex(currentIndex, e.shiftKey);
|
|
130
|
+
|
|
131
|
+
// mark as handled Tab event
|
|
132
|
+
hasTabbed.current = true;
|
|
133
|
+
|
|
134
|
+
// execute select callback
|
|
135
|
+
const selectedItem = nextIndex === items.length ? 'custom' : items[nextIndex];
|
|
136
|
+
onSelect(selectedItem, nextIndex);
|
|
137
|
+
|
|
138
|
+
// focus to next element
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
const elements = findNavigableElements();
|
|
141
|
+
if (elements[nextIndex]) {
|
|
142
|
+
(elements[nextIndex] as HTMLElement).focus();
|
|
143
|
+
}
|
|
144
|
+
}, 0);
|
|
145
|
+
},
|
|
146
|
+
[items, onSelect, enabled, determineCurrentIndex, getNextIndex, findNavigableElements]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// reset Tab state method
|
|
150
|
+
const resetTabNavigation = useCallback(() => {
|
|
151
|
+
hasTabbed.current = false;
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
handleKeyDown,
|
|
156
|
+
resetTabNavigation,
|
|
157
|
+
isTabNavigationActive: hasTabbed.current,
|
|
158
|
+
};
|
|
159
|
+
};
|
package/src/index.ts
CHANGED
package/src/locales/en.tsx
CHANGED
|
@@ -134,6 +134,7 @@ export default flat({
|
|
|
134
134
|
empty: 'No supporters yet',
|
|
135
135
|
gaveTips: '{count} people gave tips',
|
|
136
136
|
tipAmount: 'Tip Amount',
|
|
137
|
+
tabHint: 'to switch amount',
|
|
137
138
|
benefits: {
|
|
138
139
|
one: '{name} will receive all tips',
|
|
139
140
|
multiple: 'Tips will be distributed to {count} beneficiaries',
|
|
@@ -147,7 +148,7 @@ export default flat({
|
|
|
147
148
|
later: 'Configure Later',
|
|
148
149
|
configTip: 'Configure donation settings in Payment Kit',
|
|
149
150
|
},
|
|
150
|
-
cardPay: '{action} with card',
|
|
151
|
+
cardPay: '{action} with bank card',
|
|
151
152
|
empty: 'No thing to pay',
|
|
152
153
|
per: 'per',
|
|
153
154
|
pay: 'Pay {payee}',
|
|
@@ -164,7 +165,7 @@ export default flat({
|
|
|
164
165
|
payment: 'Thanks for your purchase',
|
|
165
166
|
subscription: 'Thanks for your subscribing',
|
|
166
167
|
setup: 'Thanks for your subscribing',
|
|
167
|
-
donate: 'Thanks for your
|
|
168
|
+
donate: 'Thanks for your tip',
|
|
168
169
|
tip: 'A payment to {payee} has been completed. You can view the details of this payment in your account.',
|
|
169
170
|
},
|
|
170
171
|
confirm: 'Confirming allows {payee} to charge or reduce your staking. You can cancel or revoke staking anytime.',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -133,6 +133,7 @@ export default flat({
|
|
|
133
133
|
empty: '❤️ 支持一下',
|
|
134
134
|
gaveTips: '已有 {count} 人打赏',
|
|
135
135
|
tipAmount: '打赏金额',
|
|
136
|
+
tabHint: '快速切换金额',
|
|
136
137
|
benefits: {
|
|
137
138
|
one: '{name} 将获得全部打赏',
|
|
138
139
|
multiple: '打赏将按比例分配给 {count} 位受益人',
|
|
@@ -146,7 +147,7 @@ export default flat({
|
|
|
146
147
|
later: '稍后配置',
|
|
147
148
|
configTip: '前往 Payment Kit 配置打赏选项',
|
|
148
149
|
},
|
|
149
|
-
cardPay: '
|
|
150
|
+
cardPay: '使用银行卡{action}',
|
|
150
151
|
empty: '没有可支付的项目',
|
|
151
152
|
per: '每',
|
|
152
153
|
pay: '付款给 {payee}',
|
|
@@ -74,6 +74,7 @@ function PaymentInner({
|
|
|
74
74
|
}: MainProps) {
|
|
75
75
|
const { t } = useLocaleContext();
|
|
76
76
|
const { settings, session } = usePaymentContext();
|
|
77
|
+
const { isMobile } = useMobile();
|
|
77
78
|
const [state, setState] = useSetState({
|
|
78
79
|
checkoutSession,
|
|
79
80
|
submitting: false,
|
|
@@ -238,17 +239,49 @@ function PaymentInner({
|
|
|
238
239
|
)}
|
|
239
240
|
|
|
240
241
|
<Stack sx={{ display: benefitsState.open ? 'none' : 'block' }}>
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
242
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
|
|
243
|
+
<Typography
|
|
244
|
+
title={t('payment.checkout.orderSummary')}
|
|
245
|
+
sx={{
|
|
246
|
+
color: 'text.primary',
|
|
247
|
+
fontSize: '18px',
|
|
248
|
+
fontWeight: '500',
|
|
249
|
+
lineHeight: '24px',
|
|
250
|
+
}}>
|
|
251
|
+
{t('payment.checkout.donation.tipAmount')}
|
|
252
|
+
</Typography>
|
|
253
|
+
|
|
254
|
+
{!isMobile && donationSettings?.amount?.presets && donationSettings.amount.presets.length > 0 && (
|
|
255
|
+
<Typography
|
|
256
|
+
sx={{
|
|
257
|
+
color: 'text.secondary',
|
|
258
|
+
fontSize: '13px',
|
|
259
|
+
display: 'flex',
|
|
260
|
+
alignItems: 'center',
|
|
261
|
+
gap: 0.5,
|
|
262
|
+
opacity: 0.8,
|
|
263
|
+
}}>
|
|
264
|
+
<Box
|
|
265
|
+
component="span"
|
|
266
|
+
sx={{
|
|
267
|
+
border: '1px solid',
|
|
268
|
+
borderColor: 'divider',
|
|
269
|
+
borderRadius: 0.75,
|
|
270
|
+
px: 0.75,
|
|
271
|
+
py: 0.25,
|
|
272
|
+
fontSize: '12px',
|
|
273
|
+
lineHeight: 1,
|
|
274
|
+
color: 'text.secondary',
|
|
275
|
+
fontWeight: '400',
|
|
276
|
+
bgcolor: 'transparent',
|
|
277
|
+
}}>
|
|
278
|
+
Tab
|
|
279
|
+
</Box>
|
|
280
|
+
{t('payment.checkout.donation.tabHint')}
|
|
281
|
+
</Typography>
|
|
282
|
+
)}
|
|
283
|
+
</Stack>
|
|
284
|
+
|
|
252
285
|
{items.map((x: TLineItemExpanded) => (
|
|
253
286
|
<ProductDonation
|
|
254
287
|
key={`${x.price_id}-${currency.id}`}
|
|
@@ -429,6 +429,26 @@ export default function PaymentForm({
|
|
|
429
429
|
setState({ stripePaying: false });
|
|
430
430
|
};
|
|
431
431
|
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
434
|
+
if (
|
|
435
|
+
e.key === 'Enter' &&
|
|
436
|
+
!state.submitting &&
|
|
437
|
+
!state.paying &&
|
|
438
|
+
!state.stripePaying &&
|
|
439
|
+
quantityInventoryStatus &&
|
|
440
|
+
payable
|
|
441
|
+
) {
|
|
442
|
+
onAction();
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
447
|
+
return () => {
|
|
448
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
449
|
+
};
|
|
450
|
+
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, payable]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
451
|
+
|
|
432
452
|
if (onlyShowBtn) {
|
|
433
453
|
return (
|
|
434
454
|
<>
|