diff --git a/src/AppWithNavigator.tsx b/src/AppWithNavigator.tsx
index c89d016..0aaeb50 100644
--- a/src/AppWithNavigator.tsx
+++ b/src/AppWithNavigator.tsx
@@ -15,9 +15,10 @@ import { ThemeColorsProvider } from './providers/ThemeColorsProvider';
import { WebSocketProvider } from './providers/WebSocketProvider';
import { ToastProvider } from './components/Toast';
import { TooltipProvider } from './components/primitives/Tooltip';
-import { isInTelegramWebApp } from './hooks/useTelegramSDK';
+import { isInTelegramWebApp, closeTelegramApp } from './hooks/useTelegramSDK';
import { getFallbackParentPath } from './utils/navigation';
import { subscriptionApi } from './api/subscription';
+import { useBlockingStore } from './store/blocking';
const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const;
@@ -44,6 +45,13 @@ function TelegramBackButton() {
const pathnameRef = useRef(location.pathname);
pathnameRef.current = location.pathname;
+ // A full-screen blocking overlay (maintenance / channel-sub / blacklist /
+ // account-deleted / backend-unavailable) takes over the native back button:
+ // there is nowhere to navigate, so it becomes a single, stable EXIT control.
+ const blockingType = useBlockingStore((state) => state.blockingType);
+ const blockingTypeRef = useRef(blockingType);
+ blockingTypeRef.current = blockingType;
+
// Reliable in-app navigation depth (the app's entry point is 0). Driven by
// React Router's navigation TYPE — NOT window.history.state.idx, which the
// app's own redirects mutate unpredictably and which is the root flake behind
@@ -89,6 +97,15 @@ function TelegramBackButton() {
subsCountRef.current = subsCount;
useEffect(() => {
+ // On a blocking overlay, keep exactly one visible Back button (its click
+ // exits the app — see handler). Skip the route logic so it can't flip
+ // between Back and Close as the hidden route changes underneath.
+ if (blockingType) {
+ try {
+ showBackButton();
+ } catch {}
+ return;
+ }
const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname);
// Depth-independent on purpose: whether the user deep-linked in or navigated
// here in-app, a single-tariff detail whose list just bounces back has no
@@ -103,10 +120,17 @@ function TelegramBackButton() {
showBackButton();
}
} catch {}
- }, [location, listRedirectsToDetail]);
+ }, [location, listRedirectsToDetail, blockingType]);
// Stable handler — ref prevents re-subscription on every render
const handler = useCallback(() => {
+ // A blocking overlay is a hard block with nowhere to navigate — the back
+ // button's only job is to EXIT the Mini App (no SPA navigation, so it can't
+ // flip-flop between Back and Close).
+ if (blockingTypeRef.current) {
+ closeTelegramApp();
+ return;
+ }
// Real in-app history (depth > 0): a normal back. Otherwise we were opened
// directly on this route via a deep-link — navigate(-1) is a no-op, so fall
// back to a sensible parent route instead.
diff --git a/src/components/blocking/ServiceUnavailableScreen.tsx b/src/components/blocking/ServiceUnavailableScreen.tsx
index cfcd64b..4cd2287 100644
--- a/src/components/blocking/ServiceUnavailableScreen.tsx
+++ b/src/components/blocking/ServiceUnavailableScreen.tsx
@@ -4,8 +4,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useBlockingStore } from '../../store/blocking';
import { useFocusTrap } from '../../hooks/useFocusTrap';
import { pingBackend, hasEverReachedBackend } from '../../api/health';
-import { isInTelegramWebApp, closeTelegramApp } from '../../hooks/useTelegramSDK';
-import { CloudWarningIcon, RestartIcon, CloseIcon } from '@/components/icons';
+import { CloudWarningIcon, RestartIcon } from '@/components/icons';
import { Button } from '@/components/primitives';
import { cn } from '@/lib/utils';
import BlockingShell from './BlockingShell';
@@ -25,7 +24,6 @@ export default function ServiceUnavailableScreen() {
const queryClient = useQueryClient();
const [isChecking, setIsChecking] = useState(false);
const isCheckingRef = useRef(false);
- const inTelegram = isInTelegramWebApp();
const recover = useCallback(() => {
clearBlocking();
@@ -87,33 +85,18 @@ export default function ServiceUnavailableScreen() {
pulse
footer={t('blocking.serviceUnavailable.hint')}
actions={
- <>
- }
- >
- {isChecking
- ? t('blocking.serviceUnavailable.checking')
- : t('blocking.serviceUnavailable.retry')}
-
- {/* Telegram Mini App only — a browser tab can't be closed by script.
- Reliably exits the Mini App instead of routing back. */}
- {inTelegram && (
- }
- >
- {t('blocking.serviceUnavailable.close')}
-
- )}
- >
+ }
+ >
+ {isChecking
+ ? t('blocking.serviceUnavailable.checking')
+ : t('blocking.serviceUnavailable.retry')}
+
}
/>
);