@blocklet/payment-react 1.24.4 → 1.25.1

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 (98) hide show
  1. package/es/components/auto-topup/modal.d.ts +2 -0
  2. package/es/components/auto-topup/modal.js +48 -6
  3. package/es/components/auto-topup/product-card.d.ts +16 -1
  4. package/es/components/auto-topup/product-card.js +97 -15
  5. package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
  6. package/es/components/dynamic-pricing-unavailable.js +58 -0
  7. package/es/components/loading-amount.d.ts +17 -0
  8. package/es/components/loading-amount.js +46 -0
  9. package/es/components/price-change-confirm.d.ts +18 -0
  10. package/es/components/price-change-confirm.js +107 -0
  11. package/es/components/quote-details-panel.d.ts +21 -0
  12. package/es/components/quote-details-panel.js +170 -0
  13. package/es/components/quote-lock-banner.d.ts +7 -0
  14. package/es/components/quote-lock-banner.js +79 -0
  15. package/es/components/slippage-config.d.ts +20 -0
  16. package/es/components/slippage-config.js +261 -0
  17. package/es/history/invoice/list.js +125 -15
  18. package/es/hooks/dynamic-pricing.d.ts +102 -0
  19. package/es/hooks/dynamic-pricing.js +393 -0
  20. package/es/index.d.ts +6 -1
  21. package/es/index.js +9 -1
  22. package/es/libs/util.d.ts +42 -5
  23. package/es/libs/util.js +345 -57
  24. package/es/locales/en.js +114 -3
  25. package/es/locales/zh.js +114 -3
  26. package/es/payment/form/index.d.ts +4 -1
  27. package/es/payment/form/index.js +454 -22
  28. package/es/payment/index.d.ts +1 -1
  29. package/es/payment/index.js +279 -16
  30. package/es/payment/product-item.d.ts +26 -1
  31. package/es/payment/product-item.js +330 -51
  32. package/es/payment/summary-section/promotion-section.d.ts +32 -0
  33. package/es/payment/summary-section/promotion-section.js +143 -0
  34. package/es/payment/summary-section/total-section.d.ts +39 -0
  35. package/es/payment/summary-section/total-section.js +83 -0
  36. package/es/payment/summary.d.ts +17 -2
  37. package/es/payment/summary.js +300 -253
  38. package/es/types/index.d.ts +11 -0
  39. package/lib/components/auto-topup/modal.d.ts +2 -0
  40. package/lib/components/auto-topup/modal.js +54 -6
  41. package/lib/components/auto-topup/product-card.d.ts +16 -1
  42. package/lib/components/auto-topup/product-card.js +75 -7
  43. package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
  44. package/lib/components/dynamic-pricing-unavailable.js +81 -0
  45. package/lib/components/loading-amount.d.ts +17 -0
  46. package/lib/components/loading-amount.js +53 -0
  47. package/lib/components/price-change-confirm.d.ts +18 -0
  48. package/lib/components/price-change-confirm.js +157 -0
  49. package/lib/components/quote-details-panel.d.ts +21 -0
  50. package/lib/components/quote-details-panel.js +226 -0
  51. package/lib/components/quote-lock-banner.d.ts +7 -0
  52. package/lib/components/quote-lock-banner.js +93 -0
  53. package/lib/components/slippage-config.d.ts +20 -0
  54. package/lib/components/slippage-config.js +316 -0
  55. package/lib/history/invoice/list.js +167 -27
  56. package/lib/hooks/dynamic-pricing.d.ts +102 -0
  57. package/lib/hooks/dynamic-pricing.js +390 -0
  58. package/lib/index.d.ts +6 -1
  59. package/lib/index.js +32 -0
  60. package/lib/libs/util.d.ts +42 -5
  61. package/lib/libs/util.js +367 -49
  62. package/lib/locales/en.js +114 -3
  63. package/lib/locales/zh.js +114 -3
  64. package/lib/payment/form/index.d.ts +4 -1
  65. package/lib/payment/form/index.js +476 -20
  66. package/lib/payment/index.d.ts +1 -1
  67. package/lib/payment/index.js +308 -14
  68. package/lib/payment/product-item.d.ts +26 -1
  69. package/lib/payment/product-item.js +270 -35
  70. package/lib/payment/summary-section/promotion-section.d.ts +32 -0
  71. package/lib/payment/summary-section/promotion-section.js +133 -0
  72. package/lib/payment/summary-section/total-section.d.ts +39 -0
  73. package/lib/payment/summary-section/total-section.js +117 -0
  74. package/lib/payment/summary.d.ts +17 -2
  75. package/lib/payment/summary.js +205 -127
  76. package/lib/types/index.d.ts +11 -0
  77. package/package.json +3 -3
  78. package/src/components/auto-topup/modal.tsx +59 -6
  79. package/src/components/auto-topup/product-card.tsx +118 -11
  80. package/src/components/dynamic-pricing-unavailable.tsx +69 -0
  81. package/src/components/loading-amount.tsx +66 -0
  82. package/src/components/price-change-confirm.tsx +136 -0
  83. package/src/components/quote-details-panel.tsx +218 -0
  84. package/src/components/quote-lock-banner.tsx +99 -0
  85. package/src/components/slippage-config.tsx +336 -0
  86. package/src/history/invoice/list.tsx +143 -9
  87. package/src/hooks/dynamic-pricing.ts +617 -0
  88. package/src/index.ts +9 -0
  89. package/src/libs/util.ts +473 -58
  90. package/src/locales/en.tsx +117 -0
  91. package/src/locales/zh.tsx +111 -0
  92. package/src/payment/form/index.tsx +561 -19
  93. package/src/payment/index.tsx +349 -10
  94. package/src/payment/product-item.tsx +451 -37
  95. package/src/payment/summary-section/promotion-section.tsx +172 -0
  96. package/src/payment/summary-section/total-section.tsx +141 -0
  97. package/src/payment/summary.tsx +334 -192
  98. package/src/types/index.ts +15 -0
