From 4fa87804d9752002b1db9d2ca72e0a7e4cff155a Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 1 Oct 2025 05:56:51 +0300 Subject: [PATCH] Add miniapp purchase link support --- .env.example | 1 + README.md | 1 + app/config.py | 8 ++++ app/webapi/routes/miniapp.py | 13 ++++++ app/webapi/schemas/miniapp.py | 6 +++ miniapp/index.html | 82 +++++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+) diff --git a/.env.example b/.env.example index 77f05953..a15da1fc 100644 --- a/.env.example +++ b/.env.example @@ -290,6 +290,7 @@ CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +MINIAPP_SUBSCRIPTION_PURCHASE_URL= MINIAPP_SERVICE_NAME_EN=Bedolaga VPN MINIAPP_SERVICE_NAME_RU=Bedolaga VPN MINIAPP_SERVICE_DESCRIPTION_EN=Secure & Fast Connection diff --git a/README.md b/README.md index c1fdb9f1..c97c466d 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,7 @@ CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +MINIAPP_SUBSCRIPTION_PURCHASE_URL= MINIAPP_SERVICE_NAME_EN=Bedolaga VPN MINIAPP_SERVICE_NAME_RU=Bedolaga VPN MINIAPP_SERVICE_DESCRIPTION_EN=Secure & Fast Connection diff --git a/app/config.py b/app/config.py index 773f4f45..7ab0198b 100644 --- a/app/config.py +++ b/app/config.py @@ -213,6 +213,7 @@ class Settings(BaseSettings): CONNECT_BUTTON_MODE: str = "guide" MINIAPP_CUSTOM_URL: str = "" + MINIAPP_SUBSCRIPTION_PURCHASE_URL: str = "" MINIAPP_SERVICE_NAME_EN: str = "Bedolaga VPN" MINIAPP_SERVICE_NAME_RU: str = "Bedolaga VPN" MINIAPP_SERVICE_DESCRIPTION_EN: str = "Secure & Fast Connection" @@ -550,6 +551,13 @@ class Settings(BaseSettings): "ru": desc_ru, }, } + + def get_miniapp_purchase_url(self) -> Optional[str]: + value = getattr(self, "MINIAPP_SUBSCRIPTION_PURCHASE_URL", "") + if value is None: + return None + purchase_url = str(value).strip() + return purchase_url or None def get_app_config_cache_ttl(self) -> int: return self.APP_CONFIG_CACHE_TTL diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 64bc7bd4..eded3fa6 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -24,6 +24,8 @@ from app.utils.telegram_webapp import ( from ..dependencies import get_db_session from ..schemas.miniapp import ( + MiniAppBranding, + MiniAppConfigResponse, MiniAppConnectedServer, MiniAppDevice, MiniAppPromoGroup, @@ -258,6 +260,16 @@ async def _load_subscription_links( return payload +@router.get("/config", response_model=MiniAppConfigResponse) +async def get_miniapp_config() -> MiniAppConfigResponse: + branding_data = settings.get_miniapp_branding() + branding = MiniAppBranding(**branding_data) if branding_data else None + return MiniAppConfigResponse( + branding=branding, + subscription_purchase_url=settings.get_miniapp_purchase_url(), + ) + + @router.post("/subscription", response_model=MiniAppSubscriptionResponse) async def get_subscription_details( payload: MiniAppSubscriptionRequest, @@ -383,6 +395,7 @@ async def get_subscription_details( else None, subscription_type="trial" if subscription.is_trial else "paid", autopay_enabled=bool(subscription.autopay_enabled), + subscription_purchase_url=settings.get_miniapp_purchase_url(), branding=settings.get_miniapp_branding(), ) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index a0d5cb1a..8effe4bb 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -11,6 +11,11 @@ class MiniAppBranding(BaseModel): service_description: Dict[str, Optional[str]] = Field(default_factory=dict) +class MiniAppConfigResponse(BaseModel): + branding: Optional[MiniAppBranding] = None + subscription_purchase_url: Optional[str] = None + + class MiniAppSubscriptionRequest(BaseModel): init_data: str = Field(..., alias="initData") @@ -74,6 +79,7 @@ class MiniAppSubscriptionResponse(BaseModel): user: MiniAppSubscriptionUser subscription_url: Optional[str] = None subscription_crypto_link: Optional[str] = None + subscription_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 27253b84..9e1fed9b 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -314,6 +314,16 @@ line-height: 1.6; } + .error-actions { + margin-top: 24px; + display: flex; + justify-content: center; + } + + .error-actions .btn { + min-width: 200px; + } + /* Cards */ .card { background: var(--bg-secondary); @@ -1186,6 +1196,9 @@
⚠️
Subscription Not Found
Please contact support to activate your subscription
+
+ +
@@ -1522,6 +1535,7 @@ 'button.connect.default': 'Connect to VPN', 'button.connect.happ': 'Connect', 'button.copy': 'Copy subscription link', + 'button.purchase': 'Buy subscription', 'card.balance.title': 'Balance', 'card.history.title': 'Transaction History', 'card.servers.title': 'Connected Servers', @@ -1584,6 +1598,7 @@ 'button.connect.default': 'Подключиться к VPN', 'button.connect.happ': 'Подключиться', 'button.copy': 'Скопировать ссылку подписки', + 'button.purchase': 'Купить подписку', 'card.balance.title': 'Баланс', 'card.history.title': 'История операций', 'card.servers.title': 'Подключённые серверы', @@ -1696,6 +1711,10 @@ } let userData = null; + let miniAppConfig = { + subscriptionPurchaseUrl: null, + branding: null, + }; let appsConfig = {}; let currentPlatform = 'android'; let preferredLanguage = 'en'; @@ -1771,6 +1790,18 @@ const message = currentErrorState?.message || t('error.default.message'); titleElement.textContent = title; textElement.textContent = message; + updatePurchaseButton(); + } + + function updatePurchaseButton() { + const purchaseBtn = document.getElementById('purchaseBtn'); + if (!purchaseBtn) { + return; + } + const link = getPurchaseLink(); + const hasLink = Boolean(link); + purchaseBtn.classList.toggle('hidden', !hasLink); + purchaseBtn.disabled = !hasLink; } function applyTranslations() { @@ -1867,6 +1898,7 @@ } } + await loadMiniAppConfig(); await loadAppsConfig(); const initData = tg.initData || ''; @@ -1903,9 +1935,14 @@ userData = await response.json(); userData.subscriptionUrl = userData.subscription_url || null; userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; + userData.subscriptionPurchaseUrl = + (userData.subscription_purchase_url + || miniAppConfig.subscriptionPurchaseUrl + || '').trim() || null; if (userData.branding) { applyBrandingOverrides(userData.branding); } + updatePurchaseButton(); const responseLanguage = resolveLanguage(userData?.user?.language); if (responseLanguage && !languageLockedByUser) { @@ -1931,6 +1968,36 @@ } } + 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'); + } + + const data = await response.json(); + miniAppConfig = { + subscriptionPurchaseUrl: + (data?.subscription_purchase_url + || data?.subscriptionPurchaseUrl + || '').trim() || null, + branding: data?.branding || null, + }; + + if (miniAppConfig.branding) { + applyBrandingOverrides(miniAppConfig.branding); + } + } catch (error) { + console.warn('Unable to load mini app config:', error); + miniAppConfig = { + subscriptionPurchaseUrl: null, + branding: null, + }; + } finally { + updatePurchaseButton(); + } + } + async function loadAppsConfig() { try { const response = await fetch('/app-config.json', { cache: 'no-cache' }); @@ -2464,6 +2531,16 @@ ); } + function getPurchaseLink() { + if (userData?.subscriptionPurchaseUrl) { + return userData.subscriptionPurchaseUrl; + } + if (miniAppConfig?.subscriptionPurchaseUrl) { + return miniAppConfig.subscriptionPurchaseUrl; + } + return null; + } + function getConnectLink() { if (!userData) { return null; @@ -2589,6 +2666,11 @@ } }); + document.getElementById('purchaseBtn')?.addEventListener('click', () => { + const link = getPurchaseLink(); + openExternalLink(link); + }); + init();