payment-kit 1.24.4 → 1.25.0

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 (115) hide show
  1. package/api/src/index.ts +3 -0
  2. package/api/src/libs/credit-utils.ts +21 -0
  3. package/api/src/libs/discount/discount.ts +13 -0
  4. package/api/src/libs/env.ts +5 -0
  5. package/api/src/libs/error.ts +14 -0
  6. package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
  7. package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
  8. package/api/src/libs/exchange-rate/index.ts +5 -0
  9. package/api/src/libs/exchange-rate/service.ts +583 -0
  10. package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
  11. package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
  12. package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
  13. package/api/src/libs/exchange-rate/types.ts +114 -0
  14. package/api/src/libs/exchange-rate/validator.ts +319 -0
  15. package/api/src/libs/invoice-quote.ts +158 -0
  16. package/api/src/libs/invoice.ts +143 -7
  17. package/api/src/libs/math-utils.ts +46 -0
  18. package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
  19. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
  20. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
  21. package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
  22. package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
  23. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
  24. package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
  25. package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
  26. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
  27. package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
  28. package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
  29. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
  30. package/api/src/libs/payment.ts +1 -1
  31. package/api/src/libs/price.ts +4 -1
  32. package/api/src/libs/queue/index.ts +8 -0
  33. package/api/src/libs/quote-service.ts +1132 -0
  34. package/api/src/libs/quote-validation.ts +388 -0
  35. package/api/src/libs/session.ts +686 -39
  36. package/api/src/libs/slippage.ts +135 -0
  37. package/api/src/libs/subscription.ts +185 -15
  38. package/api/src/libs/util.ts +64 -3
  39. package/api/src/locales/en.ts +50 -0
  40. package/api/src/locales/zh.ts +48 -0
  41. package/api/src/queues/auto-recharge.ts +295 -21
  42. package/api/src/queues/exchange-rate-health.ts +242 -0
  43. package/api/src/queues/invoice.ts +48 -1
  44. package/api/src/queues/notification.ts +167 -1
  45. package/api/src/queues/payment.ts +177 -7
  46. package/api/src/queues/subscription.ts +436 -6
  47. package/api/src/routes/auto-recharge-configs.ts +71 -6
  48. package/api/src/routes/checkout-sessions.ts +1730 -81
  49. package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
  50. package/api/src/routes/connect/change-payer.ts +2 -0
  51. package/api/src/routes/connect/change-payment.ts +61 -8
  52. package/api/src/routes/connect/change-plan.ts +161 -17
  53. package/api/src/routes/connect/collect.ts +9 -6
  54. package/api/src/routes/connect/delegation.ts +1 -0
  55. package/api/src/routes/connect/pay.ts +157 -0
  56. package/api/src/routes/connect/setup.ts +32 -10
  57. package/api/src/routes/connect/shared.ts +159 -13
  58. package/api/src/routes/connect/subscribe.ts +32 -9
  59. package/api/src/routes/credit-grants.ts +99 -0
  60. package/api/src/routes/exchange-rate-providers.ts +248 -0
  61. package/api/src/routes/exchange-rates.ts +87 -0
  62. package/api/src/routes/index.ts +4 -0
  63. package/api/src/routes/invoices.ts +280 -2
  64. package/api/src/routes/payment-links.ts +13 -0
  65. package/api/src/routes/prices.ts +84 -2
  66. package/api/src/routes/subscriptions.ts +526 -15
  67. package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
  68. package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
  69. package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
  70. package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
  71. package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
  72. package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
  73. package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
  74. package/api/src/store/models/auto-recharge-config.ts +12 -0
  75. package/api/src/store/models/checkout-session.ts +7 -0
  76. package/api/src/store/models/exchange-rate-provider.ts +225 -0
  77. package/api/src/store/models/index.ts +6 -0
  78. package/api/src/store/models/payment-intent.ts +6 -0
  79. package/api/src/store/models/price-quote.ts +284 -0
  80. package/api/src/store/models/price.ts +53 -5
  81. package/api/src/store/models/subscription.ts +11 -0
  82. package/api/src/store/models/types.ts +61 -1
  83. package/api/tests/libs/change-payment-plan.spec.ts +282 -0
  84. package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
  85. package/api/tests/libs/quote-service.spec.ts +199 -0
  86. package/api/tests/libs/session.spec.ts +464 -0
  87. package/api/tests/libs/slippage.spec.ts +109 -0
  88. package/api/tests/libs/token-data-provider.spec.ts +267 -0
  89. package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
  90. package/api/tests/models/price-dynamic.spec.ts +100 -0
  91. package/api/tests/models/price-quote.spec.ts +112 -0
  92. package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
  93. package/api/tests/routes/subscription-slippage.spec.ts +254 -0
  94. package/blocklet.yml +1 -1
  95. package/package.json +7 -6
  96. package/src/components/customer/credit-overview.tsx +14 -0
  97. package/src/components/discount/discount-info.tsx +8 -2
  98. package/src/components/invoice/list.tsx +146 -16
  99. package/src/components/invoice/table.tsx +276 -71
  100. package/src/components/invoice-pdf/template.tsx +3 -7
  101. package/src/components/metadata/form.tsx +6 -8
  102. package/src/components/price/form.tsx +519 -149
  103. package/src/components/promotion/active-redemptions.tsx +5 -3
  104. package/src/components/quote/info.tsx +234 -0
  105. package/src/hooks/subscription.ts +132 -2
  106. package/src/locales/en.tsx +145 -0
  107. package/src/locales/zh.tsx +143 -1
  108. package/src/pages/admin/billing/invoices/detail.tsx +41 -4
  109. package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
  110. package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
  111. package/src/pages/admin/products/index.tsx +12 -1
  112. package/src/pages/customer/invoice/detail.tsx +36 -12
  113. package/src/pages/customer/subscription/change-payment.tsx +65 -3
  114. package/src/pages/customer/subscription/change-plan.tsx +207 -38
  115. package/src/pages/customer/subscription/detail.tsx +599 -419
