diff --git a/miniapp/index.html b/miniapp/index.html
index 5058bea8..b1f33202 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -705,368 +705,6 @@
color: #fff;
}
- .purchase-card .card-header {
- cursor: default;
- }
-
- .purchase-card-subtitle {
- font-size: 13px;
- color: var(--text-secondary);
- margin-top: 6px;
- line-height: 1.5;
- }
-
- .purchase-content {
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding-top: 12px;
- }
-
- .purchase-loading {
- padding: 28px 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 12px;
- text-align: center;
- }
-
- .purchase-loading .spinner {
- width: 42px;
- height: 42px;
- }
-
- .purchase-loading-text {
- font-size: 14px;
- color: var(--text-secondary);
- font-weight: 600;
- }
-
- .purchase-error {
- padding: 16px;
- border-radius: var(--radius);
- background: rgba(var(--danger-rgb), 0.08);
- border: 1px solid rgba(var(--danger-rgb), 0.2);
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .purchase-error-text {
- font-size: 14px;
- color: var(--text-primary);
- line-height: 1.5;
- }
-
- .purchase-retry-button {
- align-self: flex-start;
- padding: 10px 16px;
- border-radius: var(--radius);
- border: none;
- background: var(--primary);
- color: #fff;
- font-weight: 600;
- cursor: pointer;
- box-shadow: var(--shadow-sm);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
- }
-
- .purchase-retry-button:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
- }
-
- .purchase-section {
- display: flex;
- flex-direction: column;
- gap: 14px;
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 18px;
- }
-
- .purchase-section:last-child {
- border-bottom: none;
- padding-bottom: 0;
- }
-
- .purchase-section-header {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- align-items: flex-start;
- }
-
- .purchase-section-title {
- font-size: 16px;
- font-weight: 700;
- color: var(--text-primary);
- }
-
- .purchase-section-description {
- font-size: 13px;
- color: var(--text-secondary);
- margin-top: 4px;
- line-height: 1.5;
- }
-
- .purchase-section-meta {
- font-size: 12px;
- color: var(--text-secondary);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- }
-
- .purchase-options-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- }
-
- .purchase-option {
- flex: 1 1 calc(50% - 12px);
- min-width: 140px;
- padding: 14px;
- border-radius: var(--radius);
- border: 1px solid var(--border-color);
- background: var(--bg-primary);
- color: var(--text-primary);
- text-align: left;
- cursor: pointer;
- display: flex;
- flex-direction: column;
- gap: 6px;
- transition: all 0.2s ease;
- }
-
- .purchase-option:hover {
- border-color: rgba(var(--primary-rgb), 0.6);
- box-shadow: var(--shadow-sm);
- }
-
- .purchase-option.active {
- border-color: transparent;
- background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.16), rgba(var(--primary-rgb), 0.08));
- box-shadow: var(--shadow-md);
- }
-
- .purchase-option-title {
- font-size: 15px;
- font-weight: 700;
- }
-
- .purchase-option-price {
- display: flex;
- align-items: baseline;
- gap: 6px;
- font-weight: 700;
- }
-
- .purchase-option-price s {
- font-size: 12px;
- color: var(--text-secondary);
- opacity: 0.7;
- }
-
- .purchase-option-hint {
- font-size: 12px;
- color: var(--text-secondary);
- line-height: 1.4;
- }
-
- .purchase-option-badges {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- }
-
- .purchase-option-badge {
- display: inline-flex;
- align-items: center;
- padding: 4px 8px;
- border-radius: 999px;
- font-size: 11px;
- font-weight: 600;
- background: rgba(var(--primary-rgb), 0.12);
- color: var(--primary);
- text-transform: uppercase;
- letter-spacing: 0.04em;
- }
-
- .purchase-meta-note {
- font-size: 12px;
- color: var(--text-secondary);
- line-height: 1.4;
- }
-
- .purchase-servers-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .purchase-chip {
- border-radius: 999px;
- padding: 8px 14px;
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- color: var(--text-primary);
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s ease;
- }
-
- .purchase-chip:hover {
- border-color: rgba(var(--primary-rgb), 0.6);
- box-shadow: var(--shadow-sm);
- }
-
- .purchase-chip.active {
- background: var(--primary);
- border-color: transparent;
- color: #fff;
- box-shadow: var(--shadow-md);
- }
-
- .purchase-stepper {
- display: flex;
- align-items: center;
- gap: 12px;
- }
-
- .purchase-stepper button {
- width: 40px;
- height: 40px;
- 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;
- }
-
- .purchase-stepper button:hover:not(:disabled) {
- border-color: rgba(var(--primary-rgb), 0.6);
- box-shadow: var(--shadow-sm);
- }
-
- .purchase-stepper button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- .purchase-stepper-value {
- font-size: 20px;
- font-weight: 700;
- }
-
- .purchase-summary {
- border-radius: var(--radius-lg);
- background: rgba(var(--primary-rgb), 0.08);
- padding: 18px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .purchase-summary-header {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- gap: 12px;
- }
-
- .purchase-summary-total {
- font-size: 22px;
- font-weight: 700;
- color: var(--text-primary);
- }
-
- .purchase-summary-original {
- font-size: 14px;
- color: var(--text-secondary);
- text-decoration: line-through;
- }
-
- .purchase-summary-discount {
- font-size: 13px;
- color: var(--success);
- font-weight: 600;
- }
-
- .purchase-summary-line {
- display: flex;
- justify-content: space-between;
- gap: 8px;
- font-size: 13px;
- color: var(--text-secondary);
- }
-
- .purchase-summary-line strong {
- color: var(--text-primary);
- }
-
- .purchase-discount-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- }
-
- .purchase-discount-tag {
- font-size: 12px;
- font-weight: 600;
- color: var(--primary);
- background: rgba(var(--primary-rgb), 0.12);
- border-radius: 999px;
- padding: 6px 10px;
- }
-
- .purchase-actions {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
-
- .purchase-actions .btn {
- width: 100%;
- }
-
- .purchase-validation {
- font-size: 13px;
- color: var(--danger);
- line-height: 1.5;
- }
-
- .purchase-empty {
- font-size: 13px;
- color: var(--text-secondary);
- }
-
- :root[data-theme="dark"] .purchase-option,
- :root[data-theme="dark"] .purchase-chip {
- background: rgba(15, 23, 42, 0.8);
- border-color: rgba(148, 163, 184, 0.25);
- }
-
- :root[data-theme="dark"] .purchase-option.active,
- :root[data-theme="dark"] .purchase-chip.active {
- background: rgba(var(--primary-rgb), 0.25);
- color: #fff;
- }
-
- :root[data-theme="dark"] .purchase-summary {
- background: rgba(var(--primary-rgb), 0.16);
- }
-
- :root[data-theme="dark"] .purchase-error {
- background: rgba(var(--danger-rgb), 0.18);
- border-color: rgba(var(--danger-rgb), 0.35);
- }
-
.promo-offers {
display: flex;
flex-direction: column;
@@ -3707,87 +3345,6 @@
-
@@ -4420,40 +3977,6 @@
'topup.status.retry': 'Try again',
'topup.done': 'Done',
'button.buy_subscription': 'Buy Subscription',
- 'purchase.title': 'Configure your subscription',
- 'purchase.subtitle': 'Choose the plan parameters and activate a subscription instantly.',
- 'purchase.loading': 'Loading purchase options…',
- 'purchase.error.generic': 'Unable to load purchase options. Please try again later.',
- 'purchase.action.retry': 'Try again',
- 'purchase.action.buy': 'Buy subscription',
- 'purchase.action.topup': 'Top up balance',
- 'purchase.period.title': 'Subscription period',
- 'purchase.period.subtitle': 'Select how long you need VPN access.',
- 'purchase.traffic.title': 'Traffic',
- 'purchase.traffic.subtitle': 'Choose a monthly traffic package.',
- 'purchase.traffic.fixed': 'Monthly traffic: {amount}',
- 'purchase.servers.title': 'Servers',
- 'purchase.servers.subtitle': 'Select available connection regions.',
- 'purchase.servers.single': 'Server: {name}',
- 'purchase.servers.meta': '{selected} of {max} selected',
- 'purchase.servers.meta_unlimited': '{selected} selected',
- 'purchase.devices.title': 'Devices',
- 'purchase.devices.subtitle': 'Number of simultaneously connected devices.',
- 'purchase.devices.meta': 'Up to {max} devices',
- 'purchase.summary.total': 'Total',
- 'purchase.summary.original': 'Without discounts',
- 'purchase.summary.discount': 'You save {amount}',
- 'purchase.summary.balance': 'Balance',
- 'purchase.summary.balance_after': 'After purchase',
- 'purchase.summary.missing': 'Top up needed',
- 'purchase.validation.period': 'Select a subscription period.',
- 'purchase.validation.traffic': 'Select a traffic package.',
- 'purchase.validation.servers_min': 'Select at least {min} server(s).',
- 'purchase.validation.devices': 'Select the number of devices.',
- 'purchase.discount.promo_group': 'Promo group discount',
- 'purchase.discount.promo_offer': 'Promo offer',
- 'purchase.discount.active_offer': 'Active discount: {percent}%',
- 'purchase.empty': 'No purchase options are available yet.',
'card.balance.title': 'Balance',
'subscription_settings.title': 'Subscription settings',
'subscription_settings.summary.servers': 'Servers: {count}',
@@ -4735,40 +4258,6 @@
'topup.status.retry': 'Повторить попытку',
'topup.done': 'Готово',
'button.buy_subscription': 'Купить подписку',
- 'purchase.title': 'Оформление подписки',
- 'purchase.subtitle': 'Выберите параметры тарифа и подключите подписку в пару кликов.',
- 'purchase.loading': 'Загружаем варианты подписки…',
- 'purchase.error.generic': 'Не удалось загрузить варианты подписки. Попробуйте позже.',
- 'purchase.action.retry': 'Попробовать снова',
- 'purchase.action.buy': 'Оформить подписку',
- 'purchase.action.topup': 'Пополнить баланс',
- 'purchase.period.title': 'Период подписки',
- 'purchase.period.subtitle': 'Выберите, на какой срок нужна подписка.',
- 'purchase.traffic.title': 'Трафик',
- 'purchase.traffic.subtitle': 'Выберите ежемесячный пакет трафика.',
- 'purchase.traffic.fixed': 'Трафик в месяц: {amount}',
- 'purchase.servers.title': 'Серверы',
- 'purchase.servers.subtitle': 'Выберите доступные регионы подключения.',
- 'purchase.servers.single': 'Сервер: {name}',
- 'purchase.servers.meta': 'Выбрано: {selected} из {max}',
- 'purchase.servers.meta_unlimited': 'Выбрано: {selected}',
- 'purchase.devices.title': 'Устройства',
- 'purchase.devices.subtitle': 'Сколько устройств подключится одновременно.',
- 'purchase.devices.meta': 'До {max} устройств',
- 'purchase.summary.total': 'Итого',
- 'purchase.summary.original': 'Без скидок',
- 'purchase.summary.discount': 'Вы экономите {amount}',
- 'purchase.summary.balance': 'Баланс',
- 'purchase.summary.balance_after': 'После оплаты',
- 'purchase.summary.missing': 'Не хватает',
- 'purchase.validation.period': 'Выберите период подписки.',
- 'purchase.validation.traffic': 'Выберите пакет трафика.',
- 'purchase.validation.servers_min': 'Выберите минимум {min} сервер(а).',
- 'purchase.validation.devices': 'Укажите количество устройств.',
- 'purchase.discount.promo_group': 'Скидка промогруппы',
- 'purchase.discount.promo_offer': 'Промо-предложение',
- 'purchase.discount.active_offer': 'Активная скидка: {percent}%',
- 'purchase.empty': 'Варианты для покупки пока недоступны.',
'card.balance.title': 'Баланс',
'subscription_settings.title': 'Настройка подписки',
'subscription_settings.summary.servers': 'Серверов: {count}',
@@ -5115,18 +4604,6 @@
devices: null,
};
- let subscriptionPurchaseConfig = null;
- let subscriptionPurchasePromise = null;
- let subscriptionPurchaseError = null;
- const subscriptionPurchaseSelections = {
- period: null,
- traffic: null,
- servers: new Set(),
- devices: null,
- };
- let subscriptionPurchaseSubmitting = false;
- let subscriptionPurchaseLastTotals = null;
-
const PAYMENT_STATUS_INITIAL_DELAY_MS = 2000;
const PAYMENT_STATUS_POLL_INTERVAL_MS = 5000;
const PAYMENT_STATUS_TIMEOUT_MS = 180000;
@@ -5750,362 +5227,6 @@
return trimmed.length ? trimmed : null;
}
- function getPurchaseRoot() {
- if (!subscriptionPurchaseConfig || typeof subscriptionPurchaseConfig !== 'object') {
- return null;
- }
- if (subscriptionPurchaseConfig.config && typeof subscriptionPurchaseConfig.config === 'object') {
- return subscriptionPurchaseConfig.config;
- }
- return subscriptionPurchaseConfig;
- }
-
- function getPurchaseCurrency() {
- const root = getPurchaseRoot();
- const raw = root?.currency
- || root?.pricing?.currency
- || root?.balance_currency
- || userData?.balance_currency
- || 'RUB';
- return String(raw || 'RUB').toUpperCase();
- }
-
- function getFirstDefined(source, fields) {
- if (!source) {
- return undefined;
- }
- for (const field of fields) {
- if (Object.prototype.hasOwnProperty.call(source, field)) {
- const value = source[field];
- if (value !== undefined && value !== null && value !== '') {
- return value;
- }
- }
- }
- return undefined;
- }
-
- function getOptionLabel(option, fallback) {
- const raw = getFirstDefined(option, ['label', 'name', 'title', 'display_name', 'displayName']);
- if (typeof raw === 'string' && raw.trim()) {
- return raw.trim();
- }
- if (fallback) {
- return fallback;
- }
- const value = getFirstDefined(option, ['value', 'id', 'key', 'uuid']);
- return value !== undefined ? String(value) : '';
- }
-
- function getOptionDescription(option) {
- const raw = getFirstDefined(option, ['description', 'subtitle', 'hint', 'note']);
- if (typeof raw === 'string' && raw.trim()) {
- return raw.trim();
- }
- return '';
- }
-
- function getOptionPriceKopeks(option) {
- const candidates = [
- 'price_kopeks',
- 'priceKopeks',
- 'price_kop',
- 'price',
- 'cost_kopeks',
- 'costKopeks',
- 'amount_kopeks',
- 'amountKopeks',
- 'total_kopeks',
- 'totalKopeks',
- ];
- const raw = getFirstDefined(option, candidates);
- const value = coercePositiveInt(raw, null);
- return value ?? 0;
- }
-
- function getOptionOriginalPriceKopeks(option) {
- const candidates = [
- 'original_price_kopeks',
- 'originalPriceKopeks',
- 'base_price_kopeks',
- 'basePriceKopeks',
- 'full_price_kopeks',
- 'fullPriceKopeks',
- 'original_price',
- 'compare_at_price_kopeks',
- 'compareAtPriceKopeks',
- ];
- const raw = getFirstDefined(option, candidates);
- const value = coercePositiveInt(raw, null);
- return value ?? null;
- }
-
- function getOptionDiscountPercent(option) {
- const raw = getFirstDefined(option, ['discount_percent', 'discountPercent', 'discount']);
- const value = coercePositiveInt(raw, null);
- return value ?? 0;
- }
-
- function isOptionPreselected(option) {
- const raw = getFirstDefined(option, ['is_default', 'isDefault', 'default', 'selected', 'is_selected', 'isSelected', 'current', 'active', 'included']);
- if (typeof raw === 'boolean') {
- return raw;
- }
- if (typeof raw === 'number') {
- return raw > 0;
- }
- if (typeof raw === 'string') {
- return ['true', '1', 'yes', 'selected', 'included'].includes(raw.toLowerCase());
- }
- return false;
- }
-
- function getPurchasePeriods() {
- const root = getPurchaseRoot();
- if (!root) {
- return [];
- }
- const candidates = [root.periods, root.available_periods, root.period_options, root.subscription_periods];
- for (const candidate of candidates) {
- if (Array.isArray(candidate) && candidate.length) {
- return candidate;
- }
- }
- return [];
- }
-
- function getPurchasePeriodId(period) {
- const raw = getFirstDefined(period, ['id', 'key', 'uuid', 'value', 'period_id', 'periodId']);
- if (raw !== undefined) {
- return String(raw);
- }
- const days = getFirstDefined(period, ['days', 'period_days', 'periodDays', 'duration_days']);
- if (days !== undefined) {
- return `days-${days}`;
- }
- return JSON.stringify(period || {});
- }
-
- function getPurchasePeriodLabel(period) {
- const label = getOptionLabel(period);
- if (label) {
- return label;
- }
- const days = getFirstDefined(period, ['days', 'period_days', 'periodDays', 'duration_days']);
- if (days !== undefined) {
- const numeric = Number(days);
- if (Number.isFinite(numeric) && numeric > 0) {
- const suffix = numeric === 1 ? 'day' : 'days';
- return `${numeric} ${suffix}`;
- }
- }
- return t('purchase.period.title');
- }
-
- function getPurchaseTrafficConfig() {
- const root = getPurchaseRoot();
- if (!root) {
- return null;
- }
- const traffic = root.traffic || root.traffic_options || root.trafficPackages;
- if (traffic && typeof traffic === 'object') {
- return traffic;
- }
- return null;
- }
-
- function getPurchaseTrafficOptions() {
- const traffic = getPurchaseTrafficConfig();
- if (!traffic) {
- return [];
- }
- const options = traffic.options || traffic.available || traffic.packages || traffic.choices;
- return Array.isArray(options) ? options : [];
- }
-
- function getTrafficId(option) {
- const raw = getFirstDefined(option, ['id', 'key', 'uuid', 'value']);
- if (raw !== undefined) {
- return String(raw);
- }
- const gb = getFirstDefined(option, ['gb', 'traffic_gb', 'trafficGb', 'limit_gb', 'limitGb']);
- if (gb !== undefined) {
- return `gb-${gb}`;
- }
- return JSON.stringify(option || {});
- }
-
- function getTrafficLabel(option) {
- const label = getOptionLabel(option);
- if (label) {
- return label;
- }
- const gb = getFirstDefined(option, ['gb', 'traffic_gb', 'trafficGb', 'limit_gb', 'limitGb']);
- if (gb !== undefined) {
- const numeric = Number(gb);
- if (Number.isFinite(numeric)) {
- return formatTraffic(Math.max(numeric, 0));
- }
- }
- return '';
- }
-
- function getPurchaseServersConfig() {
- const root = getPurchaseRoot();
- if (!root) {
- return null;
- }
- const servers = root.servers || root.server_options || root.squads;
- if (servers && typeof servers === 'object') {
- return servers;
- }
- return null;
- }
-
- function getPurchaseServerOptions() {
- const config = getPurchaseServersConfig();
- if (!config) {
- return [];
- }
- const options = config.available || config.options || config.items || config.choices;
- return Array.isArray(options) ? options : [];
- }
-
- function getServerId(option) {
- const raw = getFirstDefined(option, ['uuid', 'id', 'key', 'value', 'code']);
- return raw !== undefined ? String(raw) : null;
- }
-
- function getServerLabel(option) {
- const label = getOptionLabel(option);
- if (label) {
- return label;
- }
- const code = getFirstDefined(option, ['country', 'code']);
- if (code !== undefined) {
- return String(code);
- }
- return t('values.not_available');
- }
-
- function getPurchaseServersMin() {
- const config = getPurchaseServersConfig();
- if (!config) {
- return 0;
- }
- const raw = getFirstDefined(config, ['min', 'minimum', 'min_required', 'minRequired', 'min_select']);
- const value = coercePositiveInt(raw, null);
- return value ?? 0;
- }
-
- function getPurchaseServersMax() {
- const config = getPurchaseServersConfig();
- if (!config) {
- return 0;
- }
- const raw = getFirstDefined(config, ['max', 'maximum', 'max_allowed', 'maxAllowed', 'limit']);
- const value = coercePositiveInt(raw, null);
- return value ?? 0;
- }
-
- function getPurchaseDevicesConfig() {
- const root = getPurchaseRoot();
- if (!root) {
- return null;
- }
- const devices = root.devices || root.device_options || root.deviceLimit;
- if (!devices) {
- return null;
- }
- if (Array.isArray(devices)) {
- return { options: devices };
- }
- if (typeof devices === 'object') {
- return devices;
- }
- return null;
- }
-
- function getPurchaseDeviceOptions() {
- const devices = getPurchaseDevicesConfig();
- if (!devices) {
- return [];
- }
- const options = devices.options || devices.available || devices.choices;
- return Array.isArray(options) ? options : [];
- }
-
- function getDeviceValue(option) {
- const raw = getFirstDefined(option, ['value', 'devices', 'count', 'device_limit', 'deviceLimit']);
- const value = coercePositiveInt(raw, null);
- return value ?? null;
- }
-
- function getPurchaseDevicesMin() {
- const devices = getPurchaseDevicesConfig();
- if (!devices) {
- return 0;
- }
- const raw = getFirstDefined(devices, ['min', 'minimum', 'min_allowed', 'minAllowed', 'min_devices']);
- const value = coercePositiveInt(raw, null);
- if (value !== null) {
- return value;
- }
- const values = getPurchaseDeviceOptions().map(getDeviceValue).filter(v => v !== null);
- return values.length ? Math.min(...values) : 0;
- }
-
- function getPurchaseDevicesMax() {
- const devices = getPurchaseDevicesConfig();
- if (!devices) {
- return 0;
- }
- const raw = getFirstDefined(devices, ['max', 'maximum', 'max_allowed', 'maxAllowed', 'max_devices']);
- const value = coercePositiveInt(raw, null);
- if (value !== null) {
- return value;
- }
- const values = getPurchaseDeviceOptions().map(getDeviceValue).filter(v => v !== null);
- return values.length ? Math.max(...values) : 0;
- }
-
- function getPurchaseDevicesStep() {
- const devices = getPurchaseDevicesConfig();
- if (!devices) {
- return 1;
- }
- const raw = getFirstDefined(devices, ['step', 'increment']);
- const value = coercePositiveInt(raw, null);
- return value ?? 1;
- }
-
- function getPurchaseBalanceKopeks(root) {
- const candidates = [
- root?.balance_kopeks,
- root?.balanceKopeks,
- root?.balance?.amount_kopeks,
- root?.balance?.kopeks,
- root?.pricing?.balance_kopeks,
- ];
- for (const candidate of candidates) {
- const value = coercePositiveInt(candidate, null);
- if (value !== null) {
- return value;
- }
- }
- const fromUser = coercePositiveInt(userData?.balance_kopeks, null);
- if (fromUser !== null) {
- return fromUser;
- }
- const balanceRubles = typeof userData?.balance_rubles === 'number'
- ? userData.balance_rubles
- : Number.parseFloat(userData?.balance_rubles ?? '0');
- if (Number.isFinite(balanceRubles)) {
- return Math.max(0, Math.round(balanceRubles * 100));
- }
- return 0;
- }
-
function getEffectivePurchaseUrl() {
const candidates = [
currentErrorState?.purchaseUrl,
@@ -6186,7 +5307,6 @@
}
renderApps();
updateActionButtons();
- renderSubscriptionPurchaseConfigurator();
}
function setLanguage(language, options = {}) {
@@ -7145,953 +6265,6 @@
registerPromoOfferTimers(hasContent ? timers : []);
}
- function shouldShowSubscriptionPurchase() {
- if (subscriptionPurchaseError && !subscriptionPurchaseConfig) {
- return true;
- }
- if (!userData || !userData.user) {
- return true;
- }
- const user = userData.user;
- const status = String(user.subscription_actual_status || user.subscription_status || '').toLowerCase();
- const type = String(userData.subscription_type || '').toLowerCase();
- const hasActive = Boolean(user.has_active_subscription);
- if (!hasActive) {
- return true;
- }
- if (type === 'trial' || status.includes('trial')) {
- return true;
- }
- if (['expired', 'disabled'].includes(status)) {
- return true;
- }
- return false;
- }
-
- function initializeSubscriptionPurchaseSelections() {
- const periods = getPurchasePeriods();
- if (periods.length) {
- const defaultPeriod = periods.find(isOptionPreselected) || periods[0];
- subscriptionPurchaseSelections.period = getPurchasePeriodId(defaultPeriod);
- } else {
- subscriptionPurchaseSelections.period = null;
- }
-
- const trafficOptions = getPurchaseTrafficOptions();
- if (trafficOptions.length) {
- const defaultTraffic = trafficOptions.find(isOptionPreselected) || trafficOptions[0];
- subscriptionPurchaseSelections.traffic = getTrafficId(defaultTraffic);
- } else {
- subscriptionPurchaseSelections.traffic = null;
- }
-
- const serverOptions = getPurchaseServerOptions();
- subscriptionPurchaseSelections.servers = new Set();
- if (serverOptions.length) {
- serverOptions.forEach(option => {
- if (option?.is_available === false) {
- return;
- }
- const id = getServerId(option);
- if (!id) {
- return;
- }
- if (isOptionPreselected(option) || serverOptions.length === 1) {
- subscriptionPurchaseSelections.servers.add(id);
- }
- });
-
- const minServers = getPurchaseServersMin();
- if (subscriptionPurchaseSelections.servers.size < minServers) {
- for (const option of serverOptions) {
- if (option?.is_available === false) {
- continue;
- }
- const id = getServerId(option);
- if (!id) {
- continue;
- }
- subscriptionPurchaseSelections.servers.add(id);
- if (subscriptionPurchaseSelections.servers.size >= minServers) {
- break;
- }
- }
- }
- }
-
- const deviceOptions = getPurchaseDeviceOptions();
- if (deviceOptions.length) {
- const defaultDevice = deviceOptions.find(isOptionPreselected) || deviceOptions[0];
- subscriptionPurchaseSelections.devices = getDeviceValue(defaultDevice);
- } else {
- const devicesConfig = getPurchaseDevicesConfig();
- if (devicesConfig) {
- const raw = getFirstDefined(devicesConfig, ['current', 'value', 'default', 'device_limit', 'deviceLimit']);
- let value = coercePositiveInt(raw, null);
- const min = getPurchaseDevicesMin();
- const max = getPurchaseDevicesMax();
- if (value === null) {
- value = min || max || 0;
- }
- if (min && value < min) {
- value = min;
- }
- if (max && max > 0 && value > max) {
- value = max;
- }
- subscriptionPurchaseSelections.devices = value || null;
- } else {
- subscriptionPurchaseSelections.devices = null;
- }
- }
- }
-
- function getSelectedPeriod() {
- const periods = getPurchasePeriods();
- if (!periods.length) {
- return null;
- }
- const selectedId = subscriptionPurchaseSelections.period;
- let period = periods.find(item => getPurchasePeriodId(item) === selectedId);
- if (!period) {
- period = periods[0];
- subscriptionPurchaseSelections.period = getPurchasePeriodId(period);
- }
- return period;
- }
-
- function getSelectedTrafficOption() {
- const options = getPurchaseTrafficOptions();
- if (!options.length) {
- return null;
- }
- const selectedId = subscriptionPurchaseSelections.traffic;
- let option = options.find(item => getTrafficId(item) === selectedId);
- if (!option) {
- option = options[0];
- subscriptionPurchaseSelections.traffic = getTrafficId(option);
- }
- return option;
- }
-
- async function ensureSubscriptionPurchaseData(options = {}) {
- const { force = false } = options;
- if (subscriptionPurchasePromise) {
- return subscriptionPurchasePromise;
- }
- if (subscriptionPurchaseConfig && !force) {
- return subscriptionPurchaseConfig;
- }
-
- const initData = tg.initData || '';
- if (!initData) {
- subscriptionPurchaseError = createError('Authorization', t('purchase.error.generic'));
- renderSubscriptionPurchaseConfigurator();
- return null;
- }
-
- subscriptionPurchasePromise = (async () => {
- try {
- const response = await fetch('/miniapp/subscription/purchase/config', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ initData }),
- });
-
- if (!response.ok) {
- let detail = null;
- try {
- const payload = await response.json();
- detail = payload?.detail || payload?.message || null;
- } catch (parseError) {
- // ignore
- }
- const message = typeof detail === 'string' && detail.trim()
- ? detail.trim()
- : t('purchase.error.generic');
- throw createError('Purchase', message, response.status);
- }
-
- const payload = await response.json().catch(() => ({}));
- subscriptionPurchaseConfig = payload?.config || payload || null;
- subscriptionPurchaseError = null;
- initializeSubscriptionPurchaseSelections();
- } catch (error) {
- subscriptionPurchaseError = error;
- subscriptionPurchaseConfig = null;
- throw error;
- } finally {
- subscriptionPurchasePromise = null;
- renderSubscriptionPurchaseConfigurator();
- }
-
- return subscriptionPurchaseConfig;
- })();
-
- return subscriptionPurchasePromise;
- }
-
- function renderSubscriptionPurchaseConfigurator() {
- const wrapper = document.getElementById('subscriptionPurchaseWrapper');
- if (!wrapper) {
- return;
- }
-
- const shouldShow = shouldShowSubscriptionPurchase();
- wrapper.classList.toggle('hidden', !shouldShow);
- if (!shouldShow) {
- return;
- }
-
- const loadingEl = document.getElementById('subscriptionPurchaseLoading');
- const contentEl = document.getElementById('subscriptionPurchaseContent');
- const errorEl = document.getElementById('subscriptionPurchaseError');
- const errorText = document.getElementById('subscriptionPurchaseErrorText');
-
- if (!subscriptionPurchaseConfig && !subscriptionPurchaseError && !subscriptionPurchasePromise) {
- loadingEl.classList.remove('hidden');
- contentEl.classList.add('hidden');
- errorEl.classList.add('hidden');
- ensureSubscriptionPurchaseData().catch(error => {
- console.warn('Failed to load purchase options:', error);
- });
- return;
- }
-
- const isLoading = Boolean(subscriptionPurchasePromise);
- loadingEl.classList.toggle('hidden', !isLoading);
-
- if (subscriptionPurchaseError && !isLoading) {
- const message = subscriptionPurchaseError?.message || t('purchase.error.generic');
- if (errorText) {
- errorText.textContent = message === 'purchase.error.generic' ? t('purchase.error.generic') : message;
- }
- errorEl.classList.remove('hidden');
- contentEl.classList.add('hidden');
- return;
- }
-
- errorEl.classList.add('hidden');
- contentEl.classList.toggle('hidden', isLoading || !subscriptionPurchaseConfig);
- if (!subscriptionPurchaseConfig || isLoading) {
- return;
- }
-
- if (!(subscriptionPurchaseSelections.servers instanceof Set)) {
- subscriptionPurchaseSelections.servers = new Set(subscriptionPurchaseSelections.servers);
- }
-
- renderSubscriptionPurchaseSections();
- }
-
- function renderSubscriptionPurchaseSections() {
- renderSubscriptionPurchasePeriods();
- renderSubscriptionPurchaseTraffic();
- renderSubscriptionPurchaseServers();
- renderSubscriptionPurchaseDevices();
- renderSubscriptionPurchaseSummary();
- renderSubscriptionPurchaseDiscounts();
- renderSubscriptionPurchaseValidation();
- }
-
- function renderSubscriptionPurchasePeriods() {
- const container = document.getElementById('subscriptionPurchasePeriodOptions');
- const meta = document.getElementById('subscriptionPurchasePeriodMeta');
- if (!container) {
- return;
- }
- container.innerHTML = '';
- if (meta) {
- meta.textContent = '';
- }
-
- const periods = getPurchasePeriods();
- if (!periods.length) {
- container.innerHTML = `
${escapeHtml(t('purchase.empty'))}
`;
- return;
- }
-
- const currency = getPurchaseCurrency();
- const selectedId = subscriptionPurchaseSelections.period;
-
- periods.forEach(period => {
- const periodId = getPurchasePeriodId(period);
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'purchase-option';
- if (periodId === selectedId) {
- button.classList.add('active');
- }
-
- const label = getPurchasePeriodLabel(period);
- const price = getOptionPriceKopeks(period);
- const original = getOptionOriginalPriceKopeks(period);
- const priceLabel = formatPriceFromKopeks(price, currency);
- const originalHtml = Number.isFinite(original) && original !== null && original > price
- ? `
${escapeHtml(formatPriceFromKopeks(original, currency))}`
- : '';
- const description = getOptionDescription(period);
- const discount = getOptionDiscountPercent(period);
-
- button.innerHTML = `
-
${escapeHtml(label)}
-
- ${escapeHtml(priceLabel)}
- ${originalHtml}
-
- ${discount > 0 ? `
-${escapeHtml(String(discount))}%
` : ''}
- ${description ? `
${escapeHtml(description)}
` : ''}
- `;
-
- button.addEventListener('click', () => {
- subscriptionPurchaseSelections.period = periodId;
- renderSubscriptionPurchaseSections();
- });
-
- container.appendChild(button);
- });
- }
-
- function renderSubscriptionPurchaseTraffic() {
- const container = document.getElementById('subscriptionPurchaseTrafficOptions');
- const meta = document.getElementById('subscriptionPurchaseTrafficMeta');
- const note = document.getElementById('subscriptionPurchaseTrafficNote');
- if (!container) {
- return;
- }
- container.innerHTML = '';
- if (meta) {
- meta.textContent = '';
- }
- if (note) {
- note.classList.add('hidden');
- note.textContent = '';
- }
-
- const trafficConfig = getPurchaseTrafficConfig();
- if (!trafficConfig) {
- container.innerHTML = `
${escapeHtml(t('purchase.empty'))}
`;
- return;
- }
-
- const options = getPurchaseTrafficOptions();
- const currency = getPurchaseCurrency();
- if (!options.length) {
- const limitRaw = getFirstDefined(trafficConfig, ['label', 'limit_label', 'limitLabel']);
- let amountLabel = '';
- if (typeof limitRaw === 'string' && limitRaw.trim()) {
- amountLabel = limitRaw.trim();
- } else {
- const limitValue = getFirstDefined(trafficConfig, ['gb', 'traffic_gb', 'trafficGb', 'limit_gb', 'limitGb']);
- if (limitValue !== undefined) {
- const numeric = Number(limitValue);
- if (Number.isFinite(numeric)) {
- amountLabel = formatTraffic(Math.max(numeric, 0));
- }
- }
- }
- if (note && amountLabel) {
- const template = t('purchase.traffic.fixed');
- note.textContent = template === 'purchase.traffic.fixed'
- ? `Monthly traffic: ${amountLabel}`
- : template.replace('{amount}', amountLabel);
- note.classList.remove('hidden');
- }
- return;
- }
-
- const selectedId = subscriptionPurchaseSelections.traffic;
-
- options.forEach(option => {
- const optionId = getTrafficId(option);
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'purchase-option';
- if (optionId === selectedId) {
- button.classList.add('active');
- }
-
- const label = getTrafficLabel(option) || t('purchase.traffic.title');
- const price = getOptionPriceKopeks(option);
- const original = getOptionOriginalPriceKopeks(option);
- const priceLabel = formatPriceFromKopeks(price, currency);
- const originalHtml = Number.isFinite(original) && original !== null && original > price
- ? `
${escapeHtml(formatPriceFromKopeks(original, currency))}`
- : '';
- const description = getOptionDescription(option);
-
- button.innerHTML = `
-
${escapeHtml(label)}
-
- ${escapeHtml(priceLabel)}
- ${originalHtml}
-
- ${description ? `
${escapeHtml(description)}
` : ''}
- `;
-
- button.addEventListener('click', () => {
- subscriptionPurchaseSelections.traffic = optionId;
- renderSubscriptionPurchaseSections();
- });
-
- container.appendChild(button);
- });
- }
-
- function renderSubscriptionPurchaseServers() {
- const container = document.getElementById('subscriptionPurchaseServersChips');
- const meta = document.getElementById('subscriptionPurchaseServersMeta');
- const note = document.getElementById('subscriptionPurchaseServersNote');
- if (!container) {
- return;
- }
- container.innerHTML = '';
- if (meta) {
- meta.textContent = '';
- }
- if (note) {
- note.classList.add('hidden');
- note.textContent = '';
- }
-
- const options = getPurchaseServerOptions().filter(option => option?.is_available !== false);
- const selectable = getFirstDefined(getPurchaseServersConfig(), ['selectable', 'mode']) !== 'fixed';
- if (!options.length) {
- container.innerHTML = `
${escapeHtml(t('purchase.empty'))}
`;
- return;
- }
-
- if (!selectable || options.length === 1) {
- const option = options[0];
- const label = getServerLabel(option);
- if (note) {
- const template = t('purchase.servers.single');
- note.textContent = template === 'purchase.servers.single'
- ? `Server: ${label}`
- : template.replace('{name}', label);
- note.classList.remove('hidden');
- }
- subscriptionPurchaseSelections.servers = new Set();
- const id = getServerId(option);
- if (id) {
- subscriptionPurchaseSelections.servers.add(id);
- }
- return;
- }
-
- const max = getPurchaseServersMax();
- const min = getPurchaseServersMin();
- const currency = getPurchaseCurrency();
-
- options.forEach(option => {
- const id = getServerId(option);
- if (!id) {
- return;
- }
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'purchase-chip';
- const price = getOptionPriceKopeks(option);
- const label = getServerLabel(option);
- const priceLabel = price > 0 ? ` +${formatPriceFromKopeks(price, currency)}` : '';
- button.innerHTML = `
${escapeHtml(label)}${price > 0 ? `
${escapeHtml(priceLabel)}` : ''}`;
-
- if (subscriptionPurchaseSelections.servers.has(id)) {
- button.classList.add('active');
- }
-
- button.addEventListener('click', () => {
- if (subscriptionPurchaseSelections.servers.has(id)) {
- subscriptionPurchaseSelections.servers.delete(id);
- } else {
- if (max > 0 && subscriptionPurchaseSelections.servers.size >= max) {
- return;
- }
- subscriptionPurchaseSelections.servers.add(id);
- }
- renderSubscriptionPurchaseSections();
- });
-
- container.appendChild(button);
- });
-
- if (meta) {
- const selectedCount = subscriptionPurchaseSelections.servers.size;
- if (max > 0) {
- const template = t('purchase.servers.meta');
- meta.textContent = template === 'purchase.servers.meta'
- ? `${selectedCount} of ${max} selected`
- : template.replace('{selected}', String(selectedCount)).replace('{max}', String(max));
- } else {
- const template = t('purchase.servers.meta_unlimited');
- meta.textContent = template === 'purchase.servers.meta_unlimited'
- ? `${selectedCount} selected`
- : template.replace('{selected}', String(selectedCount));
- }
- }
-
- if (note && min > 0 && subscriptionPurchaseSelections.servers.size < min) {
- note.textContent = t('purchase.validation.servers_min').replace('{min}', String(min));
- note.classList.remove('hidden');
- }
- }
-
- function renderSubscriptionPurchaseDevices() {
- const container = document.getElementById('subscriptionPurchaseDevicesOptions');
- const stepper = document.getElementById('subscriptionPurchaseDevicesStepper');
- const valueEl = document.getElementById('subscriptionPurchaseDevicesValue');
- const decreaseBtn = document.getElementById('subscriptionPurchaseDevicesDecrease');
- const increaseBtn = document.getElementById('subscriptionPurchaseDevicesIncrease');
- const meta = document.getElementById('subscriptionPurchaseDevicesMeta');
- const note = document.getElementById('subscriptionPurchaseDevicesNote');
- if (!container) {
- return;
- }
- container.innerHTML = '';
- if (meta) {
- meta.textContent = '';
- }
- if (note) {
- note.textContent = '';
- note.classList.add('hidden');
- }
-
- const options = getPurchaseDeviceOptions();
- const currency = getPurchaseCurrency();
- if (options.length) {
- if (stepper) {
- stepper.classList.add('hidden');
- }
-
- const selectedValue = subscriptionPurchaseSelections.devices;
- options.forEach(option => {
- const value = getDeviceValue(option);
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'purchase-option';
- if (value !== null && value === selectedValue) {
- button.classList.add('active');
- }
-
- const label = value !== null ? formatDeviceCountLabel(value) : getOptionLabel(option);
- const price = getOptionPriceKopeks(option);
- const original = getOptionOriginalPriceKopeks(option);
- const priceLabel = formatPriceFromKopeks(price, currency);
- const originalHtml = Number.isFinite(original) && original !== null && original > price
- ? `
${escapeHtml(formatPriceFromKopeks(original, currency))}`
- : '';
- const description = getOptionDescription(option);
-
- button.innerHTML = `
-
${escapeHtml(label)}
-
- ${escapeHtml(priceLabel)}
- ${originalHtml}
-
- ${description ? `
${escapeHtml(description)}
` : ''}
- `;
-
- button.addEventListener('click', () => {
- subscriptionPurchaseSelections.devices = value;
- renderSubscriptionPurchaseSections();
- });
-
- container.appendChild(button);
- });
-
- const max = getPurchaseDevicesMax();
- if (meta && max) {
- meta.textContent = t('purchase.devices.meta').replace('{max}', String(max));
- }
- return;
- }
-
- const devicesConfig = getPurchaseDevicesConfig();
- if (!devicesConfig || !stepper || !valueEl || !decreaseBtn || !increaseBtn) {
- container.innerHTML = `
${escapeHtml(t('purchase.empty'))}
`;
- return;
- }
-
- stepper.classList.remove('hidden');
- const min = getPurchaseDevicesMin() || 1;
- const max = getPurchaseDevicesMax();
- const step = Math.max(1, getPurchaseDevicesStep());
- let value = subscriptionPurchaseSelections.devices ?? min;
- if (value < min) {
- value = min;
- }
- if (max > 0 && value > max) {
- value = max;
- }
- subscriptionPurchaseSelections.devices = value;
- valueEl.textContent = String(value);
-
- decreaseBtn.disabled = value <= min;
- increaseBtn.disabled = max > 0 ? value >= max : false;
-
- decreaseBtn.onclick = () => {
- const next = subscriptionPurchaseSelections.devices - step;
- if (next < min) {
- return;
- }
- subscriptionPurchaseSelections.devices = next;
- renderSubscriptionPurchaseSections();
- };
-
- increaseBtn.onclick = () => {
- const next = subscriptionPurchaseSelections.devices + step;
- if (max > 0 && next > max) {
- return;
- }
- subscriptionPurchaseSelections.devices = next;
- renderSubscriptionPurchaseSections();
- };
-
- if (meta && max) {
- meta.textContent = t('purchase.devices.meta').replace('{max}', String(max));
- }
- }
-
- function computeSubscriptionPurchaseTotals() {
- const root = getPurchaseRoot();
- if (!root) {
- subscriptionPurchaseLastTotals = null;
- return null;
- }
-
- const currency = getPurchaseCurrency();
- let total = 0;
- let original = 0;
-
- const period = getSelectedPeriod();
- if (period) {
- const price = getOptionPriceKopeks(period);
- const originalPrice = getOptionOriginalPriceKopeks(period);
- total += price;
- original += originalPrice !== null ? Math.max(originalPrice, price) : price;
- }
-
- const traffic = getSelectedTrafficOption();
- if (traffic) {
- const price = getOptionPriceKopeks(traffic);
- const originalPrice = getOptionOriginalPriceKopeks(traffic);
- total += price;
- original += originalPrice !== null ? Math.max(originalPrice, price) : price;
- }
-
- const serverOptions = getPurchaseServerOptions();
- if (serverOptions.length && subscriptionPurchaseSelections.servers instanceof Set) {
- serverOptions.forEach(option => {
- const id = getServerId(option);
- if (!id || !subscriptionPurchaseSelections.servers.has(id)) {
- return;
- }
- const price = getOptionPriceKopeks(option);
- const originalPrice = getOptionOriginalPriceKopeks(option);
- total += price;
- original += originalPrice !== null ? Math.max(originalPrice, price) : price;
- });
- }
-
- const deviceOptions = getPurchaseDeviceOptions();
- if (deviceOptions.length) {
- const selected = deviceOptions.find(option => getDeviceValue(option) === subscriptionPurchaseSelections.devices);
- if (selected) {
- const price = getOptionPriceKopeks(selected);
- const originalPrice = getOptionOriginalPriceKopeks(selected);
- total += price;
- original += originalPrice !== null ? Math.max(originalPrice, price) : price;
- }
- }
-
- if (original < total) {
- original = total;
- }
-
- const pricing = root.pricing || root.summary;
- if (pricing && typeof pricing === 'object') {
- const explicitTotal = coercePositiveInt(getFirstDefined(pricing, ['total_kopeks', 'totalKopeks', 'total_price_kopeks', 'totalPriceKopeks']), null);
- if (explicitTotal !== null) {
- total = explicitTotal;
- }
- const explicitOriginal = coercePositiveInt(getFirstDefined(pricing, ['original_total_kopeks', 'originalTotalKopeks', 'original_price_kopeks', 'base_total_kopeks', 'baseTotalKopeks', 'full_price_kopeks']), null);
- if (explicitOriginal !== null) {
- original = explicitOriginal;
- }
- }
-
- const balance = getPurchaseBalanceKopeks(root);
- const discount = Math.max(0, original - total);
- const missing = Math.max(0, total - balance);
-
- const totals = {
- currency,
- totalKopeks: total,
- originalKopeks: original,
- discountKopeks: discount,
- balanceKopeks: balance,
- missingKopeks: missing,
- };
-
- subscriptionPurchaseLastTotals = totals;
- return totals;
- }
-
- function renderSubscriptionPurchaseSummary() {
- const container = document.getElementById('subscriptionPurchaseSummary');
- if (!container) {
- return;
- }
-
- const totals = computeSubscriptionPurchaseTotals();
- if (!totals) {
- container.innerHTML = `
${escapeHtml(t('purchase.empty'))}
`;
- updateSubscriptionPurchaseButtons();
- return;
- }
-
- const totalLabel = formatPriceFromKopeks(totals.totalKopeks, totals.currency);
- const originalLabel = totals.originalKopeks > totals.totalKopeks
- ? formatPriceFromKopeks(totals.originalKopeks, totals.currency)
- : null;
- const discountLabel = totals.discountKopeks > 0
- ? t('purchase.summary.discount').replace('{amount}', formatPriceFromKopeks(totals.discountKopeks, totals.currency))
- : '';
- const balanceLabel = formatPriceFromKopeks(totals.balanceKopeks, totals.currency);
- const afterPurchaseLabel = formatPriceFromKopeks(Math.max(totals.balanceKopeks - totals.totalKopeks, 0), totals.currency);
- const missingLabel = totals.missingKopeks > 0
- ? formatPriceFromKopeks(totals.missingKopeks, totals.currency)
- : null;
-
- container.innerHTML = `
-
- ${discountLabel ? `
${escapeHtml(discountLabel)}
` : ''}
-
- ${escapeHtml(t('purchase.summary.balance'))}
- ${escapeHtml(balanceLabel)}
-
-
- ${escapeHtml(t('purchase.summary.balance_after'))}
- ${escapeHtml(afterPurchaseLabel)}
-
- ${missingLabel ? `
${escapeHtml(t('purchase.summary.missing'))}${escapeHtml(missingLabel)}
` : ''}
- `;
-
- updateSubscriptionPurchaseButtons();
- }
-
- function collectPurchaseDiscountTags() {
- const tags = [];
- const root = getPurchaseRoot();
- const promoGroup = root?.promo_group || userData?.promo_group;
- if (promoGroup) {
- const values = [
- promoGroup.server_discount_percent,
- promoGroup.traffic_discount_percent,
- promoGroup.device_discount_percent,
- promoGroup.discount_percent,
- ].map(value => coercePositiveInt(value, null)).filter(value => value && value > 0);
- if (values.length) {
- const percent = Math.max(...values);
- tags.push(`${t('purchase.discount.promo_group')}: -${percent}%`);
- }
- }
-
- const activeOffer = (userData?.promo_offers || []).find(offer => {
- if (String(offer?.status || '').toLowerCase() !== 'active') {
- return false;
- }
- const percent = coercePositiveInt(offer?.discount_percent, null);
- return percent && percent > 0;
- });
- if (activeOffer) {
- const percent = coercePositiveInt(activeOffer.discount_percent, null);
- if (percent) {
- tags.push(`${t('purchase.discount.promo_offer')}: -${percent}%`);
- }
- } else {
- const percent = coercePositiveInt(getFirstDefined(root, ['active_discount_percent', 'discount_percent']), null);
- if (percent) {
- tags.push(t('purchase.discount.active_offer').replace('{percent}', String(percent)));
- }
- }
-
- return tags;
- }
-
- function renderSubscriptionPurchaseDiscounts() {
- const container = document.getElementById('subscriptionPurchaseDiscounts');
- if (!container) {
- return;
- }
-
- const tags = collectPurchaseDiscountTags();
- if (!tags.length) {
- container.classList.add('hidden');
- container.innerHTML = '';
- return;
- }
-
- container.classList.remove('hidden');
- container.innerHTML = tags
- .map(tag => `
${escapeHtml(tag)}`)
- .join('');
- }
-
- function validateSubscriptionPurchaseSelections() {
- const errors = [];
- const periods = getPurchasePeriods();
- if (periods.length && !getSelectedPeriod()) {
- errors.push(t('purchase.validation.period'));
- }
-
- const trafficOptions = getPurchaseTrafficOptions();
- if (trafficOptions.length && !getSelectedTrafficOption()) {
- errors.push(t('purchase.validation.traffic'));
- }
-
- const serverOptions = getPurchaseServerOptions().filter(option => option?.is_available !== false);
- const minServers = getPurchaseServersMin();
- if (serverOptions.length && minServers > 0 && subscriptionPurchaseSelections.servers.size < minServers) {
- errors.push(t('purchase.validation.servers_min').replace('{min}', String(minServers)));
- }
-
- const devicesConfig = getPurchaseDevicesConfig();
- if ((devicesConfig || getPurchaseDeviceOptions().length) && subscriptionPurchaseSelections.devices == null) {
- errors.push(t('purchase.validation.devices'));
- }
-
- return errors;
- }
-
- function renderSubscriptionPurchaseValidation() {
- const container = document.getElementById('subscriptionPurchaseValidation');
- if (!container) {
- updateSubscriptionPurchaseButtons();
- return;
- }
-
- const errors = validateSubscriptionPurchaseSelections();
- if (!errors.length) {
- container.classList.add('hidden');
- container.innerHTML = '';
- } else {
- container.classList.remove('hidden');
- container.innerHTML = errors.map(message => `
${escapeHtml(message)}
`).join('');
- }
-
- updateSubscriptionPurchaseButtons(errors);
- }
-
- function updateSubscriptionPurchaseButtons(validationErrors) {
- const submitBtn = document.getElementById('subscriptionPurchaseSubmit');
- const topupBtn = document.getElementById('subscriptionPurchaseTopup');
- const errors = validationErrors || validateSubscriptionPurchaseSelections();
- const totals = subscriptionPurchaseLastTotals;
-
- if (submitBtn) {
- const disabled = subscriptionPurchaseSubmitting
- || !totals
- || totals.totalKopeks <= 0
- || errors.length > 0;
- submitBtn.disabled = disabled;
- submitBtn.textContent = subscriptionPurchaseSubmitting
- ? (t('subscription_settings.pending_action') || 'Saving…')
- : t('purchase.action.buy');
- }
-
- if (topupBtn) {
- const showTopup = Boolean(totals && totals.missingKopeks > 0);
- topupBtn.classList.toggle('hidden', !showTopup);
- }
- }
-
- function buildSubscriptionPurchasePayload() {
- const initData = tg.initData || '';
- if (!initData) {
- return null;
- }
-
- const payload = {
- initData,
- period: subscriptionPurchaseSelections.period,
- periodId: subscriptionPurchaseSelections.period,
- traffic: subscriptionPurchaseSelections.traffic,
- trafficId: subscriptionPurchaseSelections.traffic,
- servers: Array.from(subscriptionPurchaseSelections.servers || []),
- serverUuids: Array.from(subscriptionPurchaseSelections.servers || []),
- devices: subscriptionPurchaseSelections.devices,
- deviceLimit: subscriptionPurchaseSelections.devices,
- };
-
- const root = getPurchaseRoot();
- const contextId = getFirstDefined(root, ['context_id', 'contextId', 'purchase_context_id', 'purchaseContextId']);
- if (contextId !== undefined) {
- payload.contextId = contextId;
- payload.purchaseContextId = contextId;
- }
-
- if (userData?.subscription_id || userData?.subscriptionId) {
- payload.subscriptionId = userData.subscription_id || userData.subscriptionId;
- }
-
- return payload;
- }
-
- async function submitSubscriptionPurchase() {
- if (subscriptionPurchaseSubmitting) {
- return;
- }
-
- const errors = validateSubscriptionPurchaseSelections();
- if (errors.length) {
- renderSubscriptionPurchaseValidation();
- return;
- }
-
- const payload = buildSubscriptionPurchasePayload();
- if (!payload) {
- const fallbackUrl = getEffectivePurchaseUrl();
- if (fallbackUrl) {
- openExternalLink(fallbackUrl, { openInMiniApp: true });
- }
- return;
- }
-
- subscriptionPurchaseSubmitting = true;
- updateSubscriptionPurchaseButtons(errors);
-
- 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 || result?.success === false) {
- const message = result?.message || result?.detail || t('purchase.error.generic');
- showPopup(message === 'purchase.error.generic' ? t('purchase.error.generic') : message, t('purchase.title'));
- return;
- }
-
- const successMessage = result?.message || t('purchase.action.buy');
- showPopup(successMessage === 'purchase.action.buy' ? t('purchase.action.buy') : successMessage, t('purchase.title'));
- await refreshSubscriptionData({ silent: false });
- } catch (error) {
- console.error('Failed to submit subscription purchase:', error);
- const message = error?.message || t('purchase.error.generic');
- showPopup(message === 'purchase.error.generic' ? t('purchase.error.generic') : message, t('purchase.title'));
- } finally {
- subscriptionPurchaseSubmitting = false;
- renderSubscriptionPurchaseConfigurator();
- }
- }
-
async function handlePromoOfferAccept(offerId, button) {
if (!offerId) {
return;
@@ -12651,20 +10824,6 @@
openExternalLink(link, { openInMiniApp: true });
});
- document.getElementById('subscriptionPurchaseRetry')?.addEventListener('click', () => {
- ensureSubscriptionPurchaseData({ force: true }).catch(error => {
- console.warn('Failed to reload subscription purchase config:', error);
- });
- });
-
- document.getElementById('subscriptionPurchaseSubmit')?.addEventListener('click', () => {
- submitSubscriptionPurchase();
- });
-
- document.getElementById('subscriptionPurchaseTopup')?.addEventListener('click', () => {
- openTopupModal();
- });
-
initializePromoCodeForm();
init();