@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.
Files changed (98) hide show
  1. package/es/components/auto-topup/modal.d.ts +2 -0
  2. package/es/components/auto-topup/modal.js +48 -6
  3. package/es/components/auto-topup/product-card.d.ts +16 -1
  4. package/es/components/auto-topup/product-card.js +97 -15
  5. package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
  6. package/es/components/dynamic-pricing-unavailable.js +58 -0
  7. package/es/components/loading-amount.d.ts +17 -0
  8. package/es/components/loading-amount.js +46 -0
  9. package/es/components/price-change-confirm.d.ts +18 -0
  10. package/es/components/price-change-confirm.js +107 -0
  11. package/es/components/quote-details-panel.d.ts +21 -0
  12. package/es/components/quote-details-panel.js +170 -0
  13. package/es/components/quote-lock-banner.d.ts +7 -0
  14. package/es/components/quote-lock-banner.js +79 -0
  15. package/es/components/slippage-config.d.ts +20 -0
  16. package/es/components/slippage-config.js +261 -0
  17. package/es/history/invoice/list.js +125 -15
  18. package/es/hooks/dynamic-pricing.d.ts +102 -0
  19. package/es/hooks/dynamic-pricing.js +393 -0
  20. package/es/index.d.ts +6 -1
  21. package/es/index.js +9 -1
  22. package/es/libs/util.d.ts +42 -5
  23. package/es/libs/util.js +345 -57
  24. package/es/locales/en.js +114 -3
  25. package/es/locales/zh.js +114 -3
  26. package/es/payment/form/index.d.ts +4 -1
  27. package/es/payment/form/index.js +454 -22
  28. package/es/payment/index.d.ts +1 -1
  29. package/es/payment/index.js +279 -16
  30. package/es/payment/product-item.d.ts +26 -1
  31. package/es/payment/product-item.js +330 -51
  32. package/es/payment/summary-section/promotion-section.d.ts +32 -0
  33. package/es/payment/summary-section/promotion-section.js +143 -0
  34. package/es/payment/summary-section/total-section.d.ts +39 -0
  35. package/es/payment/summary-section/total-section.js +83 -0
  36. package/es/payment/summary.d.ts +17 -2
  37. package/es/payment/summary.js +300 -253
  38. package/es/types/index.d.ts +11 -0
  39. package/lib/components/auto-topup/modal.d.ts +2 -0
  40. package/lib/components/auto-topup/modal.js +54 -6
  41. package/lib/components/auto-topup/product-card.d.ts +16 -1
  42. package/lib/components/auto-topup/product-card.js +75 -7
  43. package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
  44. package/lib/components/dynamic-pricing-unavailable.js +81 -0
  45. package/lib/components/loading-amount.d.ts +17 -0
  46. package/lib/components/loading-amount.js +53 -0
  47. package/lib/components/price-change-confirm.d.ts +18 -0
  48. package/lib/components/price-change-confirm.js +157 -0
  49. package/lib/components/quote-details-panel.d.ts +21 -0
  50. package/lib/components/quote-details-panel.js +226 -0
  51. package/lib/components/quote-lock-banner.d.ts +7 -0
  52. package/lib/components/quote-lock-banner.js +93 -0
  53. package/lib/components/slippage-config.d.ts +20 -0
  54. package/lib/components/slippage-config.js +316 -0
  55. package/lib/history/invoice/list.js +167 -27
  56. package/lib/hooks/dynamic-pricing.d.ts +102 -0
  57. package/lib/hooks/dynamic-pricing.js +390 -0
  58. package/lib/index.d.ts +6 -1
  59. package/lib/index.js +32 -0
  60. package/lib/libs/util.d.ts +42 -5
  61. package/lib/libs/util.js +367 -49
  62. package/lib/locales/en.js +114 -3
  63. package/lib/locales/zh.js +114 -3
  64. package/lib/payment/form/index.d.ts +4 -1
  65. package/lib/payment/form/index.js +476 -20
  66. package/lib/payment/index.d.ts +1 -1
  67. package/lib/payment/index.js +308 -14
  68. package/lib/payment/product-item.d.ts +26 -1
  69. package/lib/payment/product-item.js +270 -35
  70. package/lib/payment/summary-section/promotion-section.d.ts +32 -0
  71. package/lib/payment/summary-section/promotion-section.js +133 -0
  72. package/lib/payment/summary-section/total-section.d.ts +39 -0
  73. package/lib/payment/summary-section/total-section.js +117 -0
  74. package/lib/payment/summary.d.ts +17 -2
  75. package/lib/payment/summary.js +205 -127
  76. package/lib/types/index.d.ts +11 -0
  77. package/package.json +3 -3
  78. package/src/components/auto-topup/modal.tsx +59 -6
  79. package/src/components/auto-topup/product-card.tsx +118 -11
  80. package/src/components/dynamic-pricing-unavailable.tsx +69 -0
  81. package/src/components/loading-amount.tsx +66 -0
  82. package/src/components/price-change-confirm.tsx +136 -0
  83. package/src/components/quote-details-panel.tsx +218 -0
  84. package/src/components/quote-lock-banner.tsx +99 -0
  85. package/src/components/slippage-config.tsx +336 -0
  86. package/src/history/invoice/list.tsx +143 -9
  87. package/src/hooks/dynamic-pricing.ts +617 -0
  88. package/src/index.ts +9 -0
  89. package/src/libs/util.ts +473 -58
  90. package/src/locales/en.tsx +117 -0
  91. package/src/locales/zh.tsx +111 -0
  92. package/src/payment/form/index.tsx +561 -19
  93. package/src/payment/index.tsx +349 -10
  94. package/src/payment/product-item.tsx +451 -37
  95. package/src/payment/summary-section/promotion-section.tsx +172 -0
  96. package/src/payment/summary-section/total-section.tsx +141 -0
  97. package/src/payment/summary.tsx +334 -192
  98. package/src/types/index.ts +15 -0