@@ -1,16 +1,22 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
3
4
  import {
4
5
  api,
5
- CreditGrantsList,
6
+ SlippageConfig,
6
7
  CreditTransactionsList,
7
8
  CustomerInvoiceList,
8
9
  formatBNStr,
10
+ formatError,
11
+ formatExchangeRateDisplay,
9
12
  formatTime,
13
+ getPrefix,
10
14
  hasDelegateTxHash,
11
15
  TxLink,
12
16
  useMobile,
17
+ usePaymentContext,
13
18
  } from '@blocklet/payment-react';
19
+ import type { SlippageConfigValue } from '@blocklet/payment-react';
14
20
  import type { TPaymentCurrency, TSubscriptionExpanded } from '@blocklet/payment-types';
15
21
  import {
16
22
  AccountBalanceWalletOutlined,
@@ -28,6 +34,9 @@ import {
28
34
  Divider,
29
35
  IconButton,
30
36
  Link as MuiLink,
37
+ Dialog,
38
+ DialogContent,
39
+ DialogTitle,
31
40
  Stack,
32
41
  Tooltip,
33
42
  Typography,
@@ -36,8 +45,9 @@ import {
36
45
  import { styled } from '@mui/system';
37
46
  import { BN, fromUnitToToken } from '@ocap/util';
38
47
  import { useRequest } from 'ahooks';
39
- import { useCallback, useRef } from 'react';
48
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
40
49
  import { Link, useNavigate, useParams } from 'react-router-dom';
50
+ import { joinURL } from 'ufo';
41
51
  import Currency from '../../../components/currency';
42
52
  import CustomerLink from '../../../components/customer/link';
43
53
  import InfoMetric from '../../../components/info-metric';
@@ -50,12 +60,18 @@ import SubscriptionActions, { ActionMethods } from '../../../components/subscrip
50
60
  import VendorServiceList from '../../../components/subscription/vendor-service-list';
51
61
  import DiscountInfo from '../../../components/discount/discount-info';
52
62
  import { useSessionContext } from '../../../contexts/session';
53
- import { usePendingAmountForSubscription, useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
63
+ import {
64
+ usePendingAmountForSubscription,
65
+ useSubscriptionExchangeRate,
66
+ useUnpaidInvoicesCheckForSubscription,
67
+ } from '../../../hooks/subscription';
54
68
  import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
55
69
  import { canChangePaymentMethod } from '../../../libs/util';
56
70
  import PaymentMethodInfo from '../../../components/subscription/payment-method-info';
57
71
  import { useArcsphere } from '../../../hooks/browser';
58
72
 
73
+ const defaultSlippageConfig: SlippageConfigValue = { mode: 'percent', percent: 0.5 };
74
+
59
75
  const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
60
76
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
61
77
  };
@@ -76,15 +92,78 @@ const fetchCycleAmount = (
76
92
  export default function CustomerSubscriptionDetail() {
77
93
  const { id } = useParams() as { id: string };
78
94
  const navigate = useNavigate();
79
- const { t } = useLocaleContext();
95
+ const { t, locale } = useLocaleContext();
80
96
  const { isMobile } = useMobile();
81
97
  const { session } = useSessionContext();
98
+ const { connect } = usePaymentContext();
82
99
  const inArcsphere = useArcsphere();
83
100
  const { loading, error, data, refresh } = useRequest(() => fetchData(id));
84
101
  const { hasUnpaid, checkUnpaidInvoices } = useUnpaidInvoicesCheckForSubscription(id);
85
102
  const { hasPendingAmount, pendingAmount } = usePendingAmountForSubscription(id, data?.paymentCurrency);
86
103
  const overdraftProtectionReady =
87
104
  ['active', 'trialing', 'past_due'].includes(data?.status || '') && data?.paymentMethod?.type === 'arcblock';
105
+ const [slippageDialogOpen, setSlippageDialogOpen] = useState(false);
106
+ const [slippageConfig, setSlippageConfig] = useState<SlippageConfigValue>(defaultSlippageConfig);
107
+ const [savedSlippageConfig, setSavedSlippageConfig] = useState<SlippageConfigValue>(defaultSlippageConfig);
108
+ const [slippageSaving, setSlippageSaving] = useState(false);
109
+ const { liveRateInfo } = useSubscriptionExchangeRate({
110
+ subscriptionId: id,
111
+ currencyId: data?.paymentCurrency?.id,
112
+ enabled: !!data?.paymentCurrency?.id,
113
+ });
114
+ useEffect(() => {
115
+ const rawConfig = (data as any)?.slippage_config;
116
+ if (rawConfig && typeof rawConfig === 'object') {
117
+ const mode: 'percent' | 'rate' = rawConfig.mode === 'rate' ? 'rate' : 'percent';
118
+ const percentValue = Number(rawConfig.percent);
119
+ const percent = Number.isFinite(percentValue) && percentValue >= 0 ? percentValue : 0.5;
120
+ const normalized: SlippageConfigValue = {
121
+ mode,
122
+ percent,
123
+ ...(rawConfig.min_acceptable_rate ? { min_acceptable_rate: String(rawConfig.min_acceptable_rate) } : {}),
124
+ ...(rawConfig.base_currency ? { base_currency: String(rawConfig.base_currency) } : {}),
125
+ ...(rawConfig.updated_at_ms ? { updated_at_ms: Number(rawConfig.updated_at_ms) } : {}),
126
+ };
127
+ setSlippageConfig(normalized);
128
+ setSavedSlippageConfig(normalized);
129
+ return;
130
+ }
131
+ setSlippageConfig(defaultSlippageConfig);
132
+ setSavedSlippageConfig(defaultSlippageConfig);
133
+ }, [data]);
134
+
135
+ // Check if subscription has any dynamic pricing items
136
+ // Slippage settings are only relevant for dynamic pricing subscriptions
137
+ const hasDynamicPricing = useMemo(
138
+ () =>
139
+ (data?.items || []).some((item: any) => {
140
+ const price = item.upsell_price || item.price;
141
+ return price && (price as any)?.pricing_type === 'dynamic';
142
+ }),
143
+ [data?.items]
144
+ );
145
+
146
+ // Calculate display min_acceptable_rate
147
+ // If min_acceptable_rate is not set (e.g., trial period with percent mode), calculate from live rate
148
+ const displayMinAcceptableRate = useMemo(() => {
149
+ // If already has min_acceptable_rate, use it directly
150
+ if (slippageConfig.min_acceptable_rate) {
151
+ return slippageConfig.min_acceptable_rate;
152
+ }
153
+ // For percent mode without min_acceptable_rate, calculate from live rate
154
+ if (liveRateInfo?.rate && slippageConfig.percent !== undefined) {
155
+ const rate = Number(liveRateInfo.rate);
156
+ const { percent } = slippageConfig;
157
+ if (Number.isFinite(rate) && rate > 0 && Number.isFinite(percent)) {
158
+ // min_rate = rate * (1 - percent/100)
159
+ const minRate = rate * (1 - percent / 100);
160
+ // Format to reasonable precision (8 decimals for crypto rates)
161
+ return minRate.toFixed(8).replace(/\.?0+$/, '');
162
+ }
163
+ }
164
+ return null;
165
+ }, [slippageConfig.min_acceptable_rate, slippageConfig.percent, liveRateInfo?.rate]);
166
+
88
167
  const {
89
168
  data: overdraftProtection = null,
90
169
  loading: overdraftProtectionLoading,
@@ -119,6 +198,68 @@ export default function CustomerSubscriptionDetail() {
119
198
  actionRef.current = methods;
120
199
  }, []);
121
200
 
201
+ const handleSlippageDialogClose = useCallback(() => {
202
+ setSlippageDialogOpen(false);
203
+ setSlippageConfig(savedSlippageConfig);
204
+ }, [savedSlippageConfig]);
205
+
206
+ const handleSlippageSave = async () => {
207
+ if (!id) {
208
+ return;
209
+ }
210
+ setSlippageSaving(true);
211
+ try {
212
+ const payloadConfig = {
213
+ ...slippageConfig,
214
+ ...(slippageConfig.base_currency ? {} : { base_currency: liveRateInfo?.base_currency || 'USD' }),
215
+ };
216
+ const result = await api.put(`/api/subscriptions/${id}/slippage`, {
217
+ slippage_config: payloadConfig,
218
+ });
219
+ setSavedSlippageConfig(payloadConfig);
220
+ await refresh();
221
+ setSlippageDialogOpen(false);
222
+
223
+ // Check if delegation is insufficient and needs re-authorization
224
+ const delegationWarning = result.data?.delegation_warning;
225
+ if (delegationWarning && !delegationWarning.sufficient) {
226
+ // Trigger delegation flow for re-authorization
227
+ Toast.info(t('payment.customer.changePayment.reauthorizationRequired'));
228
+ connect.open({
229
+ locale: locale as 'en' | 'zh',
230
+ containerEl: undefined as unknown as Element,
231
+ action: 'delegation',
232
+ prefix: joinURL(getPrefix(), '/api/did'),
233
+ saveConnect: false,
234
+ extraParams: { subscriptionId: id },
235
+ messages: {
236
+ scan: t('common.connect.defaultScan'),
237
+ title: t('payment.customer.changePayment.reauthorizationTitle'),
238
+ confirm: t('common.connect.confirm'),
239
+ } as any,
240
+ onSuccess: async () => {
241
+ connect.close();
242
+ Toast.success(t('payment.customer.changePayment.reauthorizationCompleted'));
243
+ await refresh();
244
+ },
245
+ onClose: () => {
246
+ connect.close();
247
+ },
248
+ onError: (err: any) => {
249
+ console.error('Delegation failed:', err);
250
+ Toast.error(formatError(err));
251
+ },
252
+ });
253
+ } else {
254
+ Toast.success(t('common.saved'));
255
+ }
256
+ } catch (err) {
257
+ Toast.error(formatError(err));
258
+ } finally {
259
+ setSlippageSaving(false);
260
+ }
261
+ };
262
+
122
263
  if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
123
264
  return <Alert severity="error">{t('common.accessDenied')}</Alert>;
124
265
  }
@@ -310,461 +451,479 @@ export default function CustomerSubscriptionDetail() {
310
451
  };
311
452
 
312
453
  return (
313
- <Root>
314
- <Box>
315
- {hasUnpaid && (
316
- <Alert severity="error" sx={{ mb: 2 }}>
317
- {t('customer.unpaidInvoicesWarningTip')}
318
- </Alert>
319
- )}
320
- {hasPendingAmount && pendingAmount && (
321
- <Alert severity="error" sx={{ mb: 2 }}>
322
- {t('customer.pendingAmountWarningTip', {
323
- amount: formatBNStr(pendingAmount, data?.paymentCurrency?.decimal),
324
- symbol: data?.paymentCurrency?.symbol,
325
- })}
326
- </Alert>
327
- )}
328
- <Stack
329
- className="page-header"
330
- direction="row"
331
- sx={{
332
- justifyContent: 'space-between',
333
- alignItems: 'center',
334
- position: 'relative',
335
- }}>
336
- {!inArcsphere ? (
337
- <Stack
338
- direction="row"
339
- onClick={() => navigate('/customer', { replace: true })}
340
- sx={{
341
- alignItems: 'center',
342
- fontWeight: 'normal',
343
- cursor: 'pointer',
344
- }}>
345
- <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
346
- <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
347
- {t('payment.customer.subscriptions.title')}
348
- </Typography>
349
- </Stack>
350
- ) : (
351
- <Box />
454
+ <>
455
+ <Root>
456
+ <Box>
457
+ {hasUnpaid && (
458
+ <Alert severity="error" sx={{ mb: 2 }}>
459
+ {t('customer.unpaidInvoicesWarningTip')}
460
+ </Alert>
461
+ )}
462
+ {hasPendingAmount && pendingAmount && (
463
+ <Alert severity="error" sx={{ mb: 2 }}>
464
+ {t('customer.pendingAmountWarningTip', {
465
+ amount: formatBNStr(pendingAmount, data?.paymentCurrency?.decimal),
466
+ symbol: data?.paymentCurrency?.symbol,
467
+ })}
468
+ </Alert>
352
469
  )}
353
470
  <Stack
354
- direction="row"
355
- sx={{
356
- gap: 1,
357
- }}>
358
- <SubscriptionActions
359
- subscription={data}
360
- onChange={(action) => {
361
- refresh();
362
- if (action === 'batch-pay') {
363
- checkUnpaidInvoices();
364
- }
365
- }}
366
- showExtra
367
- showDelegation
368
- showOverdraftProtection={{
369
- show: showOverdraftProtection,
370
- onChange: () => refreshOverdraftProtection(),
371
- }}
372
- showRecharge
373
- mode={isMobile ? 'menu-only' : 'primary-buttons'}
374
- actionProps={{
375
- cancel: {
376
- variant: 'outlined',
377
- color: 'primary',
378
- },
379
- recover: {
380
- variant: 'outlined',
381
- color: 'info',
382
- },
383
- pastDue: {
384
- variant: 'outlined',
385
- color: 'primary',
386
- },
387
- }}
388
- setUp={actionSetUp}
389
- />
390
- </Stack>
391
- </Stack>
392
- <Box
393
- sx={{
394
- mt: isMobile ? 2 : 4,
395
- display: 'flex',
396
- border: isMobile ? `1px solid ${muiTheme.palette.divider}` : 'none',
397
- borderRadius: isMobile ? 1 : 0,
398
- p: isMobile ? 2 : 0,
399
-
400
- gap: {
401
- xs: 2,
402
- sm: 2,
403
- md: 5,
404
- },
405
-
406
- flexWrap: 'wrap',
407
-
408
- flexDirection: {
409
- xs: 'column',
410
- sm: 'column',
411
- md: 'row',
412
- },
413
-
414
- alignItems: {
415
- xs: 'flex-start',
416
- sm: 'flex-start',
417
- md: 'center',
418
- },
419
- }}>
420
- <Stack
471
+ className="page-header"
421
472
  direction="row"
422
473
  sx={{
423
474
  justifyContent: 'space-between',
424
475
  alignItems: 'center',
476
+ position: 'relative',
425
477
  }}>
478
+ {!inArcsphere ? (
479
+ <Stack
480
+ direction="row"
481
+ onClick={() => navigate('/customer', { replace: true })}
482
+ sx={{
483
+ alignItems: 'center',
484
+ fontWeight: 'normal',
485
+ cursor: 'pointer',
486
+ }}>
487
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
488
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
489
+ {t('payment.customer.subscriptions.title')}
490
+ </Typography>
491
+ </Stack>
492
+ ) : (
493
+ <Box />
494
+ )}
426
495
  <Stack
427
496
  direction="row"
428
497
  sx={{
429
- alignItems: 'center',
430
- flexWrap: 'wrap',
431
498
  gap: 1,
432
499
  }}>
433
- <SubscriptionDescription subscription={data} variant="h1" />
500
+ <SubscriptionActions
501
+ subscription={data}
502
+ onChange={(action) => {
503
+ refresh();
504
+ if (action === 'batch-pay') {
505
+ checkUnpaidInvoices();
506
+ }
507
+ }}
508
+ showExtra
509
+ showDelegation
510
+ showOverdraftProtection={{
511
+ show: showOverdraftProtection,
512
+ onChange: () => refreshOverdraftProtection(),
513
+ }}
514
+ showRecharge
515
+ mode={isMobile ? 'menu-only' : 'primary-buttons'}
516
+ actionProps={{
517
+ cancel: {
518
+ variant: 'outlined',
519
+ color: 'primary',
520
+ },
521
+ recover: {
522
+ variant: 'outlined',
523
+ color: 'info',
524
+ },
525
+ pastDue: {
526
+ variant: 'outlined',
527
+ color: 'primary',
528
+ },
529
+ }}
530
+ setUp={actionSetUp}
531
+ />
434
532
  </Stack>
435
533
  </Stack>
436
- <Stack
437
- className="section-body"
534
+ <Box
438
535
  sx={{
439
- justifyContent: 'flex-start',
440
- flexWrap: 'wrap',
441
- flex: 1,
442
- width: '100%',
536
+ mt: isMobile ? 2 : 4,
537
+ display: 'flex',
538
+ border: isMobile ? `1px solid ${muiTheme.palette.divider}` : 'none',
539
+ borderRadius: isMobile ? 1 : 0,
540
+ p: isMobile ? 2 : 0,
443
541
 
444
- 'hr.MuiDivider-root:last-child': {
445
- display: 'none',
542
+ gap: {
543
+ xs: 2,
544
+ sm: 2,
545
+ md: 5,
446
546
  },
447
547
 
548
+ flexWrap: 'wrap',
549
+
448
550
  flexDirection: {
449
551
  xs: 'column',
450
552
  sm: 'column',
451
553
  md: 'row',
452
554
  },
453
555
 
454
- alignItems: 'flex-start',
455
-
456
- gap: {
457
- xs: 1,
458
- sm: 1,
459
- md: 3,
556
+ alignItems: {
557
+ xs: 'flex-start',
558
+ sm: 'flex-start',
559
+ md: 'center',
460
560
  },
461
561
  }}>
462
- <SubscriptionMetrics subscription={data} />
463
- {showOverdraftProtection && (
464
- <InfoMetric
465
- label={
466
- <Stack
467
- direction="row"
468
- spacing={0.5}
469
- sx={{
470
- alignItems: 'center',
471
- }}>
472
- <Typography
473
- component="span"
474
- sx={{
475
- fontSize: 14,
476
- fontWeight: 500,
477
- }}>
478
- {t('customer.overdraftProtection.title')}
479
- </Typography>
480
- <MuiLink
481
- href="https://www.arcblock.io/content/blog/en/payment-kit-v117-sub-guard#listen-to-the-audio-overview"
482
- target="_blank"
483
- rel="noopener noreferrer"
562
+ <Stack
563
+ direction="row"
564
+ sx={{
565
+ justifyContent: 'space-between',
566
+ alignItems: 'center',
567
+ }}>
568
+ <Stack
569
+ direction="row"
570
+ sx={{
571
+ alignItems: 'center',
572
+ flexWrap: 'wrap',
573
+ gap: 1,
574
+ }}>
575
+ <SubscriptionDescription subscription={data} variant="h1" />
576
+ </Stack>
577
+ </Stack>
578
+ <Stack
579
+ className="section-body"
580
+ sx={{
581
+ justifyContent: 'flex-start',
582
+ flexWrap: 'wrap',
583
+ flex: 1,
584
+ width: '100%',
585
+
586
+ 'hr.MuiDivider-root:last-child': {
587
+ display: 'none',
588
+ },
589
+
590
+ flexDirection: {
591
+ xs: 'column',
592
+ sm: 'column',
593
+ md: 'row',
594
+ },
595
+
596
+ alignItems: 'flex-start',
597
+
598
+ gap: {
599
+ xs: 1,
600
+ sm: 1,
601
+ md: 3,
602
+ },
603
+ }}>
604
+ <SubscriptionMetrics subscription={data} />
605
+ {showOverdraftProtection && (
606
+ <InfoMetric
607
+ label={
608
+ <Stack
609
+ direction="row"
610
+ spacing={0.5}
484
611
  sx={{
485
- display: 'flex',
486
612
  alignItems: 'center',
487
613
  }}>
488
- <Tooltip title={t('customer.overdraftProtection.learnMore')} placement="top">
489
- <HelpOutline
490
- fontSize="small"
491
- sx={{
492
- fontSize: '14px',
493
- ml: -0.2,
494
- color: 'text.secondary',
495
- cursor: 'pointer',
496
- opacity: 0.8,
497
- '&:hover': { color: 'primary.main' },
498
- }}
499
- />
500
- </Tooltip>
501
- </MuiLink>
502
- {data.overdraft_protection?.payment_details?.arcblock?.staking?.address && (
503
- <Tooltip
504
- title={
505
- <Typography sx={{ fontFamily: 'monospace', fontSize: '13px' }}>
506
- {t('customer.overdraftProtection.stakingAddress')}:
507
- {data.overdraft_protection?.payment_details?.arcblock?.staking?.address}
508
- </Typography>
509
- }
510
- arrow
511
- placement="top">
512
- <AccountBalanceWalletOutlined
513
- sx={{
514
- fontSize: '16px',
515
- color: 'text.secondary',
516
- cursor: 'pointer',
517
- ml: 1,
518
- '&:hover': { color: 'primary.main' },
519
- display: {
520
- xs: 'none',
521
- md: 'block',
522
- },
523
- }}
524
- />
525
- </Tooltip>
526
- )}
527
- </Stack>
528
- }
529
- value={renderOverdraftProtectionLabel()}
530
- />
531
- )}
532
- {isCredit && hasPendingAmount && (
533
- <InfoMetric
534
- label={t('admin.customer.creditGrants.pendingAmount')}
535
- value={
536
- <Typography
537
- sx={{
538
- color: 'error.main',
539
- }}>{`${formatBNStr(pendingAmount, data?.paymentCurrency?.decimal)} ${data?.paymentCurrency?.symbol}`}</Typography>
540
- }
541
- />
542
- )}
543
- </Stack>
614
+ <Typography
615
+ component="span"
616
+ sx={{
617
+ fontSize: 14,
618
+ fontWeight: 500,
619
+ }}>
620
+ {t('customer.overdraftProtection.title')}
621
+ </Typography>
622
+ <MuiLink
623
+ href="https://www.arcblock.io/content/blog/en/payment-kit-v117-sub-guard#listen-to-the-audio-overview"
624
+ target="_blank"
625
+ rel="noopener noreferrer"
626
+ sx={{
627
+ display: 'flex',
628
+ alignItems: 'center',
629
+ }}>
630
+ <Tooltip title={t('customer.overdraftProtection.learnMore')} placement="top">
631
+ <HelpOutline
632
+ fontSize="small"
633
+ sx={{
634
+ fontSize: '14px',
635
+ ml: -0.2,
636
+ color: 'text.secondary',
637
+ cursor: 'pointer',
638
+ opacity: 0.8,
639
+ '&:hover': { color: 'primary.main' },
640
+ }}
641
+ />
642
+ </Tooltip>
643
+ </MuiLink>
644
+ {data.overdraft_protection?.payment_details?.arcblock?.staking?.address && (
645
+ <Tooltip
646
+ title={
647
+ <Typography sx={{ fontFamily: 'monospace', fontSize: '13px' }}>
648
+ {t('customer.overdraftProtection.stakingAddress')}
649
+ {data.overdraft_protection?.payment_details?.arcblock?.staking?.address}
650
+ </Typography>
651
+ }
652
+ arrow
653
+ placement="top">
654
+ <AccountBalanceWalletOutlined
655
+ sx={{
656
+ fontSize: '16px',
657
+ color: 'text.secondary',
658
+ cursor: 'pointer',
659
+ ml: 1,
660
+ '&:hover': { color: 'primary.main' },
661
+ display: {
662
+ xs: 'none',
663
+ md: 'block',
664
+ },
665
+ }}
666
+ />
667
+ </Tooltip>
668
+ )}
669
+ </Stack>
670
+ }
671
+ value={renderOverdraftProtectionLabel()}
672
+ />
673
+ )}
674
+ {isCredit && hasPendingAmount && (
675
+ <InfoMetric
676
+ label={t('admin.customer.creditGrants.pendingAmount')}
677
+ value={
678
+ <Typography
679
+ sx={{
680
+ color: 'error.main',
681
+ }}>{`${formatBNStr(pendingAmount, data?.paymentCurrency?.decimal)} ${data?.paymentCurrency?.symbol}`}</Typography>
682
+ }
683
+ />
684
+ )}
685
+ </Stack>
686
+ </Box>
544
687
  </Box>
545
- </Box>
546
- {!isMobile ? <Divider /> : null}
547
- <Box
548
- className="section"
549
- sx={{
550
- containerType: 'inline-size',
551
- border: isMobile ? `1px solid ${muiTheme.palette.divider}` : 'none',
552
- borderRadius: isMobile ? 1 : 0,
553
- p: isMobile ? 2 : 0,
554
- }}>
555
- <Typography
556
- variant="h3"
557
- className="section-header"
688
+ {!isMobile ? <Divider /> : null}
689
+ <Box
690
+ className="section"
558
691
  sx={{
559
- mb: 3,
692
+ containerType: 'inline-size',
693
+ border: isMobile ? `1px solid ${muiTheme.palette.divider}` : 'none',
694
+ borderRadius: isMobile ? 1 : 0,
695
+ p: isMobile ? 2 : 0,
560
696
  }}>
561
- {t('admin.details')}
562
- </Typography>
563
- <InfoRowGroup
564
- sx={{
565
- display: 'grid',
566
- gridTemplateColumns: {
567
- xs: 'repeat(1, 1fr)',
568
- xl: 'repeat(2, 1fr)',
569
- },
570
- '@container (min-width: 980px)': {
571
- gridTemplateColumns: 'repeat(2, 1fr)',
572
- },
573
- '.info-row-wrapper': {
574
- gap: isMobile ? 0.5 : 1,
575
- flexDirection: {
576
- xs: 'column',
577
- xl: 'row',
578
- },
579
- alignItems: {
580
- xs: 'flex-start',
581
- xl: 'center',
697
+ <Typography
698
+ variant="h3"
699
+ className="section-header"
700
+ sx={{
701
+ mb: 3,
702
+ }}>
703
+ {t('admin.details')}
704
+ </Typography>
705
+ <InfoRowGroup
706
+ sx={{
707
+ display: 'grid',
708
+ gridTemplateColumns: {
709
+ xs: 'repeat(1, 1fr)',
710
+ xl: 'repeat(2, 1fr)',
582
711
  },
583
712
  '@container (min-width: 980px)': {
584
- flexDirection: 'row',
585
- alignItems: 'center',
713
+ gridTemplateColumns: 'repeat(2, 1fr)',
586
714
  },
587
- },
588
- '.currency-name': {
589
- color: 'text.secondary',
590
- lineHeight: 1,
591
- fontSize: 14,
592
- },
593
- '.tx-link-text': {
594
- fontSize: 14,
595
- lineHeight: 1,
596
- },
597
- }}>
598
- <InfoRow
599
- label={t('common.customer')}
600
- value={<CustomerLink customer={data.customer} linked={false} size="small" />}
601
- />
602
- <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
603
-
604
- <InfoRow
605
- label={t('admin.subscription.currentPeriod')}
606
- value={[formatTime(data.current_period_start * 1000), formatTime(data.current_period_end * 1000)].join(
607
- ' ~ '
608
- )}
609
- />
610
- <InfoRow
611
- label={t('admin.subscription.trialingPeriod')}
612
- value={
613
- data.trial_end && data.trial_start
614
- ? [formatTime(data.trial_start * 1000), formatTime(data.trial_end * 1000)].join(' ~ ')
615
- : ''
616
- }
617
- />
618
- {data.status === 'paused' && !!data.pause_collection?.resumes_at && (
619
- <InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
620
- )}
621
-
622
- <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
623
-
624
- <InfoRow
625
- label={t('admin.paymentMethod._name')}
626
- value={<Currency logo={data.paymentMethod?.logo} name={data.paymentMethod?.name} size={16} />}
627
- />
628
- <InfoRow
629
- label={t('admin.paymentCurrency.name')}
630
- value={
631
- <Stack direction="row" spacing={2}>
632
- <Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} size={16} />
633
- {canChangePaymentMethod(data) && (
634
- <Button
635
- variant="text"
636
- sx={{ color: 'text.link' }}
637
- size="small"
638
- onClick={async () => {
639
- // only check unpaid invoices when overdraft protection is enabled
640
- if (data?.overdraft_protection?.enabled) {
641
- const result = await checkUnpaidInvoices();
642
- if (result) {
643
- return;
644
- }
645
- }
646
- navigate(`/customer/subscription/${data.id}/change-payment`);
647
- }}>
648
- {t('payment.customer.subscriptions.changePayment')}
649
- </Button>
650
- )}
651
- </Stack>
652
- }
653
- />
654
- {(data as any).paymentMethodDetails && (
715
+ '.info-row-wrapper': {
716
+ gap: isMobile ? 0.5 : 1,
717
+ flexDirection: {
718
+ xs: 'column',
719
+ xl: 'row',
720
+ },
721
+ alignItems: {
722
+ xs: 'flex-start',
723
+ xl: 'center',
724
+ },
725
+ '@container (min-width: 980px)': {
726
+ flexDirection: 'row',
727
+ alignItems: 'center',
728
+ },
729
+ },
730
+ '.currency-name': {
731
+ color: 'text.secondary',
732
+ lineHeight: 1,
733
+ fontSize: 14,
734
+ },
735
+ '.tx-link-text': {
736
+ fontSize: 14,
737
+ lineHeight: 1,
738
+ },
739
+ }}>
655
740
  <InfoRow
656
- label={t('admin.subscription.payerAddress')}
657
- value={
658
- <PaymentMethodInfo
659
- subscriptionId={id}
660
- customer={data.customer}
661
- paymentMethodDetails={(data as any).paymentMethodDetails}
662
- editable={['active', 'trialing', 'past_due'].includes(data.status)}
663
- onUpdate={() => {
664
- refresh();
665
- checkUnpaidInvoices();
666
- }}
667
- paymentMethodType={data.paymentMethod?.type}
668
- />
669
- }
741
+ label={t('common.customer')}
742
+ value={<CustomerLink customer={data.customer} linked={false} size="small" />}
670
743
  />
671
- )}
744
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
672
745
 
673
- {data.payment_details && hasDelegateTxHash(data.payment_details, data.paymentMethod) && (
674
746
  <InfoRow
675
- label={t('common.delegateTxHash')}
676
- value={<TxLink details={data.payment_details} method={data.paymentMethod} size={16} />}
747
+ label={t('admin.subscription.currentPeriod')}
748
+ value={[formatTime(data.current_period_start * 1000), formatTime(data.current_period_end * 1000)].join(
749
+ ' ~ '
750
+ )}
677
751
  />
678
- )}
679
- {data.paymentMethod?.type === 'arcblock' && data.payment_details?.arcblock?.staking?.tx_hash && (
680
752
  <InfoRow
681
- label={t('common.stakeTxHash')}
753
+ label={t('admin.subscription.trialingPeriod')}
682
754
  value={
683
- <Box
684
- sx={{
685
- '.MuiTypography-root': {
686
- color: 'text.link',
687
- },
688
- }}>
689
- <TxLink
690
- details={{ arcblock: { tx_hash: data.payment_details?.arcblock?.staking?.tx_hash, payer: '' } }}
691
- method={data.paymentMethod}
692
- size={16}
693
- />
694
- </Box>
755
+ data.trial_end && data.trial_start
756
+ ? [formatTime(data.trial_start * 1000), formatTime(data.trial_end * 1000)].join(' ~ ')
757
+ : ''
695
758
  }
696
759
  />
697
- )}
698
- {!!data.recovered_from && (
760
+ {data.status === 'paused' && !!data.pause_collection?.resumes_at && (
761
+ <InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
762
+ )}
763
+
764
+ <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
765
+
699
766
  <InfoRow
700
- label={t('common.recoverFrom')}
767
+ label={t('admin.paymentMethod._name')}
768
+ value={<Currency logo={data.paymentMethod?.logo} name={data.paymentMethod?.name} size={16} />}
769
+ />
770
+ <InfoRow
771
+ label={t('admin.paymentCurrency.name')}
701
772
  value={
702
- <Link to={`/customer/subscription/${data.recovered_from}`} target="_blank">
703
- {data.recovered_from}
704
- </Link>
773
+ <Stack direction="row" spacing={2}>
774
+ <Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} size={16} />
775
+ {canChangePaymentMethod(data) && (
776
+ <Button
777
+ variant="text"
778
+ sx={{ color: 'text.link' }}
779
+ size="small"
780
+ onClick={async () => {
781
+ // only check unpaid invoices when overdraft protection is enabled
782
+ if (data?.overdraft_protection?.enabled) {
783
+ const result = await checkUnpaidInvoices();
784
+ if (result) {
785
+ return;
786
+ }
787
+ }
788
+ navigate(`/customer/subscription/${data.id}/change-payment`);
789
+ }}>
790
+ {t('payment.customer.subscriptions.changePayment')}
791
+ </Button>
792
+ )}
793
+ </Stack>
705
794
  }
706
795
  />
707
- )}
708
- </InfoRowGroup>
709
- </Box>
710
- <Divider />
796
+ {hasDynamicPricing && data.paymentMethod?.type !== 'stripe' && (
797
+ <InfoRow
798
+ label={
799
+ <Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
800
+ <span>{t('common.slippage')}</span>
801
+ <Tooltip title={t('common.slippageTooltip')} placement="top">
802
+ <HelpOutline sx={{ fontSize: '0.875rem', color: 'text.secondary', cursor: 'help' }} />
803
+ </Tooltip>
804
+ </Stack>
805
+ }
806
+ value={
807
+ <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
808
+ <Typography color={displayMinAcceptableRate ? 'text.primary' : 'text.secondary'}>
809
+ {formatExchangeRateDisplay(displayMinAcceptableRate) || '—'}
810
+ </Typography>
811
+ <Button
812
+ variant="text"
813
+ size="small"
814
+ sx={{ color: 'text.link', minWidth: 'auto', p: 0.5 }}
815
+ onClick={() => setSlippageDialogOpen(true)}
816
+ disabled={slippageSaving}>
817
+ <SettingsOutlined sx={{ fontSize: '1rem' }} />
818
+ </Button>
819
+ </Stack>
820
+ }
821
+ />
822
+ )}
823
+ {(data as any).paymentMethodDetails && (
824
+ <InfoRow
825
+ label={t('admin.subscription.payerAddress')}
826
+ value={
827
+ <PaymentMethodInfo
828
+ subscriptionId={id}
829
+ customer={data.customer}
830
+ paymentMethodDetails={(data as any).paymentMethodDetails}
831
+ editable={['active', 'trialing', 'past_due'].includes(data.status)}
832
+ onUpdate={() => {
833
+ refresh();
834
+ checkUnpaidInvoices();
835
+ }}
836
+ paymentMethodType={data.paymentMethod?.type}
837
+ />
838
+ }
839
+ />
840
+ )}
711
841
 
712
- {/* Discount Information */}
713
- {(data as any).discountStats && (
714
- <Box className="section">
715
- <Typography variant="h3" className="section-header" sx={{ mb: 1.5 }}>
716
- {t('admin.subscription.discount')}
717
- </Typography>
718
- <Box className="section-body">
719
- <DiscountInfo discountStats={(data as any).discountStats} />
720
- </Box>
842
+ {data.payment_details && hasDelegateTxHash(data.payment_details, data.paymentMethod) && (
843
+ <InfoRow
844
+ label={t('common.delegateTxHash')}
845
+ value={<TxLink details={data.payment_details} method={data.paymentMethod} size={16} />}
846
+ />
847
+ )}
848
+ {data.paymentMethod?.type === 'arcblock' && data.payment_details?.arcblock?.staking?.tx_hash && (
849
+ <InfoRow
850
+ label={t('common.stakeTxHash')}
851
+ value={
852
+ <Box
853
+ sx={{
854
+ '.MuiTypography-root': {
855
+ color: 'text.link',
856
+ },
857
+ }}>
858
+ <TxLink
859
+ details={{ arcblock: { tx_hash: data.payment_details?.arcblock?.staking?.tx_hash, payer: '' } }}
860
+ method={data.paymentMethod}
861
+ size={16}
862
+ />
863
+ </Box>
864
+ }
865
+ />
866
+ )}
867
+ {!!data.recovered_from && (
868
+ <InfoRow
869
+ label={t('common.recoverFrom')}
870
+ value={
871
+ <Link to={`/customer/subscription/${data.recovered_from}`} target="_blank">
872
+ {data.recovered_from}
873
+ </Link>
874
+ }
875
+ />
876
+ )}
877
+ </InfoRowGroup>
721
878
  </Box>
722
- )}
723
- <Box className="divider" />
879
+ <Divider />
724
880
 
725
- <Box className="section">
726
- <Typography
727
- variant="h3"
728
- className="section-header"
729
- sx={{
730
- mb: 1.5,
731
- }}>
732
- {t('admin.products')}
733
- </Typography>
734
- <Box className="section-body">
735
- <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
736
- </Box>
737
- </Box>
738
- {(() => {
739
- const vendorServices = data.items?.map((item) => item.price?.product?.vendor_config || []).flat();
740
-
741
- if (!vendorServices || vendorServices.length === 0) return null;
742
-
743
- return (
744
- <>
745
- <Divider />
746
- <Box className="section">
747
- <VendorServiceList
748
- vendorServices={vendorServices}
749
- subscriptionId={id}
750
- isCanceled={data.status === 'canceled'}
751
- />
752
- </Box>
753
- </>
754
- );
755
- })()}
756
- <Box className="divider" />
757
- {isCredit ? (
758
- <>
759
- <Divider />
881
+ {/* Discount Information */}
882
+ {(data as any).discountStats && (
760
883
  <Box className="section">
761
- <Typography variant="h3" className="section-header">
762
- {t('admin.creditGrants.tab')}
884
+ <Typography variant="h3" className="section-header" sx={{ mb: 1.5 }}>
885
+ {t('admin.subscription.discount')}
763
886
  </Typography>
764
887
  <Box className="section-body">
765
- <CreditGrantsList customer_id={data.customer_id} subscription_id={data.id} mode="portal" />
888
+ <DiscountInfo discountStats={(data as any).discountStats} />
766
889
  </Box>
767
890
  </Box>
891
+ )}
892
+ <Box className="divider" />
893
+
894
+ <Box className="section">
895
+ <Typography
896
+ variant="h3"
897
+ className="section-header"
898
+ sx={{
899
+ mb: 1.5,
900
+ }}>
901
+ {t('admin.products')}
902
+ </Typography>
903
+ <Box className="section-body">
904
+ <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
905
+ </Box>
906
+ </Box>
907
+ {(() => {
908
+ const vendorServices = data.items?.map((item) => item.price?.product?.vendor_config || []).flat();
909
+
910
+ if (!vendorServices || vendorServices.length === 0) return null;
911
+
912
+ return (
913
+ <>
914
+ <Divider />
915
+ <Box className="section">
916
+ <VendorServiceList
917
+ vendorServices={vendorServices}
918
+ subscriptionId={id}
919
+ isCanceled={data.status === 'canceled'}
920
+ />
921
+ </Box>
922
+ </>
923
+ );
924
+ })()}
925
+ <Box className="divider" />
926
+ {isCredit ? (
768
927
  <Box className="section">
769
928
  <Typography variant="h3" className="section-header">
770
929
  {t('admin.creditTransactions.title')}
@@ -778,23 +937,44 @@ export default function CustomerSubscriptionDetail() {
778
937
  />
779
938
  </Box>
780
939
  </Box>
781
- </>
782
- ) : (
783
- <Box className="section">
784
- <Typography variant="h3" className="section-header">
785
- {t('customer.invoiceHistory')}
786
- </Typography>
787
- <Box className="section-body">
788
- <CustomerInvoiceList
789
- subscription_id={data.id}
790
- type="table"
791
- include_staking
792
- status="open,paid,uncollectible,void"
793
- />
940
+ ) : (
941
+ <Box className="section">
942
+ <Typography variant="h3" className="section-header">
943
+ {t('customer.invoiceHistory')}
944
+ </Typography>
945
+ <Box className="section-body">
946
+ <CustomerInvoiceList
947
+ subscription_id={data.id}
948
+ type="table"
949
+ include_staking
950
+ status="open,paid,uncollectible,void"
951
+ />
952
+ </Box>
794
953
  </Box>
795
- </Box>
796
- )}
797
- </Root>
954
+ )}
955
+ </Root>
956
+ <Dialog
957
+ open={slippageDialogOpen}
958
+ onClose={handleSlippageDialogClose}
959
+ maxWidth="xs"
960
+ fullWidth
961
+ PaperProps={{ sx: { borderRadius: 2 } }}>
962
+ <DialogTitle>{t('payment.checkout.quote.slippageLimit.title')}</DialogTitle>
963
+ <DialogContent sx={{ pb: 3 }}>
964
+ <SlippageConfig
965
+ value={slippageConfig.percent}
966
+ onChange={(percent: number) => setSlippageConfig((prev) => ({ ...prev, percent }))}
967
+ config={slippageConfig}
968
+ onConfigChange={setSlippageConfig}
969
+ exchangeRate={liveRateInfo?.rate || null}
970
+ baseCurrency={liveRateInfo?.base_currency || 'USD'}
971
+ disabled={slippageSaving}
972
+ onCancel={handleSlippageDialogClose}
973
+ onSave={handleSlippageSave}
974
+ />
975
+ </DialogContent>
976
+ </Dialog>
977
+ </>
798
978
  );
799
979
  }
800
980