From 0a789d3bd1cf3ded6b1be17818cfa370c72fc2c3 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 1 Oct 2025 03:58:34 +0300 Subject: [PATCH] feat: allow configuring miniapp branding --- .env.example | 2 ++ README.md | 2 ++ app/config.py | 2 ++ app/services/system_settings_service.py | 2 ++ app/webapi/routes/miniapp.py | 12 +++++++++ app/webapi/schemas/miniapp.py | 2 ++ docs/miniapp-setup.md | 1 + miniapp/index.html | 33 ++++++++++++++++++++++++- 8 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 93e8a4ca..2bb28ef7 100644 --- a/.env.example +++ b/.env.example @@ -290,6 +290,8 @@ CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +MINIAPP_SERVICE_NAME=RemnaWave VPN +MINIAPP_SERVICE_DESCRIPTION=Secure & Fast Connection # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false diff --git a/README.md b/README.md index bbddbaa9..60e4b6ce 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,8 @@ CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +MINIAPP_SERVICE_NAME=RemnaWave VPN +MINIAPP_SERVICE_DESCRIPTION=Secure & Fast Connection # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false diff --git a/app/config.py b/app/config.py index a2cbaf80..74c0247f 100644 --- a/app/config.py +++ b/app/config.py @@ -212,6 +212,8 @@ class Settings(BaseSettings): PAL24_CARD_BUTTON_TEXT: Optional[str] = None CONNECT_BUTTON_MODE: str = "guide" + MINIAPP_SERVICE_NAME: str = "RemnaWave VPN" + MINIAPP_SERVICE_DESCRIPTION: str = "Secure & Fast Connection" MINIAPP_CUSTOM_URL: str = "" CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED: bool = False HAPP_CRYPTOLINK_REDIRECT_TEMPLATE: Optional[str] = None diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 0df8e252..2367bb8c 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -161,6 +161,8 @@ class BotConfigurationService: "PAYMENT_BALANCE_TEMPLATE": "PAYMENT", "PAYMENT_SUBSCRIPTION_TEMPLATE": "PAYMENT", "INACTIVE_USER_DELETE_MONTHS": "MONITORING", + "MINIAPP_SERVICE_NAME": "INTERFACE_BRANDING", + "MINIAPP_SERVICE_DESCRIPTION": "INTERFACE_BRANDING", "LANGUAGE_SELECTION_ENABLED": "LOCALIZATION", } diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 244c6d2e..48fd006c 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -79,6 +79,16 @@ def _status_label(status: str) -> str: return mapping.get(status, status.title()) +def _miniapp_service_name() -> str: + value = (settings.MINIAPP_SERVICE_NAME or "").strip() + return value or "RemnaWave VPN" + + +def _miniapp_service_description() -> str: + value = (settings.MINIAPP_SERVICE_DESCRIPTION or "").strip() + return value or "Secure & Fast Connection" + + def _parse_datetime_string(value: Optional[str]) -> Optional[str]: if not value: return None @@ -359,6 +369,8 @@ async def get_subscription_details( ) return MiniAppSubscriptionResponse( + service_name=_miniapp_service_name(), + service_description=_miniapp_service_description(), subscription_id=subscription.id, remnawave_short_uuid=subscription.remnawave_short_uuid, user=response_user, diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 4c42e230..2dc7cc9c 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -64,6 +64,8 @@ class MiniAppTransaction(BaseModel): class MiniAppSubscriptionResponse(BaseModel): success: bool = True + service_name: Optional[str] = None + service_description: Optional[str] = None subscription_id: int remnawave_short_uuid: Optional[str] = None user: MiniAppSubscriptionUser diff --git a/docs/miniapp-setup.md b/docs/miniapp-setup.md index 2ab7d0e0..3d2a9faf 100644 --- a/docs/miniapp-setup.md +++ b/docs/miniapp-setup.md @@ -30,6 +30,7 @@ - `WEB_API_ALLOWED_ORIGINS` должен содержать домен, с которого будет открываться мини-приложение. - `WEB_API_DEFAULT_TOKEN` создаёт bootstrap-токен для запросов от страницы. Его можно заменить на токен, созданный через `POST /tokens`. 3. Если используете RemnaWave, убедитесь, что заданы `REMNAWAVE_API_URL` и `REMNAWAVE_API_KEY`, чтобы в мини-приложении отображались дополнительные ссылки подписки. +4. Настройте брендирование заголовка при необходимости: `MINIAPP_SERVICE_NAME` и `MINIAPP_SERVICE_DESCRIPTION` обновят название сервиса и описание в шапке мини-приложения. ## 3. Запуск административного API diff --git a/miniapp/index.html b/miniapp/index.html index 7858021c..e203c8f1 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -892,12 +892,40 @@ } }; + const DEFAULT_BRANDING = { + name: translations.en?.['app.name'] || 'RemnaWave VPN', + description: translations.en?.['app.subtitle'] || 'Secure & Fast Connection' + }; + let userData = null; let appsConfig = {}; let currentPlatform = 'android'; let preferredLanguage = 'en'; let languageLockedByUser = false; let currentErrorState = null; + let branding = { ...DEFAULT_BRANDING }; + + function applyBrandingConfig(payload) { + const nameRaw = typeof payload?.service_name === 'string' + ? payload.service_name.trim() + : ''; + const descriptionRaw = typeof payload?.service_description === 'string' + ? payload.service_description.trim() + : ''; + + branding = { + name: nameRaw || DEFAULT_BRANDING.name, + description: descriptionRaw || DEFAULT_BRANDING.description + }; + + SUPPORTED_LANGUAGES.forEach(lang => { + if (!translations[lang]) { + translations[lang] = {}; + } + translations[lang]['app.name'] = branding.name; + translations[lang]['app.subtitle'] = branding.description; + }); + } function resolveLanguage(lang) { if (!lang) { @@ -971,7 +999,8 @@ } function applyTranslations() { - document.title = t('app.title'); + const baseTitle = t('app.title'); + document.title = branding?.name ? `${branding.name} — ${baseTitle}` : baseTitle; document.documentElement.setAttribute('lang', preferredLanguage); document.querySelectorAll('[data-i18n]').forEach(element => { const key = element.getAttribute('data-i18n'); @@ -1101,6 +1130,8 @@ userData.subscriptionUrl = userData.subscription_url || null; userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; + applyBrandingConfig(userData); + const responseLanguage = resolveLanguage(userData?.user?.language); if (responseLanguage && !languageLockedByUser) { preferredLanguage = responseLanguage;