@@ -23,6 +23,7 @@ var _DID = _interopRequireDefault(require("@arcblock/ux/lib/DID"));
23
23
  var _isEmpty = _interopRequireDefault(require("lodash/isEmpty"));
24
24
  var _iconsMaterial = require("@mui/icons-material");
25
25
  var _withTracker = require("@arcblock/ux/lib/withTracker");
26
+ var _trim = _interopRequireDefault(require("lodash/trim"));
26
27
  var _input = _interopRequireDefault(require("../../components/input"));
27
28
  var _label = _interopRequireDefault(require("../../components/label"));
28
29
  var _payment = require("../../contexts/payment");
@@ -39,8 +40,12 @@ var _loadingButton = _interopRequireDefault(require("../../components/loading-bu
39
40
  var _overDueInvoicePayment = _interopRequireDefault(require("../../components/over-due-invoice-payment"));
40
41
  var _currency2 = require("../../libs/currency");
41
42
  var _confirm = _interopRequireDefault(require("../../components/confirm"));
43
+ var _priceChangeConfirm = _interopRequireDefault(require("../../components/price-change-confirm"));
42
44
  var _validator = require("../../libs/validator");
43
45
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
46
+ const generateIdempotencyKey = (sessionId, currencyId) => {
47
+ return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
48
+ };
44
49
  const waitForCheckoutComplete = async sessionId => {
45
50
  let result;
46
51
  await (0, _pWaitFor.default)(async () => {
@@ -121,10 +126,13 @@ function PaymentForm({
121
126
  customer,
122
127
  onPaid,
123
128
  onError,
129
+ onQuoteUpdated = void 0,
130
+ onPaymentIntentUpdate = void 0,
124
131
  // mode,
125
132
  action,
126
133
  onlyShowBtn = false,
127
- isDonation = false
134
+ isDonation = false,
135
+ rateUnavailable = false
128
136
  }) {
129
137
  const {
130
138
  t,
@@ -174,7 +182,8 @@ function PaymentForm({
174
182
  stripePaying: false,
175
183
  fastCheckoutInfo: null,
176
184
  creditInsufficientInfo: null,
177
- showEditForm: false
185
+ showEditForm: false,
186
+ priceChangeConfirm: null
178
187
  });
179
188
  const currencies = (0, _util2.flattenPaymentMethods)(paymentMethods);
180
189
  const searchParams = (0, _util2.getQueryParams)(window.location.href);
@@ -295,7 +304,192 @@ function PaymentForm({
295
304
  const method = paymentMethods.find(x => x.id === paymentMethod);
296
305
  const paymentCurrency = currencies.find(x => x.id === paymentCurrencyId);
297
306
  const showStake = method.type === "arcblock" && !checkoutSession.subscription_data?.no_stake;
307
+ const hasDynamicPricing = (0, _react.useMemo)(() => (checkoutSession.line_items || []).some(item => {
308
+ const price = item.upsell_price || item.price;
309
+ return price && price?.pricing_type === "dynamic";
310
+ }), [checkoutSession.line_items]);
311
+ const rateUnavailableForDynamic = hasDynamicPricing && rateUnavailable;
312
+ const canPay = payable && !rateUnavailableForDynamic;
298
313
  const isDonationMode = checkoutSession?.submit_type === "donate" && isDonation;
314
+ const [priceUpdateInfo, setPriceUpdateInfo] = (0, _ahooks.useSetState)({
315
+ open: false,
316
+ total: "",
317
+ usd: null,
318
+ hasQuotes: false,
319
+ baseCurrency: "USD",
320
+ oldTotal: "",
321
+ reason: "recalculated"
322
+ });
323
+ const normalizeExchangeRate = (0, _ahooks.useMemoizedFn)(rate => {
324
+ if (!rate) {
325
+ return null;
326
+ }
327
+ const value = Number(rate);
328
+ if (!Number.isFinite(value)) {
329
+ return null;
330
+ }
331
+ return value.toFixed(8);
332
+ });
333
+ const getExchangeRateFromSession = (0, _ahooks.useMemoizedFn)(sessionData => {
334
+ if (!sessionData?.line_items?.length) {
335
+ return null;
336
+ }
337
+ for (const item of sessionData.line_items) {
338
+ const rate = item?.exchange_rate;
339
+ if (rate) {
340
+ return rate;
341
+ }
342
+ }
343
+ return null;
344
+ });
345
+ const quoteAutoRetryRef = (0, _react.useRef)(false);
346
+ const lastRetryKeyRef = (0, _react.useRef)("");
347
+ const buildRetryKey = (0, _ahooks.useMemoizedFn)(sessionData => {
348
+ if (!sessionData?.line_items?.length) {
349
+ return "";
350
+ }
351
+ return sessionData.line_items.map(item => {
352
+ const priceId = item.price_id || item.price?.id || "";
353
+ const quoteId = item?.quote_id || "";
354
+ const quotedAmount = item?.quoted_amount || "";
355
+ const exchangeRate = item?.exchange_rate || "";
356
+ return `${priceId}:${quoteId}:${quotedAmount}:${exchangeRate}`;
357
+ }).join("|");
358
+ });
359
+ const buildPriceUpdateSummary = (0, _ahooks.useMemoizedFn)(sessionData => {
360
+ if (!paymentCurrency) {
361
+ return {
362
+ total: "",
363
+ usd: null,
364
+ hasQuotes: false,
365
+ baseCurrency: "USD",
366
+ totalUnit: null
367
+ };
368
+ }
369
+ const lineItems = sessionData.line_items || [];
370
+ let baseCurrency = "USD";
371
+ for (const item of lineItems) {
372
+ const price = item.upsell_price || item.price;
373
+ const base = price?.base_currency;
374
+ if (base) {
375
+ baseCurrency = base;
376
+ break;
377
+ }
378
+ }
379
+ const hasQuotes = lineItems.some(item => item?.quoted_amount && item?.exchange_rate);
380
+ if (!hasQuotes) {
381
+ return {
382
+ total: "",
383
+ usd: null,
384
+ hasQuotes: false,
385
+ baseCurrency,
386
+ totalUnit: null
387
+ };
388
+ }
389
+ let trialInDays = Number(sessionData?.subscription_data?.trial_period_days || 0);
390
+ const trialCurrencyIds = (sessionData?.subscription_data?.trial_currency || "").split(",").map(_trim.default).filter(Boolean);
391
+ if (trialCurrencyIds.length > 0 && paymentCurrencyId && trialCurrencyIds.includes(paymentCurrencyId) === false) {
392
+ trialInDays = 0;
393
+ }
394
+ const {
395
+ total
396
+ } = (0, _util2.getCheckoutAmount)(lineItems, paymentCurrency, trialInDays > 0);
397
+ const discountAmount = new _util.BN(sessionData.total_details?.amount_discount || "0");
398
+ const totalUnit = new _util.BN(total).sub(discountAmount);
399
+ const normalizedTotalUnit = totalUnit.isNeg() ? new _util.BN(0) : totalUnit;
400
+ const totalDisplay = `${(0, _util2.formatNumber)((0, _util.fromUnitToToken)(normalizedTotalUnit.toString(), paymentCurrency.decimal), 6)} ${paymentCurrency.symbol}`;
401
+ const itemUsdReferences = lineItems.map(item => {
402
+ const price = item.upsell_price || item.price;
403
+ const baseAmount = price?.base_amount;
404
+ const hasBaseAmount = baseAmount !== void 0 && baseAmount !== null;
405
+ if (hasBaseAmount) {
406
+ return (0, _util2.getUsdAmountFromBaseAmount)(baseAmount, item.quantity || 0);
407
+ }
408
+ const exchangeRate = item?.exchange_rate;
409
+ const quotedAmount = item?.quoted_amount;
410
+ if (!exchangeRate || !quotedAmount) {
411
+ return null;
412
+ }
413
+ return (0, _util2.getUsdAmountFromTokenUnits)(new _util.BN(quotedAmount), paymentCurrency.decimal, exchangeRate);
414
+ });
415
+ const usdValues = itemUsdReferences.filter(value => Boolean(value));
416
+ if (!usdValues.length) {
417
+ return {
418
+ total: totalDisplay,
419
+ usd: null,
420
+ hasQuotes,
421
+ baseCurrency,
422
+ totalUnit: normalizedTotalUnit
423
+ };
424
+ }
425
+ const sumUnit = usdValues.reduce((acc, value) => acc.add(new _util.BN((0, _util.fromTokenToUnit)(value, 8))), new _util.BN(0));
426
+ const totalUsdReference = (0, _util.fromUnitToToken)(sumUnit.toString(), 8);
427
+ return {
428
+ total: totalDisplay,
429
+ usd: (0, _util2.formatUsdAmount)(totalUsdReference, locale),
430
+ hasQuotes,
431
+ baseCurrency,
432
+ totalUnit: normalizedTotalUnit
433
+ };
434
+ });
435
+ const compareTotals = (0, _ahooks.useMemoizedFn)((prevSession, nextSession) => {
436
+ const prev = buildPriceUpdateSummary(prevSession);
437
+ const next = buildPriceUpdateSummary(nextSession);
438
+ if (!prev.totalUnit || !next.totalUnit) {
439
+ return {
440
+ changed: false,
441
+ prev,
442
+ next
443
+ };
444
+ }
445
+ const diff = next.totalUnit.sub(prev.totalUnit).abs();
446
+ const epsilon = new _util.BN(1);
447
+ return {
448
+ changed: diff.gt(epsilon),
449
+ prev,
450
+ next
451
+ };
452
+ });
453
+ const applyQuoteUpdate = (0, _ahooks.useMemoizedFn)((payload, options = {}) => {
454
+ if (!payload?.checkoutSession) {
455
+ return;
456
+ }
457
+ onQuoteUpdated?.({
458
+ checkoutSession: payload.checkoutSession,
459
+ quotes: payload.quotes,
460
+ rateUnavailable: payload.rateUnavailable,
461
+ rateError: payload.rateError
462
+ });
463
+ const {
464
+ changed,
465
+ prev,
466
+ next
467
+ } = compareTotals(checkoutSession, payload.checkoutSession);
468
+ const previousRate = normalizeExchangeRate(getExchangeRateFromSession(checkoutSession));
469
+ const nextRate = normalizeExchangeRate(getExchangeRateFromSession(payload.checkoutSession));
470
+ const rateChanged = !!(previousRate && nextRate && previousRate !== nextRate);
471
+ const shouldShowModal = (options.forceConfirm || changed) && next.hasQuotes;
472
+ if (shouldShowModal) {
473
+ setPriceUpdateInfo({
474
+ open: true,
475
+ total: next.total,
476
+ usd: next.usd,
477
+ hasQuotes: next.hasQuotes,
478
+ baseCurrency: next.baseCurrency,
479
+ oldTotal: prev.total,
480
+ reason: options.reason || (rateChanged ? "rateChanged" : "recalculated")
481
+ });
482
+ return;
483
+ }
484
+ setPriceUpdateInfo({
485
+ open: false
486
+ });
487
+ const retryKey = buildRetryKey(payload.checkoutSession);
488
+ if (retryKey && retryKey !== lastRetryKeyRef.current) {
489
+ lastRetryKeyRef.current = retryKey;
490
+ quoteAutoRetryRef.current = true;
491
+ }
492
+ });
299
493
  const validateUserInfo = values => {
300
494
  if (!values) {
301
495
  return false;
@@ -403,6 +597,16 @@ function PaymentForm({
403
597
  name: "billing_address"
404
598
  });
405
599
  const showForm = session?.user ? state.showEditForm : false;
600
+ (0, _react.useEffect)(() => {
601
+ if (!quoteAutoRetryRef.current) {
602
+ return;
603
+ }
604
+ if (state.submitting || state.paying) {
605
+ return;
606
+ }
607
+ quoteAutoRetryRef.current = false;
608
+ onAction();
609
+ }, [state.submitting, state.paying]);
406
610
  const handleConnected = async () => {
407
611
  if (processingRef.current) {
408
612
  return;
@@ -473,7 +677,10 @@ function PaymentForm({
473
677
  }
474
678
  });
475
679
  try {
476
- const result = await _api.default.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`);
680
+ const result = await _api.default.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`, {});
681
+ if (result.data.paymentIntent) {
682
+ onPaymentIntentUpdate?.(result.data.paymentIntent);
683
+ }
477
684
  if (result.data.fastPaid) {
478
685
  setState({
479
686
  fastCheckoutInfo: null,
@@ -490,6 +697,57 @@ function PaymentForm({
490
697
  }
491
698
  } catch (err) {
492
699
  console.error(err);
700
+ const errorCode = err?.response?.data?.code;
701
+ if (["QUOTE_LOCK_EXPIRED", "QUOTE_AMOUNT_MISMATCH", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED"].includes(errorCode)) {
702
+ try {
703
+ const {
704
+ data: refreshed
705
+ } = await _api.default.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, {
706
+ params: {
707
+ forceRefresh: "1"
708
+ }
709
+ });
710
+ if (refreshed?.checkoutSession) {
711
+ applyQuoteUpdate(refreshed, {
712
+ reason: "rateChanged"
713
+ });
714
+ _Toast.default.info(t("payment.checkout.quote.updated.pleaseRetry") || "Price updated, please resubmit");
715
+ }
716
+ } catch (refreshError) {
717
+ console.error(refreshError);
718
+ _Toast.default.error((0, _util2.formatError)(refreshError));
719
+ } finally {
720
+ setState({
721
+ fastCheckoutInfo: null
722
+ });
723
+ }
724
+ return;
725
+ }
726
+ if (errorCode === "QUOTE_UPDATED") {
727
+ const payload = err?.response?.data;
728
+ if (payload?.checkoutSession) {
729
+ applyQuoteUpdate(payload);
730
+ }
731
+ setState({
732
+ fastCheckoutInfo: null
733
+ });
734
+ return;
735
+ }
736
+ if (errorCode === "RATE_UNAVAILABLE") {
737
+ const payload = err?.response?.data;
738
+ if (payload?.checkoutSession) {
739
+ onQuoteUpdated?.({
740
+ checkoutSession: payload.checkoutSession,
741
+ quotes: payload.quotes,
742
+ rateUnavailable: payload.rateUnavailable,
743
+ rateError: payload.rateError
744
+ });
745
+ }
746
+ setState({
747
+ fastCheckoutInfo: null
748
+ });
749
+ return;
750
+ }
493
751
  _Toast.default.error((0, _util2.formatError)(err));
494
752
  setState({
495
753
  fastCheckoutInfo: null
@@ -506,6 +764,31 @@ function PaymentForm({
506
764
  creditInsufficientInfo: null
507
765
  });
508
766
  };
767
+ const handlePriceUpdateConfirm = () => {
768
+ setPriceUpdateInfo({
769
+ open: false
770
+ });
771
+ quoteAutoRetryRef.current = true;
772
+ };
773
+ const handlePriceUpdateCancel = () => {
774
+ setPriceUpdateInfo({
775
+ open: false
776
+ });
777
+ };
778
+ const handlePriceChangeConfirm = () => {
779
+ const formData = state.priceChangeConfirm?.formData;
780
+ setState({
781
+ priceChangeConfirm: null
782
+ });
783
+ if (formData) {
784
+ onFormSubmit(formData);
785
+ }
786
+ };
787
+ const handlePriceChangeCancel = () => {
788
+ setState({
789
+ priceChangeConfirm: null
790
+ });
791
+ };
509
792
  const openConnect = () => {
510
793
  try {
511
794
  if (!["arcblock", "ethereum", "base"].includes(method.type)) {
@@ -558,6 +841,9 @@ function PaymentForm({
558
841
  }
559
842
  };
560
843
  const onFormSubmit = async data => {
844
+ if (state.submitting) {
845
+ return;
846
+ }
561
847
  const userInfo = session.user;
562
848
  if (!userInfo.sourceAppPid) {
563
849
  const hasVendorConfig = checkoutSession.line_items?.some(item => !!item?.price?.product?.vendor_config?.length);
@@ -571,10 +857,18 @@ function PaymentForm({
571
857
  });
572
858
  try {
573
859
  let result;
860
+ const previewRate = checkoutSession.line_items?.find(item => item?.exchange_rate)?.exchange_rate || void 0;
861
+ const payload = {
862
+ ...data,
863
+ // Final Freeze: Include these for new quote creation at submit
864
+ idempotency_key: generateIdempotencyKey(checkoutSession.id, paymentCurrency?.id || ""),
865
+ preview_rate: previewRate || void 0,
866
+ price_confirmed: state.priceChangeConfirm?.formData ? true : void 0
867
+ };
574
868
  if (isDonationMode) {
575
- result = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`, data);
869
+ result = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`, payload);
576
870
  } else {
577
- result = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, data);
871
+ result = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, payload);
578
872
  }
579
873
  setState({
580
874
  paymentIntent: result.data.paymentIntent,
@@ -583,7 +877,30 @@ function PaymentForm({
583
877
  submitting: false,
584
878
  customerLimited: false
585
879
  });
880
+ if (result.data.paymentIntent) {
881
+ onPaymentIntentUpdate?.(result.data.paymentIntent);
882
+ }
586
883
  if (["arcblock", "ethereum", "base"].includes(method.type)) {
884
+ if (result.data.noPaymentRequired) {
885
+ try {
886
+ const confirmResult = await _api.default.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`, {});
887
+ if (confirmResult.data.paymentIntent) {
888
+ onPaymentIntentUpdate?.(confirmResult.data.paymentIntent);
889
+ }
890
+ if (confirmResult.data.fastPaid || confirmResult.data.checkoutSession?.status === "complete") {
891
+ setState({
892
+ paying: true
893
+ });
894
+ await handleConnected();
895
+ } else {
896
+ openConnect();
897
+ }
898
+ } catch (confirmErr) {
899
+ console.error("noPaymentRequired confirm failed", confirmErr);
900
+ openConnect();
901
+ }
902
+ return;
903
+ }
587
904
  if (paymentCurrency?.type === "credit") {
588
905
  if (result.data.creditSufficient === true) {
589
906
  setState({
@@ -634,13 +951,80 @@ function PaymentForm({
634
951
  } catch (err) {
635
952
  console.error(err);
636
953
  let shouldToast = true;
637
- if (err.response?.data?.code) {
638
- (0, _useBus.dispatch)(`error.${err.response?.data?.code}`);
639
- if (err.response?.data?.code === "UNIFIED_APP_REQUIRED") {
954
+ const errorCode = err.response?.data?.code;
955
+ if (errorCode) {
956
+ if (!["QUOTE_UPDATED", "RATE_UNAVAILABLE", "QUOTE_LOCK_EXPIRED", "QUOTE_AMOUNT_MISMATCH", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED", "QUOTE_MAX_PAYABLE_EXCEEDED"].includes(errorCode)) {
957
+ (0, _useBus.dispatch)(`error.${errorCode}`);
958
+ }
959
+ if (["QUOTE_LOCK_EXPIRED", "QUOTE_AMOUNT_MISMATCH", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED", "QUOTE_MAX_PAYABLE_EXCEEDED", "quote_validation_failed"].includes(errorCode)) {
960
+ shouldToast = false;
961
+ try {
962
+ const {
963
+ data: refreshed
964
+ } = await _api.default.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, {
965
+ params: {
966
+ forceRefresh: "1"
967
+ }
968
+ });
969
+ if (refreshed?.checkoutSession) {
970
+ applyQuoteUpdate(refreshed, {
971
+ reason: "rateChanged"
972
+ });
973
+ _Toast.default.info(t("payment.checkout.quote.updated.pleaseRetry") || "Price updated, please resubmit");
974
+ }
975
+ } catch (refreshError) {
976
+ console.error(refreshError);
977
+ _Toast.default.error((0, _util2.formatError)(refreshError));
978
+ }
979
+ }
980
+ if (errorCode === "QUOTE_UPDATED") {
981
+ shouldToast = false;
982
+ const payload = err.response?.data;
983
+ if (payload?.checkoutSession) {
984
+ applyQuoteUpdate(payload);
985
+ }
986
+ }
987
+ if (errorCode === "RATE_UNAVAILABLE") {
988
+ shouldToast = false;
989
+ const payload = err.response?.data;
990
+ if (payload?.checkoutSession) {
991
+ onQuoteUpdated?.({
992
+ checkoutSession: payload.checkoutSession,
993
+ quotes: payload.quotes,
994
+ rateUnavailable: payload.rateUnavailable,
995
+ rateError: payload.rateError
996
+ });
997
+ }
998
+ }
999
+ if (errorCode === "PRICE_UNAVAILABLE") {
1000
+ shouldToast = false;
1001
+ _Toast.default.error(t("payment.checkout.priceChange.unavailable", {
1002
+ fallback: "Unable to fetch exchange rate. Please try again later."
1003
+ }));
1004
+ }
1005
+ if (errorCode === "PRICE_UNSTABLE") {
1006
+ shouldToast = false;
1007
+ _Toast.default.error(t("payment.checkout.priceChange.unstable", {
1008
+ fallback: "Price is volatile. Please try again later."
1009
+ }));
1010
+ }
1011
+ if (errorCode === "PRICE_CHANGED") {
1012
+ shouldToast = false;
1013
+ const errorData = err.response?.data;
1014
+ setState({
1015
+ priceChangeConfirm: {
1016
+ open: true,
1017
+ changePercent: errorData?.change_percent || 0,
1018
+ formData: data
1019
+ // Save form data for retry
1020
+ }
1021
+ });
1022
+ }
1023
+ if (errorCode === "UNIFIED_APP_REQUIRED") {
640
1024
  shouldToast = false;
641
1025
  _Toast.default.error(t("payment.checkout.vendor.accountRequired"));
642
1026
  }
643
- if (err.response.data.code === "CUSTOMER_LIMITED") {
1027
+ if (errorCode === "CUSTOMER_LIMITED") {
644
1028
  shouldToast = false;
645
1029
  setState({
646
1030
  customerLimited: true
@@ -665,7 +1049,7 @@ function PaymentForm({
665
1049
  });
666
1050
  };
667
1051
  const onAction = () => {
668
- if (state.submitting || state.paying) {
1052
+ if (state.submitting || state.paying || !canPay) {
669
1053
  return;
670
1054
  }
671
1055
  if (errorRef.current && !(0, _isEmpty.default)(errors) && isMobile) {
@@ -714,7 +1098,7 @@ function PaymentForm({
714
1098
  };
715
1099
  (0, _react.useEffect)(() => {
716
1100
  const handleKeyDown = e => {
717
- if (e.key === "Enter" && !state.submitting && !state.paying && !state.stripePaying && quantityInventoryStatus && payable) {
1101
+ if (e.key === "Enter" && !state.submitting && !state.paying && !state.stripePaying && quantityInventoryStatus && canPay) {
718
1102
  onAction();
719
1103
  }
720
1104
  };
@@ -722,7 +1106,7 @@ function PaymentForm({
722
1106
  return () => {
723
1107
  window.removeEventListener("keydown", handleKeyDown);
724
1108
  };
725
- }, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, payable]);
1109
+ }, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, canPay]);
726
1110
  const balanceLink = (0, _util2.getTokenBalanceLink)(method, state.fastCheckoutInfo?.payer || "");
727
1111
  const FastCheckoutConfirmDialog = state.fastCheckoutInfo && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, {
728
1112
  onConfirm: handleFastCheckoutConfirm,
@@ -730,7 +1114,7 @@ function PaymentForm({
730
1114
  title: state.fastCheckoutInfo.sourceType === "credit" ? t("payment.checkout.fastPay.credit.title") : t("payment.checkout.fastPay.title"),
731
1115
  message: state.fastCheckoutInfo.sourceType === "credit" ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
732
1116
  children: t("payment.checkout.fastPay.credit.meteringSubscriptionMessage", {
733
- available: `${(0, _util.fromUnitToToken)(state.fastCheckoutInfo?.balance || "0", paymentCurrency?.decimal || 18).toString()} ${paymentCurrency?.symbol}`
1117
+ available: `${(0, _util2.formatAmount)(state.fastCheckoutInfo?.balance || "0", paymentCurrency?.decimal || 18)} ${paymentCurrency?.symbol}`
734
1118
  })
735
1119
  }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
736
1120
  children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
@@ -794,7 +1178,7 @@ function PaymentForm({
794
1178
  },
795
1179
  children: t("payment.checkout.fastPay.amount")
796
1180
  }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
797
- children: [(0, _util.fromUnitToToken)(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18).toString(), " ", paymentCurrency?.symbol]
1181
+ children: [(0, _util2.formatAmount)(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18), " ", paymentCurrency?.symbol]
798
1182
  })]
799
1183
  })]
800
1184
  })]
@@ -811,6 +1195,47 @@ function PaymentForm({
811
1195
  }),
812
1196
  confirm: t("common.confirm")
813
1197
  });
1198
+ const PriceUpdatedDialog = priceUpdateInfo.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, {
1199
+ onConfirm: handlePriceUpdateConfirm,
1200
+ onCancel: handlePriceUpdateCancel,
1201
+ title: t("payment.checkout.quote.priceUpdatedTitle"),
1202
+ message: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
1203
+ spacing: 1,
1204
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
1205
+ children: t(priceUpdateInfo.reason === "rateChanged" ? "payment.checkout.quote.priceUpdatedDescriptionRate" : "payment.checkout.quote.priceUpdatedDescriptionRecalc")
1206
+ }), priceUpdateInfo.hasQuotes && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
1207
+ spacing: 0.25,
1208
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
1209
+ sx: {
1210
+ color: "text.secondary",
1211
+ fontSize: "0.7875rem"
1212
+ },
1213
+ children: t("payment.checkout.quote.priceUpdatedNewTotalLabel")
1214
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
1215
+ sx: {
1216
+ fontWeight: 600
1217
+ },
1218
+ children: priceUpdateInfo.total
1219
+ }), priceUpdateInfo.usd && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
1220
+ sx: {
1221
+ color: "text.secondary"
1222
+ },
1223
+ children: ["\u2248 ", priceUpdateInfo.usd, " ", priceUpdateInfo.baseCurrency]
1224
+ }), priceUpdateInfo.oldTotal && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
1225
+ sx: {
1226
+ color: "text.secondary",
1227
+ fontSize: "0.75rem"
1228
+ },
1229
+ children: t("payment.checkout.quote.priceUpdatedOldTotal", {
1230
+ total: priceUpdateInfo.oldTotal
1231
+ })
1232
+ })]
1233
+ })]
1234
+ }),
1235
+ confirm: t("payment.checkout.quote.priceUpdatedConfirm"),
1236
+ cancel: t("common.cancel"),
1237
+ color: "primary"
1238
+ });
814
1239
  const getRedirectUrl = () => {
815
1240
  if (searchParams.redirect) {
816
1241
  return decodeURIComponent(searchParams.redirect);
@@ -833,13 +1258,13 @@ function PaymentForm({
833
1258
  size: "large",
834
1259
  className: "cko-submit-button",
835
1260
  onClick: () => {
836
- if (state.submitting || state.paying) {
1261
+ if (state.submitting || state.paying || !canPay) {
837
1262
  return;
838
1263
  }
839
1264
  onAction();
840
1265
  },
841
1266
  fullWidth: true,
842
- disabled: state.stripePaying || !quantityInventoryStatus || !payable,
1267
+ disabled: state.stripePaying || !quantityInventoryStatus || !canPay,
843
1268
  children: [(state.submitting || state.paying) && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CircularProgress, {
844
1269
  size: 16,
845
1270
  sx: {
@@ -873,7 +1298,13 @@ function PaymentForm({
873
1298
  }),
874
1299
  title: t("payment.customer.pastDue.alert.title")
875
1300
  }
876
- }), FastCheckoutConfirmDialog, CreditInsufficientDialog]
1301
+ }), FastCheckoutConfirmDialog, CreditInsufficientDialog, PriceUpdatedDialog, state.priceChangeConfirm?.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_priceChangeConfirm.default, {
1302
+ open: true,
1303
+ changePercent: state.priceChangeConfirm.changePercent,
1304
+ onConfirm: handlePriceChangeConfirm,
1305
+ onCancel: handlePriceChangeCancel,
1306
+ loading: state.submitting
1307
+ })]
877
1308
  });
878
1309
  }
879
1310
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
@@ -915,10 +1346,29 @@ function PaymentForm({
915
1346
  }) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_currency.default, {
916
1347
  value: field.value,
917
1348
  currencies,
918
- onChange: (id, methodId) => {
1349
+ onChange: async (id, methodId) => {
1350
+ const oldCurrencyId = field.value;
919
1351
  field.onChange(id);
920
1352
  setValue("payment_method", methodId);
921
1353
  (0, _currency2.saveCurrencyPreference)(id, session?.user?.did);
1354
+ if (oldCurrencyId && oldCurrencyId !== id) {
1355
+ try {
1356
+ const {
1357
+ data
1358
+ } = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/switch-currency`, {
1359
+ currency_id: id,
1360
+ payment_method_id: methodId
1361
+ });
1362
+ if (data.currency_changed && onQuoteUpdated) {
1363
+ onQuoteUpdated({
1364
+ checkoutSession: data,
1365
+ quotes: data.quotes
1366
+ });
1367
+ }
1368
+ } catch (err) {
1369
+ console.error("Failed to switch currency:", err);
1370
+ }
1371
+ }
922
1372
  }
923
1373
  })
924
1374
  })
@@ -1096,7 +1546,7 @@ function PaymentForm({
1096
1546
  },
1097
1547
  fullWidth: true,
1098
1548
  loading: state.submitting || state.paying,
1099
- disabled: state.stripePaying || !quantityInventoryStatus || !payable,
1549
+ disabled: state.stripePaying || !quantityInventoryStatus || !canPay,
1100
1550
  children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
1101
1551
  })
1102
1552
  }), ["subscription", "setup"].includes(checkoutSession.mode) && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
@@ -1158,6 +1608,12 @@ function PaymentForm({
1158
1608
  }),
1159
1609
  title: t("payment.customer.pastDue.alert.title")
1160
1610
  }
1161
- }), FastCheckoutConfirmDialog, CreditInsufficientDialog]
1611
+ }), FastCheckoutConfirmDialog, CreditInsufficientDialog, PriceUpdatedDialog, state.priceChangeConfirm?.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_priceChangeConfirm.default, {
1612
+ open: true,
1613
+ changePercent: state.priceChangeConfirm.changePercent,
1614
+ onConfirm: handlePriceChangeConfirm,
1615
+ onCancel: handlePriceChangeCancel,
1616
+ loading: state.submitting
1617
+ })]
1162
1618
  });
1163
1619
  }
@@ -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;