mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Show maintenance notice in miniapp
This commit is contained in:
@@ -128,6 +128,7 @@ class PlategaPaymentMixin:
|
||||
"status": status,
|
||||
"expires_at": expires_at,
|
||||
"correlation_id": correlation_id,
|
||||
"payload": payload_token,
|
||||
}
|
||||
|
||||
async def process_platega_webhook(
|
||||
|
||||
@@ -64,6 +64,7 @@ from app.services.remnawave_service import (
|
||||
from app.services.payment_service import PaymentService, get_wata_payment_by_link_id
|
||||
from app.services.promo_offer_service import promo_offer_service
|
||||
from app.services.promocode_service import PromoCodeService
|
||||
from app.services.maintenance_service import maintenance_service
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.services.subscription_renewal_service import (
|
||||
SubscriptionRenewalChargeError,
|
||||
@@ -115,6 +116,7 @@ from ..schemas.miniapp import (
|
||||
MiniAppDevice,
|
||||
MiniAppDeviceRemovalRequest,
|
||||
MiniAppDeviceRemovalResponse,
|
||||
MiniAppMaintenanceStatusResponse,
|
||||
MiniAppFaq,
|
||||
MiniAppFaqItem,
|
||||
MiniAppLegalDocuments,
|
||||
@@ -125,6 +127,7 @@ from ..schemas.miniapp import (
|
||||
MiniAppPaymentMethod,
|
||||
MiniAppPaymentMethodsRequest,
|
||||
MiniAppPaymentMethodsResponse,
|
||||
MiniAppPaymentOption,
|
||||
MiniAppPaymentStatusQuery,
|
||||
MiniAppPaymentStatusRequest,
|
||||
MiniAppPaymentStatusResponse,
|
||||
@@ -625,6 +628,23 @@ def _build_mulenpay_iframe_config() -> Optional[MiniAppPaymentIframeConfig]:
|
||||
return None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/maintenance/status",
|
||||
response_model=MiniAppMaintenanceStatusResponse,
|
||||
)
|
||||
async def get_maintenance_status(
|
||||
payload: MiniAppSubscriptionRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> MiniAppMaintenanceStatusResponse:
|
||||
_, _ = await _resolve_user_from_init_data(db, payload.init_data)
|
||||
status_info = maintenance_service.get_status_info()
|
||||
return MiniAppMaintenanceStatusResponse(
|
||||
is_active=bool(status_info.get("is_active")),
|
||||
message=maintenance_service.get_maintenance_message(),
|
||||
reason=status_info.get("reason"),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/payments/methods",
|
||||
response_model=MiniAppPaymentMethodsResponse,
|
||||
@@ -708,6 +728,24 @@ async def get_payment_methods(
|
||||
min_amount_kopeks=settings.PAL24_MIN_AMOUNT_KOPEKS,
|
||||
max_amount_kopeks=settings.PAL24_MAX_AMOUNT_KOPEKS,
|
||||
integration_type=MiniAppPaymentIntegrationType.REDIRECT,
|
||||
options=[
|
||||
MiniAppPaymentOption(
|
||||
id="sbp",
|
||||
icon="🏦",
|
||||
title_key="topup.method.pal24.option.sbp.title",
|
||||
description_key="topup.method.pal24.option.sbp.description",
|
||||
title="Faster Payments (SBP)",
|
||||
description="Instant SBP transfer with no fees.",
|
||||
),
|
||||
MiniAppPaymentOption(
|
||||
id="card",
|
||||
icon="💳",
|
||||
title_key="topup.method.pal24.option.card.title",
|
||||
description_key="topup.method.pal24.option.card.description",
|
||||
title="Bank card",
|
||||
description="Pay with a bank card via PayPalych.",
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -724,6 +762,37 @@ async def get_payment_methods(
|
||||
)
|
||||
)
|
||||
|
||||
if settings.is_platega_enabled() and settings.get_platega_active_methods():
|
||||
platega_methods = settings.get_platega_active_methods()
|
||||
definitions = settings.get_platega_method_definitions()
|
||||
options: List[MiniAppPaymentOption] = []
|
||||
|
||||
for method_code in platega_methods:
|
||||
info = definitions.get(method_code, {})
|
||||
options.append(
|
||||
MiniAppPaymentOption(
|
||||
id=str(method_code),
|
||||
icon=info.get("icon") or ("🏦" if method_code == 2 else "💳"),
|
||||
title_key=f"topup.method.platega.option.{method_code}.title",
|
||||
description_key=f"topup.method.platega.option.{method_code}.description",
|
||||
title=info.get("title") or info.get("name") or f"Platega {method_code}",
|
||||
description=info.get("description") or info.get("name"),
|
||||
)
|
||||
)
|
||||
|
||||
methods.append(
|
||||
MiniAppPaymentMethod(
|
||||
id="platega",
|
||||
icon="💳",
|
||||
requires_amount=True,
|
||||
currency=settings.PLATEGA_CURRENCY,
|
||||
min_amount_kopeks=settings.PLATEGA_MIN_AMOUNT_KOPEKS,
|
||||
max_amount_kopeks=settings.PLATEGA_MAX_AMOUNT_KOPEKS,
|
||||
integration_type=MiniAppPaymentIntegrationType.REDIRECT,
|
||||
options=options,
|
||||
)
|
||||
)
|
||||
|
||||
if settings.is_cryptobot_enabled():
|
||||
rate = await _get_usd_to_rub_rate()
|
||||
min_amount_kopeks, max_amount_kopeks = _compute_cryptobot_limits(rate)
|
||||
@@ -769,10 +838,11 @@ async def get_payment_methods(
|
||||
"yookassa": 3,
|
||||
"mulenpay": 4,
|
||||
"pal24": 5,
|
||||
"wata": 6,
|
||||
"cryptobot": 7,
|
||||
"heleket": 8,
|
||||
"tribute": 9,
|
||||
"platega": 6,
|
||||
"wata": 7,
|
||||
"cryptobot": 8,
|
||||
"heleket": 9,
|
||||
"tribute": 10,
|
||||
}
|
||||
methods.sort(key=lambda item: order_map.get(item.id, 99))
|
||||
|
||||
@@ -949,6 +1019,54 @@ async def create_payment_link(
|
||||
},
|
||||
)
|
||||
|
||||
if method == "platega":
|
||||
if not settings.is_platega_enabled() or not settings.get_platega_active_methods():
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
|
||||
if amount_kopeks is None or amount_kopeks <= 0:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
|
||||
if amount_kopeks < settings.PLATEGA_MIN_AMOUNT_KOPEKS:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
|
||||
if amount_kopeks > settings.PLATEGA_MAX_AMOUNT_KOPEKS:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum")
|
||||
|
||||
active_methods = settings.get_platega_active_methods()
|
||||
method_option = payload.payment_option or str(active_methods[0])
|
||||
try:
|
||||
method_code = int(str(method_option).strip())
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Invalid Platega payment option")
|
||||
|
||||
if method_code not in active_methods:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Selected Platega method is unavailable")
|
||||
|
||||
payment_service = PaymentService()
|
||||
result = await payment_service.create_platega_payment(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=settings.get_balance_payment_description(amount_kopeks),
|
||||
language=user.language or settings.DEFAULT_LANGUAGE,
|
||||
payment_method_code=method_code,
|
||||
)
|
||||
|
||||
redirect_url = result.get("redirect_url") if result else None
|
||||
if not result or not redirect_url:
|
||||
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
|
||||
|
||||
return MiniAppPaymentCreateResponse(
|
||||
method=method,
|
||||
payment_url=redirect_url,
|
||||
amount_kopeks=amount_kopeks,
|
||||
extra={
|
||||
"local_payment_id": result.get("local_payment_id"),
|
||||
"payment_id": result.get("transaction_id"),
|
||||
"correlation_id": result.get("correlation_id"),
|
||||
"selected_option": str(method_code),
|
||||
"payload": result.get("payload"),
|
||||
"requested_at": _current_request_timestamp(),
|
||||
},
|
||||
)
|
||||
|
||||
if method == "wata":
|
||||
if not settings.is_wata_enabled():
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
|
||||
@@ -1243,6 +1361,8 @@ async def _resolve_payment_status_entry(
|
||||
)
|
||||
if method == "mulenpay":
|
||||
return await _resolve_mulenpay_payment_status(payment_service, db, user, query)
|
||||
if method == "platega":
|
||||
return await _resolve_platega_payment_status(payment_service, db, user, query)
|
||||
if method == "wata":
|
||||
return await _resolve_wata_payment_status(payment_service, db, user, query)
|
||||
if method == "pal24":
|
||||
@@ -1395,6 +1515,85 @@ async def _resolve_mulenpay_payment_status(
|
||||
)
|
||||
|
||||
|
||||
async def _resolve_platega_payment_status(
|
||||
payment_service: PaymentService,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
query: MiniAppPaymentStatusQuery,
|
||||
) -> MiniAppPaymentStatusResult:
|
||||
from app.database.crud.platega import (
|
||||
get_platega_payment_by_correlation_id,
|
||||
get_platega_payment_by_id,
|
||||
get_platega_payment_by_transaction_id,
|
||||
)
|
||||
|
||||
payment = None
|
||||
local_id = query.local_payment_id
|
||||
if local_id:
|
||||
payment = await get_platega_payment_by_id(db, local_id)
|
||||
|
||||
if not payment and query.payment_id:
|
||||
payment = await get_platega_payment_by_transaction_id(db, query.payment_id)
|
||||
|
||||
if not payment and query.payload:
|
||||
correlation = str(query.payload).replace("platega:", "")
|
||||
payment = await get_platega_payment_by_correlation_id(db, correlation)
|
||||
|
||||
if not payment or payment.user_id != user.id:
|
||||
return MiniAppPaymentStatusResult(
|
||||
method="platega",
|
||||
status="pending",
|
||||
is_paid=False,
|
||||
amount_kopeks=query.amount_kopeks,
|
||||
message="Payment not found",
|
||||
extra={
|
||||
"local_payment_id": query.local_payment_id,
|
||||
"payment_id": query.payment_id,
|
||||
"payload": query.payload,
|
||||
"started_at": query.started_at,
|
||||
},
|
||||
)
|
||||
|
||||
status_info = await payment_service.get_platega_payment_status(db, payment.id)
|
||||
refreshed_payment = (status_info or {}).get("payment") or payment
|
||||
|
||||
status_raw = (status_info or {}).get("status") or getattr(payment, "status", None)
|
||||
is_paid_flag = bool((status_info or {}).get("is_paid") or getattr(payment, "is_paid", False))
|
||||
status_value = _classify_status(status_raw, is_paid_flag)
|
||||
|
||||
completed_at = (
|
||||
getattr(refreshed_payment, "paid_at", None)
|
||||
or getattr(refreshed_payment, "updated_at", None)
|
||||
or getattr(refreshed_payment, "created_at", None)
|
||||
)
|
||||
|
||||
extra: Dict[str, Any] = {
|
||||
"local_payment_id": refreshed_payment.id,
|
||||
"payment_id": refreshed_payment.platega_transaction_id,
|
||||
"correlation_id": refreshed_payment.correlation_id,
|
||||
"status": status_raw,
|
||||
"is_paid": getattr(refreshed_payment, "is_paid", False),
|
||||
"payload": query.payload,
|
||||
"started_at": query.started_at,
|
||||
}
|
||||
|
||||
if status_info and status_info.get("remote"):
|
||||
extra["remote"] = status_info.get("remote")
|
||||
|
||||
return MiniAppPaymentStatusResult(
|
||||
method="platega",
|
||||
status=status_value,
|
||||
is_paid=status_value == "paid",
|
||||
amount_kopeks=refreshed_payment.amount_kopeks,
|
||||
currency=refreshed_payment.currency,
|
||||
completed_at=completed_at,
|
||||
transaction_id=refreshed_payment.transaction_id,
|
||||
external_id=refreshed_payment.platega_transaction_id,
|
||||
message=None,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
|
||||
async def _resolve_wata_payment_status(
|
||||
payment_service: PaymentService,
|
||||
db: AsyncSession,
|
||||
|
||||
@@ -17,6 +17,12 @@ class MiniAppSubscriptionRequest(BaseModel):
|
||||
init_data: str = Field(..., alias="initData")
|
||||
|
||||
|
||||
class MiniAppMaintenanceStatusResponse(BaseModel):
|
||||
is_active: bool = Field(..., alias="isActive")
|
||||
message: Optional[str] = None
|
||||
reason: Optional[str] = None
|
||||
|
||||
|
||||
class MiniAppSubscriptionUser(BaseModel):
|
||||
telegram_id: int
|
||||
username: Optional[str] = None
|
||||
@@ -373,6 +379,17 @@ class MiniAppPaymentIntegrationType(str, Enum):
|
||||
REDIRECT = "redirect"
|
||||
|
||||
|
||||
class MiniAppPaymentOption(BaseModel):
|
||||
id: str
|
||||
icon: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
title_key: Optional[str] = Field(default=None, alias="titleKey")
|
||||
description_key: Optional[str] = Field(default=None, alias="descriptionKey")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MiniAppPaymentIframeConfig(BaseModel):
|
||||
expected_origin: str
|
||||
|
||||
@@ -402,6 +419,7 @@ class MiniAppPaymentMethod(BaseModel):
|
||||
max_amount_kopeks: Optional[int] = None
|
||||
amount_step_kopeks: Optional[int] = None
|
||||
integration_type: MiniAppPaymentIntegrationType
|
||||
options: List[MiniAppPaymentOption] = Field(default_factory=list)
|
||||
iframe_config: Optional[MiniAppPaymentIframeConfig] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
--success: #10b981;
|
||||
--success-rgb: 16, 185, 129;
|
||||
--warning: #f59e0b;
|
||||
--warning-rgb: 245, 158, 11;
|
||||
--danger: #ef4444;
|
||||
--danger-rgb: 239, 68, 68;
|
||||
--info: #3b82f6;
|
||||
@@ -288,6 +289,41 @@
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.maintenance-banner {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgba(var(--warning-rgb), 0.18);
|
||||
background: linear-gradient(135deg, rgba(var(--warning-rgb), 0.08), rgba(var(--warning-rgb), 0.02));
|
||||
color: var(--text-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.maintenance-icon {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.maintenance-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.maintenance-title {
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.maintenance-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -4766,6 +4802,16 @@
|
||||
<div class="subtitle" data-i18n="app.subtitle">Secure & Fast Connection</div>
|
||||
</div>
|
||||
|
||||
<div id="maintenanceBanner" class="maintenance-banner hidden" role="status" aria-live="polite">
|
||||
<div class="maintenance-icon" aria-hidden="true">🔧</div>
|
||||
<div class="maintenance-content">
|
||||
<div class="maintenance-title" data-i18n="maintenance.title">Технические работы</div>
|
||||
<div id="maintenanceMessage" class="maintenance-text" data-i18n="maintenance.message">
|
||||
Сервис временно недоступен из-за технических работ. Попробуйте позже.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loadingState" class="loading">
|
||||
<div class="spinner"></div>
|
||||
@@ -5586,6 +5632,8 @@
|
||||
'values.not_available': 'Not available',
|
||||
'app.subtitle': 'Secure & Fast Connection',
|
||||
'app.loading': 'Loading your subscription...',
|
||||
'maintenance.title': 'Technical maintenance',
|
||||
'maintenance.message': 'The service is temporarily in maintenance mode. Some actions may be unavailable.',
|
||||
'error.default.title': 'Subscription Not Found',
|
||||
'error.default.message': 'Please contact support to activate your subscription.',
|
||||
'error.user_not_found.title': 'Register in the bot',
|
||||
@@ -5615,6 +5663,18 @@
|
||||
'topup.method.yookassa.description': 'Pay securely with a bank card',
|
||||
'topup.method.mulenpay.title': 'Bank card (Mulen Pay)',
|
||||
'topup.method.mulenpay.description': 'Fast payment with bank card',
|
||||
'topup.method.platega.title': 'Platega.io',
|
||||
'topup.method.platega.description': 'Bank cards and SBP via Platega',
|
||||
'topup.method.platega.option.2.title': 'SBP (QR)',
|
||||
'topup.method.platega.option.2.description': 'Pay with Faster Payments QR code.',
|
||||
'topup.method.platega.option.10.title': 'Bank cards (RUB)',
|
||||
'topup.method.platega.option.10.description': 'Russian bank cards through Platega.',
|
||||
'topup.method.platega.option.11.title': 'Bank cards',
|
||||
'topup.method.platega.option.11.description': 'Local bank cards via Platega.',
|
||||
'topup.method.platega.option.12.title': 'International cards',
|
||||
'topup.method.platega.option.12.description': 'International cards supported by Platega.',
|
||||
'topup.method.platega.option.13.title': 'Cryptocurrency',
|
||||
'topup.method.platega.option.13.description': 'Top up balance with crypto via Platega.',
|
||||
'topup.method.wata.title': 'Bank card (Wata)',
|
||||
'topup.method.wata.description': 'Pay with a bank card via Wata',
|
||||
'topup.method.pal24.title': 'SBP (PayPalych)',
|
||||
@@ -5992,6 +6052,8 @@
|
||||
'values.not_available': 'Закрыто',
|
||||
'app.subtitle': 'Безопасное и быстрое подключение',
|
||||
'app.loading': 'Загружаем вашу подписку...',
|
||||
'maintenance.title': 'Технические работы',
|
||||
'maintenance.message': 'Сервис находится в режиме технических работ. Некоторые действия могут быть недоступны.',
|
||||
'error.default.title': 'Подписка не найдена',
|
||||
'error.default.message': 'Свяжитесь с поддержкой, чтобы активировать подписку.',
|
||||
'error.user_not_found.title': 'Зарегистрируйтесь в боте',
|
||||
@@ -6021,6 +6083,18 @@
|
||||
'topup.method.yookassa.description': 'Безопасная оплата банковской картой',
|
||||
'topup.method.mulenpay.title': 'Банковская карта (Mulen Pay)',
|
||||
'topup.method.mulenpay.description': 'Мгновенное списание с карты',
|
||||
'topup.method.platega.title': 'Platega.io',
|
||||
'topup.method.platega.description': 'Карта или СБП через Platega',
|
||||
'topup.method.platega.option.2.title': 'СБП (QR)',
|
||||
'topup.method.platega.option.2.description': 'Оплата по QR-коду через СБП.',
|
||||
'topup.method.platega.option.10.title': 'Банковские карты (RUB)',
|
||||
'topup.method.platega.option.10.description': 'Российские карты через Platega.',
|
||||
'topup.method.platega.option.11.title': 'Банковские карты',
|
||||
'topup.method.platega.option.11.description': 'Оплата картами через Platega.',
|
||||
'topup.method.platega.option.12.title': 'Международные карты',
|
||||
'topup.method.platega.option.12.description': 'Оплата международными картами.',
|
||||
'topup.method.platega.option.13.title': 'Криптовалюта',
|
||||
'topup.method.platega.option.13.description': 'Пополнение через криптовалюту в Platega.',
|
||||
'topup.method.wata.title': 'Банковская карта (Wata)',
|
||||
'topup.method.wata.description': 'Оплата банковской картой через Wata',
|
||||
'topup.method.pal24.title': 'СБП (PayPalych)',
|
||||
@@ -6664,6 +6738,7 @@
|
||||
let paymentMethodsCache = null;
|
||||
let paymentMethodsPromise = null;
|
||||
let activePaymentMethod = null;
|
||||
let maintenanceState = { isActive: false, message: null };
|
||||
const paymentMethodSelections = {};
|
||||
const activePaymentMonitors = new Map();
|
||||
let paymentStatusPollTimer = null;
|
||||
@@ -6841,13 +6916,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.method.id === 'pal24') {
|
||||
const option = (monitor.option || 'sbp').toLowerCase();
|
||||
const optionKey = option === 'card' ? 'card' : 'sbp';
|
||||
const fallback = optionKey === 'card'
|
||||
? 'Bank card payment'
|
||||
: 'Faster Payments (SBP)';
|
||||
setTopupModalSubtitle(`topup.method.pal24.option.${optionKey}.title`, fallback);
|
||||
const optionsMap = (Array.isArray(monitor.method.options) ? monitor.method.options : []).reduce((map, item) => {
|
||||
map[String(item.id)] = item;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
if (monitor.method.id === 'pal24' || monitor.method.id === 'platega') {
|
||||
const option = (monitor.option || monitor.extra?.selected_option || 'sbp').toString();
|
||||
const optionKey = ['card', 'sbp'].includes(option) ? option : option;
|
||||
const optionConfig = optionsMap[optionKey];
|
||||
const fallback = optionConfig?.title || (optionKey === 'card' ? 'Bank card payment' : 'Faster Payments (SBP)');
|
||||
const titleKey = optionConfig?.titleKey || optionConfig?.title_key || `topup.method.${monitor.method.id}.option.${optionKey}.title`;
|
||||
setTopupModalSubtitle(titleKey, fallback || monitor.method.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6878,6 +6958,11 @@
|
||||
identifiers.paymentId = extra.payment_id;
|
||||
}
|
||||
|
||||
if (extra.correlation_id && !query.payload) {
|
||||
query.payload = extra.correlation_id;
|
||||
identifiers.payload = extra.correlation_id;
|
||||
}
|
||||
|
||||
const payloadValue = extra.payload || extra.invoice_payload;
|
||||
if (payloadValue) {
|
||||
query.payload = payloadValue;
|
||||
@@ -7462,6 +7547,41 @@
|
||||
label.textContent = t(key);
|
||||
}
|
||||
|
||||
function renderMaintenanceBanner() {
|
||||
const banner = document.getElementById('maintenanceBanner');
|
||||
const messageElement = document.getElementById('maintenanceMessage');
|
||||
|
||||
if (!banner || !messageElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!maintenanceState?.isActive) {
|
||||
banner.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedMessage = (typeof maintenanceState.message === 'string'
|
||||
? maintenanceState.message.trim()
|
||||
: '')
|
||||
|| t('maintenance.message');
|
||||
const translatedFallback = t('maintenance.message');
|
||||
const messageFallback = translatedFallback === 'maintenance.message'
|
||||
? 'The service is temporarily unavailable due to maintenance. Please try again later.'
|
||||
: translatedFallback;
|
||||
messageElement.textContent = resolvedMessage === 'maintenance.message'
|
||||
? messageFallback
|
||||
: resolvedMessage;
|
||||
|
||||
banner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function applyMaintenanceStatus(status) {
|
||||
const isActive = Boolean(status?.isActive ?? status?.is_active);
|
||||
const message = typeof status?.message === 'string' ? status.message : null;
|
||||
maintenanceState = { isActive, message };
|
||||
renderMaintenanceBanner();
|
||||
}
|
||||
|
||||
function refreshAfterLanguageChange() {
|
||||
applyTranslations();
|
||||
if (userData) {
|
||||
@@ -7471,6 +7591,7 @@
|
||||
}
|
||||
renderApps();
|
||||
updateActionButtons();
|
||||
renderMaintenanceBanner();
|
||||
}
|
||||
|
||||
function setLanguage(language, options = {}) {
|
||||
@@ -7669,6 +7790,22 @@
|
||||
hasAnimatedCards = true;
|
||||
}
|
||||
|
||||
async function fetchMaintenanceStatus(initData) {
|
||||
const response = await fetch('/miniapp/maintenance/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ initData })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
throw new Error('Unable to fetch maintenance status');
|
||||
}
|
||||
|
||||
async function fetchSubscriptionPayload(initData) {
|
||||
const response = await fetch('/miniapp/subscription', {
|
||||
method: 'POST',
|
||||
@@ -7910,6 +8047,21 @@
|
||||
return applySubscriptionData(payload);
|
||||
}
|
||||
|
||||
async function checkMaintenance(initData) {
|
||||
if (!initData) {
|
||||
applyMaintenanceStatus({ isActive: false, message: null });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await fetchMaintenanceStatus(initData);
|
||||
applyMaintenanceStatus(status);
|
||||
} catch (error) {
|
||||
console.warn('Unable to load maintenance status:', error);
|
||||
applyMaintenanceStatus({ isActive: false, message: null });
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const telegramUser = tg.initDataUnsafe?.user;
|
||||
@@ -7923,6 +8075,8 @@
|
||||
}
|
||||
|
||||
await loadAppsConfig();
|
||||
const initData = tg.initData || '';
|
||||
await checkMaintenance(initData);
|
||||
await refreshSubscriptionData();
|
||||
} catch (error) {
|
||||
console.error('Initialization error:', error);
|
||||
@@ -11503,32 +11657,44 @@
|
||||
form.appendChild(hint);
|
||||
}
|
||||
|
||||
if (method.id === 'pal24') {
|
||||
const optionsConfig = [
|
||||
const providedOptions = Array.isArray(method.options) ? method.options : [];
|
||||
const fallbackOptions = method.id === 'pal24'
|
||||
? [
|
||||
{
|
||||
id: 'sbp',
|
||||
icon: '🏦',
|
||||
titleKey: 'topup.method.pal24.option.sbp.title',
|
||||
descriptionKey: 'topup.method.pal24.option.sbp.description',
|
||||
fallbackTitle: 'Faster Payments (SBP)',
|
||||
fallbackDescription: 'Instant SBP transfer with no fees.',
|
||||
title: 'Faster Payments (SBP)',
|
||||
description: 'Instant SBP transfer with no fees.',
|
||||
},
|
||||
{
|
||||
id: 'card',
|
||||
icon: '💳',
|
||||
titleKey: 'topup.method.pal24.option.card.title',
|
||||
descriptionKey: 'topup.method.pal24.option.card.description',
|
||||
fallbackTitle: 'Bank card',
|
||||
fallbackDescription: 'Pay with a bank card via PayPalych.',
|
||||
title: 'Bank card',
|
||||
description: 'Pay with a bank card via PayPalych.',
|
||||
},
|
||||
];
|
||||
]
|
||||
: [];
|
||||
|
||||
const selectedDefault = options.selectedOption
|
||||
const optionsConfig = (providedOptions.length ? providedOptions : fallbackOptions).map(option => ({
|
||||
id: String(option.id),
|
||||
icon: option.icon || '💳',
|
||||
titleKey: option.titleKey || option.title_key || option.titlekey,
|
||||
descriptionKey: option.descriptionKey || option.description_key || option.descriptionkey,
|
||||
fallbackTitle: option.title || option.name || String(option.id),
|
||||
fallbackDescription: option.description || '',
|
||||
}));
|
||||
|
||||
if (optionsConfig.length) {
|
||||
const defaultSelection = options.selectedOption
|
||||
|| paymentMethodSelections[method.id]
|
||||
|| 'sbp';
|
||||
let currentOption = optionsConfig.some(option => option.id === selectedDefault)
|
||||
? selectedDefault
|
||||
: 'sbp';
|
||||
|| optionsConfig[0]?.id;
|
||||
let currentOption = optionsConfig.some(option => option.id === defaultSelection)
|
||||
? defaultSelection
|
||||
: optionsConfig[0]?.id;
|
||||
paymentMethodSelections[method.id] = currentOption;
|
||||
form.dataset.paymentOption = currentOption;
|
||||
|
||||
@@ -11537,7 +11703,7 @@
|
||||
|
||||
const optionTitle = document.createElement('div');
|
||||
optionTitle.className = 'payment-option-title';
|
||||
const titleKey = 'topup.method.pal24.title';
|
||||
const titleKey = `topup.method.${method.id}.title`;
|
||||
const titleValue = t(titleKey);
|
||||
optionTitle.textContent = titleValue === titleKey ? 'Choose payment type' : titleValue;
|
||||
optionGroup.appendChild(optionTitle);
|
||||
@@ -11563,22 +11729,23 @@
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'payment-option-label';
|
||||
const labelValue = t(config.titleKey);
|
||||
label.textContent = labelValue === config.titleKey ? config.fallbackTitle : labelValue;
|
||||
const labelValue = config.titleKey ? t(config.titleKey) : config.fallbackTitle;
|
||||
label.textContent = labelValue && labelValue !== config.titleKey
|
||||
? labelValue
|
||||
: config.fallbackTitle;
|
||||
|
||||
const description = document.createElement('div');
|
||||
description.className = 'payment-option-description';
|
||||
const descriptionValue = t(config.descriptionKey);
|
||||
const finalDescription = descriptionValue === config.descriptionKey
|
||||
? config.fallbackDescription
|
||||
: descriptionValue;
|
||||
description.textContent = finalDescription;
|
||||
|
||||
text.appendChild(label);
|
||||
const descriptionValue = config.descriptionKey ? t(config.descriptionKey) : config.fallbackDescription;
|
||||
const finalDescription = descriptionValue && descriptionValue !== config.descriptionKey
|
||||
? descriptionValue
|
||||
: config.fallbackDescription;
|
||||
if (finalDescription) {
|
||||
description.textContent = finalDescription;
|
||||
text.appendChild(description);
|
||||
}
|
||||
|
||||
text.appendChild(label);
|
||||
button.appendChild(icon);
|
||||
button.appendChild(text);
|
||||
|
||||
@@ -11816,8 +11983,19 @@
|
||||
const normalizedAmount = Number.isFinite(amountKopeks) ? Number(amountKopeks) : null;
|
||||
const monitorExtra = { ...extra };
|
||||
|
||||
const methodOptions = Array.isArray(method.options) ? method.options : [];
|
||||
const optionsMap = methodOptions.reduce((map, item) => {
|
||||
const key = String(item.id);
|
||||
map[key] = item;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
let option = null;
|
||||
if (method.id === 'pal24') {
|
||||
if (methodOptions.length) {
|
||||
option = (options.providerOption || monitorExtra.selected_option || paymentMethodSelections[method.id] || methodOptions[0]?.id || '').toString();
|
||||
paymentMethodSelections[method.id] = option;
|
||||
monitorExtra.selected_option = option;
|
||||
} else if (method.id === 'pal24') {
|
||||
option = (options.providerOption || monitorExtra.selected_option || paymentMethodSelections[method.id] || 'sbp').toLowerCase();
|
||||
if (!['card', 'sbp'].includes(option)) {
|
||||
option = 'sbp';
|
||||
@@ -11826,12 +12004,19 @@
|
||||
monitorExtra.selected_option = option;
|
||||
}
|
||||
|
||||
const titleKey = method.id === 'pal24' && option
|
||||
? `topup.method.pal24.option.${option}.title`
|
||||
: `topup.method.${method.id}.title`;
|
||||
const titleFallback = method.id === 'pal24'
|
||||
? (option === 'card' ? 'Bank card payment' : 'Faster Payments (SBP)')
|
||||
: method.id;
|
||||
const selectedOption = option && (optionsMap[option] || optionsMap[String(option)]);
|
||||
const optionTitleKey = selectedOption?.titleKey || selectedOption?.title_key;
|
||||
const optionTitleFallback = selectedOption?.title || selectedOption?.name || option || method.id;
|
||||
const titleKey = selectedOption && optionTitleKey
|
||||
? optionTitleKey
|
||||
: option
|
||||
? `topup.method.${method.id}.option.${option}.title`
|
||||
: `topup.method.${method.id}.title`;
|
||||
const titleFallback = selectedOption
|
||||
? optionTitleFallback
|
||||
: method.id === 'pal24'
|
||||
? (option === 'card' ? 'Bank card payment' : 'Faster Payments (SBP)')
|
||||
: method.id;
|
||||
setTopupModalSubtitle(titleKey, titleFallback);
|
||||
|
||||
body.innerHTML = '';
|
||||
@@ -11902,8 +12087,8 @@
|
||||
summary.appendChild(usdAmount);
|
||||
}
|
||||
|
||||
const descriptionKey = method.id === 'pal24' && option
|
||||
? `topup.method.pal24.option.${option}.description`
|
||||
const descriptionKey = option
|
||||
? `topup.method.${method.id}.option.${option}.description`
|
||||
: `topup.method.${method.id}.description`;
|
||||
const descriptionValue = t(descriptionKey);
|
||||
if (descriptionValue && descriptionValue !== descriptionKey) {
|
||||
|
||||
Reference in New Issue
Block a user