@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.
@@ -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.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.26",
58
- "@arcblock/ux": "^2.12.26",
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.26",
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.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": "511a531693837c4e0784d4961f8755e5818a27d5"
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: 18,
20
- lineHeight: 1.2,
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
@@ -44,6 +44,7 @@ export * from './hooks/subscription';
44
44
  export * from './hooks/mobile';
45
45
  export * from './hooks/table';
46
46
  export * from './hooks/scroll';
47
+ export * from './hooks/keyboard';
47
48
 
48
49
  export { translations, createTranslator } from './locales';
49
50
 
@@ -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 support',
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.',
@@ -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: '使用卡片{action}',
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
- <Typography
242
- title={t('payment.checkout.orderSummary')}
243
- sx={{
244
- color: 'text.primary',
245
- fontSize: '18px',
246
- fontWeight: '500',
247
- lineHeight: '24px',
248
- mb: 2,
249
- }}>
250
- {t('payment.checkout.donation.tipAmount')}
251
- </Typography>
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
  <>