mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(cabinet): bulletproof Close button + kill the /login flash on the service-unavailable screen
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.
This commit is contained in:
@@ -85,8 +85,18 @@ let confirmInFlight = false;
|
|||||||
* already shown.
|
* already shown.
|
||||||
*/
|
*/
|
||||||
export async function reportPossibleBackendDown(): Promise<void> {
|
export async function reportPossibleBackendDown(): Promise<void> {
|
||||||
if (confirmInFlight) return;
|
|
||||||
if (useBlockingStore.getState().blockingType === 'backend_unavailable') 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;
|
confirmInFlight = true;
|
||||||
try {
|
try {
|
||||||
const reachable = await pingBackend();
|
const reachable = await pingBackend();
|
||||||
@@ -97,3 +107,20 @@ export async function reportPossibleBackendDown(): Promise<void> {
|
|||||||
confirmInFlight = false;
|
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<void> {
|
||||||
|
if (everReachedBackend) return;
|
||||||
|
if (useBlockingStore.getState().blockingType !== null) return;
|
||||||
|
const reachable = await pingBackend();
|
||||||
|
if (!reachable && !everReachedBackend && useBlockingStore.getState().blockingType === null) {
|
||||||
|
useBlockingStore.getState().setBackendUnavailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useBlockingStore } from '../../store/blocking';
|
import { useBlockingStore } from '../../store/blocking';
|
||||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||||
import { pingBackend, hasEverReachedBackend } from '../../api/health';
|
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;
|
const POLL_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export default function ServiceUnavailableScreen() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
const isCheckingRef = useRef(false);
|
const isCheckingRef = useRef(false);
|
||||||
|
const inTelegram = isInTelegramWebApp();
|
||||||
|
|
||||||
const recover = useCallback(() => {
|
const recover = useCallback(() => {
|
||||||
clearBlocking();
|
clearBlocking();
|
||||||
@@ -133,6 +135,18 @@ export default function ServiceUnavailableScreen() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* 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 && (
|
||||||
|
<button
|
||||||
|
onClick={closeTelegramApp}
|
||||||
|
className="mt-3 flex w-full items-center justify-center gap-2 rounded-xl border border-dark-700 px-6 py-4 font-semibold text-dark-300 transition-colors duration-200 hover:bg-dark-800 hover:text-white"
|
||||||
|
>
|
||||||
|
<CloseIcon className="h-5 w-5" />
|
||||||
|
{t('blocking.serviceUnavailable.close')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Decorative dots */}
|
{/* Decorative dots */}
|
||||||
<div className="mt-8 flex items-center justify-center gap-2">
|
<div className="mt-8 flex items-center justify-center gap-2">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
retrieveLaunchParams,
|
retrieveLaunchParams,
|
||||||
retrieveRawInitData,
|
retrieveRawInitData,
|
||||||
themeParamsState,
|
themeParamsState,
|
||||||
|
closeMiniApp as sdkCloseMiniApp,
|
||||||
|
postEvent,
|
||||||
} from '@telegram-apps/sdk-react';
|
} from '@telegram-apps/sdk-react';
|
||||||
|
|
||||||
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
|
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
|
||||||
@@ -50,6 +52,34 @@ export function isInTelegramWebApp(): boolean {
|
|||||||
return detectTelegram();
|
return detectTelegram();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the Telegram Mini App as reliably as possible. All three paths emit the
|
||||||
|
* same `web_app_close` event to the Telegram client; we try the most broadly
|
||||||
|
* compatible first and stop at the first that doesn't throw:
|
||||||
|
* 1) the legacy `window.Telegram.WebApp.close()` global (telegram-web-app.js,
|
||||||
|
* loaded in index.html) — widest client coverage, no SDK-mount dependency;
|
||||||
|
* 2) the modern SDK `closeMiniApp()` (mini app is mounted on init);
|
||||||
|
* 3) the raw `postEvent('web_app_close')` protocol event — no global/mount
|
||||||
|
* dependency at all.
|
||||||
|
* Outside Telegram it is a safe no-op.
|
||||||
|
*/
|
||||||
|
export function closeTelegramApp(): void {
|
||||||
|
try {
|
||||||
|
const wa = window.Telegram?.WebApp;
|
||||||
|
if (wa?.close) {
|
||||||
|
wa.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
sdkCloseMiniApp();
|
||||||
|
return;
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
postEvent('web_app_close');
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
export function isTelegramMobile(): boolean {
|
export function isTelegramMobile(): boolean {
|
||||||
try {
|
try {
|
||||||
const { tgWebAppPlatform } = retrieveLaunchParams();
|
const { tgWebAppPlatform } = retrieveLaunchParams();
|
||||||
|
|||||||
@@ -4816,7 +4816,8 @@
|
|||||||
"description": "We can't reach the service right now. It may be undergoing maintenance or there's a temporary connection issue.",
|
"description": "We can't reach the service right now. It may be undergoing maintenance or there's a temporary connection issue.",
|
||||||
"retry": "Try again",
|
"retry": "Try again",
|
||||||
"checking": "Checking connection...",
|
"checking": "Checking connection...",
|
||||||
"hint": "This page will refresh automatically as soon as the service is back."
|
"hint": "This page will refresh automatically as soon as the service is back.",
|
||||||
|
"close": "Close"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
|
|||||||
@@ -4476,7 +4476,8 @@
|
|||||||
"description": "در حال حاضر امکان اتصال به سرویس وجود ندارد. ممکن است در حال تعمیر باشد یا مشکل موقتی در اتصال وجود داشته باشد.",
|
"description": "در حال حاضر امکان اتصال به سرویس وجود ندارد. ممکن است در حال تعمیر باشد یا مشکل موقتی در اتصال وجود داشته باشد.",
|
||||||
"retry": "تلاش مجدد",
|
"retry": "تلاش مجدد",
|
||||||
"checking": "در حال بررسی اتصال...",
|
"checking": "در حال بررسی اتصال...",
|
||||||
"hint": "بهمحض در دسترس قرار گرفتن سرویس، این صفحه بهطور خودکار بازخوانی میشود."
|
"hint": "بهمحض در دسترس قرار گرفتن سرویس، این صفحه بهطور خودکار بازخوانی میشود.",
|
||||||
|
"close": "بستن"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
|
|||||||
@@ -5371,7 +5371,8 @@
|
|||||||
"description": "Не удаётся подключиться к сервису. Возможно, идут технические работы или временные неполадки со связью.",
|
"description": "Не удаётся подключиться к сервису. Возможно, идут технические работы или временные неполадки со связью.",
|
||||||
"retry": "Повторить попытку",
|
"retry": "Повторить попытку",
|
||||||
"checking": "Проверяем соединение...",
|
"checking": "Проверяем соединение...",
|
||||||
"hint": "Страница обновится автоматически, как только сервис снова станет доступен."
|
"hint": "Страница обновится автоматически, как только сервис снова станет доступен.",
|
||||||
|
"close": "Закрыть"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
|
|||||||
@@ -4357,7 +4357,8 @@
|
|||||||
"description": "暂时无法连接到服务。可能正在进行维护,或存在临时的网络问题。",
|
"description": "暂时无法连接到服务。可能正在进行维护,或存在临时的网络问题。",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"checking": "正在检查连接...",
|
"checking": "正在检查连接...",
|
||||||
"hint": "服务恢复后,本页面将自动刷新。"
|
"hint": "服务恢复后,本页面将自动刷新。",
|
||||||
|
"close": "关闭"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"banSystem": {
|
"banSystem": {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useAuthStore } from './store/auth';
|
|||||||
import { AppWithNavigator } from './AppWithNavigator';
|
import { AppWithNavigator } from './AppWithNavigator';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
import { initLogoPreload } from './api/branding';
|
import { initLogoPreload } from './api/branding';
|
||||||
|
import { checkBackendOnStartup } from './api/health';
|
||||||
import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK';
|
import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK';
|
||||||
import { applyTelegramLanguage } from './i18n';
|
import { applyTelegramLanguage } from './i18n';
|
||||||
import './styles/globals.css';
|
import './styles/globals.css';
|
||||||
@@ -106,6 +107,11 @@ if (isTelegramEnv && !alreadyInitialized) {
|
|||||||
// are only available post-init()).
|
// are only available post-init()).
|
||||||
void useAuthStore.getState().initialize();
|
void useAuthStore.getState().initialize();
|
||||||
|
|
||||||
|
// In parallel with auth bootstrap, eagerly check backend liveness so a dead
|
||||||
|
// backend paints the ServiceUnavailableScreen immediately instead of flashing
|
||||||
|
// the /login page first.
|
||||||
|
void checkBackendOnStartup();
|
||||||
|
|
||||||
if ('requestIdleCallback' in window) {
|
if ('requestIdleCallback' in window) {
|
||||||
requestIdleCallback(() => initLogoPreload());
|
requestIdleCallback(() => initLogoPreload());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@@ -19,6 +19,8 @@ interface ImportMeta {
|
|||||||
interface TelegramWebAppGlobal {
|
interface TelegramWebAppGlobal {
|
||||||
onEvent?: (event: string, callback: () => void) => void;
|
onEvent?: (event: string, callback: () => void) => void;
|
||||||
offEvent?: (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 */
|
/** Telegram Login JS SDK — loaded from https://oauth.telegram.org/js/telegram-login.js */
|
||||||
|
|||||||
Reference in New Issue
Block a user