@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
@@ -8,21 +8,26 @@ import { useInfiniteScroll, useRequest, useSetState } from "ahooks";
8
8
  import React, { useEffect, useRef, useState } from "react";
9
9
  import { useNavigate } from "react-router-dom";
10
10
  import debounce from "lodash/debounce";
11
+ import { BN } from "@ocap/util";
11
12
  import Status from "../../components/status.js";
12
13
  import { usePaymentContext } from "../../contexts/payment.js";
13
14
  import { useSubscription } from "../../hooks/subscription.js";
14
15
  import api from "../../libs/api.js";
15
16
  import StripePaymentAction from "../../components/stripe-payment-action.js";
16
17
  import {
17
- formatBNStr,
18
18
  formatCreditAmount,
19
19
  formatError,
20
20
  formatToDate,
21
21
  formatToDatetime,
22
+ formatTime,
23
+ formatExchangeRate,
22
24
  getInvoiceDescriptionAndReason,
23
25
  getInvoiceStatusColor,
24
26
  getTxLink,
25
- isCrossOrigin
27
+ isCrossOrigin,
28
+ getUsdAmountFromTokenUnits,
29
+ formatUsdAmount,
30
+ formatAmount
26
31
  } from "../../libs/util.js";
27
32
  import Table from "../../components/table.js";
28
33
  import { createLink, handleNavigation } from "../../libs/navigation.js";
@@ -39,13 +44,24 @@ const groupByDate = (items) => {
39
44
  };
