Merge pull request #153 from BEDOLAGA-DEV/feat/blacklist-blocking-screen

feat: add blacklisted user blocking screen
This commit is contained in:
Egor
2026-02-06 15:54:06 +03:00
committed by GitHub
9 changed files with 123 additions and 2 deletions

View File

@@ -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 <ChannelSubscriptionScreen />;
}
if (blockingType === 'blacklisted') {
return <BlacklistedScreen />;
}
return null;
}

View File

@@ -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 - пусть ошибка дойдет до компонента

View File

@@ -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 (
<div className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-dark-950 p-6">
<div className="w-full max-w-md text-center">
{/* Icon */}
<div className="mb-8">
<div className="mx-auto flex h-24 w-24 items-center justify-center rounded-full bg-dark-800">
<svg
className="h-12 w-12 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
/>
</svg>
</div>
</div>
{/* Title */}
<h1 className="mb-4 text-2xl font-bold text-white">{t('blocking.blacklisted.title')}</h1>
{/* Message */}
<p className="mb-6 text-lg text-gray-400">{t('blocking.blacklisted.defaultMessage')}</p>
{/* Reason */}
{blacklistedInfo?.message && (
<div className="mb-6 rounded-xl bg-dark-800/50 p-4">
<p className="mb-1 text-sm text-gray-500">{t('blocking.blacklisted.reason')}:</p>
<p className="text-gray-300">{blacklistedInfo.message}</p>
</div>
)}
<p className="mt-8 text-sm text-gray-500">{t('blocking.blacklisted.contactSupport')}</p>
</div>
</div>
);
}

View File

@@ -1,2 +1,3 @@
export { default as MaintenanceScreen } from './MaintenanceScreen';
export { default as ChannelSubscriptionScreen } from './ChannelSubscriptionScreen';
export { default as BlacklistedScreen } from './BlacklistedScreen';

View File

@@ -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."
}
}
}

View File

@@ -2271,6 +2271,12 @@
"hint": "پس از عضویت دکمه بررسی را بزنید",
"notSubscribed": "شما هنوز در کانال عضو نشده‌اید",
"checkError": "بررسی ناموفق بود. لطفاً بعداً دوباره امتحان کنید."
},
"blacklisted": {
"title": "دسترسی ممنوع",
"defaultMessage": "حساب شما مسدود شده است.",
"reason": "دلیل",
"contactSupport": "اگر فکر می‌کنید این اشتباه است، با پشتیبانی تماس بگیرید."
}
}
}

View File

@@ -3340,6 +3340,12 @@
"hint": "После подписки нажмите кнопку проверки",
"notSubscribed": "Вы ещё не подписались на канал",
"checkError": "Ошибка проверки. Попробуйте позже."
},
"blacklisted": {
"title": "Доступ запрещён",
"defaultMessage": "Ваш аккаунт заблокирован.",
"reason": "Причина",
"contactSupport": "Если вы считаете, что это ошибка, обратитесь в поддержку."
}
}
}

View File

@@ -2152,6 +2152,12 @@
"hint": "订阅后点击检查按钮",
"notSubscribed": "您还没有订阅该频道",
"checkError": "检查失败。请稍后重试。"
},
"blacklisted": {
"title": "访问被拒绝",
"defaultMessage": "您的帐户已被封锁。",
"reason": "原因",
"contactSupport": "如果您认为这是错误,请联系客服。"
}
},
"banSystem": {

View File

@@ -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<BlockingState>((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<BlockingState>((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<BlockingState>((set) => ({
blockingType: null,
maintenanceInfo: null,
channelInfo: null,
blacklistedInfo: null,
}),
}));