@blocklet/payment-react 1.24.3 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) 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/credit/transactions-list.js +11 -1
  18. package/es/history/invoice/list.js +125 -15
  19. package/es/hooks/dynamic-pricing.d.ts +102 -0
  20. package/es/hooks/dynamic-pricing.js +393 -0
  21. package/es/index.d.ts +6 -1
  22. package/es/index.js +9 -1
  23. package/es/libs/util.d.ts +42 -5
  24. package/es/libs/util.js +345 -57
  25. package/es/locales/en.js +114 -3
  26. package/es/locales/zh.js +114 -3
  27. package/es/payment/form/index.d.ts +4 -1
  28. package/es/payment/form/index.js +454 -22
  29. package/es/payment/index.d.ts +1 -1
  30. package/es/payment/index.js +279 -16
  31. package/es/payment/product-item.d.ts +26 -1
  32. package/es/payment/product-item.js +330 -51
  33. package/es/payment/summary-section/promotion-section.d.ts +32 -0
  34. package/es/payment/summary-section/promotion-section.js +143 -0
  35. package/es/payment/summary-section/total-section.d.ts +39 -0
  36. package/es/payment/summary-section/total-section.js +83 -0
  37. package/es/payment/summary.d.ts +17 -2
  38. package/es/payment/summary.js +300 -253
  39. package/es/types/index.d.ts +11 -0
  40. package/lib/components/auto-topup/modal.d.ts +2 -0
  41. package/lib/components/auto-topup/modal.js +54 -6
  42. package/lib/components/auto-topup/product-card.d.ts +16 -1
  43. package/lib/components/auto-topup/product-card.js +75 -7
  44. package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
  45. package/lib/components/dynamic-pricing-unavailable.js +81 -0
  46. package/lib/components/loading-amount.d.ts +17 -0
  47. package/lib/components/loading-amount.js +53 -0
  48. package/lib/components/price-change-confirm.d.ts +18 -0
  49. package/lib/components/price-change-confirm.js +157 -0
  50. package/lib/components/quote-details-panel.d.ts +21 -0
  51. package/lib/components/quote-details-panel.js +226 -0
  52. package/lib/components/quote-lock-banner.d.ts +7 -0
  53. package/lib/components/quote-lock-banner.js +93 -0
  54. package/lib/components/slippage-config.d.ts +20 -0
  55. package/lib/components/slippage-config.js +316 -0
  56. package/lib/history/credit/transactions-list.js +11 -1
  57. package/lib/history/invoice/list.js +167 -27
  58. package/lib/hooks/dynamic-pricing.d.ts +102 -0
  59. package/lib/hooks/dynamic-pricing.js +390 -0
  60. package/lib/index.d.ts +6 -1
  61. package/lib/index.js +32 -0
  62. package/lib/libs/util.d.ts +42 -5
  63. package/lib/libs/util.js +367 -49
  64. package/lib/locales/en.js +114 -3
  65. package/lib/locales/zh.js +114 -3
  66. package/lib/payment/form/index.d.ts +4 -1
  67. package/lib/payment/form/index.js +476 -20
  68. package/lib/payment/index.d.ts +1 -1
  69. package/lib/payment/index.js +308 -14
  70. package/lib/payment/product-item.d.ts +26 -1
  71. package/lib/payment/product-item.js +270 -35
  72. package/lib/payment/summary-section/promotion-section.d.ts +32 -0
  73. package/lib/payment/summary-section/promotion-section.js +133 -0
  74. package/lib/payment/summary-section/total-section.d.ts +39 -0
  75. package/lib/payment/summary-section/total-section.js +117 -0
  76. package/lib/payment/summary.d.ts +17 -2
  77. package/lib/payment/summary.js +205 -127
  78. package/lib/types/index.d.ts +11 -0
  79. package/package.json +3 -3
  80. package/src/components/auto-topup/modal.tsx +59 -6
  81. package/src/components/auto-topup/product-card.tsx +118 -11
  82. package/src/components/dynamic-pricing-unavailable.tsx +69 -0
  83. package/src/components/loading-amount.tsx +66 -0
  84. package/src/components/price-change-confirm.tsx +136 -0
  85. package/src/components/quote-details-panel.tsx +218 -0
  86. package/src/components/quote-lock-banner.tsx +99 -0
  87. package/src/components/slippage-config.tsx +336 -0
  88. package/src/history/credit/transactions-list.tsx +14 -1
  89. package/src/history/invoice/list.tsx +143 -9
  90. package/src/hooks/dynamic-pricing.ts +617 -0
  91. package/src/index.ts +9 -0
  92. package/src/libs/util.ts +473 -58
  93. package/src/locales/en.tsx +117 -0
  94. package/src/locales/zh.tsx +111 -0
  95. package/src/payment/form/index.tsx +561 -19
  96. package/src/payment/index.tsx +349 -10
  97. package/src/payment/product-item.tsx +451 -37
  98. package/src/payment/summary-section/promotion-section.tsx +172 -0
  99. package/src/payment/summary-section/total-section.tsx +141 -0
  100. package/src/payment/summary.tsx +334 -192
  101. package/src/types/index.ts +15 -0
