@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.
- 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/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/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/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
package/src/payment/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TCustomer,
|
|
10
10
|
TPaymentCurrency,
|
|
11
11
|
TPaymentMethod,
|
|
12
|
+
TPaymentIntent,
|
|
12
13
|
TPaymentMethodExpanded,
|
|
13
14
|
} from '@blocklet/payment-types';
|
|
14
15
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -16,7 +17,7 @@ import { Box, Fade, Stack, type BoxProps } from '@mui/material';
|
|
|
16
17
|
import { styled } from '@mui/system';
|
|
17
18
|
import { fromTokenToUnit } from '@ocap/util';
|
|
18
19
|
import { useSetState } from 'ahooks';
|
|
19
|
-
import { useEffect,
|
|
20
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
20
21
|
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
|
21
22
|
import trim from 'lodash/trim';
|
|
22
23
|
import type { LiteralUnion } from 'type-fest';
|
|
@@ -61,11 +62,54 @@ function PaymentInner({
|
|
|
61
62
|
onChange,
|
|
62
63
|
action,
|
|
63
64
|
showCheckoutSummary = true,
|
|
65
|
+
quotes,
|
|
66
|
+
rateUnavailable,
|
|
67
|
+
rateError,
|
|
64
68
|
}: MainProps) {
|
|
65
69
|
const { t } = useLocaleContext();
|
|
66
70
|
const { settings, session } = usePaymentContext();
|
|
67
71
|
const { isMobile } = useMobile();
|
|
68
|
-
const [state, setState] = useSetState<{
|
|
72
|
+
const [state, setState] = useSetState<{
|
|
73
|
+
checkoutSession: TCheckoutSessionExpanded;
|
|
74
|
+
quotes?: any;
|
|
75
|
+
rateUnavailable?: boolean;
|
|
76
|
+
rateError?: string;
|
|
77
|
+
paymentIntent?: TPaymentIntent | null;
|
|
78
|
+
liveRateInfo?: {
|
|
79
|
+
rate?: string;
|
|
80
|
+
provider_id?: string;
|
|
81
|
+
provider_name?: string;
|
|
82
|
+
base_currency?: string;
|
|
83
|
+
timestamp_ms?: number;
|
|
84
|
+
volatility_threshold?: number;
|
|
85
|
+
};
|
|
86
|
+
liveQuoteSnapshot?: {
|
|
87
|
+
id: string;
|
|
88
|
+
quoted_amount: string;
|
|
89
|
+
exchange_rate: string;
|
|
90
|
+
expires_at: number;
|
|
91
|
+
rate_timestamp_ms?: number | null;
|
|
92
|
+
renewed?: boolean;
|
|
93
|
+
};
|
|
94
|
+
liveRateUnavailable?: boolean;
|
|
95
|
+
liveRateError?: string;
|
|
96
|
+
isRateLoading?: boolean;
|
|
97
|
+
}>({
|
|
98
|
+
checkoutSession,
|
|
99
|
+
quotes,
|
|
100
|
+
rateUnavailable,
|
|
101
|
+
rateError,
|
|
102
|
+
paymentIntent,
|
|
103
|
+
liveRateInfo: undefined,
|
|
104
|
+
liveQuoteSnapshot: undefined,
|
|
105
|
+
liveRateUnavailable: false,
|
|
106
|
+
liveRateError: undefined,
|
|
107
|
+
isRateLoading: false,
|
|
108
|
+
});
|
|
109
|
+
// Track if currency is being switched (vs just rate refresh)
|
|
110
|
+
// Only show skeleton during currency switch, not during rate refresh
|
|
111
|
+
const isCurrencySwitchRef = useRef(false);
|
|
112
|
+
const prevCurrencyIdRef = useRef<string | null>(null);
|
|
69
113
|
const query = getQueryParams(window.location.href);
|
|
70
114
|
|
|
71
115
|
const availableCurrencyIds = useMemo(() => {
|
|
@@ -172,16 +216,227 @@ function PaymentInner({
|
|
|
172
216
|
(findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
|
|
173
217
|
settings.baseCurrency;
|
|
174
218
|
const method = paymentMethods.find((x: any) => x.id === currency.payment_method_id) as TPaymentMethod;
|
|
219
|
+
const hasDynamicPricing = useMemo(
|
|
220
|
+
() =>
|
|
221
|
+
state.checkoutSession.line_items.some(
|
|
222
|
+
(item: any) => (item.upsell_price || item.price)?.pricing_type === 'dynamic'
|
|
223
|
+
),
|
|
224
|
+
[state.checkoutSession.line_items]
|
|
225
|
+
);
|
|
226
|
+
// Stripe/USD payments don't need exchange rate - base_amount is already in USD
|
|
227
|
+
const isStripePayment = method?.type === 'stripe';
|
|
228
|
+
const needsExchangeRate = hasDynamicPricing && !isStripePayment;
|
|
229
|
+
const effectiveRateUnavailable = needsExchangeRate && (state.rateUnavailable || state.liveRateUnavailable);
|
|
230
|
+
// Don't expose technical errors to UI - only log them for debugging
|
|
231
|
+
if (state.liveRateError || state.rateError) {
|
|
232
|
+
console.error('[Rate Error]', { liveRateError: state.liveRateError, rateError: state.rateError });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Manual refresh function - exposed for retry button
|
|
236
|
+
const refreshRateRef = useRef<(() => Promise<void>) | null>(null);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!currencyId) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Detect currency switch (not initial load)
|
|
242
|
+
const isCurrencySwitch = prevCurrencyIdRef.current !== null && prevCurrencyIdRef.current !== currencyId;
|
|
243
|
+
prevCurrencyIdRef.current = currencyId;
|
|
244
|
+
|
|
245
|
+
if (isCurrencySwitch) {
|
|
246
|
+
isCurrencySwitchRef.current = true;
|
|
247
|
+
// Show skeleton during currency switch
|
|
248
|
+
setState({ isRateLoading: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (needsExchangeRate) {
|
|
252
|
+
setState({
|
|
253
|
+
liveRateInfo: undefined,
|
|
254
|
+
liveQuoteSnapshot: undefined,
|
|
255
|
+
liveRateUnavailable: false,
|
|
256
|
+
liveRateError: undefined,
|
|
257
|
+
});
|
|
258
|
+
liveRateRefreshRef.current = false;
|
|
259
|
+
refreshRateRef.current?.();
|
|
260
|
+
} else {
|
|
261
|
+
setState({
|
|
262
|
+
liveRateInfo: undefined,
|
|
263
|
+
liveQuoteSnapshot: undefined,
|
|
264
|
+
liveRateUnavailable: false,
|
|
265
|
+
liveRateError: undefined,
|
|
266
|
+
});
|
|
267
|
+
liveRateRefreshRef.current = false;
|
|
268
|
+
// For Stripe payments (no exchange rate needed), clear loading after promotion recalc
|
|
269
|
+
// If no discounts, clear immediately
|
|
270
|
+
if (isCurrencySwitch && !(state.checkoutSession as any)?.discounts?.length) {
|
|
271
|
+
setState({ isRateLoading: false });
|
|
272
|
+
isCurrencySwitchRef.current = false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
276
|
+
}, [currencyId, needsExchangeRate]);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
// Skip exchange rate fetching for Stripe payments (USD base_amount is used directly)
|
|
280
|
+
if (!state.checkoutSession?.id || completed || !needsExchangeRate) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
let cancelled = false;
|
|
284
|
+
let consecutiveFailures = 0;
|
|
285
|
+
const baseInterval = 30000;
|
|
286
|
+
const MAX_INTERVAL = 5 * 60 * 1000;
|
|
287
|
+
const QUICK_RETRY_DELAY = 1000; // 1s quick retry
|
|
288
|
+
const MAX_QUICK_RETRIES = 2; // Quick retry 2 times before exponential backoff
|
|
289
|
+
let currentInterval = baseInterval;
|
|
290
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
291
|
+
|
|
292
|
+
const scheduleNext = () => {
|
|
293
|
+
if (timer) {
|
|
294
|
+
clearInterval(timer);
|
|
295
|
+
}
|
|
296
|
+
timer = window.setInterval(() => {
|
|
297
|
+
fetchRate(false);
|
|
298
|
+
}, currentInterval) as unknown as ReturnType<typeof setInterval>;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const fetchRate = async (isManualRetry = false) => {
|
|
302
|
+
if (document.hidden && !isManualRetry) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (liveRateRefreshRef.current) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
liveRateRefreshRef.current = true;
|
|
314
|
+
|
|
315
|
+
// Quick retry logic: try up to MAX_QUICK_RETRIES times with short delays
|
|
316
|
+
let quickRetryCount = 0;
|
|
317
|
+
let lastError: any = null;
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
// eslint-disable-next-line no-await-in-loop
|
|
321
|
+
while (quickRetryCount <= MAX_QUICK_RETRIES) {
|
|
322
|
+
try {
|
|
323
|
+
// eslint-disable-next-line no-await-in-loop
|
|
324
|
+
const { data } = await api.get(`/api/checkout-sessions/${state.checkoutSession.id}/exchange-rate`, {
|
|
325
|
+
params: currencyId ? { currency_id: currencyId } : undefined,
|
|
326
|
+
});
|
|
327
|
+
if (cancelled) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
consecutiveFailures = 0;
|
|
331
|
+
currentInterval = baseInterval;
|
|
332
|
+
// Final Freeze: Only store rate info, no quote_snapshot
|
|
333
|
+
setState({
|
|
334
|
+
liveRateInfo: data,
|
|
335
|
+
liveQuoteSnapshot: undefined, // Quote is created at submit time only
|
|
336
|
+
liveRateUnavailable: false,
|
|
337
|
+
liveRateError: undefined,
|
|
338
|
+
});
|
|
339
|
+
// Clear loading state after rate fetch - but only if this was a currency switch
|
|
340
|
+
// and no discounts need recalculation (recalculatePromotion will clear it otherwise)
|
|
341
|
+
if (isCurrencySwitchRef.current && !(state.checkoutSession as any)?.discounts?.length) {
|
|
342
|
+
setState({ isRateLoading: false });
|
|
343
|
+
isCurrencySwitchRef.current = false;
|
|
344
|
+
}
|
|
345
|
+
scheduleNext();
|
|
346
|
+
return;
|
|
347
|
+
} catch (err: any) {
|
|
348
|
+
lastError = err;
|
|
349
|
+
quickRetryCount++;
|
|
350
|
+
if (quickRetryCount <= MAX_QUICK_RETRIES && !cancelled) {
|
|
351
|
+
// eslint-disable-next-line no-console
|
|
352
|
+
console.log(
|
|
353
|
+
`[Exchange Rate] Quick retry ${quickRetryCount}/${MAX_QUICK_RETRIES} after ${QUICK_RETRY_DELAY}ms`
|
|
354
|
+
);
|
|
355
|
+
// eslint-disable-next-line no-await-in-loop
|
|
356
|
+
await new Promise((resolve) => {
|
|
357
|
+
setTimeout(resolve, QUICK_RETRY_DELAY);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// All quick retries failed
|
|
364
|
+
if (cancelled) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
consecutiveFailures++;
|
|
368
|
+
const technicalError = lastError?.response?.data?.error || formatError(lastError);
|
|
369
|
+
console.error('[Exchange Rate Fetch Error]', {
|
|
370
|
+
error: technicalError,
|
|
371
|
+
consecutiveFailures,
|
|
372
|
+
sessionId: state.checkoutSession?.id,
|
|
373
|
+
currencyId,
|
|
374
|
+
});
|
|
175
375
|
|
|
176
|
-
|
|
376
|
+
setState({
|
|
377
|
+
liveRateUnavailable: true,
|
|
378
|
+
liveRateError: undefined,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (consecutiveFailures >= 3) {
|
|
382
|
+
console.warn('Exchange rate fetch failed multiple times', { consecutiveFailures, technicalError });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const nextInterval = Math.min(baseInterval * 2 ** (consecutiveFailures - 1), MAX_INTERVAL);
|
|
386
|
+
currentInterval = nextInterval;
|
|
387
|
+
scheduleNext();
|
|
388
|
+
} finally {
|
|
389
|
+
liveRateRefreshRef.current = false;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Expose refresh function for manual retry
|
|
394
|
+
refreshRateRef.current = async () => {
|
|
395
|
+
liveRateRefreshRef.current = false; // Allow immediate retry
|
|
396
|
+
await fetchRate(true);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
fetchRate(false);
|
|
400
|
+
|
|
401
|
+
const handleVisibilityChange = () => {
|
|
402
|
+
if (!document.hidden) {
|
|
403
|
+
fetchRate(false);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
407
|
+
|
|
408
|
+
return () => {
|
|
409
|
+
cancelled = true;
|
|
410
|
+
refreshRateRef.current = null;
|
|
411
|
+
if (timer) {
|
|
412
|
+
clearInterval(timer);
|
|
413
|
+
}
|
|
414
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
415
|
+
};
|
|
416
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
417
|
+
}, [state.checkoutSession?.id, currencyId, completed, needsExchangeRate]);
|
|
418
|
+
|
|
419
|
+
const recalculatePromotion = async () => {
|
|
177
420
|
if ((state.checkoutSession as any)?.discounts?.length) {
|
|
178
|
-
|
|
179
|
-
.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
|
|
421
|
+
try {
|
|
422
|
+
await api.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
|
|
180
423
|
currency_id: currencyId,
|
|
181
|
-
})
|
|
182
|
-
.then(() => {
|
|
183
|
-
onPromotionUpdate();
|
|
184
424
|
});
|
|
425
|
+
// Wait for session update before clearing loading state
|
|
426
|
+
await onPromotionUpdate();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error('[recalculatePromotion] Error:', err);
|
|
429
|
+
} finally {
|
|
430
|
+
// Clear loading state after promotion recalculation AND session update complete
|
|
431
|
+
if (isCurrencySwitchRef.current) {
|
|
432
|
+
setState({ isRateLoading: false });
|
|
433
|
+
isCurrencySwitchRef.current = false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else if (isCurrencySwitchRef.current) {
|
|
437
|
+
// No discounts to recalculate - clear loading immediately
|
|
438
|
+
setState({ isRateLoading: false });
|
|
439
|
+
isCurrencySwitchRef.current = false;
|
|
185
440
|
}
|
|
186
441
|
};
|
|
187
442
|
|
|
@@ -239,12 +494,18 @@ function PaymentInner({
|
|
|
239
494
|
const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/adjust-quantity`, {
|
|
240
495
|
itemId,
|
|
241
496
|
quantity,
|
|
497
|
+
currency_id: currencyId,
|
|
242
498
|
});
|
|
243
499
|
if (data.discounts?.length) {
|
|
244
500
|
recalculatePromotion();
|
|
245
501
|
return;
|
|
246
502
|
}
|
|
247
|
-
setState({
|
|
503
|
+
setState({
|
|
504
|
+
checkoutSession: data,
|
|
505
|
+
...(data.rateUnavailable !== undefined && { rateUnavailable: data.rateUnavailable }),
|
|
506
|
+
...(data.rateError !== undefined && { rateError: data.rateError }),
|
|
507
|
+
...(data.quotes !== undefined && { quotes: data.quotes }),
|
|
508
|
+
});
|
|
248
509
|
} catch (err) {
|
|
249
510
|
console.error(err);
|
|
250
511
|
Toast.error(formatError(err));
|
|
@@ -297,6 +558,52 @@ function PaymentInner({
|
|
|
297
558
|
onPaid(result);
|
|
298
559
|
};
|
|
299
560
|
|
|
561
|
+
const handleQuoteUpdated = (payload: {
|
|
562
|
+
checkoutSession: TCheckoutSessionExpanded;
|
|
563
|
+
quotes?: any;
|
|
564
|
+
rateUnavailable?: boolean;
|
|
565
|
+
rateError?: string;
|
|
566
|
+
paymentIntent?: TPaymentIntent | null;
|
|
567
|
+
}) => {
|
|
568
|
+
setState({
|
|
569
|
+
checkoutSession: payload.checkoutSession,
|
|
570
|
+
...(payload.quotes !== undefined && { quotes: payload.quotes }),
|
|
571
|
+
...(payload.rateUnavailable !== undefined && { rateUnavailable: payload.rateUnavailable }),
|
|
572
|
+
...(payload.rateError !== undefined && { rateError: payload.rateError }),
|
|
573
|
+
...(payload.paymentIntent !== undefined && { paymentIntent: payload.paymentIntent }),
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const handlePaymentIntentUpdate = (intent: TPaymentIntent | null) => {
|
|
578
|
+
setState({ paymentIntent: intent });
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const quoteRefreshRef = useRef(false);
|
|
582
|
+
const liveRateRefreshRef = useRef(false);
|
|
583
|
+
const handleQuoteExpired = async (forceRefresh = false) => {
|
|
584
|
+
if (quoteRefreshRef.current) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
quoteRefreshRef.current = true;
|
|
588
|
+
try {
|
|
589
|
+
const { data } = await api.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`, {
|
|
590
|
+
params: forceRefresh ? { forceRefresh: '1' } : undefined,
|
|
591
|
+
});
|
|
592
|
+
handleQuoteUpdated({
|
|
593
|
+
checkoutSession: data.checkoutSession,
|
|
594
|
+
quotes: data.quotes,
|
|
595
|
+
rateUnavailable: data.rateUnavailable,
|
|
596
|
+
rateError: data.rateError,
|
|
597
|
+
paymentIntent: data.paymentIntent,
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error(err);
|
|
601
|
+
Toast.error(formatError(err));
|
|
602
|
+
} finally {
|
|
603
|
+
quoteRefreshRef.current = false;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
300
607
|
// trialing can be limited with trial_currency, which can be a list
|
|
301
608
|
let trialInDays = Number(state.checkoutSession?.subscription_data?.trial_period_days || 0);
|
|
302
609
|
let trialEnd = Number(state.checkoutSession?.subscription_data?.trial_end || 0);
|
|
@@ -328,6 +635,7 @@ function PaymentInner({
|
|
|
328
635
|
)}
|
|
329
636
|
showStaking={showStaking(method, currency, !!state.checkoutSession.subscription_data?.no_stake)}
|
|
330
637
|
currency={currency}
|
|
638
|
+
paymentIntent={state.paymentIntent || paymentIntent}
|
|
331
639
|
onUpsell={onUpsell}
|
|
332
640
|
onDownsell={onDownsell}
|
|
333
641
|
onQuantityChange={onQuantityChange}
|
|
@@ -343,6 +651,28 @@ function PaymentInner({
|
|
|
343
651
|
onPromotionUpdate={onPromotionUpdate}
|
|
344
652
|
paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
|
|
345
653
|
showFeatures={showFeatures}
|
|
654
|
+
rateUnavailable={effectiveRateUnavailable}
|
|
655
|
+
isRateLoading={state.isRateLoading}
|
|
656
|
+
liveRate={state.liveRateInfo}
|
|
657
|
+
liveQuoteSnapshot={state.liveQuoteSnapshot}
|
|
658
|
+
onQuoteExpired={handleQuoteExpired}
|
|
659
|
+
onRefreshRate={refreshRateRef.current || undefined}
|
|
660
|
+
isStripePayment={isStripePayment}
|
|
661
|
+
onSlippageChange={async (slippageConfig) => {
|
|
662
|
+
try {
|
|
663
|
+
const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/slippage`, {
|
|
664
|
+
slippage_config: slippageConfig,
|
|
665
|
+
});
|
|
666
|
+
handleQuoteUpdated({
|
|
667
|
+
checkoutSession: data.checkoutSession || state.checkoutSession,
|
|
668
|
+
quotes: data.quotes,
|
|
669
|
+
rateUnavailable: data.rateUnavailable,
|
|
670
|
+
rateError: data.rateError,
|
|
671
|
+
});
|
|
672
|
+
} catch (err) {
|
|
673
|
+
console.error('Failed to update slippage', err);
|
|
674
|
+
}
|
|
675
|
+
}}
|
|
346
676
|
/>
|
|
347
677
|
{mode === 'standalone' && !isMobile && (
|
|
348
678
|
<CheckoutFooter className="cko-footer" sx={{ color: 'text.lighter' }} />
|
|
@@ -385,13 +715,16 @@ function PaymentInner({
|
|
|
385
715
|
currencyId={currencyId}
|
|
386
716
|
checkoutSession={state.checkoutSession}
|
|
387
717
|
paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
|
|
388
|
-
paymentIntent={paymentIntent}
|
|
718
|
+
paymentIntent={state.paymentIntent || paymentIntent}
|
|
389
719
|
paymentLink={paymentLink}
|
|
390
720
|
customer={customer}
|
|
391
721
|
onPaid={handlePaid}
|
|
392
722
|
onError={onError}
|
|
723
|
+
onQuoteUpdated={handleQuoteUpdated}
|
|
724
|
+
onPaymentIntentUpdate={handlePaymentIntentUpdate}
|
|
393
725
|
mode={mode}
|
|
394
726
|
action={action}
|
|
727
|
+
rateUnavailable={effectiveRateUnavailable}
|
|
395
728
|
/>
|
|
396
729
|
)}
|
|
397
730
|
</Stack>
|
|
@@ -416,6 +749,9 @@ export default function Payment({
|
|
|
416
749
|
goBack,
|
|
417
750
|
action,
|
|
418
751
|
showCheckoutSummary = true,
|
|
752
|
+
quotes,
|
|
753
|
+
rateUnavailable,
|
|
754
|
+
rateError,
|
|
419
755
|
}: Props) {
|
|
420
756
|
const { t } = useLocaleContext();
|
|
421
757
|
const { refresh, livemode, setLivemode } = usePaymentContext();
|
|
@@ -492,6 +828,9 @@ export default function Payment({
|
|
|
492
828
|
mode={mode}
|
|
493
829
|
action={action}
|
|
494
830
|
showCheckoutSummary={showCheckoutSummary}
|
|
831
|
+
quotes={quotes}
|
|
832
|
+
rateUnavailable={rateUnavailable}
|
|
833
|
+
rateError={rateError}
|
|
495
834
|
/>
|
|
496
835
|
);
|
|
497
836
|
};
|