From d7534e098cc6527e2b830897d66ac00d5364ec00 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 10 Oct 2025 08:05:17 +0300 Subject: [PATCH] Revert "Add subscription configurator to mini app" --- miniapp/index.html | 2098 -------------------------------------------- 1 file changed, 2098 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index 327307bd..b1f33202 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -705,298 +705,6 @@ color: #fff; } - .subscription-configurator-card { - position: relative; - } - - .subscription-configurator-summary { - font-size: 13px; - color: var(--text-secondary); - padding: 12px 0; - border-bottom: 1px solid var(--border-color); - } - - .subscription-configurator-summary strong { - color: var(--text-primary); - font-weight: 700; - } - - .subscription-configurator-discount-badge { - margin-left: auto; - padding: 6px 12px; - border-radius: 999px; - background: rgba(var(--primary-rgb), 0.12); - color: var(--primary); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - } - - .subscription-configurator-content { - display: flex; - flex-direction: column; - gap: 20px; - padding-top: 12px; - } - - .subscription-configurator-section { - display: flex; - flex-direction: column; - gap: 12px; - padding-bottom: 16px; - border-bottom: 1px solid var(--border-color); - } - - .subscription-configurator-section:last-child { - border-bottom: none; - } - - .subscription-configurator-section-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - } - - .subscription-configurator-section-title { - font-size: 16px; - font-weight: 700; - color: var(--text-primary); - } - - .subscription-configurator-section-description { - font-size: 13px; - color: var(--text-secondary); - line-height: 1.45; - } - - .subscription-configurator-section-meta { - font-size: 12px; - font-weight: 600; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.04em; - } - - .subscription-configurator-options { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 12px; - } - - .subscription-configurator-option { - position: relative; - padding: 14px; - border-radius: var(--radius); - border: 1px solid var(--border-color); - background: var(--bg-primary); - color: var(--text-primary); - font-size: 14px; - font-weight: 600; - display: flex; - flex-direction: column; - gap: 8px; - cursor: pointer; - transition: all 0.2s ease; - } - - .subscription-configurator-option:hover:not(.disabled) { - border-color: rgba(var(--primary-rgb), 0.5); - box-shadow: var(--shadow-sm); - transform: translateY(-1px); - } - - .subscription-configurator-option.active { - border-color: var(--primary); - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.12), rgba(var(--primary-rgb), 0.04)); - box-shadow: var(--shadow-sm); - } - - .subscription-configurator-option.disabled { - opacity: 0.6; - cursor: not-allowed; - } - - .subscription-configurator-option-title { - font-weight: 700; - font-size: 15px; - color: inherit; - } - - .subscription-configurator-option-meta { - font-size: 12px; - color: var(--text-secondary); - font-weight: 500; - } - - .subscription-configurator-option-price { - font-size: 13px; - font-weight: 600; - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 8px; - } - - .subscription-configurator-option-price .original { - text-decoration: line-through; - color: var(--text-secondary); - font-size: 12px; - font-weight: 500; - } - - .subscription-configurator-option-price .current { - color: var(--text-primary); - } - - .subscription-configurator-option-price .discount { - color: var(--success); - font-size: 12px; - font-weight: 700; - } - - .subscription-configurator-fixed-value { - font-size: 14px; - color: var(--text-secondary); - font-weight: 600; - } - - .subscription-configurator-stepper { - display: flex; - align-items: center; - gap: 12px; - } - - .subscription-configurator-stepper button { - width: 42px; - height: 42px; - border-radius: var(--radius); - border: 1px solid var(--border-color); - background: var(--bg-primary); - color: var(--text-primary); - font-size: 20px; - font-weight: 700; - cursor: pointer; - transition: all 0.2s ease; - } - - .subscription-configurator-stepper button:hover:not(:disabled) { - border-color: rgba(var(--primary-rgb), 0.5); - box-shadow: var(--shadow-sm); - } - - .subscription-configurator-stepper button:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .subscription-configurator-stepper-value { - font-size: 20px; - font-weight: 700; - color: var(--text-primary); - min-width: 36px; - text-align: center; - } - - .subscription-configurator-devices-note { - font-size: 12px; - color: var(--text-secondary); - font-weight: 600; - } - - .subscription-configurator-footer { - display: flex; - flex-direction: column; - gap: 12px; - padding-top: 4px; - } - - .subscription-configurator-price { - display: flex; - flex-direction: column; - gap: 6px; - } - - .subscription-configurator-price-main { - display: flex; - align-items: baseline; - gap: 10px; - } - - .subscription-configurator-price-main .original { - text-decoration: line-through; - color: var(--text-secondary); - font-size: 15px; - font-weight: 600; - } - - .subscription-configurator-price-main .current { - font-size: 24px; - font-weight: 800; - color: var(--text-primary); - } - - .subscription-configurator-price-extra { - font-size: 13px; - color: var(--text-secondary); - font-weight: 600; - } - - .subscription-configurator-balance-warning { - padding: 12px 14px; - border-radius: var(--radius); - border: 1px solid rgba(var(--danger-rgb, 239, 68, 68), 0.3); - background: rgba(var(--danger-rgb, 239, 68, 68), 0.08); - display: flex; - flex-direction: column; - gap: 10px; - } - - .subscription-configurator-balance-text { - font-size: 13px; - color: var(--danger); - font-weight: 600; - } - - .subscription-configurator-actions { - display: flex; - flex-direction: column; - gap: 8px; - } - - .subscription-configurator-status { - font-size: 13px; - font-weight: 600; - } - - .subscription-configurator-status.error { - color: var(--danger); - } - - .subscription-configurator-status.success { - color: var(--success); - } - - .subscription-configurator-status.info { - color: var(--text-secondary); - } - - :root[data-theme="dark"] .subscription-configurator-option { - background: rgba(15, 23, 42, 0.6); - border-color: rgba(148, 163, 184, 0.28); - } - - :root[data-theme="dark"] .subscription-configurator-option.active { - background: rgba(37, 99, 235, 0.12); - border-color: rgba(96, 165, 250, 0.6); - } - - :root[data-theme="dark"] .subscription-configurator-balance-warning { - background: rgba(239, 68, 68, 0.2); - border-color: rgba(239, 68, 68, 0.4); - } - .promo-offers { display: flex; flex-direction: column; @@ -3642,94 +3350,6 @@ - - -
@@ -4159,14 +3779,6 @@ let promoOfferTimerHandle = null; let referralListExpanded = false; let referralCopyResetHandle = null; - let subscriptionConfiguratorData = null; - let subscriptionConfiguratorLoading = false; - const subscriptionConfiguratorSelections = { - periodId: null, - trafficId: null, - servers: new Set(), - devices: null, - }; if (typeof tg.expand === 'function') { tg.expand(); @@ -4365,46 +3977,6 @@ 'topup.status.retry': 'Try again', 'topup.done': 'Done', 'button.buy_subscription': 'Buy Subscription', - 'subscription_configurator.title': 'Subscription configurator', - 'subscription_configurator.summary.placeholder': 'Choose parameters to see the final price.', - 'subscription_configurator.period.title': 'Subscription period', - 'subscription_configurator.period.description': 'Choose how long you need access.', - 'subscription_configurator.period.meta': 'Valid for {months} month(s)', - 'subscription_configurator.traffic.title': 'Traffic', - 'subscription_configurator.traffic.description': 'Select the monthly traffic package.', - 'subscription_configurator.traffic.unlimited': 'Unlimited traffic', - 'subscription_configurator.servers.title': 'Servers', - 'subscription_configurator.servers.description': 'Choose the regions for connection.', - 'subscription_configurator.servers.meta.exact': 'Select {count} server(s)', - 'subscription_configurator.servers.meta.range': 'Select {min}–{max} servers', - 'subscription_configurator.servers.meta.min': 'Select at least {count} server(s)', - 'subscription_configurator.servers.meta.max': 'Up to {count} servers', - 'subscription_configurator.servers.fixed_single': 'Included server: {list}', - 'subscription_configurator.servers.fixed_multiple': 'Included servers: {list}', - 'subscription_configurator.devices.title': 'Devices', - 'subscription_configurator.devices.description': 'Number of simultaneously connected devices.', - 'subscription_configurator.devices.included': 'Included: {count}', - 'subscription_configurator.devices.meta': '{min}–{max} devices', - 'subscription_configurator.summary.period': 'Period: {label}', - 'subscription_configurator.summary.traffic': 'Traffic: {label}', - 'subscription_configurator.summary.servers': '{count} servers: {list}', - 'subscription_configurator.summary.servers_one': '1 server: {list}', - 'subscription_configurator.summary.devices': '{count} devices', - 'subscription_configurator.summary.devices_one': '{count} device', - 'subscription_configurator.price.discount': 'You save {value}.', - 'subscription_configurator.discount.badge': '-{percent}%', - 'subscription_configurator.balance.insufficient': 'Not enough balance. Missing {amount}.', - 'subscription_configurator.status.validation.servers_min': 'At least {count} server required.', - 'subscription_configurator.status.validation.servers_max': 'Maximum {count} servers allowed.', - 'subscription_configurator.status.loading': 'Processing purchase…', - 'subscription_configurator.action.topup': 'Top up balance', - 'subscription_configurator.action.purchase': 'Buy subscription', - 'subscription_configurator.action.activate': 'Activate for free', - 'subscription_configurator.recommended': 'Recommended', - 'subscription_configurator.success': 'Subscription purchased successfully!', - 'subscription_configurator.error.unauthorized': 'Authorization error. Please reopen the mini app.', - 'subscription_configurator.error.insufficient_funds': 'Not enough balance to complete the purchase.', - 'subscription_configurator.error.generic': 'Failed to complete the purchase. Please try again later.', 'card.balance.title': 'Balance', 'subscription_settings.title': 'Subscription settings', 'subscription_settings.summary.servers': 'Servers: {count}', @@ -4686,46 +4258,6 @@ 'topup.status.retry': 'Повторить попытку', 'topup.done': 'Готово', 'button.buy_subscription': 'Купить подписку', - 'subscription_configurator.title': 'Конфигуратор подписки', - 'subscription_configurator.summary.placeholder': 'Выберите параметры, чтобы увидеть итоговую стоимость.', - 'subscription_configurator.period.title': 'Период подписки', - 'subscription_configurator.period.description': 'Выберите срок доступа.', - 'subscription_configurator.period.meta': 'Доступ на {months} мес.', - 'subscription_configurator.traffic.title': 'Трафик', - 'subscription_configurator.traffic.description': 'Выберите объём трафика в месяц.', - 'subscription_configurator.traffic.unlimited': 'Безлимитный трафик', - 'subscription_configurator.servers.title': 'Серверы', - 'subscription_configurator.servers.description': 'Выберите регионы подключения.', - 'subscription_configurator.servers.meta.exact': 'Нужно выбрать {count} сервер(ов)', - 'subscription_configurator.servers.meta.range': 'Выберите от {min} до {max} серверов', - 'subscription_configurator.servers.meta.min': 'Выберите минимум {count} сервер(ов)', - 'subscription_configurator.servers.meta.max': 'Не более {count} серверов', - 'subscription_configurator.servers.fixed_single': 'Доступный сервер: {list}', - 'subscription_configurator.servers.fixed_multiple': 'Доступные серверы: {list}', - 'subscription_configurator.devices.title': 'Устройства', - 'subscription_configurator.devices.description': 'Количество одновременных подключений.', - 'subscription_configurator.devices.included': 'Включено: {count}', - 'subscription_configurator.devices.meta': '{min}–{max} устройств', - 'subscription_configurator.summary.period': 'Период: {label}', - 'subscription_configurator.summary.traffic': 'Трафик: {label}', - 'subscription_configurator.summary.servers': '{count} серверов: {list}', - 'subscription_configurator.summary.servers_one': '1 сервер: {list}', - 'subscription_configurator.summary.devices': '{count} устройств', - 'subscription_configurator.summary.devices_one': '{count} устройство', - 'subscription_configurator.price.discount': 'Экономия {value}.', - 'subscription_configurator.discount.badge': '-{percent}%', - 'subscription_configurator.balance.insufficient': 'Недостаточно средств. Не хватает {amount}.', - 'subscription_configurator.status.validation.servers_min': 'Нужно выбрать минимум {count} сервер(ов).', - 'subscription_configurator.status.validation.servers_max': 'Можно выбрать не более {count} серверов.', - 'subscription_configurator.status.loading': 'Оформляем подписку…', - 'subscription_configurator.action.topup': 'Пополнить баланс', - 'subscription_configurator.action.purchase': 'Купить подписку', - 'subscription_configurator.action.activate': 'Активировать бесплатно', - 'subscription_configurator.recommended': 'Рекомендуем', - 'subscription_configurator.success': 'Подписка успешно оформлена!', - 'subscription_configurator.error.unauthorized': 'Ошибка авторизации. Перезапустите мини-приложение.', - 'subscription_configurator.error.insufficient_funds': 'Недостаточно средств для покупки.', - 'subscription_configurator.error.generic': 'Не удалось завершить покупку. Попробуйте позже.', 'card.balance.title': 'Баланс', 'subscription_settings.title': 'Настройка подписки', 'subscription_settings.summary.servers': 'Серверов: {count}', @@ -6114,10 +5646,6 @@ : autopayLabel; } - subscriptionConfiguratorData = normalizeSubscriptionConfigurator(userData); - resetSubscriptionConfiguratorSelections(subscriptionConfiguratorData); - renderSubscriptionConfigurator(); - renderSubscriptionSettingsCard(); renderPromoOffers(); renderPromoSection(); @@ -7176,29 +6704,6 @@ return Array.isArray(value) ? value : []; } - function clampValue(value, min, max) { - let minValue = Number.isFinite(min) ? min : 0; - let maxValue = Number.isFinite(max) && max > 0 ? max : max === 0 ? 0 : null; - if (maxValue !== null && maxValue < minValue) { - const temp = maxValue; - maxValue = minValue; - minValue = temp; - } - - const numeric = coerceNumber(value, null); - if (numeric === null || Number.isNaN(numeric)) { - return minValue; - } - - if (numeric < minValue) { - return minValue; - } - if (maxValue !== null && numeric > maxValue) { - return maxValue; - } - return Math.trunc(numeric); - } - function coerceNumber(value, fallback = null) { if (typeof value === 'number') { return Number.isFinite(value) ? value : fallback; @@ -10079,1601 +9584,6 @@ }; } - function normalizeConfiguratorPeriodOption(option, index) { - if (!option || typeof option !== 'object') { - return null; - } - const idRaw = option.id - ?? option.key - ?? option.code - ?? option.period_id - ?? option.periodId - ?? option.identifier - ?? null; - const id = idRaw != null ? String(idRaw) : `period-${index}`; - if (!id) { - return null; - } - - const days = coercePositiveInt( - option.period_days - ?? option.days - ?? option.duration_days - ?? option.durationDays, - null - ); - let months = coercePositiveInt( - option.months - ?? option.period_months - ?? option.periodMonths - ?? option.duration_months - ?? option.durationMonths, - null - ); - if (!months && days) { - months = Math.max(1, Math.round(days / 30)); - } - - const fallbackLabel = months - ? `${months} ${months === 1 ? 'month' : 'months'}` - : id; - const label = option.label - || option.title - || option.name - || option.display_name - || option.displayName - || fallbackLabel; - - const priceKopeks = coercePositiveInt( - option.total_price_kopeks - ?? option.price_kopeks - ?? option.final_price_kopeks - ?? option.discounted_price_kopeks - ?? option.amount_kopeks - ?? option.priceKopeks - ?? option.price - ?? option.cost, - 0 - ); - const originalKopeks = coercePositiveInt( - option.original_price_kopeks - ?? option.base_price_kopeks - ?? option.price_before_discount_kopeks - ?? option.list_price_kopeks - ?? option.originalPriceKopeks - ?? option.originalPrice - ?? null, - null - ); - const discountPercent = coercePositiveInt( - option.discount_percent - ?? option.period_discount_percent - ?? option.discountPercent - ?? option.discount, - null - ); - - return { - id, - label, - days, - months: months || (days ? Math.max(1, Math.round(days / 30)) : 1), - priceKopeks: Number.isFinite(priceKopeks) ? priceKopeks : 0, - originalKopeks: Number.isFinite(originalKopeks) ? originalKopeks : null, - discountPercent, - priceLabel: option.price_label || option.priceLabel || null, - originalLabel: option.original_price_label || option.originalPriceLabel || option.base_price_label || null, - description: option.description || option.subtitle || null, - recommended: coerceBoolean(option.is_recommended ?? option.recommended ?? option.best_value ?? false, false), - disabled: coerceBoolean(option.disabled ?? option.is_disabled ?? option.unavailable ?? false, false), - }; - } - - function normalizeConfiguratorTrafficOption(option, index) { - if (!option || typeof option !== 'object') { - return null; - } - - const idRaw = option.id - ?? option.key - ?? option.code - ?? option.option_id - ?? option.optionId - ?? option.value - ?? null; - const id = idRaw != null ? String(idRaw) : `traffic-${index}`; - const valueGb = coerceNumber( - option.value - ?? option.gb - ?? option.limit - ?? option.traffic_gb - ?? option.trafficGb - ?? null, - null - ); - - const pricePerMonth = coercePositiveInt( - option.price_per_month_kopeks - ?? option.pricePerMonthKopeks - ?? option.price_kopeks - ?? option.priceKopeks - ?? option.final_price_kopeks - ?? option.discounted_price_kopeks - ?? option.amount_kopeks - ?? option.price - ?? 0, - 0 - ); - const originalPerMonth = coercePositiveInt( - option.original_price_per_month_kopeks - ?? option.originalPricePerMonthKopeks - ?? option.original_price_kopeks - ?? option.originalPriceKopeks - ?? option.base_price_per_month_kopeks - ?? option.price_before_discount_kopeks - ?? null, - null - ); - const discountPercent = coercePositiveInt( - option.discount_percent - ?? option.discountPercent - ?? option.discount, - null - ); - - return { - id, - valueGb, - label: option.label - || option.title - || option.name - || (valueGb === 0 - ? t('values.unlimited') - : (valueGb !== null ? `${valueGb} ${t('units.gb')}` : id)), - pricePerMonthKopeks: Number.isFinite(pricePerMonth) ? pricePerMonth : 0, - originalPricePerMonthKopeks: Number.isFinite(originalPerMonth) ? originalPerMonth : null, - discountPercent, - priceLabel: option.price_label || option.priceLabel || null, - originalLabel: option.original_price_label || option.originalPriceLabel || null, - description: option.description || option.subtitle || null, - isAvailable: coerceBoolean(option.is_available ?? option.available ?? option.enabled ?? option.selectable ?? true, true), - isIncluded: coerceBoolean(option.is_included ?? option.included ?? option.default ?? false, false), - recommended: coerceBoolean(option.is_recommended ?? option.recommended ?? false, false), - }; - } - - function normalizeConfiguratorServerOption(option, index) { - if (!option) { - return null; - } - const base = normalizeServerEntry(option); - const uuid = base?.uuid - || option.id - || option.code - || option.squad_uuid - || option.short_uuid - || option.shortUuid - || `server-${index}`; - const name = base?.name - || option.name - || option.title - || option.display_name - || option.displayName - || option.label - || uuid; - if (!uuid) { - return null; - } - - const pricePerMonth = coercePositiveInt( - option.price_per_month_kopeks - ?? option.pricePerMonthKopeks - ?? option.price_kopeks - ?? option.priceKopeks - ?? option.final_price_kopeks - ?? option.discounted_price_kopeks - ?? option.amount_kopeks - ?? option.price - ?? option.cost - ?? 0, - 0 - ); - const originalPerMonth = coercePositiveInt( - option.original_price_per_month_kopeks - ?? option.originalPricePerMonthKopeks - ?? option.original_price_kopeks - ?? option.originalPriceKopeks - ?? option.base_price_per_month_kopeks - ?? option.price_before_discount_kopeks - ?? null, - null - ); - const discountPercent = coercePositiveInt( - option.discount_percent - ?? option.discountPercent - ?? option.discount, - null - ); - - return { - uuid: String(uuid), - name, - pricePerMonthKopeks: Number.isFinite(pricePerMonth) ? pricePerMonth : 0, - originalPricePerMonthKopeks: Number.isFinite(originalPerMonth) ? originalPerMonth : null, - discountPercent, - priceLabel: option.price_label || option.priceLabel || null, - originalLabel: option.original_price_label || option.originalPriceLabel || null, - isAvailable: coerceBoolean(option.is_available ?? option.available ?? option.enabled ?? option.selectable ?? true, true), - isDefault: coerceBoolean(option.is_default ?? option.default ?? option.recommended ?? false, false), - disabledReason: option.disabled_reason || option.reason || null, - description: option.description || option.subtitle || null, - }; - } - - function normalizeSubscriptionConfiguratorPricing(root, currency) { - const pricing = root?.pricing || root?.price || root?.summary || {}; - return { - basePriceKopeks: coercePositiveInt( - pricing.base_price_kopeks - ?? pricing.basePriceKopeks - ?? null, - null - ), - baseOriginalPriceKopeks: coercePositiveInt( - pricing.base_original_price_kopeks - ?? pricing.baseOriginalPriceKopeks - ?? null, - null - ), - addonsPriceKopeks: coercePositiveInt( - pricing.addons_price_kopeks - ?? pricing.addonsPriceKopeks - ?? null, - null - ), - finalPriceKopeks: coercePositiveInt( - pricing.final_price_kopeks - ?? pricing.total_price_kopeks - ?? pricing.totalPriceKopeks - ?? root.total_price_kopeks - ?? root.totalPriceKopeks - ?? null, - null - ), - discountTotalKopeks: coercePositiveInt( - pricing.discount_total_kopeks - ?? pricing.discountTotalKopeks - ?? null, - null - ), - currency: (pricing.currency || currency || 'RUB').toString().toUpperCase(), - label: pricing.label || null, - }; - } - - function normalizeSubscriptionConfigurator(payload) { - if (!payload || typeof payload !== 'object') { - return null; - } - - const root = payload.subscription_configurator - || payload.subscriptionConfigurator - || payload.subscription_builder - || payload.subscriptionBuilder - || payload.configurator - || null; - if (!root || typeof root !== 'object') { - return null; - } - - const currency = (root.currency || payload.currency || userData?.balance_currency || 'RUB').toString().toUpperCase(); - - const periodOptionsRaw = ensureArray( - root.periods - || root.period_options - || root.periodOptions - || root.period?.options - || root.options?.periods - || [] - ); - const normalizedPeriods = periodOptionsRaw - .map((option, index) => normalizeConfiguratorPeriodOption(option, index)) - .filter(Boolean); - const periodModeRaw = root.period?.mode - ?? root.period_mode - ?? root.periodMode - ?? root.mode?.period - ?? null; - const periodMode = (periodModeRaw ? String(periodModeRaw).toLowerCase() : (normalizedPeriods.length > 1 ? 'selectable' : 'fixed')); - const selectedPeriodRaw = root.selected_period - ?? root.selected_period_id - ?? root.period?.selected - ?? root.period?.selected_id - ?? root.periodId - ?? root.period_id - ?? root.selections?.period - ?? null; - let selectedPeriodId = selectedPeriodRaw != null ? String(selectedPeriodRaw) : null; - if (periodMode === 'fixed') { - selectedPeriodId = normalizedPeriods[0]?.id || selectedPeriodId; - } else if (normalizedPeriods.length) { - const preferred = normalizedPeriods.find(option => option.id === selectedPeriodId && !option.disabled) - || normalizedPeriods.find(option => option.recommended && !option.disabled) - || normalizedPeriods.find(option => !option.disabled) - || normalizedPeriods[0]; - selectedPeriodId = preferred ? preferred.id : selectedPeriodId; - } - - const trafficOptionsRaw = ensureArray( - root.traffic?.options - || root.traffic_options - || root.available_traffic - || root.options?.traffic - || [] - ); - const normalizedTraffic = trafficOptionsRaw - .map((option, index) => normalizeConfiguratorTrafficOption(option, index)) - .filter(Boolean); - const trafficModeRaw = root.traffic?.mode - ?? root.traffic_mode - ?? root.trafficMode - ?? root.mode?.traffic - ?? null; - const trafficMode = (trafficModeRaw ? String(trafficModeRaw).toLowerCase() : (normalizedTraffic.length > 1 ? 'selectable' : 'fixed')); - const selectedTrafficRaw = root.selected_traffic - ?? root.selected_traffic_id - ?? root.traffic?.selected - ?? root.traffic?.selected_id - ?? root.selections?.traffic - ?? null; - let selectedTrafficId = selectedTrafficRaw != null ? String(selectedTrafficRaw) : null; - if (trafficMode === 'fixed') { - selectedTrafficId = normalizedTraffic[0]?.id || selectedTrafficId; - } else if (normalizedTraffic.length) { - const preferredTraffic = normalizedTraffic.find(option => option.id === selectedTrafficId && option.isAvailable) - || normalizedTraffic.find(option => option.recommended && option.isAvailable) - || normalizedTraffic.find(option => option.isAvailable) - || normalizedTraffic[0]; - selectedTrafficId = preferredTraffic ? preferredTraffic.id : selectedTrafficId; - } - - const serverOptionsRaw = ensureArray( - root.servers?.options - || root.servers?.available - || root.server_options - || root.available_servers - || root.available_squads - || root.options?.servers - || [] - ); - const normalizedServers = serverOptionsRaw - .map((option, index) => normalizeConfiguratorServerOption(option, index)) - .filter(Boolean); - const serversModeRaw = root.servers?.mode - ?? root.servers_mode - ?? root.serversMode - ?? root.mode?.servers - ?? null; - let serversMode = (serversModeRaw ? String(serversModeRaw).toLowerCase() : (normalizedServers.length > 1 ? 'selectable' : 'fixed')); - if (!normalizedServers.length) { - serversMode = 'fixed'; - } - - const minServers = coercePositiveInt( - root.servers?.min - ?? root.servers?.min_selectable - ?? root.min_servers - ?? root.serversMin - ?? 0, - 0 - ) || 0; - const maxServersValue = coercePositiveInt( - root.servers?.max - ?? root.servers?.max_selectable - ?? root.max_servers - ?? root.serversMax - ?? 0, - 0 - ) || 0; - const maxServers = maxServersValue > 0 ? maxServersValue : null; - - const selectedServersRaw = ensureArray( - root.servers?.selected - ?? root.servers?.current - ?? root.selected_servers - ?? root.selected_squads - ?? root.current_servers - ?? root.current_squads - ?? root.selections?.servers - ?? root.selections?.squads - ?? [] - ).map(item => String(item)).filter(Boolean); - const selectedServers = new Set(selectedServersRaw.filter(uuid => normalizedServers.some(server => server.uuid === uuid))); - - if (serversMode === 'fixed') { - if (!selectedServers.size && normalizedServers.length) { - normalizedServers.forEach(server => selectedServers.add(server.uuid)); - } - } else if (normalizedServers.length) { - if (!selectedServers.size) { - const selectableServers = normalizedServers.filter(server => server.isAvailable); - if (selectableServers.length) { - const required = Math.max(1, minServers || 1); - selectableServers.slice(0, required).forEach(server => selectedServers.add(server.uuid)); - } - } - if (minServers && selectedServers.size < minServers) { - const remaining = normalizedServers - .filter(server => server.isAvailable && !selectedServers.has(server.uuid)); - for (const server of remaining) { - selectedServers.add(server.uuid); - if (selectedServers.size >= minServers) { - break; - } - } - } - if (maxServers && selectedServers.size > maxServers) { - const keep = Array.from(selectedServers).slice(0, maxServers); - selectedServers.clear(); - keep.forEach(uuid => selectedServers.add(uuid)); - } - } - - const fixedServers = ensureArray( - root.servers?.fixed - ?? root.servers?.fixed_servers - ?? root.fixed_servers - ?? root.fixed_squads - ?? [] - ).map(entry => { - if (typeof entry === 'string') { - return entry; - } - if (entry && typeof entry === 'object') { - return entry.name || entry.title || entry.label || entry.uuid || ''; - } - return ''; - }).filter(Boolean); - - const devicesRoot = root.devices || root.device_options || root.options?.devices || {}; - const devicesMin = Math.max(1, coercePositiveInt( - devicesRoot.min - ?? devicesRoot.min_devices - ?? root.devices_min - ?? root.min_devices - ?? 1, - 1 - ) || 1); - const devicesMaxValue = coercePositiveInt( - devicesRoot.max - ?? devicesRoot.max_devices - ?? root.devices_max - ?? root.max_devices - ?? 0, - 0 - ); - const devicesMax = devicesMaxValue && devicesMaxValue >= devicesMin ? devicesMaxValue : Math.max(devicesMin, devicesMaxValue || devicesMin); - const devicesStep = Math.max(1, coercePositiveInt(devicesRoot.step ?? devicesRoot.increment ?? 1, 1) || 1); - const devicesIncluded = coercePositiveInt( - devicesRoot.included - ?? devicesRoot.base - ?? devicesRoot.default - ?? root.default_devices - ?? devicesMin, - devicesMin - ); - const devicesPricePerMonth = coercePositiveInt( - devicesRoot.price_per_device_per_month_kopeks - ?? devicesRoot.pricePerDevicePerMonthKopeks - ?? devicesRoot.price_per_month_kopeks - ?? devicesRoot.price_kopeks - ?? devicesRoot.priceKopeks - ?? 0, - 0 - ); - const devicesOriginalPerMonth = coercePositiveInt( - devicesRoot.original_price_per_device_per_month_kopeks - ?? devicesRoot.originalPricePerDevicePerMonthKopeks - ?? devicesRoot.original_price_per_month_kopeks - ?? devicesRoot.original_price_kopeks - ?? null, - null - ); - const devicesSelectedRaw = coercePositiveInt( - root.selected_devices - ?? root.device_limit - ?? devicesRoot.selected - ?? devicesRoot.value - ?? root.selections?.devices - ?? root.selections?.device_limit, - null - ); - const devicesSelected = clampValue( - devicesSelectedRaw ?? devicesIncluded ?? devicesMin, - devicesMin, - devicesMax - ); - - return { - enabled: coerceBoolean(root.enabled ?? root.available ?? true, true), - currency, - period: { - mode: periodMode === 'selectable' ? 'selectable' : 'fixed', - options: normalizedPeriods, - selectedId: selectedPeriodId, - fixedLabel: root.period?.fixed_label || root.period?.label || (periodMode === 'fixed' ? normalizedPeriods[0]?.label : null), - }, - traffic: { - mode: trafficMode === 'selectable' ? 'selectable' : 'fixed', - options: normalizedTraffic, - selectedId: selectedTrafficId, - fixedLabel: root.traffic?.fixed_label || root.traffic?.label || (trafficMode === 'fixed' ? normalizedTraffic[0]?.label : null), - }, - servers: { - mode: serversMode === 'selectable' ? 'selectable' : 'fixed', - options: normalizedServers, - min: minServers, - max: maxServers, - selected: Array.from(selectedServers), - fixed: fixedServers, - hint: root.servers?.hint || null, - }, - devices: { - min: devicesMin, - max: devicesMax, - step: devicesStep, - included: devicesIncluded, - pricePerDevicePerMonthKopeks: devicesPricePerMonth, - originalPricePerDevicePerMonthKopeks: devicesOriginalPerMonth, - note: devicesRoot.note || devicesRoot.description || null, - selected: devicesSelected, - }, - pricing: normalizeSubscriptionConfiguratorPricing(root, currency), - }; - } - - function resetSubscriptionConfiguratorSelections(data) { - subscriptionConfiguratorSelections.periodId = null; - subscriptionConfiguratorSelections.trafficId = null; - subscriptionConfiguratorSelections.servers = new Set(); - subscriptionConfiguratorSelections.devices = null; - - if (!data || !data.enabled) { - return; - } - - if (data.period?.selectedId) { - subscriptionConfiguratorSelections.periodId = data.period.selectedId; - } else if (data.period?.options?.length) { - subscriptionConfiguratorSelections.periodId = data.period.options[0].id; - } - - if (data.traffic?.selectedId) { - subscriptionConfiguratorSelections.trafficId = data.traffic.selectedId; - } else if (data.traffic?.options?.length) { - subscriptionConfiguratorSelections.trafficId = data.traffic.options[0].id; - } - - if (Array.isArray(data.servers?.selected)) { - subscriptionConfiguratorSelections.servers = new Set(data.servers.selected); - } else { - subscriptionConfiguratorSelections.servers = new Set(); - } - - if (subscriptionConfiguratorSelections.servers.size === 0 && Array.isArray(data.servers?.options)) { - const available = data.servers.options.filter(server => server.isAvailable); - if (available.length) { - const min = data.servers.min || 0; - const required = data.servers.mode === 'fixed' - ? available.length - : Math.max(1, min || 1); - available.slice(0, required).forEach(server => subscriptionConfiguratorSelections.servers.add(server.uuid)); - } - } - - const devicesMin = data.devices?.min || 1; - const devicesMax = data.devices?.max || devicesMin; - const defaultDevices = data.devices?.selected ?? data.devices?.included ?? devicesMin; - subscriptionConfiguratorSelections.devices = clampValue(defaultDevices, devicesMin, devicesMax); - } - - function getConfiguratorSelectedPeriod(data) { - if (!data?.period?.options?.length) { - return null; - } - const selectedId = subscriptionConfiguratorSelections.periodId; - return data.period.options.find(option => option.id === selectedId) - || data.period.options[0] - || null; - } - - function getConfiguratorSelectedTraffic(data) { - if (!data?.traffic?.options?.length) { - return null; - } - const selectedId = subscriptionConfiguratorSelections.trafficId; - return data.traffic.options.find(option => option.id === selectedId) - || data.traffic.options[0] - || null; - } - - function getConfiguratorSelectedServers(data) { - if (!data?.servers?.options?.length) { - return []; - } - const selectedSet = subscriptionConfiguratorSelections.servers instanceof Set - ? subscriptionConfiguratorSelections.servers - : new Set(); - return data.servers.options.filter(option => selectedSet.has(option.uuid)); - } - - function getConfiguratorSelectedDevices() { - return subscriptionConfiguratorSelections.devices ?? null; - } - - function shouldShowSubscriptionConfigurator() { - if (!subscriptionConfiguratorData || !subscriptionConfiguratorData.enabled) { - return false; - } - const statusRaw = String( - userData?.user?.subscription_actual_status - || userData?.user?.subscription_status - || '' - ).toLowerCase(); - if (!statusRaw) { - return true; - } - if (['trial', 'expired', 'disabled'].includes(statusRaw)) { - return true; - } - if (statusRaw === 'active') { - const typeRaw = String(userData?.subscription_type || '').toLowerCase(); - if (typeRaw === 'trial') { - return true; - } - } - return false; - } - - function renderSubscriptionConfigurator() { - const card = document.getElementById('subscriptionConfiguratorCard'); - if (!card) { - return; - } - - if (!shouldShowSubscriptionConfigurator()) { - card.classList.add('hidden'); - showSubscriptionConfiguratorStatus(null); - return; - } - - const data = subscriptionConfiguratorData; - if (!data || !data.enabled) { - card.classList.add('hidden'); - showSubscriptionConfiguratorStatus(null); - return; - } - - card.classList.remove('hidden'); - - renderSubscriptionConfiguratorPeriod(data); - renderSubscriptionConfiguratorTraffic(data); - renderSubscriptionConfiguratorServers(data); - renderSubscriptionConfiguratorDevices(data); - updateSubscriptionConfiguratorSummary(); - updateSubscriptionConfiguratorPricing(); - } - - function renderSubscriptionConfiguratorPeriod(data) { - const section = document.getElementById('subscriptionConfiguratorPeriodSection'); - const list = document.getElementById('subscriptionConfiguratorPeriodOptions'); - const fixedValue = document.getElementById('subscriptionConfiguratorPeriodFixed'); - const meta = document.getElementById('subscriptionConfiguratorPeriodMeta'); - if (!section) { - return; - } - - if (!data?.period?.options?.length) { - section.classList.add('hidden'); - return; - } - - section.classList.remove('hidden'); - - const selected = getConfiguratorSelectedPeriod(data); - if (meta) { - if (selected?.months) { - const template = t('subscription_configurator.period.meta'); - if (template && template !== 'subscription_configurator.period.meta') { - meta.textContent = template.replace('{months}', String(selected.months)); - } else { - meta.textContent = `${selected.months} ${selected.months === 1 ? 'month' : 'months'}`; - } - } else { - meta.textContent = ''; - } - } - - if (data.period.mode === 'fixed') { - if (list) { - list.innerHTML = ''; - list.classList.add('hidden'); - } - if (fixedValue) { - fixedValue.textContent = data.period.fixedLabel || selected?.label || ''; - fixedValue.classList.remove('hidden'); - } - return; - } - - if (fixedValue) { - fixedValue.classList.add('hidden'); - fixedValue.textContent = ''; - } - - if (!list) { - return; - } - list.classList.remove('hidden'); - list.innerHTML = ''; - - data.period.options.forEach(option => { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'subscription-configurator-option'; - if (option.disabled) { - button.classList.add('disabled'); - button.disabled = true; - } - if (subscriptionConfiguratorSelections.periodId === option.id) { - button.classList.add('active'); - } - - const title = document.createElement('div'); - title.className = 'subscription-configurator-option-title'; - title.textContent = option.label || option.id; - button.appendChild(title); - - if (option.description) { - const metaText = document.createElement('div'); - metaText.className = 'subscription-configurator-option-meta'; - metaText.textContent = option.description; - button.appendChild(metaText); - } - - const price = document.createElement('div'); - price.className = 'subscription-configurator-option-price'; - - const original = option.originalKopeks; - if (original !== null && original > option.priceKopeks) { - const originalSpan = document.createElement('span'); - originalSpan.className = 'original'; - originalSpan.textContent = option.originalLabel || formatPriceFromKopeks(original, data.currency); - price.appendChild(originalSpan); - } - - const currentSpan = document.createElement('span'); - currentSpan.className = 'current'; - currentSpan.textContent = option.priceLabel || formatPriceFromKopeks(option.priceKopeks, data.currency); - price.appendChild(currentSpan); - - if (option.discountPercent) { - const discountSpan = document.createElement('span'); - discountSpan.className = 'discount'; - discountSpan.textContent = `−${option.discountPercent}%`; - price.appendChild(discountSpan); - } - - button.appendChild(price); - - if (option.recommended) { - const badge = document.createElement('div'); - badge.className = 'subscription-settings-chip'; - const badgeKey = 'subscription_configurator.recommended'; - const label = t(badgeKey); - badge.textContent = label && label !== badgeKey ? label : 'Recommended'; - button.appendChild(badge); - } - - button.addEventListener('click', () => { - if (subscriptionConfiguratorSelections.periodId === option.id || option.disabled) { - return; - } - subscriptionConfiguratorSelections.periodId = option.id; - showSubscriptionConfiguratorStatus(null); - renderSubscriptionConfiguratorPeriod(data); - updateSubscriptionConfiguratorSummary(); - updateSubscriptionConfiguratorPricing(); - }); - - list.appendChild(button); - }); - } - - function renderSubscriptionConfiguratorTraffic(data) { - const section = document.getElementById('subscriptionConfiguratorTrafficSection'); - const list = document.getElementById('subscriptionConfiguratorTrafficOptions'); - const fixedValue = document.getElementById('subscriptionConfiguratorTrafficFixed'); - const meta = document.getElementById('subscriptionConfiguratorTrafficMeta'); - if (!section) { - return; - } - - if (!data?.traffic?.options?.length) { - section.classList.add('hidden'); - return; - } - - section.classList.remove('hidden'); - - const selected = getConfiguratorSelectedTraffic(data); - if (meta) { - if (selected?.valueGb === 0) { - const unlimitedKey = 'subscription_configurator.traffic.unlimited'; - const unlimitedLabel = t(unlimitedKey); - meta.textContent = unlimitedLabel && unlimitedLabel !== unlimitedKey - ? unlimitedLabel - : t('values.unlimited'); - } else if (selected?.valueGb) { - meta.textContent = `${selected.valueGb} ${t('units.gb')}`; - } else { - meta.textContent = ''; - } - } - - if (data.traffic.mode === 'fixed') { - if (list) { - list.innerHTML = ''; - list.classList.add('hidden'); - } - if (fixedValue) { - fixedValue.textContent = data.traffic.fixedLabel || selected?.label || ''; - fixedValue.classList.remove('hidden'); - } - return; - } - - if (fixedValue) { - fixedValue.classList.add('hidden'); - fixedValue.textContent = ''; - } - - if (!list) { - return; - } - list.classList.remove('hidden'); - list.innerHTML = ''; - - data.traffic.options.forEach(option => { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'subscription-configurator-option'; - if (!option.isAvailable) { - button.classList.add('disabled'); - button.disabled = true; - } - if (subscriptionConfiguratorSelections.trafficId === option.id) { - button.classList.add('active'); - } - - const title = document.createElement('div'); - title.className = 'subscription-configurator-option-title'; - title.textContent = option.label || option.id; - button.appendChild(title); - - if (option.description) { - const metaText = document.createElement('div'); - metaText.className = 'subscription-configurator-option-meta'; - metaText.textContent = option.description; - button.appendChild(metaText); - } - - const price = document.createElement('div'); - price.className = 'subscription-configurator-option-price'; - - const original = option.originalPricePerMonthKopeks; - if (original !== null && original > option.pricePerMonthKopeks) { - const originalSpan = document.createElement('span'); - originalSpan.className = 'original'; - originalSpan.textContent = option.originalLabel || formatPriceFromKopeks(original, data.currency); - price.appendChild(originalSpan); - } - - const currentSpan = document.createElement('span'); - currentSpan.className = 'current'; - currentSpan.textContent = option.priceLabel || formatPriceFromKopeks(option.pricePerMonthKopeks, data.currency); - price.appendChild(currentSpan); - - if (option.discountPercent) { - const discountSpan = document.createElement('span'); - discountSpan.className = 'discount'; - discountSpan.textContent = `−${option.discountPercent}%`; - price.appendChild(discountSpan); - } - - button.appendChild(price); - - button.addEventListener('click', () => { - if (!option.isAvailable) { - return; - } - if (subscriptionConfiguratorSelections.trafficId === option.id) { - return; - } - subscriptionConfiguratorSelections.trafficId = option.id; - showSubscriptionConfiguratorStatus(null); - renderSubscriptionConfiguratorTraffic(data); - updateSubscriptionConfiguratorSummary(); - updateSubscriptionConfiguratorPricing(); - }); - - list.appendChild(button); - }); - } - - function renderSubscriptionConfiguratorServers(data) { - const section = document.getElementById('subscriptionConfiguratorServersSection'); - const list = document.getElementById('subscriptionConfiguratorServersOptions'); - const fixedValue = document.getElementById('subscriptionConfiguratorServersFixed'); - const hintElement = document.getElementById('subscriptionConfiguratorServersHint'); - const meta = document.getElementById('subscriptionConfiguratorServersMeta'); - if (!section) { - return; - } - - if (!data?.servers?.options?.length) { - section.classList.add('hidden'); - return; - } - - section.classList.remove('hidden'); - - if (hintElement) { - hintElement.textContent = data.servers.hint || ''; - hintElement.classList.toggle('hidden', !hintElement.textContent); - } - - if (meta) { - const min = data.servers.min || 0; - const max = data.servers.max || null; - let metaText = ''; - if (min && max && min === max) { - const key = 'subscription_configurator.servers.meta.exact'; - const template = t(key); - metaText = template && template !== key - ? template.replace('{count}', String(min)) - : `Select ${min}`; - } else if (min && max) { - const key = 'subscription_configurator.servers.meta.range'; - const template = t(key); - metaText = template && template !== key - ? template.replace('{min}', String(min)).replace('{max}', String(max)) - : `Select ${min}-${max}`; - } else if (min) { - const key = 'subscription_configurator.servers.meta.min'; - const template = t(key); - metaText = template && template !== key - ? template.replace('{count}', String(min)) - : `Select at least ${min}`; - } else if (max) { - const key = 'subscription_configurator.servers.meta.max'; - const template = t(key); - metaText = template && template !== key - ? template.replace('{count}', String(max)) - : `Up to ${max}`; - } else { - metaText = ''; - } - meta.textContent = metaText; - } - - if (data.servers.mode === 'fixed') { - if (list) { - list.innerHTML = ''; - list.classList.add('hidden'); - } - if (fixedValue) { - const selectedServers = getConfiguratorSelectedServers(data); - const names = selectedServers.map(server => server.name).join(', ') || data.servers.fixed?.join(', ') || ''; - const key = selectedServers.length === 1 - ? 'subscription_configurator.servers.fixed_single' - : 'subscription_configurator.servers.fixed_multiple'; - const template = t(key); - fixedValue.textContent = template && template !== key - ? template.replace('{list}', names) - : names; - fixedValue.classList.remove('hidden'); - } - return; - } - - if (fixedValue) { - fixedValue.classList.add('hidden'); - fixedValue.textContent = ''; - } - - if (!list) { - return; - } - list.classList.remove('hidden'); - list.innerHTML = ''; - - const selectedSet = subscriptionConfiguratorSelections.servers instanceof Set - ? subscriptionConfiguratorSelections.servers - : new Set(); - const min = data.servers.min || 0; - const max = data.servers.max || null; - - data.servers.options.forEach(option => { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'subscription-configurator-option'; - if (!option.isAvailable) { - button.classList.add('disabled'); - button.disabled = true; - } - if (selectedSet.has(option.uuid)) { - button.classList.add('active'); - } - - const title = document.createElement('div'); - title.className = 'subscription-configurator-option-title'; - title.textContent = option.name || option.uuid; - button.appendChild(title); - - if (option.description) { - const metaText = document.createElement('div'); - metaText.className = 'subscription-configurator-option-meta'; - metaText.textContent = option.description; - button.appendChild(metaText); - } - - const price = document.createElement('div'); - price.className = 'subscription-configurator-option-price'; - - const original = option.originalPricePerMonthKopeks; - if (original !== null && original > option.pricePerMonthKopeks) { - const originalSpan = document.createElement('span'); - originalSpan.className = 'original'; - originalSpan.textContent = option.originalLabel || formatPriceFromKopeks(original, data.currency); - price.appendChild(originalSpan); - } - - const currentSpan = document.createElement('span'); - currentSpan.className = 'current'; - currentSpan.textContent = option.priceLabel || formatPriceFromKopeks(option.pricePerMonthKopeks, data.currency); - price.appendChild(currentSpan); - - if (option.discountPercent) { - const discountSpan = document.createElement('span'); - discountSpan.className = 'discount'; - discountSpan.textContent = `−${option.discountPercent}%`; - price.appendChild(discountSpan); - } - - button.appendChild(price); - - button.addEventListener('click', () => { - if (!option.isAvailable) { - return; - } - const alreadySelected = selectedSet.has(option.uuid); - if (alreadySelected) { - if (selectedSet.size <= (min || 0)) { - const key = 'subscription_configurator.status.validation.servers_min'; - const template = t(key); - const message = template && template !== key - ? template.replace('{count}', String(min || 1)) - : `At least ${min || 1} server required.`; - showSubscriptionConfiguratorStatus(message, 'error'); - return; - } - selectedSet.delete(option.uuid); - } else { - if (max && selectedSet.size >= max) { - const key = 'subscription_configurator.status.validation.servers_max'; - const template = t(key); - const message = template && template !== key - ? template.replace('{count}', String(max)) - : `Maximum ${max} servers.`; - showSubscriptionConfiguratorStatus(message, 'error'); - return; - } - selectedSet.add(option.uuid); - } - - subscriptionConfiguratorSelections.servers = new Set(selectedSet); - showSubscriptionConfiguratorStatus(null); - renderSubscriptionConfiguratorServers(data); - updateSubscriptionConfiguratorSummary(); - updateSubscriptionConfiguratorPricing(); - }); - - list.appendChild(button); - }); - } - - function renderSubscriptionConfiguratorDevices(data) { - const section = document.getElementById('subscriptionConfiguratorDevicesSection'); - if (!section) { - return; - } - - const decreaseBtn = document.getElementById('subscriptionConfiguratorDevicesDecrease'); - const increaseBtn = document.getElementById('subscriptionConfiguratorDevicesIncrease'); - const valueElement = document.getElementById('subscriptionConfiguratorDevicesValue'); - const noteElement = document.getElementById('subscriptionConfiguratorDevicesNote'); - const meta = document.getElementById('subscriptionConfiguratorDevicesMeta'); - if (!data?.devices) { - section.classList.add('hidden'); - return; - } - - section.classList.remove('hidden'); - - const current = subscriptionConfiguratorSelections.devices ?? data.devices.selected ?? data.devices.min; - const min = data.devices.min ?? 1; - const max = data.devices.max ?? min; - const step = data.devices.step ?? 1; - const included = data.devices.included ?? min; - - if (valueElement) { - valueElement.textContent = String(current); - } - - if (noteElement) { - const key = 'subscription_configurator.devices.included'; - const template = t(key); - const message = template && template !== key - ? template.replace('{count}', String(included)) - : `Included: ${included}`; - noteElement.textContent = message; - noteElement.classList.toggle('hidden', !message); - } - - if (meta) { - const metaKey = 'subscription_configurator.devices.meta'; - const template = t(metaKey); - meta.textContent = template && template !== metaKey - ? template.replace('{min}', String(min)).replace('{max}', String(max)) - : `${min}–${max}`; - } - - if (decreaseBtn) { - decreaseBtn.disabled = current <= min; - decreaseBtn.onclick = () => { - setConfiguratorDeviceCount(current - step); - }; - } - - if (increaseBtn) { - increaseBtn.disabled = current >= max; - increaseBtn.onclick = () => { - setConfiguratorDeviceCount(current + step); - }; - } - } - - function setConfiguratorDeviceCount(next) { - if (!subscriptionConfiguratorData) { - return; - } - const min = subscriptionConfiguratorData.devices?.min ?? 1; - const max = subscriptionConfiguratorData.devices?.max ?? min; - const clamped = clampValue(next, min, max); - if (clamped === subscriptionConfiguratorSelections.devices) { - return; - } - subscriptionConfiguratorSelections.devices = clamped; - showSubscriptionConfiguratorStatus(null); - renderSubscriptionConfiguratorDevices(subscriptionConfiguratorData); - updateSubscriptionConfiguratorSummary(); - updateSubscriptionConfiguratorPricing(); - } - - function updateSubscriptionConfiguratorSummary() { - const summary = document.getElementById('subscriptionConfiguratorSummary'); - if (!summary) { - return; - } - - if (!shouldShowSubscriptionConfigurator()) { - const key = 'subscription_configurator.summary.placeholder'; - const fallback = t(key); - summary.textContent = fallback && fallback !== key - ? fallback - : 'Choose parameters to see the final price.'; - return; - } - - const data = subscriptionConfiguratorData; - const parts = []; - - const period = getConfiguratorSelectedPeriod(data); - if (period) { - const key = 'subscription_configurator.summary.period'; - const template = t(key); - const part = template && template !== key - ? template.replace('{label}', period.label) - : period.label; - if (part) { - parts.push(part); - } - } - - const traffic = getConfiguratorSelectedTraffic(data); - if (traffic) { - const key = 'subscription_configurator.summary.traffic'; - const template = t(key); - const label = traffic.label || (traffic.valueGb === 0 ? t('values.unlimited') : `${traffic.valueGb} ${t('units.gb')}`); - const part = template && template !== key - ? template.replace('{label}', label) - : label; - if (part) { - parts.push(part); - } - } - - const servers = getConfiguratorSelectedServers(data); - if (servers.length) { - const names = servers.map(server => server.name).join(', '); - const key = servers.length === 1 - ? 'subscription_configurator.summary.servers_one' - : 'subscription_configurator.summary.servers'; - const template = t(key); - const part = template && template !== key - ? template.replace('{count}', String(servers.length)).replace('{list}', names) - : names; - if (part) { - parts.push(part); - } - } - - const devices = getConfiguratorSelectedDevices(); - if (devices !== null) { - const key = devices === 1 - ? 'subscription_configurator.summary.devices_one' - : 'subscription_configurator.summary.devices'; - const template = t(key); - const part = template && template !== key - ? template.replace('{count}', String(devices)) - : `${devices}`; - if (part) { - parts.push(part); - } - } - - if (!parts.length) { - const key = 'subscription_configurator.summary.placeholder'; - const fallback = t(key); - summary.textContent = fallback && fallback !== key - ? fallback - : 'Choose parameters to see the final price.'; - return; - } - - summary.textContent = ''; - const strong = document.createElement('strong'); - strong.textContent = parts.join(' • '); - summary.appendChild(strong); - } - - function calculateSubscriptionConfiguratorPrice(data, selections) { - const period = getConfiguratorSelectedPeriod(data); - const months = period?.months || 1; - const basePrice = Number.isFinite(period?.priceKopeks) ? period.priceKopeks : 0; - const baseOriginal = Number.isFinite(period?.originalKopeks) ? period.originalKopeks : basePrice; - - const servers = getConfiguratorSelectedServers(data); - let serversPrice = 0; - let serversOriginal = 0; - servers.forEach(server => { - const price = Number.isFinite(server.pricePerMonthKopeks) ? server.pricePerMonthKopeks : 0; - serversPrice += price * months; - const original = Number.isFinite(server.originalPricePerMonthKopeks) - ? server.originalPricePerMonthKopeks - : price; - serversOriginal += original * months; - }); - - const traffic = getConfiguratorSelectedTraffic(data); - let trafficPrice = 0; - let trafficOriginal = 0; - if (traffic) { - const price = Number.isFinite(traffic.pricePerMonthKopeks) ? traffic.pricePerMonthKopeks : 0; - const original = Number.isFinite(traffic.originalPricePerMonthKopeks) - ? traffic.originalPricePerMonthKopeks - : price; - trafficPrice = price * months; - trafficOriginal = original * months; - } - - const devicesCount = selections.devices ?? data.devices?.selected ?? data.devices?.min ?? 1; - const included = data.devices?.included ?? data.devices?.min ?? 1; - const additionalDevices = Math.max(0, devicesCount - included); - const devicePricePerMonth = Number.isFinite(data.devices?.pricePerDevicePerMonthKopeks) - ? data.devices.pricePerDevicePerMonthKopeks - : 0; - const deviceOriginalPerMonth = Number.isFinite(data.devices?.originalPricePerDevicePerMonthKopeks) - ? data.devices.originalPricePerDevicePerMonthKopeks - : devicePricePerMonth; - const devicesPrice = additionalDevices * devicePricePerMonth * months; - const devicesOriginal = additionalDevices * deviceOriginalPerMonth * months; - - const finalPrice = basePrice + serversPrice + trafficPrice + devicesPrice; - const originalPrice = (baseOriginal ?? basePrice) + serversOriginal + trafficOriginal + devicesOriginal; - const discount = Math.max(0, originalPrice - finalPrice); - - return { - finalPrice, - originalPrice, - discount, - months, - basePrice, - baseOriginal, - serversPrice, - serversOriginal, - trafficPrice, - trafficOriginal, - devicesPrice, - devicesOriginal, - }; - } - - function updateSubscriptionConfiguratorPricing() { - const data = subscriptionConfiguratorData; - if (!data || !shouldShowSubscriptionConfigurator()) { - return; - } - - const priceElement = document.getElementById('subscriptionConfiguratorPriceCurrent'); - const originalElement = document.getElementById('subscriptionConfiguratorPriceOriginal'); - const discountElement = document.getElementById('subscriptionConfiguratorPriceDiscount'); - const discountBadge = document.getElementById('subscriptionConfiguratorDiscountBadge'); - if (!priceElement) { - return; - } - - const result = calculateSubscriptionConfiguratorPrice(data, subscriptionConfiguratorSelections); - const currency = data.currency || userData?.balance_currency || 'RUB'; - priceElement.textContent = formatPriceFromKopeks(result.finalPrice, currency); - - if (originalElement) { - if (result.originalPrice > result.finalPrice) { - originalElement.textContent = formatPriceFromKopeks(result.originalPrice, currency); - originalElement.classList.remove('hidden'); - } else { - originalElement.classList.add('hidden'); - } - } - - if (discountElement) { - if (result.discount > 0) { - const key = 'subscription_configurator.price.discount'; - const template = t(key); - const message = template && template !== key - ? template.replace('{value}', formatPriceFromKopeks(result.discount, currency)) - : `Discount: ${formatPriceFromKopeks(result.discount, currency)}`; - discountElement.textContent = message; - discountElement.classList.remove('hidden'); - } else { - discountElement.classList.add('hidden'); - discountElement.textContent = ''; - } - } - - if (discountBadge) { - const percent = result.originalPrice > 0 - ? Math.round((1 - result.finalPrice / result.originalPrice) * 100) - : 0; - if (percent > 0) { - const key = 'subscription_configurator.discount.badge'; - const template = t(key); - discountBadge.textContent = template && template !== key - ? template.replace('{percent}', String(percent)) - : `-${percent}%`; - discountBadge.classList.remove('hidden'); - } else { - discountBadge.classList.add('hidden'); - discountBadge.textContent = ''; - } - } - - const balanceWarning = document.getElementById('subscriptionConfiguratorBalanceWarning'); - const balanceText = document.getElementById('subscriptionConfiguratorBalanceText'); - if (balanceWarning && balanceText) { - const balanceKopeks = coercePositiveInt(userData?.balance_kopeks, 0); - if (balanceKopeks < result.finalPrice) { - const missing = result.finalPrice - balanceKopeks; - const key = 'subscription_configurator.balance.insufficient'; - const template = t(key); - const message = template && template !== key - ? template.replace('{amount}', formatPriceFromKopeks(missing, currency)) - : `Not enough balance. Missing ${formatPriceFromKopeks(missing, currency)}.`; - balanceText.textContent = message; - balanceWarning.classList.remove('hidden'); - } else { - balanceWarning.classList.add('hidden'); - balanceText.textContent = ''; - } - } - - const button = document.getElementById('subscriptionConfiguratorPurchaseButton'); - if (button) { - button.disabled = subscriptionConfiguratorLoading; - const key = result.finalPrice <= 0 - ? 'subscription_configurator.action.activate' - : 'subscription_configurator.action.purchase'; - const label = t(key); - button.textContent = label && label !== key - ? label - : (result.finalPrice <= 0 ? 'Activate' : 'Buy subscription'); - } - } - - function showSubscriptionConfiguratorStatus(message, variant = 'info') { - const element = document.getElementById('subscriptionConfiguratorStatus'); - if (!element) { - return; - } - - if (!message) { - element.textContent = ''; - element.classList.add('hidden'); - element.classList.remove('error', 'success', 'info'); - return; - } - - element.textContent = message; - element.classList.remove('hidden', 'error', 'success', 'info'); - element.classList.add(variant); - } - - function resolveSubscriptionConfiguratorError(payload, status) { - if (status === 401) { - const key = 'subscription_configurator.error.unauthorized'; - const message = t(key); - return message && message !== key - ? message - : 'Authorization error. Please reopen the mini app.'; - } - if (status === 402) { - const key = 'subscription_configurator.error.insufficient_funds'; - const message = t(key); - return message && message !== key - ? message - : 'Not enough balance to complete the purchase.'; - } - if (!payload || typeof payload !== 'object') { - const key = 'subscription_configurator.error.generic'; - const message = t(key); - return message && message !== key - ? message - : 'Failed to complete the purchase. Please try again later.'; - } - if (typeof payload.detail === 'string') { - return payload.detail; - } - if (payload.detail && typeof payload.detail === 'object') { - if (typeof payload.detail.message === 'string') { - return payload.detail.message; - } - if (typeof payload.detail.error === 'string') { - return payload.detail.error; - } - } - if (typeof payload.message === 'string') { - return payload.message; - } - if (typeof payload.error === 'string') { - return payload.error; - } - const key = 'subscription_configurator.error.generic'; - const message = t(key); - return message && message !== key - ? message - : 'Failed to complete the purchase. Please try again later.'; - } - - async function submitSubscriptionConfiguratorPurchase() { - if (subscriptionConfiguratorLoading) { - return; - } - - if (!subscriptionConfiguratorData || !shouldShowSubscriptionConfigurator()) { - return; - } - - const initData = tg.initData || ''; - if (!initData) { - const key = 'subscription_configurator.error.unauthorized'; - const message = t(key); - showSubscriptionConfiguratorStatus( - message && message !== key ? message : 'Authorization error. Please reopen the mini app.', - 'error' - ); - return; - } - - const payload = { - initData, - periodId: subscriptionConfiguratorSelections.periodId, - trafficId: subscriptionConfiguratorSelections.trafficId, - servers: Array.from(subscriptionConfiguratorSelections.servers || []), - devices: subscriptionConfiguratorSelections.devices, - }; - - const period = getConfiguratorSelectedPeriod(subscriptionConfiguratorData); - if (period?.days) { - payload.periodDays = period.days; - } - if (period?.months) { - payload.periodMonths = period.months; - } - - const traffic = getConfiguratorSelectedTraffic(subscriptionConfiguratorData); - if (traffic && Number.isFinite(traffic.valueGb)) { - payload.trafficGb = traffic.valueGb; - } - - subscriptionConfiguratorLoading = true; - updateSubscriptionConfiguratorPricing(); - const statusKey = 'subscription_configurator.status.loading'; - const statusMessage = t(statusKey); - showSubscriptionConfiguratorStatus( - statusMessage && statusMessage !== statusKey - ? statusMessage - : 'Processing purchase...', - 'info' - ); - - try { - const response = await fetch('/miniapp/subscription/purchase', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - const result = await response.json().catch(() => ({})); - - if (!response.ok) { - const message = resolveSubscriptionConfiguratorError(result, response.status); - showSubscriptionConfiguratorStatus(message, 'error'); - if (response.status === 402) { - const warning = document.getElementById('subscriptionConfiguratorBalanceWarning'); - if (warning) { - warning.classList.remove('hidden'); - } - } - return; - } - - const key = 'subscription_configurator.success'; - const defaultMessage = t(key); - const successMessage = typeof result?.message === 'string' - ? result.message - : (defaultMessage && defaultMessage !== key - ? defaultMessage - : 'Subscription purchased successfully!'); - showSubscriptionConfiguratorStatus(successMessage, 'success'); - - await refreshSubscriptionData({ silent: true }); - } catch (error) { - console.error('Subscription purchase failed:', error); - const key = 'subscription_configurator.error.generic'; - const message = t(key); - showSubscriptionConfiguratorStatus( - message && message !== key - ? message - : 'Failed to complete the purchase. Please try again later.', - 'error' - ); - } finally { - subscriptionConfiguratorLoading = false; - updateSubscriptionConfiguratorPricing(); - } - } - function extractSettingsError(payload, status) { if (status === 401) { return t('subscription_settings.error.unauthorized'); @@ -12914,14 +10824,6 @@ openExternalLink(link, { openInMiniApp: true }); }); - document.getElementById('subscriptionConfiguratorTopupButton')?.addEventListener('click', () => { - openTopupModal(); - }); - - document.getElementById('subscriptionConfiguratorPurchaseButton')?.addEventListener('click', () => { - submitSubscriptionConfiguratorPurchase(); - }); - initializePromoCodeForm(); init();