@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
|
@@ -7,6 +7,7 @@ import { FlagEmoji } from 'react-international-phone';
|
|
|
7
7
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
8
8
|
import type {
|
|
9
9
|
TCheckoutSession,
|
|
10
|
+
TCheckoutSessionExpanded,
|
|
10
11
|
TCustomer,
|
|
11
12
|
TLineItemExpanded,
|
|
12
13
|
TPaymentIntent,
|
|
@@ -20,13 +21,14 @@ import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
|
20
21
|
import { joinURL } from 'ufo';
|
|
21
22
|
import { dispatch } from 'use-bus';
|
|
22
23
|
import isEmail from 'validator/es/lib/isEmail';
|
|
23
|
-
import { fromUnitToToken } from '@ocap/util';
|
|
24
|
+
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
24
25
|
import DID from '@arcblock/ux/lib/DID';
|
|
25
26
|
import { PayFailedEvent } from '@arcblock/ux/lib/withTracker/action/pay';
|
|
26
27
|
|
|
27
28
|
import isEmpty from 'lodash/isEmpty';
|
|
28
29
|
import { HelpOutline, OpenInNew } from '@mui/icons-material';
|
|
29
30
|
import { ReactGA } from '@arcblock/ux/lib/withTracker';
|
|
31
|
+
import trim from 'lodash/trim';
|
|
30
32
|
import FormInput from '../../components/input';
|
|
31
33
|
import FormLabel from '../../components/label';
|
|
32
34
|
import { usePaymentContext } from '../../contexts/payment';
|
|
@@ -41,6 +43,12 @@ import {
|
|
|
41
43
|
getStatementDescriptor,
|
|
42
44
|
getTokenBalanceLink,
|
|
43
45
|
isCrossOrigin,
|
|
46
|
+
getCheckoutAmount,
|
|
47
|
+
formatNumber,
|
|
48
|
+
formatUsdAmount,
|
|
49
|
+
getUsdAmountFromBaseAmount,
|
|
50
|
+
getUsdAmountFromTokenUnits,
|
|
51
|
+
formatAmount,
|
|
44
52
|
} from '../../libs/util';
|
|
45
53
|
import type { CheckoutCallbacks, CheckoutContext } from '../../types';
|
|
46
54
|
import AddressForm from './address';
|
|
@@ -53,8 +61,14 @@ import LoadingButton from '../../components/loading-button';
|
|
|
53
61
|
import OverdueInvoicePayment from '../../components/over-due-invoice-payment';
|
|
54
62
|
import { saveCurrencyPreference } from '../../libs/currency';
|
|
55
63
|
import ConfirmDialog from '../../components/confirm';
|
|
64
|
+
import PriceChangeConfirm from '../../components/price-change-confirm';
|
|
56
65
|
import { getFieldValidation, validatePostalCode } from '../../libs/validator';
|
|
57
66
|
|
|
67
|
+
// Generate unique idempotency key for submit (Final Freeze Architecture)
|
|
68
|
+
const generateIdempotencyKey = (sessionId: string, currencyId: string): string => {
|
|
69
|
+
return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
58
72
|
export const waitForCheckoutComplete = async (sessionId: string) => {
|
|
59
73
|
let result: CheckoutContext;
|
|
60
74
|
|
|
@@ -91,6 +105,10 @@ export const hasDidWallet = (user: any) => {
|
|
|
91
105
|
|
|
92
106
|
type PageData = CheckoutContext &
|
|
93
107
|
CheckoutCallbacks & {
|
|
108
|
+
onQuoteUpdated?: (
|
|
109
|
+
data: Pick<CheckoutContext, 'checkoutSession' | 'quotes' | 'rateUnavailable' | 'rateError'>
|
|
110
|
+
) => void;
|
|
111
|
+
onPaymentIntentUpdate?: (intent: TPaymentIntent | null) => void;
|
|
94
112
|
onlyShowBtn?: boolean;
|
|
95
113
|
isDonation?: boolean;
|
|
96
114
|
};
|
|
@@ -178,6 +196,15 @@ const setUserFormValues = (
|
|
|
178
196
|
return updatedFields;
|
|
179
197
|
};
|
|
180
198
|
|
|
199
|
+
// ✅ No longer need to collect quotes from frontend - backend auto-finds them
|
|
200
|
+
// const collectQuotes = (lineItems: TLineItemExpanded[]) =>
|
|
201
|
+
// lineItems
|
|
202
|
+
// ?.filter((item) => (item.price as any)?.pricing_type === 'dynamic' && (item as any)?.quote_id)
|
|
203
|
+
// .map((item) => ({
|
|
204
|
+
// price_id: item.price_id,
|
|
205
|
+
// quote_id: (item as any).quote_id,
|
|
206
|
+
// })) || [];
|
|
207
|
+
|
|
181
208
|
// FIXME: https://stripe.com/docs/elements/address-element
|
|
182
209
|
// TODO: https://country-regions.github.io/react-country-region-selector/
|
|
183
210
|
// https://www.npmjs.com/package/postal-codes-js
|
|
@@ -191,10 +218,13 @@ export default function PaymentForm({
|
|
|
191
218
|
customer,
|
|
192
219
|
onPaid,
|
|
193
220
|
onError,
|
|
221
|
+
onQuoteUpdated = undefined,
|
|
222
|
+
onPaymentIntentUpdate = undefined,
|
|
194
223
|
// mode,
|
|
195
224
|
action,
|
|
196
225
|
onlyShowBtn = false,
|
|
197
226
|
isDonation = false,
|
|
227
|
+
rateUnavailable = false,
|
|
198
228
|
}: PageData) {
|
|
199
229
|
// const theme = useTheme();
|
|
200
230
|
const { t, locale } = useLocaleContext();
|
|
@@ -240,6 +270,14 @@ export default function PaymentForm({
|
|
|
240
270
|
open: boolean;
|
|
241
271
|
} | null;
|
|
242
272
|
showEditForm: boolean;
|
|
273
|
+
// Final Freeze: Price change confirmation state
|
|
274
|
+
priceChangeConfirm: {
|
|
275
|
+
open: boolean;
|
|
276
|
+
previewRate?: string;
|
|
277
|
+
submitRate?: string;
|
|
278
|
+
changePercent: number;
|
|
279
|
+
formData?: any;
|
|
280
|
+
} | null;
|
|
243
281
|
}>({
|
|
244
282
|
submitting: false,
|
|
245
283
|
paying: false,
|
|
@@ -252,6 +290,7 @@ export default function PaymentForm({
|
|
|
252
290
|
fastCheckoutInfo: null,
|
|
253
291
|
creditInsufficientInfo: null,
|
|
254
292
|
showEditForm: false,
|
|
293
|
+
priceChangeConfirm: null,
|
|
255
294
|
});
|
|
256
295
|
|
|
257
296
|
const currencies = flattenPaymentMethods(paymentMethods);
|
|
@@ -388,8 +427,192 @@ export default function PaymentForm({
|
|
|
388
427
|
const method = paymentMethods.find((x) => x.id === paymentMethod) as TPaymentMethodExpanded;
|
|
389
428
|
const paymentCurrency = currencies.find((x) => x.id === paymentCurrencyId);
|
|
390
429
|
const showStake = method.type === 'arcblock' && !checkoutSession.subscription_data?.no_stake;
|
|
430
|
+
const hasDynamicPricing = useMemo(
|
|
431
|
+
() =>
|
|
432
|
+
(checkoutSession.line_items || []).some((item: any) => {
|
|
433
|
+
const price = item.upsell_price || item.price;
|
|
434
|
+
return price && (price as any)?.pricing_type === 'dynamic';
|
|
435
|
+
}),
|
|
436
|
+
[checkoutSession.line_items]
|
|
437
|
+
);
|
|
438
|
+
const rateUnavailableForDynamic = hasDynamicPricing && rateUnavailable;
|
|
439
|
+
const canPay = payable && !rateUnavailableForDynamic;
|
|
391
440
|
|
|
392
441
|
const isDonationMode = checkoutSession?.submit_type === 'donate' && isDonation;
|
|
442
|
+
const [priceUpdateInfo, setPriceUpdateInfo] = useSetState<{
|
|
443
|
+
open: boolean;
|
|
444
|
+
total: string;
|
|
445
|
+
usd: string | null;
|
|
446
|
+
hasQuotes: boolean;
|
|
447
|
+
baseCurrency: string;
|
|
448
|
+
oldTotal: string;
|
|
449
|
+
reason: 'rateChanged' | 'recalculated';
|
|
450
|
+
}>({
|
|
451
|
+
open: false,
|
|
452
|
+
total: '',
|
|
453
|
+
usd: null,
|
|
454
|
+
hasQuotes: false,
|
|
455
|
+
baseCurrency: 'USD',
|
|
456
|
+
oldTotal: '',
|
|
457
|
+
reason: 'recalculated',
|
|
458
|
+
});
|
|
459
|
+
// lockExpiredInfo state removed - now auto-refreshes instead
|
|
460
|
+
|
|
461
|
+
const normalizeExchangeRate = useMemoizedFn((rate?: string | null): string | null => {
|
|
462
|
+
if (!rate) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const value = Number(rate);
|
|
466
|
+
if (!Number.isFinite(value)) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
return value.toFixed(8);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const getExchangeRateFromSession = useMemoizedFn((sessionData?: TCheckoutSession | null): string | null => {
|
|
473
|
+
if (!sessionData?.line_items?.length) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
for (const item of sessionData.line_items as TLineItemExpanded[]) {
|
|
477
|
+
const rate = (item as any)?.exchange_rate;
|
|
478
|
+
if (rate) {
|
|
479
|
+
return rate;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const quoteAutoRetryRef = useRef(false);
|
|
486
|
+
const lastRetryKeyRef = useRef<string>('');
|
|
487
|
+
const buildRetryKey = useMemoizedFn((sessionData?: TCheckoutSession | null) => {
|
|
488
|
+
if (!sessionData?.line_items?.length) {
|
|
489
|
+
return '';
|
|
490
|
+
}
|
|
491
|
+
return (sessionData.line_items as TLineItemExpanded[])
|
|
492
|
+
.map((item) => {
|
|
493
|
+
const priceId = item.price_id || item.price?.id || '';
|
|
494
|
+
const quoteId = (item as any)?.quote_id || '';
|
|
495
|
+
const quotedAmount = (item as any)?.quoted_amount || '';
|
|
496
|
+
const exchangeRate = (item as any)?.exchange_rate || '';
|
|
497
|
+
return `${priceId}:${quoteId}:${quotedAmount}:${exchangeRate}`;
|
|
498
|
+
})
|
|
499
|
+
.join('|');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const buildPriceUpdateSummary = useMemoizedFn((sessionData: TCheckoutSession) => {
|
|
503
|
+
if (!paymentCurrency) {
|
|
504
|
+
return { total: '', usd: null, hasQuotes: false, baseCurrency: 'USD', totalUnit: null as BN | null };
|
|
505
|
+
}
|
|
506
|
+
const lineItems = (sessionData.line_items || []) as TLineItemExpanded[];
|
|
507
|
+
let baseCurrency = 'USD';
|
|
508
|
+
for (const item of lineItems) {
|
|
509
|
+
const price = item.upsell_price || item.price;
|
|
510
|
+
const base = (price as any)?.base_currency;
|
|
511
|
+
if (base) {
|
|
512
|
+
baseCurrency = base;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const hasQuotes = lineItems.some((item) => (item as any)?.quoted_amount && (item as any)?.exchange_rate);
|
|
517
|
+
if (!hasQuotes) {
|
|
518
|
+
return { total: '', usd: null, hasQuotes: false, baseCurrency, totalUnit: null as BN | null };
|
|
519
|
+
}
|
|
520
|
+
let trialInDays = Number(sessionData?.subscription_data?.trial_period_days || 0);
|
|
521
|
+
const trialCurrencyIds = (sessionData?.subscription_data?.trial_currency || '')
|
|
522
|
+
.split(',')
|
|
523
|
+
.map(trim)
|
|
524
|
+
.filter(Boolean);
|
|
525
|
+
if (trialCurrencyIds.length > 0 && paymentCurrencyId && trialCurrencyIds.includes(paymentCurrencyId) === false) {
|
|
526
|
+
trialInDays = 0;
|
|
527
|
+
}
|
|
528
|
+
const { total } = getCheckoutAmount(lineItems, paymentCurrency, trialInDays > 0);
|
|
529
|
+
const discountAmount = new BN(sessionData.total_details?.amount_discount || '0');
|
|
530
|
+
const totalUnit = new BN(total).sub(discountAmount);
|
|
531
|
+
const normalizedTotalUnit = totalUnit.isNeg() ? new BN(0) : totalUnit;
|
|
532
|
+
const totalDisplay = `${formatNumber(
|
|
533
|
+
fromUnitToToken(normalizedTotalUnit.toString(), paymentCurrency.decimal),
|
|
534
|
+
6
|
|
535
|
+
)} ${paymentCurrency.symbol}`;
|
|
536
|
+
|
|
537
|
+
const itemUsdReferences = lineItems.map((item) => {
|
|
538
|
+
const price = item.upsell_price || item.price;
|
|
539
|
+
const baseAmount = (price as any)?.base_amount;
|
|
540
|
+
const hasBaseAmount = baseAmount !== undefined && baseAmount !== null;
|
|
541
|
+
if (hasBaseAmount) {
|
|
542
|
+
return getUsdAmountFromBaseAmount(baseAmount, item.quantity || 0);
|
|
543
|
+
}
|
|
544
|
+
const exchangeRate = (item as any)?.exchange_rate;
|
|
545
|
+
const quotedAmount = (item as any)?.quoted_amount;
|
|
546
|
+
if (!exchangeRate || !quotedAmount) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
return getUsdAmountFromTokenUnits(new BN(quotedAmount), paymentCurrency.decimal, exchangeRate);
|
|
550
|
+
});
|
|
551
|
+
const usdValues = itemUsdReferences.filter((value): value is string => Boolean(value));
|
|
552
|
+
if (!usdValues.length) {
|
|
553
|
+
return { total: totalDisplay, usd: null, hasQuotes, baseCurrency, totalUnit: normalizedTotalUnit };
|
|
554
|
+
}
|
|
555
|
+
const sumUnit = usdValues.reduce((acc, value) => acc.add(new BN(fromTokenToUnit(value, 8))), new BN(0));
|
|
556
|
+
const totalUsdReference = fromUnitToToken(sumUnit.toString(), 8);
|
|
557
|
+
return {
|
|
558
|
+
total: totalDisplay,
|
|
559
|
+
usd: formatUsdAmount(totalUsdReference, locale),
|
|
560
|
+
hasQuotes,
|
|
561
|
+
baseCurrency,
|
|
562
|
+
totalUnit: normalizedTotalUnit,
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const compareTotals = useMemoizedFn((prevSession: TCheckoutSession, nextSession: TCheckoutSession) => {
|
|
567
|
+
const prev = buildPriceUpdateSummary(prevSession);
|
|
568
|
+
const next = buildPriceUpdateSummary(nextSession);
|
|
569
|
+
if (!prev.totalUnit || !next.totalUnit) {
|
|
570
|
+
return { changed: false, prev, next };
|
|
571
|
+
}
|
|
572
|
+
const diff = next.totalUnit.sub(prev.totalUnit).abs();
|
|
573
|
+
const epsilon = new BN(1);
|
|
574
|
+
return { changed: diff.gt(epsilon), prev, next };
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const applyQuoteUpdate = useMemoizedFn(
|
|
578
|
+
(
|
|
579
|
+
payload: { checkoutSession: TCheckoutSession; quotes?: any; rateUnavailable?: boolean; rateError?: string },
|
|
580
|
+
options: { forceConfirm?: boolean; reason?: 'rateChanged' | 'recalculated' } = {}
|
|
581
|
+
) => {
|
|
582
|
+
if (!payload?.checkoutSession) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
onQuoteUpdated?.({
|
|
586
|
+
checkoutSession: payload.checkoutSession as TCheckoutSessionExpanded,
|
|
587
|
+
quotes: payload.quotes,
|
|
588
|
+
rateUnavailable: payload.rateUnavailable,
|
|
589
|
+
rateError: payload.rateError,
|
|
590
|
+
});
|
|
591
|
+
const { changed, prev, next } = compareTotals(checkoutSession, payload.checkoutSession);
|
|
592
|
+
const previousRate = normalizeExchangeRate(getExchangeRateFromSession(checkoutSession));
|
|
593
|
+
const nextRate = normalizeExchangeRate(getExchangeRateFromSession(payload.checkoutSession));
|
|
594
|
+
const rateChanged = !!(previousRate && nextRate && previousRate !== nextRate);
|
|
595
|
+
const shouldShowModal = (options.forceConfirm || changed) && next.hasQuotes;
|
|
596
|
+
if (shouldShowModal) {
|
|
597
|
+
setPriceUpdateInfo({
|
|
598
|
+
open: true,
|
|
599
|
+
total: next.total,
|
|
600
|
+
usd: next.usd,
|
|
601
|
+
hasQuotes: next.hasQuotes,
|
|
602
|
+
baseCurrency: next.baseCurrency,
|
|
603
|
+
oldTotal: prev.total,
|
|
604
|
+
reason: options.reason || (rateChanged ? 'rateChanged' : 'recalculated'),
|
|
605
|
+
});
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
setPriceUpdateInfo({ open: false });
|
|
609
|
+
const retryKey = buildRetryKey(payload.checkoutSession);
|
|
610
|
+
if (retryKey && retryKey !== lastRetryKeyRef.current) {
|
|
611
|
+
lastRetryKeyRef.current = retryKey;
|
|
612
|
+
quoteAutoRetryRef.current = true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
);
|
|
393
616
|
|
|
394
617
|
const validateUserInfo = (values: any) => {
|
|
395
618
|
if (!values) {
|
|
@@ -496,6 +719,17 @@ export default function PaymentForm({
|
|
|
496
719
|
|
|
497
720
|
const showForm = session?.user ? state.showEditForm : false;
|
|
498
721
|
|
|
722
|
+
useEffect(() => {
|
|
723
|
+
if (!quoteAutoRetryRef.current) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (state.submitting || state.paying) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
quoteAutoRetryRef.current = false;
|
|
730
|
+
onAction();
|
|
731
|
+
}, [state.submitting, state.paying]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
732
|
+
|
|
499
733
|
const handleConnected = async () => {
|
|
500
734
|
if (processingRef.current) {
|
|
501
735
|
return;
|
|
@@ -560,7 +794,11 @@ export default function PaymentForm({
|
|
|
560
794
|
});
|
|
561
795
|
|
|
562
796
|
try {
|
|
563
|
-
|
|
797
|
+
// ✅ No longer need to send quotes - backend auto-finds them
|
|
798
|
+
const result = await api.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`, {});
|
|
799
|
+
if (result.data.paymentIntent) {
|
|
800
|
+
onPaymentIntentUpdate?.(result.data.paymentIntent);
|
|
801
|
+
}
|
|
564
802
|
if (result.data.fastPaid) {
|
|
565
803
|
setState({
|
|
566
804
|
fastCheckoutInfo: null,
|
|
@@ -575,8 +813,56 @@ export default function PaymentForm({
|
|
|
575
813
|
});
|
|
576
814
|
openConnect();
|
|
577
815
|
}
|
|
578
|
-
} catch (err) {
|
|
816
|
+
} catch (err: unknown) {
|
|
579
817
|
console.error(err);
|
|
818
|
+
const errorCode = (err as any)?.response?.data?.code;
|
|
819
|
+
// Auto-refresh for quote-related errors (including lock expired)
|
|
820
|
+
if (
|
|
821
|
+
[
|
|
822
|
+
'QUOTE_LOCK_EXPIRED',
|
|
823
|
+
'QUOTE_AMOUNT_MISMATCH',
|
|
824
|
+
'QUOTE_EXPIRED_OR_USED',
|
|
825
|
+
'QUOTE_NOT_FOUND',
|
|
826
|
+
'QUOTE_REQUIRED',
|
|
827
|
+
].includes(errorCode)
|
|
828
|
+
) {
|
|
829
|
+
try {
|
|
830
|
+
const { data: refreshed } = await api.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, {
|
|
831
|
+
params: { forceRefresh: '1' },
|
|
832
|
+
});
|
|
833
|
+
if (refreshed?.checkoutSession) {
|
|
834
|
+
applyQuoteUpdate(refreshed, { reason: 'rateChanged' });
|
|
835
|
+
Toast.info(t('payment.checkout.quote.updated.pleaseRetry') || 'Price updated, please resubmit');
|
|
836
|
+
}
|
|
837
|
+
} catch (refreshError) {
|
|
838
|
+
console.error(refreshError);
|
|
839
|
+
Toast.error(formatError(refreshError));
|
|
840
|
+
} finally {
|
|
841
|
+
setState({ fastCheckoutInfo: null });
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (errorCode === 'QUOTE_UPDATED') {
|
|
846
|
+
const payload = (err as any)?.response?.data;
|
|
847
|
+
if (payload?.checkoutSession) {
|
|
848
|
+
applyQuoteUpdate(payload);
|
|
849
|
+
}
|
|
850
|
+
setState({ fastCheckoutInfo: null });
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (errorCode === 'RATE_UNAVAILABLE') {
|
|
854
|
+
const payload = (err as any)?.response?.data;
|
|
855
|
+
if (payload?.checkoutSession) {
|
|
856
|
+
onQuoteUpdated?.({
|
|
857
|
+
checkoutSession: payload.checkoutSession as TCheckoutSessionExpanded,
|
|
858
|
+
quotes: payload.quotes,
|
|
859
|
+
rateUnavailable: payload.rateUnavailable,
|
|
860
|
+
rateError: payload.rateError,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
setState({ fastCheckoutInfo: null });
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
580
866
|
Toast.error(formatError(err));
|
|
581
867
|
setState({
|
|
582
868
|
fastCheckoutInfo: null,
|
|
@@ -592,6 +878,29 @@ export default function PaymentForm({
|
|
|
592
878
|
setState({ creditInsufficientInfo: null });
|
|
593
879
|
};
|
|
594
880
|
|
|
881
|
+
const handlePriceUpdateConfirm = () => {
|
|
882
|
+
setPriceUpdateInfo({ open: false });
|
|
883
|
+
quoteAutoRetryRef.current = true;
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const handlePriceUpdateCancel = () => {
|
|
887
|
+
setPriceUpdateInfo({ open: false });
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// Final Freeze: Handle price change confirmation (PRICE_CHANGED error)
|
|
891
|
+
const handlePriceChangeConfirm = () => {
|
|
892
|
+
const formData = state.priceChangeConfirm?.formData;
|
|
893
|
+
setState({ priceChangeConfirm: null });
|
|
894
|
+
if (formData) {
|
|
895
|
+
// Retry submit with price_confirmed flag
|
|
896
|
+
onFormSubmit(formData);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const handlePriceChangeCancel = () => {
|
|
901
|
+
setState({ priceChangeConfirm: null });
|
|
902
|
+
};
|
|
903
|
+
|
|
595
904
|
const openConnect = () => {
|
|
596
905
|
try {
|
|
597
906
|
if (!['arcblock', 'ethereum', 'base'].includes(method.type)) {
|
|
@@ -632,6 +941,10 @@ export default function PaymentForm({
|
|
|
632
941
|
};
|
|
633
942
|
|
|
634
943
|
const onFormSubmit = async (data: any) => {
|
|
944
|
+
if (state.submitting) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
635
948
|
const userInfo = session.user;
|
|
636
949
|
|
|
637
950
|
if (!userInfo.sourceAppPid) {
|
|
@@ -644,13 +957,36 @@ export default function PaymentForm({
|
|
|
644
957
|
}
|
|
645
958
|
}
|
|
646
959
|
|
|
960
|
+
// ✅ No longer need to collect or validate quotes - backend auto-finds them
|
|
961
|
+
// const quotes = collectQuotes(checkoutSession.line_items as TLineItemExpanded[]);
|
|
962
|
+
// if (
|
|
963
|
+
// (checkoutSession.line_items || []).some((item: any) => item.price?.pricing_type === 'dynamic') &&
|
|
964
|
+
// quotes.length === 0
|
|
965
|
+
// ) {
|
|
966
|
+
// Toast.error(t('payment.checkout.quote.expired'));
|
|
967
|
+
// return;
|
|
968
|
+
// }
|
|
969
|
+
|
|
647
970
|
setState({ submitting: true });
|
|
648
971
|
try {
|
|
649
972
|
let result;
|
|
973
|
+
// Final Freeze: Add idempotency_key and preview_rate for dynamic pricing
|
|
974
|
+
const previewRate =
|
|
975
|
+
checkoutSession.line_items?.find((item: TLineItemExpanded) => (item as any)?.exchange_rate)?.exchange_rate ||
|
|
976
|
+
undefined;
|
|
977
|
+
|
|
978
|
+
const payload = {
|
|
979
|
+
...data,
|
|
980
|
+
// Final Freeze: Include these for new quote creation at submit
|
|
981
|
+
idempotency_key: generateIdempotencyKey(checkoutSession.id, paymentCurrency?.id || ''),
|
|
982
|
+
preview_rate: (previewRate as unknown as string) || undefined,
|
|
983
|
+
price_confirmed: state.priceChangeConfirm?.formData ? true : undefined,
|
|
984
|
+
};
|
|
985
|
+
|
|
650
986
|
if (isDonationMode) {
|
|
651
|
-
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`,
|
|
987
|
+
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`, payload);
|
|
652
988
|
} else {
|
|
653
|
-
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`,
|
|
989
|
+
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, payload);
|
|
654
990
|
}
|
|
655
991
|
setState({
|
|
656
992
|
paymentIntent: result.data.paymentIntent,
|
|
@@ -659,8 +995,34 @@ export default function PaymentForm({
|
|
|
659
995
|
submitting: false,
|
|
660
996
|
customerLimited: false,
|
|
661
997
|
});
|
|
998
|
+
if (result.data.paymentIntent) {
|
|
999
|
+
onPaymentIntentUpdate?.(result.data.paymentIntent);
|
|
1000
|
+
}
|
|
662
1001
|
|
|
663
1002
|
if (['arcblock', 'ethereum', 'base'].includes(method.type)) {
|
|
1003
|
+
// 如果不需要支付(如免费试用),直接确认
|
|
1004
|
+
if (result.data.noPaymentRequired) {
|
|
1005
|
+
try {
|
|
1006
|
+
const confirmResult = await api.post(
|
|
1007
|
+
`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`,
|
|
1008
|
+
{}
|
|
1009
|
+
);
|
|
1010
|
+
if (confirmResult.data.paymentIntent) {
|
|
1011
|
+
onPaymentIntentUpdate?.(confirmResult.data.paymentIntent);
|
|
1012
|
+
}
|
|
1013
|
+
if (confirmResult.data.fastPaid || confirmResult.data.checkoutSession?.status === 'complete') {
|
|
1014
|
+
setState({ paying: true });
|
|
1015
|
+
await handleConnected();
|
|
1016
|
+
} else {
|
|
1017
|
+
openConnect();
|
|
1018
|
+
}
|
|
1019
|
+
} catch (confirmErr) {
|
|
1020
|
+
console.error('noPaymentRequired confirm failed', confirmErr);
|
|
1021
|
+
openConnect();
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
664
1026
|
// 优先判断 credit 支付
|
|
665
1027
|
if (paymentCurrency?.type === 'credit') {
|
|
666
1028
|
if (result.data.creditSufficient === true) {
|
|
@@ -715,15 +1077,109 @@ export default function PaymentForm({
|
|
|
715
1077
|
console.error(err);
|
|
716
1078
|
let shouldToast = true;
|
|
717
1079
|
|
|
718
|
-
|
|
719
|
-
|
|
1080
|
+
const errorCode = err.response?.data?.code;
|
|
1081
|
+
if (errorCode) {
|
|
1082
|
+
if (
|
|
1083
|
+
![
|
|
1084
|
+
'QUOTE_UPDATED',
|
|
1085
|
+
'RATE_UNAVAILABLE',
|
|
1086
|
+
'QUOTE_LOCK_EXPIRED',
|
|
1087
|
+
'QUOTE_AMOUNT_MISMATCH',
|
|
1088
|
+
'QUOTE_EXPIRED_OR_USED',
|
|
1089
|
+
'QUOTE_NOT_FOUND',
|
|
1090
|
+
'QUOTE_REQUIRED',
|
|
1091
|
+
'QUOTE_MAX_PAYABLE_EXCEEDED',
|
|
1092
|
+
].includes(errorCode)
|
|
1093
|
+
) {
|
|
1094
|
+
dispatch(`error.${errorCode}`);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Auto-refresh for all quote-related errors (no modal dialog)
|
|
1098
|
+
if (
|
|
1099
|
+
[
|
|
1100
|
+
'QUOTE_LOCK_EXPIRED',
|
|
1101
|
+
'QUOTE_AMOUNT_MISMATCH',
|
|
1102
|
+
'QUOTE_EXPIRED_OR_USED',
|
|
1103
|
+
'QUOTE_NOT_FOUND',
|
|
1104
|
+
'QUOTE_REQUIRED',
|
|
1105
|
+
'QUOTE_MAX_PAYABLE_EXCEEDED',
|
|
1106
|
+
'quote_validation_failed',
|
|
1107
|
+
].includes(errorCode)
|
|
1108
|
+
) {
|
|
1109
|
+
shouldToast = false;
|
|
1110
|
+
try {
|
|
1111
|
+
const { data: refreshed } = await api.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, {
|
|
1112
|
+
params: { forceRefresh: '1' },
|
|
1113
|
+
});
|
|
1114
|
+
if (refreshed?.checkoutSession) {
|
|
1115
|
+
applyQuoteUpdate(refreshed, { reason: 'rateChanged' });
|
|
1116
|
+
Toast.info(t('payment.checkout.quote.updated.pleaseRetry') || 'Price updated, please resubmit');
|
|
1117
|
+
}
|
|
1118
|
+
} catch (refreshError) {
|
|
1119
|
+
console.error(refreshError);
|
|
1120
|
+
Toast.error(formatError(refreshError));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (errorCode === 'QUOTE_UPDATED') {
|
|
1125
|
+
shouldToast = false;
|
|
1126
|
+
const payload = err.response?.data;
|
|
1127
|
+
if (payload?.checkoutSession) {
|
|
1128
|
+
applyQuoteUpdate(payload);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (errorCode === 'RATE_UNAVAILABLE') {
|
|
1133
|
+
shouldToast = false;
|
|
1134
|
+
const payload = err.response?.data;
|
|
1135
|
+
if (payload?.checkoutSession) {
|
|
1136
|
+
onQuoteUpdated?.({
|
|
1137
|
+
checkoutSession: payload.checkoutSession,
|
|
1138
|
+
quotes: payload.quotes,
|
|
1139
|
+
rateUnavailable: payload.rateUnavailable,
|
|
1140
|
+
rateError: payload.rateError,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Final Freeze: Handle new error codes
|
|
1146
|
+
if (errorCode === 'PRICE_UNAVAILABLE') {
|
|
1147
|
+
shouldToast = false;
|
|
1148
|
+
Toast.error(
|
|
1149
|
+
t('payment.checkout.priceChange.unavailable', {
|
|
1150
|
+
fallback: 'Unable to fetch exchange rate. Please try again later.',
|
|
1151
|
+
})
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
720
1154
|
|
|
721
|
-
if (
|
|
1155
|
+
if (errorCode === 'PRICE_UNSTABLE') {
|
|
1156
|
+
shouldToast = false;
|
|
1157
|
+
Toast.error(
|
|
1158
|
+
t('payment.checkout.priceChange.unstable', {
|
|
1159
|
+
fallback: 'Price is volatile. Please try again later.',
|
|
1160
|
+
})
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (errorCode === 'PRICE_CHANGED') {
|
|
1165
|
+
shouldToast = false;
|
|
1166
|
+
const errorData = err.response?.data;
|
|
1167
|
+
// Show price change confirmation dialog
|
|
1168
|
+
setState({
|
|
1169
|
+
priceChangeConfirm: {
|
|
1170
|
+
open: true,
|
|
1171
|
+
changePercent: errorData?.change_percent || 0,
|
|
1172
|
+
formData: data, // Save form data for retry
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (errorCode === 'UNIFIED_APP_REQUIRED') {
|
|
722
1178
|
shouldToast = false;
|
|
723
1179
|
Toast.error(t('payment.checkout.vendor.accountRequired'));
|
|
724
1180
|
}
|
|
725
1181
|
|
|
726
|
-
if (
|
|
1182
|
+
if (errorCode === 'CUSTOMER_LIMITED') {
|
|
727
1183
|
shouldToast = false;
|
|
728
1184
|
setState({ customerLimited: true });
|
|
729
1185
|
}
|
|
@@ -744,7 +1200,7 @@ export default function PaymentForm({
|
|
|
744
1200
|
};
|
|
745
1201
|
|
|
746
1202
|
const onAction = () => {
|
|
747
|
-
if (state.submitting || state.paying) {
|
|
1203
|
+
if (state.submitting || state.paying || !canPay) {
|
|
748
1204
|
return;
|
|
749
1205
|
}
|
|
750
1206
|
if (errorRef.current && !isEmpty(errors) && isMobile) {
|
|
@@ -769,6 +1225,8 @@ export default function PaymentForm({
|
|
|
769
1225
|
}
|
|
770
1226
|
};
|
|
771
1227
|
|
|
1228
|
+
// Lock expired handlers removed - now auto-refreshes instead
|
|
1229
|
+
|
|
772
1230
|
const onStripeConfirm = async () => {
|
|
773
1231
|
setState({ stripePaying: false, paying: true });
|
|
774
1232
|
await handleConnected();
|
|
@@ -793,7 +1251,7 @@ export default function PaymentForm({
|
|
|
793
1251
|
!state.paying &&
|
|
794
1252
|
!state.stripePaying &&
|
|
795
1253
|
quantityInventoryStatus &&
|
|
796
|
-
|
|
1254
|
+
canPay
|
|
797
1255
|
) {
|
|
798
1256
|
onAction();
|
|
799
1257
|
}
|
|
@@ -803,7 +1261,7 @@ export default function PaymentForm({
|
|
|
803
1261
|
return () => {
|
|
804
1262
|
window.removeEventListener('keydown', handleKeyDown);
|
|
805
1263
|
};
|
|
806
|
-
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus,
|
|
1264
|
+
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, canPay]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
807
1265
|
|
|
808
1266
|
const balanceLink = getTokenBalanceLink(method, state.fastCheckoutInfo?.payer || '');
|
|
809
1267
|
|
|
@@ -820,7 +1278,7 @@ export default function PaymentForm({
|
|
|
820
1278
|
state.fastCheckoutInfo.sourceType === 'credit' ? (
|
|
821
1279
|
<Typography>
|
|
822
1280
|
{t('payment.checkout.fastPay.credit.meteringSubscriptionMessage', {
|
|
823
|
-
available: `${
|
|
1281
|
+
available: `${formatAmount(state.fastCheckoutInfo?.balance || '0', paymentCurrency?.decimal || 18)} ${paymentCurrency?.symbol}`,
|
|
824
1282
|
})}
|
|
825
1283
|
</Typography>
|
|
826
1284
|
) : (
|
|
@@ -873,7 +1331,7 @@ export default function PaymentForm({
|
|
|
873
1331
|
{t('payment.checkout.fastPay.amount')}
|
|
874
1332
|
</Typography>
|
|
875
1333
|
<Typography>
|
|
876
|
-
{
|
|
1334
|
+
{formatAmount(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18)}{' '}
|
|
877
1335
|
{paymentCurrency?.symbol}
|
|
878
1336
|
</Typography>
|
|
879
1337
|
</Stack>
|
|
@@ -896,6 +1354,47 @@ export default function PaymentForm({
|
|
|
896
1354
|
/>
|
|
897
1355
|
);
|
|
898
1356
|
|
|
1357
|
+
const PriceUpdatedDialog = priceUpdateInfo.open && (
|
|
1358
|
+
<ConfirmDialog
|
|
1359
|
+
onConfirm={handlePriceUpdateConfirm}
|
|
1360
|
+
onCancel={handlePriceUpdateCancel}
|
|
1361
|
+
title={t('payment.checkout.quote.priceUpdatedTitle')}
|
|
1362
|
+
message={
|
|
1363
|
+
<Stack spacing={1}>
|
|
1364
|
+
<Typography>
|
|
1365
|
+
{t(
|
|
1366
|
+
priceUpdateInfo.reason === 'rateChanged'
|
|
1367
|
+
? 'payment.checkout.quote.priceUpdatedDescriptionRate'
|
|
1368
|
+
: 'payment.checkout.quote.priceUpdatedDescriptionRecalc'
|
|
1369
|
+
)}
|
|
1370
|
+
</Typography>
|
|
1371
|
+
{priceUpdateInfo.hasQuotes && (
|
|
1372
|
+
<Stack spacing={0.25}>
|
|
1373
|
+
<Typography sx={{ color: 'text.secondary', fontSize: '0.7875rem' }}>
|
|
1374
|
+
{t('payment.checkout.quote.priceUpdatedNewTotalLabel')}
|
|
1375
|
+
</Typography>
|
|
1376
|
+
<Typography sx={{ fontWeight: 600 }}>{priceUpdateInfo.total}</Typography>
|
|
1377
|
+
{priceUpdateInfo.usd && (
|
|
1378
|
+
<Typography sx={{ color: 'text.secondary' }}>
|
|
1379
|
+
≈ {priceUpdateInfo.usd} {priceUpdateInfo.baseCurrency}
|
|
1380
|
+
</Typography>
|
|
1381
|
+
)}
|
|
1382
|
+
{priceUpdateInfo.oldTotal && (
|
|
1383
|
+
<Typography sx={{ color: 'text.secondary', fontSize: '0.75rem' }}>
|
|
1384
|
+
{t('payment.checkout.quote.priceUpdatedOldTotal', { total: priceUpdateInfo.oldTotal })}
|
|
1385
|
+
</Typography>
|
|
1386
|
+
)}
|
|
1387
|
+
</Stack>
|
|
1388
|
+
)}
|
|
1389
|
+
</Stack>
|
|
1390
|
+
}
|
|
1391
|
+
confirm={t('payment.checkout.quote.priceUpdatedConfirm')}
|
|
1392
|
+
cancel={t('common.cancel')}
|
|
1393
|
+
color="primary"
|
|
1394
|
+
/>
|
|
1395
|
+
);
|
|
1396
|
+
// LockExpiredDialog removed - now auto-refreshes instead
|
|
1397
|
+
|
|
899
1398
|
const getRedirectUrl = () => {
|
|
900
1399
|
if (searchParams.redirect) {
|
|
901
1400
|
return decodeURIComponent(searchParams.redirect);
|
|
@@ -919,13 +1418,13 @@ export default function PaymentForm({
|
|
|
919
1418
|
size="large"
|
|
920
1419
|
className="cko-submit-button"
|
|
921
1420
|
onClick={() => {
|
|
922
|
-
if (state.submitting || state.paying) {
|
|
1421
|
+
if (state.submitting || state.paying || !canPay) {
|
|
923
1422
|
return;
|
|
924
1423
|
}
|
|
925
1424
|
onAction();
|
|
926
1425
|
}}
|
|
927
1426
|
fullWidth
|
|
928
|
-
disabled={state.stripePaying || !quantityInventoryStatus || !
|
|
1427
|
+
disabled={state.stripePaying || !quantityInventoryStatus || !canPay}>
|
|
929
1428
|
{(state.submitting || state.paying) && (
|
|
930
1429
|
<CircularProgress size={16} sx={{ mr: 0.5, color: 'primary.contrastText' }} />
|
|
931
1430
|
)}
|
|
@@ -962,6 +1461,17 @@ export default function PaymentForm({
|
|
|
962
1461
|
)}
|
|
963
1462
|
{FastCheckoutConfirmDialog}
|
|
964
1463
|
{CreditInsufficientDialog}
|
|
1464
|
+
{PriceUpdatedDialog}
|
|
1465
|
+
{/* Final Freeze: Price change confirmation dialog */}
|
|
1466
|
+
{state.priceChangeConfirm?.open && (
|
|
1467
|
+
<PriceChangeConfirm
|
|
1468
|
+
open
|
|
1469
|
+
changePercent={state.priceChangeConfirm.changePercent}
|
|
1470
|
+
onConfirm={handlePriceChangeConfirm}
|
|
1471
|
+
onCancel={handlePriceChangeCancel}
|
|
1472
|
+
loading={state.submitting}
|
|
1473
|
+
/>
|
|
1474
|
+
)}
|
|
965
1475
|
</>
|
|
966
1476
|
);
|
|
967
1477
|
}
|
|
@@ -998,10 +1508,31 @@ export default function PaymentForm({
|
|
|
998
1508
|
<CurrencySelector
|
|
999
1509
|
value={field.value}
|
|
1000
1510
|
currencies={currencies}
|
|
1001
|
-
onChange={(id: string, methodId: string) => {
|
|
1511
|
+
onChange={async (id: string, methodId: string) => {
|
|
1512
|
+
const oldCurrencyId = field.value;
|
|
1002
1513
|
field.onChange(id);
|
|
1003
1514
|
setValue('payment_method', methodId);
|
|
1004
1515
|
saveCurrencyPreference(id, session?.user?.did);
|
|
1516
|
+
|
|
1517
|
+
// Call API to switch currency and clear quote-related fields if currency changed
|
|
1518
|
+
// This is essential because quotes are currency-specific (e.g., TBA quote with 18 decimals
|
|
1519
|
+
// cannot be used for USD with 2 decimals)
|
|
1520
|
+
if (oldCurrencyId && oldCurrencyId !== id) {
|
|
1521
|
+
try {
|
|
1522
|
+
const { data } = await api.put(
|
|
1523
|
+
`/api/checkout-sessions/${checkoutSession.id}/switch-currency`,
|
|
1524
|
+
{
|
|
1525
|
+
currency_id: id,
|
|
1526
|
+
payment_method_id: methodId,
|
|
1527
|
+
}
|
|
1528
|
+
);
|
|
1529
|
+
if (data.currency_changed && onQuoteUpdated) {
|
|
1530
|
+
onQuoteUpdated({ checkoutSession: data, quotes: data.quotes });
|
|
1531
|
+
}
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
console.error('Failed to switch currency:', err);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1005
1536
|
}}
|
|
1006
1537
|
/>
|
|
1007
1538
|
)}
|
|
@@ -1154,11 +1685,10 @@ export default function PaymentForm({
|
|
|
1154
1685
|
}}
|
|
1155
1686
|
fullWidth
|
|
1156
1687
|
loading={state.submitting || state.paying}
|
|
1157
|
-
disabled={state.stripePaying || !quantityInventoryStatus || !
|
|
1688
|
+
disabled={state.stripePaying || !quantityInventoryStatus || !canPay}>
|
|
1158
1689
|
{state.submitting || state.paying ? t('payment.checkout.processing') : buttonText}
|
|
1159
1690
|
</LoadingButton>
|
|
1160
1691
|
</Box>
|
|
1161
|
-
|
|
1162
1692
|
{['subscription', 'setup'].includes(checkoutSession.mode) && (
|
|
1163
1693
|
<Typography sx={{ mt: 2.5, color: 'text.lighter', fontSize: '0.7875rem', lineHeight: '0.9625rem' }}>
|
|
1164
1694
|
{showStake
|
|
@@ -1205,6 +1735,18 @@ export default function PaymentForm({
|
|
|
1205
1735
|
)}
|
|
1206
1736
|
{FastCheckoutConfirmDialog}
|
|
1207
1737
|
{CreditInsufficientDialog}
|
|
1738
|
+
{PriceUpdatedDialog}
|
|
1739
|
+
{/* Final Freeze: Price change confirmation dialog */}
|
|
1740
|
+
{state.priceChangeConfirm?.open && (
|
|
1741
|
+
<PriceChangeConfirm
|
|
1742
|
+
open
|
|
1743
|
+
changePercent={state.priceChangeConfirm.changePercent}
|
|
1744
|
+
onConfirm={handlePriceChangeConfirm}
|
|
1745
|
+
onCancel={handlePriceChangeCancel}
|
|
1746
|
+
loading={state.submitting}
|
|
1747
|
+
/>
|
|
1748
|
+
)}
|
|
1749
|
+
{/* LockExpiredDialog removed - now auto-refreshes instead */}
|
|
1208
1750
|
</>
|
|
1209
1751
|
);
|
|
1210
1752
|
}
|