40
45
  const fetchData = (params = {}) => {
41
46
  const search = new URLSearchParams();
42
- Object.keys(params).forEach((key) => {
43
- if (params[key]) {
44
- search.set(key, String(params[key]));
47
+ const mergedParams = { include_quote: true, ...params };
48
+ Object.keys(mergedParams).forEach((key) => {
49
+ if (mergedParams[key]) {
50
+ search.set(key, String(mergedParams[key]));
45
51
  }
46
52
  });
47
53
  return api.get(`/api/invoices?${search.toString()}`).then((res) => res.data);
48
54
  };
55
+ const getInvoiceQuoteInfo = (invoice) => {
56
+ const lines = invoice.lines || [];
57
+ for (const line of lines) {
58
+ const quote = line.metadata?.quote;
59
+ if (quote?.exchange_rate) {
60
+ return quote;
61
+ }
62
+ }
63
+ return null;
64
+ };
49
65
  const getInvoiceLink = (invoice, action) => {
50
66
  if (invoice.id.startsWith("in_")) {
51
67
  const path = `/customer/invoice/${invoice.id}${invoice.status === "uncollectible" && action ? `?action=${action}` : ""}`;
@@ -155,10 +171,98 @@ const InvoiceTable = React.memo((props) => {
155
171
  customBodyRenderLite: (_, index) => {
156
172
  const invoice = data?.list[index];
157
173
  const isVoid = invoice.status === "void";
158
- return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: /* @__PURE__ */ jsxs(Typography, { sx: isVoid ? { textDecoration: "line-through" } : {}, children: [
159
- formatBNStr(invoice.total, invoice.paymentCurrency.decimal),
160
- "\xA0",
161
- invoice.paymentCurrency.symbol
174
+ const quoteInfo = getInvoiceQuoteInfo(invoice);
175
+ const providers = quoteInfo?.providers || [];
176
+ const providerNames = providers.map((provider) => provider.provider_name).filter(Boolean);
177
+ const providerDisplay = providerNames.length > 0 ? providerNames.join(", ") : quoteInfo?.rate_provider_name || quoteInfo?.rate_provider_id || "\u2014";
178
+ const providerRates = quoteInfo?.providers?.map((provider) => {
179
+ const name = provider.provider_name || provider.provider_id || "\u2014";
180
+ return provider.rate ? `${name}` : name;
181
+ }).filter(Boolean) || [];
182
+ const rateTimestamp = quoteInfo?.rate_timestamp_ms ? formatTime(quoteInfo.rate_timestamp_ms) : "\u2014";
183
+ const formattedRate = formatExchangeRate(quoteInfo?.exchange_rate || null);
184
+ const rateLine = formattedRate ? (() => {
185
+ const currencyMap = {
186
+ USD: "$",
187
+ CNY: "\xA5"
188
+ };
189
+ const currencySymbol = currencyMap[quoteInfo?.base_currency];
190
+ return `1 ${invoice.paymentCurrency.symbol} \u2248 ${currencySymbol ? `${currencySymbol}${formattedRate}` : `${formattedRate} ${quoteInfo?.base_currency || "USD"}`}`;
191
+ })() : null;
192
+ let usdAmount = null;
193
+ if (quoteInfo?.base_amount) {
194
+ usdAmount = formatUsdAmount(quoteInfo.base_amount, locale);
195
+ } else if (quoteInfo?.exchange_rate && invoice.total) {
196
+ const calculatedUsd = getUsdAmountFromTokenUnits(
197
+ new BN(invoice.total),
198
+ invoice.paymentCurrency.decimal,
199
+ quoteInfo.exchange_rate
200
+ );
201
+ if (calculatedUsd) {
202
+ usdAmount = formatUsdAmount(calculatedUsd, locale);
203
+ }
204
+ }
205
+ const tooltipContent = quoteInfo ? /* @__PURE__ */ jsxs(Stack, { spacing: 0.5, sx: { p: 1 }, children: [
206
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", spacing: 2, children: [
207
+ /* @__PURE__ */ jsxs(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: [
208
+ t("payment.customer.invoice.quote.providers"),
209
+ ":"
210
+ ] }),
211
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.primary" }, children: (providerRates.length > 0 ? providerRates.join(", ") : providerDisplay) || "\u2014" })
212
+ ] }),
213
+ rateLine && /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", spacing: 2, children: [
214
+ /* @__PURE__ */ jsxs(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: [
215
+ t("payment.customer.invoice.quote.exchangeRate"),
216
+ ":"
217
+ ] }),
218
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.primary" }, children: rateLine })
219
+ ] }),
220
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", spacing: 2, children: [
221
+ /* @__PURE__ */ jsxs(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: [
222
+ t("payment.customer.invoice.quote.rateTimestamp"),
223
+ ":"
224
+ ] }),
225
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.primary" }, children: rateTimestamp })
226
+ ] })
227
+ ] }) : null;
228
+ return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleLinkClick(e, invoice), sx: linkStyle, children: /* @__PURE__ */ jsxs(Stack, { spacing: 0.25, alignItems: "flex-end", children: [
229
+ /* @__PURE__ */ jsxs(Typography, { sx: isVoid ? { textDecoration: "line-through" } : {}, children: [
230
+ formatAmount(invoice.total, invoice.paymentCurrency.decimal),
231
+ "\xA0",
232
+ invoice.paymentCurrency.symbol
233
+ ] }),
234
+ (usdAmount || rateLine) && /* @__PURE__ */ jsx(
235
+ Tooltip,
236
+ {
237
+ title: tooltipContent,
238
+ placement: "top",
239
+ arrow: true,
240
+ slotProps: {
241
+ tooltip: {
242
+ sx: {
243
+ backgroundColor: "background.paper",
244
+ boxShadow: 1
245
+ }
246
+ }
247
+ },
248
+ children: /* @__PURE__ */ jsx(Stack, { spacing: 0.25, alignItems: "flex-end", children: usdAmount && /* @__PURE__ */ jsxs(
249
+ Typography,
250
+ {
251
+ variant: "caption",
252
+ sx: {
253
+ color: "text.secondary",
254
+ fontSize: "0.75rem",
255
+ fontWeight: 400,
256
+ lineHeight: 1.2
257
+ },
258
+ children: [
259
+ "\u2248 $",
260
+ usdAmount
261
+ ]
262
+ }
263
+ ) })
264
+ }
265
+ )
162
266
  ] }) });
163
267
  }
164
268
  }
