mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(cabinet): blocking screens exit via the native Telegram back button, drop the in-page Close
On a full-screen blocking overlay in the Telegram Mini App there were two exit affordances: the native back button — which ran SPA navigation on the hidden route underneath, so it didn't dismiss the block and kept flip-flopping between Back and Close as the route changed — and a redundant in-page "Close" button on the service-unavailable screen. Make the native back button the single, stable exit: while any blocking screen is active, TelegramBackButton shows one Back button whose click closes the Mini App (closeTelegramApp) instead of navigating the SPA — no route change, no Back/Close flip-flop. The normal route-based back button is restored once the block clears. Remove the in-page Close button.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
fullWidth
|
||||
onClick={handleRetry}
|
||||
disabled={isChecking}
|
||||
leftIcon={<RestartIcon className={cn('h-5 w-5', isChecking && 'animate-spin')} />}
|
||||
>
|
||||
{isChecking
|
||||
? t('blocking.serviceUnavailable.checking')
|
||||
: t('blocking.serviceUnavailable.retry')}
|
||||
</Button>
|
||||
{/* Telegram Mini App only — a browser tab can't be closed by script.
|
||||
Reliably exits the Mini App instead of routing back. */}
|
||||
{inTelegram && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
fullWidth
|
||||
onClick={closeTelegramApp}
|
||||
leftIcon={<CloseIcon className="h-5 w-5" />}
|
||||
>
|
||||
{t('blocking.serviceUnavailable.close')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
fullWidth
|
||||
onClick={handleRetry}
|
||||
disabled={isChecking}
|
||||
leftIcon={<RestartIcon className={cn('h-5 w-5', isChecking && 'animate-spin')} />}
|
||||
>
|
||||
{isChecking
|
||||
? t('blocking.serviceUnavailable.checking')
|
||||
: t('blocking.serviceUnavailable.retry')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user