@blocklet/payment-react
Version:
Reusable react components for payment kit v2
1,058 lines (1,057 loc) • 36.2 kB
JavaScript
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}`;
}
}