@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
|
@@ -4,7 +4,8 @@ import { Box, Stack, Typography, IconButton, TextField, Alert, Chip } from "@mui
|
|
|
4
4
|
import { Add, Remove, LocalOffer } from "@mui/icons-material";
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { useRequest } from "ahooks";
|
|
7
|
-
import { BN, fromTokenToUnit } from "@ocap/util";
|
|
7
|
+
import { BN, fromTokenToUnit, fromUnitToToken } from "@ocap/util";
|
|
8
|
+
import LoadingAmount from "../components/loading-amount.js";
|
|
8
9
|
import Status from "../components/status.js";
|
|
9
10
|
import Switch from "../components/switch-button.js";
|
|
10
11
|
import {
|
|
@@ -18,7 +19,10 @@ import {
|
|
|
18
19
|
formatAmount,
|
|
19
20
|
formatCreditForCheckout,
|
|
20
21
|
formatCreditAmount,
|
|
21
|
-
formatBNStr
|
|
22
|
+
formatBNStr,
|
|
23
|
+
getUsdAmountFromBaseAmount,
|
|
24
|
+
formatUsdAmount,
|
|
25
|
+
formatDynamicPrice
|
|
22
26
|
} from "../libs/util.js";
|
|
23
27
|
import ProductCard from "./product-card.js";
|
|
24
28
|
import dayjs from "../libs/dayjs.js";
|
|
@@ -62,14 +66,23 @@ export default function ProductItem({
|
|
|
62
66
|
adjustableQuantity = { enabled: false },
|
|
63
67
|
onQuantityChange = () => {
|
|
64
68
|
},
|
|
65
|
-
showFeatures = false
|
|
69
|
+
showFeatures = false,
|
|
70
|
+
exchangeRate = null,
|
|
71
|
+
isStripePayment = false,
|
|
72
|
+
isPriceLocked = false,
|
|
73
|
+
isRateLoading = false,
|
|
74
|
+
discounts = [],
|
|
75
|
+
calculatedDiscountAmount = null
|
|
66
76
|
}) {
|
|
67
77
|
const { t, locale } = useLocaleContext();
|
|
68
78
|
const { settings, setPayable, session, api } = usePaymentContext();
|
|
69
|
-
const
|
|
79
|
+
const pricingSource = item.upsell_price || item.price;
|
|
80
|
+
const isDynamicPricing = pricingSource?.pricing_type === "dynamic";
|
|
81
|
+
const pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays, exchangeRate }, locale);
|
|
70
82
|
const saving = formatUpsellSaving(items, currency);
|
|
71
83
|
const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : "";
|
|
72
84
|
const canUpsell = mode === "normal" && items.length === 1;
|
|
85
|
+
const isTrial = trialInDays > 0 || trialEnd > dayjs().unix();
|
|
73
86
|
const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config;
|
|
74
87
|
const creditAmount = isCreditProduct && item.price.metadata?.credit_config?.credit_amount ? Number(item.price.metadata.credit_config.credit_amount) : 0;
|
|
75
88
|
const creditCurrency = isCreditProduct && item.price.metadata?.credit_config?.currency_id ? findCurrency(settings.paymentMethods, item.price.metadata.credit_config.currency_id) : null;
|
|
@@ -100,7 +113,10 @@ export default function ProductItem({
|
|
|
100
113
|
);
|
|
101
114
|
const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal";
|
|
102
115
|
const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
|
|
103
|
-
const quantityAvailable = Math.min(
|
|
116
|
+
const quantityAvailable = Math.min(
|
|
117
|
+
item.price?.quantity_limit_per_checkout ?? Infinity,
|
|
118
|
+
item.price?.quantity_available ?? Infinity
|
|
119
|
+
);
|
|
104
120
|
const maxQuantity = quantityAvailable ? Math.min(adjustableQuantity.maximum || Infinity, quantityAvailable) : adjustableQuantity.maximum || Infinity;
|
|
105
121
|
const getMinQuantityForPending = useMemo(() => {
|
|
106
122
|
if (!isCreditProduct || !pendingAmount) return null;
|
|
@@ -173,7 +189,7 @@ export default function ProductItem({
|
|
|
173
189
|
setLocalQuantity(newQuantity);
|
|
174
190
|
onQuantityChange(item.price_id, newQuantity);
|
|
175
191
|
if (userDid && newQuantity > 0) {
|
|
176
|
-
saveUserQuantityPreference(userDid, item.price
|
|
192
|
+
saveUserQuantityPreference(userDid, item.price?.id, newQuantity);
|
|
177
193
|
}
|
|
178
194
|
}
|
|
179
195
|
};
|
|
@@ -199,7 +215,7 @@ export default function ProductItem({
|
|
|
199
215
|
const currencySymbol = creditCurrency?.symbol || "Credits";
|
|
200
216
|
const formattedTotalCredit = formatCreditForCheckout(totalCreditStr, currencySymbol, locale);
|
|
201
217
|
const hasPendingAmount = pendingAmount && new BN(pendingAmount || "0").gt(new BN(0));
|
|
202
|
-
const isRecurring = item.price
|
|
218
|
+
const isRecurring = item.price?.type === "recurring";
|
|
203
219
|
const hasExpiry = validDuration && validDuration > 0;
|
|
204
220
|
const buildBaseParams = () => ({
|
|
205
221
|
amount: formattedTotalCredit,
|
|
@@ -208,7 +224,7 @@ export default function ProductItem({
|
|
|
208
224
|
unit: t(`common.${validDurationUnit}`)
|
|
209
225
|
},
|
|
210
226
|
...isRecurring && {
|
|
211
|
-
period: formatRecurring(item.price
|
|
227
|
+
period: formatRecurring(item.price?.recurring, true, "per", locale)
|
|
212
228
|
}
|
|
213
229
|
});
|
|
214
230
|
const buildPendingParams = (pendingBN, availableAmount) => ({
|
|
@@ -230,7 +246,7 @@ export default function ProductItem({
|
|
|
230
246
|
unit: t(`common.${validDurationUnit}`)
|
|
231
247
|
},
|
|
232
248
|
...isRecurring && {
|
|
233
|
-
period: formatRecurring(item.price
|
|
249
|
+
period: formatRecurring(item.price?.recurring, true, "per", locale)
|
|
234
250
|
}
|
|
235
251
|
});
|
|
236
252
|
const getLocaleKey = (category, type2) => {
|
|
@@ -259,6 +275,46 @@ export default function ProductItem({
|
|
|
259
275
|
const type = isRecurring ? "recurringEnough" : "oneTimeEnough";
|
|
260
276
|
return t(getLocaleKey("pending", type), buildPendingParams(pendingAmountBN, actualAvailable));
|
|
261
277
|
};
|
|
278
|
+
const quantityForUsd = Number.isFinite(localQuantity) ? Number(localQuantity) : item.quantity || 0;
|
|
279
|
+
const dynamicTokenDisplay = useMemo(() => {
|
|
280
|
+
if (isStripePayment || !currency) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const baseAmount = pricingSource?.base_amount;
|
|
284
|
+
const unitAmount = pricingSource?.unit_amount;
|
|
285
|
+
if (isDynamicPricing && baseAmount !== void 0 && baseAmount !== null) {
|
|
286
|
+
if (isPriceLocked) {
|
|
287
|
+
const quotedAmount = item?.quoted_amount;
|
|
288
|
+
if (quotedAmount) {
|
|
289
|
+
const totalBN = new BN(quotedAmount);
|
|
290
|
+
const tokenValue = fromUnitToToken(totalBN, currency.decimal);
|
|
291
|
+
return `${formatDynamicPrice(tokenValue, true, 6)} ${currency.symbol}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!exchangeRate) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const rate = Number(exchangeRate);
|
|
298
|
+
if (rate <= 0 || Number.isNaN(rate)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const usdAmount = Number(baseAmount);
|
|
302
|
+
const tokenAmount = usdAmount / rate;
|
|
303
|
+
const totalTokens = tokenAmount * (quantityForUsd || 1);
|
|
304
|
+
return `${formatDynamicPrice(totalTokens, true, 6)} ${currency.symbol}`;
|
|
305
|
+
}
|
|
306
|
+
if (!isDynamicPricing && exchangeRate && unitAmount) {
|
|
307
|
+
const rate = Number(exchangeRate);
|
|
308
|
+
if (rate <= 0 || Number.isNaN(rate)) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const usdAmount = Number(unitAmount) / 100;
|
|
312
|
+
const tokenAmount = usdAmount / rate;
|
|
313
|
+
const totalTokens = tokenAmount * (quantityForUsd || 1);
|
|
314
|
+
return `${formatDynamicPrice(totalTokens, true, 6)} ${currency.symbol}`;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}, [isStripePayment, isDynamicPricing, currency, pricingSource, item, exchangeRate, quantityForUsd, isPriceLocked]);
|
|
262
318
|
const formatScheduleInfo = () => {
|
|
263
319
|
if (!hasSchedule || !scheduleConfig) return null;
|
|
264
320
|
const totalCredit = creditAmount * (localQuantity || 0);
|
|
@@ -300,14 +356,204 @@ export default function ProductItem({
|
|
|
300
356
|
const primaryText = useMemo(() => {
|
|
301
357
|
const price = item.upsell_price || item.price || {};
|
|
302
358
|
const isRecurring = price?.type === "recurring" && price?.recurring;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
359
|
+
if (isTrial) {
|
|
360
|
+
return pricing.primary;
|
|
361
|
+
}
|
|
362
|
+
let displayAmount;
|
|
363
|
+
if (!isStripePayment && dynamicTokenDisplay) {
|
|
364
|
+
displayAmount = dynamicTokenDisplay;
|
|
365
|
+
} else if (isDynamicPricing && !isStripePayment) {
|
|
366
|
+
const baseAmount = pricingSource?.base_amount;
|
|
367
|
+
if (baseAmount !== void 0 && baseAmount !== null) {
|
|
368
|
+
const usdValue = Number(baseAmount);
|
|
369
|
+
displayAmount = `\u2248 $${usdValue.toFixed(2)}`;
|
|
370
|
+
} else {
|
|
371
|
+
displayAmount = pricing.primary;
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
displayAmount = pricing.primary;
|
|
375
|
+
}
|
|
376
|
+
if (isRecurring && price?.recurring?.usage_type !== "metered") {
|
|
377
|
+
return `${displayAmount} ${price.recurring ? formatRecurring(price.recurring, false, "slash", locale) : ""}`;
|
|
378
|
+
}
|
|
379
|
+
return displayAmount;
|
|
380
|
+
}, [isTrial, pricing, item, locale, dynamicTokenDisplay, isDynamicPricing, pricingSource, isStripePayment]);
|
|
381
|
+
const usdReference = useMemo(() => {
|
|
382
|
+
if (!currency || !isDynamicPricing || isStripePayment) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const baseAmount = pricingSource?.base_amount;
|
|
386
|
+
const hasBaseAmount = baseAmount !== void 0 && baseAmount !== null;
|
|
387
|
+
if (hasBaseAmount) {
|
|
388
|
+
return getUsdAmountFromBaseAmount(baseAmount, quantityForUsd);
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}, [currency, pricingSource, quantityForUsd, isDynamicPricing, isStripePayment]);
|
|
392
|
+
const usdReferenceDisplay = useMemo(() => formatUsdAmount(usdReference, locale), [usdReference, locale]);
|
|
393
|
+
const upsellTokenDisplay = useMemo(() => {
|
|
394
|
+
const upsellPrice = item.price?.upsell?.upsells_to;
|
|
395
|
+
if (!upsellPrice) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (isStripePayment && currency) {
|
|
399
|
+
const baseAmount2 = upsellPrice?.base_amount;
|
|
400
|
+
const unitAmount2 = upsellPrice?.unit_amount;
|
|
401
|
+
let usdAmount2;
|
|
402
|
+
if (baseAmount2 !== void 0 && baseAmount2 !== null) {
|
|
403
|
+
usdAmount2 = Number(baseAmount2);
|
|
404
|
+
} else if (unitAmount2) {
|
|
405
|
+
usdAmount2 = Number(unitAmount2) / 100;
|
|
406
|
+
} else {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const recurring2 = upsellPrice?.recurring ? ` ${formatRecurring(upsellPrice.recurring, false, "slash", locale)}` : "";
|
|
410
|
+
return `${usdAmount2} ${currency.symbol}${recurring2}`;
|
|
411
|
+
}
|
|
412
|
+
if (!currency || !exchangeRate) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const rate = Number(exchangeRate);
|
|
416
|
+
if (rate <= 0 || Number.isNaN(rate)) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const baseAmount = upsellPrice?.base_amount;
|
|
420
|
+
const unitAmount = upsellPrice?.unit_amount;
|
|
421
|
+
let usdAmount;
|
|
422
|
+
if (baseAmount !== void 0 && baseAmount !== null) {
|
|
423
|
+
usdAmount = Number(baseAmount);
|
|
424
|
+
} else if (unitAmount) {
|
|
425
|
+
usdAmount = Number(unitAmount) / 100;
|
|
426
|
+
} else {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const tokenAmount = usdAmount / rate;
|
|
430
|
+
const recurring = upsellPrice?.recurring ? ` ${formatRecurring(upsellPrice.recurring, false, "slash", locale)}` : "";
|
|
431
|
+
return `${formatDynamicPrice(tokenAmount, true, 6)} ${currency.symbol}${recurring}`;
|
|
432
|
+
}, [isStripePayment, currency, exchangeRate, item.price?.upsell?.upsells_to, locale]);
|
|
433
|
+
const downsellTokenDisplay = useMemo(() => {
|
|
434
|
+
if (!item.upsell_price_id || !item.price) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const originalPrice = item.price;
|
|
438
|
+
if (isStripePayment && currency) {
|
|
439
|
+
const baseAmount2 = originalPrice?.base_amount;
|
|
440
|
+
const unitAmount2 = originalPrice?.unit_amount;
|
|
441
|
+
let usdAmount2;
|
|
442
|
+
if (baseAmount2 !== void 0 && baseAmount2 !== null) {
|
|
443
|
+
usdAmount2 = Number(baseAmount2);
|
|
444
|
+
} else if (unitAmount2) {
|
|
445
|
+
usdAmount2 = Number(unitAmount2) / 100;
|
|
446
|
+
} else {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const recurring2 = originalPrice?.recurring ? ` ${formatRecurring(originalPrice.recurring, false, "slash", locale)}` : "";
|
|
450
|
+
return `${usdAmount2} ${currency.symbol}${recurring2}`;
|
|
451
|
+
}
|
|
452
|
+
if (!currency || !exchangeRate) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const rate = Number(exchangeRate);
|
|
456
|
+
if (rate <= 0 || Number.isNaN(rate)) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const baseAmount = originalPrice?.base_amount;
|
|
460
|
+
const unitAmount = originalPrice?.unit_amount;
|
|
461
|
+
let usdAmount;
|
|
462
|
+
if (baseAmount !== void 0 && baseAmount !== null) {
|
|
463
|
+
usdAmount = Number(baseAmount);
|
|
464
|
+
} else if (unitAmount) {
|
|
465
|
+
usdAmount = Number(unitAmount) / 100;
|
|
466
|
+
} else {
|
|
467
|
+
return null;
|
|
306
468
|
}
|
|
307
|
-
|
|
308
|
-
|
|
469
|
+
const tokenAmount = usdAmount / rate;
|
|
470
|
+
const recurring = originalPrice?.recurring ? ` ${formatRecurring(originalPrice.recurring, false, "slash", locale)}` : "";
|
|
471
|
+
return `${formatDynamicPrice(tokenAmount, true, 6)} ${currency.symbol}${recurring}`;
|
|
472
|
+
}, [isStripePayment, currency, exchangeRate, item.price, item.upsell_price_id, locale]);
|
|
473
|
+
const displayItemDiscountAmount = useMemo(() => {
|
|
474
|
+
if (isTrial && item.price?.type === "recurring" || item.price?.recurring?.usage_type === "metered") {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (!discounts?.length) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const couponDetails = discounts[0]?.coupon_details;
|
|
481
|
+
if (isStripePayment && couponDetails) {
|
|
482
|
+
if (!item.discountable) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const isDynamic = pricingSource?.pricing_type === "dynamic";
|
|
486
|
+
const baseAmount = pricingSource?.base_amount;
|
|
487
|
+
const unitAmount = pricingSource?.unit_amount;
|
|
488
|
+
let amountCents;
|
|
489
|
+
if (isDynamic && baseAmount !== void 0 && baseAmount !== null) {
|
|
490
|
+
amountCents = Number(baseAmount) * 100;
|
|
491
|
+
} else if (unitAmount) {
|
|
492
|
+
amountCents = Number(unitAmount);
|
|
493
|
+
} else {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
const quantity = localQuantity || item.quantity || 1;
|
|
497
|
+
const itemSubtotalCents = amountCents * quantity;
|
|
498
|
+
if (couponDetails.percent_off && couponDetails.percent_off > 0) {
|
|
499
|
+
const discountCents = Math.round(itemSubtotalCents * couponDetails.percent_off / 100);
|
|
500
|
+
return discountCents.toString();
|
|
501
|
+
}
|
|
502
|
+
if (couponDetails.amount_off && items.length === 1 && calculatedDiscountAmount) {
|
|
503
|
+
return calculatedDiscountAmount;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (isDynamicPricing && exchangeRate && couponDetails) {
|
|
508
|
+
const rateNum = Number(exchangeRate);
|
|
509
|
+
if (rateNum <= 0) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
if (!item.discountable) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const baseAmount = pricingSource?.base_amount;
|
|
516
|
+
if (!baseAmount) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const quantity = localQuantity || item.quantity || 1;
|
|
520
|
+
const itemTokenAmount = Number(baseAmount) * quantity / rateNum;
|
|
521
|
+
if (couponDetails.percent_off && couponDetails.percent_off > 0) {
|
|
522
|
+
const discountAmount = itemTokenAmount * couponDetails.percent_off / 100;
|
|
523
|
+
const discountAmountUnit = fromTokenToUnit(
|
|
524
|
+
discountAmount.toFixed(currency.decimal || 8),
|
|
525
|
+
currency.decimal || 8
|
|
526
|
+
);
|
|
527
|
+
return discountAmountUnit.toString();
|
|
528
|
+
}
|
|
529
|
+
if (couponDetails.amount_off && items.length === 1 && calculatedDiscountAmount) {
|
|
530
|
+
return calculatedDiscountAmount;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (items.length === 1 && calculatedDiscountAmount) {
|
|
534
|
+
return calculatedDiscountAmount;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}, [
|
|
538
|
+
isTrial,
|
|
539
|
+
discounts,
|
|
540
|
+
isStripePayment,
|
|
541
|
+
isDynamicPricing,
|
|
542
|
+
exchangeRate,
|
|
543
|
+
item,
|
|
544
|
+
pricingSource,
|
|
545
|
+
localQuantity,
|
|
546
|
+
currency.decimal,
|
|
547
|
+
items.length,
|
|
548
|
+
calculatedDiscountAmount
|
|
549
|
+
]);
|
|
550
|
+
const discountCodeDisplay = useMemo(() => {
|
|
551
|
+
if (!discounts?.length) return null;
|
|
552
|
+
const discount = discounts[0];
|
|
553
|
+
return discount.promotion_code_details?.code || discount.verification_data?.code || "DISCOUNT";
|
|
554
|
+
}, [discounts]);
|
|
309
555
|
const quantityInventoryError = formatQuantityInventory(item.price, localQuantityNum, locale);
|
|
310
|
-
const features = item.price
|
|
556
|
+
const features = item.price?.product?.features || [];
|
|
311
557
|
return /* @__PURE__ */ jsxs(
|
|
312
558
|
Stack,
|
|
313
559
|
{
|
|
@@ -344,8 +590,8 @@ export default function ProductItem({
|
|
|
344
590
|
/* @__PURE__ */ jsx(
|
|
345
591
|
ProductCard,
|
|
346
592
|
{
|
|
347
|
-
logo: item.price
|
|
348
|
-
name: item.price
|
|
593
|
+
logo: item.price?.product?.images[0],
|
|
594
|
+
name: item.price?.product?.name,
|
|
349
595
|
extra: /* @__PURE__ */ jsx(
|
|
350
596
|
Box,
|
|
351
597
|
{
|
|
@@ -353,7 +599,7 @@ export default function ProductItem({
|
|
|
353
599
|
display: "flex",
|
|
354
600
|
alignItems: "center"
|
|
355
601
|
},
|
|
356
|
-
children: item.price
|
|
602
|
+
children: item.price?.type === "recurring" && item.price?.recurring ? [pricing.quantity, t("common.billed", { rule: `${formatRecurring(item.upsell_price?.recurring || item.price?.recurring, true, "per", locale)} ${metered}` })].filter(Boolean).join(", ") : pricing.quantity
|
|
357
603
|
}
|
|
358
604
|
)
|
|
359
605
|
}
|
|
@@ -367,42 +613,70 @@ export default function ProductItem({
|
|
|
367
613
|
flex: 1
|
|
368
614
|
},
|
|
369
615
|
children: [
|
|
370
|
-
/* @__PURE__ */ jsx(
|
|
371
|
-
|
|
616
|
+
/* @__PURE__ */ jsx(
|
|
617
|
+
LoadingAmount,
|
|
618
|
+
{
|
|
619
|
+
value: primaryText,
|
|
620
|
+
loading: isRateLoading,
|
|
621
|
+
skeletonWidth: 80,
|
|
622
|
+
height: 20,
|
|
623
|
+
sx: { color: "text.primary", fontWeight: 500 }
|
|
624
|
+
}
|
|
625
|
+
),
|
|
626
|
+
isDynamicPricing && !isStripePayment && !isTrial && usdReferenceDisplay && /* @__PURE__ */ jsxs(Typography, { sx: { fontSize: "0.74375rem", color: "text.lighter" }, children: [
|
|
627
|
+
"\u2248 $",
|
|
628
|
+
usdReferenceDisplay
|
|
629
|
+
] }),
|
|
630
|
+
pricing.secondary && !isTrial && /* @__PURE__ */ jsx(Typography, { sx: { fontSize: "0.74375rem", color: "text.lighter" }, children: pricing.secondary })
|
|
372
631
|
]
|
|
373
632
|
}
|
|
374
633
|
)
|
|
375
634
|
]
|
|
376
635
|
}
|
|
377
636
|
),
|
|
378
|
-
|
|
379
|
-
|
|
637
|
+
!(isTrial && item.price?.type === "recurring" || item.price?.recurring?.usage_type === "metered") && (displayItemDiscountAmount || item.discount_amounts && item.discount_amounts.length > 0) && /* @__PURE__ */ jsx(
|
|
638
|
+
Stack,
|
|
380
639
|
{
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
/* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "0.75rem", fontWeight: "medium" }, children: discountAmount.promotion_code?.code || "DISCOUNT" }),
|
|
384
|
-
/* @__PURE__ */ jsxs(Typography, { component: "span", sx: { fontSize: "0.75rem" }, children: [
|
|
385
|
-
"(-",
|
|
386
|
-
formatAmount(discountAmount.amount || "0", currency.decimal),
|
|
387
|
-
" ",
|
|
388
|
-
currency.symbol,
|
|
389
|
-
")"
|
|
390
|
-
] })
|
|
391
|
-
] }),
|
|
392
|
-
size: "small",
|
|
393
|
-
variant: "filled",
|
|
640
|
+
direction: "row",
|
|
641
|
+
spacing: 1,
|
|
394
642
|
sx: {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
643
|
+
mt: 1,
|
|
644
|
+
alignItems: "center",
|
|
645
|
+
opacity: isRateLoading ? 0 : 1,
|
|
646
|
+
transition: "opacity 300ms ease-in-out"
|
|
647
|
+
},
|
|
648
|
+
children: /* @__PURE__ */ jsx(
|
|
649
|
+
Chip,
|
|
650
|
+
{
|
|
651
|
+
icon: /* @__PURE__ */ jsx(LocalOffer, { sx: { fontSize: "0.8rem !important" } }),
|
|
652
|
+
label: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [
|
|
653
|
+
/* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: "0.75rem", fontWeight: "medium" }, children: discountCodeDisplay || "DISCOUNT" }),
|
|
654
|
+
/* @__PURE__ */ jsxs(Typography, { component: "span", sx: { fontSize: "0.75rem" }, children: [
|
|
655
|
+
"(-",
|
|
656
|
+
formatAmount(
|
|
657
|
+
displayItemDiscountAmount || item.discount_amounts?.[0]?.amount || "0",
|
|
658
|
+
currency.decimal
|
|
659
|
+
),
|
|
660
|
+
" ",
|
|
661
|
+
currency.symbol,
|
|
662
|
+
")"
|
|
663
|
+
] })
|
|
664
|
+
] }),
|
|
665
|
+
size: "small",
|
|
666
|
+
variant: "filled",
|
|
667
|
+
sx: {
|
|
668
|
+
height: 20,
|
|
669
|
+
"& .MuiChip-icon": {
|
|
670
|
+
color: "warning.main"
|
|
671
|
+
},
|
|
672
|
+
"& .MuiChip-label": {
|
|
673
|
+
px: 1
|
|
674
|
+
}
|
|
675
|
+
}
|
|
401
676
|
}
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
)) }),
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
),
|
|
406
680
|
showFeatures && features.length > 0 && /* @__PURE__ */ jsx(
|
|
407
681
|
Box,
|
|
408
682
|
{
|
|
@@ -543,7 +817,7 @@ export default function ProductItem({
|
|
|
543
817
|
]
|
|
544
818
|
}
|
|
545
819
|
),
|
|
546
|
-
canUpsell && !item.upsell_price_id && item.price
|
|
820
|
+
canUpsell && !item.upsell_price_id && item.price?.upsell?.upsells_to && /* @__PURE__ */ jsxs(
|
|
547
821
|
Stack,
|
|
548
822
|
{
|
|
549
823
|
direction: "row",
|
|
@@ -572,11 +846,16 @@ export default function ProductItem({
|
|
|
572
846
|
sx: { mr: 1 },
|
|
573
847
|
variant: "success",
|
|
574
848
|
checked: false,
|
|
575
|
-
onChange: () => onUpsell(item.price_id, item.price
|
|
849
|
+
onChange: () => onUpsell(item.price_id, item.price?.upsell?.upsells_to_id)
|
|
576
850
|
}
|
|
577
851
|
),
|
|
578
852
|
t("payment.checkout.upsell.save", {
|
|
579
|
-
recurring: formatRecurring(
|
|
853
|
+
recurring: formatRecurring(
|
|
854
|
+
item.price?.upsell?.upsells_to?.recurring,
|
|
855
|
+
true,
|
|
856
|
+
"per",
|
|
857
|
+
locale
|
|
858
|
+
)
|
|
580
859
|
}),
|
|
581
860
|
/* @__PURE__ */ jsx(
|
|
582
861
|
Status,
|
|
@@ -594,7 +873,7 @@ export default function ProductItem({
|
|
|
594
873
|
]
|
|
595
874
|
}
|
|
596
875
|
),
|
|
597
|
-
/* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 12 }, children: formatPrice(item.price
|
|
876
|
+
/* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 12 }, children: upsellTokenDisplay || formatPrice(item.price?.upsell?.upsells_to, currency, item.price?.product?.unit_label, 1, true, locale) })
|
|
598
877
|
]
|
|
599
878
|
}
|
|
600
879
|
),
|
|
@@ -631,12 +910,12 @@ export default function ProductItem({
|
|
|
631
910
|
}
|
|
632
911
|
),
|
|
633
912
|
t("payment.checkout.upsell.revert", {
|
|
634
|
-
recurring: t(`common.${formatRecurring(item.price
|
|
913
|
+
recurring: t(`common.${formatRecurring(item.price?.recurring)}`)
|
|
635
914
|
})
|
|
636
915
|
]
|
|
637
916
|
}
|
|
638
917
|
),
|
|
639
|
-
/* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 12 }, children: formatPrice(item.price, currency, item.price
|
|
918
|
+
/* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 12 }, children: downsellTokenDisplay || formatPrice(item.price, currency, item.price?.product?.unit_label, 1, true, locale) })
|
|
640
919
|
]
|
|
641
920
|
}
|
|
642
921
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromotionSection Component
|
|
3
|
+
*
|
|
4
|
+
* Handles promotion code input, applied discounts display, and removal.
|
|
5
|
+
*/
|
|
6
|
+
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
7
|
+
export interface DiscountInfo {
|
|
8
|
+
promotion_code?: string;
|
|
9
|
+
coupon?: string;
|
|
10
|
+
discount_amount?: string;
|
|
11
|
+
promotion_code_details?: {
|
|
12
|
+
code?: string;
|
|
13
|
+
};
|
|
14
|
+
coupon_details?: any;
|
|
15
|
+
verification_data?: {
|
|
16
|
+
code?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface PromotionSectionProps {
|
|
20
|
+
checkoutSessionId: string;
|
|
21
|
+
currency: TPaymentCurrency;
|
|
22
|
+
currencyId: string;
|
|
23
|
+
discounts: DiscountInfo[];
|
|
24
|
+
allowPromotionCodes: boolean;
|
|
25
|
+
completed?: boolean;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
onPromotionUpdate: () => void;
|
|
28
|
+
onRemovePromotion: (sessionId: string) => void;
|
|
29
|
+
calculatedDiscountAmount?: string | null;
|
|
30
|
+
isRateLoading?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export default function PromotionSection({ checkoutSessionId, currency, currencyId, discounts, allowPromotionCodes, completed, disabled, onPromotionUpdate, onRemovePromotion, calculatedDiscountAmount, isRateLoading, }: PromotionSectionProps): import("react").JSX.Element | null;
|