From 7783cbc61d26e835565d9cb3d1dd9a6900a5fc74 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 03:48:45 +0300 Subject: [PATCH 1/3] Update index.ts --- src/types/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types/index.ts b/src/types/index.ts index 0259362..0d9e1ff 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -300,6 +300,10 @@ export interface TariffsPurchaseOptions { current_tariff_id: number | null; balance_kopeks: number; balance_label: string; + // New fields for expired subscription handling + subscription_status?: string; + subscription_is_expired?: boolean; + has_subscription?: boolean; } export interface ClassicPurchaseOptions { From 192ac71a02381843fd00c26767103f72502b653a Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 03:49:06 +0300 Subject: [PATCH 2/3] Update Subscription.tsx --- src/pages/Subscription.tsx | 88 +++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 402de39..e5b1891 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -312,6 +312,28 @@ export default function Subscription() { queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); setSwitchTariffId(null); }, + onError: (error: unknown) => { + // Handle subscription_expired error - redirect to purchase flow + if (error instanceof AxiosError) { + const detail = error.response?.data?.detail; + if ( + typeof detail === 'object' && + detail?.error_code === 'subscription_expired' && + detail?.use_purchase_flow === true + ) { + // Find the tariff user was trying to switch to and open purchase form + const targetTariff = tariffs.find((t) => t.id === switchTariffId); + if (targetTariff) { + setSwitchTariffId(null); + setSelectedTariff(targetTariff); + setSelectedTariffPeriod(targetTariff.periods[0] || null); + setShowTariffPurchase(true); + // Refetch purchase-options to get updated expired status + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); + } + } + } + }, }); // Tariff purchase mutation @@ -1955,6 +1977,40 @@ export default function Subscription() { )} + {/* Expired subscription notice - prompt to purchase new tariff */} + {isTariffsMode && + purchaseOptions && + 'subscription_is_expired' in purchaseOptions && + purchaseOptions.subscription_is_expired && ( +
+
+
+ + + +
+
+
+ {t('subscription.expired.title')} +
+
+ {t('subscription.expired.selectTariff')} +
+
+
+
+ )} + {/* Legacy subscription notice - if user has subscription without tariff */} {subscription && !subscription.is_trial && !subscription.tariff_id && (
@@ -2066,6 +2122,27 @@ export default function Subscription() { t('subscription.switchTariff.switch') )} + + {/* Show error (except subscription_expired which redirects to purchase) */} + {switchTariffMutation.isError && + (() => { + const detail = + switchTariffMutation.error instanceof AxiosError + ? switchTariffMutation.error.response?.data?.detail + : null; + // Skip displaying if it's subscription_expired (handled by redirect) + if ( + typeof detail === 'object' && + detail?.error_code === 'subscription_expired' + ) { + return null; + } + return ( +
+ {getErrorMessage(switchTariffMutation.error)} +
+ ); + })()} ); })() @@ -2125,11 +2202,20 @@ export default function Subscription() { .map((tariff) => { const isCurrentTariff = tariff.is_current || tariff.id === subscription?.tariff_id; + // Check if subscription is expired from purchaseOptions + const isSubscriptionExpired = + isTariffsMode && + purchaseOptions && + 'subscription_is_expired' in purchaseOptions && + purchaseOptions.subscription_is_expired === true; + // canSwitch only if subscription is active (not expired, not trial) const canSwitch = subscription && subscription.tariff_id && !isCurrentTariff && - !subscription.is_trial; + !subscription.is_trial && + !isSubscriptionExpired && + subscription.is_active; // Если есть подписка БЕЗ tariff_id (классическая) - разрешить выбрать тариф const isLegacySubscription = subscription && !subscription.is_trial && !subscription.tariff_id; From 334b2718ee98abc183f2b4890b04e6504caa6f81 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 03:49:26 +0300 Subject: [PATCH 3/3] Add files via upload --- src/locales/en.json | 4 ++++ src/locales/ru.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index 2eb78e2..d98e720 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -320,6 +320,10 @@ "title": "Choose a plan to continue", "description": "Your trial period is ending soon. Select a plan to continue using VPN without restrictions." }, + "expired": { + "title": "Subscription expired", + "selectTariff": "Your subscription has expired. Choose a plan below to restore access to VPN." + }, "connection": { "title": "Connect VPN", "selectDevice": "Select your device", diff --git a/src/locales/ru.json b/src/locales/ru.json index 22beb2b..d0acebb 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -342,6 +342,10 @@ "title": "Выберите тариф для продолжения", "description": "Ваш пробный период скоро закончится. Выберите подходящий тариф, чтобы продолжить пользоваться VPN без ограничений." }, + "expired": { + "title": "Подписка истекла", + "selectTariff": "Ваша подписка истекла. Выберите тариф ниже, чтобы возобновить доступ к VPN." + }, "connection": { "title": "Подключить VPN", "selectDevice": "Выберите устройство",