feat: allow configuring miniapp branding

This commit is contained in:
Egor
2025-10-01 03:58:34 +03:00
parent 9c44c3ebd0
commit 0a789d3bd1
8 changed files with 55 additions and 1 deletions
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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",
}
+12
View File
@@ -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,
+2
View File
@@ -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
+1
View File
@@ -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
+32 -1
View File
@@ -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;