diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index 1ede0fa..fc7c0c6 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -167,6 +167,16 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Embedded install guides", "Open install instructions inside the Web App instead of an external connect page.", ), + SettingField( + "SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED", + "bool", + "subscription_guides", + "Open install guides from bot", + ( + "Use the Telegram Mini App install screen for bot connect buttons and show " + "public install guide links." + ), + ), SettingField( "SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED", "bool", diff --git a/backend/bot/handlers/user/promo_user.py b/backend/bot/handlers/user/promo_user.py index 2996bee..973e1eb 100644 --- a/backend/bot/handlers/user/promo_user.py +++ b/backend/bot/handlers/user/promo_user.py @@ -16,6 +16,10 @@ from bot.services.promo_code_service import PromoCodeService from bot.services.subscription_service import SubscriptionService from bot.states.user_states import UserPromoStates from bot.utils.callback_answer import safe_answer_callback +from bot.utils.install_links import ( + append_install_share_link_text, + ensure_user_install_guide_links, +) from config.settings import Settings from .start import send_main_menu @@ -160,12 +164,30 @@ async def process_promo_code_input( end_date=(new_end_date.strftime("%d.%m.%Y %H:%M:%S") if new_end_date else "N/A"), config_link=config_link_text, ) + install_links = await ensure_user_install_guide_links(session, settings, user.id) + install_share_url = install_links.public_share_url + if install_share_url: + try: + await session.commit() + response_to_user_text = append_install_share_link_text( + response_to_user_text, + _, + install_share_url, + ) + except Exception: + await session.rollback() + logging.exception( + "Failed to persist install guide share token for promo user %s.", + user.id, + ) + install_share_url = None reply_markup = get_connect_and_main_keyboard( current_lang, i18n, settings, config_link_display, connect_button_url=connect_button_url, + install_share_url=install_share_url, ) else: await session.commit() diff --git a/backend/bot/handlers/user/start.py b/backend/bot/handlers/user/start.py index 4233a07..981fb73 100644 --- a/backend/bot/handlers/user/start.py +++ b/backend/bot/handlers/user/start.py @@ -23,6 +23,10 @@ from bot.services.promo_code_service import PromoCodeService from bot.services.referral_service import ReferralService from bot.services.subscription_service import SubscriptionService from bot.utils.callback_answer import safe_answer_callback +from bot.utils.install_links import ( + append_install_share_link_text, + ensure_user_install_guide_links, +) from bot.utils.text_sanitizer import sanitize_display_name, sanitize_username from config.settings import Settings from db.dal import user_dal @@ -715,6 +719,23 @@ async def start_command_handler( ), config_link=config_link_text, ) + install_links = await ensure_user_install_guide_links(session, settings, user_id) + install_share_url = install_links.public_share_url + if install_share_url: + try: + await session.commit() + promo_success_text = append_install_share_link_text( + promo_success_text, + _, + install_share_url, + ) + except Exception: + await session.rollback() + logging.exception( + "Failed to persist install guide share token for promo user %s.", + user_id, + ) + install_share_url = None from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard @@ -726,6 +747,7 @@ async def start_command_handler( settings, config_link_display, connect_button_url=connect_button_url, + install_share_url=install_share_url, ), parse_mode="HTML", ) diff --git a/backend/bot/handlers/user/subscription/core.py b/backend/bot/handlers/user/subscription/core.py index 76327eb..d39278b 100644 --- a/backend/bot/handlers/user/subscription/core.py +++ b/backend/bot/handlers/user/subscription/core.py @@ -27,6 +27,10 @@ from bot.keyboards.inline.user_keyboards import ( from bot.middlewares.i18n import JsonI18n from bot.services.panel_api_service import PanelApiService from bot.services.subscription_service import SubscriptionService +from bot.utils.install_links import ( + append_install_share_link_text, + ensure_user_install_guide_links, +) from config.settings import Settings from db.dal import subscription_dal, user_billing_dal from db.models import Subscription @@ -1026,12 +1030,50 @@ async def my_subscription_command_handler( local_sub = await subscription_dal.get_active_subscription_by_user_id( session, event.from_user.id ) + install_links = await ensure_user_install_guide_links( + session, + settings, + event.from_user.id, + local_subscription=local_sub, + ) + install_url = install_links.personal_url + install_share_url = install_links.public_share_url + if install_share_url: + try: + await session.commit() + text = append_install_share_link_text(text, get_text, install_share_url) + except Exception: + await session.rollback() + logging.exception( + "Failed to persist install guide share token for user %s.", + event.from_user.id, + ) + install_share_url = None + # Build rows to prepend above the base "back" markup prepend_rows = [] # 1) Connect button: prefer the actual subscription URL; fall back to mini-app cfg_link_val = connect_button_url or config_link_display - if cfg_link_val: + if install_url: + prepend_rows.append( + [ + InlineKeyboardButton( + text=get_text("connect_button"), + web_app=WebAppInfo(url=install_url), + ) + ] + ) + if install_share_url: + prepend_rows.append( + [ + InlineKeyboardButton( + text=get_text("install_guide_share_button"), + url=install_share_url, + ) + ] + ) + elif cfg_link_val: prepend_rows.append( [ InlineKeyboardButton( diff --git a/backend/bot/handlers/user/trial_handler.py b/backend/bot/handlers/user/trial_handler.py index 078080f..843675f 100644 --- a/backend/bot/handlers/user/trial_handler.py +++ b/backend/bot/handlers/user/trial_handler.py @@ -14,6 +14,10 @@ from bot.services.notification_service import NotificationService from bot.services.panel_api_service import PanelApiService from bot.services.subscription_service import SubscriptionService from bot.utils.config_link import prepare_config_links +from bot.utils.install_links import ( + append_install_share_link_text, + ensure_user_install_guide_links, +) from config.settings import Settings from .start import send_main_menu @@ -74,6 +78,7 @@ async def request_trial_confirmation_handler( config_link_display_for_trial = None config_link_for_trial = None connect_button_url_for_trial = None + install_share_url = None if activation_result and activation_result.get("activated"): try: @@ -104,6 +109,14 @@ async def request_trial_confirmation_handler( traffic_gb=traffic_display, ) + install_links = await ensure_user_install_guide_links(session, settings, user_id) + install_share_url = install_links.public_share_url + final_message_text_in_chat = append_install_share_link_text( + final_message_text_in_chat, + _, + install_share_url, + ) + # Send notification to admin about new trial notification_service = NotificationService(callback.bot, settings, i18n) await notification_service.notify_trial_activation(user_id, end_date_obj) @@ -139,6 +152,7 @@ async def request_trial_confirmation_handler( settings, config_link_display_for_trial, connect_button_url=connect_button_url_for_trial, + install_share_url=install_share_url, ) if activation_result and activation_result.get("activated") else get_main_menu_inline_keyboard( @@ -214,6 +228,7 @@ async def confirm_activate_trial_handler( config_link_display_for_trial = None config_link_for_trial = None connect_button_url_for_trial = None + install_share_url = None if activation_result and activation_result.get("activated"): try: @@ -243,6 +258,13 @@ async def confirm_activate_trial_handler( config_link=config_link_for_trial, traffic_gb=traffic_display, ) + install_links = await ensure_user_install_guide_links(session, settings, user_id) + install_share_url = install_links.public_share_url + final_message_text_in_chat = append_install_share_link_text( + final_message_text_in_chat, + _, + install_share_url, + ) else: message_key_from_service = ( activation_result.get("message_key", "trial_activation_failed") @@ -266,6 +288,7 @@ async def confirm_activate_trial_handler( settings, config_link_display_for_trial, connect_button_url=connect_button_url_for_trial, + install_share_url=install_share_url, ) if activation_result and activation_result.get("activated") else get_main_menu_inline_keyboard( diff --git a/backend/bot/keyboards/inline/user_keyboards.py b/backend/bot/keyboards/inline/user_keyboards.py index ae2ea06..035eb20 100644 --- a/backend/bot/keyboards/inline/user_keyboards.py +++ b/backend/bot/keyboards/inline/user_keyboards.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple from aiogram.types import InlineKeyboardMarkup, WebAppInfo from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton +from bot.utils.install_links import bot_install_guide_url from config.settings import Settings BOT_MENU_CONTEXT = "bot" @@ -689,13 +690,29 @@ def get_connect_and_main_keyboard( config_link: Optional[str], connect_button_url: Optional[str] = None, preserve_message: bool = False, + install_share_url: Optional[str] = None, ) -> InlineKeyboardMarkup: """Keyboard with a connect button and a back to main menu button.""" _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) builder = InlineKeyboardBuilder() + install_url = bot_install_guide_url(settings) button_target = connect_button_url or config_link - if button_target: + if install_url: + builder.row( + InlineKeyboardButton( + text=_("connect_button"), + web_app=WebAppInfo(url=install_url), + ) + ) + if install_share_url: + builder.row( + InlineKeyboardButton( + text=_("install_guide_share_button"), + url=install_share_url, + ) + ) + elif button_target: builder.row(InlineKeyboardButton(text=_("connect_button"), url=button_target)) elif settings.SUBSCRIPTION_MINI_APP_URL: builder.row( diff --git a/backend/bot/payment_providers/shared/success.py b/backend/bot/payment_providers/shared/success.py index 0a09b58..c47c72d 100644 --- a/backend/bot/payment_providers/shared/success.py +++ b/backend/bot/payment_providers/shared/success.py @@ -11,6 +11,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.services.notification_service import NotificationService from bot.utils.config_link import prepare_config_links +from bot.utils.install_links import ( + append_install_share_link_text, + ensure_user_install_guide_links, +) from bot.utils.text_sanitizer import sanitize_display_name, username_for_display from db.dal import payment_dal, user_dal from db.models import Payment, User @@ -136,6 +140,7 @@ async def send_success_message_to_user( settings: Any, config_link_display: Optional[str], connect_button_url: Optional[str], + install_share_url: Optional[str] = None, include_keyboard: bool = True, log_prefix: str = "payment_providers", ) -> None: @@ -148,6 +153,7 @@ async def send_success_message_to_user( settings, config_link_display, connect_button_url=connect_button_url, + install_share_url=install_share_url, preserve_message=True, ) try: @@ -332,6 +338,31 @@ async def finalize_successful_payment( if req.text_prefix: success_text = f"{req.text_prefix}\n{success_text}" + install_share_url = None + if not req.skip_keyboard: + install_links = await ensure_user_install_guide_links( + req.session, + req.settings, + req.user_id, + ) + install_share_url = install_links.public_share_url + if install_share_url: + try: + await req.session.commit() + success_text = append_install_share_link_text( + success_text, + translator, + install_share_url, + ) + except Exception: + await req.session.rollback() + logging.exception( + "%s: failed to persist install guide share token for user %s.", + req.log_prefix, + req.user_id, + ) + install_share_url = None + await send_success_message_to_user( bot=req.bot, user_id=req.user_id, @@ -341,6 +372,7 @@ async def finalize_successful_payment( settings=req.settings, config_link_display=config_link_display, connect_button_url=connect_button_url, + install_share_url=install_share_url, include_keyboard=not req.skip_keyboard, log_prefix=req.log_prefix, ) diff --git a/backend/bot/payment_providers/yookassa.py b/backend/bot/payment_providers/yookassa.py index 3f82b9e..719ff6d 100644 --- a/backend/bot/payment_providers/yookassa.py +++ b/backend/bot/payment_providers/yookassa.py @@ -36,6 +36,10 @@ from bot.services.panel_api_service import PanelApiService from bot.services.referral_service import ReferralService from bot.services.subscription_service import SubscriptionService from bot.utils.config_link import prepare_config_links +from bot.utils.install_links import ( + append_install_share_link_text, + ensure_user_install_guide_links, +) from bot.utils.request_security import ip_in_allowlist, request_client_ip from config.settings import Settings from db.dal import payment_dal, user_billing_dal, user_dal @@ -741,6 +745,16 @@ async def process_successful_payment( ) ) include_keyboard = True + + install_share_url = None + if include_keyboard: + install_links = await ensure_user_install_guide_links(session, settings, user_id) + install_share_url = install_links.public_share_url + details_message = append_install_share_link_text( + details_message, + translator, + install_share_url, + ) await send_success_message_to_user( bot=bot, user_id=user_id, @@ -750,6 +764,7 @@ async def process_successful_payment( settings=settings, config_link_display=config_link_display, connect_button_url=connect_button_url, + install_share_url=install_share_url, include_keyboard=include_keyboard, log_prefix="YooKassa webhook", ) diff --git a/backend/bot/utils/install_links.py b/backend/bot/utils/install_links.py new file mode 100644 index 0000000..1089c70 --- /dev/null +++ b/backend/bot/utils/install_links.py @@ -0,0 +1,84 @@ +"""Helpers for Telegram bot install-guide links.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Any, Optional + +from sqlalchemy.ext.asyncio import AsyncSession + +from bot.utils.mini_app_url import ( + subscription_mini_app_install_url, + subscription_public_install_url, +) +from config.subscription_guides_config import subscription_guides_available +from db.dal import subscription_dal + + +@dataclass(frozen=True) +class InstallGuideLinks: + personal_url: Optional[str] = None + public_share_url: Optional[str] = None + + +def bot_install_guides_enabled(settings: Any) -> bool: + return bool( + getattr(settings, "SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED", False) + and subscription_guides_available(settings) + and subscription_mini_app_install_url(settings) + ) + + +def bot_install_guide_url(settings: Any) -> Optional[str]: + if not bot_install_guides_enabled(settings): + return None + return subscription_mini_app_install_url(settings) + + +async def ensure_user_install_guide_links( + session: AsyncSession, + settings: Any, + user_id: int, + panel_user_uuid: Optional[str] = None, + local_subscription: Optional[Any] = None, +) -> InstallGuideLinks: + personal_url = bot_install_guide_url(settings) + if not personal_url: + return InstallGuideLinks() + + public_share_url = None + try: + local_sub = ( + local_subscription + if local_subscription is not None + else await subscription_dal.get_active_subscription_by_user_id( + session, + user_id, + panel_user_uuid, + ) + ) + if local_sub is not None: + share_token = await subscription_dal.ensure_install_share_token(session, local_sub) + public_share_url = subscription_public_install_url(settings, share_token) + except Exception: + logging.exception("Failed to resolve install guide share link for user %s.", user_id) + + return InstallGuideLinks(personal_url=personal_url, public_share_url=public_share_url) + + +def append_install_share_link_text( + text: str, + translator: Any, + public_share_url: Optional[str], +) -> str: + if not public_share_url: + return text + try: + share_line = translator( + "install_guide_share_link_line", + install_share_link=public_share_url, + ) + except Exception: + share_line = f"\n\nInstall guide:\n{public_share_url}" + return f"{text}{share_line}" diff --git a/backend/bot/utils/mini_app_url.py b/backend/bot/utils/mini_app_url.py index 3dbea92..904001e 100644 --- a/backend/bot/utils/mini_app_url.py +++ b/backend/bot/utils/mini_app_url.py @@ -3,9 +3,10 @@ from __future__ import annotations from typing import Optional -from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit +from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit from config.settings import Settings +from db.dal.subscription_dal import normalize_install_share_token def append_query_params(base_url: str, params: dict[str, str]) -> str: @@ -31,3 +32,31 @@ def subscription_mini_app_topup_url(settings: Settings, kind: str) -> Optional[s return None normalized = "premium" if str(kind or "").strip().lower() == "premium" else "regular" return append_query_params(base, {"topup": normalized}) + + +def subscription_mini_app_path_url(settings: Settings, path: str) -> Optional[str]: + """Return a Mini App URL with ``path`` appended to the configured app base.""" + base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", None) or "").strip() + if not base: + return None + normalized_path = f"/{str(path or '').lstrip('/')}" + return f"{base.rstrip('/')}{normalized_path}" + + +def subscription_mini_app_install_url(settings: Settings) -> Optional[str]: + """Return the personal embedded install guide URL.""" + return subscription_mini_app_path_url(settings, "/install") + + +def subscription_public_install_url(settings: Settings, share_token: str) -> Optional[str]: + """Return the public install guide URL for a normalized share token.""" + token = normalize_install_share_token(share_token) + base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", None) or "").strip() + if not token or not base: + return None + parts = urlsplit(base) + if parts.scheme and parts.netloc: + public_base = urlunsplit((parts.scheme, parts.netloc, "", "", "")) + else: + public_base = base.rstrip("/") + return f"{public_base.rstrip('/')}/s/{quote(token)}" diff --git a/backend/config/settings.py b/backend/config/settings.py index d003055..6bf2a76 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -327,6 +327,13 @@ class Settings(BaseSettings): default=True, description="Show embedded install instructions inside the subscription Mini App.", ) + SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED: bool = Field( + default=False, + description=( + "Open Mini App install guides from Telegram bot connect buttons and show public " + "install guide share links." + ), + ) SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED: bool = Field( default=True, description="Use Remnawave Panel Subscription Page config for embedded guides when available.", diff --git a/locales/en.json b/locales/en.json index 48c154c..4f49d1b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -50,6 +50,8 @@ "yookassa_autopay_charge_initiated": "Charge request sent to the selected card. We'll notify you once the payment completes.", "back_to_payment_methods_button": "⬅️ Back", "connect_button": "🔗 Connect", + "install_guide_share_button": "🔗 Share install guide", + "install_guide_share_link_line": "\n\nInstall guide for sharing:\n{install_share_link}", "cancel_button": "❌ Cancel", "devices_button": "📱 My Devices ({current_devices}/{max_devices})", "my_devices_details": "📱 My Devices ({current_devices}/{max_devices})\n\n{devices}\n\nYou can disconnect a device by selecting it from the list below.\n
Note: If you disconnect a device, it will be automatically connected again when you use it next. Before deleting, make sure you have deleted the subscription from the application.
", @@ -1618,6 +1620,8 @@ "admin_settings_section_subscription_guides": "Install guides", "admin_settings_field_subscription_guides_enabled_label": "Embedded install guides", "admin_settings_field_subscription_guides_enabled_description": "Open install instructions inside the Web App instead of an external connect page.", + "admin_settings_field_subscription_guides_bot_menu_enabled_label": "Open install guides from bot", + "admin_settings_field_subscription_guides_bot_menu_enabled_description": "Use the Telegram Mini App install screen for bot connect buttons and show public install guide links.", "admin_settings_field_subscription_page_config_panel_enabled_label": "Use Remnawave Panel config", "admin_settings_field_subscription_page_config_panel_enabled_description": "Fetch Subscription Page config from Remnawave Panel by the user's subscription short UUID.", "admin_settings_field_subscription_page_config_json_override_enabled_label": "Enable admin JSON override", diff --git a/locales/ru.json b/locales/ru.json index f77c31f..aa2014d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -50,6 +50,8 @@ "yookassa_autopay_charge_initiated": "Запрос на списание с выбранной карты отправлен. Сообщим, как только платёж завершится.", "back_to_payment_methods_button": "⬅️ Назад", "connect_button": "🔗 Подключиться", + "install_guide_share_button": "🔗 Поделиться инструкцией", + "install_guide_share_link_line": "\n\nИнструкция для передачи:\n{install_share_link}", "devices_button": "📱 Мои устройства ({current_devices}/{max_devices})", "my_devices_details": "📱 Список ваших устройств ({current_devices}/{max_devices})\n\n{devices}\n\nВы можете отключить устройство, выбрав его в списке ниже.\n
Примечание: Если вы отключили устройство, оно будет автоматически подключено заново при следующем использовании. Перед удалением убедитесь, что вы удалили подписку из приложения.
", "no_devices_details_found_message": "📱 Список ваших устройств\n\nУ вас пока нет устройств.\nВам доступно {max_devices} устройств. Подключить их можно через кнопку \"🔗 Подключиться\" в меню подписки.", @@ -1618,6 +1620,8 @@ "admin_settings_section_subscription_guides": "Инструкции подключения", "admin_settings_field_subscription_guides_enabled_label": "Встроенные инструкции подключения", "admin_settings_field_subscription_guides_enabled_description": "Открывать инструкции прямо внутри Web App вместо внешней страницы подключения.", + "admin_settings_field_subscription_guides_bot_menu_enabled_label": "Открывать инструкции из бота", + "admin_settings_field_subscription_guides_bot_menu_enabled_description": "Открывать экран установки в Telegram Mini App для кнопок подключения в боте и показывать публичные ссылки на инструкцию.", "admin_settings_field_subscription_page_config_panel_enabled_label": "Использовать конфиг Remnawave Panel", "admin_settings_field_subscription_page_config_panel_enabled_description": "Брать Subscription Page config из Remnawave Panel по short UUID подписки пользователя.", "admin_settings_field_subscription_page_config_json_override_enabled_label": "Включить JSON-override из админки", diff --git a/tests/test_admin_settings_manifest_i18n.py b/tests/test_admin_settings_manifest_i18n.py index 39d55e0..2232ac7 100644 --- a/tests/test_admin_settings_manifest_i18n.py +++ b/tests/test_admin_settings_manifest_i18n.py @@ -24,6 +24,7 @@ SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS = ( SUBSCRIPTION_GUIDE_SETTINGS = ( "SUBSCRIPTION_GUIDES_ENABLED", + "SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED", "SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED", "SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED", "SUBSCRIPTION_PAGE_CONFIG_PATH", diff --git a/tests/test_mini_app_url.py b/tests/test_mini_app_url.py index 29109ff..1f551f4 100644 --- a/tests/test_mini_app_url.py +++ b/tests/test_mini_app_url.py @@ -1,6 +1,12 @@ import unittest -from bot.utils.mini_app_url import append_query_params, subscription_mini_app_topup_url +from bot.utils.mini_app_url import ( + append_query_params, + subscription_mini_app_install_url, + subscription_mini_app_path_url, + subscription_mini_app_topup_url, + subscription_public_install_url, +) from config.settings import Settings @@ -39,3 +45,33 @@ class MiniAppUrlTests(unittest.TestCase): subscription_mini_app_topup_url(s, "regular"), "https://app.example.com/webapp?topup=regular", ) + + def test_subscription_mini_app_path_url(self): + s = Settings( + _env_file=None, + BOT_TOKEN="x", + POSTGRES_USER="u", + POSTGRES_PASSWORD="p", + SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp/", + ) + self.assertEqual( + subscription_mini_app_path_url(s, "/install"), + "https://app.example.com/webapp/install", + ) + self.assertEqual( + subscription_mini_app_install_url(s), + "https://app.example.com/webapp/install", + ) + + def test_subscription_public_install_url_uses_origin(self): + s = Settings( + _env_file=None, + BOT_TOKEN="x", + POSTGRES_USER="u", + POSTGRES_PASSWORD="p", + SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp", + ) + self.assertEqual( + subscription_public_install_url(s, "8f559061460e8fede78ef18dce887236"), + "https://app.example.com/s/8f559061460e8fede78ef18dce887236", + ) diff --git a/tests/test_settings.py b/tests/test_settings.py index aec7a25..95c1302 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -41,6 +41,7 @@ class SettingsTests(unittest.TestCase): ) self.assertTrue(settings.SUBSCRIPTION_GUIDES_ENABLED) + self.assertFalse(settings.SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED) self.assertTrue(settings.SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED) self.assertFalse(settings.SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED) self.assertEqual( diff --git a/tests/test_user_bot_menu.py b/tests/test_user_bot_menu.py index 32657e2..f5e6ebb 100644 --- a/tests/test_user_bot_menu.py +++ b/tests/test_user_bot_menu.py @@ -8,6 +8,7 @@ from bot.handlers.user import referral from bot.handlers.user.subscription.core import _with_subscription_purchase_description from bot.keyboards.inline.user_keyboards import ( get_bot_interface_inline_keyboard, + get_connect_and_main_keyboard, get_information_links_keyboard, get_language_selection_keyboard, get_main_menu_inline_keyboard, @@ -41,6 +42,13 @@ class UserBotMenuTests(unittest.TestCase): TERMS_OF_SERVICE_URL="", TRIAL_ENABLED=True, SERVER_STATUS_URL="", + SUBSCRIPTION_GUIDES_ENABLED=True, + SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED=False, + SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED=True, + SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED=False, + SUBSCRIPTION_PAGE_CONFIG_JSON="", + PANEL_API_URL="https://panel.example.com", + PANEL_API_KEY="token", ) def _callback_data(self, markup): @@ -70,6 +78,38 @@ class UserBotMenuTests(unittest.TestCase): self.assertIn("main_action:bot_info", callbacks) self.assertIn("main_action:back_to_main", callbacks) + def test_connect_keyboard_uses_subscription_url_when_bot_guides_disabled(self): + markup = get_connect_and_main_keyboard( + "en", + self.i18n, + self.settings, + "https://sb.example.com/user", + connect_button_url=None, + ) + + self.assertEqual(markup.inline_keyboard[0][0].url, "https://sb.example.com/user") + self.assertIsNone(markup.inline_keyboard[0][0].web_app) + + def test_connect_keyboard_opens_install_guide_when_bot_guides_enabled(self): + self.settings.SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED = True + markup = get_connect_and_main_keyboard( + "en", + self.i18n, + self.settings, + "https://sb.example.com/user", + install_share_url="https://app.example.com/s/8f559061460e8fede78ef18dce887236", + ) + + self.assertIsNone(markup.inline_keyboard[0][0].url) + self.assertEqual( + markup.inline_keyboard[0][0].web_app.url, + "https://app.example.com/install", + ) + self.assertEqual( + markup.inline_keyboard[1][0].url, + "https://app.example.com/s/8f559061460e8fede78ef18dce887236", + ) + def test_nested_bot_menu_keyboards_can_target_bot_interface_back(self): subscription_markup = get_subscription_options_keyboard( {1: 100},