refactor: slice web app wip
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { rememberReferral, readReferral } from "./session.js";
|
||||
|
||||
export function readReferralParam(tg) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fromQuery = params.get("ref") || params.get("start") || params.get("start_param") || "";
|
||||
const fromTelegram = tg?.initDataUnsafe?.start_param || "";
|
||||
const value = String(fromTelegram || fromQuery || "").trim();
|
||||
return value ? rememberReferral(value) : readReferral();
|
||||
}
|
||||
|
||||
export function readTelegramAuthStatus() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return (params.get("telegram_auth") || "").trim().toLowerCase() || null;
|
||||
}
|
||||
|
||||
export function readMagicLoginToken() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return (params.get("login_token") || "").trim() || null;
|
||||
}
|
||||
|
||||
export function readTelegramLoginWidgetAuthData() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const keys = ["id", "first_name", "last_name", "username", "photo_url", "auth_date", "hash"];
|
||||
const authData = {};
|
||||
let hasAuthValue = false;
|
||||
keys.forEach((key) => {
|
||||
if (!params.has(key)) return;
|
||||
authData[key] = params.get(key) || "";
|
||||
hasAuthValue = true;
|
||||
});
|
||||
if (!hasAuthValue || !authData.id || !authData.auth_date || !authData.hash) return null;
|
||||
return authData;
|
||||
}
|
||||
|
||||
export function clearAuthQuery() {
|
||||
const url = new URL(window.location.href);
|
||||
[
|
||||
"login_token",
|
||||
"login_purpose",
|
||||
"telegram_auth",
|
||||
"id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"username",
|
||||
"photo_url",
|
||||
"auth_date",
|
||||
"hash",
|
||||
].forEach((key) => url.searchParams.delete(key));
|
||||
window.history?.replaceState?.({}, document.title, url.pathname + url.search + url.hash);
|
||||
}
|
||||
|
||||
export function buildTelegramOAuthStartUrl(purpose = "login", tg = null) {
|
||||
const url = new URL("/auth/telegram/start", window.location.origin);
|
||||
url.searchParams.set("purpose", purpose);
|
||||
const referralParam = readReferralParam(tg);
|
||||
if (referralParam) url.searchParams.set("referral_code", referralParam);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function emailError(error, fallback, t) {
|
||||
if (error?.error === "rate_limited")
|
||||
return t("wa_auth_resend_wait", { seconds: error.retry_after || 60 });
|
||||
if (error?.error === "invalid_email") return t("wa_auth_invalid_email");
|
||||
if (error?.error === "expired_code") return t("wa_auth_code_expired");
|
||||
if (error?.error === "invalid_code" || error?.error === "too_many_attempts")
|
||||
return t("wa_auth_invalid_code");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createCooldownTimer() {
|
||||
let timer = null;
|
||||
let cooldown = 0;
|
||||
const listeners = new Set();
|
||||
function notify() {
|
||||
for (const fn of listeners) fn(cooldown);
|
||||
}
|
||||
function clear() {
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
function start(seconds = 60) {
|
||||
clear();
|
||||
cooldown = Math.max(0, Number(seconds || 60));
|
||||
notify();
|
||||
timer = window.setInterval(() => {
|
||||
if (cooldown <= 1) {
|
||||
cooldown = 0;
|
||||
clear();
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
cooldown -= 1;
|
||||
notify();
|
||||
}, 1000);
|
||||
}
|
||||
function subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(cooldown);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
return {
|
||||
start,
|
||||
clear,
|
||||
subscribe,
|
||||
get value() {
|
||||
return cooldown;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
export function createBillingActions({ api, t }) {
|
||||
async function fetchTopupOptions(kind) {
|
||||
return api(`/tariffs/topup-options?kind=${encodeURIComponent(kind)}`);
|
||||
}
|
||||
|
||||
async function fetchDeviceTopupOptions() {
|
||||
return api("/devices/topup-options");
|
||||
}
|
||||
|
||||
async function fetchTariffChangeOptions() {
|
||||
return api("/tariffs/change-options");
|
||||
}
|
||||
|
||||
async function postPayment(body) {
|
||||
return api("/payments", { method: "POST", body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
async function postTariffChange(body) {
|
||||
return api("/tariffs/change", { method: "POST", body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
async function postTariffChangePayment(body) {
|
||||
return api("/tariffs/change-payment", { method: "POST", body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
function planPaymentBody(plan, method) {
|
||||
return {
|
||||
months: plan.months,
|
||||
traffic_gb: plan.traffic_gb,
|
||||
device_count: plan.device_count,
|
||||
tariff_key: plan.tariff_key,
|
||||
sale_mode: plan.sale_mode,
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
function topupPaymentBody(plan, method, fallbackTariffKey) {
|
||||
return {
|
||||
months: plan.months,
|
||||
traffic_gb: plan.traffic_gb,
|
||||
tariff_key: plan.tariff_key || fallbackTariffKey,
|
||||
sale_mode: plan.sale_mode || "topup",
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
function deviceTopupPaymentBody(plan, method, fallbackTariffKey) {
|
||||
return {
|
||||
months: plan.device_count || plan.months,
|
||||
device_count: plan.device_count || plan.months,
|
||||
tariff_key: plan.tariff_key || fallbackTariffKey,
|
||||
sale_mode: "hwid_devices",
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
function changePaymentBody(action, target, method) {
|
||||
if (action.mode === "buy_package") {
|
||||
return {
|
||||
tariff_key: target.tariff_key,
|
||||
traffic_gb: action.traffic_gb,
|
||||
months: action.traffic_gb,
|
||||
sale_mode: "topup",
|
||||
method,
|
||||
};
|
||||
}
|
||||
if (action.mode === "buy_period") {
|
||||
return {
|
||||
tariff_key: target.tariff_key,
|
||||
months: action.months,
|
||||
method,
|
||||
};
|
||||
}
|
||||
return { tariff_key: target.tariff_key, method };
|
||||
}
|
||||
|
||||
return {
|
||||
fetchTopupOptions,
|
||||
fetchDeviceTopupOptions,
|
||||
fetchTariffChangeOptions,
|
||||
postPayment,
|
||||
postTariffChange,
|
||||
postTariffChangePayment,
|
||||
planPaymentBody,
|
||||
topupPaymentBody,
|
||||
deviceTopupPaymentBody,
|
||||
changePaymentBody,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
export function formatTemplate(template, params = {}) {
|
||||
const text = String(template ?? "");
|
||||
return text.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
||||
}
|
||||
|
||||
export function formatMoney(value, currency = "RUB") {
|
||||
const numeric = Number(value || 0);
|
||||
const formatted = Number.isInteger(numeric) ? String(numeric) : numeric.toFixed(2);
|
||||
const symbol = currency === "RUB" ? "₽" : currency;
|
||||
return `${formatted} ${symbol}`;
|
||||
}
|
||||
|
||||
export function formatTrafficGb(value) {
|
||||
const numeric = Number(value || 0);
|
||||
const formatted = Number.isInteger(numeric)
|
||||
? String(numeric)
|
||||
: numeric.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
||||
return `${formatted} GB`;
|
||||
}
|
||||
|
||||
export function formatTrafficBytes(value) {
|
||||
const gb = Number(value || 0) / 1073741824;
|
||||
return formatTrafficGb(gb);
|
||||
}
|
||||
|
||||
export function formatCompactNumber(value) {
|
||||
const numeric = Number(value || 0);
|
||||
return Number.isInteger(numeric)
|
||||
? String(numeric)
|
||||
: numeric.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
||||
}
|
||||
|
||||
export function roundToHalf(value) {
|
||||
return Math.round(Number(value || 0) * 2) / 2;
|
||||
}
|
||||
|
||||
export function formatFraction(value) {
|
||||
const n = Number(value || 0);
|
||||
if (Number.isInteger(n)) return String(n);
|
||||
return n.toFixed(1);
|
||||
}
|
||||
|
||||
export function normalizedEmail(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function telegramName(profile, fallback) {
|
||||
const first = String(profile?.first_name || "").trim();
|
||||
const last = String(profile?.last_name || "").trim();
|
||||
if (first || last) return `${first} ${last}`.trim();
|
||||
const username = String(profile?.username || "").trim();
|
||||
if (username) return `@${username}`;
|
||||
return fallback;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
function bytesToHex(buffer) {
|
||||
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function sha256Hex(value) {
|
||||
const data = new TextEncoder().encode(value);
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-256", data);
|
||||
return bytesToHex(hashBuffer);
|
||||
}
|
||||
|
||||
export async function buildGravatarUrl(emailValue) {
|
||||
if (!emailValue || !window.crypto?.subtle) return "";
|
||||
try {
|
||||
const hash = await sha256Hex(emailValue);
|
||||
return `https://www.gravatar.com/avatar/${hash}?d=mp&s=160`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { LANGUAGE_LABELS } from "./constants.js";
|
||||
import { formatTemplate, formatFraction, roundToHalf } from "./formatters.js";
|
||||
import { unitPluralBucket } from "./plurals.js";
|
||||
|
||||
export function createI18n({ messages = {}, defaultLang = "ru", getLang = null } = {}) {
|
||||
function normalizeLangCode(lang) {
|
||||
const key = String(lang || "").trim().toLowerCase();
|
||||
if (!key) return defaultLang;
|
||||
const base = key.split("-")[0];
|
||||
if (LANGUAGE_LABELS[base]) return base;
|
||||
if (messages[base]) return base;
|
||||
if (messages[key]) return key;
|
||||
return defaultLang;
|
||||
}
|
||||
|
||||
function currentLang() {
|
||||
return normalizeLangCode(typeof getLang === "function" ? getLang() : defaultLang);
|
||||
}
|
||||
|
||||
function t(key, params = {}, fallback = "") {
|
||||
const lang = currentLang();
|
||||
const variants = [
|
||||
messages?.[lang]?.[key],
|
||||
messages?.en?.[key],
|
||||
messages?.ru?.[key],
|
||||
fallback,
|
||||
key,
|
||||
];
|
||||
const raw = variants.find((value) => typeof value === "string" && value.length);
|
||||
return formatTemplate(raw, params);
|
||||
}
|
||||
|
||||
function languageName(code) {
|
||||
const key = String(code || "").trim().toLowerCase();
|
||||
if (!key) return t("wa_language_default");
|
||||
return LANGUAGE_LABELS[key] || key.toUpperCase();
|
||||
}
|
||||
|
||||
function termUnitLabel(value, unit) {
|
||||
const bucket = unitPluralBucket(value, currentLang());
|
||||
return t(`wa_sub_term_${unit}_${bucket}`);
|
||||
}
|
||||
|
||||
return { normalizeLangCode, t, currentLang, languageName, termUnitLabel };
|
||||
}
|
||||
|
||||
export { formatFraction, roundToHalf };
|
||||
@@ -0,0 +1,33 @@
|
||||
export function ruPlural(value, one, few, many) {
|
||||
const n = Math.abs(Number(value || 0));
|
||||
const mod10 = n % 10;
|
||||
const mod100 = n % 100;
|
||||
if (mod10 === 1 && mod100 !== 11) return one;
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return few;
|
||||
return many;
|
||||
}
|
||||
|
||||
export function ruFractionAware(value, one, few, many) {
|
||||
const n = Number(value || 0);
|
||||
if (!Number.isInteger(n)) return few;
|
||||
return ruPlural(n, one, few, many);
|
||||
}
|
||||
|
||||
export function unitPluralBucket(value, lang) {
|
||||
if (String(lang || "").toLowerCase() === "ru") {
|
||||
const n = Number(value || 0);
|
||||
if (!Number.isInteger(n)) {
|
||||
const base = Math.floor(Math.abs(n));
|
||||
const mod10 = base % 10;
|
||||
const mod100 = base % 100;
|
||||
return mod10 >= 1 && mod10 <= 4 && (mod100 < 11 || mod100 > 14) ? "few" : "many";
|
||||
}
|
||||
const abs = Math.abs(n);
|
||||
const mod10 = abs % 10;
|
||||
const mod100 = abs % 100;
|
||||
if (mod10 === 1 && mod100 !== 11) return "one";
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
|
||||
return "many";
|
||||
}
|
||||
return Number(value) === 1 ? "one" : "many";
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { formatMoney, formatTrafficGb } from "./formatters.js";
|
||||
|
||||
export function planKey(plan) {
|
||||
return (
|
||||
plan?.id ||
|
||||
`${plan?.tariff_key || "legacy"}:${plan?.sale_mode || "subscription"}:${plan?.months || plan?.traffic_gb || ""}`
|
||||
);
|
||||
}
|
||||
|
||||
export function buildTariffCatalog(planList) {
|
||||
const byKey = new Map();
|
||||
for (const plan of planList || []) {
|
||||
const key = String(plan?.tariff_key || planKey(plan) || "").trim();
|
||||
if (!key) continue;
|
||||
const entry = byKey.get(key) || {
|
||||
key,
|
||||
title: plan?.tariff_name || plan?.title || key,
|
||||
description: plan?.description || "",
|
||||
billing_model:
|
||||
plan?.billing_model ||
|
||||
(plan?.sale_mode === "traffic_package" || plan?.sale_mode === "traffic"
|
||||
? "traffic"
|
||||
: "period"),
|
||||
monthly_gb: Number(plan?.monthly_gb || 0),
|
||||
traffic_packages: [],
|
||||
plans_count: 0,
|
||||
};
|
||||
if (!entry.description && plan?.description) entry.description = plan.description;
|
||||
if (!entry.monthly_gb && Number(plan?.monthly_gb || 0) > 0)
|
||||
entry.monthly_gb = Number(plan.monthly_gb);
|
||||
const trafficGb = Number(plan?.traffic_gb || 0);
|
||||
if (trafficGb > 0) entry.traffic_packages.push(trafficGb);
|
||||
entry.plans_count += 1;
|
||||
byKey.set(key, entry);
|
||||
}
|
||||
return Array.from(byKey.values());
|
||||
}
|
||||
|
||||
export function activeTariffName(sub, planList) {
|
||||
const direct = String(sub?.tariff_name || "").trim();
|
||||
if (direct) return direct;
|
||||
const key = String(sub?.tariff_key || "").trim();
|
||||
if (!key) return "";
|
||||
const plan = (planList || []).find((item) => item?.tariff_key === key);
|
||||
return String(plan?.tariff_name || plan?.title || key).trim();
|
||||
}
|
||||
|
||||
export function priceLabel(plan, methodId = "") {
|
||||
if (String(methodId || "").toLowerCase().includes("stars") && Number(plan?.stars_price || 0) > 0) {
|
||||
return `${Number(plan.stars_price)} ⭐`;
|
||||
}
|
||||
return formatMoney(plan?.price || 0, plan?.currency);
|
||||
}
|
||||
|
||||
export function tariffLimitLabel(tariff, { t }) {
|
||||
if (!tariff) return "";
|
||||
if (String(tariff.billing_model || "") === "traffic") {
|
||||
const values = (tariff.traffic_packages || [])
|
||||
.filter((value) => Number(value) > 0)
|
||||
.sort((a, b) => a - b);
|
||||
if (!values.length) return t("wa_tariff_model_traffic");
|
||||
const min = values[0];
|
||||
const max = values[values.length - 1];
|
||||
return min === max ? formatTrafficGb(min) : `${formatTrafficGb(min)} - ${formatTrafficGb(max)}`;
|
||||
}
|
||||
if (Number(tariff.monthly_gb || 0) > 0) return formatTrafficGb(tariff.monthly_gb);
|
||||
return t("wa_unlimited_traffic");
|
||||
}
|
||||
|
||||
export function actionKey(action) {
|
||||
return `${action?.mode || ""}:${action?.months || ""}:${action?.traffic_gb || ""}:${action?.price || ""}`;
|
||||
}
|
||||
|
||||
function formatMonthsForClient(value, lang) {
|
||||
const months = Number(value || 0);
|
||||
if (months === 1) return lang === "en" ? "1 month" : "1 месяц";
|
||||
if (months === 12) return lang === "en" ? "1 year" : "1 год";
|
||||
return lang === "en" ? `${months} months` : `${months} мес.`;
|
||||
}
|
||||
|
||||
export function planDisplayTitle(plan, { trafficMode, t }) {
|
||||
if (plan?.tariff_key) {
|
||||
return plan?.tariff_name || plan?.title || plan?.tariff_key;
|
||||
}
|
||||
if (trafficMode || plan?.sale_mode === "traffic") {
|
||||
return plan?.title || formatTrafficGb(plan?.traffic_gb || plan?.months);
|
||||
}
|
||||
const months = Number(plan?.months || 0);
|
||||
if (months === 12) return t("wa_plan_one_year");
|
||||
return plan?.title || "";
|
||||
}
|
||||
|
||||
export function planSubtitle(plan, { lang }) {
|
||||
if (!plan?.tariff_key) return "";
|
||||
if (plan?.subtitle) return plan.subtitle;
|
||||
if (
|
||||
plan?.sale_mode === "traffic_package" ||
|
||||
plan?.sale_mode === "topup" ||
|
||||
plan?.sale_mode === "premium_topup" ||
|
||||
plan?.billing_model === "traffic"
|
||||
) {
|
||||
return formatTrafficGb(plan?.traffic_gb || plan?.months);
|
||||
}
|
||||
return formatMonthsForClient(plan?.months, lang);
|
||||
}
|
||||
|
||||
export function planUnitHint(plan, { trafficMode, selectedMethod, t }) {
|
||||
if (
|
||||
trafficMode ||
|
||||
plan?.sale_mode === "traffic" ||
|
||||
plan?.sale_mode === "traffic_package" ||
|
||||
plan?.sale_mode === "topup" ||
|
||||
plan?.sale_mode === "premium_topup"
|
||||
) {
|
||||
const gb = Number(plan?.traffic_gb || plan?.months || 0);
|
||||
if (!gb) return "";
|
||||
if (String(selectedMethod || "").toLowerCase().includes("stars") && Number(plan?.stars_price || 0) > 0) {
|
||||
return `${Number(plan.stars_price / gb).toFixed(0)} ⭐${t("wa_per_gb_short")}`;
|
||||
}
|
||||
return `${formatMoney(Number(plan?.price || 0) / gb, plan?.currency)}${t("wa_per_gb_short")}`;
|
||||
}
|
||||
const months = Number(plan?.months || 0);
|
||||
if (!months || months <= 1) return "";
|
||||
if (String(selectedMethod || "").toLowerCase().includes("stars") && Number(plan?.stars_price || 0) > 0) {
|
||||
return `${Number(plan.stars_price / months).toFixed(0)} ⭐${t("wa_per_month_short")}`;
|
||||
}
|
||||
return `${formatMoney(Number(plan?.price || 0) / months, plan?.currency)}${t("wa_per_month_short")}`;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { formatTrafficBytes, formatFraction, roundToHalf } from "./formatters.js";
|
||||
|
||||
export function trafficPercent(sub) {
|
||||
const used = Number(sub?.traffic_used_bytes || 0);
|
||||
const limit = Number(sub?.traffic_limit_bytes || 0);
|
||||
if (!limit || limit <= 0) return 100;
|
||||
return Math.max(0, Math.min(100, Math.round((used / limit) * 100)));
|
||||
}
|
||||
|
||||
export function trafficLabel(sub, t) {
|
||||
if (!sub?.traffic_limit_bytes || Number(sub.traffic_limit_bytes) <= 0) return t("wa_unlimited_traffic");
|
||||
return t("wa_traffic_of", { used: sub.traffic_used || "0 GB", limit: sub.traffic_limit || "0 GB" });
|
||||
}
|
||||
|
||||
export function trafficResetLabel(sub, t) {
|
||||
const strategy = String(sub?.traffic_limit_strategy || "").trim().toUpperCase();
|
||||
if (!strategy || strategy.includes("NO_RESET")) return t("wa_traffic_reset_none");
|
||||
if (strategy.includes("MONTH")) return t("wa_traffic_reset_monthly");
|
||||
if (strategy.includes("WEEK")) return t("wa_traffic_reset_weekly");
|
||||
if (strategy.includes("DAY")) return t("wa_traffic_reset_daily");
|
||||
if (strategy.includes("YEAR")) return t("wa_traffic_reset_yearly");
|
||||
return t("wa_traffic_reset_policy");
|
||||
}
|
||||
|
||||
export function premiumTrafficPercent(sub) {
|
||||
const used = Number(sub?.premium_used_bytes || 0);
|
||||
const limit = Number(sub?.premium_limit_bytes || 0);
|
||||
if (!limit || limit <= 0) return 0;
|
||||
return Math.max(0, Math.min(100, Math.round((used / limit) * 100)));
|
||||
}
|
||||
|
||||
export function premiumTrafficLabel(sub, t) {
|
||||
return t("wa_traffic_of", { used: sub?.premium_used || "0 GB", limit: sub?.premium_limit || "0 GB" });
|
||||
}
|
||||
|
||||
export function premiumTitle(sub, t) {
|
||||
return String(sub?.premium_title || "").trim() || t("wa_premium_traffic_title", {}, "Premium-серверы");
|
||||
}
|
||||
|
||||
export function premiumTrafficLeftLabel(sub) {
|
||||
const left = Math.max(0, Number(sub?.premium_limit_bytes || 0) - Number(sub?.premium_used_bytes || 0));
|
||||
return formatTrafficBytes(left);
|
||||
}
|
||||
|
||||
export function premiumTopupBalanceLabel(sub) {
|
||||
return formatTrafficBytes(Number(sub?.premium_topup_balance_bytes || 0));
|
||||
}
|
||||
|
||||
export function premiumServerLabels(sub) {
|
||||
const labels =
|
||||
Array.isArray(sub?.premium_node_labels) && sub.premium_node_labels.length
|
||||
? sub.premium_node_labels
|
||||
: sub?.premium_squad_labels || [];
|
||||
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function extractYear(text) {
|
||||
const iso = String(text || "").match(/\b(\d{4})-\d{1,2}-\d{1,2}\b/);
|
||||
if (iso) return Number(iso[1] || 0);
|
||||
const dmy = String(text || "").match(/\b\d{1,2}\.\d{1,2}\.(\d{4})\b/);
|
||||
if (dmy) return Number(dmy[1] || 0);
|
||||
const any4 = String(text || "").match(/\b(\d{4})\b/);
|
||||
if (any4) return Number(any4[1] || 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isForeverSubscription(sub) {
|
||||
const raw = String(sub?.end_date_text || "").trim();
|
||||
if (!raw) return false;
|
||||
return extractYear(raw) >= 2099;
|
||||
}
|
||||
|
||||
export function activeSubscriptionTermLabel(sub, { t, termUnitLabel }) {
|
||||
if (isForeverSubscription(sub)) return t("wa_sub_term_forever");
|
||||
|
||||
const days = Math.max(0, Number(sub?.days_left || 0));
|
||||
if (!days) return t("wa_sub_term_value_unit", { value: "0", unit: termUnitLabel(0, "day") });
|
||||
|
||||
if (days < 30) {
|
||||
return t("wa_sub_term_value_unit", { value: String(days), unit: termUnitLabel(days, "day") });
|
||||
}
|
||||
if (days < 365) {
|
||||
const months = roundToHalf(days / 30);
|
||||
return t("wa_sub_term_value_unit", {
|
||||
value: formatFraction(months),
|
||||
unit: termUnitLabel(months, "month"),
|
||||
});
|
||||
}
|
||||
const years = roundToHalf(days / 365);
|
||||
return t("wa_sub_term_value_unit", {
|
||||
value: formatFraction(years),
|
||||
unit: termUnitLabel(years, "year"),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user