diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py index 73501edb..d975141a 100644 --- a/app/external/remnawave_api.py +++ b/app/external/remnawave_api.py @@ -538,10 +538,25 @@ class RemnaWaveAPI: user = self._parse_user(response['response']) return await self.enrich_user_with_happ_link(user) - async def revoke_user_subscription(self, uuid: str, new_short_uuid: Optional[str] = None) -> RemnaWaveUser: + async def revoke_user_subscription( + self, + uuid: str, + new_short_uuid: Optional[str] = None, + revoke_only_passwords: bool = False + ) -> RemnaWaveUser: + """ + Отзывает подписку пользователя (меняет ссылку/пароли). + + Args: + uuid: UUID пользователя + new_short_uuid: Новый короткий UUID (опционально, рекомендуется генерировать автоматически) + revoke_only_passwords: Если True, меняются только пароли без изменения URL подписки + """ data = {} if new_short_uuid: data['shortUuid'] = new_short_uuid + if revoke_only_passwords: + data['revokeOnlyPasswords'] = True response = await self._make_request('POST', f'/api/users/{uuid}/actions/revoke', data) user = self._parse_user(response['response']) @@ -809,6 +824,19 @@ class RemnaWaveAPI: async def get_system_stats(self) -> Dict[str, Any]: response = await self._make_request('GET', '/api/system/stats') return response['response'] + + async def get_system_metadata(self) -> Dict[str, Any]: + """ + Получает метаданные системы Remnawave. + + Returns: + Dict с полями: + - version: версия Remnawave + - build: {time, number} - информация о сборке + - git: {backend: {commitSha}, node: {commitSha}} - информация о коммитах + """ + response = await self._make_request('GET', '/api/system/metadata') + return response['response'] async def get_bandwidth_stats(self) -> Dict[str, Any]: response = await self._make_request('GET', '/api/system/stats/bandwidth') diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index f6beb775..2f6acade 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -5052,6 +5052,8 @@ async def get_subscription_renewal_options_endpoint( autopay_days_options=renewal_autopay_days_options, autopay=renewal_autopay_payload, autopay_settings=renewal_autopay_payload, + is_trial=bool(getattr(subscription, "is_trial", False)), + sales_mode=settings.get_sales_mode(), **renewal_autopay_extras, ) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 73360418..e33ad8b1 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -201,6 +201,9 @@ class MiniAppSubscriptionRenewalOptionsResponse(BaseModel): autopay_days_options: List[int] = Field(default_factory=list) autopay: Optional[MiniAppSubscriptionAutopay] = None autopay_settings: Optional[MiniAppSubscriptionAutopay] = None + # Флаги для определения типа действия (покупка vs продление) + is_trial: bool = Field(default=False, alias="isTrial") + sales_mode: str = Field(default="classic", alias="salesMode") model_config = ConfigDict(populate_by_name=True, extra="allow") diff --git a/miniapp/index.html b/miniapp/index.html index e07f5459..69e65b06 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -5903,7 +5903,11 @@ 'subscription_settings.confirm.months.one': '{count} month', 'subscription_settings.confirm.months.other': '{count} months', 'subscription_renewal.title': 'Renew subscription', + 'subscription_renewal.title.trial_classic': 'Buy subscription', + 'subscription_renewal.title.trial_tariffs': 'Buy tariff', 'subscription_renewal.subtitle': 'Choose how long you want to extend access.', + 'subscription_renewal.subtitle.trial_classic': 'Choose a period to get started.', + 'subscription_renewal.subtitle.trial_tariffs': 'Choose a period to get started.', 'subscription_renewal.status.loading': 'Loading renewal options…', 'subscription_renewal.status.empty': 'No renewal options are currently available.', 'subscription_renewal.error.generic': 'Unable to load renewal options. Please try again later.', @@ -5915,9 +5919,12 @@ 'subscription_renewal.summary.discount': 'You save {percent}%', 'subscription_renewal.summary.insufficient': 'Not enough balance — missing {missing}.', 'subscription_renewal.submit': 'Renew', + 'subscription_renewal.submit.trial': 'Buy', 'subscription_renewal.submit.loading': 'Processing…', 'subscription_renewal.confirm.title': 'Confirm renewal', + 'subscription_renewal.confirm.title.trial': 'Confirm purchase', 'subscription_renewal.confirm.message': 'Renew the subscription for {period} and pay {amount}?', + 'subscription_renewal.confirm.message.trial': 'Buy subscription for {period} and pay {amount}?', 'subscription_renewal.confirm.balance': 'Current balance: {balance}', 'subscription_renewal.confirm.accept': 'Pay', 'subscription_renewal.confirm.cancel': 'Cancel', @@ -6327,7 +6334,11 @@ 'subscription_settings.confirm.months.one': '{count} месяц', 'subscription_settings.confirm.months.other': '{count} месяцев', 'subscription_renewal.title': 'Продление подписки', + 'subscription_renewal.title.trial_classic': 'Покупка подписки', + 'subscription_renewal.title.trial_tariffs': 'Покупка тарифа', 'subscription_renewal.subtitle': 'Выберите период продления.', + 'subscription_renewal.subtitle.trial_classic': 'Выберите период для начала.', + 'subscription_renewal.subtitle.trial_tariffs': 'Выберите период для начала.', 'subscription_renewal.status.loading': 'Загружаем доступные периоды…', 'subscription_renewal.status.empty': 'Доступных периодов продления нет.', 'subscription_renewal.error.generic': 'Не удалось загрузить параметры продления. Попробуйте позже.', @@ -6339,9 +6350,12 @@ 'subscription_renewal.summary.discount': 'Выгода {percent}%', 'subscription_renewal.summary.insufficient': 'Недостаточно средств — не хватает {missing}.', 'subscription_renewal.submit': 'Продлить', + 'subscription_renewal.submit.trial': 'Купить', 'subscription_renewal.submit.loading': 'Продление…', 'subscription_renewal.confirm.title': 'Подтвердите продление', + 'subscription_renewal.confirm.title.trial': 'Подтвердите покупку', 'subscription_renewal.confirm.message': 'Продлить подписку на {period} и списать {amount}?', + 'subscription_renewal.confirm.message.trial': 'Купить подписку на {period} и списать {amount}?', 'subscription_renewal.confirm.balance': 'Текущий баланс: {balance}', 'subscription_renewal.confirm.accept': 'Оплатить', 'subscription_renewal.confirm.cancel': 'Отмена', @@ -14919,6 +14933,44 @@ statusText.classList.add('hidden'); } + // Обновляем заголовок, подзаголовок и кнопку в зависимости от типа подписки + const isTrial = subscriptionRenewalData.isTrial || subscriptionRenewalData.is_trial || false; + const salesMode = subscriptionRenewalData.salesMode || subscriptionRenewalData.sales_mode || 'classic'; + + // Обновляем заголовок карточки + const titleEl = card.querySelector('[data-i18n="subscription_renewal.title"]'); + if (titleEl) { + let titleKey = 'subscription_renewal.title'; + if (isTrial) { + titleKey = salesMode === 'tariffs' + ? 'subscription_renewal.title.trial_tariffs' + : 'subscription_renewal.title.trial_classic'; + } + const titleText = t(titleKey); + titleEl.textContent = titleText !== titleKey ? titleText : (isTrial ? (salesMode === 'tariffs' ? 'Buy tariff' : 'Buy subscription') : 'Renew subscription'); + } + + // Обновляем подзаголовок + const subtitleEl = card.querySelector('[data-i18n="subscription_renewal.subtitle"]'); + if (subtitleEl) { + let subtitleKey = 'subscription_renewal.subtitle'; + if (isTrial) { + subtitleKey = salesMode === 'tariffs' + ? 'subscription_renewal.subtitle.trial_tariffs' + : 'subscription_renewal.subtitle.trial_classic'; + } + const subtitleText = t(subtitleKey); + subtitleEl.textContent = subtitleText !== subtitleKey ? subtitleText : (isTrial ? 'Choose a period to get started.' : 'Choose how long you want to extend access.'); + } + + // Обновляем текст кнопки + const submitBtn = document.getElementById('subscriptionRenewalSubmit'); + if (submitBtn && !subscriptionRenewalSubmitting) { + const btnKey = isTrial ? 'subscription_renewal.submit.trial' : 'subscription_renewal.submit'; + const btnText = t(btnKey); + submitBtn.textContent = btnText !== btnKey ? btnText : (isTrial ? 'Buy' : 'Renew'); + } + renderSubscriptionRenewalMeta(subscriptionRenewalData); renderSubscriptionRenewalOptions(subscriptionRenewalData); renderSubscriptionRenewalSummary(subscriptionRenewalData); @@ -14930,10 +14982,17 @@ || (option.priceKopeks !== null ? formatPriceFromKopeks(option.priceKopeks, data.currency) : ''); const periodLabel = buildRenewalPeriodLabel(option); - const messageTemplate = t('subscription_renewal.confirm.message'); - const message = messageTemplate && messageTemplate !== 'subscription_renewal.confirm.message' - ? messageTemplate.replace('{period}', periodLabel).replace('{amount}', amountLabel) + // Определяем, триал ли это + const isTrial = data?.isTrial || data?.is_trial || false; + + const messageKey = isTrial ? 'subscription_renewal.confirm.message.trial' : 'subscription_renewal.confirm.message'; + const messageTemplate = t(messageKey); + const defaultMessage = isTrial + ? `Buy subscription for ${periodLabel} and pay ${amountLabel}?` : `Renew the subscription for ${periodLabel} and pay ${amountLabel}?`; + const message = messageTemplate && messageTemplate !== messageKey + ? messageTemplate.replace('{period}', periodLabel).replace('{amount}', amountLabel) + : defaultMessage; const balanceLabel = data?.balanceLabel || (data.balanceKopeks !== null ? formatPriceFromKopeks(data.balanceKopeks, data.currency) : null); @@ -14947,7 +15006,7 @@ finalMessage = `${message}\n${balanceText}`; } - const titleKey = 'subscription_renewal.confirm.title'; + const titleKey = isTrial ? 'subscription_renewal.confirm.title.trial' : 'subscription_renewal.confirm.title'; const confirmKey = 'subscription_renewal.confirm.accept'; const cancelKey = 'subscription_renewal.confirm.cancel'; @@ -14956,7 +15015,7 @@ const cancelText = t(cancelKey); return showConfirmationPopup({ - title: title === titleKey ? 'Confirm renewal' : title, + title: title === titleKey ? (isTrial ? 'Confirm purchase' : 'Confirm renewal') : title, message: finalMessage, confirmText: confirmText === confirmKey ? 'Pay' : confirmText, cancelText: cancelText === cancelKey ? 'Cancel' : cancelText,