@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/libs/util.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
TSubscriptionExpanded,
|
|
18
18
|
TSubscriptionItemExpanded,
|
|
19
19
|
} from '@blocklet/payment-types';
|
|
20
|
-
import { BN, fromUnitToToken } from '@ocap/util';
|
|
20
|
+
import { BN, fromUnitToToken, fromTokenToUnit } from '@ocap/util';
|
|
21
21
|
import omit from 'lodash/omit';
|
|
22
22
|
import trimEnd from 'lodash/trimEnd';
|
|
23
23
|
import numbro from 'numbro';
|
|
@@ -265,6 +265,136 @@ export function formatNumber(
|
|
|
265
265
|
return right ? [left, trimEnd(right, '0')].filter(Boolean).join('.') : left;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
export function formatDynamicPrice(
|
|
269
|
+
n: number | string,
|
|
270
|
+
isDynamic: boolean,
|
|
271
|
+
precision: number = 6,
|
|
272
|
+
trim: boolean = true,
|
|
273
|
+
thousandSeparated: boolean = true
|
|
274
|
+
) {
|
|
275
|
+
if (!isDynamic) {
|
|
276
|
+
return formatNumber(n, precision, trim, thousandSeparated);
|
|
277
|
+
}
|
|
278
|
+
const num = Number(n);
|
|
279
|
+
if (!Number.isFinite(num)) {
|
|
280
|
+
return formatNumber(n, precision, trim, thousandSeparated);
|
|
281
|
+
}
|
|
282
|
+
const abs = Math.abs(num);
|
|
283
|
+
const targetPrecision = abs > 0 && abs < 0.01 ? precision : 2;
|
|
284
|
+
return formatNumber(n, targetPrecision, trim, thousandSeparated);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const USD_DECIMALS = 8;
|
|
288
|
+
|
|
289
|
+
export function getUsdAmountFromBaseAmount(
|
|
290
|
+
amount: string | number | undefined,
|
|
291
|
+
quantity: number,
|
|
292
|
+
scale = BASE_AMOUNT_SCALE
|
|
293
|
+
): string | null {
|
|
294
|
+
if (amount === undefined || amount === null || !quantity || quantity <= 0) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const scaled = toScaledBaseAmount(amount, quantity, scale);
|
|
298
|
+
return formatScaledAmount(scaled, scale);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function getUsdAmountFromTokenUnits(
|
|
302
|
+
tokenAmount: BN | string,
|
|
303
|
+
tokenDecimals: number,
|
|
304
|
+
exchangeRate?: string | null
|
|
305
|
+
): string | null {
|
|
306
|
+
if (!exchangeRate && exchangeRate !== '0') {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
if (tokenDecimals === undefined || tokenDecimals === null) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// exchangeRate is in token format (e.g., "0.25535177995959574"), may have more than 8 decimal places
|
|
315
|
+
// tokenAmount is in unit format (smallest unit)
|
|
316
|
+
// We need to calculate: (tokenAmount / 10^tokenDecimals) * exchangeRate
|
|
317
|
+
// To avoid precision loss, we use: (tokenAmount * exchangeRate) / 10^tokenDecimals
|
|
318
|
+
// But exchangeRate is a decimal string, so we need to handle it carefully
|
|
319
|
+
|
|
320
|
+
const amountBN = tokenAmount instanceof BN ? tokenAmount : new BN(tokenAmount || '0');
|
|
321
|
+
const tokenScale = new BN(10).pow(new BN(tokenDecimals));
|
|
322
|
+
if (tokenScale.isZero()) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Convert exchangeRate (token format) to BN with proper precision
|
|
327
|
+
// exchangeRate may have more than 8 decimal places, so we need to preserve all precision
|
|
328
|
+
// We'll use a higher precision scale for the rate calculation
|
|
329
|
+
const rateBN = new BN(exchangeRate.replace('.', ''));
|
|
330
|
+
const rateDecimalPlaces = exchangeRate.includes('.') ? exchangeRate.split('.')[1]?.length || 0 : 0;
|
|
331
|
+
const rateScale = new BN(10).pow(new BN(rateDecimalPlaces));
|
|
332
|
+
|
|
333
|
+
// Calculate: (amountBN * rateBN * USD_PRECISION_SCALE) / (tokenScale * rateScale)
|
|
334
|
+
// The USD_PRECISION_SCALE keeps enough trailing digits so the final conversion via fromUnitToToken
|
|
335
|
+
// produces a value with 8 decimal places.
|
|
336
|
+
const usdPrecisionScale = new BN(10).pow(new BN(USD_DECIMALS));
|
|
337
|
+
const usdUnit = amountBN.mul(rateBN).mul(usdPrecisionScale).div(tokenScale.mul(rateScale));
|
|
338
|
+
|
|
339
|
+
// Convert from unit format to token format with 8 decimals
|
|
340
|
+
return fromUnitToToken(usdUnit.toString(), USD_DECIMALS);
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function formatUsdAmount(amount: string | null, locale: string = 'en'): string | null {
|
|
347
|
+
if (!amount && amount !== '0') {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const num = Number(amount);
|
|
351
|
+
if (!Number.isFinite(num)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return new Intl.NumberFormat(locale, {
|
|
355
|
+
minimumFractionDigits: 2,
|
|
356
|
+
maximumFractionDigits: 2,
|
|
357
|
+
}).format(num);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function formatExchangeRate(amount: string | null): string | null {
|
|
361
|
+
if (!amount && amount !== '0') {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const value = String(amount);
|
|
365
|
+
const num = Number(value);
|
|
366
|
+
if (!Number.isFinite(num)) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Format exchange rate with currency symbol for display
|
|
374
|
+
* @param rate - The exchange rate value
|
|
375
|
+
* @param currency - The currency code (default: 'USD')
|
|
376
|
+
* @param decimals - Number of decimal places (default: 2, use 4 for live exchange rates)
|
|
377
|
+
* @returns Formatted string like "$0.12" for USD, or "0.12 EUR" for other currencies
|
|
378
|
+
*/
|
|
379
|
+
export function formatExchangeRateDisplay(
|
|
380
|
+
rate: string | number | null | undefined,
|
|
381
|
+
currency: string = 'USD',
|
|
382
|
+
decimals: number = 2
|
|
383
|
+
): string | null {
|
|
384
|
+
if (rate === null || rate === undefined) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const num = Number(rate);
|
|
388
|
+
if (!Number.isFinite(num)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const formattedValue = num.toFixed(decimals);
|
|
392
|
+
if (currency === 'USD') {
|
|
393
|
+
return `$${formattedValue}`;
|
|
394
|
+
}
|
|
395
|
+
return `${formattedValue} ${currency}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
268
398
|
export const formatPrice = (
|
|
269
399
|
price: TPrice,
|
|
270
400
|
currency: TPaymentCurrency,
|
|
@@ -280,6 +410,33 @@ export const formatPrice = (
|
|
|
280
410
|
return `Custom (${currency.symbol})`;
|
|
281
411
|
}
|
|
282
412
|
|
|
413
|
+
// For dynamic pricing, display USD reference price
|
|
414
|
+
if (
|
|
415
|
+
(price as any).pricing_type === 'dynamic' &&
|
|
416
|
+
(price as any).base_amount &&
|
|
417
|
+
(price as any).base_currency === 'USD'
|
|
418
|
+
) {
|
|
419
|
+
const baseAmount = (price as any).base_amount;
|
|
420
|
+
const usdAmount = Number(baseAmount) * quantity;
|
|
421
|
+
const formattedUsd = formatUsdAmount(usdAmount.toString(), locale);
|
|
422
|
+
|
|
423
|
+
if (price?.type === 'recurring' && price.recurring) {
|
|
424
|
+
const recurring = formatRecurring(price.recurring, false, 'slash', locale);
|
|
425
|
+
|
|
426
|
+
if (unit_label) {
|
|
427
|
+
return `$${formattedUsd} / ${unit_label} ${recurring}`;
|
|
428
|
+
}
|
|
429
|
+
if (price.recurring.usage_type === 'metered') {
|
|
430
|
+
return `$${formattedUsd} / unit ${recurring}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return `$${formattedUsd} ${recurring}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return `$${formattedUsd}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// For fixed pricing, display token amount
|
|
283
440
|
const unit = getPriceUintAmountByCurrency(price, currency);
|
|
284
441
|
const amount = bn
|
|
285
442
|
? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
|
|
@@ -305,11 +462,38 @@ export const formatPriceAmount = (
|
|
|
305
462
|
currency: TPaymentCurrency,
|
|
306
463
|
unit_label?: string,
|
|
307
464
|
quantity: number = 1,
|
|
308
|
-
bn: boolean = true
|
|
465
|
+
bn: boolean = true,
|
|
466
|
+
locale: string = 'en'
|
|
309
467
|
) => {
|
|
310
468
|
if (!currency) {
|
|
311
469
|
return '';
|
|
312
470
|
}
|
|
471
|
+
|
|
472
|
+
// For dynamic pricing, display USD reference price
|
|
473
|
+
if (
|
|
474
|
+
(price as any).pricing_type === 'dynamic' &&
|
|
475
|
+
(price as any).base_amount &&
|
|
476
|
+
(price as any).base_currency === 'USD'
|
|
477
|
+
) {
|
|
478
|
+
const baseAmount = (price as any).base_amount;
|
|
479
|
+
const usdAmount = Number(baseAmount) * quantity;
|
|
480
|
+
const formattedUsd = formatUsdAmount(usdAmount.toString(), locale);
|
|
481
|
+
|
|
482
|
+
if (price?.type === 'recurring' && price.recurring) {
|
|
483
|
+
if (unit_label) {
|
|
484
|
+
return `$${formattedUsd} / ${unit_label}`;
|
|
485
|
+
}
|
|
486
|
+
if (price.recurring.usage_type === 'metered') {
|
|
487
|
+
return `$${formattedUsd} / unit`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return `$${formattedUsd}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return `$${formattedUsd}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// For fixed pricing, display token amount
|
|
313
497
|
const unit = getPriceUintAmountByCurrency(price, currency);
|
|
314
498
|
const amount = bn
|
|
315
499
|
? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
|
|
@@ -413,26 +597,167 @@ export function getPriceCurrencyOptions(price: TPrice): PriceCurrency[] {
|
|
|
413
597
|
];
|
|
414
598
|
}
|
|
415
599
|
|
|
600
|
+
const BASE_AMOUNT_SCALE = 8;
|
|
601
|
+
|
|
602
|
+
function formatScaledAmount(value: BN, scale = BASE_AMOUNT_SCALE) {
|
|
603
|
+
const isNegative = value.isNeg();
|
|
604
|
+
const absValue = value.abs();
|
|
605
|
+
const str = absValue.toString().padStart(scale + 1, '0');
|
|
606
|
+
const integerPart = str.slice(0, str.length - scale) || '0';
|
|
607
|
+
const fraction = str.slice(-scale).replace(/0+$/, '');
|
|
608
|
+
const formatted = fraction ? `${integerPart}.${fraction}` : integerPart;
|
|
609
|
+
return isNegative ? `-${formatted}` : formatted;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function toScaledBaseAmount(amount: string | number | undefined, quantity: number, scale = BASE_AMOUNT_SCALE) {
|
|
613
|
+
if (!amount || !quantity) {
|
|
614
|
+
return new BN(0);
|
|
615
|
+
}
|
|
616
|
+
const normalized = String(amount);
|
|
617
|
+
const [integer = '0', fraction = ''] = normalized.split('.');
|
|
618
|
+
const frac = `${fraction}`.padEnd(scale, '0').slice(0, scale);
|
|
619
|
+
const digits = `${integer.replace('-', '')}${frac}` || '0';
|
|
620
|
+
const scaled = new BN(digits).mul(new BN(quantity));
|
|
621
|
+
return normalized.startsWith('-') ? scaled.neg() : scaled;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function getLineItemAmounts(
|
|
625
|
+
item: TLineItemExpanded,
|
|
626
|
+
currency: TPaymentCurrency,
|
|
627
|
+
{ useUpsell = true, exchangeRate = null }: { useUpsell?: boolean; exchangeRate?: string | null } = {}
|
|
628
|
+
): { unitAmount: BN; totalAmount: BN; isDynamicQuote: boolean } {
|
|
629
|
+
if (!currency) {
|
|
630
|
+
return { unitAmount: new BN(0), totalAmount: new BN(0), isDynamicQuote: false };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const price = (useUpsell ? item.upsell_price || item.price : item.price) as any;
|
|
634
|
+
const quantity = new BN(item.quantity || 0);
|
|
635
|
+
const quoteAmount = (item as any)?.quoted_amount;
|
|
636
|
+
const quoteCurrencyId = (item as any)?.quote_currency_id;
|
|
637
|
+
const isDynamicPrice = price?.pricing_type === 'dynamic';
|
|
638
|
+
|
|
639
|
+
// For dynamic pricing, only use quoted_amount if the quote was created for the current currency
|
|
640
|
+
// This prevents using a quote with wrong decimal precision when user switches currencies
|
|
641
|
+
// e.g., ABT quote (decimal=18) should not be used with USD (decimal=2)
|
|
642
|
+
const isQuoteValidForCurrency = isDynamicPrice && quoteAmount && quoteCurrencyId === currency.id;
|
|
643
|
+
|
|
644
|
+
if (isQuoteValidForCurrency) {
|
|
645
|
+
const totalAmount = new BN(quoteAmount || '0');
|
|
646
|
+
const unitAmount = quantity.gt(new BN(0)) ? totalAmount.add(quantity).sub(new BN(1)).div(quantity) : new BN(0);
|
|
647
|
+
|
|
648
|
+
return { unitAmount, totalAmount, isDynamicQuote: true };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// For dynamic pricing without valid quote, calculate from base_amount and exchange rate
|
|
652
|
+
if (isDynamicPrice && exchangeRate && price?.base_amount) {
|
|
653
|
+
const rate = Number(exchangeRate);
|
|
654
|
+
if (rate > 0 && Number.isFinite(rate)) {
|
|
655
|
+
// base_amount is stored in dollar format (e.g., "1.00" = $1.00)
|
|
656
|
+
const baseAmountUsd = Number(price.base_amount);
|
|
657
|
+
if (baseAmountUsd > 0 && Number.isFinite(baseAmountUsd)) {
|
|
658
|
+
// Calculate token amount: USD / rate
|
|
659
|
+
const tokenAmount = baseAmountUsd / rate;
|
|
660
|
+
const unitAmount = fromTokenToUnit(tokenAmount.toFixed(currency.decimal || 8), currency.decimal || 8);
|
|
661
|
+
return {
|
|
662
|
+
unitAmount,
|
|
663
|
+
totalAmount: unitAmount.mul(quantity),
|
|
664
|
+
isDynamicQuote: false,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// For all other cases (non-dynamic, or quote not valid for current currency), use currency_options
|
|
671
|
+
const unitAmount = new BN(getPriceUintAmountByCurrency(price, currency));
|
|
672
|
+
return {
|
|
673
|
+
unitAmount,
|
|
674
|
+
totalAmount: unitAmount.mul(quantity),
|
|
675
|
+
isDynamicQuote: false,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export type QuoteLockInfo = {
|
|
680
|
+
baseAmount: string;
|
|
681
|
+
baseCurrency: string;
|
|
682
|
+
tokenAmount: string;
|
|
683
|
+
tokenSymbol: string;
|
|
684
|
+
expiresAt: number | null;
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
export function getQuoteLockInfo(items: TLineItemExpanded[], currency?: TPaymentCurrency | null): QuoteLockInfo | null {
|
|
688
|
+
if (!items?.length || !currency) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const dynamicItems = items.filter((item) => {
|
|
693
|
+
const price = (item.upsell_price || item.price) as any;
|
|
694
|
+
return price?.pricing_type === 'dynamic' && (item as any)?.quoted_amount;
|
|
695
|
+
});
|
|
696
|
+
if (!dynamicItems.length) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let totalBaseAmount = new BN(0);
|
|
701
|
+
let totalTokenAmount = new BN(0);
|
|
702
|
+
let expiresAt: number | null = null;
|
|
703
|
+
|
|
704
|
+
dynamicItems.forEach((item) => {
|
|
705
|
+
const price = (item.upsell_price || item.price) as any;
|
|
706
|
+
const quoteAmount = new BN((item as any)?.quoted_amount || '0');
|
|
707
|
+
totalTokenAmount = totalTokenAmount.add(quoteAmount);
|
|
708
|
+
|
|
709
|
+
const baseAmount = price?.base_amount;
|
|
710
|
+
if (baseAmount) {
|
|
711
|
+
totalBaseAmount = totalBaseAmount.add(toScaledBaseAmount(baseAmount, item.quantity));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if ((item as any)?.expires_at) {
|
|
715
|
+
expiresAt = expiresAt === null ? (item as any)?.expires_at : Math.min(expiresAt, (item as any)?.expires_at);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
baseAmount: formatScaledAmount(totalBaseAmount, BASE_AMOUNT_SCALE),
|
|
721
|
+
baseCurrency: ((dynamicItems[0]?.upsell_price || dynamicItems[0]?.price) as any)?.base_currency || 'USD',
|
|
722
|
+
tokenAmount: fromUnitToToken(totalTokenAmount, currency.decimal).toString(),
|
|
723
|
+
tokenSymbol: currency.symbol,
|
|
724
|
+
expiresAt,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
416
728
|
export function formatLineItemPricing(
|
|
417
729
|
item: TLineItemExpanded,
|
|
418
730
|
currency: TPaymentCurrency,
|
|
419
|
-
{
|
|
731
|
+
{
|
|
732
|
+
trialEnd,
|
|
733
|
+
trialInDays,
|
|
734
|
+
exchangeRate = null,
|
|
735
|
+
}: { trialEnd: number; trialInDays: number; exchangeRate?: string | null },
|
|
420
736
|
locale: string = 'en'
|
|
421
737
|
): { primary: string; secondary?: string; quantity: string } {
|
|
422
738
|
if (!currency) {
|
|
423
739
|
return { primary: '', secondary: '', quantity: '' };
|
|
424
740
|
}
|
|
425
741
|
const price = item.upsell_price || item.price;
|
|
742
|
+
if (!price) {
|
|
743
|
+
return { primary: '', secondary: '', quantity: '' };
|
|
744
|
+
}
|
|
426
745
|
|
|
427
746
|
let quantity = t('common.qty', locale, { count: item.quantity });
|
|
428
747
|
if (price.recurring?.usage_type === 'metered' || +item.quantity === 1) {
|
|
429
748
|
quantity = '';
|
|
430
749
|
}
|
|
431
|
-
const
|
|
750
|
+
const { unitAmount, totalAmount } = getLineItemAmounts(item, currency, { exchangeRate });
|
|
751
|
+
const isDynamic = (price as any)?.pricing_type === 'dynamic';
|
|
752
|
+
|
|
753
|
+
const formatLineItemAmount = (bn: BN) => {
|
|
754
|
+
const value = fromUnitToToken(bn, currency.decimal);
|
|
755
|
+
return formatDynamicPrice(value, isDynamic, 6);
|
|
756
|
+
};
|
|
432
757
|
|
|
433
|
-
const total = `${
|
|
758
|
+
const total = `${formatLineItemAmount(totalAmount)} ${currency.symbol}`;
|
|
434
759
|
|
|
435
|
-
const unit = `${
|
|
760
|
+
const unit = `${formatLineItemAmount(unitAmount)} ${currency.symbol}`;
|
|
436
761
|
|
|
437
762
|
const trialResult = getFreeTrialTime({ trialInDays, trialEnd }, locale);
|
|
438
763
|
|
|
@@ -563,9 +888,16 @@ export function getCheckoutAmount(
|
|
|
563
888
|
items: TLineItemExpanded[],
|
|
564
889
|
currency: TPaymentCurrency,
|
|
565
890
|
trialing = false,
|
|
566
|
-
upsell = true
|
|
891
|
+
upsell = true,
|
|
892
|
+
{ exchangeRate = null }: { exchangeRate?: string | null } = {}
|
|
567
893
|
) {
|
|
568
|
-
if (
|
|
894
|
+
if (
|
|
895
|
+
items.find((x) => {
|
|
896
|
+
const price = upsell ? x.upsell_price || x.price : x.price;
|
|
897
|
+
return price?.custom_unit_amount;
|
|
898
|
+
}) &&
|
|
899
|
+
items.length > 1
|
|
900
|
+
) {
|
|
569
901
|
throw new Error('Multiple items with custom unit amount are not supported');
|
|
570
902
|
}
|
|
571
903
|
|
|
@@ -576,20 +908,22 @@ export function getCheckoutAmount(
|
|
|
576
908
|
return price != null;
|
|
577
909
|
})
|
|
578
910
|
.reduce((acc, x) => {
|
|
579
|
-
|
|
911
|
+
// For custom_amount, we need to check if it was created for the current currency
|
|
912
|
+
// custom_amount can be set from: 1) quoted_amount when a quote is applied, or 2) user input for custom pricing
|
|
913
|
+
// If quote_currency_id exists and doesn't match current currency, we should NOT use custom_amount
|
|
914
|
+
// because the quote was created for a different currency (e.g., ABT quote shouldn't be used for USD)
|
|
915
|
+
const quoteCurrencyId = (x as any)?.quote_currency_id;
|
|
916
|
+
const isQuoteForDifferentCurrency = quoteCurrencyId && quoteCurrencyId !== currency.id;
|
|
917
|
+
|
|
918
|
+
if (x.custom_amount && !isQuoteForDifferentCurrency) {
|
|
580
919
|
return acc.add(new BN(x.custom_amount));
|
|
581
920
|
}
|
|
582
921
|
|
|
583
922
|
const price = upsell ? x.upsell_price || x.price : x.price;
|
|
584
|
-
const
|
|
585
|
-
if (price.custom_unit_amount) {
|
|
586
|
-
if (unitPrice) {
|
|
587
|
-
return acc.add(new BN(unitPrice).mul(new BN(x.quantity)));
|
|
588
|
-
}
|
|
589
|
-
}
|
|
923
|
+
const { totalAmount } = getLineItemAmounts(x, currency, { useUpsell: upsell, exchangeRate });
|
|
590
924
|
|
|
591
925
|
if (price?.type === 'recurring') {
|
|
592
|
-
renew = renew.add(
|
|
926
|
+
renew = renew.add(totalAmount);
|
|
593
927
|
|
|
594
928
|
if (trialing) {
|
|
595
929
|
return acc;
|
|
@@ -599,11 +933,18 @@ export function getCheckoutAmount(
|
|
|
599
933
|
}
|
|
600
934
|
}
|
|
601
935
|
|
|
602
|
-
return acc.add(
|
|
936
|
+
return acc.add(totalAmount);
|
|
603
937
|
}, new BN(0))
|
|
604
938
|
.toString();
|
|
605
939
|
|
|
606
|
-
return {
|
|
940
|
+
return {
|
|
941
|
+
subtotal: total,
|
|
942
|
+
total,
|
|
943
|
+
renew: formatDynamicPrice(renew.toString(), !!exchangeRate),
|
|
944
|
+
discount: '0',
|
|
945
|
+
shipping: '0',
|
|
946
|
+
tax: '0',
|
|
947
|
+
};
|
|
607
948
|
}
|
|
608
949
|
|
|
609
950
|
export function getRecurringPeriod(recurring: PriceRecurring) {
|
|
@@ -631,7 +972,7 @@ export function formatUpsellSaving(items: TLineItemExpanded[], currency: TPaymen
|
|
|
631
972
|
if (items[0]?.upsell_price_id) {
|
|
632
973
|
return '0';
|
|
633
974
|
}
|
|
634
|
-
if (!items[0]?.price
|
|
975
|
+
if (!items[0]?.price?.upsell?.upsells_to) {
|
|
635
976
|
return '0';
|
|
636
977
|
}
|
|
637
978
|
|
|
@@ -673,6 +1014,18 @@ export function formatMeteredThen(
|
|
|
673
1014
|
return t('payment.checkout.then', locale, { subscription, recurring });
|
|
674
1015
|
}
|
|
675
1016
|
|
|
1017
|
+
export function formatThenValue(
|
|
1018
|
+
subscription: string,
|
|
1019
|
+
recurring: string,
|
|
1020
|
+
hasMetered: boolean,
|
|
1021
|
+
locale: string = 'en'
|
|
1022
|
+
): string {
|
|
1023
|
+
if (hasMetered) {
|
|
1024
|
+
return t('payment.checkout.metered', locale, { recurring });
|
|
1025
|
+
}
|
|
1026
|
+
return [subscription, recurring].filter(Boolean).join(' ');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
676
1029
|
export function formatPriceDisplay(
|
|
677
1030
|
{ amount, then, actualAmount, showThen }: { amount: string; then?: string; actualAmount: string; showThen?: boolean },
|
|
678
1031
|
recurring: string,
|
|
@@ -742,18 +1095,20 @@ export function formatCheckoutHeadlines(
|
|
|
742
1095
|
items: TLineItemExpanded[],
|
|
743
1096
|
currency: TPaymentCurrency,
|
|
744
1097
|
{ trialInDays, trialEnd }: { trialInDays: number; trialEnd: number },
|
|
745
|
-
locale: string = 'en'
|
|
1098
|
+
locale: string = 'en',
|
|
1099
|
+
{ exchangeRate = null }: { exchangeRate?: string | null } = {}
|
|
746
1100
|
): {
|
|
747
1101
|
action: string;
|
|
748
1102
|
amount: string;
|
|
749
1103
|
then?: string;
|
|
1104
|
+
thenValue?: string;
|
|
750
1105
|
secondary?: string;
|
|
751
1106
|
showThen?: boolean;
|
|
752
1107
|
actualAmount: string;
|
|
753
1108
|
priceDisplay: string;
|
|
754
1109
|
} {
|
|
755
1110
|
const brand = getStatementDescriptor(items);
|
|
756
|
-
const { total } = getCheckoutAmount(items, currency, trialInDays > 0);
|
|
1111
|
+
const { total } = getCheckoutAmount(items, currency, trialInDays > 0, true, { exchangeRate });
|
|
757
1112
|
const actualAmount = fromUnitToToken(total, currency.decimal);
|
|
758
1113
|
const amount = `${fromUnitToToken(total, currency.decimal)} ${currency.symbol}`;
|
|
759
1114
|
const trialResult = getFreeTrialTime({ trialInDays, trialEnd }, locale);
|
|
@@ -769,10 +1124,10 @@ export function formatCheckoutHeadlines(
|
|
|
769
1124
|
};
|
|
770
1125
|
}
|
|
771
1126
|
|
|
772
|
-
const { name } = items[0]?.price
|
|
1127
|
+
const { name } = items[0]?.price?.product || { name: '' };
|
|
773
1128
|
|
|
774
1129
|
// all one time
|
|
775
|
-
if (items.every((x) => x.price
|
|
1130
|
+
if (items.every((x) => x.price?.type === 'one_time')) {
|
|
776
1131
|
const action = t('payment.checkout.pay', locale, { payee: brand });
|
|
777
1132
|
if (items.length > 1) {
|
|
778
1133
|
return { action, amount, actualAmount, priceDisplay: amount };
|
|
@@ -781,31 +1136,34 @@ export function formatCheckoutHeadlines(
|
|
|
781
1136
|
return { action, amount, then: '', actualAmount, priceDisplay: amount };
|
|
782
1137
|
}
|
|
783
1138
|
|
|
784
|
-
const item = items.find((x) => x.price
|
|
1139
|
+
const item = items.find((x) => x.price?.type === 'recurring');
|
|
785
1140
|
const recurring = formatRecurring(
|
|
786
1141
|
(item?.upsell_price || item?.price)?.recurring as PriceRecurring,
|
|
787
1142
|
false,
|
|
788
1143
|
'per',
|
|
789
1144
|
locale
|
|
790
1145
|
);
|
|
791
|
-
|
|
1146
|
+
|
|
1147
|
+
const hasMetered = items.some((x) => x.price?.type === 'recurring' && x.price?.recurring?.usage_type === 'metered');
|
|
792
1148
|
const differentRecurring = hasMultipleRecurringIntervals(items);
|
|
793
1149
|
// all recurring
|
|
794
|
-
if (items.every((x) => x.price
|
|
1150
|
+
if (items.every((x) => x.price?.type === 'recurring')) {
|
|
795
1151
|
// check if there has different recurring price
|
|
796
1152
|
const subscription = [
|
|
797
1153
|
hasMetered ? t('payment.checkout.least', locale) : '',
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1154
|
+
formatDynamicPrice(
|
|
1155
|
+
fromUnitToToken(
|
|
1156
|
+
items.reduce((acc, x) => {
|
|
1157
|
+
const price = (x.upsell_price || x.price) as any;
|
|
1158
|
+
if (price?.recurring?.usage_type === 'metered') {
|
|
1159
|
+
return acc;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return acc.add(getLineItemAmounts(x, currency, { exchangeRate }).totalAmount);
|
|
1163
|
+
}, new BN(0)),
|
|
1164
|
+
currency.decimal
|
|
1165
|
+
),
|
|
1166
|
+
!!exchangeRate
|
|
809
1167
|
),
|
|
810
1168
|
currency.symbol,
|
|
811
1169
|
]
|
|
@@ -813,91 +1171,148 @@ export function formatCheckoutHeadlines(
|
|
|
813
1171
|
.join(' ');
|
|
814
1172
|
if (items.length > 1) {
|
|
815
1173
|
if (trialResult.count > 0) {
|
|
1174
|
+
const thenDisplay = formatMeteredThen(
|
|
1175
|
+
subscription,
|
|
1176
|
+
recurring,
|
|
1177
|
+
hasMetered && Number(subscription) === 0,
|
|
1178
|
+
locale
|
|
1179
|
+
);
|
|
1180
|
+
const thenValue = formatThenValue(subscription, recurring, hasMetered && Number(subscription) === 0, locale);
|
|
816
1181
|
const result = {
|
|
817
1182
|
action: t('payment.checkout.try2', locale, { name, count: items.length - 1 }),
|
|
818
1183
|
amount: t('payment.checkout.free', locale, { count: trialResult.count, interval: trialResult.interval }),
|
|
819
|
-
then:
|
|
1184
|
+
then: thenDisplay,
|
|
1185
|
+
thenValue,
|
|
820
1186
|
showThen: true,
|
|
821
1187
|
actualAmount: '0',
|
|
822
1188
|
};
|
|
823
1189
|
return {
|
|
824
1190
|
...result,
|
|
825
|
-
priceDisplay: formatPriceDisplay(
|
|
1191
|
+
priceDisplay: formatPriceDisplay(
|
|
1192
|
+
{ amount: result.amount, then: thenDisplay, actualAmount: result.actualAmount, showThen: result.showThen },
|
|
1193
|
+
recurring,
|
|
1194
|
+
hasMetered,
|
|
1195
|
+
locale
|
|
1196
|
+
),
|
|
826
1197
|
};
|
|
827
1198
|
}
|
|
1199
|
+
const thenDisplay = hasMetered ? t('payment.checkout.meteredThen', locale, { recurring }) : recurring;
|
|
1200
|
+
const thenValue = hasMetered ? t('payment.checkout.metered', locale, { recurring }) : recurring;
|
|
828
1201
|
const result = {
|
|
829
1202
|
action: t('payment.checkout.sub2', locale, { name, count: items.length - 1 }),
|
|
830
1203
|
amount,
|
|
831
|
-
then:
|
|
1204
|
+
then: thenDisplay,
|
|
1205
|
+
thenValue,
|
|
832
1206
|
showThen: hasMetered,
|
|
833
1207
|
actualAmount,
|
|
834
1208
|
};
|
|
835
1209
|
return {
|
|
836
1210
|
...result,
|
|
837
|
-
priceDisplay: formatPriceDisplay(
|
|
1211
|
+
priceDisplay: formatPriceDisplay(
|
|
1212
|
+
{ amount: result.amount, then: thenDisplay, actualAmount: result.actualAmount, showThen: result.showThen },
|
|
1213
|
+
recurring,
|
|
1214
|
+
hasMetered,
|
|
1215
|
+
locale
|
|
1216
|
+
),
|
|
838
1217
|
};
|
|
839
1218
|
}
|
|
840
1219
|
|
|
841
1220
|
if (trialResult.count > 0) {
|
|
1221
|
+
const thenDisplay = formatMeteredThen(subscription, recurring, hasMetered && Number(subscription) === 0, locale);
|
|
1222
|
+
const thenValue = formatThenValue(subscription, recurring, hasMetered && Number(subscription) === 0, locale);
|
|
842
1223
|
const result = {
|
|
843
1224
|
action: t('payment.checkout.try1', locale, { name }),
|
|
844
1225
|
amount: t('payment.checkout.free', locale, { count: trialResult.count, interval: trialResult.interval }),
|
|
845
|
-
then:
|
|
1226
|
+
then: thenDisplay,
|
|
1227
|
+
thenValue,
|
|
846
1228
|
showThen: true,
|
|
847
1229
|
actualAmount: '0',
|
|
848
1230
|
};
|
|
849
1231
|
return {
|
|
850
1232
|
...result,
|
|
851
|
-
priceDisplay: formatPriceDisplay(
|
|
1233
|
+
priceDisplay: formatPriceDisplay(
|
|
1234
|
+
{ amount: result.amount, then: thenDisplay, actualAmount: result.actualAmount, showThen: result.showThen },
|
|
1235
|
+
recurring,
|
|
1236
|
+
hasMetered,
|
|
1237
|
+
locale
|
|
1238
|
+
),
|
|
852
1239
|
};
|
|
853
1240
|
}
|
|
1241
|
+
const thenDisplay = hasMetered ? t('payment.checkout.meteredThen', locale, { recurring }) : recurring;
|
|
1242
|
+
const thenValue = hasMetered ? t('payment.checkout.metered', locale, { recurring }) : recurring;
|
|
854
1243
|
const result = {
|
|
855
1244
|
action: t('payment.checkout.sub1', locale, { name }),
|
|
856
1245
|
amount,
|
|
857
|
-
then:
|
|
1246
|
+
then: thenDisplay,
|
|
1247
|
+
thenValue,
|
|
858
1248
|
showThen: hasMetered && !differentRecurring,
|
|
859
1249
|
actualAmount,
|
|
860
1250
|
};
|
|
861
1251
|
return {
|
|
862
1252
|
...result,
|
|
863
|
-
priceDisplay: formatPriceDisplay(
|
|
1253
|
+
priceDisplay: formatPriceDisplay(
|
|
1254
|
+
{ amount: result.amount, then: thenDisplay, actualAmount: result.actualAmount, showThen: result.showThen },
|
|
1255
|
+
recurring,
|
|
1256
|
+
hasMetered,
|
|
1257
|
+
locale
|
|
1258
|
+
),
|
|
864
1259
|
};
|
|
865
1260
|
}
|
|
866
1261
|
|
|
867
1262
|
// mixed
|
|
868
1263
|
const subscription = fromUnitToToken(
|
|
869
1264
|
items
|
|
870
|
-
.filter((x) => x.price
|
|
1265
|
+
.filter((x) => x.price?.type === 'recurring')
|
|
871
1266
|
.reduce((acc, x) => {
|
|
872
|
-
if (x.price
|
|
1267
|
+
if (x.price?.recurring?.usage_type === 'metered') {
|
|
873
1268
|
return acc;
|
|
874
1269
|
}
|
|
875
|
-
return acc.add(
|
|
1270
|
+
return acc.add(getLineItemAmounts(x, currency, { useUpsell: false, exchangeRate }).totalAmount);
|
|
876
1271
|
}, new BN(0)),
|
|
877
1272
|
currency.decimal
|
|
878
1273
|
);
|
|
879
1274
|
|
|
1275
|
+
const thenDisplay = formatMeteredThen(
|
|
1276
|
+
`${subscription} ${currency.symbol}`,
|
|
1277
|
+
recurring,
|
|
1278
|
+
hasMetered && Number(subscription) === 0,
|
|
1279
|
+
locale
|
|
1280
|
+
);
|
|
1281
|
+
const thenValue = formatThenValue(
|
|
1282
|
+
`${subscription} ${currency.symbol}`,
|
|
1283
|
+
recurring,
|
|
1284
|
+
hasMetered && Number(subscription) === 0,
|
|
1285
|
+
locale
|
|
1286
|
+
);
|
|
880
1287
|
const result = {
|
|
881
1288
|
action: t('payment.checkout.pay', locale, { payee: brand }),
|
|
882
1289
|
amount,
|
|
883
|
-
then:
|
|
884
|
-
|
|
885
|
-
recurring,
|
|
886
|
-
hasMetered && Number(subscription) === 0,
|
|
887
|
-
locale
|
|
888
|
-
),
|
|
1290
|
+
then: thenDisplay,
|
|
1291
|
+
thenValue,
|
|
889
1292
|
showThen: !differentRecurring,
|
|
890
1293
|
actualAmount,
|
|
891
1294
|
};
|
|
892
1295
|
|
|
893
1296
|
return {
|
|
894
1297
|
...result,
|
|
895
|
-
priceDisplay: formatPriceDisplay(
|
|
1298
|
+
priceDisplay: formatPriceDisplay(
|
|
1299
|
+
{ amount: result.amount, then: thenDisplay, actualAmount: result.actualAmount, showThen: result.showThen },
|
|
1300
|
+
recurring,
|
|
1301
|
+
hasMetered,
|
|
1302
|
+
locale
|
|
1303
|
+
),
|
|
896
1304
|
};
|
|
897
1305
|
}
|
|
898
1306
|
|
|
899
|
-
export function formatAmount(amount: string, decimals: number) {
|
|
900
|
-
|
|
1307
|
+
export function formatAmount(amount: string, decimals: number, precision: number = 2) {
|
|
1308
|
+
const tokenAmount = fromUnitToToken(amount, decimals);
|
|
1309
|
+
const numericValue = Number(tokenAmount);
|
|
1310
|
+
if (!Number.isFinite(numericValue)) {
|
|
1311
|
+
return formatBNStr(amount, decimals, precision, true, false);
|
|
1312
|
+
}
|
|
1313
|
+
const abs = Math.abs(numericValue);
|
|
1314
|
+
const targetPrecision = abs > 0 && abs < 0.01 ? decimals : 2;
|
|
1315
|
+
return formatNumber(tokenAmount, targetPrecision, true, false);
|
|
901
1316
|
}
|
|
902
1317
|
|
|
903
1318
|
export function findCurrency(methods: TPaymentMethodExpanded[], currencyId: string): TPaymentCurrencyExpanded | null {
|
|
@@ -1386,7 +1801,7 @@ export function showStaking(method: TPaymentMethod, currency: TPaymentCurrency,
|
|
|
1386
1801
|
if (noStake) {
|
|
1387
1802
|
return false;
|
|
1388
1803
|
}
|
|
1389
|
-
if (method.type === 'arcblock') {
|
|
1804
|
+
if (method && method.type === 'arcblock') {
|
|
1390
1805
|
return currency.type !== 'credit';
|
|
1391
1806
|
}
|
|
1392
1807
|
return false;
|