Files
bedolaga-cabinet/src/main.tsx
Fringg f81af401f7 fix(auth): читать initData из моста Telegram, а не только из кэша SDK
retrieveRawInitData() ищет параметры запуска по цепочке location.href ->
запись performance о навигации -> кэш tapps/launchParams в sessionStorage,
записывая удачный результат обратно. Последние два источника привязаны к
документу, а не к текущему запуску мини-аппы: как только SPA сменила
маршрут и hash с tgWebAppData ушёл из адреса, остаются данные ПЕРВОГО
запуска в этой сессии WebView. На iOS WebView переживает переоткрытия, и
SDK молча отдаёт старую копию.

Бэкенд принимает initData с auth_date не старше 30 суток, поэтому
застрявшая копия рано или поздно пересекает порог и вход отваливается с
«Invalid or expired Telegram authentication data».

Спрашиваем ещё и window.Telegram.WebApp.initData (официальный мост,
telegram-web-app.js из index.html) и берём вариант со свежим auth_date:
какой источник протух, зависит от платформы, «самый свежий» верен всегда.
Все три места чтения initData сведены в один модуль.
2026-08-19 02:00:41 +03:00

147 lines
4.8 KiB
TypeScript

import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
init,
restoreInitData,
mountMiniApp,
miniAppReady,
mountViewport,
expandViewport,
mountSwipeBehavior,
disableVerticalSwipes,
mountClosingBehavior,
disableClosingConfirmation,
mountBackButton,
bindThemeParamsCssVars,
bindViewportCssVars,
requestFullscreen,
isFullscreen,
} from '@telegram-apps/sdk-react';
import { clearStaleSessionIfNeeded } from './utils/token';
import { installEncodingSurrogateGuard } from './utils/installEncodingSurrogateGuard';
import { getTelegramInitData } from './utils/telegramInitData';
import { useAuthStore } from './store/auth';
import { AppWithNavigator } from './AppWithNavigator';
import { ErrorBoundary } from './components/ErrorBoundary';
import { initLogoPreload } from './api/branding';
import { checkBackendOnStartup } from './api/health';
import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK';
import { applyTelegramLanguage } from './i18n';
import './styles/globals.css';
// Harden the global encoders against lone UTF-16 surrogates (truncated emoji in
// backend names/remarks) BEFORE anything renders or fetches — otherwise such a
// string crashes any encodeURI/encodeURIComponent/btoa path on iOS WebKit,
// including qrcode.react's internal encodeURI. See installEncodingSurrogateGuard.
installEncodingSurrogateGuard();
// Polyfill Object.hasOwn for older iOS/Android WebViews (Safari < 15.4, old Chrome).
// @telegram-apps/sdk v3 depends on valibot which uses Object.hasOwn internally.
// Without this, init() and any launch-params retrieval below throw
// LaunchParamsRetrieveError on affected devices.
// See: https://github.com/Telegram-Mini-Apps/tma.js/issues/683
if (typeof (Object as { hasOwn?: unknown }).hasOwn !== 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Object as any).hasOwn = (obj: object, prop: PropertyKey): boolean =>
Object.prototype.hasOwnProperty.call(obj, prop);
}
// Only initialize Telegram SDK when running inside Telegram
const isTelegramEnv =
!!(window as unknown as Record<string, unknown>).TelegramWebviewProxy ||
location.hash.includes('tgWebApp') ||
location.search.includes('tgWebApp');
const HMR_KEY = '__tg_sdk_initialized';
const alreadyInitialized = (window as unknown as Record<string, unknown>)[HMR_KEY] === true;
if (isTelegramEnv && !alreadyInitialized) {
(window as unknown as Record<string, unknown>)[HMR_KEY] = true;
try {
init();
restoreInitData();
clearStaleSessionIfNeeded(getTelegramInitData());
// Adopt the user's Telegram client language on first run (no explicit choice yet).
applyTelegramLanguage();
// Each mount in its own try/catch so one failure doesn't block others.
// mountMiniApp() internally mounts themeParams in SDK v3,
// so we don't call mountThemeParams() separately to avoid ConcurrentCallError.
try {
mountMiniApp();
} catch {}
try {
bindThemeParamsCssVars();
} catch {}
try {
mountSwipeBehavior();
disableVerticalSwipes();
} catch {}
try {
mountClosingBehavior();
disableClosingConfirmation();
} catch {}
try {
mountBackButton();
} catch {}
// Viewport must be mounted before requesting fullscreen
mountViewport()
.then(() => {
bindViewportCssVars();
expandViewport();
// Auto-enter fullscreen if enabled in settings (mobile only)
if (getCachedFullscreenEnabled() && isTelegramMobile()) {
if (!isFullscreen()) {
requestFullscreen();
}
}
})
.catch(() => {});
miniAppReady();
} catch {}
} else if (!isTelegramEnv) {
// Outside Telegram — still clear stale session tokens if any
clearStaleSessionIfNeeded(null);
}
// Bootstrap auth after the Telegram SDK is initialised so CloudStorage-backed
// refresh-token recovery can run inside initialize() (launch params + CloudStorage
// are only available post-init()).
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) {
requestIdleCallback(() => initLogoPreload());
} else {
setTimeout(initLogoPreload, 100);
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary level="app">
<QueryClientProvider client={queryClient}>
<AppWithNavigator />
</QueryClientProvider>
</ErrorBoundary>
</React.StrictMode>,
);