UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

390 lines (388 loc) 14.3 kB
"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;