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,
}),
}));