mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-13 23:50:27 +00:00
Add miniapp purchase link support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user