Add miniapp purchase link support

This commit is contained in:
Egor
2025-10-01 05:56:51 +03:00
parent 37c552c705
commit 4fa87804d9
6 changed files with 111 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 @@
<div class="error-icon">⚠️</div>
<div class="error-title" id="errorTitle" data-i18n="error.default.title">Subscription Not Found</div>
<div class="error-text" id="errorText" data-i18n="error.default.message">Please contact support to activate your subscription</div>
<div class="error-actions">
<button class="btn btn-primary hidden" id="purchaseBtn" type="button" data-i18n="button.purchase">Купить подписку</button>
</div>
</div>
<!-- Main Content -->
@@ -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();
</script>