@@ -45,7 +45,10 @@ function PaymentInner({
45
45
  onError,
46
46
  onChange,
47
47
  action,
48
- showCheckoutSummary = true
48
+ showCheckoutSummary = true,
49
+ quotes,
50
+ rateUnavailable,
51
+ rateError
49
52
  }) {
50
53
  const {
51
54
  t
@@ -58,8 +61,19 @@ function PaymentInner({
58
61
  isMobile
59
62
  } = (0, _mobile.useMobile)();
60
63
  const [state, setState] = (0, _ahooks.useSetState)({
61
- checkoutSession
64
+ checkoutSession,
65
+ quotes,
66
+ rateUnavailable,
67
+ rateError,
68
+ paymentIntent,
69
+ liveRateInfo: void 0,
70
+ liveQuoteSnapshot: void 0,
71
+ liveRateUnavailable: false,
72
+ liveRateError: void 0,
73
+ isRateLoading: false
62
74
  });
75
+ const isCurrencySwitchRef = (0, _react.useRef)(false);
76
+ const prevCurrencyIdRef = (0, _react.useRef)(null);
63
77
  const query = (0, _util2.getQueryParams)(window.location.href);
64
78
  const availableCurrencyIds = (0, _react.useMemo)(() => {
65
79
  const currencyIds = /* @__PURE__ */new Set();
@@ -145,13 +159,198 @@ function PaymentInner({
145
159
  });
146
160
  const currency = (0, _util2.findCurrency)(paymentMethods, currencyId) || settings.baseCurrency;
147
161
  const method = paymentMethods.find(x => x.id === currency.payment_method_id);
148
- const recalculatePromotion = () => {
162
+ const hasDynamicPricing = (0, _react.useMemo)(() => state.checkoutSession.line_items.some(item => (item.upsell_price || item.price)?.pricing_type === "dynamic"), [state.checkoutSession.line_items]);
163
+ const isStripePayment = method?.type === "stripe";
164
+ const needsExchangeRate = hasDynamicPricing && !isStripePayment;
165
+ const effectiveRateUnavailable = needsExchangeRate && (state.rateUnavailable || state.liveRateUnavailable);
166
+ if (state.liveRateError || state.rateError) {
167
+ console.error("[Rate Error]", {
168
+ liveRateError: state.liveRateError,
169
+ rateError: state.rateError
170
+ });
171
+ }
172
+ const refreshRateRef = (0, _react.useRef)(null);
173
+ (0, _react.useEffect)(() => {
174
+ if (!currencyId) {
175
+ return;
176
+ }
177
+ const isCurrencySwitch = prevCurrencyIdRef.current !== null && prevCurrencyIdRef.current !== currencyId;
178
+ prevCurrencyIdRef.current = currencyId;
179
+ if (isCurrencySwitch) {
180
+ isCurrencySwitchRef.current = true;
181
+ setState({
182
+ isRateLoading: true
183
+ });
184
+ }
185
+ if (needsExchangeRate) {
186
+ setState({
187
+ liveRateInfo: void 0,
188
+ liveQuoteSnapshot: void 0,
189
+ liveRateUnavailable: false,
190
+ liveRateError: void 0
191
+ });
192
+ liveRateRefreshRef.current = false;
193
+ refreshRateRef.current?.();
194
+ } else {
195
+ setState({
196
+ liveRateInfo: void 0,
197
+ liveQuoteSnapshot: void 0,
198
+ liveRateUnavailable: false,
199
+ liveRateError: void 0
200
+ });
201
+ liveRateRefreshRef.current = false;
202
+ if (isCurrencySwitch && !state.checkoutSession?.discounts?.length) {
203
+ setState({
204
+ isRateLoading: false
205
+ });
206
+ isCurrencySwitchRef.current = false;
207
+ }
208
+ }
209
+ }, [currencyId, needsExchangeRate]);
210
+ (0, _react.useEffect)(() => {
211
+ if (!state.checkoutSession?.id || completed || !needsExchangeRate) {
212
+ return void 0;
213
+ }
214
+ let cancelled = false;
215
+ let consecutiveFailures = 0;
216
+ const baseInterval = 3e4;
217
+ const MAX_INTERVAL = 5 * 60 * 1e3;
218
+ const QUICK_RETRY_DELAY = 1e3;
219
+ const MAX_QUICK_RETRIES = 2;
220
+ let currentInterval = baseInterval;
221
+ let timer = null;
222
+ const scheduleNext = () => {
223
+ if (timer) {
224
+ clearInterval(timer);
225
+ }
226
+ timer = window.setInterval(() => {
227
+ fetchRate(false);
228
+ }, currentInterval);
229
+ };
230
+ const fetchRate = async (isManualRetry = false) => {
231
+ if (document.hidden && !isManualRetry) {
232
+ return;
233
+ }
234
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
235
+ return;
236
+ }
237
+ if (liveRateRefreshRef.current) {
238
+ return;
239
+ }
240
+ liveRateRefreshRef.current = true;
241
+ let quickRetryCount = 0;
242
+ let lastError = null;
243
+ try {
244
+ while (quickRetryCount <= MAX_QUICK_RETRIES) {
245
+ try {
246
+ const {
247
+ data
248
+ } = await _api.default.get(`/api/checkout-sessions/${state.checkoutSession.id}/exchange-rate`, {
249
+ params: currencyId ? {
250
+ currency_id: currencyId
251
+ } : void 0
252
+ });
253
+ if (cancelled) {
254
+ return;
255
+ }
256
+ consecutiveFailures = 0;
257
+ currentInterval = baseInterval;
258
+ setState({
259
+ liveRateInfo: data,
260
+ liveQuoteSnapshot: void 0,
261
+ // Quote is created at submit time only
262
+ liveRateUnavailable: false,
263
+ liveRateError: void 0
264
+ });
265
+ if (isCurrencySwitchRef.current && !state.checkoutSession?.discounts?.length) {
266
+ setState({
267
+ isRateLoading: false
268
+ });
269
+ isCurrencySwitchRef.current = false;
270
+ }
271
+ scheduleNext();
272
+ return;
273
+ } catch (err) {
274
+ lastError = err;
275
+ quickRetryCount++;
276
+ if (quickRetryCount <= MAX_QUICK_RETRIES && !cancelled) {
277
+ console.log(`[Exchange Rate] Quick retry ${quickRetryCount}/${MAX_QUICK_RETRIES} after ${QUICK_RETRY_DELAY}ms`);
278
+ await new Promise(resolve => {
279
+ setTimeout(resolve, QUICK_RETRY_DELAY);
280
+ });
281
+ }
282
+ }
283
+ }
284
+ if (cancelled) {
285
+ return;
286
+ }
287
+ consecutiveFailures++;
288
+ const technicalError = lastError?.response?.data?.error || (0, _util2.formatError)(lastError);
289
+ console.error("[Exchange Rate Fetch Error]", {
290
+ error: technicalError,
291
+ consecutiveFailures,
292
+ sessionId: state.checkoutSession?.id,
293
+ currencyId
294
+ });
295
+ setState({
296
+ liveRateUnavailable: true,
297
+ liveRateError: void 0
298
+ });
299
+ if (consecutiveFailures >= 3) {
300
+ console.warn("Exchange rate fetch failed multiple times", {
301
+ consecutiveFailures,
302
+ technicalError
303
+ });
304
+ }
305
+ const nextInterval = Math.min(baseInterval * 2 ** (consecutiveFailures - 1), MAX_INTERVAL);
306
+ currentInterval = nextInterval;
307
+ scheduleNext();
308
+ } finally {
309
+ liveRateRefreshRef.current = false;
310
+ }
311
+ };
312
+ refreshRateRef.current = async () => {
313
+ liveRateRefreshRef.current = false;
314
+ await fetchRate(true);
315
+ };
316
+ fetchRate(false);
317
+ const handleVisibilityChange = () => {
318
+ if (!document.hidden) {
319
+ fetchRate(false);
320
+ }
321
+ };
322
+ document.addEventListener("visibilitychange", handleVisibilityChange);
323
+ return () => {
324
+ cancelled = true;
325
+ refreshRateRef.current = null;
326
+ if (timer) {
327
+ clearInterval(timer);
328
+ }
329
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
330
+ };
331
+ }, [state.checkoutSession?.id, currencyId, completed, needsExchangeRate]);
332
+ const recalculatePromotion = async () => {
149
333
  if (state.checkoutSession?.discounts?.length) {
150
- _api.default.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
151
- currency_id: currencyId
152
- }).then(() => {
153
- onPromotionUpdate();
334
+ try {
335
+ await _api.default.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
336
+ currency_id: currencyId
337
+ });
338
+ await onPromotionUpdate();
339
+ } catch (err) {
340
+ console.error("[recalculatePromotion] Error:", err);
341
+ } finally {
342
+ if (isCurrencySwitchRef.current) {
343
+ setState({
344
+ isRateLoading: false
345
+ });
346
+ isCurrencySwitchRef.current = false;
347
+ }
348
+ }
349
+ } else if (isCurrencySwitchRef.current) {
350
+ setState({
351
+ isRateLoading: false
154
352
  });
353
+ isCurrencySwitchRef.current = false;
155
354
  }
156
355
  };
157
356
  (0, _react.useEffect)(() => {
@@ -224,14 +423,24 @@ function PaymentInner({
224
423
  data
225
424
  } = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/adjust-quantity`, {
226
425
  itemId,
227
- quantity
426
+ quantity,
427
+ currency_id: currencyId
228
428
  });
229
429
  if (data.discounts?.length) {
230
430
  recalculatePromotion();
231
431
  return;
232
432
  }
233
433
  setState({
234
- checkoutSession: data
434
+ checkoutSession: data,
435
+ ...(data.rateUnavailable !== void 0 && {
436
+ rateUnavailable: data.rateUnavailable
437
+ }),
438
+ ...(data.rateError !== void 0 && {
439
+ rateError: data.rateError
440
+ }),
441
+ ...(data.quotes !== void 0 && {
442
+ quotes: data.quotes
443
+ })
235
444
  });
236
445
  } catch (err) {
237
446
  console.error(err);
@@ -297,6 +506,57 @@ function PaymentInner({
297
506
  });
298
507
  onPaid(result);
299
508
  };
509
+ const handleQuoteUpdated = payload => {
510
+ setState({
511
+ checkoutSession: payload.checkoutSession,
512
+ ...(payload.quotes !== void 0 && {
513
+ quotes: payload.quotes
514
+ }),
515
+ ...(payload.rateUnavailable !== void 0 && {
516
+ rateUnavailable: payload.rateUnavailable
517
+ }),
518
+ ...(payload.rateError !== void 0 && {
519
+ rateError: payload.rateError
520
+ }),
521
+ ...(payload.paymentIntent !== void 0 && {
522
+ paymentIntent: payload.paymentIntent
523
+ })
524
+ });
525
+ };
526
+ const handlePaymentIntentUpdate = intent => {
527
+ setState({
528
+ paymentIntent: intent
529
+ });
530
+ };
531
+ const quoteRefreshRef = (0, _react.useRef)(false);
532
+ const liveRateRefreshRef = (0, _react.useRef)(false);
533
+ const handleQuoteExpired = async (forceRefresh = false) => {
534
+ if (quoteRefreshRef.current) {
535
+ return;
536
+ }
537
+ quoteRefreshRef.current = true;
538
+ try {
539
+ const {
540
+ data
541
+ } = await _api.default.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`, {
542
+ params: forceRefresh ? {
543
+ forceRefresh: "1"
544
+ } : void 0
545
+ });
546
+ handleQuoteUpdated({
547
+ checkoutSession: data.checkoutSession,
548
+ quotes: data.quotes,
549
+ rateUnavailable: data.rateUnavailable,
550
+ rateError: data.rateError,
551
+ paymentIntent: data.paymentIntent
552
+ });
553
+ } catch (err) {
554
+ console.error(err);
555
+ _Toast.default.error((0, _util2.formatError)(err));
556
+ } finally {
557
+ quoteRefreshRef.current = false;
558
+ }
559
+ };
300
560
  let trialInDays = Number(state.checkoutSession?.subscription_data?.trial_period_days || 0);
301
561
  let trialEnd = Number(state.checkoutSession?.subscription_data?.trial_end || 0);
302
562
  const trialCurrencyIds = (state.checkoutSession?.subscription_data?.trial_currency || "").split(",").map(_trim.default).filter(Boolean);
@@ -328,6 +588,7 @@ function PaymentInner({
328
588
  state.checkoutSession.subscription_data?.min_stake_amount || 0),
329
589
  showStaking: (0, _util2.showStaking)(method, currency, !!state.checkoutSession.subscription_data?.no_stake),
330
590
  currency,
591
+ paymentIntent: state.paymentIntent || paymentIntent,
331
592
  onUpsell,
332
593
  onDownsell,
333
594
  onQuantityChange,
@@ -342,7 +603,31 @@ function PaymentInner({
342
603
  checkoutSession: state.checkoutSession,
343
604
  onPromotionUpdate,
344
605
  paymentMethods,
345
- showFeatures
606
+ showFeatures,
607
+ rateUnavailable: effectiveRateUnavailable,
608
+ isRateLoading: state.isRateLoading,
609
+ liveRate: state.liveRateInfo,
610
+ liveQuoteSnapshot: state.liveQuoteSnapshot,
611
+ onQuoteExpired: handleQuoteExpired,
612
+ onRefreshRate: refreshRateRef.current || void 0,
613
+ isStripePayment,
614
+ onSlippageChange: async slippageConfig => {
615
+ try {
616
+ const {
617
+ data
618
+ } = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/slippage`, {
619
+ slippage_config: slippageConfig
620
+ });
621
+ handleQuoteUpdated({
622
+ checkoutSession: data.checkoutSession || state.checkoutSession,
623
+ quotes: data.quotes,
624
+ rateUnavailable: data.rateUnavailable,
625
+ rateError: data.rateError
626
+ });
627
+ } catch (err) {
628
+ console.error("Failed to update slippage", err);
629
+ }
630
+ }
346
631
  }), mode === "standalone" && !isMobile && /* @__PURE__ */(0, _jsxRuntime.jsx)(_footer.default, {
347
632
  className: "cko-footer",
348
633
  sx: {
@@ -374,13 +659,16 @@ function PaymentInner({
374
659
  currencyId,
375
660
  checkoutSession: state.checkoutSession,
376
661
  paymentMethods,
377
- paymentIntent,
662
+ paymentIntent: state.paymentIntent || paymentIntent,
378
663
  paymentLink,
379
664
  customer,
380
665
  onPaid: handlePaid,
381
666
  onError,
667
+ onQuoteUpdated: handleQuoteUpdated,
668
+ onPaymentIntentUpdate: handlePaymentIntentUpdate,
382
669
  mode,
383
- action
670
+ action,
671
+ rateUnavailable: effectiveRateUnavailable
384
672
  })]
385
673
  }), mode === "standalone" && isMobile && /* @__PURE__ */(0, _jsxRuntime.jsx)(_footer.default, {
386
674
  className: "cko-footer",
@@ -405,7 +693,10 @@ function Payment({
405
693
  onChange,
406
694
  goBack,
407
695
  action,
408
- showCheckoutSummary = true
696
+ showCheckoutSummary = true,
697
+ quotes,
698
+ rateUnavailable,
699
+ rateError
409
700
  }) {
410
701
  const {
411
702
  t
@@ -488,7 +779,10 @@ function Payment({
488
779
  goBack,
489
780
  mode,
490
781
  action,
491
- showCheckoutSummary
782
+ showCheckoutSummary,
783
+ quotes,
784
+ rateUnavailable,
785
+ rateError
492
786
  });
493
787
  };
494
788
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
@@ -1,5 +1,24 @@
1
1
  import type { TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
2
2
  import React from 'react';
3
+ interface DiscountInfo {
4
+ promotion_code?: string;
5
+ coupon?: string;
6
+ discount_amount?: string;
7
+ promotion_code_details?: {
8
+ code?: string;
9
+ };
10
+ coupon_details?: {
11
+ percent_off?: number;
12
+ amount_off?: string;
13
+ currency_id?: string;
14
+ currency_options?: Record<string, {
15
+ amount_off?: string;
16
+ }>;
17
+ };
18
+ verification_data?: {
19
+ code?: string;
20
+ };
21
+ }
3
22
  type Props = {
4
23
  item: TLineItemExpanded;
5
24
  items: TLineItemExpanded[];
@@ -18,6 +37,12 @@ type Props = {
18
37
  onQuantityChange?: (itemId: string, quantity: number) => void;
19
38
  completed?: boolean;
20
39
  showFeatures?: boolean;
40
+ exchangeRate?: string | null;
41
+ isStripePayment?: boolean;
42
+ isPriceLocked?: boolean;
43
+ isRateLoading?: boolean;
44
+ discounts?: DiscountInfo[];
45
+ calculatedDiscountAmount?: string | null;
21
46
  };
22
- export default function ProductItem({ item, items, trialInDays, trialEnd, currency, mode, children, onUpsell, onDownsell, completed, adjustableQuantity, onQuantityChange, showFeatures, }: Props): JSX.Element;
47
+ export default function ProductItem({ item, items, trialInDays, trialEnd, currency, mode, children, onUpsell, onDownsell, completed, adjustableQuantity, onQuantityChange, showFeatures, exchangeRate, isStripePayment, isPriceLocked, isRateLoading, discounts, calculatedDiscountAmount, }: Props): JSX.Element;
23
48
  export {};