@blocklet/payment-react
Version:
Reusable react components for payment kit v2
390 lines (388 loc) • 14.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useDynamicPricing = useDynamicPricing;
var _react = require("react");
var _util = require("@ocap/util");
var _util2 = require("../libs/util");
function extractQuoteMeta(items) {
if (!items?.length) {
return null;
}
let exchangeRate = null;
let baseCurrency = null;
let expiresAt = null;
let providerName = null;
let providerId = null;
let rateTimestampMs = null;
let slippagePercent = null;
items.forEach(item => {
const price = item.upsell_price || item.price;
const rate = item?.exchange_rate;
if (!exchangeRate && rate) {
exchangeRate = rate;
}
const base = price?.base_currency;
if (!baseCurrency && base) {
baseCurrency = base;
}
const expires = item?.expires_at;
if (expires) {
expiresAt = expiresAt === null ? expires : Math.min(expiresAt, expires);
}
const itemProviderName = item?.rate_provider_name;
if (!providerName && itemProviderName) {
providerName = itemProviderName;
}
const itemProviderId = item?.rate_provider_id;
if (!providerId && itemProviderId) {
providerId = itemProviderId;
}
const itemRateTimestamp = item?.rate_timestamp_ms;
if (!rateTimestampMs && Number.isFinite(Number(itemRateTimestamp))) {
rateTimestampMs = Number(itemRateTimestamp);
}
const itemSlippage = item?.slippage_percent;
if (slippagePercent === null && Number.isFinite(Number(itemSlippage))) {
slippagePercent = Number(itemSlippage);
}
});
return {
exchangeRate,
baseCurrency: baseCurrency || "USD",
expiresAt,
providerName,
providerId,
rateTimestampMs,
slippagePercent
};
}
function calculateTokenAmount(items, rate, currencyDecimal, trialing = false) {
const usdTotal = items.reduce((acc, item) => {
const price = item.upsell_price || item.price;
if (trialing && price?.type === "recurring") {
return acc;
}
if (price?.type === "recurring" && price?.recurring?.usage_type === "metered") {
return acc;
}
const baseAmount = price?.base_amount;
if (baseAmount) {
const quantity = item.quantity || 1;
return acc + Number(baseAmount) * quantity;
}
return acc;
}, 0);
if (usdTotal <= 0) {
return null;
}
const rateNum = Number(rate);
if (rateNum <= 0) {
return null;
}
const tokenAmount = usdTotal / rateNum;
const tokenAmountUnit = (0, _util.fromTokenToUnit)(tokenAmount.toFixed(currencyDecimal || 8), currencyDecimal || 8);
return tokenAmountUnit.toString();
}
function extractQuoteLockedAt(paymentIntent, checkoutSession) {
if (paymentIntent?.quote_locked_at) {
const lockedAtMs = new Date(paymentIntent.quote_locked_at).getTime();
if (Number.isFinite(lockedAtMs)) {
return Math.floor(lockedAtMs / 1e3);
}
}
const metaLockedAt = checkoutSession?.metadata?.quote_locked_at;
if (typeof metaLockedAt === "number") {
return metaLockedAt;
}
if (typeof metaLockedAt === "string") {
const parsed = Number(metaLockedAt);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function useDynamicPricing(options) {
const {
items,
currency,
liveRate,
liveQuoteSnapshot,
checkoutSession,
paymentIntent,
locale = "en",
isStripePayment = false,
isSubscription = checkoutSession?.mode === "subscription" || checkoutSession?.mode === "setup",
slippageConfig,
trialInDays = 0,
trialEnd = 0,
discounts
} = options;
const currentTime = Math.floor(Date.now() / 1e3);
const trialing = trialInDays > 0 || trialEnd > currentTime;
const hasDynamicPricing = (0, _react.useMemo)(() => items.some(item => (item.upsell_price || item.price)?.pricing_type === "dynamic"), [items]);
const quoteMeta = (0, _react.useMemo)(() => hasDynamicPricing ? extractQuoteMeta(items) : null, [items, hasDynamicPricing]);
const rateInfo = (0, _react.useMemo)(() => ({
exchangeRate: isStripePayment ? null : liveRate?.rate || quoteMeta?.exchangeRate || null,
baseCurrency: liveRate?.base_currency || quoteMeta?.baseCurrency || "USD",
providerName: isStripePayment ? null : liveRate?.provider_name || quoteMeta?.providerName || null,
providerId: isStripePayment ? null : liveRate?.provider_id || quoteMeta?.providerId || null,
timestampMs: isStripePayment ? null : liveRate?.timestamp_ms || quoteMeta?.rateTimestampMs || null,
fetchedAt: isStripePayment ? null : liveRate?.fetched_at || null
// When we fetched the data
}), [liveRate, quoteMeta, isStripePayment]);
const calculatedTokenAmount = (0, _react.useMemo)(() => {
if (isStripePayment || !hasDynamicPricing || !liveRate?.rate) {
return null;
}
return calculateTokenAmount(items, liveRate.rate, currency.decimal, trialing);
}, [hasDynamicPricing, liveRate?.rate, items, currency.decimal, isStripePayment, trialing]);
const calculatedDiscountAmount = (0, _react.useMemo)(() => {
if (!discounts?.length) {
return null;
}
const couponDetails = discounts[0]?.coupon_details;
if (!couponDetails) {
return null;
}
if (isStripePayment) {
const discountableSubtotalCents = items.reduce((sum, item) => {
if (!item.discountable) {
return sum;
}
const price = item.upsell_price || item.price;
if (trialing && price?.type === "recurring") {
return sum;
}
if (price?.type === "recurring" && price?.recurring?.usage_type === "metered") {
return sum;
}
const isDynamic = price?.pricing_type === "dynamic";
const baseAmount = price?.base_amount;
let amountCents;
if (isDynamic && baseAmount !== void 0 && baseAmount !== null) {
amountCents = Number(baseAmount) * 100;
} else {
const unitAmount = price?.unit_amount;
if (!unitAmount) {
return sum;
}
amountCents = Number(unitAmount);
}
const quantity = item.quantity || 1;
return sum + amountCents * quantity;
}, 0);
if (discountableSubtotalCents <= 0) {
return null;
}
if (couponDetails.percent_off && couponDetails.percent_off > 0) {
const discountCents = Math.round(discountableSubtotalCents * couponDetails.percent_off / 100);
return discountCents.toString();
}
if (couponDetails.amount_off) {
const amountOffCents = Number(couponDetails.amount_off);
if (Number.isFinite(amountOffCents) && amountOffCents > 0) {
return Math.min(amountOffCents, discountableSubtotalCents).toString();
}
}
return null;
}
if (!hasDynamicPricing || !liveRate?.rate) {
return null;
}
const rateNum = Number(liveRate.rate);
if (rateNum <= 0) {
return null;
}
const discountableSubtotal = items.reduce((sum, item) => {
if (!item.discountable) {
return sum;
}
const price = item.upsell_price || item.price;
if (trialing && price?.type === "recurring") {
return sum;
}
if (price?.type === "recurring" && price?.recurring?.usage_type === "metered") {
return sum;
}
const baseAmount = price?.base_amount;
if (!baseAmount) {
return sum;
}
const quantity = item.quantity || 1;
const tokenAmount = Number(baseAmount) * quantity / rateNum;
return sum + tokenAmount;
}, 0);
if (discountableSubtotal <= 0) {
return null;
}
if (couponDetails.percent_off && couponDetails.percent_off > 0) {
const discountAmount = discountableSubtotal * couponDetails.percent_off / 100;
const discountAmountUnit = (0, _util.fromTokenToUnit)(discountAmount.toFixed(currency.decimal || 8), currency.decimal || 8);
return discountAmountUnit.toString();
}
if (couponDetails.amount_off) {
const amountOff = couponDetails.currency_id === currency.id ? couponDetails.amount_off : couponDetails.currency_options?.[currency.id]?.amount_off;
if (!amountOff) {
return null;
}
const subtotalUnit = (0, _util.fromTokenToUnit)(discountableSubtotal.toFixed(currency.decimal || 8), currency.decimal || 8);
const amountOffBN = new _util.BN(amountOff);
const subtotalBN = new _util.BN(subtotalUnit);
return _util.BN.min(amountOffBN, subtotalBN).toString();
}
return null;
}, [isStripePayment, hasDynamicPricing, liveRate?.rate, discounts, items, trialing, currency.decimal, currency.id]);
const rateDisplay = (0, _react.useMemo)(() => {
if (!rateInfo.exchangeRate) {
return null;
}
const formattedRate = (0, _util2.formatExchangeRate)(rateInfo.exchangeRate);
if (!formattedRate) {
return null;
}
if (rateInfo.baseCurrency === "USD") {
return `$${formattedRate}`;
}
return `${formattedRate} ${rateInfo.baseCurrency}`;
}, [rateInfo]);
const quoteLockedAt = (0, _react.useMemo)(() => extractQuoteLockedAt(paymentIntent, checkoutSession), [paymentIntent, checkoutSession]);
const isPriceLocked = (0, _react.useMemo)(() => {
if (!quoteLockedAt) return false;
const now = Math.floor(Date.now() / 1e3);
const lockRemaining = quoteLockedAt + 180 - now;
return lockRemaining > 0;
}, [quoteLockedAt]);
const lockExpired = (0, _react.useMemo)(() => {
if (!quoteLockedAt) return false;
const now = Math.floor(Date.now() / 1e3);
const lockRemaining = quoteLockedAt + 180 - now;
return lockRemaining <= 0;
}, [quoteLockedAt]);
const currentSlippagePercent = (0, _react.useMemo)(() => {
let slippageValue = quoteMeta?.slippagePercent;
if (slippageValue === null || slippageValue === void 0) {
slippageValue = slippageConfig?.percent ?? checkoutSession?.metadata?.slippage?.percent ?? checkoutSession?.slippage_percent;
}
if (slippageValue === null || slippageValue === void 0) {
slippageValue = 0.5;
}
return slippageValue;
}, [quoteMeta?.slippagePercent, slippageConfig?.percent, checkoutSession]);
const providerDisplay = (0, _react.useMemo)(() => {
const fallback = "\u2014";
if (liveRate?.provider_display) {
return liveRate.provider_display;
}
if (!rateInfo.providerName && !rateInfo.providerId) {
return fallback;
}
return rateInfo.providerName || rateInfo.providerId || fallback;
}, [liveRate?.provider_display, rateInfo.providerName, rateInfo.providerId]);
const formatTotalDisplay = (totalAmountValue, fallback = "\u2014") => {
if (hasDynamicPricing && calculatedTokenAmount) {
const displayAmount = (0, _util.fromUnitToToken)(calculatedTokenAmount, currency.decimal);
const numericValue2 = Number(displayAmount);
if (Number.isFinite(numericValue2)) {
return (0, _util2.formatDynamicPrice)(displayAmount, true, 6);
}
}
if (hasDynamicPricing && liveQuoteSnapshot?.quoted_amount) {
const snapshotAmount = (0, _util.fromUnitToToken)(liveQuoteSnapshot.quoted_amount, currency.decimal);
const numericValue2 = Number(snapshotAmount);
if (Number.isFinite(numericValue2)) {
return (0, _util2.formatDynamicPrice)(snapshotAmount, true, 6);
}
}
if (totalAmountValue === null || totalAmountValue === void 0 || totalAmountValue === "") {
return fallback;
}
const numericValue = Number(totalAmountValue);
if (!Number.isFinite(numericValue)) {
return fallback;
}
return (0, _util2.formatDynamicPrice)(totalAmountValue, hasDynamicPricing, 6);
};
const calculateUsdDisplay = totalAmountValue => {
const fallback = "\u2014";
if (!hasDynamicPricing || !totalAmountValue || totalAmountValue === fallback) {
return null;
}
const numericValue = Number(totalAmountValue);
if (!Number.isFinite(numericValue) || numericValue < 0) {
return null;
}
if (numericValue === 0) {
return (0, _util2.formatUsdAmount)("0", locale);
}
const {
exchangeRate
} = rateInfo;
if (!exchangeRate) {
return null;
}
const totalAmountInUnits = (0, _util.fromTokenToUnit)(totalAmountValue, currency.decimal);
const totalUsd = (0, _util2.getUsdAmountFromTokenUnits)(new _util.BN(totalAmountInUnits), currency.decimal, exchangeRate);
if (!totalUsd) {
return null;
}
return (0, _util2.formatUsdAmount)(totalUsd, locale);
};
const buildQuoteDetailRows = t => {
if (!hasDynamicPricing) {
return [];
}
const rows = [];
const fallback = "\u2014";
if (rateDisplay) {
const displayTimestamp = rateInfo.fetchedAt || rateInfo.timestampMs;
const updatedAt = displayTimestamp ? (0, _util2.formatDateTime)(displayTimestamp) : fallback;
rows.push({
label: t("payment.checkout.quote.detailProvider"),
value: providerDisplay
}, {
label: t("payment.checkout.quote.detailUpdatedAt"),
value: updatedAt
});
}
if (isSubscription) {
let slippageDisplay = `${currentSlippagePercent}%`;
if (slippageConfig?.mode === "rate" && slippageConfig.min_acceptable_rate) {
const formattedRate = (0, _util2.formatExchangeRate)(slippageConfig.min_acceptable_rate);
const displayRate = formattedRate || slippageConfig.min_acceptable_rate;
const displayCurrency = slippageConfig.base_currency || rateInfo.baseCurrency || "USD";
const displayText = displayCurrency === "USD" ? `$${displayRate}` : `${displayRate} ${displayCurrency}`;
slippageDisplay = `${displayText}`;
}
rows.push({
label: t("payment.checkout.quote.detailSlippage"),
value: slippageDisplay,
isSlippage: true,
tooltip: t("payment.checkout.quote.slippage.tooltip")
});
}
return rows;
};
return {
// Flags
hasDynamicPricing,
isPriceLocked,
lockExpired,
// Data
quoteMeta,
rateInfo,
quoteLockedAt,
calculatedTokenAmount,
calculatedDiscountAmount,
currentSlippagePercent,
// Display helpers
rateDisplay,
providerDisplay,
formatTotalDisplay,
calculateUsdDisplay,
buildQuoteDetailRows
};
}
module.exports = useDynamicPricing;