diff --git a/src/App.tsx b/src/App.tsx index 7cd8f0f..9935cf0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,11 @@ import { useAuthStore } from './store/auth'; import { useBlockingStore } from './store/blocking'; import Layout from './components/layout/Layout'; import PageLoader from './components/common/PageLoader'; -import { MaintenanceScreen, ChannelSubscriptionScreen } from './components/blocking'; +import { + MaintenanceScreen, + ChannelSubscriptionScreen, + BlacklistedScreen, +} from './components/blocking'; import { saveReturnUrl } from './utils/token'; import { useAnalyticsCounters } from './hooks/useAnalyticsCounters'; // Auth pages - load immediately (small) @@ -122,6 +126,10 @@ function BlockingOverlay() { return ; } + if (blockingType === 'blacklisted') { + return ; + } + return null; } diff --git a/src/api/client.ts b/src/api/client.ts index 49d7b0a..1240afd 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -111,6 +111,11 @@ export interface ChannelSubscriptionError { channel_link?: string; } +export interface BlacklistedError { + code: 'blacklisted'; + message: string; +} + export function isMaintenanceError( error: unknown, ): error is { response: { status: 503; data: { detail: MaintenanceError } } } { @@ -130,6 +135,14 @@ export function isChannelSubscriptionError( ); } +export function isBlacklistedError( + error: unknown, +): error is { response: { status: 403; data: { detail: BlacklistedError } } } { + if (!error || typeof error !== 'object') return false; + const err = error as AxiosError<{ detail: BlacklistedError }>; + return err.response?.status === 403 && err.response?.data?.detail?.code === 'blacklisted'; +} + // Response interceptor - handle 401, 503 (maintenance), 403 (channel subscription) apiClient.interceptors.response.use( (response) => response, @@ -156,6 +169,15 @@ apiClient.interceptors.response.use( return Promise.reject(error); } + // Handle blacklisted user (403) + if (isBlacklistedError(error)) { + const detail = (error.response?.data as { detail: BlacklistedError }).detail; + useBlockingStore.getState().setBlacklisted({ + message: detail.message, + }); + return Promise.reject(error); + } + // Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала) if (error.response?.status === 401 && !originalRequest._retry) { // Не обрабатываем 401 для авторизационных endpoints - пусть ошибка дойдет до компонента diff --git a/src/components/blocking/BlacklistedScreen.tsx b/src/components/blocking/BlacklistedScreen.tsx new file mode 100644 index 0000000..1cbedb8 --- /dev/null +++ b/src/components/blocking/BlacklistedScreen.tsx @@ -0,0 +1,48 @@ +import { useTranslation } from 'react-i18next'; +import { useBlockingStore } from '../../store/blocking'; + +export default function BlacklistedScreen() { + const { t } = useTranslation(); + const { blacklistedInfo } = useBlockingStore(); + + return ( +
+
+ {/* Icon */} +
+
+ + + +
+
+ + {/* Title */} +

{t('blocking.blacklisted.title')}

+ + {/* Message */} +

{t('blocking.blacklisted.defaultMessage')}

+ + {/* Reason */} + {blacklistedInfo?.message && ( +
+

{t('blocking.blacklisted.reason')}:

+

{blacklistedInfo.message}

+
+ )} + +

{t('blocking.blacklisted.contactSupport')}

