@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.
Files changed (98) hide show
  1. package/es/components/auto-topup/modal.d.ts +2 -0
  2. package/es/components/auto-topup/modal.js +48 -6
  3. package/es/components/auto-topup/product-card.d.ts +16 -1
  4. package/es/components/auto-topup/product-card.js +97 -15
  5. package/es/components/dynamic-pricing-unavailable.d.ts +9 -0
  6. package/es/components/dynamic-pricing-unavailable.js +58 -0
  7. package/es/components/loading-amount.d.ts +17 -0
  8. package/es/components/loading-amount.js +46 -0
  9. package/es/components/price-change-confirm.d.ts +18 -0
  10. package/es/components/price-change-confirm.js +107 -0
  11. package/es/components/quote-details-panel.d.ts +21 -0
  12. package/es/components/quote-details-panel.js +170 -0
  13. package/es/components/quote-lock-banner.d.ts +7 -0
  14. package/es/components/quote-lock-banner.js +79 -0
  15. package/es/components/slippage-config.d.ts +20 -0
  16. package/es/components/slippage-config.js +261 -0
  17. package/es/history/invoice/list.js +125 -15
  18. package/es/hooks/dynamic-pricing.d.ts +102 -0
  19. package/es/hooks/dynamic-pricing.js +393 -0
  20. package/es/index.d.ts +6 -1
  21. package/es/index.js +9 -1
  22. package/es/libs/util.d.ts +42 -5
  23. package/es/libs/util.js +345 -57
  24. package/es/locales/en.js +114 -3
  25. package/es/locales/zh.js +114 -3
  26. package/es/payment/form/index.d.ts +4 -1
  27. package/es/payment/form/index.js +454 -22
  28. package/es/payment/index.d.ts +1 -1
  29. package/es/payment/index.js +279 -16
  30. package/es/payment/product-item.d.ts +26 -1
  31. package/es/payment/product-item.js +330 -51
  32. package/es/payment/summary-section/promotion-section.d.ts +32 -0
  33. package/es/payment/summary-section/promotion-section.js +143 -0
  34. package/es/payment/summary-section/total-section.d.ts +39 -0
  35. package/es/payment/summary-section/total-section.js +83 -0
  36. package/es/payment/summary.d.ts +17 -2
  37. package/es/payment/summary.js +300 -253
  38. package/es/types/index.d.ts +11 -0
  39. package/lib/components/auto-topup/modal.d.ts +2 -0
  40. package/lib/components/auto-topup/modal.js +54 -6
  41. package/lib/components/auto-topup/product-card.d.ts +16 -1
  42. package/lib/components/auto-topup/product-card.js +75 -7
  43. package/lib/components/dynamic-pricing-unavailable.d.ts +9 -0
  44. package/lib/components/dynamic-pricing-unavailable.js +81 -0
  45. package/lib/components/loading-amount.d.ts +17 -0
  46. package/lib/components/loading-amount.js +53 -0
  47. package/lib/components/price-change-confirm.d.ts +18 -0
  48. package/lib/components/price-change-confirm.js +157 -0
  49. package/lib/components/quote-details-panel.d.ts +21 -0
  50. package/lib/components/quote-details-panel.js +226 -0
  51. package/lib/components/quote-lock-banner.d.ts +7 -0
  52. package/lib/components/quote-lock-banner.js +93 -0
  53. package/lib/components/slippage-config.d.ts +20 -0
  54. package/lib/components/slippage-config.js +316 -0
  55. package/lib/history/invoice/list.js +167 -27
  56. package/lib/hooks/dynamic-pricing.d.ts +102 -0
  57. package/lib/hooks/dynamic-pricing.js +390 -0
  58. package/lib/index.d.ts +6 -1
  59. package/lib/index.js +32 -0
  60. package/lib/libs/util.d.ts +42 -5
  61. package/lib/libs/util.js +367 -49
  62. package/lib/locales/en.js +114 -3
  63. package/lib/locales/zh.js +114 -3
  64. package/lib/payment/form/index.d.ts +4 -1
  65. package/lib/payment/form/index.js +476 -20
  66. package/lib/payment/index.d.ts +1 -1
  67. package/lib/payment/index.js +308 -14
  68. package/lib/payment/product-item.d.ts +26 -1
  69. package/lib/payment/product-item.js +270 -35
  70. package/lib/payment/summary-section/promotion-section.d.ts +32 -0
  71. package/lib/payment/summary-section/promotion-section.js +133 -0
  72. package/lib/payment/summary-section/total-section.d.ts +39 -0
  73. package/lib/payment/summary-section/total-section.js +117 -0
  74. package/lib/payment/summary.d.ts +17 -2
  75. package/lib/payment/summary.js +205 -127
  76. package/lib/types/index.d.ts +11 -0
  77. package/package.json +3 -3
  78. package/src/components/auto-topup/modal.tsx +59 -6
  79. package/src/components/auto-topup/product-card.tsx +118 -11
  80. package/src/components/dynamic-pricing-unavailable.tsx +69 -0
  81. package/src/components/loading-amount.tsx +66 -0
  82. package/src/components/price-change-confirm.tsx +136 -0
  83. package/src/components/quote-details-panel.tsx +218 -0
  84. package/src/components/quote-lock-banner.tsx +99 -0
  85. package/src/components/slippage-config.tsx +336 -0
  86. package/src/history/invoice/list.tsx +143 -9
  87. package/src/hooks/dynamic-pricing.ts +617 -0
  88. package/src/index.ts +9 -0
  89. package/src/libs/util.ts +473 -58
  90. package/src/locales/en.tsx +117 -0
  91. package/src/locales/zh.tsx +111 -0
  92. package/src/payment/form/index.tsx +561 -19
  93. package/src/payment/index.tsx +349 -10
  94. package/src/payment/product-item.tsx +451 -37
  95. package/src/payment/summary-section/promotion-section.tsx +172 -0
  96. package/src/payment/summary-section/total-section.tsx +141 -0
  97. package/src/payment/summary.tsx +334 -192
  98. package/src/types/index.ts +15 -0
