diff --git a/.env.example b/.env.example index 77f05953..439962de 100644 --- a/.env.example +++ b/.env.example @@ -294,6 +294,7 @@ MINIAPP_SERVICE_NAME_EN=Bedolaga VPN MINIAPP_SERVICE_NAME_RU=Bedolaga VPN MINIAPP_SERVICE_DESCRIPTION_EN=Secure & Fast Connection MINIAPP_SERVICE_DESCRIPTION_RU=Безопасное и быстрое подключение +MINIAPP_PURCHASE_URL= # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false diff --git a/README.md b/README.md index c1fdb9f1..8c688eaf 100644 --- a/README.md +++ b/README.md @@ -539,6 +539,7 @@ MINIAPP_SERVICE_NAME_EN=Bedolaga VPN MINIAPP_SERVICE_NAME_RU=Bedolaga VPN MINIAPP_SERVICE_DESCRIPTION_EN=Secure & Fast Connection MINIAPP_SERVICE_DESCRIPTION_RU=Безопасное и быстрое подключение +MINIAPP_PURCHASE_URL= # Параметры режима happ_cryptolink CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false diff --git a/app/config.py b/app/config.py index 773f4f45..a4057c24 100644 --- a/app/config.py +++ b/app/config.py @@ -217,6 +217,7 @@ class Settings(BaseSettings): MINIAPP_SERVICE_NAME_RU: str = "Bedolaga VPN" MINIAPP_SERVICE_DESCRIPTION_EN: str = "Secure & Fast Connection" MINIAPP_SERVICE_DESCRIPTION_RU: str = "Безопасное и быстрое подключение" + MINIAPP_PURCHASE_URL: str = "" CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED: bool = False HAPP_CRYPTOLINK_REDIRECT_TEMPLATE: Optional[str] = None HAPP_DOWNLOAD_LINK_IOS: Optional[str] = None diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 64bc7bd4..b6aca0a0 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -24,6 +24,7 @@ from app.utils.telegram_webapp import ( from ..dependencies import get_db_session from ..schemas.miniapp import ( + MiniAppConfigResponse, MiniAppConnectedServer, MiniAppDevice, MiniAppPromoGroup, @@ -63,6 +64,11 @@ def _format_limit_label(limit: Optional[int]) -> str: return f"{limit} GB" +def _get_purchase_url() -> Optional[str]: + value = (settings.MINIAPP_PURCHASE_URL or "").strip() + return value or None + + def _bytes_to_gb(bytes_value: Optional[int]) -> float: if not bytes_value: return 0.0 @@ -258,6 +264,11 @@ async def _load_subscription_links( return payload +@router.get("/config", response_model=MiniAppConfigResponse) +async def get_miniapp_config() -> MiniAppConfigResponse: + return MiniAppConfigResponse(purchase_url=_get_purchase_url()) + + @router.post("/subscription", response_model=MiniAppSubscriptionResponse) async def get_subscription_details( payload: MiniAppSubscriptionRequest, @@ -364,6 +375,7 @@ async def get_subscription_details( user=response_user, subscription_url=subscription_url, subscription_crypto_link=subscription_crypto_link, + purchase_url=_get_purchase_url(), links=links, ss_conf_links=ss_conf_links, connected_squads=connected_squads, diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index a0d5cb1a..3e644aa7 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -11,6 +11,10 @@ class MiniAppBranding(BaseModel): service_description: Dict[str, Optional[str]] = Field(default_factory=dict) +class MiniAppConfigResponse(BaseModel): + purchase_url: Optional[str] = None + + class MiniAppSubscriptionRequest(BaseModel): init_data: str = Field(..., alias="initData") @@ -74,6 +78,7 @@ class MiniAppSubscriptionResponse(BaseModel): user: MiniAppSubscriptionUser subscription_url: Optional[str] = None subscription_crypto_link: Optional[str] = None + purchase_url: Optional[str] = None links: List[str] = Field(default_factory=list) ss_conf_links: Dict[str, str] = Field(default_factory=dict) connected_squads: List[str] = Field(default_factory=list) diff --git a/miniapp/index.html b/miniapp/index.html index fe03f136..b84030cd 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -314,6 +314,19 @@ line-height: 1.6; } + .error-actions { + margin-top: 24px; + display: flex; + justify-content: center; + } + + .error-actions .btn { + width: auto; + min-width: 200px; + padding-left: 32px; + padding-right: 32px; + } + /* Cards */ .card { background: var(--bg-secondary); @@ -758,6 +771,7 @@ overflow: hidden; text-transform: uppercase; letter-spacing: 0.5px; + text-decoration: none; } .btn::before { @@ -1186,6 +1200,9 @@
⚠️
Subscription Not Found
Please contact support to activate your subscription
+ @@ -1521,6 +1538,7 @@ 'info.autopay': 'Auto-pay', 'button.connect.default': 'Connect to VPN', 'button.connect.happ': 'Connect', + 'button.buy_subscription': 'Buy subscription', 'button.copy': 'Copy subscription link', 'card.balance.title': 'Balance', 'card.history.title': 'Transaction History', @@ -1583,6 +1601,7 @@ 'info.autopay': 'Автоплатеж', 'button.connect.default': 'Подключиться к VPN', 'button.connect.happ': 'Подключиться', + 'button.buy_subscription': 'Купить подписку', 'button.copy': 'Скопировать ссылку подписки', 'card.balance.title': 'Баланс', 'card.history.title': 'История операций', @@ -1697,6 +1716,7 @@ let userData = null; let appsConfig = {}; + let miniAppConfig = {}; let currentPlatform = 'android'; let preferredLanguage = 'en'; let languageLockedByUser = false; @@ -1773,6 +1793,38 @@ textElement.textContent = message; } + function getPurchaseUrl() { + const raw = miniAppConfig?.purchase_url + || miniAppConfig?.purchaseUrl + || userData?.purchase_url + || userData?.purchaseUrl + || ''; + return typeof raw === 'string' ? raw.trim() : ''; + } + + function updateBuySubscriptionButtonVisibility(showInErrorState = false) { + const actions = document.getElementById('errorActions'); + const button = document.getElementById('buySubscriptionBtn'); + if (!actions || !button) { + return; + } + + const url = getPurchaseUrl(); + const shouldShow = Boolean(showInErrorState && url); + + if (shouldShow) { + button.href = url; + button.setAttribute('target', '_blank'); + button.setAttribute('rel', 'noopener'); + } else { + button.removeAttribute('href'); + button.removeAttribute('target'); + button.removeAttribute('rel'); + } + + actions.classList.toggle('hidden', !shouldShow); + } + function applyTranslations() { document.title = t('app.title'); document.documentElement.setAttribute('lang', preferredLanguage); @@ -1796,7 +1848,8 @@ if (!label) { return; } - const useHappLabel = Boolean(userData?.happ_cryptolink_redirect_link); + const link = getConnectLink(); + const useHappLabel = typeof link === 'string' && link.startsWith('happ:'); const key = useHappLabel ? 'button.connect.happ' : 'button.connect.default'; label.textContent = t(key); } @@ -1867,6 +1920,7 @@ } } + await loadMiniAppConfig(); await loadAppsConfig(); const initData = tg.initData || ''; @@ -1918,6 +1972,7 @@ document.getElementById('loadingState').classList.add('hidden'); document.getElementById('mainContent').classList.remove('hidden'); + updateBuySubscriptionButtonVisibility(false); // Add animation to cards document.querySelectorAll('.card').forEach((card, index) => { @@ -1931,6 +1986,23 @@ } } + async function loadMiniAppConfig() { + try { + const response = await fetch('/miniapp/config', { cache: 'no-cache' }); + if (!response.ok) { + throw new Error(`Failed to load mini app config: ${response.status}`); + } + + const data = await response.json(); + miniAppConfig = data || {}; + } catch (error) { + console.warn('Unable to load mini app config:', error); + miniAppConfig = {}; + } finally { + updateBuySubscriptionButtonVisibility(false); + } + } + async function loadAppsConfig() { try { const response = await fetch('/app-config.json', { cache: 'no-cache' }); @@ -2456,10 +2528,6 @@ return null; } - if (userData.happ_cryptolink_redirect_link) { - return userData.happ_cryptolink_redirect_link; - } - const subscriptionUrl = getCurrentSubscriptionUrl(); if (!subscriptionUrl) { return null; @@ -2516,6 +2584,7 @@ }; updateErrorTexts(); document.getElementById('errorState').classList.remove('hidden'); + updateBuySubscriptionButtonVisibility(true); updateActionButtons(); }