diff --git a/.env.example b/.env.example index 93e8a4ca..77f05953 100644 --- a/.env.example +++ b/.env.example @@ -290,6 +290,10 @@ CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +MINIAPP_SERVICE_NAME_EN=Bedolaga VPN +MINIAPP_SERVICE_NAME_RU=Bedolaga VPN +MINIAPP_SERVICE_DESCRIPTION_EN=Secure & Fast Connection +MINIAPP_SERVICE_DESCRIPTION_RU=Безопасное и быстрое подключение # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false diff --git a/README.md b/README.md index bbddbaa9..c1fdb9f1 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,10 @@ CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +MINIAPP_SERVICE_NAME_EN=Bedolaga VPN +MINIAPP_SERVICE_NAME_RU=Bedolaga VPN +MINIAPP_SERVICE_DESCRIPTION_EN=Secure & Fast Connection +MINIAPP_SERVICE_DESCRIPTION_RU=Безопасное и быстрое подключение # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false diff --git a/app/config.py b/app/config.py index a2cbaf80..773f4f45 100644 --- a/app/config.py +++ b/app/config.py @@ -213,6 +213,10 @@ class Settings(BaseSettings): CONNECT_BUTTON_MODE: str = "guide" MINIAPP_CUSTOM_URL: str = "" + MINIAPP_SERVICE_NAME_EN: str = "Bedolaga VPN" + MINIAPP_SERVICE_NAME_RU: str = "Bedolaga VPN" + MINIAPP_SERVICE_DESCRIPTION_EN: str = "Secure & Fast Connection" + MINIAPP_SERVICE_DESCRIPTION_RU: str = "Безопасное и быстрое подключение" CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED: bool = False HAPP_CRYPTOLINK_REDIRECT_TEMPLATE: Optional[str] = None HAPP_DOWNLOAD_LINK_IOS: Optional[str] = None @@ -518,6 +522,34 @@ class Settings(BaseSettings): def is_deep_links_enabled(self) -> bool: return self.ENABLE_DEEP_LINKS + + def get_miniapp_branding(self) -> Dict[str, Dict[str, Optional[str]]]: + def _clean(value: Optional[str]) -> Optional[str]: + if value is None: + return None + value_str = str(value).strip() + return value_str or None + + name_en = _clean(self.MINIAPP_SERVICE_NAME_EN) + name_ru = _clean(self.MINIAPP_SERVICE_NAME_RU) + desc_en = _clean(self.MINIAPP_SERVICE_DESCRIPTION_EN) + desc_ru = _clean(self.MINIAPP_SERVICE_DESCRIPTION_RU) + + default_name = name_en or name_ru or "RemnaWave VPN" + default_description = desc_en or desc_ru or "Secure & Fast Connection" + + return { + "service_name": { + "default": default_name, + "en": name_en, + "ru": name_ru, + }, + "service_description": { + "default": default_description, + "en": desc_en, + "ru": desc_ru, + }, + } def get_app_config_cache_ttl(self) -> int: return self.APP_CONFIG_CACHE_TTL diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 0df8e252..1ce9432a 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -89,6 +89,7 @@ class BotConfigurationService: "HAPP": "🅷 Happ настройки", "SKIP": "⚡ Быстрый старт", "ADDITIONAL": "📱 Приложения и DeepLinks", + "MINIAPP": "📱 Mini App", "DATABASE": "🗄️ Режим БД", "POSTGRES": "🐘 PostgreSQL", "SQLITE": "💾 SQLite", @@ -189,6 +190,7 @@ class BotConfigurationService: "CONNECT_BUTTON_HAPP": "HAPP", "HAPP_": "HAPP", "SKIP_": "SKIP", + "MINIAPP_": "MINIAPP", "MONITORING_": "MONITORING", "NOTIFICATION_": "NOTIFICATIONS", "SERVER_STATUS": "SERVER", diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 244c6d2e..64bc7bd4 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -383,5 +383,6 @@ async def get_subscription_details( else None, subscription_type="trial" if subscription.is_trial else "paid", autopay_enabled=bool(subscription.autopay_enabled), + branding=settings.get_miniapp_branding(), ) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 4c42e230..a0d5cb1a 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -6,6 +6,11 @@ from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field +class MiniAppBranding(BaseModel): + service_name: Dict[str, Optional[str]] = Field(default_factory=dict) + service_description: Dict[str, Optional[str]] = Field(default_factory=dict) + + class MiniAppSubscriptionRequest(BaseModel): init_data: str = Field(..., alias="initData") @@ -86,4 +91,5 @@ class MiniAppSubscriptionResponse(BaseModel): promo_group: Optional[MiniAppPromoGroup] = None subscription_type: str autopay_enabled: bool = False + branding: Optional[MiniAppBranding] = None diff --git a/miniapp/index.html b/miniapp/index.html index 7858021c..b9c7da7b 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -892,6 +892,72 @@ } }; + function applyBrandingOverrides(branding) { + if (!branding || typeof branding !== 'object') { + return; + } + + const { + service_name: rawServiceName = {}, + service_description: rawServiceDescription = {} + } = branding; + + function normalizeMap(map) { + const normalized = {}; + Object.entries(map || {}).forEach(([lang, value]) => { + if (typeof value !== 'string') { + return; + } + const trimmed = value.trim(); + if (!trimmed) { + return; + } + normalized[lang.toLowerCase()] = trimmed; + }); + return normalized; + } + + function applyKey(key, map) { + const normalized = normalizeMap(map); + if (!Object.keys(normalized).length) { + return; + } + + const defaultValue = normalized.default + || normalized.en + || normalized.ru + || null; + + const languages = new Set( + Object.keys(translations).map(lang => lang.toLowerCase()) + ); + + Object.keys(normalized).forEach(lang => { + if (lang !== 'default') { + languages.add(lang); + } + }); + + languages.forEach(lang => { + const value = Object.prototype.hasOwnProperty.call(normalized, lang) + ? normalized[lang] + : defaultValue; + if (!value) { + return; + } + const targetLang = lang.toLowerCase(); + if (!translations[targetLang]) { + translations[targetLang] = {}; + } + translations[targetLang][key] = value; + }); + } + + applyKey('app.name', rawServiceName); + applyKey('app.title', rawServiceName); + applyKey('app.subtitle', rawServiceDescription); + } + let userData = null; let appsConfig = {}; let currentPlatform = 'android'; @@ -1100,6 +1166,9 @@ userData = await response.json(); userData.subscriptionUrl = userData.subscription_url || null; userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; + if (userData.branding) { + applyBrandingOverrides(userData.branding); + } const responseLanguage = resolveLanguage(userData?.user?.language); if (responseLanguage && !languageLockedByUser) {