@blocklet/payment-react 1.19.18 → 1.19.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.
Files changed (55) hide show
  1. package/README.md +313 -0
  2. package/es/checkout/form.js +2 -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 +6 -0
  21. package/es/payment/product-item.js +17 -10
  22. package/lib/checkout/form.js +2 -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 +6 -0
  41. package/lib/payment/product-item.js +18 -10
  42. package/package.json +9 -9
  43. package/src/checkout/form.tsx +2 -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 +6 -0
  55. package/src/payment/product-item.tsx +18 -11
@@ -0,0 +1,773 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ Box,
4
+ Typography,
5
+ Stack,
6
+ Button,
7
+ FormControlLabel,
8
+ Alert,
9
+ CircularProgress,
10
+ InputAdornment,
11
+ MenuItem,
12
+ Avatar,
13
+ Select,
14
+ TextField,
15
+ } from '@mui/material';
16
+ import { AccountBalanceWalletOutlined, AddOutlined, CreditCard, SwapHoriz } from '@mui/icons-material';
17
+
18
+ import { useForm, FormProvider, Controller } from 'react-hook-form';
19
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
20
+ import Toast from '@arcblock/ux/lib/Toast';
21
+ import Dialog from '@arcblock/ux/lib/Dialog';
22
+ import { useRequest, useSetState } from 'ahooks';
23
+ // eslint-disable-next-line import/no-extraneous-dependencies
24
+ import { useNavigate } from 'react-router-dom';
25
+
26
+ import { joinURL } from 'ufo';
27
+ import pWaitFor from 'p-wait-for';
28
+ import type { AutoRechargeConfig, TCustomer, TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
29
+ import DidAddress from '@arcblock/ux/lib/DID';
30
+ import Switch from '../switch-button';
31
+ import api from '../../libs/api';
32
+ import { formatError, flattenPaymentMethods, getPrefix, formatBNStr } from '../../libs/util';
33
+ import { usePaymentContext } from '../../contexts/payment';
34
+ import { createLink, handleNavigation } from '../../libs/navigation';
35
+
36
+ import Collapse from '../collapse';
37
+ import FormInput from '../input';
38
+ import StripeCheckout from '../../payment/form/stripe';
39
+ import AutoTopupProductCard from './product-card';
40
+ import FormLabel from '../label';
41
+
42
+ export interface AutoTopupFormData {
43
+ enabled: boolean;
44
+ threshold: string;
45
+ quantity: number;
46
+ payment_method_id: string;
47
+ recharge_currency_id: string;
48
+ price_id: string;
49
+ change_payment_method?: boolean;
50
+ customer_name?: string;
51
+ customer_email?: string;
52
+ daily_limits: {
53
+ max_attempts: number;
54
+ max_amount: number;
55
+ };
56
+ billing_address?: {
57
+ country?: string;
58
+ state?: string;
59
+ line1?: string;
60
+ line2?: string;
61
+ city?: string;
62
+ postal_code?: string;
63
+ };
64
+ }
65
+
66
+ export interface AutoTopupModalProps {
67
+ open: boolean;
68
+ onClose: () => void;
69
+ customerId?: string;
70
+ currencyId: string;
71
+ onSuccess?: (config: AutoRechargeConfig) => void;
72
+ onError?: (error: any) => void;
73
+ defaultEnabled?: boolean; // 默认是否启用
74
+ }
75
+
76
+ const fetchConfig = async (customerId: string, currencyId: string) => {
77
+ const { data } = await api.get(`/api/auto-recharge-configs/customer/${customerId}`, {
78
+ params: { currency_id: currencyId },
79
+ });
80
+ return data;
81
+ };
82
+
83
+ const fetchCurrencyBalance = async (currencyId: string, payerAddress: string) => {
84
+ const { data } = await api.get('/api/customers/payer-token', {
85
+ params: { currencyId, payerAddress },
86
+ });
87
+ return data;
88
+ };
89
+
90
+ const DEFAULT_VALUES = {
91
+ enabled: false,
92
+ threshold: '100',
93
+ quantity: 1,
94
+ payment_method_id: '',
95
+ recharge_currency_id: '',
96
+ price_id: '',
97
+ daily_max_amount: 0,
98
+ daily_max_attempts: 0,
99
+ };
100
+
101
+ export const waitForAutoRechargeComplete = async (configId: string) => {
102
+ let result: any;
103
+
104
+ await pWaitFor(
105
+ async () => {
106
+ const { data } = await api.get(`/api/auto-recharge-configs/retrieve/${configId}`);
107
+ result = data;
108
+ return !!result.payment_settings?.payment_method_options?.[result.paymentMethod.type]?.payer;
109
+ },
110
+ { interval: 2000, timeout: 3 * 60 * 1000 }
111
+ );
112
+
113
+ // @ts-ignore
114
+ return result;
115
+ };
116
+
117
+ // 支付方式显示组件
118
+ function PaymentMethodDisplay({
119
+ config,
120
+ onChangePaymentMethod,
121
+ paymentMethod,
122
+ currency,
123
+ }: {
124
+ config: any;
125
+ paymentMethod: TPaymentMethod;
126
+ onChangePaymentMethod: (change: boolean) => void;
127
+ currency: TPaymentCurrency;
128
+ }) {
129
+ const { t } = useLocaleContext();
130
+ const [changePaymentMethod, setChangePaymentMethod] = useState(false);
131
+ const navigate = useNavigate();
132
+
133
+ const paymentInfo = config?.payment_settings?.payment_method_options?.[paymentMethod.type || ''];
134
+
135
+ const { data: balanceInfo, loading: balanceLoading } = useRequest(
136
+ async () => {
137
+ if (paymentMethod.type === 'stripe') {
138
+ return null;
139
+ }
140
+ const result = await fetchCurrencyBalance(currency.id, paymentInfo?.payer as string);
141
+ return result;
142
+ },
143
+ {
144
+ refreshDeps: [currency.id, paymentInfo?.payer],
145
+ ready: !!currency.id && !!paymentInfo?.payer,
146
+ }
147
+ );
148
+ const handleChangeToggle = () => {
149
+ const newChange = !changePaymentMethod;
150
+ setChangePaymentMethod(newChange);
151
+ onChangePaymentMethod(newChange);
152
+ };
153
+
154
+ if (!paymentInfo) {
155
+ return null;
156
+ }
157
+
158
+ const handleRecharge = (e: React.MouseEvent) => {
159
+ const url = joinURL(getPrefix(), `/customer/recharge/${currency.id}?rechargeAddress=${paymentInfo?.payer}`);
160
+ const link = createLink(url, true);
161
+ handleNavigation(e, link, navigate);
162
+ };
163
+
164
+ const renderPaymentMethodInfo = () => {
165
+ if (paymentMethod.type === 'stripe') {
166
+ return (
167
+ <Stack
168
+ direction="row"
169
+ spacing={1}
170
+ sx={{
171
+ alignItems: 'center',
172
+ }}>
173
+ <CreditCard fontSize="small" color="primary" />
174
+ <Typography variant="body2">**** **** **** {paymentInfo?.card_last4 || '****'}</Typography>
175
+ <Typography
176
+ variant="body2"
177
+ sx={{
178
+ color: 'text.secondary',
179
+ textTransform: 'uppercase',
180
+ }}>
181
+ {paymentInfo?.card_brand || 'CARD'}
182
+ </Typography>
183
+ {paymentInfo?.exp_time && (
184
+ <Typography
185
+ variant="body2"
186
+ sx={{
187
+ color: 'text.secondary',
188
+ borderLeft: '1px solid',
189
+ borderColor: 'divider',
190
+ pl: 1,
191
+ }}>
192
+ {paymentInfo?.exp_time}
193
+ </Typography>
194
+ )}
195
+ </Stack>
196
+ );
197
+ }
198
+
199
+ return (
200
+ <Stack
201
+ spacing={1}
202
+ sx={{
203
+ borderRadius: 1,
204
+ backgroundColor: (theme) => (theme.palette.mode === 'dark' ? 'grey.100' : 'grey.50'),
205
+ p: 2,
206
+ }}>
207
+ <DidAddress did={paymentInfo?.payer} responsive={false} compact copyable={false} />
208
+ {(balanceInfo || balanceLoading) && (
209
+ <Stack
210
+ direction="row"
211
+ spacing={1}
212
+ sx={{
213
+ alignItems: 'center',
214
+ }}>
215
+ {balanceLoading ? (
216
+ <CircularProgress size={14} sx={{ mr: 0.5 }} />
217
+ ) : (
218
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
219
+ <AccountBalanceWalletOutlined fontSize="small" sx={{ color: 'text.lighter' }} />
220
+ <Typography variant="body2" sx={{ color: 'text.primary' }}>
221
+ <strong>{formatBNStr(balanceInfo?.token || '0', currency?.decimal)} </strong>
222
+ {currency?.symbol || ''}
223
+ </Typography>
224
+ </Box>
225
+ )}
226
+ <Button
227
+ size="small"
228
+ variant="text"
229
+ onClick={handleRecharge}
230
+ sx={{ fontSize: 'smaller', color: 'primary.main' }}>
231
+ <AddOutlined fontSize="small" />
232
+ {t('payment.autoTopup.addFunds')}
233
+ </Button>
234
+ </Stack>
235
+ )}
236
+ </Stack>
237
+ );
238
+ };
239
+
240
+ return (
241
+ <Box
242
+ sx={{
243
+ p: 2,
244
+ border: '1px solid',
245
+ borderColor: changePaymentMethod ? 'primary.main' : 'divider',
246
+ borderRadius: 1,
247
+ bgcolor: changePaymentMethod ? 'primary.50' : 'background.paper',
248
+ }}>
249
+ <Stack spacing={2}>
250
+ <Stack
251
+ sx={{
252
+ flexDirection: { xs: 'column', sm: 'row' },
253
+ alignItems: { xs: 'flex-start', sm: 'center' },
254
+ justifyContent: { xs: 'flex-start', sm: 'space-between' },
255
+ }}>
256
+ <Typography variant="subtitle2" sx={{ display: 'flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap' }}>
257
+ {t('payment.autoTopup.currentPaymentMethod')}
258
+ </Typography>
259
+
260
+ <Button
261
+ size="small"
262
+ startIcon={<SwapHoriz />}
263
+ onClick={handleChangeToggle}
264
+ variant="text"
265
+ sx={{
266
+ color: 'primary.main',
267
+ whiteSpace: 'nowrap',
268
+ alignSelf: { xs: 'flex-end', sm: 'center' },
269
+ }}>
270
+ {changePaymentMethod ? t('payment.autoTopup.keepCurrent') : t('payment.autoTopup.changePaymentMethod')}
271
+ </Button>
272
+ </Stack>
273
+
274
+ {changePaymentMethod ? (
275
+ <Typography
276
+ variant="body2"
277
+ sx={{
278
+ color: 'text.secondary',
279
+ }}>
280
+ {t('payment.autoTopup.changePaymentMethodTip')}
281
+ </Typography>
282
+ ) : (
283
+ renderPaymentMethodInfo()
284
+ )}
285
+ </Stack>
286
+ </Box>
287
+ );
288
+ }
289
+
290
+ export default function AutoTopup({
291
+ open,
292
+ onClose,
293
+ currencyId,
294
+ onSuccess = () => {},
295
+ onError = () => {},
296
+ defaultEnabled = undefined,
297
+ }: AutoTopupModalProps) {
298
+ const { t, locale } = useLocaleContext();
299
+ const { session, connect, settings } = usePaymentContext();
300
+ const [changePaymentMethod, setChangePaymentMethod] = useState(false);
301
+ const [state, setState] = useSetState({
302
+ loading: false,
303
+ submitting: false,
304
+ authorizationRequired: false,
305
+ stripeContext: {
306
+ client_secret: '',
307
+ intent_type: '',
308
+ status: '',
309
+ public_key: '',
310
+ customer: {} as TCustomer,
311
+ },
312
+ });
313
+
314
+ const currencies = flattenPaymentMethods(settings.paymentMethods);
315
+
316
+ const methods = useForm<AutoTopupFormData>({
317
+ defaultValues: {
318
+ enabled: defaultEnabled || DEFAULT_VALUES.enabled,
319
+ threshold: DEFAULT_VALUES.threshold,
320
+ quantity: DEFAULT_VALUES.quantity,
321
+ payment_method_id: DEFAULT_VALUES.payment_method_id,
322
+ recharge_currency_id: DEFAULT_VALUES.recharge_currency_id,
323
+ price_id: DEFAULT_VALUES.price_id,
324
+ daily_limits: {
325
+ max_attempts: DEFAULT_VALUES.daily_max_attempts,
326
+ max_amount: DEFAULT_VALUES.daily_max_amount,
327
+ },
328
+ },
329
+ });
330
+
331
+ const { handleSubmit, setValue, watch } = methods;
332
+ const enabled = watch('enabled');
333
+ const quantity = watch('quantity') as number;
334
+ const rechargeCurrencyId = watch('recharge_currency_id');
335
+
336
+ const handleClose = () => {
337
+ setState({
338
+ loading: false,
339
+ submitting: false,
340
+ authorizationRequired: false,
341
+ });
342
+ onClose();
343
+ };
344
+
345
+ const { data: config } = useRequest(() => fetchConfig(session?.user?.did, currencyId), {
346
+ refreshDeps: [session?.user?.did, currencyId],
347
+ ready: !!session?.user?.did && !!currencyId,
348
+ onError: (error: any) => {
349
+ Toast.error(formatError(error));
350
+ },
351
+ onSuccess: (data) => {
352
+ setValue('enabled', defaultEnabled || data.enabled);
353
+ setValue('threshold', data.threshold);
354
+ setValue('quantity', data.quantity);
355
+ setValue('payment_method_id', data.payment_method_id);
356
+ setValue('recharge_currency_id', data.recharge_currency_id || data.price?.currency_id);
357
+ setValue('price_id', data.price_id);
358
+ setValue('daily_limits', {
359
+ max_amount: data.daily_limits?.max_amount || 0,
360
+ max_attempts: data.daily_limits?.max_attempts || 0,
361
+ });
362
+ },
363
+ });
364
+
365
+ const filterCurrencies = useMemo(() => {
366
+ return currencies.filter((c) => config?.price?.currency_options?.find((o: any) => o.currency_id === c.id));
367
+ }, [currencies, config]);
368
+
369
+ const handleConnected = async () => {
370
+ try {
371
+ const result = await waitForAutoRechargeComplete(config?.id);
372
+ if (result) {
373
+ setState({ submitting: false, authorizationRequired: false });
374
+ onSuccess?.(config);
375
+ handleClose();
376
+ Toast.success(t('payment.autoTopup.saveSuccess'));
377
+ }
378
+ } catch (err) {
379
+ Toast.error(formatError(err));
380
+ } finally {
381
+ setState({ submitting: false, authorizationRequired: false });
382
+ }
383
+ };
384
+
385
+ const handleDisable = async () => {
386
+ try {
387
+ const submitData = {
388
+ ...config,
389
+ enabled: false,
390
+ };
391
+
392
+ if (!config?.enabled) {
393
+ return;
394
+ }
395
+ const { data } = await api.post('/api/auto-recharge-configs/submit', submitData);
396
+ onSuccess?.(data);
397
+ Toast.success(t('payment.autoTopup.disableSuccess'));
398
+ } catch (error) {
399
+ Toast.error(formatError(error));
400
+ onError?.(error);
401
+ }
402
+ };
403
+
404
+ const handleEnableChange = async (checked: boolean) => {
405
+ setValue('enabled', checked);
406
+ if (!checked) {
407
+ await handleDisable();
408
+ }
409
+ };
410
+
411
+ const handleAuthorizationRequired = (authData: any) => {
412
+ setState({ authorizationRequired: true });
413
+
414
+ if (authData.stripeContext) {
415
+ // 处理Stripe授权
416
+ setState({
417
+ stripeContext: {
418
+ client_secret: authData.stripeContext.client_secret,
419
+ intent_type: authData.stripeContext.intent_type,
420
+ status: authData.stripeContext.status,
421
+ public_key: authData.paymentMethod.settings.stripe.publishable_key,
422
+ customer: authData.customer,
423
+ },
424
+ });
425
+ } else if (authData.delegation) {
426
+ // 处理DID Connect授权
427
+ handleDidConnect();
428
+ }
429
+ };
430
+
431
+ const handleDidConnect = () => {
432
+ try {
433
+ setState({ submitting: true });
434
+
435
+ connect.open({
436
+ containerEl: undefined as unknown as Element,
437
+ saveConnect: false,
438
+ locale: locale as 'en' | 'zh',
439
+ action: 'auto-recharge-auth',
440
+ prefix: joinURL(getPrefix(), '/api/did'),
441
+ extraParams: {
442
+ autoRechargeConfigId: config?.id,
443
+ },
444
+ messages: {
445
+ scan: t('payment.autoTopup.authTip'),
446
+ title: t('payment.autoTopup.authTitle'),
447
+ confirm: t('common.connect.confirm'),
448
+ } as any,
449
+ onSuccess: async () => {
450
+ connect.close();
451
+ await handleConnected();
452
+ },
453
+ onClose: () => {
454
+ connect.close();
455
+ setState({ submitting: false, authorizationRequired: false });
456
+ },
457
+ onError: (err: any) => {
458
+ setState({ submitting: false, authorizationRequired: false });
459
+ Toast.error(formatError(err));
460
+ },
461
+ });
462
+ } catch (error) {
463
+ setState({ submitting: false, authorizationRequired: false });
464
+ Toast.error(formatError(error));
465
+ }
466
+ };
467
+
468
+ const handleFormSubmit = async (formData: AutoTopupFormData) => {
469
+ setState({ submitting: true });
470
+
471
+ try {
472
+ const submitData = {
473
+ customer_id: session?.user?.did,
474
+ enabled: formData.enabled,
475
+ threshold: formData.threshold,
476
+ currency_id: currencyId,
477
+ recharge_currency_id: formData.recharge_currency_id,
478
+ price_id: formData.price_id,
479
+ quantity: formData.quantity,
480
+ daily_limits: {
481
+ max_attempts: formData.daily_limits.max_attempts || 0,
482
+ max_amount: formData.daily_limits.max_amount || '0',
483
+ },
484
+ change_payment_method: changePaymentMethod,
485
+ };
486
+
487
+ const { data } = await api.post('/api/auto-recharge-configs/submit', submitData);
488
+
489
+ if (data.balanceResult && !data.balanceResult.sufficient) {
490
+ await handleAuthorizationRequired({
491
+ ...data.balanceResult,
492
+ paymentMethod: data.paymentMethod,
493
+ customer: data.customer,
494
+ });
495
+ return;
496
+ }
497
+
498
+ setState({
499
+ submitting: false,
500
+ authorizationRequired: false,
501
+ });
502
+ onSuccess?.(data);
503
+ handleClose();
504
+ Toast.success(t('payment.autoTopup.saveSuccess'));
505
+ } catch (error) {
506
+ setState({ submitting: false, authorizationRequired: false });
507
+ Toast.error(formatError(error));
508
+ onError?.(error);
509
+ }
510
+ };
511
+
512
+ const onSubmit = (formData: AutoTopupFormData) => {
513
+ handleFormSubmit(formData);
514
+ };
515
+
516
+ const rechargeCurrency = filterCurrencies.find((c) => c.id === rechargeCurrencyId);
517
+
518
+ const selectedMethod = settings.paymentMethods.find((method) => {
519
+ return method.payment_currencies.find((c) => c.id === rechargeCurrencyId);
520
+ });
521
+ const showStripeForm = state.authorizationRequired && selectedMethod?.type === 'stripe';
522
+
523
+ const onStripeConfirm = async () => {
524
+ await handleConnected();
525
+ };
526
+
527
+ const onStripeCancel = () => {
528
+ setState({ submitting: false, authorizationRequired: false });
529
+ };
530
+ return (
531
+ <Dialog
532
+ open={open}
533
+ onClose={handleClose}
534
+ maxWidth="sm"
535
+ fullWidth
536
+ className="base-dialog"
537
+ title={t('payment.autoTopup.title')}
538
+ actions={
539
+ enabled ? (
540
+ <Stack direction="row" spacing={2}>
541
+ <Button variant="outlined" onClick={handleClose} disabled={state.submitting}>
542
+ {t('common.cancel')}
543
+ </Button>
544
+ <Button
545
+ variant="contained"
546
+ onClick={() => handleSubmit(onSubmit)()}
547
+ disabled={state.loading || state.authorizationRequired || state.submitting}>
548
+ {state.submitting && <CircularProgress size={20} sx={{ mr: 1 }} />}
549
+ {t('payment.autoTopup.saveConfiguration')}
550
+ </Button>
551
+ </Stack>
552
+ ) : null
553
+ }>
554
+ <FormProvider {...methods}>
555
+ <Box component="form" onSubmit={handleSubmit(onSubmit)}>
556
+ <Stack
557
+ sx={{
558
+ gap: 2,
559
+ }}>
560
+ <Alert severity="info">{t('payment.autoTopup.tip')}</Alert>
561
+
562
+ {/* 启用开关 */}
563
+ <Stack
564
+ spacing={2}
565
+ direction="row"
566
+ sx={{
567
+ alignItems: 'center',
568
+ }}>
569
+ <Typography variant="subtitle1">{t('payment.autoTopup.enableLabel')}</Typography>
570
+ <FormControlLabel
571
+ control={<Switch checked={enabled} onChange={(e: any) => handleEnableChange(e.target.checked)} />}
572
+ label=""
573
+ />
574
+ </Stack>
575
+
576
+ {enabled && (
577
+ <>
578
+ <Box sx={{ pt: 1 }}>
579
+ <Stack spacing={2.5}>
580
+ <Stack
581
+ sx={{
582
+ gap: 2,
583
+ flexDirection: { xs: 'column', sm: 'row' },
584
+ alignItems: { xs: 'flex-start', sm: 'center' },
585
+ justifyContent: { xs: 'flex-start', sm: 'space-between' },
586
+ '.MuiTextField-root': {
587
+ width: {
588
+ xs: '100%',
589
+ sm: 'auto',
590
+ },
591
+ },
592
+ }}>
593
+ <FormLabel boxSx={{ width: 'fit-content', whiteSpace: 'nowrap', marginBottom: 0 }}>
594
+ {t('payment.autoTopup.triggerThreshold')}
595
+ </FormLabel>
596
+ <Controller
597
+ name="threshold"
598
+ control={methods.control}
599
+ rules={{
600
+ required: t('payment.checkout.required'),
601
+ min: { value: 0, message: t('payment.autoTopup.thresholdMinError') },
602
+ }}
603
+ render={({ field }) => (
604
+ <TextField
605
+ {...field}
606
+ type="number"
607
+ placeholder={t('payment.autoTopup.thresholdPlaceholder')}
608
+ sx={{
609
+ input: {
610
+ minWidth: 80,
611
+ },
612
+ }}
613
+ slotProps={{
614
+ input: {
615
+ endAdornment: <InputAdornment position="end">{config?.currency?.name}</InputAdornment>,
616
+ },
617
+ htmlInput: {
618
+ min: 0,
619
+ step: 0.01,
620
+ },
621
+ }}
622
+ />
623
+ )}
624
+ />
625
+ </Stack>
626
+ <Stack
627
+ sx={{
628
+ gap: 2,
629
+ flexDirection: { xs: 'column', sm: 'row' },
630
+ alignItems: { xs: 'flex-start', sm: 'center' },
631
+ justifyContent: { xs: 'flex-start', sm: 'space-between' },
632
+ }}>
633
+ <FormLabel boxSx={{ width: 'fit-content', whiteSpace: 'nowrap', marginBottom: 0 }}>
634
+ {t('payment.autoTopup.purchaseBelow')}
635
+ </FormLabel>
636
+ <Controller
637
+ name="recharge_currency_id"
638
+ control={methods.control}
639
+ render={({ field }) => (
640
+ <Select
641
+ {...field}
642
+ value={field.value}
643
+ onChange={(e) => field.onChange(e.target.value)}
644
+ size="small">
645
+ {filterCurrencies.map((x) => (
646
+ <MenuItem key={x?.id} value={x?.id}>
647
+ <Stack
648
+ direction="row"
649
+ sx={{
650
+ alignItems: 'center',
651
+ gap: 1,
652
+ }}>
653
+ <Avatar src={x?.logo} sx={{ width: 20, height: 20 }} alt={x?.symbol} />
654
+ <Typography
655
+ sx={{
656
+ color: 'text.secondary',
657
+ }}>
658
+ {x?.symbol} ({(x as any)?.method?.name})
659
+ </Typography>
660
+ </Stack>
661
+ </MenuItem>
662
+ ))}
663
+ </Select>
664
+ )}
665
+ />
666
+ </Stack>
667
+
668
+ {config?.price?.product && (
669
+ <AutoTopupProductCard
670
+ product={config.price.product}
671
+ price={config.price}
672
+ creditCurrency={config.currency}
673
+ currency={filterCurrencies.find((c) => c.id === rechargeCurrencyId) || filterCurrencies[0]}
674
+ quantity={quantity}
675
+ onQuantityChange={(newQuantity) => setValue('quantity', newQuantity)}
676
+ maxQuantity={9999}
677
+ minQuantity={1}
678
+ />
679
+ )}
680
+
681
+ {config && rechargeCurrency && (
682
+ <PaymentMethodDisplay
683
+ config={config}
684
+ onChangePaymentMethod={setChangePaymentMethod}
685
+ currency={rechargeCurrency}
686
+ paymentMethod={selectedMethod as TPaymentMethod}
687
+ />
688
+ )}
689
+
690
+ <Collapse trigger={t('payment.autoTopup.advanced')}>
691
+ <Stack
692
+ sx={{
693
+ gap: 2,
694
+ pl: 2,
695
+ pr: 1,
696
+ }}>
697
+ <FormInput
698
+ name="daily_limits.max_amount"
699
+ label={t('payment.autoTopup.dailyLimits.maxAmount')}
700
+ type="number"
701
+ placeholder={t('payment.autoTopup.dailyLimits.maxAmountPlaceholder')}
702
+ tooltip={t('payment.autoTopup.dailyLimits.maxAmountDescription')}
703
+ inputProps={{
704
+ min: 0,
705
+ step: 0.01,
706
+ }}
707
+ slotProps={{
708
+ input: {
709
+ endAdornment: (
710
+ <InputAdornment position="end">
711
+ {filterCurrencies.find((c) => c.id === rechargeCurrencyId)?.symbol || ''}
712
+ </InputAdornment>
713
+ ),
714
+ },
715
+ }}
716
+ sx={{
717
+ maxWidth: {
718
+ xs: '100%',
719
+ sm: '220px',
720
+ },
721
+ }}
722
+ layout="horizontal"
723
+ />
724
+ <FormInput
725
+ name="daily_limits.max_attempts"
726
+ label={t('payment.autoTopup.dailyLimits.maxAttempts')}
727
+ type="number"
728
+ placeholder={t('payment.autoTopup.dailyLimits.maxAttemptsPlaceholder')}
729
+ tooltip={t('payment.autoTopup.dailyLimits.maxAttemptsDescription')}
730
+ inputProps={{
731
+ min: 0,
732
+ step: 1,
733
+ }}
734
+ sx={{
735
+ maxWidth: {
736
+ xs: '100%',
737
+ sm: '220px',
738
+ },
739
+ }}
740
+ layout="horizontal"
741
+ />
742
+ </Stack>
743
+ </Collapse>
744
+ </Stack>
745
+ </Box>
746
+ <Stack spacing={2}>
747
+ {/* Stripe 表单 */}
748
+ {showStripeForm && (
749
+ <Box sx={{ mt: 2 }}>
750
+ {state.stripeContext && (
751
+ <Box sx={{ mt: 2 }}>
752
+ <StripeCheckout
753
+ clientSecret={state.stripeContext.client_secret}
754
+ intentType={state.stripeContext.intent_type}
755
+ publicKey={state.stripeContext.public_key}
756
+ customer={state.stripeContext.customer}
757
+ mode="setup"
758
+ onConfirm={onStripeConfirm}
759
+ onCancel={onStripeCancel}
760
+ />
761
+ </Box>
762
+ )}
763
+ </Box>
764
+ )}
765
+ </Stack>
766
+ </>
767
+ )}
768
+ </Stack>
769
+ </Box>
770
+ </FormProvider>
771
+ </Dialog>
772
+ );
773
+ }