@@ -520,6 +624,9 @@ const InvoiceList = React.memo((props) => {
520
624
  invoices.map((invoice) => {
521
625
  const { link, connect } = getInvoiceLink(invoice, action);
522
626
  const isVoid = invoice.status === "void";
627
+ const quoteInfo = getInvoiceQuoteInfo(invoice);
628
+ const formattedRate = formatExchangeRate(quoteInfo?.exchange_rate || null);
629
+ const rateLine = formattedRate ? `1 ${invoice.paymentCurrency.symbol} \u2248 ${formattedRate} ${quoteInfo?.base_currency || "USD"}` : null;
523
630
  return /* @__PURE__ */ jsxs(
524
631
  Stack,
525
632
  {
@@ -575,18 +682,21 @@ const InvoiceList = React.memo((props) => {
575
682
  )
576
683
  }
577
684
  ),
578
- /* @__PURE__ */ jsx(
685
+ /* @__PURE__ */ jsxs(
579
686
  Box,
580
687
  {
581
688
  sx: {
582
689
  flex: 1,
583
690
  textAlign: "right"
584
691
  },
585
- children: /* @__PURE__ */ jsxs(Typography, { sx: isVoid ? { textDecoration: "line-through" } : {}, children: [
586
- formatBNStr(invoice.total, invoice.paymentCurrency.decimal),
587
- "\xA0",
588
- invoice.paymentCurrency.symbol
589
- ] })
692
+ children: [
693
+ /* @__PURE__ */ jsxs(Typography, { sx: isVoid ? { textDecoration: "line-through" } : {}, children: [
694
+ formatAmount(invoice.total, invoice.paymentCurrency.decimal),
695
+ "\xA0",
696
+ invoice.paymentCurrency.symbol
697
+ ] }),
698
+ rateLine && /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.secondary", display: "block" }, children: rateLine })
699
+ ]
590
700
  }
591
701
  ),
592
702
  /* @__PURE__ */ jsx(
@@ -0,0 +1,102 @@
1
+ /**
2
+ * useDynamicPricing Hook
3
+ *
4
+ * Extracts and centralizes all dynamic pricing related calculations
5
+ * for the checkout summary component.
6
+ *
7
+ * Final Freeze Architecture: Quotes are created at Submit time only.
8
+ * This hook handles preview-time display calculations.
9
+ */
10
+ import type { TCheckoutSession, TLineItemExpanded, TPaymentCurrency, TPaymentIntent } from '@blocklet/payment-types';
11
+ export interface LiveRateInfo {
12
+ rate?: string;
13
+ provider_id?: string;
14
+ provider_name?: string;
15
+ provider_display?: string;
16
+ base_currency?: string;
17
+ timestamp_ms?: number;
18
+ fetched_at?: number;
19
+ }
20
+ export interface LiveQuoteSnapshot {
21
+ id: string;
22
+ quoted_amount: string;
23
+ exchange_rate: string;
24
+ expires_at: number;
25
+ rate_timestamp_ms?: number | null;
26
+ renewed?: boolean;
27
+ }
28
+ export interface DiscountInfo {
29
+ promotion_code?: string;
30
+ coupon?: string;
31
+ discount_amount?: string;
32
+ coupon_details?: {
33
+ percent_off?: number;
34
+ amount_off?: string;
35
+ currency_id?: string;
36
+ currency_options?: Record<string, {
37
+ amount_off?: string;
38
+ }>;
39
+ };
40
+ }
41
+ export interface DynamicPricingOptions {
42
+ items: TLineItemExpanded[];
43
+ currency: TPaymentCurrency;
44
+ liveRate?: LiveRateInfo;
45
+ liveQuoteSnapshot?: LiveQuoteSnapshot;
46
+ checkoutSession?: TCheckoutSession;
47
+ paymentIntent?: TPaymentIntent | null;
48
+ locale?: string;
49
+ isStripePayment?: boolean;
50
+ isSubscription?: boolean;
51
+ slippageConfig?: {
52
+ mode?: 'percent' | 'rate';
53
+ percent?: number | null;
54
+ min_acceptable_rate?: string;
55
+ base_currency?: string;
56
+ };
57
+ trialInDays?: number;
58
+ trialEnd?: number;
59
+ discounts?: DiscountInfo[];
60
+ }
61
+ export interface RateInfo {
62
+ exchangeRate: string | null;
63
+ baseCurrency: string;
64
+ providerName: string | null;
65
+ providerId: string | null;
66
+ timestampMs: number | null;
67
+ fetchedAt: number | null;
68
+ }
69
+ export interface QuoteMeta {
70
+ exchangeRate: string | null;
71
+ baseCurrency: string;
72
+ expiresAt: number | null;
73
+ providerName: string | null;
74
+ providerId: string | null;
75
+ rateTimestampMs: number | null;
76
+ slippagePercent: number | null;
77
+ }
78
+ /**
79
+ * Custom hook for dynamic pricing calculations
80
+ */
81
+ export declare function useDynamicPricing(options: DynamicPricingOptions): {
82
+ hasDynamicPricing: boolean;
83
+ isPriceLocked: boolean;
84
+ lockExpired: boolean;
85
+ quoteMeta: QuoteMeta | null;
86
+ rateInfo: RateInfo;
87
+ quoteLockedAt: number | null;
88
+ calculatedTokenAmount: string | null;
89
+ calculatedDiscountAmount: any;
90
+ currentSlippagePercent: number;
91
+ rateDisplay: string | null;
92
+ providerDisplay: string;
93
+ formatTotalDisplay: (totalAmountValue: string, fallback?: string) => string;
94
+ calculateUsdDisplay: (totalAmountValue: string) => string | null;
95
+ buildQuoteDetailRows: (t: (key: string) => string) => {
96
+ label: string;
97
+ value: string | React.ReactNode;
98
+ isSlippage?: boolean;
99
+ tooltip?: string;
100
+ }[];
101
+ };
102
+ export default useDynamicPricing;
@@ -0,0 +1,393 @@
1
+ import { useMemo } from "react";
2
+ import { BN, fromTokenToUnit, fromUnitToToken } from "@ocap/util";
3
+ import {
4
+ formatDateTime,
5
+ formatDynamicPrice,
6
+ formatExchangeRate,
7
+ formatUsdAmount,
8
+ getUsdAmountFromTokenUnits
9
+ } from "../libs/util.js";
10
+ function extractQuoteMeta(items) {
11
+ if (!items?.length) {
12
+ return null;
13
+ }
14
+ let exchangeRate = null;
15
+ let baseCurrency = null;
16
+ let expiresAt = null;
17
+ let providerName = null;
18
+ let providerId = null;
19
+ let rateTimestampMs = null;
20
+ let slippagePercent = null;
21
+ items.forEach((item) => {
22
+ const price = item.upsell_price || item.price;
23
+ const rate = item?.exchange_rate;
24
+ if (!exchangeRate && rate) {
25
+ exchangeRate = rate;
26
+ }
27
+ const base = price?.base_currency;
28
+ if (!baseCurrency && base) {
29
+ baseCurrency = base;
30
+ }
31
+ const expires = item?.expires_at;
32
+ if (expires) {
33
+ expiresAt = expiresAt === null ? expires : Math.min(expiresAt, expires);
34
+ }
35
+ const itemProviderName = item?.rate_provider_name;
36
+ if (!providerName && itemProviderName) {
37
+ providerName = itemProviderName;
38
+ }
39
+ const itemProviderId = item?.rate_provider_id;
40
+ if (!providerId && itemProviderId) {
41
+ providerId = itemProviderId;
42
+ }
43
+ const itemRateTimestamp = item?.rate_timestamp_ms;
44
+ if (!rateTimestampMs && Number.isFinite(Number(itemRateTimestamp))) {
45
+ rateTimestampMs = Number(itemRateTimestamp);
46
+ }
47
+ const itemSlippage = item?.slippage_percent;
48
+ if (slippagePercent === null && Number.isFinite(Number(itemSlippage))) {
49
+ slippagePercent = Number(itemSlippage);
50
+ }
51
+ });
52
+ return {
53
+ exchangeRate,
54
+ baseCurrency: baseCurrency || "USD",
55
+ expiresAt,
56
+ providerName,
57
+ providerId,
58
+ rateTimestampMs,
59
+ slippagePercent
60
+ };
61
+ }
62
+ function calculateTokenAmount(items, rate, currencyDecimal, trialing = false) {
63
+ const usdTotal = items.reduce((acc, item) => {
64
+ const price = item.upsell_price || item.price;
65
+ if (trialing && price?.type === "recurring") {
66
+ return acc;
67
+ }
68
+ if (price?.type === "recurring" && price?.recurring?.usage_type === "metered") {
69
+ return acc;
70
+ }
71
+ const baseAmount = price?.base_amount;
72
+ if (baseAmount) {
73
+ const quantity = item.quantity || 1;
74
+ return acc + Number(baseAmount) * quantity;
75
+ }
76
+ return acc;
77
+ }, 0);
78
+ if (usdTotal <= 0) {
79
+ return null;
80
+ }
81
+ const rateNum = Number(rate);
82
+ if (rateNum <= 0) {
83
+ return null;
84
+ }
85
+ const tokenAmount = usdTotal / rateNum;
86
+ const tokenAmountUnit = fromTokenToUnit(tokenAmount.toFixed(currencyDecimal || 8), currencyDecimal || 8);
87
+ return tokenAmountUnit.toString();
88
+ }
89
+ function extractQuoteLockedAt(paymentIntent, checkoutSession) {
90
+ if (paymentIntent?.quote_locked_at) {
91
+ const lockedAtMs = new Date(paymentIntent.quote_locked_at).getTime();
92
+ if (Number.isFinite(lockedAtMs)) {
93
+ return Math.floor(lockedAtMs / 1e3);
94
+ }
95
+ }
96
+ const metaLockedAt = checkoutSession?.metadata?.quote_locked_at;
97
+ if (typeof metaLockedAt === "number") {
98
+ return metaLockedAt;
99
+ }
100
+ if (typeof metaLockedAt === "string") {
101
+ const parsed = Number(metaLockedAt);
102
+ if (Number.isFinite(parsed)) {
103
+ return parsed;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ export function useDynamicPricing(options) {
109
+ const {
110
+ items,
111
+ currency,
112
+ liveRate,
113
+ liveQuoteSnapshot,
114
+ checkoutSession,
115
+ paymentIntent,
116
+ locale = "en",
117
+ isStripePayment = false,
118
+ isSubscription = checkoutSession?.mode === "subscription" || checkoutSession?.mode === "setup",
119
+ slippageConfig,
120
+ trialInDays = 0,
121
+ trialEnd = 0,
122
+ discounts
123
+ } = options;
124
+ const currentTime = Math.floor(Date.now() / 1e3);
125
+ const trialing = trialInDays > 0 || trialEnd > currentTime;
126
+ const hasDynamicPricing = useMemo(
127
+ () => items.some((item) => (item.upsell_price || item.price)?.pricing_type === "dynamic"),
128
+ [items]
129
+ );
130
+ const quoteMeta = useMemo(() => hasDynamicPricing ? extractQuoteMeta(items) : null, [items, hasDynamicPricing]);
131
+ const rateInfo = useMemo(
132
+ () => ({
133
+ exchangeRate: isStripePayment ? null : liveRate?.rate || quoteMeta?.exchangeRate || null,
134
+ baseCurrency: liveRate?.base_currency || quoteMeta?.baseCurrency || "USD",
135
+ providerName: isStripePayment ? null : liveRate?.provider_name || quoteMeta?.providerName || null,
136
+ providerId: isStripePayment ? null : liveRate?.provider_id || quoteMeta?.providerId || null,
137
+ timestampMs: isStripePayment ? null : liveRate?.timestamp_ms || quoteMeta?.rateTimestampMs || null,
138
+ fetchedAt: isStripePayment ? null : liveRate?.fetched_at || null
139
+ // When we fetched the data
140
+ }),
141
+ [liveRate, quoteMeta, isStripePayment]
142
+ );
143
+ const calculatedTokenAmount = useMemo(() => {
144
+ if (isStripePayment || !hasDynamicPricing || !liveRate?.rate) {
145
+ return null;
146
+ }
147
+ return calculateTokenAmount(items, liveRate.rate, currency.decimal, trialing);
148
+ }, [hasDynamicPricing, liveRate?.rate, items, currency.decimal, isStripePayment, trialing]);
149
+ const calculatedDiscountAmount = useMemo(() => {
150
+ if (!discounts?.length) {
151
+ return null;
152
+ }
153
+ const couponDetails = discounts[0]?.coupon_details;
154
+ if (!couponDetails) {
155
+ return null;
156
+ }
157
+ if (isStripePayment) {
158
+ const discountableSubtotalCents = items.reduce((sum, item) => {
159
+ if (!item.discountable) {
160
+ return sum;
161
+ }
162
+ const price = item.upsell_price || item.price;
163
+ if (trialing && price?.type === "recurring") {
164
+ return sum;
165
+ }
166
+ if (price?.type === "recurring" && price?.recurring?.usage_type === "metered") {
167
+ return sum;
168
+ }
169
+ const isDynamic = price?.pricing_type === "dynamic";
170
+ const baseAmount = price?.base_amount;
171
+ let amountCents;
172
+ if (isDynamic && baseAmount !== void 0 && baseAmount !== null) {
173
+ amountCents = Number(baseAmount) * 100;
174
+ } else {
175
+ const unitAmount = price?.unit_amount;
176
+ if (!unitAmount) {
177
+ return sum;
178
+ }
179
+ amountCents = Number(unitAmount);
180
+ }
181
+ const quantity = item.quantity || 1;
182
+ return sum + amountCents * quantity;
183
+ }, 0);
184
+ if (discountableSubtotalCents <= 0) {
185
+ return null;
186
+ }
187
+ if (couponDetails.percent_off && couponDetails.percent_off > 0) {
188
+ const discountCents = Math.round(discountableSubtotalCents * couponDetails.percent_off / 100);
189
+ return discountCents.toString();
190
+ }
191
+ if (couponDetails.amount_off) {
192
+ const amountOffCents = Number(couponDetails.amount_off);
193
+ if (Number.isFinite(amountOffCents) && amountOffCents > 0) {
194
+ return Math.min(amountOffCents, discountableSubtotalCents).toString();
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+ if (!hasDynamicPricing || !liveRate?.rate) {
200
+ return null;
201
+ }
202
+ const rateNum = Number(liveRate.rate);
203
+ if (rateNum <= 0) {
204
+ return null;
205
+ }
206
+ const discountableSubtotal = items.reduce((sum, item) => {
207
+ if (!item.discountable) {
208
+ return sum;
209
+ }
210
+ const price = item.upsell_price || item.price;
211
+ if (trialing && price?.type === "recurring") {
212
+ return sum;
213
+ }
214
+ if (price?.type === "recurring" && price?.recurring?.usage_type === "metered") {
215
+ return sum;
216
+ }
217
+ const baseAmount = price?.base_amount;
218
+ if (!baseAmount) {
219
+ return sum;
220
+ }
221
+ const quantity = item.quantity || 1;
222
+ const tokenAmount = Number(baseAmount) * quantity / rateNum;
223
+ return sum + tokenAmount;
224
+ }, 0);
225
+ if (discountableSubtotal <= 0) {
226
+ return null;
227
+ }
228
+ if (couponDetails.percent_off && couponDetails.percent_off > 0) {
229
+ const discountAmount = discountableSubtotal * couponDetails.percent_off / 100;
230
+ const discountAmountUnit = fromTokenToUnit(discountAmount.toFixed(currency.decimal || 8), currency.decimal || 8);
231
+ return discountAmountUnit.toString();
232
+ }
233
+ if (couponDetails.amount_off) {
234
+ const amountOff = couponDetails.currency_id === currency.id ? couponDetails.amount_off : couponDetails.currency_options?.[currency.id]?.amount_off;
235
+ if (!amountOff) {
236
+ return null;
237
+ }
238
+ const subtotalUnit = fromTokenToUnit(discountableSubtotal.toFixed(currency.decimal || 8), currency.decimal || 8);
239
+ const amountOffBN = new BN(amountOff);
240
+ const subtotalBN = new BN(subtotalUnit);
241
+ return BN.min(amountOffBN, subtotalBN).toString();
242
+ }
243
+ return null;
244
+ }, [isStripePayment, hasDynamicPricing, liveRate?.rate, discounts, items, trialing, currency.decimal, currency.id]);
245
+ const rateDisplay = useMemo(() => {
246
+ if (!rateInfo.exchangeRate) {
247
+ return null;
248
+ }
249
+ const formattedRate = formatExchangeRate(rateInfo.exchangeRate);
250
+ if (!formattedRate) {
251
+ return null;
252
+ }
253
+ if (rateInfo.baseCurrency === "USD") {
254
+ return `$${formattedRate}`;
255
+ }
256
+ return `${formattedRate} ${rateInfo.baseCurrency}`;
257
+ }, [rateInfo]);
258
+ const quoteLockedAt = useMemo(
259
+ () => extractQuoteLockedAt(paymentIntent, checkoutSession),
260
+ [paymentIntent, checkoutSession]
261
+ );
262
+ const isPriceLocked = useMemo(() => {
263
+ if (!quoteLockedAt) return false;
264
+ const now = Math.floor(Date.now() / 1e3);
265
+ const lockRemaining = quoteLockedAt + 180 - now;
266
+ return lockRemaining > 0;
267
+ }, [quoteLockedAt]);
268
+ const lockExpired = useMemo(() => {
269
+ if (!quoteLockedAt) return false;
270
+ const now = Math.floor(Date.now() / 1e3);
271
+ const lockRemaining = quoteLockedAt + 180 - now;
272
+ return lockRemaining <= 0;
273
+ }, [quoteLockedAt]);
274
+ const currentSlippagePercent = useMemo(() => {
275
+ let slippageValue = quoteMeta?.slippagePercent;
276
+ if (slippageValue === null || slippageValue === void 0) {
277
+ slippageValue = slippageConfig?.percent ?? checkoutSession?.metadata?.slippage?.percent ?? checkoutSession?.slippage_percent;
278
+ }
279
+ if (slippageValue === null || slippageValue === void 0) {
280
+ slippageValue = 0.5;
281
+ }
282
+ return slippageValue;
283
+ }, [quoteMeta?.slippagePercent, slippageConfig?.percent, checkoutSession]);
284
+ const providerDisplay = useMemo(() => {
285
+ const fallback = "\u2014";
286
+ if (liveRate?.provider_display) {
287
+ return liveRate.provider_display;
288
+ }
289
+ if (!rateInfo.providerName && !rateInfo.providerId) {
290
+ return fallback;
291
+ }
292
+ return rateInfo.providerName || rateInfo.providerId || fallback;
293
+ }, [liveRate?.provider_display, rateInfo.providerName, rateInfo.providerId]);
294
+ const formatTotalDisplay = (totalAmountValue, fallback = "\u2014") => {
295
+ if (hasDynamicPricing && calculatedTokenAmount) {
296
+ const displayAmount = fromUnitToToken(calculatedTokenAmount, currency.decimal);
297
+ const numericValue2 = Number(displayAmount);
298
+ if (Number.isFinite(numericValue2)) {
299
+ return formatDynamicPrice(displayAmount, true, 6);
300
+ }
301
+ }
302
+ if (hasDynamicPricing && liveQuoteSnapshot?.quoted_amount) {
303
+ const snapshotAmount = fromUnitToToken(liveQuoteSnapshot.quoted_amount, currency.decimal);
304
+ const numericValue2 = Number(snapshotAmount);
305
+ if (Number.isFinite(numericValue2)) {
306
+ return formatDynamicPrice(snapshotAmount, true, 6);
307
+ }
308
+ }
309
+ if (totalAmountValue === null || totalAmountValue === void 0 || totalAmountValue === "") {
310
+ return fallback;
311
+ }
312
+ const numericValue = Number(totalAmountValue);
313
+ if (!Number.isFinite(numericValue)) {
314
+ return fallback;
315
+ }
316
+ return formatDynamicPrice(totalAmountValue, hasDynamicPricing, 6);
317
+ };
318
+ const calculateUsdDisplay = (totalAmountValue) => {
319
+ const fallback = "\u2014";
320
+ if (!hasDynamicPricing || !totalAmountValue || totalAmountValue === fallback) {
321
+ return null;
322
+ }
323
+ const numericValue = Number(totalAmountValue);
324
+ if (!Number.isFinite(numericValue) || numericValue < 0) {
325
+ return null;
326
+ }
327
+ if (numericValue === 0) {
328
+ return formatUsdAmount("0", locale);
329
+ }
330
+ const { exchangeRate } = rateInfo;
331
+ if (!exchangeRate) {
332
+ return null;
333
+ }
334
+ const totalAmountInUnits = fromTokenToUnit(totalAmountValue, currency.decimal);
335
+ const totalUsd = getUsdAmountFromTokenUnits(new BN(totalAmountInUnits), currency.decimal, exchangeRate);
336
+ if (!totalUsd) {
337
+ return null;
338
+ }
339
+ return formatUsdAmount(totalUsd, locale);
340
+ };
341
+ const buildQuoteDetailRows = (t) => {
342
+ if (!hasDynamicPricing) {
343
+ return [];
344
+ }
345
+ const rows = [];
346
+ const fallback = "\u2014";
347
+ if (rateDisplay) {
348
+ const displayTimestamp = rateInfo.fetchedAt || rateInfo.timestampMs;
349
+ const updatedAt = displayTimestamp ? formatDateTime(displayTimestamp) : fallback;
350
+ rows.push(
351
+ { label: t("payment.checkout.quote.detailProvider"), value: providerDisplay },
352
+ { label: t("payment.checkout.quote.detailUpdatedAt"), value: updatedAt }
353
+ );
354
+ }
355
+ if (isSubscription) {
356
+ let slippageDisplay = `${currentSlippagePercent}%`;
357
+ if (slippageConfig?.mode === "rate" && slippageConfig.min_acceptable_rate) {
358
+ const formattedRate = formatExchangeRate(slippageConfig.min_acceptable_rate);
359
+ const displayRate = formattedRate || slippageConfig.min_acceptable_rate;
360
+ const displayCurrency = slippageConfig.base_currency || rateInfo.baseCurrency || "USD";
361
+ const displayText = displayCurrency === "USD" ? `$${displayRate}` : `${displayRate} ${displayCurrency}`;
362
+ slippageDisplay = `${displayText}`;
363
+ }
364
+ rows.push({
365
+ label: t("payment.checkout.quote.detailSlippage"),
366
+ value: slippageDisplay,
367
+ isSlippage: true,
368
+ tooltip: t("payment.checkout.quote.slippage.tooltip")
369
+ });
370
+ }
371
+ return rows;
372
+ };
373
+ return {
374
+ // Flags
375
+ hasDynamicPricing,
376
+ isPriceLocked,
377
+ lockExpired,
378
+ // Data
379
+ quoteMeta,
380
+ rateInfo,
381
+ quoteLockedAt,
382
+ calculatedTokenAmount,
383
+ calculatedDiscountAmount,
384
+ currentSlippagePercent,
385
+ // Display helpers
386
+ rateDisplay,
387
+ providerDisplay,
388
+ formatTotalDisplay,
389
+ calculateUsdDisplay,
390
+ buildQuoteDetailRows
391
+ };
392
+ }
393
+ export default useDynamicPricing;