@@ -38,6 +38,7 @@ import FormInput from '../input';
38
38
  import StripeCheckout from '../../payment/form/stripe';
39
39
  import AutoTopupProductCard from './product-card';
40
40
  import FormLabel from '../label';
41
+ import type { SlippageConfigValue } from '../slippage-config';
41
42
 
42
43
  export interface AutoTopupFormData {
43
44
  enabled: boolean;
@@ -61,6 +62,7 @@ export interface AutoTopupFormData {
61
62
  city?: string;
62
63
  postal_code?: string;
63
64
  };
65
+ slippage_config?: SlippageConfigValue | null;
64
66
  }
65
67
 
66
68
  export interface AutoTopupModalProps {
@@ -96,6 +98,12 @@ const DEFAULT_VALUES = {
96
98
  price_id: '',
97
99
  daily_max_amount: 0,
98
100
  daily_max_attempts: 0,
101
+ slippage_config: null as SlippageConfigValue | null,
102
+ };
103
+
104
+ const fetchExchangeRate = async (currencyId: string) => {
105
+ const { data } = await api.post('/api/exchange-rates/validate', { currency: currencyId });
106
+ return data;
99
107
  };
100
108
 
101
109
  export const waitForAutoRechargeComplete = async (configId: string) => {
@@ -298,6 +306,8 @@ export default function AutoTopup({
298
306
  const { t, locale } = useLocaleContext();
299
307
  const { session, connect, settings } = usePaymentContext();
300
308
  const [changePaymentMethod, setChangePaymentMethod] = useState(false);
309
+ const [slippagePercent, setSlippagePercent] = useState(0.5);
310
+ const [slippageConfig, setSlippageConfig] = useState<SlippageConfigValue | null>(null);
301
311
  const [state, setState] = useSetState({
302
312
  loading: false,
303
313
  submitting: false,
@@ -333,6 +343,12 @@ export default function AutoTopup({
333
343
  const quantity = watch('quantity') as number;
334
344
  const rechargeCurrencyId = watch('recharge_currency_id');
335
345
 
346
+ // Determine payment method type early for exchange rate fetching logic
347
+ const selectedMethod = settings.paymentMethods.find((method) => {
348
+ return method.payment_currencies.find((c) => c.id === rechargeCurrencyId);
349
+ });
350
+ const isStripePayment = selectedMethod?.type === 'stripe';
351
+
336
352
  const handleClose = () => {
337
353
  setState({
338
354
  loading: false,
@@ -359,6 +375,27 @@ export default function AutoTopup({
359
375
  max_amount: data.daily_limits?.max_amount || 0,
360
376
  max_attempts: data.daily_limits?.max_attempts || 0,
361
377
  });
378
+ // Set slippage config from existing data
379
+ if (data.slippage_config) {
380
+ setSlippageConfig(data.slippage_config);
381
+ setSlippagePercent(data.slippage_config.percent ?? 0.5);
382
+ }
383
+ },
384
+ });
385
+
386
+ // Check if price is dynamic pricing
387
+ const isDynamicPricing = config?.price?.pricing_type === 'dynamic';
388
+
389
+ // Fetch exchange rate for dynamic pricing with auto-refresh every 30 seconds
390
+ // Skip for Stripe payments as they use USD directly
391
+ const { data: exchangeRateData } = useRequest(() => fetchExchangeRate(rechargeCurrencyId), {
392
+ refreshDeps: [rechargeCurrencyId],
393
+ ready: !!rechargeCurrencyId && isDynamicPricing && enabled && !isStripePayment,
394
+ pollingInterval: 30000, // Refresh every 30 seconds
395
+ pollingWhenHidden: false, // Stop polling when tab is hidden
396
+ onError: (error: any) => {
397
+ // Silently handle error - exchange rate is optional for display
398
+ console.warn('Failed to fetch exchange rate:', error.message);
362
399
  },
363
400
  });
364
401
 
@@ -469,7 +506,7 @@ export default function AutoTopup({
469
506
  setState({ submitting: true });
470
507
 
471
508
  try {
472
- const submitData = {
509
+ const submitData: Record<string, any> = {
473
510
  customer_id: session?.user?.did,
474
511
  enabled: formData.enabled,
475
512
  threshold: formData.threshold,
@@ -484,6 +521,14 @@ export default function AutoTopup({
484
521
  change_payment_method: changePaymentMethod,
485
522
  };
486
523
 
524
+ // Include slippage_config for dynamic pricing
525
+ if (isDynamicPricing && slippageConfig) {
526
+ submitData.slippage_config = {
527
+ ...slippageConfig,
528
+ updated_at_ms: Date.now(),
529
+ };
530
+ }
531
+
487
532
  const { data } = await api.post('/api/auto-recharge-configs/submit', submitData);
488
533
 
489
534
  if (data.balanceResult && !data.balanceResult.sufficient) {
@@ -514,11 +559,7 @@ export default function AutoTopup({
514
559
  };
515
560
 
516
561
  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';
562
+ const showStripeForm = state.authorizationRequired && isStripePayment;
522
563
 
523
564
  const onStripeConfirm = async () => {
524
565
  await handleConnected();
@@ -679,6 +720,18 @@ export default function AutoTopup({
679
720
  onQuantityChange={(newQuantity) => setValue('quantity', newQuantity)}
680
721
  maxQuantity={9999}
681
722
  minQuantity={1}
723
+ exchangeRate={exchangeRateData?.rate}
724
+ isDynamicPricing={isDynamicPricing && !isStripePayment}
725
+ exchangeRateData={exchangeRateData}
726
+ slippageConfig={slippageConfig}
727
+ slippagePercent={slippagePercent}
728
+ onSlippageChange={(newSlippageConfig) => {
729
+ setSlippageConfig(newSlippageConfig);
730
+ if (newSlippageConfig.percent !== undefined) {
731
+ setSlippagePercent(newSlippageConfig.percent);
732
+ }
733
+ }}
734
+ disabled={state.submitting}
682
735
  />
683
736
  )}
684
737
 
@@ -1,10 +1,28 @@
1
1
  import { Stack, Typography, TextField, Card } from '@mui/material';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { useState } from 'react';
3
+ import { useState, useMemo } from 'react';
4
4
 
5
5
  import type { TPaymentCurrency } from '@blocklet/payment-types';
6
6
  import ProductCard from '../../payment/product-card';
7
- import { formatPrice, formatNumber, formatCreditForCheckout } from '../../libs/util';
7
+ import {
8
+ formatPrice,
9
+ formatNumber,
10
+ formatDynamicPrice,
11
+ formatUsdAmount,
12
+ formatExchangeRate,
13
+ formatToDatetime,
14
+ formatCreditForCheckout,
15
+ } from '../../libs/util';
16
+ import QuoteDetailsPanel from '../quote-details-panel';
17
+ import type { SlippageConfigValue } from '../slippage-config';
18
+
19
+ interface ExchangeRateData {
20
+ rate?: string;
21
+ provider_name?: string;
22
+ provider_id?: string;
23
+ provider_display?: string; // Human-readable: "CoinGecko" or "CoinGecko (2 sources)"
24
+ timestamp_ms?: number;
25
+ }
8
26
 
9
27
  interface AutoTopupProductCardProps {
10
28
  product: any;
@@ -15,6 +33,13 @@ interface AutoTopupProductCardProps {
15
33
  maxQuantity?: number;
16
34
  minQuantity?: number;
17
35
  creditCurrency: TPaymentCurrency;
36
+ exchangeRate?: string | null;
37
+ isDynamicPricing?: boolean;
38
+ exchangeRateData?: ExchangeRateData | null;
39
+ slippageConfig?: SlippageConfigValue | null;
40
+ slippagePercent?: number;
41
+ onSlippageChange?: (config: SlippageConfigValue) => void;
42
+ disabled?: boolean;
18
43
  }
19
44
 
20
45
  export default function AutoTopupProductCard({
@@ -26,11 +51,48 @@ export default function AutoTopupProductCard({
26
51
  maxQuantity = 99,
27
52
  minQuantity = 1,
28
53
  creditCurrency,
54
+ exchangeRate = null,
55
+ isDynamicPricing = false,
56
+ exchangeRateData = null,
57
+ slippageConfig = null,
58
+ slippagePercent = 0.5,
59
+ onSlippageChange = undefined,
60
+ disabled = false,
29
61
  }: AutoTopupProductCardProps) {
30
62
  const { t, locale } = useLocaleContext();
31
63
  const [localQuantity, setLocalQuantity] = useState<number | undefined>(quantity);
32
64
  const localQuantityNum = Number(localQuantity) || 0;
33
65
 
66
+ // Calculate payment amount for dynamic pricing
67
+ const { paymentAmount, usdReferenceDisplay } = useMemo(() => {
68
+ if (!isDynamicPricing || !exchangeRate || !price?.base_amount) {
69
+ // Fixed pricing: use existing formatPrice
70
+ return {
71
+ paymentAmount: formatPrice(price, currency, product?.unit_label, localQuantity, true),
72
+ usdReferenceDisplay: null,
73
+ };
74
+ }
75
+
76
+ // Dynamic pricing: calculate token amount from base_amount / exchange_rate
77
+ const baseAmount = Number(price.base_amount) * localQuantityNum;
78
+ const rate = Number(exchangeRate);
79
+ if (rate <= 0 || !Number.isFinite(baseAmount)) {
80
+ return {
81
+ paymentAmount: formatPrice(price, currency, product?.unit_label, localQuantity, true),
82
+ usdReferenceDisplay: null,
83
+ };
84
+ }
85
+
86
+ const tokenAmount = baseAmount / rate;
87
+ const formattedToken = formatDynamicPrice(tokenAmount, true, 6);
88
+ const formattedUsd = formatUsdAmount(baseAmount.toString(), locale);
89
+
90
+ return {
91
+ paymentAmount: `${formattedToken} ${currency.symbol}`,
92
+ usdReferenceDisplay: formattedUsd ? `≈ $${formattedUsd}` : null,
93
+ };
94
+ }, [isDynamicPricing, exchangeRate, price, currency, product?.unit_label, localQuantity, localQuantityNum, locale]);
95
+
34
96
  const handleQuantityChange = (newQuantity: number) => {
35
97
  if (!newQuantity) {
36
98
  setLocalQuantity(undefined);
@@ -132,7 +194,7 @@ export default function AutoTopupProductCard({
132
194
  direction="row"
133
195
  sx={{
134
196
  justifyContent: 'space-between',
135
- alignItems: 'center',
197
+ alignItems: 'flex-start',
136
198
  mt: 2,
137
199
  pt: 2,
138
200
  borderTop: '1px solid',
@@ -145,14 +207,59 @@ export default function AutoTopupProductCard({
145
207
  }}>
146
208
  {t('payment.autoTopup.rechargeAmount')}
147
209
  </Typography>
148
- <Typography
149
- variant="h6"
150
- sx={{
151
- fontWeight: 600,
152
- color: 'text.primary',
153
- }}>
154
- {formatPrice(price, currency, product?.unit_label, localQuantity, true)}
155
- </Typography>
210
+ <Stack sx={{ alignItems: 'flex-end' }}>
211
+ <Typography
212
+ variant="h6"
213
+ sx={{
214
+ fontWeight: 600,
215
+ color: 'text.primary',
216
+ }}>
217
+ {paymentAmount}
218
+ </Typography>
219
+ {usdReferenceDisplay && (
220
+ <Typography
221
+ sx={{
222
+ fontSize: '0.7875rem',
223
+ color: 'text.lighter',
224
+ }}>
225
+ {usdReferenceDisplay}
226
+ </Typography>
227
+ )}
228
+ {/* Dynamic Pricing - Exchange Rate Panel */}
229
+ {isDynamicPricing && exchangeRateData?.rate && (
230
+ <QuoteDetailsPanel
231
+ rateLine={t('payment.checkout.quote.rateLine', {
232
+ symbol: currency.symbol,
233
+ rate: `$${formatExchangeRate(exchangeRateData.rate) || exchangeRateData.rate}`,
234
+ })}
235
+ rows={[
236
+ {
237
+ label: t('payment.checkout.quote.detailProvider'),
238
+ value: exchangeRateData?.provider_display || exchangeRateData?.provider_name || '—',
239
+ },
240
+ {
241
+ label: t('payment.checkout.quote.detailUpdatedAt'),
242
+ value: exchangeRateData?.timestamp_ms ? formatToDatetime(exchangeRateData.timestamp_ms, locale) : '—',
243
+ },
244
+ {
245
+ label: t('payment.checkout.quote.detailSlippage'),
246
+ value:
247
+ slippageConfig?.mode === 'rate' && slippageConfig.min_acceptable_rate
248
+ ? `$${formatExchangeRate(slippageConfig.min_acceptable_rate) || slippageConfig.min_acceptable_rate}`
249
+ : `${slippageConfig?.percent ?? slippagePercent}%`,
250
+ isSlippage: true,
251
+ },
252
+ ]}
253
+ isSubscription
254
+ slippageValue={slippageConfig?.percent ?? slippagePercent}
255
+ slippageConfig={slippageConfig || undefined}
256
+ onSlippageChange={onSlippageChange}
257
+ exchangeRate={exchangeRateData?.rate}
258
+ baseCurrency="USD"
259
+ disabled={disabled}
260
+ />
261
+ )}
262
+ </Stack>
156
263
  </Stack>
157
264
  </Card>
158
265
  );
@@ -0,0 +1,69 @@
1
+ import { Alert, AlertTitle, Typography, Button, Box, CircularProgress, type SxProps } from '@mui/material';
2
+ import { ErrorOutline, Refresh } from '@mui/icons-material';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { useState } from 'react';
5
+
6
+ interface DynamicPricingUnavailableProps {
7
+ error?: string;
8
+ onRetry?: () => void | Promise<void>;
9
+ showRetry?: boolean;
10
+ sx?: SxProps;
11
+ }
12
+
13
+ export default function DynamicPricingUnavailable({
14
+ error = undefined,
15
+ onRetry = undefined,
16
+ showRetry = true,
17
+ sx = undefined,
18
+ }: DynamicPricingUnavailableProps) {
19
+ const { t } = useLocaleContext();
20
+ const [retrying, setRetrying] = useState(false);
21
+
22
+ // Log technical errors to console, but don't display them to users
23
+ if (error) {
24
+ console.error('[Dynamic Pricing Error]', error);
25
+ }
26
+
27
+ const handleRetry = async () => {
28
+ if (!onRetry || retrying) return;
29
+ setRetrying(true);
30
+ try {
31
+ await onRetry();
32
+ } finally {
33
+ setRetrying(false);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <Alert
39
+ severity="warning"
40
+ icon={<ErrorOutline />}
41
+ sx={{
42
+ borderRadius: 2,
43
+ '& .MuiAlert-message': {
44
+ width: '100%',
45
+ },
46
+ ...sx,
47
+ }}>
48
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', width: '100%' }}>
49
+ <Box>
50
+ <AlertTitle sx={{ fontWeight: 600 }}>{t('payment.dynamicPricing.unavailable.title')}</AlertTitle>
51
+ <Typography variant="body2" sx={{ color: 'text.secondary', mt: 0.5 }}>
52
+ {t('payment.dynamicPricing.unavailable.message')}
53
+ </Typography>
54
+ </Box>
55
+ {showRetry && onRetry && (
56
+ <Button
57
+ size="small"
58
+ variant="outlined"
59
+ onClick={handleRetry}
60
+ disabled={retrying}
61
+ startIcon={retrying ? <CircularProgress size={16} /> : <Refresh />}
62
+ sx={{ ml: 2, flexShrink: 0 }}>
63
+ {t('payment.dynamicPricing.unavailable.retry')}
64
+ </Button>
65
+ )}
66
+ </Box>
67
+ </Alert>
68
+ );
69
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * LoadingAmount Component
3
+ *
4
+ * Displays amount with skeleton loading state during currency switch.
5
+ * Only shows skeleton when isRateLoading is true (currency switch scenario).
6
+ */
7
+
8
+ import { Skeleton, Typography } from '@mui/material';
9
+ import type { SxProps, Theme } from '@mui/material';
10
+ import { useEffect, useRef, useState } from 'react';
11
+
12
+ export interface LoadingAmountProps {
13
+ value: string;
14
+ loading?: boolean;
15
+ skeletonWidth?: number;
16
+ height?: number;
17
+ sx?: SxProps<Theme>;
18
+ animateValueChange?: boolean;
19
+ transitionDuration?: number;
20
+ }
21
+
22
+ export default function LoadingAmount({
23
+ value,
24
+ loading = false,
25
+ skeletonWidth = 80,
26
+ height = 24,
27
+ sx = {},
28
+ animateValueChange = false,
29
+ transitionDuration = 300,
30
+ }: LoadingAmountProps) {
31
+ const [displayValue, setDisplayValue] = useState(value);
32
+ const [isTransitioning, setIsTransitioning] = useState(false);
33
+ const prevValueRef = useRef(value);
34
+
35
+ useEffect(() => {
36
+ if (value !== prevValueRef.current) {
37
+ prevValueRef.current = value;
38
+ if (animateValueChange && !loading) {
39
+ setIsTransitioning(true);
40
+ const timer = setTimeout(() => {
41
+ setDisplayValue(value);
42
+ setIsTransitioning(false);
43
+ }, transitionDuration / 2);
44
+ return () => clearTimeout(timer);
45
+ }
46
+ setDisplayValue(value);
47
+ }
48
+ return undefined;
49
+ }, [value, loading, animateValueChange, transitionDuration]);
50
+
51
+ if (loading) {
52
+ return <Skeleton variant="text" width={skeletonWidth} height={height} />;
53
+ }
54
+
55
+ return (
56
+ <Typography
57
+ component="span"
58
+ sx={{
59
+ ...sx,
60
+ opacity: isTransitioning ? 0 : 1,
61
+ transition: animateValueChange ? `opacity ${transitionDuration / 2}ms ease-in-out` : undefined,
62
+ }}>
63
+ {displayValue}
64
+ </Typography>
65
+ );
66
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Price Change Confirmation Dialog (Final Freeze Architecture)
3
+ *
4
+ * Displayed when the price changes between Preview and Submit
5
+ * beyond the user's configured slippage threshold.
6
+ *
7
+ * @see Intent: blocklets/core/ai/intent/20260112-dynamic-price.md
8
+ */
9
+
10
+ import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from '@mui/material';
11
+ import WarningAmberIcon from '@mui/icons-material/WarningAmber';
12
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
13
+
14
+ export interface PriceChangeConfirmProps {
15
+ open: boolean;
16
+ previewRate?: string;
17
+ submitRate?: string;
18
+ changePercent: number;
19
+ onConfirm: () => void;
20
+ onCancel: () => void;
21
+ loading?: boolean;
22
+ }
23
+
24
+ export default function PriceChangeConfirm({
25
+ open,
26
+ previewRate = undefined,
27
+ submitRate = undefined,
28
+ changePercent,
29
+ onConfirm,
30
+ onCancel,
31
+ loading = false,
32
+ }: PriceChangeConfirmProps) {
33
+ const { t } = useLocaleContext();
34
+
35
+ const changeDirection = changePercent > 0 ? 'increased' : 'decreased';
36
+ const absChangePercent = Math.abs(changePercent);
37
+
38
+ return (
39
+ <Dialog
40
+ open={open}
41
+ onClose={loading ? undefined : onCancel}
42
+ maxWidth="sm"
43
+ fullWidth
44
+ PaperProps={{
45
+ sx: {
46
+ borderRadius: 2,
47
+ },
48
+ }}>
49
+ <DialogTitle
50
+ sx={{
51
+ display: 'flex',
52
+ alignItems: 'center',
53
+ gap: 1,
54
+ pb: 1,
55
+ }}>
56
+ <WarningAmberIcon color="warning" />
57
+ <Typography variant="h6" component="span">
58
+ {t('payment.checkout.priceChange.title', { fallback: 'Price Changed' })}
59
+ </Typography>
60
+ </DialogTitle>
61
+
62
+ <DialogContent>
63
+ <Stack spacing={2}>
64
+ <Typography variant="body1" color="text.secondary">
65
+ {t('payment.checkout.priceChange.description', {
66
+ fallback: `The exchange rate has ${changeDirection} by ${absChangePercent.toFixed(2)}% since you started this checkout.`,
67
+ direction: changeDirection,
68
+ percent: absChangePercent.toFixed(2),
69
+ })}
70
+ </Typography>
71
+
72
+ {(previewRate || submitRate) && (
73
+ <Box
74
+ sx={{
75
+ bgcolor: 'action.hover',
76
+ borderRadius: 1,
77
+ p: 2,
78
+ }}>
79
+ <Stack spacing={1}>
80
+ {previewRate && (
81
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
82
+ <Typography variant="body2" color="text.secondary">
83
+ {t('payment.checkout.priceChange.previewRate', { fallback: 'Preview Rate' })}:
84
+ </Typography>
85
+ <Typography variant="body2" fontFamily="monospace">
86
+ {previewRate}
87
+ </Typography>
88
+ </Box>
89
+ )}
90
+ {submitRate && (
91
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
92
+ <Typography variant="body2" color="text.secondary">
93
+ {t('payment.checkout.priceChange.currentRate', { fallback: 'Current Rate' })}:
94
+ </Typography>
95
+ <Typography variant="body2" fontFamily="monospace">
96
+ {submitRate}
97
+ </Typography>
98
+ </Box>
99
+ )}
100
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
101
+ <Typography variant="body2" color="text.secondary">
102
+ {t('payment.checkout.priceChange.change', { fallback: 'Change' })}:
103
+ </Typography>
104
+ <Typography
105
+ variant="body2"
106
+ fontWeight="bold"
107
+ color={changePercent > 0 ? 'error.main' : 'success.main'}>
108
+ {changePercent > 0 ? '+' : ''}
109
+ {changePercent.toFixed(2)}%
110
+ </Typography>
111
+ </Box>
112
+ </Stack>
113
+ </Box>
114
+ )}
115
+
116
+ <Typography variant="body2" color="text.secondary">
117
+ {t('payment.checkout.priceChange.confirm', {
118
+ fallback: 'Do you want to continue with the new price?',
119
+ })}
120
+ </Typography>
121
+ </Stack>
122
+ </DialogContent>
123
+
124
+ <DialogActions sx={{ px: 3, pb: 2 }}>
125
+ <Button onClick={onCancel} disabled={loading} variant="outlined" color="inherit">
126
+ {t('payment.checkout.priceChange.cancel', { fallback: 'Cancel' })}
127
+ </Button>
128
+ <Button onClick={onConfirm} disabled={loading} variant="contained" color="primary" autoFocus>
129
+ {loading
130
+ ? t('payment.checkout.priceChange.confirming', { fallback: 'Confirming...' })
131
+ : t('payment.checkout.priceChange.accept', { fallback: 'Accept & Continue' })}
132
+ </Button>
133
+ </DialogActions>
134
+ </Dialog>
135
+ );
136
+ }