@blocklet/payment-react 1.18.17 → 1.18.19

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.
@@ -0,0 +1,44 @@
1
+ import { KeyboardEvent, RefObject } from 'react';
2
+ type TabNavigationOptions<T> = {
3
+ /** whether to include custom option as the last item */
4
+ includeCustom?: boolean;
5
+ /** the value or index of the current selected item */
6
+ currentValue?: T | number;
7
+ /** whether the current selected item is custom */
8
+ isCustomSelected?: boolean;
9
+ /** a function to compare values, used to determine the current selected item */
10
+ compareValue?: (item: T, value: any) => boolean;
11
+ /** whether to allow Tab key navigation */
12
+ enabled?: boolean;
13
+ /** a selector to find navigable elements */
14
+ selector?: string;
15
+ /** an element container reference, limiting the query DOM range to improve performance */
16
+ containerRef?: RefObject<HTMLElement>;
17
+ /** the type of the current value, can be 'index' or 'value' */
18
+ valueType?: 'index' | 'value';
19
+ };
20
+ /**
21
+ * Tab key navigation hook - implement Tab key circular navigation between a set of options
22
+ *
23
+ * @param items an array of options, can be a simple type (string, number) array or an object array
24
+ * @param onSelect callback when an item is selected
25
+ * @param options configuration options
26
+ * @returns an object containing the event handler and control functions
27
+ *
28
+ * @example
29
+ * // simple string array
30
+ * const { handleKeyDown } = useTabNavigation(['10', '20', '50'], handleSelect);
31
+ *
32
+ * // object array
33
+ * const { handleKeyDown } = useTabNavigation(
34
+ * [{id: 1, name: 'A'}, {id: 2, name: 'B'}],
35
+ * handleSelect,
36
+ * { compareValue: (item, value) => item.id === value.id }
37
+ * );
38
+ */
39
+ export declare const useTabNavigation: <T>(items: T[], onSelect: (item: T | "custom", index: number) => void, options?: TabNavigationOptions<T>) => {
40
+ handleKeyDown: (e: KeyboardEvent) => void;
41
+ resetTabNavigation: () => void;
42
+ isTabNavigationActive: boolean;
43
+ };
44
+ export {};
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useTabNavigation = void 0;
7
+ var _react = require("react");
8
+ const useTabNavigation = (items, onSelect, options) => {
9
+ const {
10
+ valueType = "value",
11
+ includeCustom = false,
12
+ currentValue,
13
+ isCustomSelected = false,
14
+ compareValue = (item, value) => item === value,
15
+ enabled = true,
16
+ selector = ".tab-navigable-card button",
17
+ containerRef
18
+ } = options || {};
19
+ const hasTabbed = (0, _react.useRef)(false);
20
+ const findNavigableElements = (0, _react.useCallback)(() => {
21
+ if (containerRef?.current) {
22
+ return containerRef.current.querySelectorAll(selector);
23
+ }
24
+ return document.querySelectorAll(selector);
25
+ }, [containerRef, selector]);
26
+ const determineCurrentIndex = (0, _react.useCallback)(() => {
27
+ const allOptions = includeCustom ? [...items, "custom"] : items;
28
+ if (allOptions.length === 0) return -1;
29
+ if (!hasTabbed.current) {
30
+ if (isCustomSelected && includeCustom) {
31
+ return items.length;
32
+ }
33
+ if (currentValue !== void 0) {
34
+ if (valueType === "index" && typeof currentValue === "number") {
35
+ return currentValue >= 0 && currentValue < items.length ? currentValue : -1;
36
+ }
37
+ return items.findIndex(item => compareValue(item, currentValue));
38
+ }
39
+ } else {
40
+ const focusedElement = document.activeElement;
41
+ const navigableElements = findNavigableElements();
42
+ for (let i = 0; i < navigableElements.length; i++) {
43
+ if (navigableElements[i] === focusedElement) {
44
+ return i;
45
+ }
46
+ }
47
+ }
48
+ return -1;
49
+ }, [items, includeCustom, isCustomSelected, currentValue, valueType, compareValue, findNavigableElements]);
50
+ const getNextIndex = (0, _react.useCallback)((currentIndex, isShiftKey) => {
51
+ const totalOptions = includeCustom ? items.length + 1 : items.length;
52
+ if (currentIndex === -1) {
53
+ return 0;
54
+ }
55
+ if (isShiftKey) {
56
+ return currentIndex === 0 ? totalOptions - 1 : currentIndex - 1;
57
+ }
58
+ return currentIndex === totalOptions - 1 ? 0 : currentIndex + 1;
59
+ }, [items, includeCustom]);
60
+ const handleKeyDown = (0, _react.useCallback)(e => {
61
+ if (!enabled || e.key !== "Tab") return;
62
+ e.preventDefault();
63
+ e.stopPropagation();
64
+ const currentIndex = determineCurrentIndex();
65
+ const nextIndex = getNextIndex(currentIndex, e.shiftKey);
66
+ hasTabbed.current = true;
67
+ const selectedItem = nextIndex === items.length ? "custom" : items[nextIndex];
68
+ onSelect(selectedItem, nextIndex);
69
+ setTimeout(() => {
70
+ const elements = findNavigableElements();
71
+ if (elements[nextIndex]) {
72
+ elements[nextIndex].focus();
73
+ }
74
+ }, 0);
75
+ }, [items, onSelect, enabled, determineCurrentIndex, getNextIndex, findNavigableElements]);
76
+ const resetTabNavigation = (0, _react.useCallback)(() => {
77
+ hasTabbed.current = false;
78
+ }, []);
79
+ return {
80
+ handleKeyDown,
81
+ resetTabNavigation,
82
+ isTabNavigationActive: hasTabbed.current
83
+ };
84
+ };
85
+ exports.useTabNavigation = useTabNavigation;
package/lib/index.d.ts CHANGED
@@ -42,5 +42,6 @@ export * from './hooks/subscription';
42
42
  export * from './hooks/mobile';
