UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

1,058 lines (1,057 loc) 36.2 kB
import { BN, fromUnitToToken } from "@ocap/util"; import omit from "lodash/omit"; import trimEnd from "lodash/trimEnd"; import numbro from "numbro"; import stringWidth from "string-width"; import { defaultCountries } from "react-international-phone"; import { joinURL, withQuery } from "ufo"; import { t } from "../locales/index.js"; import dayjs from "./dayjs.js"; export const PAYMENT_KIT_DID = "z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk"; export const formatCouponTerms = (coupon, currency, locale = "en") => { let couponOff = ""; if (coupon.percent_off && coupon.percent_off > 0) { couponOff = t("payment.checkout.coupon.percentage", locale, { percent: coupon.percent_off }); } if (coupon.amount_off && coupon.amount_off !== "0") { const { symbol } = currency; couponOff = coupon.currency_id === currency.id ? coupon.amount_off || "" : coupon.currency_options?.[currency.id]?.amount_off || ""; if (couponOff) { couponOff = t("payment.checkout.coupon.fixedAmount", locale, { amount: formatAmount(couponOff, currency.decimal), symbol }); } } if (!couponOff) { return t("payment.checkout.coupon.noDiscount"); } return t(`payment.checkout.coupon.terms.${coupon.duration}`, locale, { couponOff, months: coupon.duration_in_months || 0 }); }; export const isPaymentKitMounted = () => { return (window.blocklet?.componentMountPoints || []).some((x) => x.did === PAYMENT_KIT_DID); }; export const getPrefix = () => { const prefix = window.blocklet?.prefix || "/"; const baseUrl = window.location?.origin; if (window.__PAYMENT_KIT_BASE_URL) { try { const tmp = new URL(window.__PAYMENT_KIT_BASE_URL); if (tmp.origin !== window.location.origin) { return window.__PAYMENT_KIT_BASE_URL; } } catch (err) { console.warn("Invalid baseUrl for PaymentProvider", window.__PAYMENT_KIT_BASE_URL); } } const componentId = (window.blocklet?.componentId || "").split("/").pop(); if (componentId === PAYMENT_KIT_DID) { return joinURL(baseUrl, prefix); } const component = (window.blocklet?.componentMountPoints || []).find((x) => x?.did === PAYMENT_KIT_DID); if (component) { return joinURL(baseUrl, component.mountPoint); } return joinURL(baseUrl, prefix); }; export function isCrossOrigin() { try { const prefix = getPrefix(); const prefixOrigin = new URL(prefix).origin; const currentOrigin = window.location.origin; return prefixOrigin !== currentOrigin; } catch (error) { return false; } } export function formatToDate(date, locale = "en", format = "YYYY-MM-DD HH:mm:ss") { if (!date) { return "-"; } return dayjs(date).locale(formatLocale(locale)).format(format); } export function formatToDatetime(date, locale = "en") { if (!date) { return "-"; } return dayjs(date).locale(formatLocale(locale)).format("lll"); } export function formatTime(date, format = "YYYY-MM-DD HH:mm:ss", locale = "en") { if (!date) { return "-"; } return dayjs(date).locale(formatLocale(locale)).format(format); } export function formatDateTime(date, locale = "en") { return dayjs(date).locale(formatLocale(locale)).format("YYYY-MM-DD HH:mm"); } export const formatLocale = (locale = "en") => { if (locale === "tw") { return "zh"; } return locale; }; export const formatPrettyMsLocale = (locale) => locale === "zh" ? "zh_CN" : "en_US"; export const formatError = (err) => { if (!err) { return "Unknown error"; } const { details, errors, response } = err; if (Array.isArray(errors)) { return errors.map((x) => x.message).join("\n"); } if (Array.isArray(details)) { const formatted = details.map((e) => { const errorMessage = e.message.replace(/["]/g, "'"); const errorPath = e.path.join("."); return `${errorPath}: ${errorMessage}`; }); return `Validate failed: ${formatted.join(";")}`; } if (response) { return response.data?.error || `${err.message}: ${JSON.stringify(response.data)}`; } return err.message; }; export function formatBNStr(str = "", decimals = 18, precision = 6, trim = true, thousandSeparated = true) { if (!str) { return "0"; } return formatNumber(fromUnitToToken(str, decimals), precision, trim, thousandSeparated); } export function formatNumber(n, precision = 6, trim = true, thousandSeparated = true) { if (!n || n === "0") { return "0"; } const num = numbro(n); const options = { thousandSeparated, ...(precision || precision === 0) && { mantissa: precision } }; const result = num.format(options); if (!trim) { return result; } const [left, right] = result.split("."); return right ? [left, trimEnd(right, "0")].filter(Boolean).join(".") : left; } export const formatPrice = (price, currency, unit_label, quantity = 1, bn = true, locale = "en") => { if (!currency) { return ""; } if (price.custom_unit_amount) { return `Custom (${currency.symbol})`; } const unit = getPriceUintAmountByCurrency(price, currency); const amount = bn ? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString() : +unit * quantity; if (price?.type === "recurring" && price.recurring) { const recurring = formatRecurring(price.recurring, false, "slash", locale); if (unit_label) { return `${amount} ${currency.symbol} / ${unit_label} ${recurring}`; } if (price.recurring.usage_type === "metered") { return `${amount} ${currency.symbol} / unit ${recurring}`; } return `${amount} ${currency.symbol} ${recurring}`; } return `${amount} ${currency.symbol}`; }; export const formatPriceAmount = (price, currency, unit_label, quantity = 1, bn = true) => { if (!currency) { return ""; } const unit = getPriceUintAmountByCurrency(price, currency); const amount = bn ? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString() : +unit * quantity; if (price?.type === "recurring" && price.recurring) { if (unit_label) { return `${amount} ${currency.symbol} / ${unit_label}`; } if (price.recurring.usage_type === "metered") { return `${amount} ${currency.symbol} / unit`; } return `${amount} ${currency.symbol}`; } return `${amount} ${currency.symbol}`; }; export function getStatementDescriptor(items) { for (const item of items) { if (item.price?.product?.statement_descriptor) { return item.price?.product?.statement_descriptor; } } return window.blocklet?.appName; } export function formatRecurring(recurring, translate = true, separator = "per", locale = "en") { const intervals = { hour: "hourly", day: "daily", week: "weekly", month: "monthly", year: "yearly" }; if (!recurring) { return ""; } if (+recurring.interval_count === 1) { const interval = t(`common.${recurring.interval}`, locale); return translate ? t(`common.${intervals[recurring.interval]}`, locale) : separator ? t(`common.${separator}`, locale, { interval }) : interval; } if (recurring.interval === "month") { if (recurring.interval_count === 3) { return t("common.month3", locale); } if (recurring.interval_count === 6) { return t("common.month6", locale); } } return t("common.recurring", locale, { count: recurring.interval_count, interval: t(`common.${recurring.interval}s`, locale) }); } export function getPriceUintAmountByCurrency(price, currency) { const options = getPriceCurrencyOptions(price); const option = options.find((x) => x.currency_id === currency?.id); if (option) { if (option.custom_unit_amount) { return option.custom_unit_amount.preset || option.custom_unit_amount.presets[0]; } return option.unit_amount; } if (price.currency_id === currency?.id) { if (price.custom_unit_amount) { return price.custom_unit_amount.preset || price.custom_unit_amount.presets[0]; } return price.unit_amount; } console.warn(`Currency ${currency?.id} not configured for price`, price); return "0"; } export function getPriceCurrencyOptions(price) { if (Array.isArray(price.currency_options)) { return price.currency_options; } return [ { currency_id: price.currency_id, unit_amount: price.unit_amount, custom_unit_amount: price.custom_unit_amount || null, tiers: null } ]; } export function formatLineItemPricing(item, currency, { trialEnd, trialInDays }, locale = "en") { if (!currency) { return { primary: "", secondary: "", quantity: "" }; } const price = item.upsell_price || item.price; let quantity = t("common.qty", locale, { count: item.quantity }); if (price.recurring?.usage_type === "metered" || +item.quantity === 1) { quantity = ""; } const unitValue = new BN(getPriceUintAmountByCurrency(price, currency)); const total = `${fromUnitToToken(unitValue.mul(new BN(item.quantity)), currency.decimal)} ${currency.symbol}`; const unit = `${fromUnitToToken(unitValue, currency.decimal)} ${currency.symbol}`; const trialResult = getFreeTrialTime({ trialInDays, trialEnd }, locale); const appendUnit = (v, alt) => { if (price.product.unit_label) { return `${v}/${price.product.unit_label}`; } if (price.recurring?.usage_type === "metered" || item.quantity === 1) { return alt; } return quantity ? t("common.each", locale, { unit }) : ""; }; if (price.type === "recurring" && price.recurring) { if (trialResult.count > 0) { return { primary: t("common.trial", locale, { count: trialResult.count, interval: trialResult.interval }), secondary: `${appendUnit(total, total)} ${formatRecurring(price.recurring, false, "slash", locale)}`, quantity }; } return { primary: total, secondary: appendUnit(total, ""), quantity }; } return { primary: total, secondary: appendUnit(total, ""), quantity }; } export function getSubscriptionStatusColor(status) { switch (status) { case "active": return "success"; case "trialing": return "primary"; case "incomplete": case "incomplete_expired": case "paused": return "warning"; case "past_due": case "unpaid": return "error"; case "canceled": default: return "default"; } } export function getPaymentIntentStatusColor(status) { switch (status) { case "succeeded": return "success"; case "requires_payment_method": case "requires_confirmation": case "requires_action": case "requires_capture": return "warning"; case "canceled": case "processing": default: return "default"; } } export function getRefundStatusColor(status) { switch (status) { case "succeeded": return "success"; case "requires_action": return "warning"; case "canceled": case "pending": default: return "default"; } } export function getPayoutStatusColor(status) { switch (status) { case "paid": return "success"; case "deferred": return "warning"; case "failed": return "warning"; case "canceled": case "pending": case "in_transit": default: return "default"; } } export function getInvoiceStatusColor(status) { switch (status) { case "paid": return "success"; case "open": return "secondary"; case "uncollectible": return "warning"; case "draft": case "void": default: return "default"; } } export function getWebhookStatusColor(status) { switch (status) { case "enabled": return "success"; case "disabled": default: return "default"; } } export function getCheckoutAmount(items, currency, trialing = false, upsell = true) { if (items.find((x) => (x.upsell_price || x.price).custom_unit_amount) && items.length > 1) { throw new Error("Multiple items with custom unit amount are not supported"); } let renew = new BN(0); const total = items.filter((x) => { const price = upsell ? x.upsell_price || x.price : x.price; return price != null; }).reduce((acc, x) => { if (x.custom_amount) { return acc.add(new BN(x.custom_amount)); } const price = upsell ? x.upsell_price || x.price : x.price; const unitPrice = getPriceUintAmountByCurrency(price, currency); if (price.custom_unit_amount) { if (unitPrice) { return acc.add(new BN(unitPrice).mul(new BN(x.quantity))); } } if (price?.type === "recurring") { renew = renew.add(new BN(unitPrice).mul(new BN(x.quantity))); if (trialing) { return acc; } if (price.recurring?.usage_type === "metered") { return acc; } } return acc.add(new BN(unitPrice).mul(new BN(x.quantity))); }, new BN(0)).toString(); return { subtotal: total, total, renew: renew.toString(), discount: "0", shipping: "0", tax: "0" }; } export function getRecurringPeriod(recurring) { const { interval } = recurring; const count = +recurring.interval_count || 1; const dayInMs = 24 * 60 * 60 * 1e3; switch (interval) { case "hour": return 60 * 60 * 1e3; case "day": return count * dayInMs; case "week": return count * 7 * dayInMs; case "month": return count * 30 * dayInMs; case "year": return count * 365 * dayInMs; default: throw new Error(`Unsupported recurring interval: ${interval}`); } } export function formatUpsellSaving(items, currency) { if (items[0]?.upsell_price_id) { return "0"; } if (!items[0]?.price.upsell?.upsells_to) { return "0"; } const from = getCheckoutAmount(items, currency, false, false); const to = getCheckoutAmount( items.map((x) => ({ ...x, upsell_price_id: x.price.upsell?.upsells_to_id, upsell_price: x.price.upsell?.upsells_to })), currency, false, true ); const fromRecurring = items[0].price?.recurring; const toRecurring = items[0].price?.upsell?.upsells_to?.recurring; if (!fromRecurring || !toRecurring) { return "0"; } const factor = Math.floor(getRecurringPeriod(toRecurring) / getRecurringPeriod(fromRecurring)); const before = new BN(from.total).mul(new BN(factor)); const after = new BN(to.total); return Number(before.sub(after).mul(new BN(100)).div(before).toString()).toFixed(0); } export function formatMeteredThen(subscription, recurring, hasMetered, locale = "en") { if (hasMetered) { return t("payment.checkout.meteredThen", locale, { subscription, recurring }); } return t("payment.checkout.then", locale, { subscription, recurring }); } export function formatPriceDisplay({ amount, then, actualAmount, showThen }, recurring, hasMetered, locale = "en") { if (Number(actualAmount) === 0 && hasMetered) { return t("payment.checkout.metered", locale, { recurring }); } if (showThen) { return [amount, then].filter(Boolean).join(", "); } return [amount, then].filter(Boolean).join(" "); } export function hasMultipleRecurringIntervals(items) { const intervals = /* @__PURE__ */ new Set(); for (const item of items) { if (item.price?.recurring?.interval && item.price?.type === "recurring") { intervals.add(`${item.price.recurring.interval}-${item.price.recurring.interval_count}`); if (intervals.size > 1) { return true; } } } return false; } export function getFreeTrialTime({ trialInDays, trialEnd }, locale = "en") { const now = dayjs().unix(); if (trialEnd > 0 && trialEnd > now) { if (trialEnd - now < 3600) { return { count: Math.ceil((trialEnd - now) / 60), interval: t("common.minute", locale) }; } if (trialEnd - now < 86400) { return { count: Math.ceil((trialEnd - now) / 3600), interval: t("common.hour", locale) }; } return { count: Math.ceil((trialEnd - now) / 86400), interval: t("common.day", locale) }; } if (trialInDays > 0) { return { count: trialInDays, interval: t("common.day", locale) }; } return { count: 0, interval: t("common.day", locale) }; } export function formatCheckoutHeadlines(items, currency, { trialInDays, trialEnd }, locale = "en") { const brand = getStatementDescriptor(items); const { total } = getCheckoutAmount(items, currency, trialInDays > 0); const actualAmount = fromUnitToToken(total, currency.decimal); const amount = `${fromUnitToToken(total, currency.decimal)} ${currency.symbol}`; const trialResult = getFreeTrialTime({ trialInDays, trialEnd }, locale); if (items.length === 0) { return { action: t("payment.checkout.empty", locale), amount: "0", then: "", actualAmount: "0", priceDisplay: "0" }; } const { name } = items[0]?.price.product || { name: "" }; if (items.every((x) => x.price.type === "one_time")) { const action = t("payment.checkout.pay", locale, { payee: brand }); if (items.length > 1) { return { action, amount, actualAmount, priceDisplay: amount }; } return { action, amount, then: "", actualAmount, priceDisplay: amount }; } const item = items.find((x) => x.price.type === "recurring"); const recurring = formatRecurring( (item?.upsell_price || item?.price)?.recurring, false, "per", locale ); const hasMetered = items.some((x) => x.price.type === "recurring" && x.price.recurring?.usage_type === "metered"); const differentRecurring = hasMultipleRecurringIntervals(items); if (items.every((x) => x.price.type === "recurring")) { const subscription2 = [ hasMetered ? t("payment.checkout.least", locale) : "", fromUnitToToken( items.reduce((acc, x) => { if (x.price.recurring?.usage_type === "metered") { return acc; } return acc.add( new BN(getPriceUintAmountByCurrency(x.upsell_price || x.price, currency)).mul(new BN(x.quantity)) ); }, new BN(0)), currency.decimal ), currency.symbol ].filter(Boolean).join(" "); if (items.length > 1) { if (trialResult.count > 0) { const result4 = { action: t("payment.checkout.try2", locale, { name, count: items.length - 1 }), amount: t("payment.checkout.free", locale, { count: trialResult.count, interval: trialResult.interval }), then: formatMeteredThen(subscription2, recurring, hasMetered && Number(subscription2) === 0, locale), showThen: true, actualAmount: "0" }; return { ...result4, priceDisplay: formatPriceDisplay(result4, recurring, hasMetered, locale) }; } const result3 = { action: t("payment.checkout.sub2", locale, { name, count: items.length - 1 }), amount, then: hasMetered ? t("payment.checkout.meteredThen", locale, { recurring }) : recurring, showThen: hasMetered, actualAmount }; return { ...result3, priceDisplay: formatPriceDisplay(result3, recurring, hasMetered, locale) }; } if (trialResult.count > 0) { const result3 = { action: t("payment.checkout.try1", locale, { name }), amount: t("payment.checkout.free", locale, { count: trialResult.count, interval: trialResult.interval }), then: formatMeteredThen(subscription2, recurring, hasMetered && Number(subscription2) === 0, locale), showThen: true, actualAmount: "0" }; return { ...result3, priceDisplay: formatPriceDisplay(result3, recurring, hasMetered, locale) }; } const result2 = { action: t("payment.checkout.sub1", locale, { name }), amount, then: hasMetered ? t("payment.checkout.meteredThen", locale, { recurring }) : recurring, showThen: hasMetered && !differentRecurring, actualAmount }; return { ...result2, priceDisplay: formatPriceDisplay(result2, recurring, hasMetered, locale) }; } const subscription = fromUnitToToken( items.filter((x) => x.price.type === "recurring").reduce((acc, x) => { if (x.price.recurring?.usage_type === "metered") { return acc; } return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity))); }, new BN(0)), currency.decimal ); const result = { action: t("payment.checkout.pay", locale, { payee: brand }), amount, then: formatMeteredThen( `${subscription} ${currency.symbol}`, recurring, hasMetered && Number(subscription) === 0, locale ), showThen: !differentRecurring, actualAmount }; return { ...result, priceDisplay: formatPriceDisplay(result, recurring, hasMetered, locale) }; } export function formatAmount(amount, decimals) { return fromUnitToToken(amount, decimals); } export function findCurrency(methods, currencyId) { for (const method of methods) { for (const currency of method.payment_currencies) { if (currency.id === currencyId) { return { object: "payment_currency", ...currency, paymentMethod: omit(method, ["payment_currencies"]) }; } } } return null; } export function isValidCountry(code) { return defaultCountries.some((x) => x[1] === code); } export function stopEvent(e) { try { e.stopPropagation(); e.preventDefault(); } catch { } } export function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } export function formatSubscriptionProduct(items, maxLength = 2) { const names = items.map((x) => x.price?.product?.name).filter(Boolean); return names.slice(0, maxLength).join(", ") + (names.length > maxLength ? ` and ${names.length - maxLength} more` : ""); } export const getLineTimeInfo = (time, locale = "en") => { const isToday = dayjs().isSame(dayjs(time), "day"); const timeFormat = isToday ? "HH:mm:ss" : "YYYY-MM-DD"; return { time: formatToDate(time, locale, timeFormat), isToday }; }; export const getSubscriptionTimeSummary = (subscription, locale = "en") => { if (subscription.status === "active" || subscription.status === "trialing") { if (subscription.cancel_at) { const endTime = subscription.cancel_at * 1e3; const { time: time2, isToday: isToday2 } = getLineTimeInfo(endTime, locale); return { action: endTime > Date.now() ? "willEnd" : "ended", time: time2, isToday: isToday2 }; } if (subscription.cancel_at_period_end) { const endTime = subscription.current_period_end * 1e3; const { time: time2, isToday: isToday2 } = getLineTimeInfo(endTime, locale); return { action: "willEnd", time: time2, isToday: isToday2 }; } const { time, isToday } = getLineTimeInfo(subscription.current_period_end * 1e3, locale); return { action: "renew", time, isToday }; } if (subscription.status === "past_due") { const endTime = (subscription.cancel_at || subscription.current_period_end) * 1e3; const { time, isToday } = getLineTimeInfo(endTime, locale); return { action: endTime > Date.now() ? "willEnd" : "ended", time, isToday }; } if (subscription.status === "canceled") { const { time, isToday } = getLineTimeInfo(subscription.canceled_at * 1e3, locale); return { action: "ended", time, isToday }; } return null; }; export const getSubscriptionAction = (subscription, actionProps) => { if (subscription.status === "active" || subscription.status === "trialing") { if (subscription.cancel_at_period_end) { if (subscription.cancelation_details?.reason === "payment_failed") { return null; } return { action: "recover", variant: "contained", color: "primary", canRenew: false, ...actionProps?.recover }; } if (subscription.cancel_at && subscription.cancel_at !== subscription.current_period_end) { return null; } return { action: "cancel", variant: "outlined", color: "inherit", canRenew: false, ...actionProps?.cancel }; } if (subscription.status === "past_due") { const canRenew = subscription.cancel_at && subscription.cancel_at !== subscription.current_period_end; return { action: "pastDue", variant: "contained", color: "primary", canRenew, ...actionProps?.pastDue }; } if (subscription.status !== "canceled" && subscription.cancel_at_period_end) { return { action: "recover", variant: "contained", color: "primary", canRenew: false, ...actionProps?.recover }; } return null; }; export const mergeExtraParams = (extra = {}) => { const params = new URLSearchParams(window.location.search); Object.keys(extra).forEach((key) => { params.set(key, extra[key]); }); return params.toString(); }; export const flattenPaymentMethods = (methods = []) => { const out = []; methods.forEach((method) => { const currencies = method.paymentCurrencies || method.payment_currencies || []; currencies.forEach((currency) => { out.push({ ...currency, method }); }); }); return out; }; export const getTxLink = (method, details) => { if (!details) { return { text: "N/A", link: "", gas: "" }; } if (method.type === "arcblock" && details.arcblock?.tx_hash) { return { link: joinURL(method.settings.arcblock?.explorer_host, "/txs", details.arcblock?.tx_hash), text: details.arcblock?.tx_hash, gas: "" }; } if (method.type === "bitcoin" && details.bitcoin?.tx_hash) { return { link: joinURL(method.settings.bitcoin?.explorer_host, "/tx", details.bitcoin?.tx_hash), text: details.bitcoin?.tx_hash, gas: "" }; } if (method.type === "ethereum" && details.ethereum?.tx_hash) { return { link: joinURL(method.settings.ethereum?.explorer_host, "/tx", details.ethereum?.tx_hash), text: (details.ethereum?.tx_hash).toUpperCase(), gas: new BN(details.ethereum.gas_price).mul(new BN(details.ethereum.gas_used)).toString() }; } if (method.type === "base" && details.base?.tx_hash) { return { link: joinURL(method.settings.base?.explorer_host, "/tx", details.base?.tx_hash), text: (details.base?.tx_hash).toUpperCase(), gas: "" }; } if (method.type === "stripe") { const dashboard = method.livemode ? "https://dashboard.stripe.com" : "https://dashboard.stripe.com/test"; return { link: joinURL( method.settings.stripe?.dashboard || dashboard, "payments", details.stripe?.payment_intent_id ), text: details.stripe?.payment_intent_id, gas: "" }; } return { text: "N/A", link: "", gas: "" }; }; export function getQueryParams(url) { const queryParams = {}; const urlObj = new URL(url); urlObj.searchParams.forEach((value, key) => { queryParams[key] = value; }); return queryParams; } export function lazyLoad(lazyRun) { if ("requestIdleCallback" in window) { window.requestIdleCallback(() => { lazyRun(); }); return; } if (document.readyState === "complete") { lazyRun(); return; } if ("onload" in window) { window.onload = () => { lazyRun(); }; return; } setTimeout(() => { lazyRun(); }, 0); } export function formatTotalPrice({ product, quantity = 1, priceId, locale = "en" }) { const { prices = [], default_price_id: defaultPriceId } = product ?? { prices: [], default_price_id: "" }; const price = prices?.find((x) => x.id === (priceId || defaultPriceId)); if (!price || !product) { return { totalPrice: "0", unitPrice: "", quantity: t("common.qty", locale, { count: quantity }), totalAmount: "0" }; } const currency = price?.currency ?? {}; const unitValue = new BN(getPriceUintAmountByCurrency(price, currency)); const total = `${fromUnitToToken(unitValue.mul(new BN(quantity)), currency.decimal)} ${currency.symbol} `; const unit = `${fromUnitToToken(unitValue, currency.decimal)} ${currency.symbol} `; const appendUnit = (v, alt) => { if (product.unit_label) { return `${v}/${price.product.unit_label}`; } if (price.recurring?.usage_type === "metered" || quantity === 1) { return alt; } return quantity ? t("common.each", locale, { unit }) : ""; }; return { totalPrice: total, unitPrice: appendUnit(total, ""), quantity: t("common.qty", locale, { count: quantity }), totalAmount: unitValue.mul(new BN(quantity)).toString() }; } export function formatQuantityInventory(price, quantity, locale = "en") { const q = Number(quantity); const { quantity_available: quantityAvailable = 0, quantity_sold: quantitySold = 0, quantity_limit_per_checkout: quantityLimitPerCheckout = 0 } = price || {}; if (quantityAvailable > 0 && quantitySold + q > quantityAvailable) { return t("common.quantityNotEnough", locale); } if (quantityLimitPerCheckout > 0 && quantityLimitPerCheckout < q) { return t("common.quantityLimitPerCheckout", locale); } return ""; } export function formatSubscriptionStatus(status) { if (status === "canceled") { return "Ended"; } return status; } export function formatAmountPrecisionLimit(amount, locale = "en", precision = 6) { if (!amount) { return ""; } const [, decimal] = String(amount).split("."); if (decimal && decimal.length > precision) { return t("common.amountPrecisionLimit", locale, { precision }); } return ""; } export function getWordBreakStyle(value) { if (typeof value === "string" && /\s/.test(value)) { return "break-word"; } return "break-all"; } export function isMobileSafari() { const ua = navigator.userAgent.toLowerCase(); const isSafari = ua.indexOf("safari") > -1 && ua.indexOf("chrome") === -1; const isMobile = ua.indexOf("mobile") > -1 || /iphone|ipad|ipod/.test(ua); const isIOS = /iphone|ipad|ipod/.test(ua); return isSafari && isMobile && isIOS; } export function truncateText(text, maxLength, useWidth = false) { if (!text || !maxLength) { return text; } if (!useWidth) { if (text.length <= maxLength) { return text; } return `${text.substring(0, maxLength)}...`; } let width = 0; let truncated = ""; for (let i = 0; i < text.length; i++) { const charWidth = stringWidth(text.charAt(i)); if (width + charWidth > maxLength) { break; } truncated += text.charAt(i); width += charWidth; } if (truncated === text) { return truncated; } return `${truncated}...`; } export function getCustomerAvatar(did, updated_at, imageSize = 48) { const updated = typeof updated_at === "number" ? updated_at : dayjs(updated_at).unix(); return `/.well-known/service/user/avatar/${did}?imageFilter=resize&w=${imageSize}&h=${imageSize}&updateAt=${updated || dayjs().unix()}`; } export function hasDelegateTxHash(details, paymentMethod) { return paymentMethod?.type && ["arcblock", "ethereum", "base"].includes(paymentMethod?.type) && // @ts-ignore details?.[paymentMethod?.type]?.tx_hash; } export function getInvoiceDescriptionAndReason(invoice, locale = "en") { const { billing_reason: reason, description } = invoice; const reasonMap = { subscription_create: t("payment.invoice.reason.creation", locale), subscription_cycle: t("payment.invoice.reason.cycle", locale), subscription_update: t("payment.invoice.reason.update", locale), subscription_recover: t("payment.invoice.reason.recover", locale), subscription_threshold: t("payment.invoice.reason.threshold", locale), subscription_cancel: t("payment.invoice.reason.cancel", locale), manual: t("payment.invoice.reason.manual", locale), upcoming: t("payment.invoice.reason.upcoming", locale), slash_stake: t("payment.invoice.reason.slashStake", locale), stake: t("payment.invoice.reason.stake", locale), return_stake: t("payment.invoice.reason.returnStake", locale), recharge: t("payment.invoice.reason.recharge", locale), stake_overdraft_protection: t("payment.invoice.reason.stake", locale), overdraft_protection: t("payment.invoice.reason.fee", locale), auto_recharge: t("payment.invoice.reason.recharge", locale) }; let invoiceType = t("payment.invoice.reason.payment", locale); if (reason.includes("stake") || reason.includes("recharge") || reason === "overdraft_protection") { invoiceType = reasonMap[reason]; } if (description?.startsWith("Subscription ") || description?.startsWith("Slash stake")) { return { description: reasonMap[reason], reason: reasonMap[reason], type: invoiceType }; } const descMap = { "Stake for subscription plan change": t("payment.invoice.reason.stakeForChangePlan", locale), "Stake for subscription payment change": t("payment.invoice.reason.stakeForChangePayment", locale), "Stake for subscription": t("payment.invoice.reason.staking", locale), "Return Subscription staking": t("payment.invoice.reason.returnStake", locale), "Recharge for subscription": t("payment.invoice.reason.rechargeForSubscription", locale), "Add funds for subscription": t("payment.invoice.reason.rechargeForSubscription", locale), "Overdraft protection": t("payment.invoice.reason.overdraftProtection", locale), "Stake for subscription overdraft protection": t( "payment.invoice.reason.stakeForSubscriptionOverdraftProtection", locale ), "Re-stake to resume subscription": t("payment.invoice.reason.reStakeToResumeSubscription", locale) }; return { description: descMap[description] || description, reason: reasonMap[reason] || reason, type: invoiceType }; } export function getPaymentKitComponent() { const paymentKit = window.blocklet?.componentMountPoints?.find((c) => c.did === PAYMENT_KIT_DID); return paymentKit || null; } export function openDonationSettings(openInNewTab = false) { const paymentKit = getPaymentKitComponent(); if (paymentKit) { const mountPoint = paymentKit.mountPoint.endsWith("/") ? paymentKit.mountPoint.slice(0, -1) : paymentKit.mountPoint; window.open(`${window.location.origin}${mountPoint}/integrations/donations`, openInNewTab ? "_blank" : "_self"); } } export function getUserProfileLink(userDid, locale = "en") { return joinURL( window.location.origin, withQuery(".well-known/service/user", { locale, did: userDid }) ); } export function parseMarkedText(text) { if (!text) return []; const parts = text.split(/(#([^#]*)#)/); const result = []; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!part) continue; if (i % 3 === 0) { result.push({ type: "text", content: part }); } else if (i % 3 === 1 && part.startsWith("#") && part.endsWith("#")) { const content = part.slice(1, -1); if (content.length >= 0) { result.push({ type: "marked", content }); } } } return result.filter((p) => p.content !== ""); } export function getTokenBalanceLink(method, address) { if (!method || !address) { return ""; } const explorerHost = method?.settings?.[method?.type]?.explorer_host || ""; if (method.type === "arcblock" && address) { return joinURL(explorerHost, "accounts", address, "tokens"); } if (["ethereum", "base"].includes(method.type) && address) { return joinURL(explorerHost, "address", address); } return ""; } export function isCreditMetered(price) { return !!(price.type === "recurring" && price.recurring?.usage_type === "metered" && price.recurring?.meter_id); } export function showStaking(method, currency, noStake) { if (noStake) { return false; } if (method.type === "arcblock") { return currency.type !== "credit"; } return false; } export function formatLinkWithLocale(url, locale) { if (!locale || !url) { return url; } try { const urlObj = new URL(url); urlObj.searchParams.set("locale", locale); return urlObj.toString(); } catch (error) { if (/[?&]locale=[^&]*/.test(url)) { return url.replace(/([?&])locale=[^&]*/, `$1locale=${locale}`); } const separator = url.includes("?") ? "&" : "?"; return `${url}${separator}locale=${locale}`; } }