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 @@
-
-
-
-
За сколько списывать
-
-
-
-
-
-
-
-
@@ -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);