@blocklet/payment-react 1.24.4 → 1.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/components/auto-topup/modal.d.ts +2 -0
- package/es/components/auto-topup/modal.js +48 -6
- package/es/components/auto-topup/product-card.d.ts +16 -1
- package/es/components/auto-topup/product-card.js +97 -15
- package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
- package/es/components/dynamic-pricing-unavailable.js +58 -0
- package/es/components/loading-amount.d.ts +17 -0
- package/es/components/loading-amount.js +46 -0
- package/es/components/price-change-confirm.d.ts +18 -0
- package/es/components/price-change-confirm.js +107 -0
- package/es/components/quote-details-panel.d.ts +21 -0
- package/es/components/quote-details-panel.js +170 -0
- package/es/components/quote-lock-banner.d.ts +7 -0
- package/es/components/quote-lock-banner.js +79 -0
- package/es/components/slippage-config.d.ts +20 -0
- package/es/components/slippage-config.js +261 -0
- package/es/history/invoice/list.js +125 -15
- package/es/hooks/dynamic-pricing.d.ts +102 -0
- package/es/hooks/dynamic-pricing.js +393 -0
- package/es/index.d.ts +6 -1
- package/es/index.js +9 -1
- package/es/libs/util.d.ts +42 -5
- package/es/libs/util.js +345 -57
- package/es/locales/en.js +114 -3
- package/es/locales/zh.js +114 -3
- package/es/payment/form/index.d.ts +4 -1
- package/es/payment/form/index.js +454 -22
- package/es/payment/index.d.ts +1 -1
- package/es/payment/index.js +279 -16
- package/es/payment/product-item.d.ts +26 -1
- package/es/payment/product-item.js +330 -51
- package/es/payment/summary-section/promotion-section.d.ts +32 -0
- package/es/payment/summary-section/promotion-section.js +143 -0
- package/es/payment/summary-section/total-section.d.ts +39 -0
- package/es/payment/summary-section/total-section.js +83 -0
- package/es/payment/summary.d.ts +17 -2
- package/es/payment/summary.js +300 -253
- package/es/types/index.d.ts +11 -0
- package/lib/components/auto-topup/modal.d.ts +2 -0
- package/lib/components/auto-topup/modal.js +54 -6
- package/lib/components/auto-topup/product-card.d.ts +16 -1
- package/lib/components/auto-topup/product-card.js +75 -7
- package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
- package/lib/components/dynamic-pricing-unavailable.js +81 -0
- package/lib/components/loading-amount.d.ts +17 -0
- package/lib/components/loading-amount.js +53 -0
- package/lib/components/price-change-confirm.d.ts +18 -0
- package/lib/components/price-change-confirm.js +157 -0
- package/lib/components/quote-details-panel.d.ts +21 -0
- package/lib/components/quote-details-panel.js +226 -0
- package/lib/components/quote-lock-banner.d.ts +7 -0
- package/lib/components/quote-lock-banner.js +93 -0
- package/lib/components/slippage-config.d.ts +20 -0
- package/lib/components/slippage-config.js +316 -0
- package/lib/history/invoice/list.js +167 -27
- package/lib/hooks/dynamic-pricing.d.ts +102 -0
- package/lib/hooks/dynamic-pricing.js +390 -0
- package/lib/index.d.ts +6 -1
- package/lib/index.js +32 -0
- package/lib/libs/util.d.ts +42 -5
- package/lib/libs/util.js +367 -49
- package/lib/locales/en.js +114 -3
- package/lib/locales/zh.js +114 -3
- package/lib/payment/form/index.d.ts +4 -1
- package/lib/payment/form/index.js +476 -20
- package/lib/payment/index.d.ts +1 -1
- package/lib/payment/index.js +308 -14
- package/lib/payment/product-item.d.ts +26 -1
- package/lib/payment/product-item.js +270 -35
- package/lib/payment/summary-section/promotion-section.d.ts +32 -0
- package/lib/payment/summary-section/promotion-section.js +133 -0
- package/lib/payment/summary-section/total-section.d.ts +39 -0
- package/lib/payment/summary-section/total-section.js +117 -0
- package/lib/payment/summary.d.ts +17 -2
- package/lib/payment/summary.js +205 -127
- package/lib/types/index.d.ts +11 -0
- package/package.json +3 -3
- package/src/components/auto-topup/modal.tsx +59 -6
- package/src/components/auto-topup/product-card.tsx +118 -11
- package/src/components/dynamic-pricing-unavailable.tsx +69 -0
- package/src/components/loading-amount.tsx +66 -0
- package/src/components/price-change-confirm.tsx +136 -0
- package/src/components/quote-details-panel.tsx +218 -0
- package/src/components/quote-lock-banner.tsx +99 -0
- package/src/components/slippage-config.tsx +336 -0
- package/src/history/invoice/list.tsx +143 -9
- package/src/hooks/dynamic-pricing.ts +617 -0
- package/src/index.ts +9 -0
- package/src/libs/util.ts +473 -58
- package/src/locales/en.tsx +117 -0
- package/src/locales/zh.tsx +111 -0
- package/src/payment/form/index.tsx +561 -19
- package/src/payment/index.tsx +349 -10
- package/src/payment/product-item.tsx +451 -37
- package/src/payment/summary-section/promotion-section.tsx +172 -0
- package/src/payment/summary-section/total-section.tsx +141 -0
- package/src/payment/summary.tsx +334 -192
- package/src/types/index.ts +15 -0
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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__ */
|
|
685
|
+
/* @__PURE__ */ jsxs(
|
|
579
686
|
Box,
|
|
580
687
|
{
|
|
581
688
|
sx: {
|
|
582
689
|
flex: 1,
|
|
583
690
|
textAlign: "right"
|
|
584
691
|
},
|
|
585
|
-
children:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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;
|