diff --git a/bot/handlers/user/subscription/core.py b/bot/handlers/user/subscription/core.py
index cb84d1b..ad499a2 100644
--- a/bot/handlers/user/subscription/core.py
+++ b/bot/handlers/user/subscription/core.py
@@ -11,6 +11,7 @@ from config.settings import Settings
from bot.keyboards.inline.user_keyboards import (
get_subscription_options_keyboard,
get_back_to_main_menu_markup,
+ get_autorenew_confirm_keyboard,
)
from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService
@@ -240,6 +241,56 @@ async def toggle_autorenew_handler(
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
return
+ # Show confirmation popup and inline buttons
+ confirm_text = get_text("autorenew_confirm_enable") if enable else get_text("autorenew_confirm_disable")
+ kb = get_autorenew_confirm_keyboard(enable, sub.subscription_id, current_lang, i18n)
+ try:
+ await callback.message.edit_text(confirm_text, reply_markup=kb)
+ except Exception:
+ try:
+ await callback.message.answer(confirm_text, reply_markup=kb)
+ except Exception:
+ pass
+ try:
+ await callback.answer()
+ except Exception:
+ pass
+ return
+
+
+@router.callback_query(F.data.startswith("autorenew:confirm:"))
+async def confirm_autorenew_handler(
+ callback: types.CallbackQuery,
+ settings: Settings,
+ i18n_data: dict,
+ session: AsyncSession,
+ subscription_service: SubscriptionService,
+ panel_service: PanelApiService,
+ bot: Bot,
+):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
+
+ try:
+ _, _, sub_id_str, enable_str = callback.data.split(":", 3)
+ sub_id = int(sub_id_str)
+ enable = bool(int(enable_str))
+ except Exception:
+ try:
+ await callback.answer(get_text("error_try_again"), show_alert=True)
+ except Exception:
+ pass
+ return
+
+ sub = await session.get(Subscription, sub_id)
+ if not sub or sub.user_id != callback.from_user.id:
+ await callback.answer(get_text("error_try_again"), show_alert=True)
+ return
+ if sub.provider == "tribute":
+ await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
+ return
+
await subscription_dal.update_subscription(session, sub.subscription_id, {"auto_renew_enabled": enable})
await session.commit()
try:
diff --git a/bot/keyboards/inline/user_keyboards.py b/bot/keyboards/inline/user_keyboards.py
index 6fd2dae..456eae4 100644
--- a/bot/keyboards/inline/user_keyboards.py
+++ b/bot/keyboards/inline/user_keyboards.py
@@ -320,3 +320,25 @@ def get_back_to_payment_method_details_keyboard(pm_id: str, lang: str, i18n_inst
# Back one step: return to specific payment method details
builder.row(InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data=f"pm:view:{pm_id}"))
return builder.as_markup()
+
+
+def get_autorenew_cancel_keyboard(lang: str, i18n_instance) -> InlineKeyboardMarkup:
+ _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(text=_(key="autorenew_disable_button"), callback_data="autorenew:cancel")
+ )
+ builder.row(
+ InlineKeyboardButton(text=_(key="menu_my_subscription_inline"), callback_data="main_action:my_subscription")
+ )
+ return builder.as_markup()
+
+
+def get_autorenew_confirm_keyboard(enable: bool, sub_id: int, lang: str, i18n_instance) -> InlineKeyboardMarkup:
+ _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(text=_(key="yes_button"), callback_data=f"autorenew:confirm:{sub_id}:{1 if enable else 0}"),
+ InlineKeyboardButton(text=_(key="no_button"), callback_data="main_action:my_subscription"),
+ )
+ return builder.as_markup()
diff --git a/bot/services/panel_webhook_service.py b/bot/services/panel_webhook_service.py
index c2ec807..56b6fda 100644
--- a/bot/services/panel_webhook_service.py
+++ b/bot/services/panel_webhook_service.py
@@ -10,7 +10,7 @@ from typing import Optional
from config.settings import Settings
from .panel_api_service import PanelApiService
from bot.middlewares.i18n import JsonI18n
-from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup
+from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup, get_autorenew_cancel_keyboard
from db.dal import user_dal
from bot.utils.date_utils import add_months
@@ -208,6 +208,21 @@ class PanelWebhookService:
except Exception:
logging.exception("Auto-renew trigger (24h) failed pre-check")
if days_left <= self.settings.SUBSCRIPTION_NOTIFY_DAYS_BEFORE:
+ # For 48h event, if auto-renew is enabled and not tribute, show special notice with cancel button
+ if days_left == 2:
+ async with self.async_session_factory() as session:
+ from db.dal import subscription_dal
+ sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id)
+ if sub and sub.auto_renew_enabled and sub.provider != 'tribute':
+ cancel_kb = get_autorenew_cancel_keyboard(lang, self.i18n)
+ await self._send_message(
+ user_id,
+ lang,
+ "autorenew_48h_charge_tomorrow_notice",
+ reply_markup=cancel_kb,
+ user_name=first_name,
+ )
+ return
await self._send_message(
user_id,
lang,
diff --git a/locales/en.json b/locales/en.json
index 63234a7..d4a73e8 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -209,6 +209,9 @@
"subscription_24h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 1 day — {end_date}.\n\nPlease renew it using the button below.",
"subscription_expired_notification": "👋 Hi, {user_name}!\n\n⛔ Your VPN subscription expired on {end_date}.\n\nPlease renew it using the button below.",
"subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.",
+ "autorenew_48h_charge_tomorrow_notice": "🔔 Reminder\n\nTomorrow an automatic charge will occur to renew your subscription. If you don't want auto-renew, disable it using the button below.",
+ "autorenew_confirm_enable": "🔄 Enable auto-renew? An automatic charge will be attempted before your subscription ends.",
+ "autorenew_confirm_disable": "🛑 Disable auto-renew? No further automatic charges will occur.",
"tribute_subscription_cancelled": "🚨 Subscription Cancelled\n\nYour Tribute subscription has been cancelled. You have 24 hours to restore access, after which the subscription will be blocked.\n\nTo renew your subscription, press the button below.",
"tribute_auto_renewal": "🔄 Subscription Auto-Renewed\n\nYour Tribute subscription has been automatically renewed for {months} months.\nNew expiration date: {end_date}",
"yookassa_auto_renewal": "🔄 Subscription Auto-Renewed\n\nYour subscription was automatically renewed for {months} month(s).\nNew expiration date: {end_date}",
@@ -372,8 +375,8 @@
"admin_sync_not_found_in_db": "\n❌ Not found in DB: {count}",
"admin_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})",
"my_subscription_details": "🔐 My Subscription\n\n⏰ Status: {status}\n📅 Active until: {end_date}\n📆 Days left: {days_left}\n\n🔗 Configuration link:\n{config_link}\n\n📊 Traffic:\nLimit: {traffic_limit}\nUsed: {traffic_used}",
- "autorenew_enable_button": "Enable auto-renew",
- "autorenew_disable_button": "Disable auto-renew",
+ "autorenew_enable_button": "🔄 Enable auto-renew",
+ "autorenew_disable_button": "🛑 Disable auto-renew",
"subscription_autorenew_updated": "Auto-renew settings updated.",
"payment_methods_manage_button": "💳 Payment Methods",
"payment_methods_title": "💳 Payment Methods",
diff --git a/locales/ru.json b/locales/ru.json
index ea8ffaa..d28f4b8 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -139,6 +139,9 @@
"subscription_24h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 1 день — {end_date}.\n\nПродлите её по кнопке ниже.",
"subscription_expired_notification": "👋 Привет, {user_name}!\n\n⛔ Срок вашей подписки на VPN истек ({end_date}).\n\nПродлите её по кнопке ниже.",
"subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.",
+ "autorenew_48h_charge_tomorrow_notice": "🔔 Напоминание\n\nЗавтра будет автоматическое списание за продление подписки. Если вы не хотите автопродление — отключите его кнопкой ниже.",
+ "autorenew_confirm_enable": "🔄 Включить автопродление? Перед окончанием подписки будет выполняться автосписание.",
+ "autorenew_confirm_disable": "🛑 Отключить автопродление? Автосписаний больше не будет.",
"tribute_subscription_cancelled": "🚨 Подписка отменена\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.",
"yookassa_auto_renewal": "🔄 Подписка автоматически продлена\n\nВаша подписка была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}",
"admin_promo_set_validity_days": "⏰ Установить срок (дни)",
@@ -371,8 +374,8 @@
"admin_sync_not_found_in_db": "\n❌ Не найдено в БД: {count}",
"admin_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})",
"my_subscription_details": "🔐 Моя подписка\n\n⏰ Статус: {status}\n📅 Действует до: {end_date}\n📆 Осталось дней: {days_left}\n\n🔗 Ссылка на конфигурацию:\n{config_link}\n\n📊 Трафик:\nЛимит: {traffic_limit}\nИспользовано: {traffic_used}",
- "autorenew_enable_button": "Включить автопродление",
- "autorenew_disable_button": "Выключить автопродление",
+ "autorenew_enable_button": "🔄 Включить автопродление",
+ "autorenew_disable_button": "🛑 Отключить автопродление",
"subscription_autorenew_updated": "Настройки автопродления обновлены.",
"payment_methods_manage_button": "💳 Способы оплаты",
"payment_methods_title": "💳 Способы оплаты",