@@ -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 pricing = formatLineItemPricing(item, currency, { trialEnd, trialInDays }, locale);
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(item.price.quantity_limit_per_checkout, item.price.quantity_available);
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.id, newQuantity);
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.type === "recurring";
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.recurring, true, "per", locale)
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.recurring, true, "per", locale)
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
- const trial = trialInDays > 0 || trialEnd > dayjs().unix();
304
- if (isRecurring && !trial && price?.recurring?.usage_type !== "metered") {
305
- return `${pricing.primary} ${price.recurring ? formatRecurring(price.recurring, false, "slash", locale) : ""}`;
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
- return pricing.primary;
308
- }, [trialInDays, trialEnd, pricing, item, locale]);
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.product?.features || [];
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.product?.images[0],
348
- name: item.price.product?.name,
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.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
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(Typography, { sx: { color: "text.primary", fontWeight: 500, whiteSpace: "nowrap" }, gutterBottom: true, children: primaryText }),
371
- pricing.secondary && /* @__PURE__ */ jsx(Typography, { sx: { fontSize: "0.74375rem", color: "text.lighter" }, children: pricing.secondary })
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
- item.discount_amounts && item.discount_amounts.length > 0 && /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 1, sx: { mt: 1, alignItems: "center" }, children: item.discount_amounts.map((discountAmount) => /* @__PURE__ */ jsx(
379
- Chip,
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
- icon: /* @__PURE__ */ jsx(LocalOffer, { sx: { fontSize: "0.8rem !important" } }),
382
- label: /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [
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
- height: 20,
396
- "& .MuiChip-icon": {
397
- color: "warning.main"
398
- },
399
- "& .MuiChip-label": {
400
- px: 1
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
- discountAmount.promotion_code
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.upsell?.upsells_to && /* @__PURE__ */ jsxs(
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.upsell?.upsells_to_id)
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(item.price.upsell.upsells_to.recurring, true, "per", locale)
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.upsell.upsells_to, currency, item.price.product?.unit_label, 1, true, locale) })
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.recurring)}`)
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.product?.unit_label, 1, true, locale) })
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;