diff --git a/miniapp/index.html b/miniapp/index.html
index 9e97474f..8fb9d4d8 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -2707,6 +2707,168 @@
color: #41464b;
}
+ .status-paused {
+ background: linear-gradient(135deg, #e2e3e5, #eeeff1);
+ color: #41464b;
+ }
+
+ /* Daily Subscription Status Bar */
+ .daily-subscription-status {
+ margin-top: 16px;
+ padding: 16px;
+ background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.08), rgba(var(--primary-rgb), 0.04));
+ border-radius: var(--radius-lg);
+ border: 1px solid rgba(var(--primary-rgb), 0.15);
+ }
+
+ .daily-subscription-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ }
+
+ .daily-subscription-title {
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .daily-subscription-title .daily-icon {
+ font-size: 18px;
+ }
+
+ .daily-subscription-price {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--primary);
+ }
+
+ .daily-subscription-progress {
+ margin-bottom: 12px;
+ }
+
+ .daily-progress-bar {
+ height: 8px;
+ background: rgba(var(--primary-rgb), 0.15);
+ border-radius: 4px;
+ overflow: hidden;
+ position: relative;
+ }
+
+ .daily-progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary), rgba(var(--primary-rgb), 0.7));
+ border-radius: 4px;
+ transition: width 0.5s ease;
+ position: relative;
+ }
+
+ .daily-progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
+ animation: shimmer 2s infinite;
+ }
+
+ .daily-subscription-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 13px;
+ }
+
+ .daily-time-remaining {
+ color: var(--text-secondary);
+ }
+
+ .daily-time-remaining strong {
+ color: var(--text-primary);
+ font-weight: 600;
+ }
+
+ .daily-next-charge {
+ color: var(--text-secondary);
+ }
+
+ .daily-subscription-actions {
+ margin-top: 12px;
+ display: flex;
+ gap: 8px;
+ }
+
+ .daily-pause-btn {
+ flex: 1;
+ padding: 10px 16px;
+ border-radius: var(--radius);
+ border: 2px solid var(--border-color);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+
+ .daily-pause-btn:hover {
+ border-color: var(--primary);
+ background: rgba(var(--primary-rgb), 0.05);
+ }
+
+ .daily-pause-btn.paused {
+ background: linear-gradient(135deg, var(--primary), rgba(var(--primary-rgb), 0.8));
+ color: white;
+ border-color: transparent;
+ }
+
+ .daily-pause-btn.paused:hover {
+ opacity: 0.9;
+ }
+
+ .daily-pause-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ .daily-subscription-paused-notice {
+ margin-top: 12px;
+ padding: 12px;
+ background: rgba(var(--warning-rgb), 0.1);
+ border-radius: var(--radius);
+ border: 1px solid rgba(var(--warning-rgb), 0.2);
+ font-size: 13px;
+ color: var(--text-primary);
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ }
+
+ .daily-subscription-paused-notice .notice-icon {
+ font-size: 18px;
+ line-height: 1;
+ }
+
+ :root[data-theme="dark"] .daily-subscription-status {
+ background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.12), rgba(var(--primary-rgb), 0.06));
+ border-color: rgba(var(--primary-rgb), 0.25);
+ }
+
+ :root[data-theme="dark"] .status-paused {
+ background: linear-gradient(135deg, rgba(100, 116, 139, 0.3), rgba(100, 116, 139, 0.2));
+ color: #94a3b8;
+ }
+
.status-missing {
background: linear-gradient(135deg, #e0e7ff, #eef2ff);
color: #1e3a8a;
@@ -5569,6 +5731,39 @@
+
+
+
+
+
+
+ Осталось:
+ --:--:--
+
+
+
+
+
+
+
+ ⏸️
+ Подписка приостановлена. Ежедневное списание отключено. VPN не работает.
+
+
+
@@ -6433,6 +6628,16 @@
'info.promo_group': 'Promo group',
'info.device_limit': 'Device limit',
'info.autopay': 'Auto-pay',
+ // Daily subscription
+ 'daily.tariff_name': 'Daily tariff',
+ 'daily.time_remaining': 'Time left:',
+ 'daily.next_charge': 'Next charge at',
+ 'daily.pause': 'Pause',
+ 'daily.resume': 'Resume',
+ 'daily.paused_notice': 'Subscription paused. Daily billing disabled. VPN is not working.',
+ 'daily.status.active': 'Active',
+ 'daily.status.paused': 'Paused',
+ 'daily.hours_remaining': 'Hours left',
'button.connect.default': 'Connect to VPN',
'button.connect.happ': 'Connect',
'button.copy': 'Copy subscription link',
@@ -6896,6 +7101,16 @@
'info.promo_group': 'Уровень',
'info.device_limit': 'Лимит устройств',
'info.autopay': 'Автоплатеж',
+ // Суточная подписка
+ 'daily.tariff_name': 'Суточный тариф',
+ 'daily.time_remaining': 'Осталось:',
+ 'daily.next_charge': 'Следующее списание',
+ 'daily.pause': 'Приостановить',
+ 'daily.resume': 'Возобновить',
+ 'daily.paused_notice': 'Подписка приостановлена. Ежедневное списание отключено. VPN не работает.',
+ 'daily.status.active': 'Активна',
+ 'daily.status.paused': 'Приостановлена',
+ 'daily.hours_remaining': 'Часов осталось',
'button.connect.default': 'Подключиться к VPN',
'button.connect.happ': 'Подключиться',
'button.copy': 'Скопировать ссылку подписки',
@@ -9278,6 +9493,9 @@
: autopayLabel;
}
+ // Отображение суточной подписки
+ renderDailySubscriptionStatus();
+
renderSubscriptionMissingCard();
renderSubscriptionPurchaseCard();
renderSubscriptionRenewalCard();
@@ -9295,6 +9513,195 @@
updateActionButtons();
}
+ // ============================================================
+ // Суточная подписка - отображение и управление
+ // ============================================================
+ let dailyTimerInterval = null;
+
+ function renderDailySubscriptionStatus() {
+ const container = document.getElementById('dailySubscriptionStatus');
+ if (!container) return;
+
+ const user = userData?.user;
+ if (!user) {
+ container.classList.add('hidden');
+ return;
+ }
+
+ const isDailyTariff = user.is_daily_tariff ?? user.isDailyTariff ?? false;
+ const isDailyPaused = user.is_daily_paused ?? user.isDailyPaused ?? false;
+ const dailyTariffName = user.daily_tariff_name ?? user.dailyTariffName ?? '';
+ const dailyPriceLabel = user.daily_price_label ?? user.dailyPriceLabel ?? '';
+ const dailyNextChargeAt = user.daily_next_charge_at ?? user.dailyNextChargeAt ?? null;
+
+ if (!isDailyTariff) {
+ container.classList.add('hidden');
+ if (dailyTimerInterval) {
+ clearInterval(dailyTimerInterval);
+ dailyTimerInterval = null;
+ }
+ return;
+ }
+
+ container.classList.remove('hidden');
+
+ // Заполняем данные
+ const tariffNameEl = document.getElementById('dailyTariffName');
+ if (tariffNameEl && dailyTariffName) {
+ tariffNameEl.textContent = dailyTariffName;
+ }
+
+ const priceEl = document.getElementById('dailyPrice');
+ if (priceEl && dailyPriceLabel) {
+ priceEl.textContent = dailyPriceLabel;
+ }
+
+ // Настройка кнопки паузы
+ const pauseBtn = document.getElementById('dailyPauseBtn');
+ const pauseBtnIcon = document.getElementById('dailyPauseBtnIcon');
+ const pauseBtnText = document.getElementById('dailyPauseBtnText');
+ const pausedNotice = document.getElementById('dailyPausedNotice');
+ const progressSection = document.getElementById('dailyProgressSection');
+
+ if (isDailyPaused) {
+ pauseBtn?.classList.add('paused');
+ if (pauseBtnIcon) pauseBtnIcon.textContent = '▶️';
+ if (pauseBtnText) pauseBtnText.textContent = t('daily.resume');
+ pausedNotice?.classList.remove('hidden');
+ progressSection?.classList.add('hidden');
+
+ // Обновляем статус badge
+ const statusBadge = document.getElementById('statusBadge');
+ if (statusBadge) {
+ statusBadge.textContent = t('daily.status.paused');
+ statusBadge.className = 'status-badge status-paused';
+ }
+ } else {
+ pauseBtn?.classList.remove('paused');
+ if (pauseBtnIcon) pauseBtnIcon.textContent = '⏸️';
+ if (pauseBtnText) pauseBtnText.textContent = t('daily.pause');
+ pausedNotice?.classList.add('hidden');
+ progressSection?.classList.remove('hidden');
+ }
+
+ // Обработчик кнопки паузы
+ if (pauseBtn) {
+ pauseBtn.onclick = () => toggleDailyPause();
+ }
+
+ // Запуск таймера обратного отсчета
+ startDailyCountdownTimer(dailyNextChargeAt, isDailyPaused);
+ }
+
+ function startDailyCountdownTimer(nextChargeAt, isPaused) {
+ if (dailyTimerInterval) {
+ clearInterval(dailyTimerInterval);
+ dailyTimerInterval = null;
+ }
+
+ const timeRemainingEl = document.getElementById('dailyTimeRemaining');
+ const progressFill = document.getElementById('dailyProgressFill');
+ const nextChargeEl = document.getElementById('dailyNextCharge');
+
+ if (!nextChargeAt || isPaused) {
+ if (timeRemainingEl) timeRemainingEl.textContent = '--:--:--';
+ if (progressFill) progressFill.style.width = '0%';
+ if (nextChargeEl) nextChargeEl.textContent = '';
+ return;
+ }
+
+ const nextChargeDate = new Date(nextChargeAt);
+ const totalDuration = 24 * 60 * 60 * 1000; // 24 часа в миллисекундах
+
+ function updateTimer() {
+ const now = new Date();
+ const remaining = nextChargeDate - now;
+
+ if (remaining <= 0) {
+ if (timeRemainingEl) timeRemainingEl.textContent = '00:00:00';
+ if (progressFill) progressFill.style.width = '0%';
+ clearInterval(dailyTimerInterval);
+ return;
+ }
+
+ // Форматируем оставшееся время
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
+ const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
+ const seconds = Math.floor((remaining % (1000 * 60)) / 1000);
+ const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
+
+ if (timeRemainingEl) timeRemainingEl.textContent = timeStr;
+
+ // Обновляем прогресс бар (от 100% до 0%)
+ const progressPercent = Math.max(0, Math.min(100, (remaining / totalDuration) * 100));
+ if (progressFill) progressFill.style.width = `${progressPercent}%`;
+
+ // Время следующего списания
+ if (nextChargeEl) {
+ const chargeTime = nextChargeDate.toLocaleTimeString(currentLanguage === 'ru' ? 'ru-RU' : 'en-US', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ nextChargeEl.textContent = `${t('daily.next_charge')} ${chargeTime}`;
+ }
+ }
+
+ updateTimer();
+ dailyTimerInterval = setInterval(updateTimer, 1000);
+ }
+
+ async function toggleDailyPause() {
+ const pauseBtn = document.getElementById('dailyPauseBtn');
+ if (!pauseBtn || pauseBtn.disabled) return;
+
+ pauseBtn.disabled = true;
+
+ try {
+ const initData = window.Telegram?.WebApp?.initData || '';
+ const response = await fetch(`${apiBaseUrl}/miniapp/subscription/daily/toggle-pause`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ init_data: initData })
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ const errorCode = result?.detail?.code || 'unknown';
+ if (errorCode === 'insufficient_balance') {
+ showToast(t('daily.error.insufficient_balance') || 'Недостаточно средств для возобновления подписки', 'error');
+ } else {
+ showToast(result?.detail?.message || 'Error', 'error');
+ }
+ return;
+ }
+
+ // Обновляем локальные данные
+ if (userData?.user) {
+ userData.user.is_daily_paused = result.is_paused;
+ userData.user.isDailyPaused = result.is_paused;
+ }
+
+ // Обновляем баланс
+ if (result.balance_kopeks !== undefined) {
+ userData.balance_kopeks = result.balance_kopeks;
+ userData.balanceKopeks = result.balance_kopeks;
+ renderBalanceSection();
+ }
+
+ // Перерисовываем статус
+ renderDailySubscriptionStatus();
+
+ showToast(result.message || 'OK', 'success');
+
+ } catch (error) {
+ console.error('Error toggling daily pause:', error);
+ showToast('Connection error', 'error');
+ } finally {
+ pauseBtn.disabled = false;
+ }
+ }
+
function renderSubscriptionMissingCard() {
const card = document.getElementById('subscriptionMissingCard');
if (!card) {
@@ -20359,6 +20766,8 @@
// Разные стили для режима смены и покупки
if (isInstantSwitchMode) {
// Режим мгновенной смены тарифа - используем точные данные с сервера
+ const isDaily = tariff.is_daily ?? tariff.isDaily ?? false;
+ const dailyPriceLabel = tariff.daily_price_label ?? tariff.dailyPriceLabel ?? null;
const isUpgrade = tariff.is_upgrade ?? tariff.isUpgrade ?? false;
const isSwitchFree = tariff.is_switch_free ?? tariff.isSwitchFree ?? !isUpgrade;
const upgradeCost = tariff.switch_cost_kopeks ?? tariff.switchCostKopeks ?? 0;
@@ -20394,12 +20803,13 @@
const upgradeLabel = preferredLanguage === 'en' ? 'upgrade' : 'доплата';
const freeLabel = preferredLanguage === 'en' ? '✓ Free' : '✓ Бесплатно';
+ const dailyLabel = preferredLanguage === 'en' ? 'daily' : 'суточный';
div.className = 'instant-switch-tariff-item';
div.innerHTML = `
- ${isPremium ? '👑 ' : '⚡ '}${escapeHtml(tariff.name)}
+ ${isDaily ? '🔄 ' : (isPremium ? '👑 ' : '⚡ ')}${escapeHtml(tariff.name)}
${description ? `
${escapeHtml(description)}
` : ''}
@@ -20409,22 +20819,40 @@
${serverTags ? `
${serverTags}
` : ''}
-
- ${isUpgrade
- ? `${upgradeLabel}+${upgradeCostLabel}`
- : freeLabel}
-
+ ${isDaily ? `
+
+ ${dailyLabel}
+ ${dailyPriceLabel || ''}
+
+ ` : `
+
+ ${isUpgrade
+ ? `${upgradeLabel}+${upgradeCostLabel}`
+ : freeLabel}
+
+ `}
`;
div.addEventListener('click', () => showInstantSwitchConfirm(tariff, isUpgrade, upgradeCost));
} else {
// Обычный режим покупки
+ const isDaily = tariff.is_daily ?? tariff.isDaily ?? false;
+ const dailyPriceKopeks = tariff.daily_price_kopeks ?? tariff.dailyPriceKopeks ?? 0;
+ const dailyPriceLabel = tariff.daily_price_label ?? tariff.dailyPriceLabel ?? null;
+
let minPrice = null;
let minPriceOriginal = null;
let maxDiscountPercent = 0;
+ let priceDisplayLabel = null;
+ let priceDisplayOriginalLabel = null;
+ let pricePrefix = preferredLanguage === 'en' ? 'from' : 'от';
- if (periods.length > 0) {
+ if (isDaily && dailyPriceKopeks > 0) {
+ // Суточный тариф - показываем цену за день
+ priceDisplayLabel = dailyPriceLabel || formatPriceFromKopeks(dailyPriceKopeks, tariffsData?.currency || 'RUB') + '/день';
+ pricePrefix = '';
+ } else if (periods.length > 0) {
periods.forEach(p => {
const price = p.price_kopeks || p.priceKopeks || 0;
const originalPrice = p.original_price_kopeks || p.originalPriceKopeks || price;
@@ -20438,14 +20866,16 @@
maxDiscountPercent = discountPct;
}
});
+ priceDisplayLabel = minPrice !== null
+ ? formatPriceFromKopeks(minPrice, tariffsData?.currency || 'RUB')
+ : null;
+ priceDisplayOriginalLabel = minPriceOriginal !== null
+ ? formatPriceFromKopeks(minPriceOriginal, tariffsData?.currency || 'RUB')
+ : null;
}
- const minPriceLabel = minPrice !== null
- ? formatPriceFromKopeks(minPrice, tariffsData?.currency || 'RUB')
- : null;
- const minPriceOriginalLabel = minPriceOriginal !== null
- ? formatPriceFromKopeks(minPriceOriginal, tariffsData?.currency || 'RUB')
- : null;
+ const minPriceLabel = priceDisplayLabel;
+ const minPriceOriginalLabel = priceDisplayOriginalLabel;
div.className = 'subscription-settings-toggle' + (selectedTariffId === tariff.id ? ' active' : '');
@@ -20489,10 +20919,11 @@
${minPriceLabel ? `
-
от
+ ${pricePrefix ? `
${pricePrefix}
` : ''}
${minPriceOriginalLabel ? `
${minPriceOriginalLabel}
` : ''}
${minPriceLabel}
${maxDiscountPercent > 0 ? `
-${maxDiscountPercent}%
` : ''}
+ ${isDaily ? `
🔄 суточный
` : ''}
` : ''}