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 напрямую."
}