@blocklet/payment-react 1.24.4 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/es/payment/form/index.js
CHANGED
|
@@ -11,11 +11,12 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
|
|
|
11
11
|
import { joinURL } from "ufo";
|
|
12
12
|
import { dispatch } from "use-bus";
|
|
13
13
|
import isEmail from "validator/es/lib/isEmail";
|
|
14
|
-
import { fromUnitToToken } from "@ocap/util";
|
|
14
|
+
import { BN, fromTokenToUnit, fromUnitToToken } from "@ocap/util";
|
|
15
15
|
import DID from "@arcblock/ux/lib/DID";
|
|
16
16
|
import isEmpty from "lodash/isEmpty";
|
|
17
17
|
import { HelpOutline, OpenInNew } from "@mui/icons-material";
|
|
18
18
|
import { ReactGA } from "@arcblock/ux/lib/withTracker";
|
|
19
|
+
import trim from "lodash/trim";
|
|
19
20
|
import FormInput from "../../components/input.js";
|
|
20
21
|
import FormLabel from "../../components/label.js";
|
|
21
22
|
import { usePaymentContext } from "../../contexts/payment.js";
|
|
@@ -29,7 +30,13 @@ import {
|
|
|
29
30
|
getQueryParams,
|
|
30
31
|
getStatementDescriptor,
|
|
31
32
|
getTokenBalanceLink,
|
|
32
|
-
isCrossOrigin
|
|
33
|
+
isCrossOrigin,
|
|
34
|
+
getCheckoutAmount,
|
|
35
|
+
formatNumber,
|
|
36
|
+
formatUsdAmount,
|
|
37
|
+
getUsdAmountFromBaseAmount,
|
|
38
|
+
getUsdAmountFromTokenUnits,
|
|
39
|
+
formatAmount
|
|
33
40
|
} from "../../libs/util.js";
|
|
34
41
|
import AddressForm from "./address.js";
|
|
35
42
|
import CurrencySelector from "./currency.js";
|
|
@@ -41,7 +48,11 @@ import LoadingButton from "../../components/loading-button.js";
|
|
|
41
48
|
import OverdueInvoicePayment from "../../components/over-due-invoice-payment.js";
|
|
42
49
|
import { saveCurrencyPreference } from "../../libs/currency.js";
|
|
43
50
|
import ConfirmDialog from "../../components/confirm.js";
|
|
51
|
+
import PriceChangeConfirm from "../../components/price-change-confirm.js";
|
|
44
52
|
import { getFieldValidation, validatePostalCode } from "../../libs/validator.js";
|
|
53
|
+
const generateIdempotencyKey = (sessionId, currencyId) => {
|
|
54
|
+
return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
|
|
55
|
+
};
|
|
45
56
|
export const waitForCheckoutComplete = async (sessionId) => {
|
|
46
57
|
let result;
|
|
47
58
|
await pWaitFor(
|
|
@@ -110,10 +121,13 @@ export default function PaymentForm({
|
|
|
110
121
|
customer,
|
|
111
122
|
onPaid,
|
|
112
123
|
onError,
|
|
124
|
+
onQuoteUpdated = void 0,
|
|
125
|
+
onPaymentIntentUpdate = void 0,
|
|
113
126
|
// mode,
|
|
114
127
|
action,
|
|
115
128
|
onlyShowBtn = false,
|
|
116
|
-
isDonation = false
|
|
129
|
+
isDonation = false,
|
|
130
|
+
rateUnavailable = false
|
|
117
131
|
}) {
|
|
118
132
|
const { t, locale } = useLocaleContext();
|
|
119
133
|
const { isMobile } = useMobile();
|
|
@@ -151,7 +165,8 @@ export default function PaymentForm({
|
|
|
151
165
|
stripePaying: false,
|
|
152
166
|
fastCheckoutInfo: null,
|
|
153
167
|
creditInsufficientInfo: null,
|
|
154
|
-
showEditForm: false
|
|
168
|
+
showEditForm: false,
|
|
169
|
+
priceChangeConfirm: null
|
|
155
170
|
});
|
|
156
171
|
const currencies = flattenPaymentMethods(paymentMethods);
|
|
157
172
|
const searchParams = getQueryParams(window.location.href);
|
|
@@ -260,7 +275,166 @@ export default function PaymentForm({
|
|
|
260
275
|
const method = paymentMethods.find((x) => x.id === paymentMethod);
|
|
261
276
|
const paymentCurrency = currencies.find((x) => x.id === paymentCurrencyId);
|
|
262
277
|
const showStake = method.type === "arcblock" && !checkoutSession.subscription_data?.no_stake;
|
|
278
|
+
const hasDynamicPricing = useMemo(
|
|
279
|
+
() => (checkoutSession.line_items || []).some((item) => {
|
|
280
|
+
const price = item.upsell_price || item.price;
|
|
281
|
+
return price && price?.pricing_type === "dynamic";
|
|
282
|
+
}),
|
|
283
|
+
[checkoutSession.line_items]
|
|
284
|
+
);
|
|
285
|
+
const rateUnavailableForDynamic = hasDynamicPricing && rateUnavailable;
|
|
286
|
+
const canPay = payable && !rateUnavailableForDynamic;
|
|
263
287
|
const isDonationMode = checkoutSession?.submit_type === "donate" && isDonation;
|
|
288
|
+
const [priceUpdateInfo, setPriceUpdateInfo] = useSetState({
|
|
289
|
+
open: false,
|
|
290
|
+
total: "",
|
|
291
|
+
usd: null,
|
|
292
|
+
hasQuotes: false,
|
|
293
|
+
baseCurrency: "USD",
|
|
294
|
+
oldTotal: "",
|
|
295
|
+
reason: "recalculated"
|
|
296
|
+
});
|
|
297
|
+
const normalizeExchangeRate = useMemoizedFn((rate) => {
|
|
298
|
+
if (!rate) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const value = Number(rate);
|
|
302
|
+
if (!Number.isFinite(value)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return value.toFixed(8);
|
|
306
|
+
});
|
|
307
|
+
const getExchangeRateFromSession = useMemoizedFn((sessionData) => {
|
|
308
|
+
if (!sessionData?.line_items?.length) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
for (const item of sessionData.line_items) {
|
|
312
|
+
const rate = item?.exchange_rate;
|
|
313
|
+
if (rate) {
|
|
314
|
+
return rate;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
});
|
|
319
|
+
const quoteAutoRetryRef = useRef(false);
|
|
320
|
+
const lastRetryKeyRef = useRef("");
|
|
321
|
+
const buildRetryKey = useMemoizedFn((sessionData) => {
|
|
322
|
+
if (!sessionData?.line_items?.length) {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
return sessionData.line_items.map((item) => {
|
|
326
|
+
const priceId = item.price_id || item.price?.id || "";
|
|
327
|
+
const quoteId = item?.quote_id || "";
|
|
328
|
+
const quotedAmount = item?.quoted_amount || "";
|
|
329
|
+
const exchangeRate = item?.exchange_rate || "";
|
|
330
|
+
return `${priceId}:${quoteId}:${quotedAmount}:${exchangeRate}`;
|
|
331
|
+
}).join("|");
|
|
332
|
+
});
|
|
333
|
+
const buildPriceUpdateSummary = useMemoizedFn((sessionData) => {
|
|
334
|
+
if (!paymentCurrency) {
|
|
335
|
+
return { total: "", usd: null, hasQuotes: false, baseCurrency: "USD", totalUnit: null };
|
|
336
|
+
}
|
|
337
|
+
const lineItems = sessionData.line_items || [];
|
|
338
|
+
let baseCurrency = "USD";
|
|
339
|
+
for (const item of lineItems) {
|
|
340
|
+
const price = item.upsell_price || item.price;
|
|
341
|
+
const base = price?.base_currency;
|
|
342
|
+
if (base) {
|
|
343
|
+
baseCurrency = base;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const hasQuotes = lineItems.some((item) => item?.quoted_amount && item?.exchange_rate);
|
|
348
|
+
if (!hasQuotes) {
|
|
349
|
+
return { total: "", usd: null, hasQuotes: false, baseCurrency, totalUnit: null };
|
|
350
|
+
}
|
|
351
|
+
let trialInDays = Number(sessionData?.subscription_data?.trial_period_days || 0);
|
|
352
|
+
const trialCurrencyIds = (sessionData?.subscription_data?.trial_currency || "").split(",").map(trim).filter(Boolean);
|
|
353
|
+
if (trialCurrencyIds.length > 0 && paymentCurrencyId && trialCurrencyIds.includes(paymentCurrencyId) === false) {
|
|
354
|
+
trialInDays = 0;
|
|
355
|
+
}
|
|
356
|
+
const { total } = getCheckoutAmount(lineItems, paymentCurrency, trialInDays > 0);
|
|
357
|
+
const discountAmount = new BN(sessionData.total_details?.amount_discount || "0");
|
|
358
|
+
const totalUnit = new BN(total).sub(discountAmount);
|
|
359
|
+
const normalizedTotalUnit = totalUnit.isNeg() ? new BN(0) : totalUnit;
|
|
360
|
+
const totalDisplay = `${formatNumber(
|
|
361
|
+
fromUnitToToken(normalizedTotalUnit.toString(), paymentCurrency.decimal),
|
|
362
|
+
6
|
|
363
|
+
)} ${paymentCurrency.symbol}`;
|
|
364
|
+
const itemUsdReferences = lineItems.map((item) => {
|
|
365
|
+
const price = item.upsell_price || item.price;
|
|
366
|
+
const baseAmount = price?.base_amount;
|
|
367
|
+
const hasBaseAmount = baseAmount !== void 0 && baseAmount !== null;
|
|
368
|
+
if (hasBaseAmount) {
|
|
369
|
+
return getUsdAmountFromBaseAmount(baseAmount, item.quantity || 0);
|
|
370
|
+
}
|
|
371
|
+
const exchangeRate = item?.exchange_rate;
|
|
372
|
+
const quotedAmount = item?.quoted_amount;
|
|
373
|
+
if (!exchangeRate || !quotedAmount) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
return getUsdAmountFromTokenUnits(new BN(quotedAmount), paymentCurrency.decimal, exchangeRate);
|
|
377
|
+
});
|
|
378
|
+
const usdValues = itemUsdReferences.filter((value) => Boolean(value));
|
|
379
|
+
if (!usdValues.length) {
|
|
380
|
+
return { total: totalDisplay, usd: null, hasQuotes, baseCurrency, totalUnit: normalizedTotalUnit };
|
|
381
|
+
}
|
|
382
|
+
const sumUnit = usdValues.reduce((acc, value) => acc.add(new BN(fromTokenToUnit(value, 8))), new BN(0));
|
|
383
|
+
const totalUsdReference = fromUnitToToken(sumUnit.toString(), 8);
|
|
384
|
+
return {
|
|
385
|
+
total: totalDisplay,
|
|
386
|
+
usd: formatUsdAmount(totalUsdReference, locale),
|
|
387
|
+
hasQuotes,
|
|
388
|
+
baseCurrency,
|
|
389
|
+
totalUnit: normalizedTotalUnit
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
const compareTotals = useMemoizedFn((prevSession, nextSession) => {
|
|
393
|
+
const prev = buildPriceUpdateSummary(prevSession);
|
|
394
|
+
const next = buildPriceUpdateSummary(nextSession);
|
|
395
|
+
if (!prev.totalUnit || !next.totalUnit) {
|
|
396
|
+
return { changed: false, prev, next };
|
|
397
|
+
}
|
|
398
|
+
const diff = next.totalUnit.sub(prev.totalUnit).abs();
|
|
399
|
+
const epsilon = new BN(1);
|
|
400
|
+
return { changed: diff.gt(epsilon), prev, next };
|
|
401
|
+
});
|
|
402
|
+
const applyQuoteUpdate = useMemoizedFn(
|
|
403
|
+
(payload, options = {}) => {
|
|
404
|
+
if (!payload?.checkoutSession) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
onQuoteUpdated?.({
|
|
408
|
+
checkoutSession: payload.checkoutSession,
|
|
409
|
+
quotes: payload.quotes,
|
|
410
|
+
rateUnavailable: payload.rateUnavailable,
|
|
411
|
+
rateError: payload.rateError
|
|
412
|
+
});
|
|
413
|
+
const { changed, prev, next } = compareTotals(checkoutSession, payload.checkoutSession);
|
|
414
|
+
const previousRate = normalizeExchangeRate(getExchangeRateFromSession(checkoutSession));
|
|
415
|
+
const nextRate = normalizeExchangeRate(getExchangeRateFromSession(payload.checkoutSession));
|
|
416
|
+
const rateChanged = !!(previousRate && nextRate && previousRate !== nextRate);
|
|
417
|
+
const shouldShowModal = (options.forceConfirm || changed) && next.hasQuotes;
|
|
418
|
+
if (shouldShowModal) {
|
|
419
|
+
setPriceUpdateInfo({
|
|
420
|
+
open: true,
|
|
421
|
+
total: next.total,
|
|
422
|
+
usd: next.usd,
|
|
423
|
+
hasQuotes: next.hasQuotes,
|
|
424
|
+
baseCurrency: next.baseCurrency,
|
|
425
|
+
oldTotal: prev.total,
|
|
426
|
+
reason: options.reason || (rateChanged ? "rateChanged" : "recalculated")
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
setPriceUpdateInfo({ open: false });
|
|
431
|
+
const retryKey = buildRetryKey(payload.checkoutSession);
|
|
432
|
+
if (retryKey && retryKey !== lastRetryKeyRef.current) {
|
|
433
|
+
lastRetryKeyRef.current = retryKey;
|
|
434
|
+
quoteAutoRetryRef.current = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
);
|
|
264
438
|
const validateUserInfo = (values) => {
|
|
265
439
|
if (!values) {
|
|
266
440
|
return false;
|
|
@@ -350,6 +524,16 @@ export default function PaymentForm({
|
|
|
350
524
|
const customerPhone = useWatch({ control, name: "customer_phone" });
|
|
351
525
|
const billingAddress = useWatch({ control, name: "billing_address" });
|
|
352
526
|
const showForm = session?.user ? state.showEditForm : false;
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
if (!quoteAutoRetryRef.current) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (state.submitting || state.paying) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
quoteAutoRetryRef.current = false;
|
|
535
|
+
onAction();
|
|
536
|
+
}, [state.submitting, state.paying]);
|
|
353
537
|
const handleConnected = async () => {
|
|
354
538
|
if (processingRef.current) {
|
|
355
539
|
return;
|
|
@@ -407,7 +591,10 @@ export default function PaymentForm({
|
|
|
407
591
|
}
|
|
408
592
|
});
|
|
409
593
|
try {
|
|
410
|
-
const result = await api.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm
|
|
594
|
+
const result = await api.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`, {});
|
|
595
|
+
if (result.data.paymentIntent) {
|
|
596
|
+
onPaymentIntentUpdate?.(result.data.paymentIntent);
|
|
597
|
+
}
|
|
411
598
|
if (result.data.fastPaid) {
|
|
412
599
|
setState({
|
|
413
600
|
fastCheckoutInfo: null,
|
|
@@ -424,6 +611,51 @@ export default function PaymentForm({
|
|
|
424
611
|
}
|
|
425
612
|
} catch (err) {
|
|
426
613
|
console.error(err);
|
|
614
|
+
const errorCode = err?.response?.data?.code;
|
|
615
|
+
if ([
|
|
616
|
+
"QUOTE_LOCK_EXPIRED",
|
|
617
|
+
"QUOTE_AMOUNT_MISMATCH",
|
|
618
|
+
"QUOTE_EXPIRED_OR_USED",
|
|
619
|
+
"QUOTE_NOT_FOUND",
|
|
620
|
+
"QUOTE_REQUIRED"
|
|
621
|
+
].includes(errorCode)) {
|
|
622
|
+
try {
|
|
623
|
+
const { data: refreshed } = await api.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, {
|
|
624
|
+
params: { forceRefresh: "1" }
|
|
625
|
+
});
|
|
626
|
+
if (refreshed?.checkoutSession) {
|
|
627
|
+
applyQuoteUpdate(refreshed, { reason: "rateChanged" });
|
|
628
|
+
Toast.info(t("payment.checkout.quote.updated.pleaseRetry") || "Price updated, please resubmit");
|
|
629
|
+
}
|
|
630
|
+
} catch (refreshError) {
|
|
631
|
+
console.error(refreshError);
|
|
632
|
+
Toast.error(formatError(refreshError));
|
|
633
|
+
} finally {
|
|
634
|
+
setState({ fastCheckoutInfo: null });
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (errorCode === "QUOTE_UPDATED") {
|
|
639
|
+
const payload = err?.response?.data;
|
|
640
|
+
if (payload?.checkoutSession) {
|
|
641
|
+
applyQuoteUpdate(payload);
|
|
642
|
+
}
|
|
643
|
+
setState({ fastCheckoutInfo: null });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (errorCode === "RATE_UNAVAILABLE") {
|
|
647
|
+
const payload = err?.response?.data;
|
|
648
|
+
if (payload?.checkoutSession) {
|
|
649
|
+
onQuoteUpdated?.({
|
|
650
|
+
checkoutSession: payload.checkoutSession,
|
|
651
|
+
quotes: payload.quotes,
|
|
652
|
+
rateUnavailable: payload.rateUnavailable,
|
|
653
|
+
rateError: payload.rateError
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
setState({ fastCheckoutInfo: null });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
427
659
|
Toast.error(formatError(err));
|
|
428
660
|
setState({
|
|
429
661
|
fastCheckoutInfo: null
|
|
@@ -436,6 +668,23 @@ export default function PaymentForm({
|
|
|
436
668
|
const handleCreditInsufficientClose = () => {
|
|
437
669
|
setState({ creditInsufficientInfo: null });
|
|
438
670
|
};
|
|
671
|
+
const handlePriceUpdateConfirm = () => {
|
|
672
|
+
setPriceUpdateInfo({ open: false });
|
|
673
|
+
quoteAutoRetryRef.current = true;
|
|
674
|
+
};
|
|
675
|
+
const handlePriceUpdateCancel = () => {
|
|
676
|
+
setPriceUpdateInfo({ open: false });
|
|
677
|
+
};
|
|
678
|
+
const handlePriceChangeConfirm = () => {
|
|
679
|
+
const formData = state.priceChangeConfirm?.formData;
|
|
680
|
+
setState({ priceChangeConfirm: null });
|
|
681
|
+
if (formData) {
|
|
682
|
+
onFormSubmit(formData);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
const handlePriceChangeCancel = () => {
|
|
686
|
+
setState({ priceChangeConfirm: null });
|
|
687
|
+
};
|
|
439
688
|
const openConnect = () => {
|
|
440
689
|
try {
|
|
441
690
|
if (!["arcblock", "ethereum", "base"].includes(method.type)) {
|
|
@@ -475,6 +724,9 @@ export default function PaymentForm({
|
|
|
475
724
|
}
|
|
476
725
|
};
|
|
477
726
|
const onFormSubmit = async (data) => {
|
|
727
|
+
if (state.submitting) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
478
730
|
const userInfo = session.user;
|
|
479
731
|
if (!userInfo.sourceAppPid) {
|
|
480
732
|
const hasVendorConfig = checkoutSession.line_items?.some(
|
|
@@ -488,10 +740,18 @@ export default function PaymentForm({
|
|
|
488
740
|
setState({ submitting: true });
|
|
489
741
|
try {
|
|
490
742
|
let result;
|
|
743
|
+
const previewRate = checkoutSession.line_items?.find((item) => item?.exchange_rate)?.exchange_rate || void 0;
|
|
744
|
+
const payload = {
|
|
745
|
+
...data,
|
|
746
|
+
// Final Freeze: Include these for new quote creation at submit
|
|
747
|
+
idempotency_key: generateIdempotencyKey(checkoutSession.id, paymentCurrency?.id || ""),
|
|
748
|
+
preview_rate: previewRate || void 0,
|
|
749
|
+
price_confirmed: state.priceChangeConfirm?.formData ? true : void 0
|
|
750
|
+
};
|
|
491
751
|
if (isDonationMode) {
|
|
492
|
-
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`,
|
|
752
|
+
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`, payload);
|
|
493
753
|
} else {
|
|
494
|
-
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`,
|
|
754
|
+
result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, payload);
|
|
495
755
|
}
|
|
496
756
|
setState({
|
|
497
757
|
paymentIntent: result.data.paymentIntent,
|
|
@@ -500,7 +760,31 @@ export default function PaymentForm({
|
|
|
500
760
|
submitting: false,
|
|
501
761
|
customerLimited: false
|
|
502
762
|
});
|
|
763
|
+
if (result.data.paymentIntent) {
|
|
764
|
+
onPaymentIntentUpdate?.(result.data.paymentIntent);
|
|
765
|
+
}
|
|
503
766
|
if (["arcblock", "ethereum", "base"].includes(method.type)) {
|
|
767
|
+
if (result.data.noPaymentRequired) {
|
|
768
|
+
try {
|
|
769
|
+
const confirmResult = await api.post(
|
|
770
|
+
`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`,
|
|
771
|
+
{}
|
|
772
|
+
);
|
|
773
|
+
if (confirmResult.data.paymentIntent) {
|
|
774
|
+
onPaymentIntentUpdate?.(confirmResult.data.paymentIntent);
|
|
775
|
+
}
|
|
776
|
+
if (confirmResult.data.fastPaid || confirmResult.data.checkoutSession?.status === "complete") {
|
|
777
|
+
setState({ paying: true });
|
|
778
|
+
await handleConnected();
|
|
779
|
+
} else {
|
|
780
|
+
openConnect();
|
|
781
|
+
}
|
|
782
|
+
} catch (confirmErr) {
|
|
783
|
+
console.error("noPaymentRequired confirm failed", confirmErr);
|
|
784
|
+
openConnect();
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
504
788
|
if (paymentCurrency?.type === "credit") {
|
|
505
789
|
if (result.data.creditSufficient === true) {
|
|
506
790
|
setState({
|
|
@@ -547,13 +831,95 @@ export default function PaymentForm({
|
|
|
547
831
|
} catch (err) {
|
|
548
832
|
console.error(err);
|
|
549
833
|
let shouldToast = true;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
834
|
+
const errorCode = err.response?.data?.code;
|
|
835
|
+
if (errorCode) {
|
|
836
|
+
if (![
|
|
837
|
+
"QUOTE_UPDATED",
|
|
838
|
+
"RATE_UNAVAILABLE",
|
|
839
|
+
"QUOTE_LOCK_EXPIRED",
|
|
840
|
+
"QUOTE_AMOUNT_MISMATCH",
|
|
841
|
+
"QUOTE_EXPIRED_OR_USED",
|
|
842
|
+
"QUOTE_NOT_FOUND",
|
|
843
|
+
"QUOTE_REQUIRED",
|
|
844
|
+
"QUOTE_MAX_PAYABLE_EXCEEDED"
|
|
845
|
+
].includes(errorCode)) {
|
|
846
|
+
dispatch(`error.${errorCode}`);
|
|
847
|
+
}
|
|
848
|
+
if ([
|
|
849
|
+
"QUOTE_LOCK_EXPIRED",
|
|
850
|
+
"QUOTE_AMOUNT_MISMATCH",
|
|
851
|
+
"QUOTE_EXPIRED_OR_USED",
|
|
852
|
+
"QUOTE_NOT_FOUND",
|
|
853
|
+
"QUOTE_REQUIRED",
|
|
854
|
+
"QUOTE_MAX_PAYABLE_EXCEEDED",
|
|
855
|
+
"quote_validation_failed"
|
|
856
|
+
].includes(errorCode)) {
|
|
857
|
+
shouldToast = false;
|
|
858
|
+
try {
|
|
859
|
+
const { data: refreshed } = await api.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, {
|
|
860
|
+
params: { forceRefresh: "1" }
|
|
861
|
+
});
|
|
862
|
+
if (refreshed?.checkoutSession) {
|
|
863
|
+
applyQuoteUpdate(refreshed, { reason: "rateChanged" });
|
|
864
|
+
Toast.info(t("payment.checkout.quote.updated.pleaseRetry") || "Price updated, please resubmit");
|
|
865
|
+
}
|
|
866
|
+
} catch (refreshError) {
|
|
867
|
+
console.error(refreshError);
|
|
868
|
+
Toast.error(formatError(refreshError));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (errorCode === "QUOTE_UPDATED") {
|
|
872
|
+
shouldToast = false;
|
|
873
|
+
const payload = err.response?.data;
|
|
874
|
+
if (payload?.checkoutSession) {
|
|
875
|
+
applyQuoteUpdate(payload);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (errorCode === "RATE_UNAVAILABLE") {
|
|
879
|
+
shouldToast = false;
|
|
880
|
+
const payload = err.response?.data;
|
|
881
|
+
if (payload?.checkoutSession) {
|
|
882
|
+
onQuoteUpdated?.({
|
|
883
|
+
checkoutSession: payload.checkoutSession,
|
|
884
|
+
quotes: payload.quotes,
|
|
885
|
+
rateUnavailable: payload.rateUnavailable,
|
|
886
|
+
rateError: payload.rateError
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (errorCode === "PRICE_UNAVAILABLE") {
|
|
891
|
+
shouldToast = false;
|
|
892
|
+
Toast.error(
|
|
893
|
+
t("payment.checkout.priceChange.unavailable", {
|
|
894
|
+
fallback: "Unable to fetch exchange rate. Please try again later."
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
if (errorCode === "PRICE_UNSTABLE") {
|
|
899
|
+
shouldToast = false;
|
|
900
|
+
Toast.error(
|
|
901
|
+
t("payment.checkout.priceChange.unstable", {
|
|
902
|
+
fallback: "Price is volatile. Please try again later."
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (errorCode === "PRICE_CHANGED") {
|
|
907
|
+
shouldToast = false;
|
|
908
|
+
const errorData = err.response?.data;
|
|
909
|
+
setState({
|
|
910
|
+
priceChangeConfirm: {
|
|
911
|
+
open: true,
|
|
912
|
+
changePercent: errorData?.change_percent || 0,
|
|
913
|
+
formData: data
|
|
914
|
+
// Save form data for retry
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
if (errorCode === "UNIFIED_APP_REQUIRED") {
|
|
553
919
|
shouldToast = false;
|
|
554
920
|
Toast.error(t("payment.checkout.vendor.accountRequired"));
|
|
555
921
|
}
|
|
556
|
-
if (
|
|
922
|
+
if (errorCode === "CUSTOMER_LIMITED") {
|
|
557
923
|
shouldToast = false;
|
|
558
924
|
setState({ customerLimited: true });
|
|
559
925
|
}
|
|
@@ -572,7 +938,7 @@ export default function PaymentForm({
|
|
|
572
938
|
setState({ submitting: false });
|
|
573
939
|
};
|
|
574
940
|
const onAction = () => {
|
|
575
|
-
if (state.submitting || state.paying) {
|
|
941
|
+
if (state.submitting || state.paying || !canPay) {
|
|
576
942
|
return;
|
|
577
943
|
}
|
|
578
944
|
if (errorRef.current && !isEmpty(errors) && isMobile) {
|
|
@@ -610,7 +976,7 @@ export default function PaymentForm({
|
|
|
610
976
|
};
|
|
611
977
|
useEffect(() => {
|
|
612
978
|
const handleKeyDown = (e) => {
|
|
613
|
-
if (e.key === "Enter" && !state.submitting && !state.paying && !state.stripePaying && quantityInventoryStatus &&
|
|
979
|
+
if (e.key === "Enter" && !state.submitting && !state.paying && !state.stripePaying && quantityInventoryStatus && canPay) {
|
|
614
980
|
onAction();
|
|
615
981
|
}
|
|
616
982
|
};
|
|
@@ -618,7 +984,7 @@ export default function PaymentForm({
|
|
|
618
984
|
return () => {
|
|
619
985
|
window.removeEventListener("keydown", handleKeyDown);
|
|
620
986
|
};
|
|
621
|
-
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus,
|
|
987
|
+
}, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, canPay]);
|
|
622
988
|
const balanceLink = getTokenBalanceLink(method, state.fastCheckoutInfo?.payer || "");
|
|
623
989
|
const FastCheckoutConfirmDialog = state.fastCheckoutInfo && /* @__PURE__ */ jsx(
|
|
624
990
|
ConfirmDialog,
|
|
@@ -627,7 +993,7 @@ export default function PaymentForm({
|
|
|
627
993
|
onCancel: handleFastCheckoutCancel,
|
|
628
994
|
title: state.fastCheckoutInfo.sourceType === "credit" ? t("payment.checkout.fastPay.credit.title") : t("payment.checkout.fastPay.title"),
|
|
629
995
|
message: state.fastCheckoutInfo.sourceType === "credit" ? /* @__PURE__ */ jsx(Typography, { children: t("payment.checkout.fastPay.credit.meteringSubscriptionMessage", {
|
|
630
|
-
available: `${
|
|
996
|
+
available: `${formatAmount(state.fastCheckoutInfo?.balance || "0", paymentCurrency?.decimal || 18)} ${paymentCurrency?.symbol}`
|
|
631
997
|
}) }) : /* @__PURE__ */ jsxs(Stack, { children: [
|
|
632
998
|
/* @__PURE__ */ jsx(Typography, { children: t("payment.checkout.fastPay.autoPaymentReason") }),
|
|
633
999
|
/* @__PURE__ */ jsx(Divider, { sx: { mt: 1.5, mb: 1.5 } }),
|
|
@@ -690,7 +1056,7 @@ export default function PaymentForm({
|
|
|
690
1056
|
}
|
|
691
1057
|
),
|
|
692
1058
|
/* @__PURE__ */ jsxs(Typography, { children: [
|
|
693
|
-
|
|
1059
|
+
formatAmount(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18),
|
|
694
1060
|
" ",
|
|
695
1061
|
paymentCurrency?.symbol
|
|
696
1062
|
] })
|
|
@@ -713,6 +1079,33 @@ export default function PaymentForm({
|
|
|
713
1079
|
confirm: t("common.confirm")
|
|
714
1080
|
}
|
|
715
1081
|
);
|
|
1082
|
+
const PriceUpdatedDialog = priceUpdateInfo.open && /* @__PURE__ */ jsx(
|
|
1083
|
+
ConfirmDialog,
|
|
1084
|
+
{
|
|
1085
|
+
onConfirm: handlePriceUpdateConfirm,
|
|
1086
|
+
onCancel: handlePriceUpdateCancel,
|
|
1087
|
+
title: t("payment.checkout.quote.priceUpdatedTitle"),
|
|
1088
|
+
message: /* @__PURE__ */ jsxs(Stack, { spacing: 1, children: [
|
|
1089
|
+
/* @__PURE__ */ jsx(Typography, { children: t(
|
|
1090
|
+
priceUpdateInfo.reason === "rateChanged" ? "payment.checkout.quote.priceUpdatedDescriptionRate" : "payment.checkout.quote.priceUpdatedDescriptionRecalc"
|
|
1091
|
+
) }),
|
|
1092
|
+
priceUpdateInfo.hasQuotes && /* @__PURE__ */ jsxs(Stack, { spacing: 0.25, children: [
|
|
1093
|
+
/* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary", fontSize: "0.7875rem" }, children: t("payment.checkout.quote.priceUpdatedNewTotalLabel") }),
|
|
1094
|
+
/* @__PURE__ */ jsx(Typography, { sx: { fontWeight: 600 }, children: priceUpdateInfo.total }),
|
|
1095
|
+
priceUpdateInfo.usd && /* @__PURE__ */ jsxs(Typography, { sx: { color: "text.secondary" }, children: [
|
|
1096
|
+
"\u2248 ",
|
|
1097
|
+
priceUpdateInfo.usd,
|
|
1098
|
+
" ",
|
|
1099
|
+
priceUpdateInfo.baseCurrency
|
|
1100
|
+
] }),
|
|
1101
|
+
priceUpdateInfo.oldTotal && /* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary", fontSize: "0.75rem" }, children: t("payment.checkout.quote.priceUpdatedOldTotal", { total: priceUpdateInfo.oldTotal }) })
|
|
1102
|
+
] })
|
|
1103
|
+
] }),
|
|
1104
|
+
confirm: t("payment.checkout.quote.priceUpdatedConfirm"),
|
|
1105
|
+
cancel: t("common.cancel"),
|
|
1106
|
+
color: "primary"
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
716
1109
|
const getRedirectUrl = () => {
|
|
717
1110
|
if (searchParams.redirect) {
|
|
718
1111
|
return decodeURIComponent(searchParams.redirect);
|
|
@@ -735,13 +1128,13 @@ export default function PaymentForm({
|
|
|
735
1128
|
size: "large",
|
|
736
1129
|
className: "cko-submit-button",
|
|
737
1130
|
onClick: () => {
|
|
738
|
-
if (state.submitting || state.paying) {
|
|
1131
|
+
if (state.submitting || state.paying || !canPay) {
|
|
739
1132
|
return;
|
|
740
1133
|
}
|
|
741
1134
|
onAction();
|
|
742
1135
|
},
|
|
743
1136
|
fullWidth: true,
|
|
744
|
-
disabled: state.stripePaying || !quantityInventoryStatus || !
|
|
1137
|
+
disabled: state.stripePaying || !quantityInventoryStatus || !canPay,
|
|
745
1138
|
children: [
|
|
746
1139
|
(state.submitting || state.paying) && /* @__PURE__ */ jsx(CircularProgress, { size: 16, sx: { mr: 0.5, color: "primary.contrastText" } }),
|
|
747
1140
|
state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
|
|
@@ -778,7 +1171,18 @@ export default function PaymentForm({
|
|
|
778
1171
|
}
|
|
779
1172
|
),
|
|
780
1173
|
FastCheckoutConfirmDialog,
|
|
781
|
-
CreditInsufficientDialog
|
|
1174
|
+
CreditInsufficientDialog,
|
|
1175
|
+
PriceUpdatedDialog,
|
|
1176
|
+
state.priceChangeConfirm?.open && /* @__PURE__ */ jsx(
|
|
1177
|
+
PriceChangeConfirm,
|
|
1178
|
+
{
|
|
1179
|
+
open: true,
|
|
1180
|
+
changePercent: state.priceChangeConfirm.changePercent,
|
|
1181
|
+
onConfirm: handlePriceChangeConfirm,
|
|
1182
|
+
onCancel: handlePriceChangeCancel,
|
|
1183
|
+
loading: state.submitting
|
|
1184
|
+
}
|
|
1185
|
+
)
|
|
782
1186
|
] });
|
|
783
1187
|
}
|
|
784
1188
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -819,10 +1223,27 @@ export default function PaymentForm({
|
|
|
819
1223
|
{
|
|
820
1224
|
value: field.value,
|
|
821
1225
|
currencies,
|
|
822
|
-
onChange: (id, methodId) => {
|
|
1226
|
+
onChange: async (id, methodId) => {
|
|
1227
|
+
const oldCurrencyId = field.value;
|
|
823
1228
|
field.onChange(id);
|
|
824
1229
|
setValue("payment_method", methodId);
|
|
825
1230
|
saveCurrencyPreference(id, session?.user?.did);
|
|
1231
|
+
if (oldCurrencyId && oldCurrencyId !== id) {
|
|
1232
|
+
try {
|
|
1233
|
+
const { data } = await api.put(
|
|
1234
|
+
`/api/checkout-sessions/${checkoutSession.id}/switch-currency`,
|
|
1235
|
+
{
|
|
1236
|
+
currency_id: id,
|
|
1237
|
+
payment_method_id: methodId
|
|
1238
|
+
}
|
|
1239
|
+
);
|
|
1240
|
+
if (data.currency_changed && onQuoteUpdated) {
|
|
1241
|
+
onQuoteUpdated({ checkoutSession: data, quotes: data.quotes });
|
|
1242
|
+
}
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
console.error("Failed to switch currency:", err);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
826
1247
|
}
|
|
827
1248
|
}
|
|
828
1249
|
)
|
|
@@ -978,7 +1399,7 @@ export default function PaymentForm({
|
|
|
978
1399
|
},
|
|
979
1400
|
fullWidth: true,
|
|
980
1401
|
loading: state.submitting || state.paying,
|
|
981
|
-
disabled: state.stripePaying || !quantityInventoryStatus || !
|
|
1402
|
+
disabled: state.stripePaying || !quantityInventoryStatus || !canPay,
|
|
982
1403
|
children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
|
|
983
1404
|
}
|
|
984
1405
|
) }),
|
|
@@ -1015,6 +1436,17 @@ export default function PaymentForm({
|
|
|
1015
1436
|
}
|
|
1016
1437
|
),
|
|
1017
1438
|
FastCheckoutConfirmDialog,
|
|
1018
|
-
CreditInsufficientDialog
|
|
1439
|
+
CreditInsufficientDialog,
|
|
1440
|
+
PriceUpdatedDialog,
|
|
1441
|
+
state.priceChangeConfirm?.open && /* @__PURE__ */ jsx(
|
|
1442
|
+
PriceChangeConfirm,
|
|
1443
|
+
{
|
|
1444
|
+
open: true,
|
|
1445
|
+
changePercent: state.priceChangeConfirm.changePercent,
|
|
1446
|
+
onConfirm: handlePriceChangeConfirm,
|
|
1447
|
+
onCancel: handlePriceChangeCancel,
|
|
1448
|
+
loading: state.submitting
|
|
1449
|
+
}
|
|
1450
|
+
)
|
|
1019
1451
|
] });
|
|
1020
1452
|
}
|
package/es/payment/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ type Props = CheckoutContext & CheckoutCallbacks & {
|
|
|
6
6
|
error?: any;
|
|
7
7
|
showCheckoutSummary?: boolean;
|
|
8
8
|
};
|
|
9
|
-
export default function Payment({ checkoutSession, paymentMethods, paymentIntent, paymentLink, customer, completed, error, mode, onPaid, onError, onChange, goBack, action, showCheckoutSummary, }: Props): import("react").JSX.Element;
|
|
9
|
+
export default function Payment({ checkoutSession, paymentMethods, paymentIntent, paymentLink, customer, completed, error, mode, onPaid, onError, onChange, goBack, action, showCheckoutSummary, quotes, rateUnavailable, rateError, }: Props): import("react").JSX.Element;
|
|
10
10
|
type RootProps = {
|
|
11
11
|
mode: LiteralUnion<'standalone' | 'inline' | 'popup', string>;
|
|
12
12
|
} & BoxProps;
|