diff --git a/.env.example b/.env.example index 747f289c..cc6f5283 100644 --- a/.env.example +++ b/.env.example @@ -288,6 +288,10 @@ MINIAPP_CUSTOM_URL= # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false +# Включить кнопку "Подключиться" с редиректом на happ:// ссылку +CONNECT_BUTTON_HAPP_REDIRECT_ENABLED=false +# Шаблон редиректа, поддерживает плейсхолдер {subscription_link}. Если плейсхолдер не указан, ссылка будет добавлена в конец +CONNECT_BUTTON_HAPP_REDIRECT_TEMPLATE= HAPP_DOWNLOAD_LINK_IOS= HAPP_DOWNLOAD_LINK_ANDROID= HAPP_DOWNLOAD_LINK_MACOS= diff --git a/README.md b/README.md index fb7f037d..c803586f 100644 --- a/README.md +++ b/README.md @@ -529,6 +529,9 @@ MINIAPP_CUSTOM_URL= # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false +CONNECT_BUTTON_HAPP_REDIRECT_ENABLED=false +# Шаблон редиректа, поддерживает плейсхолдер {subscription_link}. Если плейсхолдер не указан, ссылка добавится в конец +CONNECT_BUTTON_HAPP_REDIRECT_TEMPLATE= HAPP_DOWNLOAD_LINK_IOS= HAPP_DOWNLOAD_LINK_ANDROID= HAPP_DOWNLOAD_LINK_MACOS= diff --git a/app/config.py b/app/config.py index 6d16054c..4a829f47 100644 --- a/app/config.py +++ b/app/config.py @@ -5,6 +5,8 @@ import html from collections import defaultdict from datetime import time from typing import List, Optional, Union, Dict +from urllib.parse import quote + from pydantic_settings import BaseSettings from pydantic import field_validator, Field from pathlib import Path @@ -210,6 +212,8 @@ class Settings(BaseSettings): CONNECT_BUTTON_MODE: str = "guide" MINIAPP_CUSTOM_URL: str = "" CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED: bool = False + CONNECT_BUTTON_HAPP_REDIRECT_ENABLED: bool = False + CONNECT_BUTTON_HAPP_REDIRECT_TEMPLATE: Optional[str] = None HAPP_DOWNLOAD_LINK_IOS: Optional[str] = None HAPP_DOWNLOAD_LINK_ANDROID: Optional[str] = None HAPP_DOWNLOAD_LINK_MACOS: Optional[str] = None @@ -573,6 +577,33 @@ class Settings(BaseSettings): link = links.get(platform_key) return link if link else None + def get_happ_redirect_url(self, subscription_link: Optional[str]) -> Optional[str]: + if not subscription_link: + return None + + if not self.is_happ_cryptolink_mode(): + return None + + if not self.CONNECT_BUTTON_HAPP_REDIRECT_ENABLED: + return None + + template = (self.CONNECT_BUTTON_HAPP_REDIRECT_TEMPLATE or "").strip() + if not template: + return None + + encoded_link = quote(subscription_link, safe="") + + if "{subscription_link}" in template: + try: + return template.format(subscription_link=encoded_link) + except Exception as exc: # pragma: no cover - safety fallback + logging.getLogger(__name__).warning( + "Failed to format Happ redirect template: %s", exc + ) + return None + + return f"{template}{encoded_link}" + def is_maintenance_mode(self) -> bool: return self.MAINTENANCE_MODE diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py index e6dab937..cdfbea65 100644 --- a/app/handlers/subscription.py +++ b/app/handlers/subscription.py @@ -4611,12 +4611,22 @@ async def handle_open_subscription_link( return if settings.is_happ_cryptolink_mode(): + redirect_link = settings.get_happ_redirect_url(subscription_link) happ_message = ( texts.t( "SUBSCRIPTION_HAPP_OPEN_TITLE", "🔗 Подключение через Happ", ) + "\n\n" + + ( + texts.t( + "SUBSCRIPTION_HAPP_OPEN_REDIRECT_HINT", + "👇 Нажмите кнопку ниже, чтобы открыть Happ напрямую.", + ) + + "\n\n" + if redirect_link + else "" + ) + texts.t( "SUBSCRIPTION_HAPP_OPEN_LINK", "🔓 Открыть ссылку в Happ", @@ -4628,7 +4638,11 @@ async def handle_open_subscription_link( ).format(subscription_link=subscription_link) ) - keyboard = get_happ_cryptolink_keyboard(subscription_link, db_user.language) + keyboard = get_happ_cryptolink_keyboard( + subscription_link, + db_user.language, + redirect_link=redirect_link, + ) await callback.message.answer( happ_message, diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 01e79ce4..bb432f38 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -257,46 +257,65 @@ def get_happ_download_button_row(texts) -> Optional[List[InlineKeyboardButton]]: def get_happ_cryptolink_keyboard( subscription_link: str, language: str = DEFAULT_LANGUAGE, + redirect_link: Optional[str] = None, ) -> InlineKeyboardMarkup: texts = get_texts(language) - buttons = [ - [ - InlineKeyboardButton( - text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), - url=subscription_link, - ) - ], + buttons: list[list[InlineKeyboardButton]] = [] + + if redirect_link: + buttons.append( + [ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=redirect_link, + ) + ] + ) + + buttons.extend( [ InlineKeyboardButton( text=texts.t("HAPP_PLATFORM_IOS", "🍎 iOS"), callback_data="happ_download_ios", ) - ], + ] + ) + + buttons.append( [ InlineKeyboardButton( text=texts.t("HAPP_PLATFORM_ANDROID", "🤖 Android"), callback_data="happ_download_android", ) - ], + ] + ) + + buttons.append( [ InlineKeyboardButton( text=texts.t("HAPP_PLATFORM_MACOS", "🖥️ Mac OS"), callback_data="happ_download_macos", ) - ], + ] + ) + + buttons.append( [ InlineKeyboardButton( text=texts.t("HAPP_PLATFORM_WINDOWS", "💻 Windows"), callback_data="happ_download_windows", ) - ], + ] + ) + + buttons.append( [ InlineKeyboardButton( text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu", ) - ], - ] + ] + ) return InlineKeyboardMarkup(inline_keyboard=buttons) diff --git a/locales/en.json b/locales/en.json index 84a68c61..d1244dda 100644 --- a/locales/en.json +++ b/locales/en.json @@ -197,7 +197,6 @@ "NO_TICKETS_ADMIN": "No tickets to display.", "ADMIN_TICKETS_TITLE": "🎫 All support tickets:", "ADMIN_TICKET_REPLY_INPUT": "Enter support reply:", - "ADMIN_TICKET_REPLY_SENT": "✅ Reply sent!", "TICKET_MARKED_ANSWERED": "✅ Ticket marked as answered.", "TICKET_UPDATE_ERROR": "❌ Error updating ticket.", @@ -519,5 +518,6 @@ "NOTIFY_PROMPT_SECOND_HOURS": "Enter the number of hours the discount is active (1-168):", "NOTIFY_PROMPT_THIRD_PERCENT": "Enter a new discount percentage for the late offer (0-100):", "NOTIFY_PROMPT_THIRD_HOURS": "Enter the number of hours the late discount is active (1-168):", - "NOTIFY_PROMPT_THIRD_DAYS": "After how many days without a subscription should we send the offer? (minimum 2):" + "NOTIFY_PROMPT_THIRD_DAYS": "After how many days without a subscription should we send the offer? (minimum 2):", + "SUBSCRIPTION_HAPP_OPEN_REDIRECT_HINT": "👇 Tap the button below to open Happ directly." } diff --git a/locales/ru.json b/locales/ru.json index b1498e4e..d2b71326 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -63,7 +63,6 @@ "NO_TICKETS_ADMIN": "Нет тикетов для отображения.", "ADMIN_TICKETS_TITLE": "🎫 Все тикеты поддержки:", "ADMIN_TICKET_REPLY_INPUT": "Введите ответ от поддержки:", - "ADMIN_TICKET_REPLY_SENT": "✅ Ответ отправлен!", "TICKET_MARKED_ANSWERED": "✅ Тикет отмечен как отвеченный.", "TICKET_UPDATE_ERROR": "❌ Ошибка при обновлении тикета.", @@ -519,5 +518,6 @@ "NOTIFY_PROMPT_SECOND_HOURS": "Введите количество часов действия скидки (1-168):", "NOTIFY_PROMPT_THIRD_PERCENT": "Введите новый процент скидки для позднего предложения (0-100):", "NOTIFY_PROMPT_THIRD_HOURS": "Введите количество часов действия скидки (1-168):", - "NOTIFY_PROMPT_THIRD_DAYS": "Через сколько дней после истечения отправлять предложение? (минимум 2):" + "NOTIFY_PROMPT_THIRD_DAYS": "Через сколько дней после истечения отправлять предложение? (минимум 2):", + "SUBSCRIPTION_HAPP_OPEN_REDIRECT_HINT": "👇 Нажмите кнопку ниже, чтобы открыть Happ напрямую." }