From 7413837fafdf7346054b72bbd05a1c42c4e38bd2 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Thu, 4 Jun 2026 13:06:05 +0300 Subject: [PATCH] feat(cabinet): bulletproof Close button + kill the /login flash on the service-unavailable screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Telegram-Mini-App follow-ups to the backend-unavailable screen: - Reliable "Close" button (Telegram only) that actually EXITS the Mini App instead of routing back. closeTelegramApp() tries the legacy window.Telegram.WebApp.close() global (telegram-web-app.js), then the SDK closeMiniApp(), then the raw postEvent('web_app_close') — all emit the same close event, so it can't silently fail. Guarded so the first path never silently no-ops. - Eliminate the flash of the /login page before the outage screen. On the bootstrap path (the app has never reached the backend) reportPossibleBackendDown() now flips the screen IMMEDIATELY and synchronously instead of waiting on the confirm probe, so the overlay is up before isLoading flips and /login can never paint uncovered. Add an eager checkBackendOnStartup() liveness ping at launch (parallel with auth) so even the no-stored-token / fresh-Telegram path that makes no early request shows the screen at once. The confirm-probe still guards already-loaded sessions from a one-off blip. i18n close key added in ru/en/zh/fa. --- src/api/health.ts | 29 +++++++++++++++++- .../blocking/ServiceUnavailableScreen.tsx | 16 +++++++++- src/hooks/useTelegramSDK.ts | 30 +++++++++++++++++++ src/locales/en.json | 3 +- src/locales/fa.json | 3 +- src/locales/ru.json | 3 +- src/locales/zh.json | 3 +- src/main.tsx | 6 ++++ src/vite-env.d.ts | 2 ++ 9 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/api/health.ts b/src/api/health.ts index 80a7c09..a4cac20 100644 --- a/src/api/health.ts +++ b/src/api/health.ts @@ -85,8 +85,18 @@ let confirmInFlight = false; * already shown. */ export async function reportPossibleBackendDown(): Promise { - if (confirmInFlight) return; if (useBlockingStore.getState().blockingType === 'backend_unavailable') return; + // During the INITIAL bootstrap (we've never reached the backend yet) the + // caller's failed request is itself the confirmation, so flip the screen + // IMMEDIATELY and synchronously. A deferred probe here would let the /login + // page paint for the ~probe duration before the outage screen — the "jump" + // we must avoid. Once the app has loaded, fall through to the confirm-probe so + // a one-off network blip can't blank a working session. + if (!everReachedBackend) { + useBlockingStore.getState().setBackendUnavailable(); + return; + } + if (confirmInFlight) return; confirmInFlight = true; try { const reachable = await pingBackend(); @@ -97,3 +107,20 @@ export async function reportPossibleBackendDown(): Promise { confirmInFlight = false; } } + +/** + * Eager liveness check fired once at app launch (from main.tsx), in parallel + * with auth bootstrap. If the backend is already down at launch this paints the + * ServiceUnavailableScreen immediately — before the auth flow can flash the + * /login page — even on the no-stored-token path that makes no early request. + * No-op if the app has already reached the backend or another blocking screen + * is showing. + */ +export async function checkBackendOnStartup(): Promise { + if (everReachedBackend) return; + if (useBlockingStore.getState().blockingType !== null) return; + const reachable = await pingBackend(); + if (!reachable && !everReachedBackend && useBlockingStore.getState().blockingType === null) { + useBlockingStore.getState().setBackendUnavailable(); + } +} diff --git a/src/components/blocking/ServiceUnavailableScreen.tsx b/src/components/blocking/ServiceUnavailableScreen.tsx index e5fa9a3..c60eadf 100644 --- a/src/components/blocking/ServiceUnavailableScreen.tsx +++ b/src/components/blocking/ServiceUnavailableScreen.tsx @@ -4,7 +4,8 @@ import { useQueryClient } from '@tanstack/react-query'; import { useBlockingStore } from '../../store/blocking'; import { useFocusTrap } from '../../hooks/useFocusTrap'; import { pingBackend, hasEverReachedBackend } from '../../api/health'; -import { CloudWarningIcon, RestartIcon } from '@/components/icons'; +import { isInTelegramWebApp, closeTelegramApp } from '../../hooks/useTelegramSDK'; +import { CloudWarningIcon, RestartIcon, CloseIcon } from '@/components/icons'; const POLL_INTERVAL_MS = 5000; @@ -21,6 +22,7 @@ export default function ServiceUnavailableScreen() { const queryClient = useQueryClient(); const [isChecking, setIsChecking] = useState(false); const isCheckingRef = useRef(false); + const inTelegram = isInTelegramWebApp(); const recover = useCallback(() => { clearBlocking(); @@ -133,6 +135,18 @@ export default function ServiceUnavailableScreen() { )} + {/* Close button — Telegram Mini App only (a browser tab can't be closed + by script). Reliably exits the Mini App instead of routing back. */} + {inTelegram && ( + + )} + {/* Decorative dots */}
initLogoPreload()); } else { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 50297fe..a05d633 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -19,6 +19,8 @@ interface ImportMeta { interface TelegramWebAppGlobal { onEvent?: (event: string, callback: () => void) => void; offEvent?: (event: string, callback: () => void) => void; + /** Closes the Mini App (injected by telegram-web-app.js). */ + close?: () => void; } /** Telegram Login JS SDK — loaded from https://oauth.telegram.org/js/telegram-login.js */