+
+
+ ); +} diff --git a/src/components/blocking/index.ts b/src/components/blocking/index.ts index ca8e854..d72b87d 100644 --- a/src/components/blocking/index.ts +++ b/src/components/blocking/index.ts @@ -1,2 +1,3 @@ export { default as MaintenanceScreen } from './MaintenanceScreen'; export { default as ChannelSubscriptionScreen } from './ChannelSubscriptionScreen'; +export { default as BlacklistedScreen } from './BlacklistedScreen'; diff --git a/src/locales/en.json b/src/locales/en.json index a12b571..f8f29b6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2627,6 +2627,12 @@ "hint": "Click check button after subscribing", "notSubscribed": "You haven't subscribed to the channel yet", "checkError": "Check failed. Please try again later." + }, + "blacklisted": { + "title": "Access Denied", + "defaultMessage": "Your account has been blocked.", + "reason": "Reason", + "contactSupport": "If you believe this is an error, please contact support." } } } diff --git a/src/locales/fa.json b/src/locales/fa.json index b4fa28b..c66978e 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -2271,6 +2271,12 @@ "hint": "پس از عضویت دکمه بررسی را بزنید", "notSubscribed": "شما هنوز در کانال عضو نشده‌اید", "checkError": "بررسی ناموفق بود. لطفاً بعداً دوباره امتحان کنید." + }, + "blacklisted": { + "title": "دسترسی ممنوع", + "defaultMessage": "حساب شما مسدود شده است.", + "reason": "دلیل", + "contactSupport": "اگر فکر می‌کنید این اشتباه است، با پشتیبانی تماس بگیرید." } } } diff --git a/src/locales/ru.json b/src/locales/ru.json index 97007ec..c0ca124 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -3340,6 +3340,12 @@ "hint": "После подписки нажмите кнопку проверки", "notSubscribed": "Вы ещё не подписались на канал", "checkError": "Ошибка проверки. Попробуйте позже." + }, + "blacklisted": { + "title": "Доступ запрещён", + "defaultMessage": "Ваш аккаунт заблокирован.", + "reason": "Причина", + "contactSupport": "Если вы считаете, что это ошибка, обратитесь в поддержку." } } } diff --git a/src/locales/zh.json b/src/locales/zh.json index fbd16d3..06e268d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2152,6 +2152,12 @@ "hint": "订阅后点击检查按钮", "notSubscribed": "您还没有订阅该频道", "checkError": "检查失败。请稍后重试。" + }, + "blacklisted": { + "title": "访问被拒绝", + "defaultMessage": "您的帐户已被封锁。", + "reason": "原因", + "contactSupport": "如果您认为这是错误,请联系客服。" } }, "banSystem": { diff --git a/src/store/blocking.ts b/src/store/blocking.ts index 3400072..7188bff 100644 --- a/src/store/blocking.ts +++ b/src/store/blocking.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -export type BlockingType = 'maintenance' | 'channel_subscription' | null; +export type BlockingType = 'maintenance' | 'channel_subscription' | 'blacklisted' | null; interface MaintenanceInfo { message: string; @@ -12,13 +12,19 @@ interface ChannelSubscriptionInfo { channel_link?: string; } +interface BlacklistedInfo { + message: string; +} + interface BlockingState { blockingType: BlockingType; maintenanceInfo: MaintenanceInfo | null; channelInfo: ChannelSubscriptionInfo | null; + blacklistedInfo: BlacklistedInfo | null; setMaintenance: (info: MaintenanceInfo) => void; setChannelSubscription: (info: ChannelSubscriptionInfo) => void; + setBlacklisted: (info: BlacklistedInfo) => void; clearBlocking: () => void; } @@ -26,12 +32,14 @@ export const useBlockingStore = create((set) => ({ blockingType: null, maintenanceInfo: null, channelInfo: null, + blacklistedInfo: null, setMaintenance: (info) => set({ blockingType: 'maintenance', maintenanceInfo: info, channelInfo: null, + blacklistedInfo: null, }), setChannelSubscription: (info) => @@ -39,6 +47,15 @@ export const useBlockingStore = create((set) => ({ blockingType: 'channel_subscription', channelInfo: info, maintenanceInfo: null, + blacklistedInfo: null, + }), + + setBlacklisted: (info) => + set({ + blockingType: 'blacklisted', + blacklistedInfo: info, + maintenanceInfo: null, + channelInfo: null, }), clearBlocking: () => @@ -46,5 +63,6 @@ export const useBlockingStore = create((set) => ({ blockingType: null, maintenanceInfo: null, channelInfo: null, + blacklistedInfo: null, }), }));