@blocklet/payment-react 1.19.18 → 1.19.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.
Files changed (55) hide show
  1. package/README.md +313 -0
  2. package/es/checkout/form.js +18 -2
  3. package/es/components/auto-topup/index.d.ts +14 -0
  4. package/es/components/auto-topup/index.js +417 -0
  5. package/es/components/auto-topup/modal.d.ts +35 -0
  6. package/es/components/auto-topup/modal.js +734 -0
  7. package/es/components/auto-topup/product-card.d.ts +13 -0
  8. package/es/components/auto-topup/product-card.js +173 -0
  9. package/es/components/collapse.d.ts +13 -0
  10. package/es/components/collapse.js +76 -0
  11. package/es/components/input.d.ts +2 -1
  12. package/es/components/input.js +64 -13
  13. package/es/components/label.d.ts +2 -1
  14. package/es/components/label.js +2 -1
  15. package/es/index.d.ts +4 -1
  16. package/es/index.js +7 -1
  17. package/es/libs/util.js +2 -1
  18. package/es/locales/en.js +56 -0
  19. package/es/locales/zh.js +56 -0
  20. package/es/payment/form/index.js +17 -1
  21. package/es/payment/product-item.js +17 -10
  22. package/lib/checkout/form.js +18 -2
  23. package/lib/components/auto-topup/index.d.ts +14 -0
  24. package/lib/components/auto-topup/index.js +451 -0
  25. package/lib/components/auto-topup/modal.d.ts +35 -0
  26. package/lib/components/auto-topup/modal.js +803 -0
  27. package/lib/components/auto-topup/product-card.d.ts +13 -0
  28. package/lib/components/auto-topup/product-card.js +149 -0
  29. package/lib/components/collapse.d.ts +13 -0
  30. package/lib/components/collapse.js +74 -0
  31. package/lib/components/input.d.ts +2 -1
  32. package/lib/components/input.js +66 -24
  33. package/lib/components/label.d.ts +2 -1
  34. package/lib/components/label.js +3 -1
  35. package/lib/index.d.ts +4 -1
  36. package/lib/index.js +24 -0
  37. package/lib/libs/util.js +2 -1
  38. package/lib/locales/en.js +56 -0
  39. package/lib/locales/zh.js +56 -0
  40. package/lib/payment/form/index.js +17 -1
  41. package/lib/payment/product-item.js +18 -10
  42. package/package.json +9 -9
  43. package/src/checkout/form.tsx +21 -2
  44. package/src/components/auto-topup/index.tsx +449 -0
  45. package/src/components/auto-topup/modal.tsx +773 -0
  46. package/src/components/auto-topup/product-card.tsx +156 -0
  47. package/src/components/collapse.tsx +82 -0
  48. package/src/components/input.tsx +71 -22
  49. package/src/components/label.tsx +8 -2
  50. package/src/index.ts +7 -0
  51. package/src/libs/util.ts +1 -0
  52. package/src/locales/en.tsx +59 -0
  53. package/src/locales/zh.tsx +57 -0
  54. package/src/payment/form/index.tsx +19 -1
  55. package/src/payment/product-item.tsx +18 -11
@@ -21,6 +21,7 @@ var _util = require("@ocap/util");
21
21
  var _DID = _interopRequireDefault(require("@arcblock/ux/lib/DID"));
22
22
  var _isEmpty = _interopRequireDefault(require("lodash/isEmpty"));
23
23
  var _iconsMaterial = require("@mui/icons-material");
24
+ var _withTracker = require("@arcblock/ux/lib/withTracker");
24
25
  var _input = _interopRequireDefault(require("../../components/input"));
25
26
  var _label = _interopRequireDefault(require("../../components/label"));
26
27
  var _payment = require("../../contexts/payment");
