@blocklet/payment-react 1.24.3 → 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.
- package/es/components/auto-topup/modal.d.ts +2 -0
- package/es/components/auto-topup/modal.js +48 -6
- package/es/components/auto-topup/product-card.d.ts +16 -1
- package/es/components/auto-topup/product-card.js +97 -15
- package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
- package/es/components/dynamic-pricing-unavailable.js +58 -0
- package/es/components/loading-amount.d.ts +17 -0
- package/es/components/loading-amount.js +46 -0
- package/es/components/price-change-confirm.d.ts +18 -0
- package/es/components/price-change-confirm.js +107 -0
- package/es/components/quote-details-panel.d.ts +21 -0
- package/es/components/quote-details-panel.js +170 -0
- package/es/components/quote-lock-banner.d.ts +7 -0
- package/es/components/quote-lock-banner.js +79 -0
- package/es/components/slippage-config.d.ts +20 -0
- package/es/components/slippage-config.js +261 -0
- package/es/history/credit/transactions-list.js +11 -1
- package/es/history/invoice/list.js +125 -15
- package/es/hooks/dynamic-pricing.d.ts +102 -0
- package/es/hooks/dynamic-pricing.js +393 -0
- package/es/index.d.ts +6 -1
- package/es/index.js +9 -1
- package/es/libs/util.d.ts +42 -5
- package/es/libs/util.js +345 -57
- package/es/locales/en.js +114 -3
- package/es/locales/zh.js +114 -3
- package/es/payment/form/index.d.ts +4 -1
- package/es/payment/form/index.js +454 -22
- package/es/payment/index.d.ts +1 -1
- package/es/payment/index.js +279 -16
- package/es/payment/product-item.d.ts +26 -1
- package/es/payment/product-item.js +330 -51
- package/es/payment/summary-section/promotion-section.d.ts +32 -0
- package/es/payment/summary-section/promotion-section.js +143 -0
- package/es/payment/summary-section/total-section.d.ts +39 -0
- package/es/payment/summary-section/total-section.js +83 -0
- package/es/payment/summary.d.ts +17 -2
- package/es/payment/summary.js +300 -253
- package/es/types/index.d.ts +11 -0
- package/lib/components/auto-topup/modal.d.ts +2 -0
- package/lib/components/auto-topup/modal.js +54 -6
- package/lib/components/auto-topup/product-card.d.ts +16 -1
- package/lib/components/auto-topup/product-card.js +75 -7
- package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
- package/lib/components/dynamic-pricing-unavailable.js +81 -0
- package/lib/components/loading-amount.d.ts +17 -0
- package/lib/components/loading-amount.js +53 -0
- package/lib/components/price-change-confirm.d.ts +18 -0
- package/lib/components/price-change-confirm.js +157 -0
- package/lib/components/quote-details-panel.d.ts +21 -0
- package/lib/components/quote-details-panel.js +226 -0
- package/lib/components/quote-lock-banner.d.ts +7 -0
- package/lib/components/quote-lock-banner.js +93 -0
- package/lib/components/slippage-config.d.ts +20 -0
- package/lib/components/slippage-config.js +316 -0
- package/lib/history/credit/transactions-list.js +11 -1
- package/lib/history/invoice/list.js +167 -27
- package/lib/hooks/dynamic-pricing.d.ts +102 -0
- package/lib/hooks/dynamic-pricing.js +390 -0
- package/lib/index.d.ts +6 -1
- package/lib/index.js +32 -0
- package/lib/libs/util.d.ts +42 -5
- package/lib/libs/util.js +367 -49
- package/lib/locales/en.js +114 -3
- package/lib/locales/zh.js +114 -3
- package/lib/payment/form/index.d.ts +4 -1
- package/lib/payment/form/index.js +476 -20
- package/lib/payment/index.d.ts +1 -1
- package/lib/payment/index.js +308 -14
- package/lib/payment/product-item.d.ts +26 -1
- package/lib/payment/product-item.js +270 -35
- package/lib/payment/summary-section/promotion-section.d.ts +32 -0
- package/lib/payment/summary-section/promotion-section.js +133 -0
- package/lib/payment/summary-section/total-section.d.ts +39 -0
- package/lib/payment/summary-section/total-section.js +117 -0
- package/lib/payment/summary.d.ts +17 -2
- package/lib/payment/summary.js +205 -127
- package/lib/types/index.d.ts +11 -0
- package/package.json +3 -3
- package/src/components/auto-topup/modal.tsx +59 -6
- package/src/components/auto-topup/product-card.tsx +118 -11
- package/src/components/dynamic-pricing-unavailable.tsx +69 -0
- package/src/components/loading-amount.tsx +66 -0
- package/src/components/price-change-confirm.tsx +136 -0
- package/src/components/quote-details-panel.tsx +218 -0
- package/src/components/quote-lock-banner.tsx +99 -0
- package/src/components/slippage-config.tsx +336 -0
- package/src/history/credit/transactions-list.tsx +14 -1
- package/src/history/invoice/list.tsx +143 -9
- package/src/hooks/dynamic-pricing.ts +617 -0
- package/src/index.ts +9 -0
- package/src/libs/util.ts +473 -58
- package/src/locales/en.tsx +117 -0
- package/src/locales/zh.tsx +111 -0
- package/src/payment/form/index.tsx +561 -19
- package/src/payment/index.tsx +349 -10
- package/src/payment/product-item.tsx +451 -37
- package/src/payment/summary-section/promotion-section.tsx +172 -0
- package/src/payment/summary-section/total-section.tsx +141 -0
- package/src/payment/summary.tsx +334 -192
- 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 {
|
|
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: '
|
|
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
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|