Merge pull request #631 from Fr1ngg/bedolaga/add-service-name-configuration-option-d3ekaj

Add configurable miniapp branding
This commit is contained in:
Egor
2025-10-01 04:03:32 +03:00
committed by GitHub
7 changed files with 118 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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(),
)

View File

@@ -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

View File

@@ -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) {