@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.
- 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
package/src/payment/summary.tsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
4
|
import type {
|
|
4
5
|
DonationSettings,
|
|
6
|
+
TCheckoutSession,
|
|
5
7
|
TLineItemExpanded,
|
|
6
8
|
TPaymentCurrency,
|
|
7
|
-
|
|
9
|
+
TPaymentIntent,
|
|
8
10
|
TPaymentMethodExpanded,
|
|
9
11
|
} from '@blocklet/payment-types';
|
|
10
|
-
import { HelpOutline
|
|
11
|
-
import { Box, Divider, Fade, Grow, Stack, Tooltip, Typography, Collapse, IconButton
|
|
12
|
+
import { HelpOutline } from '@mui/icons-material';
|
|
13
|
+
import { Box, Divider, Fade, Grow, Stack, Tooltip, Typography, Collapse, IconButton } from '@mui/material';
|
|
12
14
|
import type { IconButtonProps } from '@mui/material';
|
|
13
15
|
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
14
16
|
import { useRequest, useSetState } from 'ahooks';
|
|
@@ -16,42 +18,28 @@ import noop from 'lodash/noop';
|
|
|
16
18
|
import useBus from 'use-bus';
|
|
17
19
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
18
20
|
import { styled } from '@mui/material/styles';
|
|
21
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
19
22
|
import Status from '../components/status';
|
|
20
23
|
import api from '../libs/api';
|
|
21
24
|
import {
|
|
22
25
|
formatAmount,
|
|
23
26
|
formatCheckoutHeadlines,
|
|
24
27
|
getPriceUintAmountByCurrency,
|
|
25
|
-
|
|
26
|
-
formatNumber,
|
|
28
|
+
formatDynamicPrice,
|
|
27
29
|
findCurrency,
|
|
30
|
+
formatError,
|
|
28
31
|
} from '../libs/util';
|
|
29
|
-
import PaymentAmount from './amount';
|
|
30
32
|
import ProductDonation from './product-donation';
|
|
31
33
|
import ProductItem from './product-item';
|
|
32
34
|
import Livemode from '../components/livemode';
|
|
33
35
|
import { usePaymentContext } from '../contexts/payment';
|
|
34
36
|
import { useMobile } from '../hooks/mobile';
|
|
37
|
+
import { useDynamicPricing, type LiveRateInfo, type LiveQuoteSnapshot } from '../hooks/dynamic-pricing';
|
|
35
38
|
import LoadingButton from '../components/loading-button';
|
|
36
|
-
import
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// transform: rotate(0deg);
|
|
41
|
-
// }
|
|
42
|
-
// 25% {
|
|
43
|
-
// transform: rotate(2deg);
|
|
44
|
-
// }
|
|
45
|
-
// 50% {
|
|
46
|
-
// transform: rotate(0eg);
|
|
47
|
-
// }
|
|
48
|
-
// 75% {
|
|
49
|
-
// transform: rotate(-2deg);
|
|
50
|
-
// }
|
|
51
|
-
// 100% {
|
|
52
|
-
// transform: rotate(0deg);
|
|
53
|
-
// }
|
|
54
|
-
// `;
|
|
39
|
+
import DynamicPricingUnavailable from '../components/dynamic-pricing-unavailable';
|
|
40
|
+
import PromotionSection from './summary-section/promotion-section';
|
|
41
|
+
import TotalSection from './summary-section/total-section';
|
|
42
|
+
import type { SlippageConfigValue } from '../components/slippage-config';
|
|
55
43
|
|
|
56
44
|
interface ExpandMoreProps extends IconButtonProps {
|
|
57
45
|
expand: boolean;
|
|
@@ -83,13 +71,25 @@ type Props = {
|
|
|
83
71
|
onCancelCrossSell?: Function;
|
|
84
72
|
checkoutSessionId?: string;
|
|
85
73
|
crossSellBehavior?: string;
|
|
86
|
-
donationSettings?: DonationSettings;
|
|
74
|
+
donationSettings?: DonationSettings;
|
|
87
75
|
action?: string;
|
|
88
76
|
completed?: boolean;
|
|
89
77
|
checkoutSession?: TCheckoutSession;
|
|
78
|
+
paymentIntent?: TPaymentIntent | null;
|
|
90
79
|
onPromotionUpdate?: () => void;
|
|
91
80
|
paymentMethods?: TPaymentMethodExpanded[];
|
|
92
81
|
showFeatures?: boolean;
|
|
82
|
+
rateUnavailable?: boolean;
|
|
83
|
+
rateError?: string;
|
|
84
|
+
isRateLoading?: boolean; // Loading state for skeleton display during currency switch
|
|
85
|
+
onQuoteExpired?: (forceRefresh?: boolean) => void;
|
|
86
|
+
onRefreshRate?: () => Promise<void>;
|
|
87
|
+
onSlippageChange?: (slippageConfig: SlippageConfigValue) => void;
|
|
88
|
+
slippageConfig?: SlippageConfigValue;
|
|
89
|
+
liveRate?: LiveRateInfo;
|
|
90
|
+
liveQuoteSnapshot?: LiveQuoteSnapshot;
|
|
91
|
+
isStripePayment?: boolean;
|
|
92
|
+
isSubscription?: boolean;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
async function fetchCrossSell(id: string, skipError = true) {
|
|
@@ -98,7 +98,6 @@ async function fetchCrossSell(id: string, skipError = true) {
|
|
|
98
98
|
if (!data.error) {
|
|
99
99
|
return data;
|
|
100
100
|
}
|
|
101
|
-
|
|
102
101
|
return null;
|
|
103
102
|
} catch (err) {
|
|
104
103
|
return null;
|
|
@@ -139,6 +138,7 @@ function getStakingSetup(items: TLineItemExpanded[], currency: TPaymentCurrency,
|
|
|
139
138
|
|
|
140
139
|
return '0';
|
|
141
140
|
}
|
|
141
|
+
|
|
142
142
|
export default function PaymentSummary({
|
|
143
143
|
items,
|
|
144
144
|
currency,
|
|
@@ -158,9 +158,22 @@ export default function PaymentSummary({
|
|
|
158
158
|
trialEnd = 0,
|
|
159
159
|
completed = false,
|
|
160
160
|
checkoutSession = undefined,
|
|
161
|
+
paymentIntent = undefined,
|
|
161
162
|
paymentMethods = [],
|
|
162
163
|
onPromotionUpdate = noop,
|
|
163
164
|
showFeatures = false,
|
|
165
|
+
rateUnavailable = false,
|
|
166
|
+
isRateLoading = false,
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
168
|
+
rateError: _rateError = undefined, // Technical errors are logged but not displayed to users
|
|
169
|
+
onQuoteExpired = undefined,
|
|
170
|
+
onRefreshRate = undefined,
|
|
171
|
+
onSlippageChange = undefined,
|
|
172
|
+
slippageConfig: slippageConfigProp = undefined,
|
|
173
|
+
liveRate = undefined,
|
|
174
|
+
liveQuoteSnapshot = undefined,
|
|
175
|
+
isStripePayment = false,
|
|
176
|
+
isSubscription: isSubscriptionProp = undefined,
|
|
164
177
|
...rest
|
|
165
178
|
}: Props) {
|
|
166
179
|
const { t, locale } = useLocaleContext();
|
|
@@ -171,10 +184,12 @@ export default function PaymentSummary({
|
|
|
171
184
|
checkoutSessionId ? fetchCrossSell(checkoutSessionId, skipError) : Promise.resolve(null)
|
|
172
185
|
);
|
|
173
186
|
|
|
187
|
+
// Discounts and promotion codes
|
|
174
188
|
const sessionDiscounts = (checkoutSession as any)?.discounts || [];
|
|
175
189
|
const allowPromotionCodes = !!checkoutSession?.allow_promotion_codes;
|
|
176
190
|
const hasDiscounts = sessionDiscounts?.length > 0;
|
|
177
191
|
|
|
192
|
+
// Resolve discount currency
|
|
178
193
|
const discountCurrency =
|
|
179
194
|
paymentMethods && checkoutSession
|
|
180
195
|
? (findCurrency(
|
|
@@ -182,26 +197,183 @@ export default function PaymentSummary({
|
|
|
182
197
|
hasDiscounts ? checkoutSession?.currency_id || currency.id : (currency.id as string)
|
|
183
198
|
) as TPaymentCurrency) || settings.settings?.baseCurrency
|
|
184
199
|
: currency;
|
|
200
|
+
const slippageConfig = slippageConfigProp ?? (checkoutSession as any)?.metadata?.slippage;
|
|
201
|
+
|
|
202
|
+
// Use the dynamic pricing hook for all rate-related calculations
|
|
203
|
+
const {
|
|
204
|
+
hasDynamicPricing,
|
|
205
|
+
isPriceLocked,
|
|
206
|
+
quoteMeta,
|
|
207
|
+
rateInfo,
|
|
208
|
+
quoteLockedAt,
|
|
209
|
+
currentSlippagePercent,
|
|
210
|
+
rateDisplay,
|
|
211
|
+
calculatedTokenAmount,
|
|
212
|
+
calculatedDiscountAmount,
|
|
213
|
+
calculateUsdDisplay,
|
|
214
|
+
buildQuoteDetailRows,
|
|
215
|
+
} = useDynamicPricing({
|
|
216
|
+
items,
|
|
217
|
+
currency: discountCurrency,
|
|
218
|
+
liveRate,
|
|
219
|
+
liveQuoteSnapshot,
|
|
220
|
+
checkoutSession,
|
|
221
|
+
paymentIntent,
|
|
222
|
+
locale,
|
|
223
|
+
isStripePayment,
|
|
224
|
+
isSubscription: isSubscriptionProp,
|
|
225
|
+
slippageConfig,
|
|
226
|
+
trialInDays,
|
|
227
|
+
trialEnd,
|
|
228
|
+
discounts: sessionDiscounts,
|
|
229
|
+
});
|
|
185
230
|
|
|
186
|
-
|
|
231
|
+
// Headlines and amount calculations
|
|
232
|
+
const headlines = formatCheckoutHeadlines(items, discountCurrency, { trialEnd, trialInDays }, locale, {
|
|
233
|
+
exchangeRate: rateInfo.exchangeRate,
|
|
234
|
+
});
|
|
187
235
|
const staking = showStaking ? getStakingSetup(items, discountCurrency, billingThreshold) : '0';
|
|
188
236
|
|
|
189
|
-
|
|
190
|
-
|
|
237
|
+
// For Stripe payments, always use standard formatting regardless of dynamic pricing
|
|
238
|
+
// This prevents calculation errors when switching from crypto to fiat payments
|
|
239
|
+
const effectiveHasDynamicPricing = hasDynamicPricing && !isStripePayment;
|
|
191
240
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
241
|
+
// Check if this is a trial scenario: actualAmount is '0' AND it's a subscription
|
|
242
|
+
// Only subscriptions (recurring items) have trials - one_time products should still be charged
|
|
243
|
+
const hasRecurringItems = items.some((x) => (x.upsell_price || x.price)?.type === 'recurring');
|
|
244
|
+
const isTrialScenario = headlines.actualAmount === '0' && hasRecurringItems;
|
|
245
|
+
|
|
246
|
+
const headlineAmountDisplay = useMemo(() => {
|
|
247
|
+
// For Stripe payments, use standard formatting regardless of dynamic pricing
|
|
248
|
+
if (!effectiveHasDynamicPricing) {
|
|
249
|
+
return headlines.amount;
|
|
250
|
+
}
|
|
251
|
+
// For trial scenarios, use the formatted trial amount (e.g., "Free 7-day trial")
|
|
252
|
+
if (isTrialScenario || !headlines.amount.includes(discountCurrency.symbol)) {
|
|
253
|
+
return headlines.amount;
|
|
254
|
+
}
|
|
255
|
+
if (calculatedTokenAmount) {
|
|
256
|
+
const displayAmount = fromUnitToToken(calculatedTokenAmount, discountCurrency?.decimal);
|
|
257
|
+
const formatted = formatDynamicPrice(displayAmount, true, 6);
|
|
258
|
+
return `${formatted} ${discountCurrency.symbol}`;
|
|
259
|
+
}
|
|
260
|
+
const formatted = formatDynamicPrice(headlines.actualAmount, true, 6);
|
|
261
|
+
return `${formatted} ${discountCurrency.symbol}`;
|
|
262
|
+
}, [
|
|
263
|
+
headlines.amount,
|
|
264
|
+
headlines.actualAmount,
|
|
265
|
+
discountCurrency.symbol,
|
|
266
|
+
discountCurrency?.decimal,
|
|
267
|
+
effectiveHasDynamicPricing,
|
|
268
|
+
calculatedTokenAmount,
|
|
269
|
+
isTrialScenario,
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
// Amount calculations - wrap discountAmount in useMemo to avoid dependency warning
|
|
273
|
+
const discountAmount = useMemo(
|
|
274
|
+
() => new BN(checkoutSession?.total_details?.amount_discount || '0'),
|
|
275
|
+
[checkoutSession?.total_details?.amount_discount]
|
|
276
|
+
);
|
|
277
|
+
const subtotalAmountUnit = new BN(fromTokenToUnit(headlines.actualAmount, discountCurrency?.decimal))
|
|
278
|
+
.add(new BN(staking))
|
|
279
|
+
.toString();
|
|
280
|
+
const subtotalAmount = fromUnitToToken(subtotalAmountUnit, discountCurrency?.decimal);
|
|
281
|
+
const totalAmountUnit = new BN(subtotalAmountUnit).sub(discountAmount).toString();
|
|
282
|
+
const totalAmountValue = fromUnitToToken(totalAmountUnit, discountCurrency?.decimal);
|
|
283
|
+
|
|
284
|
+
// Format displays - for dynamic pricing, include staking in the calculated total
|
|
285
|
+
// For trial scenarios, don't add calculatedTokenAmount since payment amount is 0
|
|
286
|
+
const subtotalDisplay = useMemo(() => {
|
|
287
|
+
if (effectiveHasDynamicPricing && calculatedTokenAmount && !isTrialScenario) {
|
|
288
|
+
const dynamicSubtotalUnit = new BN(calculatedTokenAmount).add(new BN(staking)).toString();
|
|
289
|
+
const displayAmount = fromUnitToToken(dynamicSubtotalUnit, discountCurrency?.decimal);
|
|
290
|
+
return formatDynamicPrice(displayAmount, true, 6);
|
|
291
|
+
}
|
|
292
|
+
return formatDynamicPrice(subtotalAmount, effectiveHasDynamicPricing, 6);
|
|
293
|
+
}, [
|
|
294
|
+
effectiveHasDynamicPricing,
|
|
295
|
+
calculatedTokenAmount,
|
|
296
|
+
staking,
|
|
297
|
+
discountCurrency?.decimal,
|
|
298
|
+
subtotalAmount,
|
|
299
|
+
isTrialScenario,
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
const totalAmountDisplay = useMemo(() => {
|
|
303
|
+
// For trial scenarios, don't add calculatedTokenAmount since payment amount is 0
|
|
304
|
+
if (effectiveHasDynamicPricing && calculatedTokenAmount && !isTrialScenario) {
|
|
305
|
+
// Use calculatedDiscountAmount (frontend) instead of discountAmount (backend)
|
|
306
|
+
// Backend discountAmount is stale when exchange rate changes
|
|
307
|
+
const effectiveDiscount = calculatedDiscountAmount ? new BN(calculatedDiscountAmount) : discountAmount;
|
|
308
|
+
const dynamicTotalUnit = new BN(calculatedTokenAmount).add(new BN(staking)).sub(effectiveDiscount).toString();
|
|
309
|
+
const displayAmount = fromUnitToToken(dynamicTotalUnit, discountCurrency?.decimal);
|
|
310
|
+
const numericValue = Number(displayAmount);
|
|
311
|
+
if (Number.isFinite(numericValue) && numericValue >= 0) {
|
|
312
|
+
return formatDynamicPrice(displayAmount, true, 6);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// For Stripe payments: use calculatedDiscountAmount when backend discountAmount is 0 or stale
|
|
317
|
+
// This ensures correct total when discount is applied but backend hasn't recalculated
|
|
318
|
+
if (isStripePayment && calculatedDiscountAmount && !isTrialScenario) {
|
|
319
|
+
const effectiveDiscount = new BN(calculatedDiscountAmount);
|
|
320
|
+
const adjustedTotalUnit = new BN(subtotalAmountUnit).sub(effectiveDiscount).toString();
|
|
321
|
+
const displayAmount = fromUnitToToken(adjustedTotalUnit, discountCurrency?.decimal);
|
|
322
|
+
const numericValue = Number(displayAmount);
|
|
323
|
+
if (Number.isFinite(numericValue) && numericValue >= 0) {
|
|
324
|
+
return formatDynamicPrice(displayAmount, false, 6);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return formatDynamicPrice(totalAmountValue, effectiveHasDynamicPricing, 6);
|
|
329
|
+
}, [
|
|
330
|
+
effectiveHasDynamicPricing,
|
|
331
|
+
calculatedTokenAmount,
|
|
332
|
+
staking,
|
|
333
|
+
discountAmount,
|
|
334
|
+
calculatedDiscountAmount,
|
|
335
|
+
discountCurrency?.decimal,
|
|
336
|
+
totalAmountValue,
|
|
337
|
+
isTrialScenario,
|
|
338
|
+
isStripePayment,
|
|
339
|
+
subtotalAmountUnit,
|
|
340
|
+
]);
|
|
341
|
+
const totalAmountText = totalAmountDisplay === '—' ? '—' : `${totalAmountDisplay} ${discountCurrency.symbol}`;
|
|
342
|
+
|
|
343
|
+
// Fix: Use dynamically calculated total for USD display, not static totalAmountValue
|
|
344
|
+
// For Stripe payments, no USD equivalent display is needed since it's already USD
|
|
345
|
+
// For trial scenarios, don't add calculatedTokenAmount since payment amount is 0
|
|
346
|
+
const totalUsdDisplay = useMemo(() => {
|
|
347
|
+
if (effectiveHasDynamicPricing && calculatedTokenAmount && !isTrialScenario) {
|
|
348
|
+
// Use calculatedDiscountAmount (frontend) instead of discountAmount (backend)
|
|
349
|
+
const effectiveDiscount = calculatedDiscountAmount ? new BN(calculatedDiscountAmount) : discountAmount;
|
|
350
|
+
const dynamicTotalUnit = new BN(calculatedTokenAmount).add(new BN(staking)).sub(effectiveDiscount).toString();
|
|
351
|
+
const dynamicTotalToken = fromUnitToToken(dynamicTotalUnit, discountCurrency?.decimal);
|
|
352
|
+
return calculateUsdDisplay(dynamicTotalToken);
|
|
353
|
+
}
|
|
354
|
+
return calculateUsdDisplay(totalAmountValue);
|
|
355
|
+
}, [
|
|
356
|
+
effectiveHasDynamicPricing,
|
|
357
|
+
calculatedTokenAmount,
|
|
358
|
+
staking,
|
|
359
|
+
discountAmount,
|
|
360
|
+
calculatedDiscountAmount,
|
|
361
|
+
discountCurrency?.decimal,
|
|
362
|
+
totalAmountValue,
|
|
363
|
+
calculateUsdDisplay,
|
|
364
|
+
isTrialScenario,
|
|
365
|
+
]);
|
|
198
366
|
|
|
367
|
+
const quoteDetailRows = buildQuoteDetailRows(t);
|
|
368
|
+
const isSubscription =
|
|
369
|
+
isSubscriptionProp ?? (checkoutSession?.mode === 'subscription' || checkoutSession?.mode === 'setup');
|
|
370
|
+
|
|
371
|
+
// Promotion handlers
|
|
199
372
|
const handlePromotionUpdate = () => {
|
|
200
373
|
onPromotionUpdate?.();
|
|
201
374
|
};
|
|
202
375
|
|
|
203
376
|
const handleRemovePromotion = async (sessionId: string) => {
|
|
204
|
-
// Prevent removing promotion during payment process
|
|
205
377
|
if (paymentState.paying || paymentState.stripePaying) {
|
|
206
378
|
return;
|
|
207
379
|
}
|
|
@@ -213,18 +385,54 @@ export default function PaymentSummary({
|
|
|
213
385
|
}
|
|
214
386
|
};
|
|
215
387
|
|
|
216
|
-
|
|
388
|
+
// Quote expiry handling (backward compatibility - Final Freeze deprecates this)
|
|
389
|
+
const expiredHandledRef = useRef(false);
|
|
217
390
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (completed || expiredHandledRef.current) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// In Final Freeze, there's no quote during preview, so no expiry check needed
|
|
396
|
+
if (!liveQuoteSnapshot?.expires_at && !quoteMeta?.expiresAt) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
400
|
+
const effectiveExpiresAt = liveQuoteSnapshot?.expires_at ?? quoteMeta?.expiresAt;
|
|
401
|
+
const quoteRemaining = effectiveExpiresAt ? Math.max(0, effectiveExpiresAt - currentTime) : 0;
|
|
402
|
+
const lockRemaining = quoteLockedAt ? Math.max(0, quoteLockedAt + 180 - currentTime) : 0;
|
|
403
|
+
const hasExpiry = !!effectiveExpiresAt;
|
|
222
404
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
405
|
+
const lockActive = lockRemaining > 0;
|
|
406
|
+
if (hasExpiry && !lockActive && quoteRemaining <= 0) {
|
|
407
|
+
expiredHandledRef.current = true;
|
|
408
|
+
onQuoteExpired?.();
|
|
409
|
+
}
|
|
410
|
+
}, [liveQuoteSnapshot?.expires_at, quoteMeta?.expiresAt, quoteLockedAt, completed, onQuoteExpired]);
|
|
411
|
+
|
|
412
|
+
// Slippage handler
|
|
413
|
+
const handleSlippageChange = async (newSlippageConfig: SlippageConfigValue) => {
|
|
414
|
+
if (!onSlippageChange) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!checkoutSessionId) {
|
|
418
|
+
onSlippageChange(newSlippageConfig);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
await api.put(`/api/checkout-sessions/${checkoutSessionId}/slippage`, {
|
|
423
|
+
slippage_config: newSlippageConfig,
|
|
424
|
+
});
|
|
425
|
+
onSlippageChange(newSlippageConfig);
|
|
426
|
+
if (onQuoteExpired) {
|
|
427
|
+
await onQuoteExpired(true);
|
|
428
|
+
}
|
|
429
|
+
} catch (err: any) {
|
|
430
|
+
console.error('Failed to update slippage', err);
|
|
431
|
+
Toast.error(err.response?.data?.error || formatError(err));
|
|
432
|
+
}
|
|
433
|
+
};
|
|
227
434
|
|
|
435
|
+
// Cross-sell event handling
|
|
228
436
|
useBus(
|
|
229
437
|
'error.REQUIRE_CROSS_SELL',
|
|
230
438
|
() => {
|
|
@@ -236,6 +444,7 @@ export default function PaymentSummary({
|
|
|
236
444
|
[]
|
|
237
445
|
);
|
|
238
446
|
|
|
447
|
+
// Product action handlers
|
|
239
448
|
const handleUpsell = async (from: string, to: string) => {
|
|
240
449
|
await onUpsell!(from, to);
|
|
241
450
|
runAsync(false);
|
|
@@ -276,6 +485,8 @@ export default function PaymentSummary({
|
|
|
276
485
|
};
|
|
277
486
|
|
|
278
487
|
const hasSubTotal = +staking > 0 || allowPromotionCodes;
|
|
488
|
+
|
|
489
|
+
// Product list component
|
|
279
490
|
const ProductCardList = (
|
|
280
491
|
<Stack
|
|
281
492
|
className="cko-product-list"
|
|
@@ -285,7 +496,7 @@ export default function PaymentSummary({
|
|
|
285
496
|
}}>
|
|
286
497
|
<Stack spacing={{ xs: 1, sm: 2 }}>
|
|
287
498
|
{items.map((x: TLineItemExpanded) =>
|
|
288
|
-
x.price
|
|
499
|
+
x.price?.custom_unit_amount && onChangeAmount && donationSettings ? (
|
|
289
500
|
<ProductDonation
|
|
290
501
|
key={`${x.price_id}-${discountCurrency.id}`}
|
|
291
502
|
item={x}
|
|
@@ -301,12 +512,20 @@ export default function PaymentSummary({
|
|
|
301
512
|
trialInDays={trialInDays}
|
|
302
513
|
trialEnd={trialEnd}
|
|
303
514
|
currency={discountCurrency}
|
|
515
|
+
exchangeRate={rateInfo.exchangeRate}
|
|
516
|
+
isStripePayment={isStripePayment}
|
|
517
|
+
isPriceLocked={isPriceLocked}
|
|
518
|
+
isRateLoading={isRateLoading}
|
|
304
519
|
onUpsell={handleUpsell}
|
|
305
520
|
onDownsell={handleDownsell}
|
|
306
521
|
adjustableQuantity={x.adjustable_quantity}
|
|
307
522
|
completed={completed}
|
|
308
523
|
showFeatures={showFeatures}
|
|
309
|
-
onQuantityChange={handleQuantityChange}
|
|
524
|
+
onQuantityChange={handleQuantityChange}
|
|
525
|
+
discounts={sessionDiscounts}
|
|
526
|
+
calculatedDiscountAmount={
|
|
527
|
+
calculatedDiscountAmount || (isStripePayment ? discountAmount.toString() : null)
|
|
528
|
+
}>
|
|
310
529
|
{x.cross_sell && (
|
|
311
530
|
<Stack
|
|
312
531
|
direction="row"
|
|
@@ -334,16 +553,17 @@ export default function PaymentSummary({
|
|
|
334
553
|
</Stack>
|
|
335
554
|
{data && items.some((x) => x.price_id === data.id) === false && (
|
|
336
555
|
<Grow in>
|
|
337
|
-
<Stack
|
|
338
|
-
sx={{
|
|
339
|
-
mt: 1,
|
|
340
|
-
}}>
|
|
556
|
+
<Stack sx={{ mt: 1 }}>
|
|
341
557
|
<ProductItem
|
|
342
558
|
item={{ quantity: 1, price: data, price_id: data.id, cross_sell: true } as TLineItemExpanded}
|
|
343
559
|
items={items}
|
|
344
560
|
trialInDays={trialInDays}
|
|
345
561
|
currency={discountCurrency}
|
|
346
562
|
trialEnd={trialEnd}
|
|
563
|
+
exchangeRate={rateInfo.exchangeRate}
|
|
564
|
+
isStripePayment={isStripePayment}
|
|
565
|
+
isPriceLocked={isPriceLocked}
|
|
566
|
+
isRateLoading={isRateLoading}
|
|
347
567
|
onUpsell={noop}
|
|
348
568
|
onDownsell={noop}>
|
|
349
569
|
<Stack
|
|
@@ -375,9 +595,15 @@ export default function PaymentSummary({
|
|
|
375
595
|
)}
|
|
376
596
|
</Stack>
|
|
377
597
|
);
|
|
598
|
+
|
|
599
|
+
if (!discountCurrency || !items?.length) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
378
603
|
return (
|
|
379
604
|
<Fade in>
|
|
380
605
|
<Stack className="cko-product" direction="column" {...rest}>
|
|
606
|
+
{/* Header */}
|
|
381
607
|
<Box
|
|
382
608
|
sx={{
|
|
383
609
|
display: 'flex',
|
|
@@ -388,10 +614,7 @@ export default function PaymentSummary({
|
|
|
388
614
|
title={t('payment.checkout.orderSummary')}
|
|
389
615
|
sx={{
|
|
390
616
|
color: 'text.primary',
|
|
391
|
-
fontSize: {
|
|
392
|
-
xs: '18px',
|
|
393
|
-
md: '24px',
|
|
394
|
-
},
|
|
617
|
+
fontSize: { xs: '18px', md: '24px' },
|
|
395
618
|
fontWeight: '700',
|
|
396
619
|
lineHeight: '32px',
|
|
397
620
|
}}>
|
|
@@ -399,6 +622,13 @@ export default function PaymentSummary({
|
|
|
399
622
|
</Typography>
|
|
400
623
|
{!settings.livemode && <Livemode />}
|
|
401
624
|
</Box>
|
|
625
|
+
|
|
626
|
+
{/* Dynamic pricing unavailable warning - hide for Stripe payments since no exchange rate needed */}
|
|
627
|
+
{effectiveHasDynamicPricing && rateUnavailable && (
|
|
628
|
+
<DynamicPricingUnavailable sx={{ mb: 2 }} onRetry={onRefreshRate} />
|
|
629
|
+
)}
|
|
630
|
+
|
|
631
|
+
{/* Product list (collapsible on mobile) */}
|
|
402
632
|
{isMobile && !donationSettings ? (
|
|
403
633
|
<>
|
|
404
634
|
<Stack
|
|
@@ -421,42 +651,23 @@ export default function PaymentSummary({
|
|
|
421
651
|
) : (
|
|
422
652
|
ProductCardList
|
|
423
653
|
)}
|
|
654
|
+
|
|
424
655
|
<Divider sx={{ mt: 2.5, mb: 2.5 }} />
|
|
656
|
+
|
|
657
|
+
{/* Staking section */}
|
|
425
658
|
{+staking > 0 && (
|
|
426
659
|
<Stack spacing={1}>
|
|
427
|
-
<Stack
|
|
428
|
-
direction="row"
|
|
429
|
-
spacing={1}
|
|
430
|
-
sx={{
|
|
431
|
-
justifyContent: 'space-between',
|
|
432
|
-
alignItems: 'center',
|
|
433
|
-
}}>
|
|
434
|
-
<Stack
|
|
435
|
-
direction="row"
|
|
436
|
-
spacing={0.5}
|
|
437
|
-
sx={{
|
|
438
|
-
alignItems: 'center',
|
|
439
|
-
}}>
|
|
660
|
+
<Stack direction="row" spacing={1} sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
|
661
|
+
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
|
|
440
662
|
<Typography sx={{ color: 'text.secondary' }}>{t('payment.checkout.paymentRequired')}</Typography>
|
|
441
663
|
<Tooltip title={t('payment.checkout.stakingConfirm')} placement="top" sx={{ maxWidth: '150px' }}>
|
|
442
664
|
<HelpOutline fontSize="small" sx={{ color: 'text.lighter' }} />
|
|
443
665
|
</Tooltip>
|
|
444
666
|
</Stack>
|
|
445
|
-
<Typography>{
|
|
667
|
+
<Typography>{headlineAmountDisplay}</Typography>
|
|
446
668
|
</Stack>
|
|
447
|
-
<Stack
|
|
448
|
-
direction="row"
|
|
449
|
-
spacing={1}
|
|
450
|
-
sx={{
|
|
451
|
-
justifyContent: 'space-between',
|
|
452
|
-
alignItems: 'center',
|
|
453
|
-
}}>
|
|
454
|
-
<Stack
|
|
455
|
-
direction="row"
|
|
456
|
-
spacing={0.5}
|
|
457
|
-
sx={{
|
|
458
|
-
alignItems: 'center',
|
|
459
|
-
}}>
|
|
669
|
+
<Stack direction="row" spacing={1} sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
|
670
|
+
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
|
|
460
671
|
<Typography sx={{ color: 'text.secondary' }}>{t('payment.checkout.staking.title')}</Typography>
|
|
461
672
|
<Tooltip title={t('payment.checkout.staking.tooltip')} placement="top" sx={{ maxWidth: '150px' }}>
|
|
462
673
|
<HelpOutline fontSize="small" sx={{ color: 'text.lighter' }} />
|
|
@@ -468,6 +679,8 @@ export default function PaymentSummary({
|
|
|
468
679
|
</Stack>
|
|
469
680
|
</Stack>
|
|
470
681
|
)}
|
|
682
|
+
|
|
683
|
+
{/* Subtotal row */}
|
|
471
684
|
{(allowPromotionCodes || hasDiscounts) && (
|
|
472
685
|
<Stack
|
|
473
686
|
direction="row"
|
|
@@ -484,120 +697,49 @@ export default function PaymentSummary({
|
|
|
484
697
|
}}>
|
|
485
698
|
<Typography className="base-label">{t('common.subtotal')}</Typography>
|
|
486
699
|
<Typography>
|
|
487
|
-
{
|
|
700
|
+
{subtotalDisplay} {discountCurrency.symbol}
|
|
488
701
|
</Typography>
|
|
489
702
|
</Stack>
|
|
490
703
|
)}
|
|
491
|
-
{/* Promotion Code Section - only show add button if no discounts applied */}
|
|
492
|
-
{allowPromotionCodes && !hasDiscounts && (
|
|
493
|
-
<Box sx={{ mt: 1 }}>
|
|
494
|
-
<PromotionCode
|
|
495
|
-
checkoutSessionId={checkoutSession.id}
|
|
496
|
-
initialAppliedCodes={getAppliedPromotionCodes()}
|
|
497
|
-
disabled={completed}
|
|
498
|
-
onUpdate={handlePromotionUpdate}
|
|
499
|
-
currencyId={currency.id}
|
|
500
|
-
/>
|
|
501
|
-
</Box>
|
|
502
|
-
)}
|
|
503
704
|
|
|
504
|
-
{/* Promotion
|
|
505
|
-
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}}>
|
|
522
|
-
<Stack
|
|
523
|
-
direction="row"
|
|
524
|
-
spacing={1}
|
|
525
|
-
sx={{
|
|
526
|
-
alignItems: 'center',
|
|
527
|
-
backgroundColor: 'grey.100',
|
|
528
|
-
width: 'fit-content',
|
|
529
|
-
px: 1,
|
|
530
|
-
py: 1,
|
|
531
|
-
borderRadius: 1,
|
|
532
|
-
}}>
|
|
533
|
-
<Typography
|
|
534
|
-
sx={{
|
|
535
|
-
fontWeight: 'medium',
|
|
536
|
-
display: 'flex',
|
|
537
|
-
alignItems: 'center',
|
|
538
|
-
gap: 0.5,
|
|
539
|
-
}}>
|
|
540
|
-
<LocalOffer sx={{ color: 'warning.main', fontSize: 'small' }} />
|
|
541
|
-
{promotionCodeInfo?.code || discount.verification_data?.code || t('payment.checkout.discount')}
|
|
542
|
-
</Typography>
|
|
543
|
-
{!completed && (
|
|
544
|
-
<Button
|
|
545
|
-
size="small"
|
|
546
|
-
disabled={paymentState.paying || paymentState.stripePaying}
|
|
547
|
-
onClick={() => handleRemovePromotion(checkoutSessionId)}
|
|
548
|
-
sx={{
|
|
549
|
-
minWidth: 'auto',
|
|
550
|
-
width: 16,
|
|
551
|
-
height: 16,
|
|
552
|
-
color: 'text.secondary',
|
|
553
|
-
'&.Mui-disabled': {
|
|
554
|
-
color: 'text.disabled',
|
|
555
|
-
},
|
|
556
|
-
}}>
|
|
557
|
-
<Close sx={{ fontSize: 14 }} />
|
|
558
|
-
</Button>
|
|
559
|
-
)}
|
|
560
|
-
</Stack>
|
|
561
|
-
<Typography sx={{ color: 'text.secondary' }}>
|
|
562
|
-
-{formatAmount(discount.discount_amount || '0', discountCurrency.decimal)}{' '}
|
|
563
|
-
{discountCurrency.symbol}
|
|
564
|
-
</Typography>
|
|
565
|
-
</Stack>
|
|
566
|
-
{/* Show discount description */}
|
|
567
|
-
{discountDescription && (
|
|
568
|
-
<Typography
|
|
569
|
-
sx={{
|
|
570
|
-
fontSize: 'small',
|
|
571
|
-
color: notSupported ? 'error.main' : 'text.secondary',
|
|
572
|
-
mt: 0.5,
|
|
573
|
-
}}>
|
|
574
|
-
{discountDescription}
|
|
575
|
-
</Typography>
|
|
576
|
-
)}
|
|
577
|
-
</Stack>
|
|
578
|
-
);
|
|
579
|
-
})}
|
|
580
|
-
</Box>
|
|
581
|
-
)}
|
|
705
|
+
{/* Promotion section */}
|
|
706
|
+
{/* For Stripe payments, use total_details.amount_discount (backend calculated) */}
|
|
707
|
+
{/* For dynamic pricing, use calculatedDiscountAmount (frontend calculated based on rate) */}
|
|
708
|
+
<PromotionSection
|
|
709
|
+
checkoutSessionId={checkoutSession?.id || checkoutSessionId}
|
|
710
|
+
currency={discountCurrency}
|
|
711
|
+
currencyId={currency.id}
|
|
712
|
+
discounts={sessionDiscounts}
|
|
713
|
+
allowPromotionCodes={allowPromotionCodes}
|
|
714
|
+
completed={completed}
|
|
715
|
+
disabled={paymentState.paying || paymentState.stripePaying}
|
|
716
|
+
onPromotionUpdate={handlePromotionUpdate}
|
|
717
|
+
onRemovePromotion={handleRemovePromotion}
|
|
718
|
+
calculatedDiscountAmount={calculatedDiscountAmount || (isStripePayment ? discountAmount.toString() : null)}
|
|
719
|
+
isRateLoading={isRateLoading}
|
|
720
|
+
/>
|
|
721
|
+
|
|
582
722
|
{hasSubTotal && <Divider sx={{ my: 1 }} />}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
723
|
+
|
|
724
|
+
{/* Total section */}
|
|
725
|
+
<TotalSection
|
|
726
|
+
totalAmountText={totalAmountText}
|
|
727
|
+
totalUsdDisplay={totalUsdDisplay}
|
|
728
|
+
currency={discountCurrency}
|
|
729
|
+
hasDynamicPricing={hasDynamicPricing}
|
|
730
|
+
rateDisplay={rateDisplay}
|
|
731
|
+
rateInfo={rateInfo}
|
|
732
|
+
quoteDetailRows={quoteDetailRows}
|
|
733
|
+
currentSlippagePercent={currentSlippagePercent}
|
|
734
|
+
slippageConfig={slippageConfig}
|
|
735
|
+
isPriceLocked={isPriceLocked}
|
|
736
|
+
isSubscription={isSubscription}
|
|
737
|
+
completed={completed}
|
|
738
|
+
onSlippageChange={onSlippageChange ? handleSlippageChange : undefined}
|
|
739
|
+
isStripePayment={isStripePayment}
|
|
740
|
+
isRateLoading={isRateLoading}
|
|
741
|
+
thenInfo={headlines.thenValue && headlines.showThen ? headlines.thenValue : undefined}
|
|
742
|
+
/>
|
|
601
743
|
</Stack>
|
|
602
744
|
</Fade>
|
|
603
745
|
);
|