@@ -136,6 +137,7 @@ function PaymentForm({
136
137
  trigger
137
138
  } = (0, _reactHookForm.useFormContext)();
138
139
  const errorRef = (0, _react.useRef)(null);
140
+ const processingRef = (0, _react.useRef)(false);
139
141
  const quantityInventoryStatus = (0, _react.useMemo)(() => {
140
142
  let status = true;
141
143
  for (const item of checkoutSession.line_items) {
@@ -287,6 +289,10 @@ function PaymentForm({
287
289
  return true;
288
290
  }, [session?.user, method, checkoutSession]);
289
291
  const handleConnected = async () => {
292
+ if (processingRef.current) {
293
+ return;
294
+ }
295
+ processingRef.current = true;
290
296
  setState({
291
297
  paying: true
292
298
  });
@@ -300,11 +306,21 @@ function PaymentForm({
300
306
  onPaid(result);
301
307
  }
302
308
  } catch (err) {
303
- _Toast.default.error((0, _util2.formatError)(err));
309
+ const errorMessage = (0, _util2.formatError)(err);
310
+ const payFailedEvent = {
311
+ action: "payFailed",
312
+ // @ts-ignore 后续升级的话就会报错了,移除这个 lint 即可
313
+ mode: checkoutSession.mode,
314
+ errorMessage,
315
+ success: false
316
+ };
317
+ _withTracker.ReactGA.event(payFailedEvent.action, payFailedEvent);
318
+ _Toast.default.error(errorMessage);
304
319
  } finally {
305
320
  setState({
306
321
  paying: false
307
322
  });
323
+ processingRef.current = false;
308
324
  }
309
325
  };
310
326
  (0, _react.useEffect)(() => {
@@ -37,7 +37,8 @@ function ProductItem({
37
37
  locale
38
38
  } = (0, _context.useLocaleContext)();
39
39
  const {
40
- settings
40
+ settings,
41
+ setPayable
41
42
  } = (0, _payment.usePaymentContext)();
42
43
  const pricing = (0, _util.formatLineItemPricing)(item, currency, {
43
44
  trialEnd,
@@ -56,7 +57,14 @@ function ProductItem({
56
57
  const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
57
58
  const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
58
59
  const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity;
60
+ const localQuantityNum = localQuantity || 0;
59
61
  const handleQuantityChange = newQuantity => {
62
+ if (!newQuantity) {
63
+ setLocalQuantity(void 0);
64
+ setPayable(false);
65
+ return;
66
+ }
67
+ setPayable(true);
60
68
  if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
61
69
  if ((0, _util.formatQuantityInventory)(item.price, newQuantity, locale)) {
62
70
  return;
@@ -66,17 +74,17 @@ function ProductItem({
66
74
  }
67
75
  };
68
76
  const handleQuantityIncrease = () => {
69
- if (localQuantity < maxQuantity) {
70
- handleQuantityChange(localQuantity + 1);
77
+ if (localQuantityNum < maxQuantity) {
78
+ handleQuantityChange(localQuantityNum + 1);
71
79
  }
72
80
  };
73
81
  const handleQuantityDecrease = () => {
74
- if (localQuantity > minQuantity) {
75
- handleQuantityChange(localQuantity - 1);
82
+ if (localQuantityNum > minQuantity) {
83
+ handleQuantityChange(localQuantityNum - 1);
76
84
  }
77
85
  };
78
86
  const handleQuantityInputChange = event => {
79
- const value = parseInt(event.target.value, 10);
87
+ const value = parseInt(event.target.value || "0", 10);
80
88
  if (!Number.isNaN(value)) {
81
89
  handleQuantityChange(value);
82
90
  }
@@ -84,7 +92,7 @@ function ProductItem({
84
92
  const formatCreditInfo = () => {
85
93
  if (!isCreditProduct) return null;
86
94
  const isRecurring = item.price.type === "recurring";
87
- const totalCredit = (0, _util.formatNumber)(creditAmount * localQuantity);
95
+ const totalCredit = (0, _util.formatNumber)(creditAmount * (localQuantity || 0));
88
96
  let message = "";
89
97
  if (isRecurring) {
90
98
  message = t("payment.checkout.credit.recurringInfo", {
@@ -114,7 +122,7 @@ function ProductItem({
114
122
  }
115
123
  return pricing.primary;
116
124
  }, [trialInDays, trialEnd, pricing, item, locale]);
117
- const quantityInventoryError = (0, _util.formatQuantityInventory)(item.price, localQuantity, locale);
125
+ const quantityInventoryError = (0, _util.formatQuantityInventory)(item.price, localQuantityNum, locale);
118
126
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
119
127
  direction: "column",
120
128
  spacing: 1,
@@ -203,7 +211,7 @@ function ProductItem({
203
211
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
204
212
  size: "small",
205
213
  onClick: handleQuantityDecrease,
206
- disabled: localQuantity <= minQuantity,
214
+ disabled: localQuantityNum <= minQuantity,
207
215
  sx: {
208
216
  minWidth: 32,
209
217
  width: 32,
@@ -231,7 +239,7 @@ function ProductItem({
231
239
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
232
240
  size: "small",
233
241
  onClick: handleQuantityIncrease,
234
- disabled: localQuantity >= maxQuantity,
242
+ disabled: localQuantityNum >= maxQuantity,
235
243
  sx: {
236
244
  minWidth: 32,
237
245
  width: 32,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.19.18",
3
+ "version": "1.19.20",
4
4
  "description": "Reusable react components for payment kit v2",
5
5
  "keywords": [
6
6
  "react",
@@ -54,16 +54,16 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@arcblock/did-connect-react": "^3.1.18",
58
- "@arcblock/ux": "^3.1.18",
59
- "@arcblock/ws": "^1.21.3",
60
- "@blocklet/theme": "^3.1.18",
61
- "@blocklet/ui-react": "^3.1.18",
57
+ "@arcblock/did-connect-react": "^3.1.31",
58
+ "@arcblock/ux": "^3.1.31",
59
+ "@arcblock/ws": "^1.23.1",
60
+ "@blocklet/theme": "^3.1.31",
61
+ "@blocklet/ui-react": "^3.1.31",
62
62
  "@mui/icons-material": "^7.1.2",
63
63
  "@mui/lab": "7.0.0-beta.14",
64
64
  "@mui/material": "^7.1.2",
65
65
  "@mui/system": "^7.1.1",
66
- "@ocap/util": "^1.21.3",
66
+ "@ocap/util": "^1.23.1",
67
67
  "@stripe/react-stripe-js": "^2.9.0",
68
68
  "@stripe/stripe-js": "^2.4.0",
69
69
  "@vitejs/plugin-legacy": "^7.0.0",
@@ -94,7 +94,7 @@
94
94
  "@babel/core": "^7.27.4",
95
95
  "@babel/preset-env": "^7.27.2",
96
96
  "@babel/preset-react": "^7.27.1",
97
- "@blocklet/payment-types": "1.19.18",
97
+ "@blocklet/payment-types": "1.19.20",
98
98
  "@storybook/addon-essentials": "^7.6.20",
99
99
  "@storybook/addon-interactions": "^7.6.20",
100
100
  "@storybook/addon-links": "^7.6.20",
@@ -125,5 +125,5 @@
125
125
  "vite-plugin-babel": "^1.3.1",
126
126
  "vite-plugin-node-polyfills": "^0.23.0"
127
127
  },
128
- "gitHead": "b57baf21f22ae453247bc31444673aa01e35e6dc"
128
+ "gitHead": "4e09a5936748fc15c1ab6ea90b741b3e44f5eab7"
129
129
  }
@@ -4,6 +4,8 @@ import noop from 'lodash/noop';
4
4
  import { useEffect } from 'react';
5
5
  import { joinURL } from 'ufo';
6
6
 
7
+ import { ReactGA } from '@arcblock/ux/lib/withTracker';
8
+ import type { PayFailedEvent, PaySuccessEvent } from '@arcblock/ux/lib/withTracker/action/pay';
7
9
  import api from '../libs/api';
8
10
  import { getPrefix, mergeExtraParams } from '../libs/util';
9
11
  import Payment from '../payment';
@@ -67,15 +69,32 @@ export default function CheckoutForm({
67
69
  }
68
70
  }, [type, mode, data, extraParams]);
69
71
 
70
- const handlePaid = () => {
72
+ const handlePaid = (result: CheckoutContext) => {
71
73
  setState({ completed: true });
72
- onPaid?.(data as CheckoutContext);
74
+ onPaid?.(result as CheckoutContext);
75
+
76
+ const paySuccessEvent: PaySuccessEvent = {
77
+ action: 'paySuccess',
78
+ // @ts-ignore 后续升级的话就会报错了,移除这个 lint 即可
79
+ mode: data?.checkoutSession?.mode!,
80
+ success: true,
81
+ };
82
+ ReactGA.event(paySuccessEvent.action, paySuccessEvent);
73
83
  };
74
84
 
75
85
  const handleError = (err: any) => {
76
86
  console.error(err);
77
87
  setState({ appError: err });
78
88
  onError?.(err);
89
+
90
+ const payFailedEvent: PayFailedEvent = {
91
+ action: 'payFailed',
92
+ // @ts-ignore后续升级的话就会报错了,移除这个 lint 即可
93
+ mode: data?.checkoutSession?.mode!,
94
+ errorMessage: err.message,
95
+ success: false,
96
+ };
97
+ ReactGA.event(payFailedEvent.action, payFailedEvent);
79
98
  };
80
99
 
81
100
  const Checkout =
@@ -0,0 +1,449 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ Box,
4
+ Typography,
5
+ Stack,
6
+ Button,
7
+ CircularProgress,
8
+ Card,
9
+ CardContent,
10
+ IconButton,
11
+ Tooltip,
12
+ SxProps,
13
+ Collapse,
14
+ } from '@mui/material';
15
+ import { AddOutlined, CreditCard, SettingsOutlined, AccountBalanceWalletOutlined } from '@mui/icons-material';
16
+
17
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
18
+ // eslint-disable-next-line import/no-extraneous-dependencies
19
+ import { useNavigate } from 'react-router-dom';
20
+ import { joinURL } from 'ufo';
21
+
22
+ import type { AutoRechargeConfig } from '@blocklet/payment-types';
23
+ import { useRequest } from 'ahooks';
24
+
25
+ import { getPrefix, formatBNStr, formatNumber, formatPrice } from '../../libs/util';
26
+ import { createLink, handleNavigation } from '../../libs/navigation';
27
+ import { usePaymentContext } from '../../contexts/payment';
28
+ import api from '../../libs/api';
29
+ import AutoTopupModal from './modal';
30
+
31
+ export interface AutoTopupCardProps {
32
+ currencyId: string;
33
+ onConfigChange?: (config: AutoRechargeConfig) => void;
34
+ sx?: SxProps;
35
+ // 渲染模式: default=完整显示, simple=默认收起支持展开, custom=自定义渲染
36
+ mode?: 'default' | 'simple' | 'custom';
37
+ // 自定义渲染函数(custom模式下使用)
38
+ children?: (
39
+ openModal: () => void,
40
+ config: AutoRechargeConfig | null,
41
+ paymentData: { paymentInfo: any; balanceInfo: any } | null,
42
+ loading: boolean
43
+ ) => React.ReactNode;
44
+ }
45
+
46
+ const fetchConfig = async (customerId: string, currencyId: string) => {
47
+ const { data } = await api.get(`/api/auto-recharge-configs/customer/${customerId}`, {
48
+ params: { currency_id: currencyId },
49
+ });
50
+ return data;
51
+ };
52
+
53
+ const fetchCurrencyBalance = async (currencyId: string, payerAddress: string) => {
54
+ const { data } = await api.get('/api/customers/payer-token', {
55
+ params: { currencyId, payerAddress },
56
+ });
57
+ return data;
58
+ };
59
+
60
+ const cardStyle = {
61
+ height: '100%',
62
+ width: '100%',
63
+ border: '1px solid',
64
+ borderColor: 'divider',
65
+ boxShadow: 1,
66
+ borderRadius: 1,
67
+ backgroundColor: 'background.default',
68
+ };
69
+
70
+ export default function AutoTopupCard({
71
+ currencyId,
72
+ onConfigChange = () => {},
73
+ sx = {},
74
+ mode = 'default',
75
+ children = undefined,
76
+ }: AutoTopupCardProps) {
77
+ const { t } = useLocaleContext();
78
+ const navigate = useNavigate();
79
+ const { session } = usePaymentContext();
80
+ const [modalOpen, setModalOpen] = useState(false);
81
+ const [paymentData, setPaymentData] = useState<{ paymentInfo: any; balanceInfo: any } | null>(null);
82
+ const [quickSetupMode, setQuickSetupMode] = useState(false); // 是否是快速设置模式
83
+ // simple模式默认收起,default模式默认展开
84
+ const [expanded, setExpanded] = useState(mode === 'default');
85
+
86
+ const customerId = session?.user?.did || '';
87
+
88
+ const {
89
+ data: config,
90
+ loading,
91
+ refresh,
92
+ } = useRequest(() => fetchConfig(customerId, currencyId), {
93
+ refreshDeps: [customerId, currencyId],
94
+ ready: !!customerId && !!currencyId,
95
+ onSuccess: (data) => {
96
+ loadPaymentInfo(data);
97
+ },
98
+ });
99
+
100
+ const loadPaymentInfo = useCallback(async (data: any) => {
101
+ if (!data?.recharge_currency_id) return;
102
+
103
+ try {
104
+ const paymentMethodType = data?.paymentMethod?.type;
105
+ const paymentInfo = data?.payment_settings?.payment_method_options?.[paymentMethodType];
106
+ const balanceInfo =
107
+ paymentInfo?.payer && paymentMethodType !== 'stripe'
108
+ ? await fetchCurrencyBalance(data.recharge_currency_id, paymentInfo.payer as string)
109
+ : null;
110
+
111
+ setPaymentData({
112
+ paymentInfo,
113
+ balanceInfo,
114
+ });
115
+ } catch (error) {
116
+ console.error('Failed to load payment info:', error);
117
+ }
118
+ }, []);
119
+
120
+ const handleRecharge = (e: React.MouseEvent) => {
121
+ if (!paymentData?.paymentInfo?.payer) return;
122
+ const url = joinURL(
123
+ getPrefix(),
124
+ `/customer/recharge/${config?.recharge_currency_id}?rechargeAddress=${paymentData.paymentInfo.payer}`
125
+ );
126
+ const link = createLink(url, true);
127
+ handleNavigation(e, link, navigate);
128
+ };
129
+
130
+ const handleConfigSuccess = (newConfig: AutoRechargeConfig) => {
131
+ refresh();
132
+ onConfigChange?.(newConfig);
133
+ setModalOpen(false);
134
+ setQuickSetupMode(false); // 重置快速设置模式
135
+ };
136
+
137
+ const handleToggleExpanded = () => {
138
+ setExpanded(!expanded);
139
+ };
140
+
141
+ if (loading) {
142
+ return (
143
+ <Card sx={{ ...cardStyle, ...sx }}>
144
+ <CardContent>
145
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 80 }}>
146
+ <CircularProgress size={24} />
147
+ </Box>
148
+ </CardContent>
149
+ </Card>
150
+ );
151
+ }
152
+
153
+ if (!config) {
154
+ return null;
155
+ }
156
+
157
+ const renderPurchaseDetails = () => {
158
+ const { paymentInfo, balanceInfo } = paymentData || {};
159
+
160
+ if (!paymentInfo) {
161
+ return (
162
+ <Typography
163
+ variant="body2"
164
+ sx={{
165
+ color: 'text.secondary',
166
+ }}>
167
+ {t('payment.autoTopup.notConfigured')}
168
+ </Typography>
169
+ );
170
+ }
171
+
172
+ const purchaseAmount = formatPrice(
173
+ config.price,
174
+ config.rechargeCurrency,
175
+ config.price.product?.unit_label,
176
+ config.quantity,
177
+ true
178
+ );
179
+
180
+ if (config?.paymentMethod?.type === 'stripe') {
181
+ const cardBrand =
182
+ (paymentInfo?.card_brand || 'Card').charAt(0).toUpperCase() +
183
+ (paymentInfo?.card_brand || 'Card').slice(1).toLowerCase();
184
+ const last4 = paymentInfo?.card_last4;
185
+
186
+ return (
187
+ <Stack spacing={1}>
188
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
189
+ <Typography
190
+ variant="body2"
191
+ sx={{
192
+ color: 'text.secondary',
193
+ }}>
194
+ {t('payment.autoTopup.purchaseAmount')}:
195
+ </Typography>
196
+ <Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
197
+ {purchaseAmount}
198
+ </Typography>
199
+ </Box>
200
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
201
+ <Typography
202
+ variant="body2"
203
+ sx={{
204
+ color: 'text.secondary',
205
+ }}>
206
+ {t('payment.autoTopup.paymentMethod')}:
207
+ </Typography>
208
+ <Stack
209
+ direction="row"
210
+ spacing={1}
211
+ sx={{
212
+ alignItems: 'center',
213
+ }}>
214
+ <CreditCard fontSize="small" sx={{ color: 'text.secondary' }} />
215
+ <Typography variant="body2" sx={{ color: 'text.primary', fontWeight: 500 }}>
216
+ {cardBrand}({last4})
217
+ </Typography>
218
+ </Stack>
219
+ </Box>
220
+ </Stack>
221
+ );
222
+ }
223
+
224
+ return (
225
+ <Stack spacing={1}>
226
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
227
+ <Typography
228
+ variant="body2"
229
+ sx={{
230
+ color: 'text.secondary',
231
+ }}>
232
+ {t('payment.autoTopup.purchaseAmount')}:
233
+ </Typography>
234
+ <Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
235
+ {purchaseAmount}
236
+ </Typography>
237
+ </Box>
238
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
239
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
240
+ <Typography
241
+ variant="body2"
242
+ sx={{
243
+ color: 'text.secondary',
244
+ }}>
245
+ {t('payment.autoTopup.walletBalance')}:
246
+ </Typography>
247
+ <Tooltip
248
+ title={paymentInfo?.payer ? `${t('payment.autoTopup.paymentAddress')}: ${paymentInfo.payer}` : ''}
249
+ placement="top">
250
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
251
+ <AccountBalanceWalletOutlined sx={{ fontSize: 16, color: 'text.secondary' }} />
252
+ <Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
253
+ {balanceInfo
254
+ ? `${formatBNStr(balanceInfo?.token || '0', config?.rechargeCurrency?.decimal || 18)} ${config?.rechargeCurrency?.symbol || ''}`
255
+ : '--'}
256
+ </Typography>
257
+ </Box>
258
+ </Tooltip>
259
+ {balanceInfo && (
260
+ <Button
261
+ size="small"
262
+ variant="text"
263
+ onClick={handleRecharge}
264
+ sx={{
265
+ color: 'primary.main',
266
+ display: 'flex',
267
+ alignItems: 'center',
268
+ }}>
269
+ <AddOutlined fontSize="small" />
270
+ {t('payment.autoTopup.addFunds')}
271
+ </Button>
272
+ )}
273
+ </Box>
274
+ </Box>
275
+ </Stack>
276
+ );
277
+ };
278
+
279
+ const openModal = () => setModalOpen(true);
280
+
281
+ const renderInnerView = () => {
282
+ if (mode === 'custom') {
283
+ return children && typeof children === 'function' ? (
284
+ <>{children(openModal, config, paymentData, loading)}</>
285
+ ) : (
286
+ <Typography>
287
+ Please provide a valid render function
288
+ <pre>{'(openModal, config, paymentData, loading) => ReactNode'}</pre>
289
+ </Typography>
290
+ );
291
+ }
292
+
293
+ return (
294
+ <Card sx={{ ...cardStyle, ...sx }}>
295
+ <CardContent>
296
+ {/* Header */}
297
+ <Stack
298
+ direction="row"
299
+ className="auto-topup-header"
300
+ sx={{
301
+ justifyContent: 'space-between',
302
+ alignItems: 'center',
303
+ borderBottom: '1px solid',
304
+ borderColor: 'divider',
305
+ pb: 1.5,
306
+ }}>
307
+ <Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'text.primary' }}>
308
+ {t('payment.autoTopup.title')}
309
+ </Typography>
310
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
311
+ <IconButton
312
+ size="small"
313
+ onClick={openModal}
314
+ sx={{
315
+ p: 0.5,
316
+ color: 'text.secondary',
317
+ '&:hover': {
318
+ bgcolor: 'grey.50',
319
+ color: 'text.primary',
320
+ },
321
+ }}>
322
+ <SettingsOutlined fontSize="small" />
323
+ </IconButton>
324
+ </Box>
325
+ </Stack>
326
+
327
+ {config?.enabled ? (
328
+ <Stack spacing={1.5} className="auto-topup-content" sx={{ pt: 1.5 }}>
329
+ {/* Main Description */}
330
+ {(() => {
331
+ const threshold = `${formatNumber(config.threshold)} ${config.currency?.symbol || ''}`;
332
+ const credits = `${formatNumber(
333
+ Number(config.price.metadata?.credit_config?.credit_amount || 0) * Number(config.quantity)
334
+ )} ${config.currency?.name || ''}`;
335
+
336
+ return (
337
+ <Typography
338
+ variant="body2"
339
+ sx={{
340
+ color: 'text.secondary',
341
+ }}>
342
+ {t('payment.autoTopup.activeDescriptionWithCredits', { threshold, credits })}
343
+ {/* 展开收起按钮 - 仅在simple模式下显示 */}
344
+ {mode === 'simple' && (
345
+ <Button
346
+ component="span"
347
+ size="small"
348
+ variant="text"
349
+ onClick={handleToggleExpanded}
350
+ sx={{
351
+ color: 'primary.main',
352
+ minWidth: 'auto',
353
+ ml: 1,
354
+ p: 0,
355
+ fontSize: 'inherit',
356
+ textTransform: 'none',
357
+ '&:hover': {
358
+ backgroundColor: 'transparent',
359
+ textDecoration: 'underline',
360
+ },
361
+ }}>
362
+ {expanded ? t('payment.autoTopup.hideDetails') : t('payment.autoTopup.showDetails')}
363
+ </Button>
364
+ )}
365
+ </Typography>
366
+ );
367
+ })()}
368
+
369
+ <Collapse in={mode === 'default' || expanded}>
370
+ <Box
371
+ sx={{
372
+ bgcolor: 'grey.50',
373
+ borderRadius: 1,
374
+ p: 1.5,
375
+ }}>
376
+ {renderPurchaseDetails()}
377
+ </Box>
378
+ </Collapse>
379
+ </Stack>
380
+ ) : (
381
+ <Stack
382
+ className="auto-topup-content"
383
+ sx={{
384
+ minHeight: 80,
385
+ display: 'flex',
386
+ flexDirection: 'column',
387
+ alignItems: 'center',
388
+ justifyContent: 'center',
389
+ pt: 1.5,
390
+ gap: 2,
391
+ }}>
392
+ <Typography
393
+ variant="body2"
394
+ sx={{
395
+ color: 'text.secondary',
396
+ textAlign: 'left',
397
+ }}>
398
+ {t('payment.autoTopup.inactiveDescription', {
399
+ name: config?.currency?.name,
400
+ })}
401
+ <Button
402
+ component="span"
403
+ variant="text"
404
+ size="small"
405
+ onClick={() => {
406
+ setQuickSetupMode(true);
407
+ setModalOpen(true);
408
+ }}
409
+ sx={{
410
+ color: 'primary.main',
411
+ minWidth: 'auto',
412
+ ml: 1,
413
+ p: 0,
414
+ fontSize: 'inherit',
415
+ textTransform: 'none',
416
+ '&:hover': {
417
+ backgroundColor: 'transparent',
418
+ textDecoration: 'underline',
419
+ },
420
+ }}>
421
+ {t('payment.autoTopup.setup')}
422
+ </Button>
423
+ </Typography>
424
+ </Stack>
425
+ )}
426
+ </CardContent>
427
+ </Card>
428
+ );
429
+ };
430
+
431
+ return (
432
+ <>
433
+ {renderInnerView()}
434
+
435
+ {modalOpen && (
436
+ <AutoTopupModal
437
+ open={modalOpen}
438
+ onClose={() => {
439
+ setModalOpen(false);
440
+ setQuickSetupMode(false); // 关闭时重置快速设置模式
441
+ }}
442
+ currencyId={currencyId}
443
+ onSuccess={handleConfigSuccess}
444
+ defaultEnabled={quickSetupMode} // 传递默认启用状态
445
+ />
446
+ )}
447
+ </>
448
+ );
449
+ }