43
43
  export * from './hooks/table';
44
44
  export * from './hooks/scroll';
45
+ export * from './hooks/keyboard';
45
46
  export { translations, createTranslator } from './locales';
46
47
  export { createLazyComponent, api, dayjs, FormInput, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, };
package/lib/index.js CHANGED
@@ -418,6 +418,18 @@ Object.keys(_scroll).forEach(function (key) {
418
418
  }
419
419
  });
420
420
  });
421
+ var _keyboard = require("./hooks/keyboard");
422
+ Object.keys(_keyboard).forEach(function (key) {
423
+ if (key === "default" || key === "__esModule") return;
424
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
425
+ if (key in exports && exports[key] === _keyboard[key]) return;
426
+ Object.defineProperty(exports, key, {
427
+ enumerable: true,
428
+ get: function () {
429
+ return _keyboard[key];
430
+ }
431
+ });
432
+ });
421
433
  var _locales = require("./locales");
422
434
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
423
435
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
package/lib/locales/en.js CHANGED
@@ -168,7 +168,7 @@ module.exports = (0, _flat.default)({
168
168
  payment: "Thanks for your purchase",
169
169
  subscription: "Thanks for your subscribing",
170
170
  setup: "Thanks for your subscribing",
171
- donate: "Thanks for your support",
171
+ donate: "Thanks for your tip",
172
172
  tip: "A payment to {payee} has been completed. You can view the details of this payment in your account."
173
173
  },
174
174
  confirm: "Confirming allows {payee} to charge or reduce your staking. You can cancel or revoke staking anytime.",
@@ -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 preset = settings?.amount?.preset || presets?.[0] || "0";
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: preset,
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: settings.amount.presets[0],
53
- custom: false
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: [settings.amount.presets && settings.amount.presets.length > 0 && settings.amount.presets.map(amount => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grid, {
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
- }, amount)
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: () => handleCustomSelect(),
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
- }, "custom")
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: preset !== "0" ? 0 : 1
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.17",
3
+ "version": "1.18.19",
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.25",
58
- "@arcblock/ux": "^2.12.25",
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.25",
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.17",
96
+ "@blocklet/payment-types": "1.18.19",
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": "9d43381dd3744506455547260f5e621c6ad88e8f"
127
+ "gitHead": "eabd58598ad349a5177dca2e6302c82117d9b687"
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: 18,
20
- lineHeight: 1.2,
19
+ height: '18px',
20
+ lineHeight: '18px',
21
21
  textTransform: 'uppercase',
22
22
  fontSize: '0.7rem',
23
23
  fontWeight: 'bold',