@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
@@ -7,7 +7,7 @@ import { Box, Fade, Stack } from "@mui/material";
7
7
  import { styled } from "@mui/system";
8
8
  import { fromTokenToUnit } from "@ocap/util";
9
9
  import { useSetState } from "ahooks";
10
- import { useEffect, useState, useMemo } from "react";
10
+ import { useEffect, useMemo, useRef, useState } from "react";
11
11
  import { FormProvider, useForm, useWatch } from "react-hook-form";
12
12
  import trim from "lodash/trim";
13
13
  import { usePaymentContext } from "../contexts/payment.js";
@@ -43,12 +43,28 @@ function PaymentInner({
43
43
  onError,
44
44
  onChange,
45
45
  action,
46
- showCheckoutSummary = true
46
+ showCheckoutSummary = true,
47
+ quotes,
48
+ rateUnavailable,
49
+ rateError
47
50
  }) {
48
51
  const { t } = useLocaleContext();
49
52
  const { settings, session } = usePaymentContext();
50
53
  const { isMobile } = useMobile();
51
- const [state, setState] = useSetState({ checkoutSession });
54
+ const [state, setState] = useSetState({
55
+ checkoutSession,
56
+ quotes,
57
+ rateUnavailable,
58
+ rateError,
59
+ paymentIntent,
60
+ liveRateInfo: void 0,
61
+ liveQuoteSnapshot: void 0,
62
+ liveRateUnavailable: false,
63
+ liveRateError: void 0,
64
+ isRateLoading: false
65
+ });
66
+ const isCurrencySwitchRef = useRef(false);
67
+ const prevCurrencyIdRef = useRef(null);
52
68
  const query = getQueryParams(window.location.href);
53
69
  const availableCurrencyIds = useMemo(() => {
54
70
  const currencyIds = /* @__PURE__ */ new Set();
@@ -135,13 +151,185 @@ function PaymentInner({
135
151
  const currencyId = useWatch({ control: methods.control, name: "payment_currency", defaultValue: defaultCurrencyId });
136
152
  const currency = findCurrency(paymentMethods, currencyId) || settings.baseCurrency;
137
153
  const method = paymentMethods.find((x) => x.id === currency.payment_method_id);
138
- const recalculatePromotion = () => {
139
- if (state.checkoutSession?.discounts?.length) {
140
- api.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
141
- currency_id: currencyId
142
- }).then(() => {
143
- onPromotionUpdate();
154
+ const hasDynamicPricing = useMemo(
155
+ () => state.checkoutSession.line_items.some(
156
+ (item) => (item.upsell_price || item.price)?.pricing_type === "dynamic"
157
+ ),
158
+ [state.checkoutSession.line_items]
159
+ );
160
+ const isStripePayment = method?.type === "stripe";
161
+ const needsExchangeRate = hasDynamicPricing && !isStripePayment;
162
+ const effectiveRateUnavailable = needsExchangeRate && (state.rateUnavailable || state.liveRateUnavailable);
163
+ if (state.liveRateError || state.rateError) {
164
+ console.error("[Rate Error]", { liveRateError: state.liveRateError, rateError: state.rateError });
165
+ }
166
+ const refreshRateRef = useRef(null);
167
+ useEffect(() => {
168
+ if (!currencyId) {
169
+ return;
170
+ }
171
+ const isCurrencySwitch = prevCurrencyIdRef.current !== null && prevCurrencyIdRef.current !== currencyId;
172
+ prevCurrencyIdRef.current = currencyId;
173
+ if (isCurrencySwitch) {
174
+ isCurrencySwitchRef.current = true;
175
+ setState({ isRateLoading: true });
176
+ }
177
+ if (needsExchangeRate) {
178
+ setState({
179
+ liveRateInfo: void 0,
180
+ liveQuoteSnapshot: void 0,
181
+ liveRateUnavailable: false,
182
+ liveRateError: void 0
144
183
  });
184
+ liveRateRefreshRef.current = false;
185
+ refreshRateRef.current?.();
186
+ } else {
187
+ setState({
188
+ liveRateInfo: void 0,
189
+ liveQuoteSnapshot: void 0,
190
+ liveRateUnavailable: false,
191
+ liveRateError: void 0
192
+ });
193
+ liveRateRefreshRef.current = false;
194
+ if (isCurrencySwitch && !state.checkoutSession?.discounts?.length) {
195
+ setState({ isRateLoading: false });
196
+ isCurrencySwitchRef.current = false;
197
+ }
198
+ }
199
+ }, [currencyId, needsExchangeRate]);
200
+ useEffect(() => {
201
+ if (!state.checkoutSession?.id || completed || !needsExchangeRate) {
202
+ return void 0;
203
+ }
204
+ let cancelled = false;
205
+ let consecutiveFailures = 0;
206
+ const baseInterval = 3e4;
207
+ const MAX_INTERVAL = 5 * 60 * 1e3;
208
+ const QUICK_RETRY_DELAY = 1e3;
209
+ const MAX_QUICK_RETRIES = 2;
210
+ let currentInterval = baseInterval;
211
+ let timer = null;
212
+ const scheduleNext = () => {
213
+ if (timer) {
214
+ clearInterval(timer);
215
+ }
216
+ timer = window.setInterval(() => {
217
+ fetchRate(false);
218
+ }, currentInterval);
219
+ };
220
+ const fetchRate = async (isManualRetry = false) => {
221
+ if (document.hidden && !isManualRetry) {
222
+ return;
223
+ }
224
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
225
+ return;
226
+ }
227
+ if (liveRateRefreshRef.current) {
228
+ return;
229
+ }
230
+ liveRateRefreshRef.current = true;
231
+ let quickRetryCount = 0;
232
+ let lastError = null;
233
+ try {
234
+ while (quickRetryCount <= MAX_QUICK_RETRIES) {
235
+ try {
236
+ const { data } = await api.get(`/api/checkout-sessions/${state.checkoutSession.id}/exchange-rate`, {
237
+ params: currencyId ? { currency_id: currencyId } : void 0
238
+ });
239
+ if (cancelled) {
240
+ return;
241
+ }
242
+ consecutiveFailures = 0;
243
+ currentInterval = baseInterval;
244
+ setState({
245
+ liveRateInfo: data,
246
+ liveQuoteSnapshot: void 0,
247
+ // Quote is created at submit time only
248
+ liveRateUnavailable: false,
249
+ liveRateError: void 0
250
+ });
251
+ if (isCurrencySwitchRef.current && !state.checkoutSession?.discounts?.length) {
252
+ setState({ isRateLoading: false });
253
+ isCurrencySwitchRef.current = false;
254
+ }
255
+ scheduleNext();
256
+ return;
257
+ } catch (err) {
258
+ lastError = err;
259
+ quickRetryCount++;
260
+ if (quickRetryCount <= MAX_QUICK_RETRIES && !cancelled) {
261
+ console.log(
262
+ `[Exchange Rate] Quick retry ${quickRetryCount}/${MAX_QUICK_RETRIES} after ${QUICK_RETRY_DELAY}ms`
263
+ );
264
+ await new Promise((resolve) => {
265
+ setTimeout(resolve, QUICK_RETRY_DELAY);
266
+ });
267
+ }
268
+ }
269
+ }
270
+ if (cancelled) {
271
+ return;
272
+ }
273
+ consecutiveFailures++;
274
+ const technicalError = lastError?.response?.data?.error || formatError(lastError);
275
+ console.error("[Exchange Rate Fetch Error]", {
276
+ error: technicalError,
277
+ consecutiveFailures,
278
+ sessionId: state.checkoutSession?.id,
279
+ currencyId
280
+ });
281
+ setState({
282
+ liveRateUnavailable: true,
283
+ liveRateError: void 0
284
+ });
285
+ if (consecutiveFailures >= 3) {
286
+ console.warn("Exchange rate fetch failed multiple times", { consecutiveFailures, technicalError });
287
+ }
288
+ const nextInterval = Math.min(baseInterval * 2 ** (consecutiveFailures - 1), MAX_INTERVAL);
289
+ currentInterval = nextInterval;
290
+ scheduleNext();
291
+ } finally {
292
+ liveRateRefreshRef.current = false;
293
+ }
294
+ };
295
+ refreshRateRef.current = async () => {
296
+ liveRateRefreshRef.current = false;
297
+ await fetchRate(true);
298
+ };
299
+ fetchRate(false);
300
+ const handleVisibilityChange = () => {
301
+ if (!document.hidden) {
302
+ fetchRate(false);
303
+ }
304
+ };
305
+ document.addEventListener("visibilitychange", handleVisibilityChange);
306
+ return () => {
307
+ cancelled = true;
308
+ refreshRateRef.current = null;
309
+ if (timer) {
310
+ clearInterval(timer);
311
+ }
312
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
313
+ };
314
+ }, [state.checkoutSession?.id, currencyId, completed, needsExchangeRate]);
315
+ const recalculatePromotion = async () => {
316
+ if (state.checkoutSession?.discounts?.length) {
317
+ try {
318
+ await api.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
319
+ currency_id: currencyId
320
+ });
321
+ await onPromotionUpdate();
322
+ } catch (err) {
323
+ console.error("[recalculatePromotion] Error:", err);
324
+ } finally {
325
+ if (isCurrencySwitchRef.current) {
326
+ setState({ isRateLoading: false });
327
+ isCurrencySwitchRef.current = false;
328
+ }
329
+ }
330
+ } else if (isCurrencySwitchRef.current) {
331
+ setState({ isRateLoading: false });
332
+ isCurrencySwitchRef.current = false;
145
333
  }
146
334
  };
147
335
  useEffect(() => {
@@ -193,13 +381,19 @@ function PaymentInner({
193
381
  try {
194
382
  const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/adjust-quantity`, {
195
383
  itemId,
196
- quantity
384
+ quantity,
385
+ currency_id: currencyId
197
386
  });
198
387
  if (data.discounts?.length) {
199
388
  recalculatePromotion();
200
389
  return;
201
390
  }
202
- setState({ checkoutSession: data });
391
+ setState({
392
+ checkoutSession: data,
393
+ ...data.rateUnavailable !== void 0 && { rateUnavailable: data.rateUnavailable },
394
+ ...data.rateError !== void 0 && { rateError: data.rateError },
395
+ ...data.quotes !== void 0 && { quotes: data.quotes }
396
+ });
203
397
  } catch (err) {
204
398
  console.error(err);
205
399
  Toast.error(formatError(err));
@@ -247,6 +441,43 @@ function PaymentInner({
247
441
  setState({ checkoutSession: result.checkoutSession });
248
442
  onPaid(result);
249
443
  };
444
+ const handleQuoteUpdated = (payload) => {
445
+ setState({
446
+ checkoutSession: payload.checkoutSession,
447
+ ...payload.quotes !== void 0 && { quotes: payload.quotes },
448
+ ...payload.rateUnavailable !== void 0 && { rateUnavailable: payload.rateUnavailable },
449
+ ...payload.rateError !== void 0 && { rateError: payload.rateError },
450
+ ...payload.paymentIntent !== void 0 && { paymentIntent: payload.paymentIntent }
451
+ });
452
+ };
453
+ const handlePaymentIntentUpdate = (intent) => {
454
+ setState({ paymentIntent: intent });
455
+ };
456
+ const quoteRefreshRef = useRef(false);
457
+ const liveRateRefreshRef = useRef(false);
458
+ const handleQuoteExpired = async (forceRefresh = false) => {
459
+ if (quoteRefreshRef.current) {
460
+ return;
461
+ }
462
+ quoteRefreshRef.current = true;
463
+ try {
464
+ const { data } = await api.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`, {
465
+ params: forceRefresh ? { forceRefresh: "1" } : void 0
466
+ });
467
+ handleQuoteUpdated({
468
+ checkoutSession: data.checkoutSession,
469
+ quotes: data.quotes,
470
+ rateUnavailable: data.rateUnavailable,
471
+ rateError: data.rateError,
472
+ paymentIntent: data.paymentIntent
473
+ });
474
+ } catch (err) {
475
+ console.error(err);
476
+ Toast.error(formatError(err));
477
+ } finally {
478
+ quoteRefreshRef.current = false;
479
+ }
480
+ };
250
481
  let trialInDays = Number(state.checkoutSession?.subscription_data?.trial_period_days || 0);
251
482
  let trialEnd = Number(state.checkoutSession?.subscription_data?.trial_end || 0);
252
483
  const trialCurrencyIds = (state.checkoutSession?.subscription_data?.trial_currency || "").split(",").map(trim).filter(Boolean);
@@ -270,6 +501,7 @@ function PaymentInner({
270
501
  ),
271
502
  showStaking: showStaking(method, currency, !!state.checkoutSession.subscription_data?.no_stake),
272
503
  currency,
504
+ paymentIntent: state.paymentIntent || paymentIntent,
273
505
  onUpsell,
274
506
  onDownsell,
275
507
  onQuantityChange,
@@ -284,7 +516,29 @@ function PaymentInner({
284
516
  checkoutSession: state.checkoutSession,
285
517
  onPromotionUpdate,
286
518
  paymentMethods,
287
- showFeatures
519
+ showFeatures,
520
+ rateUnavailable: effectiveRateUnavailable,
521
+ isRateLoading: state.isRateLoading,
522
+ liveRate: state.liveRateInfo,
523
+ liveQuoteSnapshot: state.liveQuoteSnapshot,
524
+ onQuoteExpired: handleQuoteExpired,
525
+ onRefreshRate: refreshRateRef.current || void 0,
526
+ isStripePayment,
527
+ onSlippageChange: async (slippageConfig) => {
528
+ try {
529
+ const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/slippage`, {
530
+ slippage_config: slippageConfig
531
+ });
532
+ handleQuoteUpdated({
533
+ checkoutSession: data.checkoutSession || state.checkoutSession,
534
+ quotes: data.quotes,
535
+ rateUnavailable: data.rateUnavailable,
536
+ rateError: data.rateError
537
+ });
538
+ } catch (err) {
539
+ console.error("Failed to update slippage", err);
540
+ }
541
+ }
288
542
  }
289
543
  ),
290
544
  mode === "standalone" && !isMobile && /* @__PURE__ */ jsx(CheckoutFooter, { className: "cko-footer", sx: { color: "text.lighter" } })
@@ -324,13 +578,16 @@ function PaymentInner({
324
578
  currencyId,
325
579
  checkoutSession: state.checkoutSession,
326
580
  paymentMethods,
327
- paymentIntent,
581
+ paymentIntent: state.paymentIntent || paymentIntent,
328
582
  paymentLink,
329
583
  customer,
330
584
  onPaid: handlePaid,
331
585
  onError,
586
+ onQuoteUpdated: handleQuoteUpdated,
587
+ onPaymentIntentUpdate: handlePaymentIntentUpdate,
332
588
  mode,
333
- action
589
+ action,
590
+ rateUnavailable: effectiveRateUnavailable
334
591
  }
335
592
  )
336
593
  ]
@@ -353,7 +610,10 @@ export default function Payment({
353
610
  onChange,
354
611
  goBack,
355
612
  action,
356
- showCheckoutSummary = true
613
+ showCheckoutSummary = true,
614
+ quotes,
615
+ rateUnavailable,
616
+ rateError
357
617
  }) {
358
618
  const { t } = useLocaleContext();
359
619
  const { refresh, livemode, setLivemode } = usePaymentContext();
@@ -418,7 +678,10 @@ export default function Payment({
418
678
  goBack,
419
679
  mode,
420
680
  action,
421
- showCheckoutSummary
681
+ showCheckoutSummary,
682
+ quotes,
683
+ rateUnavailable,
684
+ rateError
422
685
  }
423
686
  );
424
687
  };
@@ -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 {};