From e0edc6bcdcaeb328954801f20b2d55810dfc3d3b Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 11 Oct 2025 05:33:42 +0300 Subject: [PATCH] Revert "Ensure mini app autopay shows all day options" --- app/webapi/routes/miniapp.py | 263 ----------- app/webapi/schemas/miniapp.py | 48 +- miniapp/index.html | 813 ---------------------------------- 3 files changed, 1 insertion(+), 1123 deletions(-) diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 323c2826..f888d33c 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -37,7 +37,6 @@ from app.database.crud.subscription import ( create_trial_subscription, extend_subscription, remove_subscription_servers, - update_subscription_autopay, ) from app.database.crud.transaction import ( create_transaction, @@ -151,9 +150,6 @@ from ..schemas.miniapp import ( MiniAppSubscriptionPurchaseResponse, MiniAppSubscriptionTrialRequest, MiniAppSubscriptionTrialResponse, - MiniAppSubscriptionAutopay, - MiniAppSubscriptionAutopayRequest, - MiniAppSubscriptionAutopayResponse, MiniAppSubscriptionRenewalOptionsRequest, MiniAppSubscriptionRenewalOptionsResponse, MiniAppSubscriptionRenewalPeriod, @@ -202,104 +198,6 @@ _PAYMENT_FAILURE_STATUSES = { _PERIOD_ID_PATTERN = re.compile(r"(\d+)") -_AUTOPAY_DEFAULT_DAY_OPTIONS = (1, 3, 7, 14) - - -def _normalize_autopay_days(value: Optional[Any]) -> Optional[int]: - if value is None: - return None - try: - numeric = int(value) - except (TypeError, ValueError): - return None - return numeric if numeric > 0 else None - - -def _get_autopay_day_options(subscription: Optional[Subscription]) -> List[int]: - options: set[int] = set() - for candidate in _AUTOPAY_DEFAULT_DAY_OPTIONS: - normalized = _normalize_autopay_days(candidate) - if normalized is not None: - options.add(normalized) - - default_setting = _normalize_autopay_days( - getattr(settings, "DEFAULT_AUTOPAY_DAYS_BEFORE", None) - ) - if default_setting is not None: - options.add(default_setting) - - if subscription is not None: - current = _normalize_autopay_days( - getattr(subscription, "autopay_days_before", None) - ) - if current is not None: - options.add(current) - - return sorted(options) - - -def _build_autopay_payload( - subscription: Optional[Subscription], -) -> Optional[MiniAppSubscriptionAutopay]: - if subscription is None: - return None - - enabled = bool(getattr(subscription, "autopay_enabled", False)) - days_before = _normalize_autopay_days( - getattr(subscription, "autopay_days_before", None) - ) - options = _get_autopay_day_options(subscription) - - default_days = days_before - if default_days is None: - default_days = _normalize_autopay_days( - getattr(settings, "DEFAULT_AUTOPAY_DAYS_BEFORE", None) - ) - if default_days is None and options: - default_days = options[0] - - autopay_kwargs: Dict[str, Any] = { - "enabled": enabled, - "autopay_enabled": enabled, - "days_before": days_before, - "autopay_days_before": days_before, - "default_days_before": default_days, - "autopay_days_options": options, - "days_options": options, - "options": options, - "available_days": options, - "availableDays": options, - "autopayEnabled": enabled, - "autopayDaysBefore": days_before, - "autopayDaysOptions": options, - "daysBefore": days_before, - "daysOptions": options, - "defaultDaysBefore": default_days, - } - - return MiniAppSubscriptionAutopay(**autopay_kwargs) - - -def _autopay_response_extras( - enabled: bool, - days_before: Optional[int], - options: List[int], - autopay_payload: Optional[MiniAppSubscriptionAutopay], -) -> Dict[str, Any]: - extras: Dict[str, Any] = { - "autopayEnabled": enabled, - "autopayDaysBefore": days_before, - "autopayDaysOptions": options, - } - if days_before is not None: - extras["daysBefore"] = days_before - if options: - extras["daysOptions"] = options - if autopay_payload is not None: - extras["autopaySettings"] = autopay_payload - return extras - - async def _get_usd_to_rub_rate() -> float: try: rate = await currency_converter.get_usd_to_rub_rate() @@ -2455,24 +2353,6 @@ async def get_subscription_details( device_limit_value = subscription.device_limit autopay_enabled = bool(subscription.autopay_enabled) - autopay_payload = _build_autopay_payload(subscription) - autopay_days_before = ( - getattr(autopay_payload, "autopay_days_before", None) - if autopay_payload - else None - ) - autopay_days_options = ( - list(getattr(autopay_payload, "autopay_days_options", []) or []) - if autopay_payload - else [] - ) - autopay_extras = _autopay_response_extras( - autopay_enabled, - autopay_days_before, - autopay_days_options, - autopay_payload, - ) - devices_count, devices = await _load_devices_info(user) response_user = MiniAppSubscriptionUser( @@ -2561,10 +2441,6 @@ async def get_subscription_details( else ("paid" if subscription else "none") ), autopay_enabled=autopay_enabled, - autopay_days_before=autopay_days_before, - autopay_days_options=autopay_days_options, - autopay=autopay_payload, - autopay_settings=autopay_payload, branding=settings.get_miniapp_branding(), faq=faq_payload, legal_documents=legal_documents_payload, @@ -2574,121 +2450,6 @@ async def get_subscription_details( trial_available=trial_available, trial_duration_days=trial_duration_days, trial_status="available" if trial_available else "unavailable", - **autopay_extras, - ) - - -@router.post( - "/subscription/autopay", - response_model=MiniAppSubscriptionAutopayResponse, -) -async def update_subscription_autopay_endpoint( - payload: MiniAppSubscriptionAutopayRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionAutopayResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - target_enabled = ( - bool(payload.enabled) - if payload.enabled is not None - else bool(subscription.autopay_enabled) - ) - - requested_days = payload.days_before - normalized_days = _normalize_autopay_days(requested_days) - current_days = _normalize_autopay_days( - getattr(subscription, "autopay_days_before", None) - ) - if normalized_days is None: - normalized_days = current_days - - options = _get_autopay_day_options(subscription) - default_day = _normalize_autopay_days( - getattr(settings, "DEFAULT_AUTOPAY_DAYS_BEFORE", None) - ) - if default_day is None and options: - default_day = options[0] - - if target_enabled and normalized_days is None: - if default_day is None: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "autopay_no_days", - "message": "Auto-pay day selection is temporarily unavailable", - }, - ) - normalized_days = default_day - - if normalized_days is None: - normalized_days = default_day or (options[0] if options else 1) - - if ( - bool(subscription.autopay_enabled) == target_enabled - and current_days == normalized_days - ): - autopay_payload = _build_autopay_payload(subscription) - autopay_days_before = ( - getattr(autopay_payload, "autopay_days_before", None) - if autopay_payload - else None - ) - autopay_days_options = ( - list(getattr(autopay_payload, "autopay_days_options", []) or []) - if autopay_payload - else options - ) - extras = _autopay_response_extras( - target_enabled, - autopay_days_before, - autopay_days_options, - autopay_payload, - ) - return MiniAppSubscriptionAutopayResponse( - subscription_id=subscription.id, - autopay_enabled=target_enabled, - autopay_days_before=autopay_days_before, - autopay_days_options=autopay_days_options, - autopay=autopay_payload, - autopay_settings=autopay_payload, - **extras, - ) - - updated_subscription = await update_subscription_autopay( - db, - subscription, - target_enabled, - normalized_days, - ) - - autopay_payload = _build_autopay_payload(updated_subscription) - autopay_days_before = ( - getattr(autopay_payload, "autopay_days_before", None) - if autopay_payload - else None - ) - autopay_days_options = ( - list(getattr(autopay_payload, "autopay_days_options", []) or []) - if autopay_payload - else _get_autopay_day_options(updated_subscription) - ) - extras = _autopay_response_extras( - bool(updated_subscription.autopay_enabled), - autopay_days_before, - autopay_days_options, - autopay_payload, - ) - - return MiniAppSubscriptionAutopayResponse( - subscription_id=updated_subscription.id, - autopay_enabled=bool(updated_subscription.autopay_enabled), - autopay_days_before=autopay_days_before, - autopay_days_options=autopay_days_options, - autopay=autopay_payload, - autopay_settings=autopay_payload, - **extras, ) @@ -3831,24 +3592,6 @@ async def get_subscription_renewal_options_endpoint( if isinstance(final_total, int) and balance_kopeks < final_total: missing_amount = final_total - balance_kopeks - renewal_autopay_payload = _build_autopay_payload(subscription) - renewal_autopay_days_before = ( - getattr(renewal_autopay_payload, "autopay_days_before", None) - if renewal_autopay_payload - else None - ) - renewal_autopay_days_options = ( - list(getattr(renewal_autopay_payload, "autopay_days_options", []) or []) - if renewal_autopay_payload - else [] - ) - renewal_autopay_extras = _autopay_response_extras( - bool(subscription.autopay_enabled), - renewal_autopay_days_before, - renewal_autopay_days_options, - renewal_autopay_payload, - ) - return MiniAppSubscriptionRenewalOptionsResponse( subscription_id=subscription.id, currency=currency, @@ -3860,12 +3603,6 @@ async def get_subscription_renewal_options_endpoint( default_period_id=default_period_id, missing_amount_kopeks=missing_amount, status_message=_build_renewal_status_message(user), - autopay_enabled=bool(subscription.autopay_enabled), - autopay_days_before=renewal_autopay_days_before, - autopay_days_options=renewal_autopay_days_options, - autopay=renewal_autopay_payload, - autopay_settings=renewal_autopay_payload, - **renewal_autopay_extras, ) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 4e2fa066..4aa41edf 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -134,20 +134,6 @@ class MiniAppPromoOfferClaimResponse(BaseModel): code: Optional[str] = None -class MiniAppSubscriptionAutopay(BaseModel): - enabled: bool = False - autopay_enabled: Optional[bool] = None - autopay_enabled_at: Optional[datetime] = None - days_before: Optional[int] = None - autopay_days_before: Optional[int] = None - default_days_before: Optional[int] = None - autopay_days_options: List[int] = Field(default_factory=list) - days_options: List[int] = Field(default_factory=list) - options: List[int] = Field(default_factory=list) - - model_config = ConfigDict(extra="allow") - - class MiniAppSubscriptionRenewalPeriod(BaseModel): id: str days: Optional[int] = None @@ -186,13 +172,8 @@ class MiniAppSubscriptionRenewalOptionsResponse(BaseModel): default_period_id: Optional[str] = Field(default=None, alias="defaultPeriodId") missing_amount_kopeks: Optional[int] = Field(default=None, alias="missingAmountKopeks") status_message: Optional[str] = Field(default=None, alias="statusMessage") - autopay_enabled: bool = False - autopay_days_before: Optional[int] = None - autopay_days_options: List[int] = Field(default_factory=list) - autopay: Optional[MiniAppSubscriptionAutopay] = None - autopay_settings: Optional[MiniAppSubscriptionAutopay] = None - model_config = ConfigDict(populate_by_name=True, extra="allow") + model_config = ConfigDict(populate_by_name=True) class MiniAppSubscriptionRenewalRequest(BaseModel): @@ -215,27 +196,6 @@ class MiniAppSubscriptionRenewalResponse(BaseModel): model_config = ConfigDict(populate_by_name=True) -class MiniAppSubscriptionAutopayRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - enabled: Optional[bool] = None - days_before: Optional[int] = Field(default=None, alias="daysBefore") - - model_config = ConfigDict(populate_by_name=True) - - -class MiniAppSubscriptionAutopayResponse(BaseModel): - success: bool = True - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - autopay_enabled: bool = False - autopay_days_before: Optional[int] = None - autopay_days_options: List[int] = Field(default_factory=list) - autopay: Optional[MiniAppSubscriptionAutopay] = None - autopay_settings: Optional[MiniAppSubscriptionAutopay] = None - - model_config = ConfigDict(populate_by_name=True, extra="allow") - - class MiniAppPromoCode(BaseModel): code: str type: Optional[str] = None @@ -451,10 +411,6 @@ class MiniAppSubscriptionResponse(BaseModel): total_spent_label: Optional[str] = None subscription_type: str autopay_enabled: bool = False - autopay_days_before: Optional[int] = None - autopay_days_options: List[int] = Field(default_factory=list) - autopay: Optional[MiniAppSubscriptionAutopay] = None - autopay_settings: Optional[MiniAppSubscriptionAutopay] = None branding: Optional[MiniAppBranding] = None faq: Optional[MiniAppFaq] = None legal_documents: Optional[MiniAppLegalDocuments] = None @@ -465,8 +421,6 @@ class MiniAppSubscriptionResponse(BaseModel): trial_duration_days: Optional[int] = None trial_status: Optional[str] = None - model_config = ConfigDict(extra="allow") - class MiniAppSubscriptionServerOption(BaseModel): uuid: str diff --git a/miniapp/index.html b/miniapp/index.html index 495cd403..1023062b 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -1524,149 +1524,6 @@ box-shadow: var(--shadow-sm); } - .subscription-autopay-section { - display: flex; - flex-direction: column; - gap: 16px; - margin-top: 20px; - padding: 16px; - border-radius: var(--radius-lg); - background: var(--bg-primary); - border: 1px solid rgba(15, 23, 42, 0.08); - } - - .subscription-autopay-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - } - - .subscription-autopay-title { - font-size: 15px; - font-weight: 700; - color: var(--text-primary); - } - - .subscription-autopay-description { - margin-top: 4px; - font-size: 13px; - color: var(--text-secondary); - line-height: 1.4; - } - - .subscription-autopay-status { - font-size: 13px; - font-weight: 600; - color: var(--text-secondary); - display: inline-flex; - align-items: center; - gap: 6px; - white-space: nowrap; - } - - .subscription-autopay-status::before { - content: ''; - width: 8px; - height: 8px; - border-radius: 50%; - background: currentColor; - opacity: 0.7; - } - - .subscription-autopay-status.enabled { - color: var(--success); - } - - .subscription-autopay-status.disabled { - color: var(--text-secondary); - } - - .subscription-autopay-status.saving { - color: var(--primary); - } - - .subscription-autopay-status.loading { - color: var(--text-secondary); - } - - .subscription-autopay-toggle-group { - display: flex; - gap: 12px; - } - - .subscription-autopay-toggle { - flex: 1; - padding: 14px; - border-radius: var(--radius); - border: 1px solid var(--border-color); - background: var(--bg-primary); - color: var(--text-primary); - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - } - - .subscription-autopay-toggle:hover:not(:disabled) { - border-color: var(--primary); - box-shadow: var(--shadow-sm); - transform: translateY(-1px); - } - - .subscription-autopay-toggle.active { - background: linear-gradient(135deg, var(--primary), rgba(var(--primary-rgb), 0.85)); - color: var(--tg-theme-button-text-color); - border-color: transparent; - box-shadow: var(--shadow-md); - } - - .subscription-autopay-toggle:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - } - - .subscription-autopay-days { - display: flex; - flex-direction: column; - gap: 12px; - } - - .subscription-autopay-days-title { - font-size: 13px; - font-weight: 600; - color: var(--text-secondary); - } - - .subscription-autopay-days-options { - display: flex; - gap: 12px; - overflow-x: auto; - padding-bottom: 4px; - margin-right: -8px; - padding-right: 8px; - } - - .subscription-autopay-days-options::-webkit-scrollbar { - display: none; - } - - .subscription-autopay-days-options .subscription-settings-toggle { - min-width: 140px; - flex: 0 0 auto; - } - - .subscription-autopay-hint { - font-size: 13px; - color: var(--text-secondary); - } - - :root[data-theme="dark"] .subscription-autopay-section { - border-color: rgba(148, 163, 184, 0.2); - background: rgba(15, 23, 42, 0.6); - } - .subscription-renewal-summary-header { display: flex; justify-content: space-between; @@ -4847,24 +4704,6 @@ -
-
-
-
Автоплатеж
-
Автоматически продлеваем подписку перед окончанием срока.
-
-
Loading…
-
- - -
- - -
-
@@ -5746,24 +5585,6 @@ 'trial.activation.error.already_active': 'You already have an active subscription.', 'autopay.enabled': 'Enabled', 'autopay.disabled': 'Disabled', - 'subscription_autopay.title': 'Auto-pay', - 'subscription_autopay.subtitle': 'Automatically renew your subscription before it expires.', - 'subscription_autopay.status.enabled': 'Enabled', - 'subscription_autopay.status.disabled': 'Disabled', - 'subscription_autopay.status.loading': 'Loading…', - 'subscription_autopay.action.enable': 'Enable', - 'subscription_autopay.action.disable': 'Disable', - 'subscription_autopay.days.title': 'Charge before expiry', - 'subscription_autopay.day.same_day': 'On renewal day', - 'subscription_autopay.day.before.one': '{count} day before', - 'subscription_autopay.day.before.few': '{count} days before', - 'subscription_autopay.day.before.many': '{count} days before', - 'subscription_autopay.saving': 'Saving…', - 'subscription_autopay.hint.disabled': 'Enable auto-pay to choose when to charge your balance.', - 'subscription_autopay.hint.no_options': 'Auto-pay day selection is temporarily unavailable.', - 'subscription_autopay.error.generic': 'Failed to update auto-pay settings. Please try again later.', - 'subscription_autopay.error.unauthorized': 'Authorization failed. Please reopen the mini app from Telegram.', - 'subscription_autopay.error.no_days': 'Select a charge day to enable auto-pay.', 'platform.ios': 'iOS', 'platform.android': 'Android', 'platform.pc': 'PC', @@ -6141,24 +5962,6 @@ 'trial.activation.error.already_active': 'У вас уже есть активная подписка.', 'autopay.enabled': 'Включен', 'autopay.disabled': 'Выключен', - 'subscription_autopay.title': 'Автоплатеж', - 'subscription_autopay.subtitle': 'Автоматически продлеваем подписку перед окончанием срока.', - 'subscription_autopay.status.enabled': 'Включен', - 'subscription_autopay.status.disabled': 'Выключен', - 'subscription_autopay.status.loading': 'Загружаем…', - 'subscription_autopay.action.enable': 'Включить', - 'subscription_autopay.action.disable': 'Отключить', - 'subscription_autopay.days.title': 'За сколько списывать', - 'subscription_autopay.day.same_day': 'В день окончания', - 'subscription_autopay.day.before.one': '{count} день до списания', - 'subscription_autopay.day.before.few': '{count} дня до списания', - 'subscription_autopay.day.before.many': '{count} дней до списания', - 'subscription_autopay.saving': 'Сохраняем…', - 'subscription_autopay.hint.disabled': 'Включите автоплатеж, чтобы выбрать день списания.', - 'subscription_autopay.hint.no_options': 'Выбор дня списания временно недоступен.', - 'subscription_autopay.error.generic': 'Не удалось обновить настройки автоплатежа. Попробуйте позже.', - 'subscription_autopay.error.unauthorized': 'Ошибка авторизации. Откройте мини-приложение из Telegram и повторите попытку.', - 'subscription_autopay.error.no_days': 'Выберите день списания, чтобы включить автоплатеж.', 'platform.ios': 'iOS', 'platform.android': 'Android', 'platform.pc': 'ПК', @@ -6353,15 +6156,6 @@ periodId: null, }; - let subscriptionAutopayState = { - enabled: false, - daysBefore: null, - defaultDaysBefore: null, - options: [], - loading: true, - saving: false, - }; - let trialActivationInProgress = false; const PAYMENT_STATUS_INITIAL_DELAY_MS = 2000; @@ -7394,31 +7188,6 @@ userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; userData.referral = userData.referral || null; - if (hasPaidSubscription()) { - subscriptionAutopayState.loading = true; - subscriptionAutopayState.saving = false; - subscriptionAutopayState.defaultDaysBefore = null; - subscriptionAutopayState.options = []; - ingestAutopayData( - payload, - payload?.autopay, - payload?.autopay_settings, - payload?.autopaySettings, - payload?.subscription_renewal, - payload?.subscriptionRenewal, - payload?.subscription_renewal?.autopay, - payload?.subscriptionRenewal?.autopay, - ); - } else { - subscriptionAutopayState.enabled = false; - subscriptionAutopayState.daysBefore = null; - subscriptionAutopayState.defaultDaysBefore = null; - subscriptionAutopayState.options = []; - subscriptionAutopayState.loading = false; - subscriptionAutopayState.saving = false; - renderSubscriptionAutopay(); - } - resetSubscriptionRenewalState(null); prepareSubscriptionRenewalFromUserData(); @@ -8974,566 +8743,6 @@ } } - function resolvePluralForm(count, language = preferredLanguage) { - const normalizedLang = (language || preferredLanguage || 'en').split('-')[0].toLowerCase(); - if (normalizedLang === 'ru') { - const mod10 = count % 10; - const mod100 = count % 100; - if (mod10 === 1 && mod100 !== 11) { - return 'one'; - } - if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) { - return 'few'; - } - return 'many'; - } - return count === 1 ? 'one' : 'many'; - } - - function formatAutopayDayLabel(days) { - const value = coercePositiveInt(days, null); - if (value === null) { - return ''; - } - if (value === 0) { - const sameDayKey = 'subscription_autopay.day.same_day'; - const sameDay = t(sameDayKey); - if (sameDay && sameDay !== sameDayKey) { - return sameDay; - } - return preferredLanguage === 'ru' ? 'В день окончания' : 'On renewal day'; - } - const pluralKey = resolvePluralForm(value); - const key = `subscription_autopay.day.before.${pluralKey}`; - const template = t(key); - if (template && template !== key) { - return template.replace('{count}', String(value)); - } - if (preferredLanguage === 'ru') { - if (pluralKey === 'one') { - return `${value} день до списания`; - } - if (pluralKey === 'few') { - return `${value} дня до списания`; - } - return `${value} дней до списания`; - } - return pluralKey === 'one' - ? `${value} day before` - : `${value} days before`; - } - - function renderSubscriptionAutopay() { - const section = document.getElementById('subscriptionAutopaySection'); - if (!section) { - return; - } - - const shouldShow = hasPaidSubscription(); - section.classList.toggle('hidden', !shouldShow); - if (!shouldShow) { - return; - } - - const statusElement = document.getElementById('subscriptionAutopayStatus'); - const enableButton = document.getElementById('subscriptionAutopayEnable'); - const disableButton = document.getElementById('subscriptionAutopayDisable'); - const daysContainer = document.getElementById('subscriptionAutopayDays'); - const optionsContainer = document.getElementById('subscriptionAutopayDaysOptions'); - const hintElement = document.getElementById('subscriptionAutopayHint'); - - const enabled = Boolean(subscriptionAutopayState.enabled); - const saving = Boolean(subscriptionAutopayState.saving); - const loading = Boolean(subscriptionAutopayState.loading); - const options = Array.isArray(subscriptionAutopayState.options) - ? subscriptionAutopayState.options.slice().sort((a, b) => a - b) - : []; - const hasOptions = options.length > 0; - - if (statusElement) { - if (saving) { - const savingKey = 'subscription_autopay.saving'; - const savingText = t(savingKey); - statusElement.textContent = savingText && savingText !== savingKey ? savingText : 'Saving…'; - statusElement.className = 'subscription-autopay-status saving'; - } else if (loading) { - const loadingKey = 'subscription_autopay.status.loading'; - const loadingText = t(loadingKey); - statusElement.textContent = loadingText && loadingText !== loadingKey ? loadingText : 'Loading…'; - statusElement.className = 'subscription-autopay-status loading'; - } else { - const key = enabled - ? 'subscription_autopay.status.enabled' - : 'subscription_autopay.status.disabled'; - const label = t(key); - statusElement.textContent = label && label !== key - ? label - : (enabled ? 'Enabled' : 'Disabled'); - statusElement.className = `subscription-autopay-status ${enabled ? 'enabled' : 'disabled'}`; - } - } - - if (enableButton) { - enableButton.disabled = saving || loading || enabled; - enableButton.classList.toggle('active', enabled && !loading); - } - - if (disableButton) { - disableButton.disabled = saving || loading || !enabled; - disableButton.classList.toggle('active', !enabled && !loading); - } - - if (daysContainer) { - daysContainer.classList.toggle('hidden', !enabled || !hasOptions); - } - - if (hintElement) { - if (!enabled) { - const hintKey = 'subscription_autopay.hint.disabled'; - const hint = t(hintKey); - hintElement.textContent = hint && hint !== hintKey - ? hint - : (preferredLanguage === 'ru' - ? 'Включите автоплатеж, чтобы выбрать день списания.' - : 'Enable auto-pay to choose when to charge your balance.'); - hintElement.classList.remove('hidden'); - } else if (enabled && !hasOptions && !loading) { - const hintKey = 'subscription_autopay.hint.no_options'; - const hint = t(hintKey); - hintElement.textContent = hint && hint !== hintKey - ? hint - : (preferredLanguage === 'ru' - ? 'Выбор дня списания временно недоступен.' - : 'Auto-pay day selection is temporarily unavailable.'); - hintElement.classList.remove('hidden'); - } else { - hintElement.classList.add('hidden'); - hintElement.textContent = ''; - } - } - - if (optionsContainer) { - optionsContainer.innerHTML = ''; - if (enabled && hasOptions) { - options.forEach(value => { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'subscription-settings-toggle'; - button.dataset.autopayDays = String(value); - button.disabled = saving; - - if (subscriptionAutopayState.daysBefore === value) { - button.classList.add('active'); - } - - const label = document.createElement('div'); - label.className = 'subscription-settings-toggle-label'; - - const title = document.createElement('div'); - title.className = 'subscription-settings-toggle-title'; - title.textContent = formatAutopayDayLabel(value); - - label.appendChild(title); - button.appendChild(label); - optionsContainer.appendChild(button); - }); - } - } - } - - function normalizeAutopayPayload(raw) { - if (!raw || typeof raw !== 'object') { - return null; - } - - const enabledCandidate = raw.autopay_enabled - ?? raw.enabled - ?? raw.is_enabled - ?? raw.active - ?? null; - let enabledValue; - if (typeof enabledCandidate === 'boolean') { - enabledValue = enabledCandidate; - } else if (enabledCandidate != null) { - enabledValue = coerceBoolean(enabledCandidate, undefined); - } - - const daysCandidate = raw.autopay_days_before - ?? raw.days_before - ?? raw.daysBefore - ?? raw.days - ?? raw.value - ?? null; - const daysBefore = daysCandidate != null - ? coercePositiveInt(daysCandidate, null) - : null; - - const optionSet = new Set(); - const optionSources = [ - raw.autopay_days_options, - raw.autopayDaysOptions, - raw.days_options, - raw.daysOptions, - raw.available_days, - raw.availableDays, - ]; - - optionSources.forEach(source => { - if (!source) { - return; - } - const normalized = Array.isArray(source) - ? source - : (typeof source === 'object' ? Object.values(source) : [source]); - normalized.forEach(item => { - if (item == null) { - return; - } - if (typeof item === 'object') { - const candidate = item.days_before - ?? item.daysBefore - ?? item.value - ?? item.days - ?? item.amount - ?? null; - const numeric = candidate != null ? coercePositiveInt(candidate, null) : null; - if (numeric !== null) { - optionSet.add(numeric); - } - return; - } - const numeric = coercePositiveInt(item, null); - if (numeric !== null) { - optionSet.add(numeric); - } - }); - }); - - const options = Array.from(optionSet).sort((a, b) => a - b); - if (daysBefore !== null && !optionSet.has(daysBefore)) { - options.push(daysBefore); - options.sort((a, b) => a - b); - } - - return { - enabled: typeof enabledValue === 'boolean' ? enabledValue : undefined, - daysBefore: daysBefore !== null ? daysBefore : null, - options, - }; - } - - const DEFAULT_AUTOPAY_DAY_OPTIONS = [1, 3, 7, 14]; - - function mergeAutopaySources(...sources) { - let hasData = false; - let enabledValue; - let daysValue = null; - const optionSet = new Set(); - - DEFAULT_AUTOPAY_DAY_OPTIONS.forEach(value => { - const numeric = coercePositiveInt(value, null); - if (numeric !== null) { - optionSet.add(numeric); - } - }); - - sources.forEach(source => { - const normalized = normalizeAutopayPayload(source); - if (!normalized) { - return; - } - hasData = true; - if (typeof normalized.enabled === 'boolean') { - enabledValue = normalized.enabled; - } - if (normalized.daysBefore !== null && normalized.daysBefore !== undefined) { - daysValue = normalized.daysBefore; - } - normalized.options.forEach(value => optionSet.add(value)); - }); - - if (!hasData) { - return null; - } - - if (daysValue !== null && daysValue !== undefined) { - optionSet.add(daysValue); - } - - const options = Array.from(optionSet).sort((a, b) => a - b); - let resolvedDays = daysValue; - if ((resolvedDays === null || resolvedDays === undefined) && options.length) { - resolvedDays = options[0]; - } - - return { - enabled: enabledValue, - daysBefore: resolvedDays, - options, - }; - } - - function ingestAutopayData(...sources) { - const candidates = sources.filter(Boolean); - if (!candidates.length) { - subscriptionAutopayState.loading = false; - renderSubscriptionAutopay(); - return; - } - - const normalized = mergeAutopaySources(...candidates); - if (!normalized) { - subscriptionAutopayState.loading = false; - renderSubscriptionAutopay(); - return; - } - - if (typeof normalized.enabled === 'boolean') { - subscriptionAutopayState.enabled = normalized.enabled; - } - - if (normalized.daysBefore !== null && normalized.daysBefore !== undefined) { - subscriptionAutopayState.daysBefore = normalized.daysBefore; - if (subscriptionAutopayState.defaultDaysBefore === null || subscriptionAutopayState.defaultDaysBefore === undefined) { - subscriptionAutopayState.defaultDaysBefore = normalized.daysBefore; - } - } - - subscriptionAutopayState.options = Array.isArray(normalized.options) - ? normalized.options.slice().sort((a, b) => a - b) - : []; - - if ((subscriptionAutopayState.daysBefore === null || subscriptionAutopayState.daysBefore === undefined) - && subscriptionAutopayState.options.length) { - subscriptionAutopayState.daysBefore = subscriptionAutopayState.options[0]; - } - - subscriptionAutopayState.loading = false; - renderSubscriptionAutopay(); - } - - function extractAutopayError(payload, status) { - if (status === 401) { - return t('subscription_autopay.error.unauthorized'); - } - if (!payload || typeof payload !== 'object') { - return t('subscription_autopay.error.generic'); - } - if (typeof payload.detail === 'string') { - return payload.detail; - } - if (payload.detail && typeof payload.detail === 'object') { - if (typeof payload.detail.message === 'string') { - return payload.detail.message; - } - if (typeof payload.detail.error === 'string') { - return payload.detail.error; - } - } - if (typeof payload.message === 'string') { - return payload.message; - } - if (typeof payload.error === 'string') { - return payload.error; - } - return t('subscription_autopay.error.generic'); - } - - function resolveAutopayErrorMessage(error, fallbackKey = 'subscription_autopay.error.generic') { - if (!error) { - return t(fallbackKey); - } - if (typeof error === 'string') { - return error; - } - if (typeof error.message === 'string' && error.message.trim()) { - return error.message; - } - if (error.detail) { - if (typeof error.detail === 'string' && error.detail.trim()) { - return error.detail; - } - if (typeof error.detail.message === 'string' && error.detail.message.trim()) { - return error.detail.message; - } - } - if (error.status === 401) { - return t('subscription_autopay.error.unauthorized'); - } - return t(fallbackKey); - } - - function handleAutopayError(error) { - const message = resolveAutopayErrorMessage(error); - const titleKey = 'subscription_autopay.title'; - const title = t(titleKey); - showPopup(message, title && title !== titleKey ? title : 'Auto-pay'); - } - - async function submitAutopaySettingsChange(changes = {}) { - if (subscriptionAutopayState.saving || subscriptionAutopayState.loading) { - return; - } - - if (!hasPaidSubscription()) { - handleAutopayError(createError('Auto-pay', t('subscription_autopay.error.generic'))); - return; - } - - const initData = tg.initData || ''; - if (!initData) { - handleAutopayError(createError('Authorization Error', t('subscription_autopay.error.unauthorized'))); - return; - } - - const subscriptionId = userData?.subscription_id ?? userData?.subscriptionId ?? null; - if (!subscriptionId) { - handleAutopayError(createError('Auto-pay', t('subscription_autopay.error.generic'))); - return; - } - - const previousState = { - ...subscriptionAutopayState, - options: [...(subscriptionAutopayState.options || [])], - }; - - const targetEnabled = typeof changes.enabled === 'boolean' - ? changes.enabled - : subscriptionAutopayState.enabled; - - let targetDays = changes.daysBefore; - if (targetDays === undefined || targetDays === null) { - targetDays = subscriptionAutopayState.daysBefore; - } - if (targetEnabled && (targetDays === undefined || targetDays === null)) { - if (subscriptionAutopayState.defaultDaysBefore !== null && subscriptionAutopayState.defaultDaysBefore !== undefined) { - targetDays = subscriptionAutopayState.defaultDaysBefore; - } else if (subscriptionAutopayState.options.length) { - targetDays = subscriptionAutopayState.options[0]; - } - } - targetDays = targetDays !== undefined && targetDays !== null - ? coercePositiveInt(targetDays, null) - : null; - - if (targetEnabled && (targetDays === null || targetDays === undefined)) { - handleAutopayError(createError('Auto-pay', t('subscription_autopay.error.no_days'))); - renderSubscriptionAutopay(); - return; - } - - subscriptionAutopayState.enabled = targetEnabled; - if (targetEnabled) { - subscriptionAutopayState.daysBefore = targetDays; - if (subscriptionAutopayState.defaultDaysBefore === null || subscriptionAutopayState.defaultDaysBefore === undefined) { - subscriptionAutopayState.defaultDaysBefore = targetDays; - } - } - subscriptionAutopayState.saving = true; - renderSubscriptionAutopay(); - - try { - const payload = { - initData, - subscription_id: subscriptionId, - subscriptionId, - enabled: targetEnabled, - days_before: targetDays, - daysBefore: targetDays, - }; - - const response = await fetch('/miniapp/subscription/autopay', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - const body = await parseJsonSafe(response); - if (!response.ok || (body && body.success === false)) { - const message = extractAutopayError(body, response.status); - throw createError('Auto-pay', message, response.status); - } - - subscriptionAutopayState.saving = false; - - if (body && typeof body === 'object') { - ingestAutopayData( - body.autopay - || body.data - || body.subscription - || body.settings - || body, - ); - } else { - renderSubscriptionAutopay(); - } - - await refreshSubscriptionData({ silent: true }); - await ensureSubscriptionRenewalData({ force: true }); - } catch (error) { - subscriptionAutopayState = { - ...previousState, - options: [...previousState.options], - saving: false, - }; - renderSubscriptionAutopay(); - handleAutopayError(error); - } - } - - function handleAutopayToggle(enabled) { - if (typeof enabled !== 'boolean') { - return; - } - if (subscriptionAutopayState.loading || subscriptionAutopayState.saving) { - return; - } - if (enabled === subscriptionAutopayState.enabled) { - return; - } - submitAutopaySettingsChange({ enabled }); - } - - function handleAutopayDaySelection(days) { - if (subscriptionAutopayState.loading || subscriptionAutopayState.saving) { - return; - } - if (!subscriptionAutopayState.enabled) { - return; - } - const value = coercePositiveInt(days, null); - if (value === null || value === undefined) { - return; - } - if (subscriptionAutopayState.daysBefore === value) { - return; - } - if (!subscriptionAutopayState.options.includes(value)) { - return; - } - submitAutopaySettingsChange({ daysBefore: value }); - } - - function setupSubscriptionAutopayEvents() { - document.getElementById('subscriptionAutopayEnable')?.addEventListener('click', () => { - handleAutopayToggle(true); - }); - document.getElementById('subscriptionAutopayDisable')?.addEventListener('click', () => { - handleAutopayToggle(false); - }); - document.getElementById('subscriptionAutopayDaysOptions')?.addEventListener('click', event => { - const target = event.target.closest('button[data-autopay-days]'); - if (!target || target.disabled) { - return; - } - const value = coercePositiveInt(target.dataset.autopayDays, null); - if (value === null || value === undefined) { - return; - } - handleAutopayDaySelection(value); - }); - } - function isSameSet(a, b) { if (!(a instanceof Set) || !(b instanceof Set)) { return false; @@ -12816,16 +12025,6 @@ const promoOffer = normalizeRenewalPromoOffer(root.promo_offer ?? root.promoOffer); - const autopayData = mergeAutopaySources( - root.autopay, - root.autopay_settings, - root.autopaySettings, - root, - payload.autopay, - payload.autopay_settings, - payload.autopaySettings, - ); - return { subscriptionId, currency, @@ -12837,7 +12036,6 @@ promoOffer, missingAmountKopeks, statusMessage, - autopay: autopayData, }; } @@ -12855,9 +12053,6 @@ if (normalized && Array.isArray(normalized.periods) && normalized.periods.length) { subscriptionRenewalData = normalized; resetSubscriptionRenewalSelection(normalized); - if (normalized.autopay) { - ingestAutopayData(normalized.autopay); - } return; } } @@ -12940,9 +12135,6 @@ subscriptionRenewalError = null; subscriptionRenewalLoading = false; resetSubscriptionRenewalSelection(inlineNormalized); - if (inlineNormalized.autopay) { - ingestAutopayData(inlineNormalized.autopay); - } renderSubscriptionRenewalCard(); return Promise.resolve(inlineNormalized); } @@ -12982,9 +12174,6 @@ subscriptionRenewalLoading = false; subscriptionRenewalPromise = null; resetSubscriptionRenewalSelection(normalized); - if (normalized.autopay) { - ingestAutopayData(normalized.autopay); - } renderSubscriptionRenewalCard(); return normalized; }).catch(error => { @@ -13511,7 +12700,6 @@ renderSubscriptionRenewalMeta(subscriptionRenewalData); renderSubscriptionRenewalOptions(subscriptionRenewalData); renderSubscriptionRenewalSummary(subscriptionRenewalData); - renderSubscriptionAutopay(); } async function confirmSubscriptionRenewal(option, data) { @@ -13643,7 +12831,6 @@ } function setupSubscriptionRenewalEvents() { - setupSubscriptionAutopayEvents(); document.getElementById('subscriptionRenewalRetry')?.addEventListener('click', () => { ensureSubscriptionRenewalData({ force: true }).catch(error => { console.warn('Failed to reload renewal options:', error);