refactor: migrate to @telegram-apps/sdk-react v3, remove all window.Telegram.WebApp usage

Полный переход с нативного window.Telegram.WebApp API на @telegram-apps/sdk-react v3.
Bridge SDK оборачивал receiveEvent, из-за чего popup_closed не доходил до нативного
обработчика — это было основной причиной бага с двойным открытием попапа.

Изменения:

- Удалён @telegram-apps/react-router-integration (привязан к SDK v1)
- Инициализация SDK v3 в main.tsx: init(), restoreInitData(), mount всех компонентов
- AppWithNavigator: убран TelegramRouter (initNavigator + useIntegration), все платформы
  на BrowserRouter, добавлен TelegramBackButton через SDK v3
- useTelegramSDK: детекция через retrieveLaunchParams(), реактивные значения через
  useSignal() (fullscreen, viewport, safe area insets), initData через retrieveRawInitData()
- TelegramAdapter: полная перезапись — попапы через showPopup() (Promise-based, без
  ручного onEvent/offEvent/timeout), кнопки через setMainButtonParams/showBackButton,
  haptic через hapticFeedbackImpactOccurred, тема через themeParamsState с конвертацией
  camelCase→snake_case, cloud storage через getCloudStorageItem/setCloudStorageItem
- api/client.ts: initData через retrieveRawInitData() вместо window.Telegram.WebApp.initData
- AppHeader: фото пользователя через initDataUser() сигнал
- ConnectionModal: openLink через SDK вместо window.Telegram.WebApp.openLink
- ColorPicker: детекция Telegram через isInTelegramWebApp()
- Удалён интерфейс TelegramWebApp (368 строк) и Window augmentation из vite-env.d.ts
- Убрано поле telegram из PlatformContext в types.ts
- Обновлён manualChunks в vite.config.ts
This commit is contained in:
c0mrade
2026-02-04 11:06:59 +03:00
parent 2d00a5c21f
commit 023c3ef6b3
17 changed files with 497 additions and 873 deletions

25
package-lock.json generated
View File

@@ -24,7 +24,6 @@
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-visually-hidden": "^1.2.4", "@radix-ui/react-visually-hidden": "^1.2.4",
"@tanstack/react-query": "^5.8.0", "@tanstack/react-query": "^5.8.0",
"@telegram-apps/react-router-integration": "^1.0.1",
"@telegram-apps/sdk-react": "^3.3.9", "@telegram-apps/sdk-react": "^3.3.9",
"axios": "^1.6.0", "axios": "^1.6.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -2640,30 +2639,6 @@
"integrity": "sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==", "integrity": "sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@telegram-apps/react-router-integration": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@telegram-apps/react-router-integration/-/react-router-integration-1.0.1.tgz",
"integrity": "sha512-q5fC0ihXyUDqrSdLQ3u3gypmjI/rMU2mTHeAz9IU1F1bl5X+J17LxWmgLp+OlD0Y97bq2XYmc+UbKrYh+eN3Wg==",
"license": "MIT",
"peerDependencies": {
"@telegram-apps/sdk": "^1",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-router-dom": "^6.22.3"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@telegram-apps/sdk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@telegram-apps/sdk/-/sdk-1.1.3.tgz",
"integrity": "sha512-gvcXNttbCouDNTxjDUbO5FS/BdaHYO0ibSddQxiYitShPIDZDxGon0Eh3OSsgUIVPSQSNdXqkXaeqYgA+jeY0A==",
"license": "MIT",
"peer": true
},
"node_modules/@telegram-apps/sdk-react": { "node_modules/@telegram-apps/sdk-react": {
"version": "3.3.9", "version": "3.3.9",
"resolved": "https://registry.npmjs.org/@telegram-apps/sdk-react/-/sdk-react-3.3.9.tgz", "resolved": "https://registry.npmjs.org/@telegram-apps/sdk-react/-/sdk-react-3.3.9.tgz",

View File

@@ -32,7 +32,6 @@
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-visually-hidden": "^1.2.4", "@radix-ui/react-visually-hidden": "^1.2.4",
"@tanstack/react-query": "^5.8.0", "@tanstack/react-query": "^5.8.0",
"@telegram-apps/react-router-integration": "^1.0.1",
"@telegram-apps/sdk-react": "^3.3.9", "@telegram-apps/sdk-react": "^3.3.9",
"axios": "^1.6.0", "axios": "^1.6.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@@ -1,102 +1,76 @@
import { useEffect, useMemo } from 'react'; import { useEffect } from 'react';
import { BrowserRouter, Router } from 'react-router-dom'; import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom';
import { useIntegration } from '@telegram-apps/react-router-integration'; import {
import { initNavigator } from '@telegram-apps/sdk'; showBackButton,
hideBackButton,
onBackButtonClick,
offBackButtonClick,
} from '@telegram-apps/sdk-react';
import App from './App'; import App from './App';
import { PlatformProvider } from './platform/PlatformProvider'; import { PlatformProvider } from './platform/PlatformProvider';
import { ThemeColorsProvider } from './providers/ThemeColorsProvider'; import { ThemeColorsProvider } from './providers/ThemeColorsProvider';
import { WebSocketProvider } from './providers/WebSocketProvider'; import { WebSocketProvider } from './providers/WebSocketProvider';
import { ToastProvider } from './components/Toast'; import { ToastProvider } from './components/Toast';
import { TooltipProvider } from './components/primitives/Tooltip'; import { TooltipProvider } from './components/primitives/Tooltip';
import { isInTelegramWebApp } from './hooks/useTelegramSDK';
/** /**
* Check if running inside Telegram Mini App * Manages Telegram BackButton visibility based on navigation location.
* Uses multiple checks to reliably detect Telegram environment * Shows back button on non-root routes, hides on root.
*/ */
function isTelegramMiniApp(): boolean { function TelegramBackButton() {
if (typeof window === 'undefined') return false; const location = useLocation();
const navigate = useNavigate();
const webApp = window.Telegram?.WebApp;
if (!webApp) return false;
// Check 1: initDataUnsafe should have user data in real Telegram
const hasUserData = webApp.initDataUnsafe?.user?.id !== undefined;
// Check 2: Platform should not be 'unknown' (which is default in browser)
const validPlatform = webApp.platform !== 'unknown' && webApp.platform !== '';
// Check 3: Version should be present (SDK loads in Telegram only)
const hasVersion = webApp.version !== undefined && webApp.version !== '';
return hasUserData || (validPlatform && hasVersion);
}
/**
* Component wrapper for Telegram navigator setup.
* Integrates Telegram Mini Apps navigator with React Router to provide
* automatic BackButton management based on navigation history.
* Falls back to BrowserRouter when not in Telegram Mini App.
*/
/**
* Navigator-based router for Telegram Mini App
*/
function TelegramRouter({ children }: { children: React.ReactNode }) {
const navigator = useMemo(() => initNavigator('app-navigation-state', { hashMode: null }), []);
const [location, reactNavigator] = useIntegration(navigator);
useEffect(() => { useEffect(() => {
const isRoot = location.pathname === '/' || location.pathname === '';
try { try {
navigator.attach(); if (isRoot) {
return () => { hideBackButton();
try { } else {
navigator.detach(); showBackButton();
} catch (err) { }
console.warn('Failed to detach navigator:', err); } catch {
} // Not in Telegram or back button not mounted
};
} catch (err) {
console.warn('Failed to attach navigator:', err);
} }
}, [navigator]); }, [location]);
return ( useEffect(() => {
<Router location={location} navigator={reactNavigator}> const handler = () => navigate(-1);
{children} try {
</Router> onBackButtonClick(handler);
); } catch {
// Not in Telegram
}
return () => {
try {
offBackButtonClick(handler);
} catch {
// Not in Telegram
}
};
}, [navigate]);
return null;
} }
export function AppWithNavigator() { export function AppWithNavigator() {
const isTelegram = useMemo(() => { const isTelegram = isInTelegramWebApp();
const result = isTelegramMiniApp();
console.log('[AppWithNavigator] Platform detection:', {
isTelegram: result,
platform: window.Telegram?.WebApp?.platform,
hasUser: window.Telegram?.WebApp?.initDataUnsafe?.user?.id !== undefined,
version: window.Telegram?.WebApp?.version,
});
return result;
}, []);
// Common app content return (
const appContent = ( <BrowserRouter>
<PlatformProvider> {isTelegram && <TelegramBackButton />}
<ThemeColorsProvider> <PlatformProvider>
<TooltipProvider> <ThemeColorsProvider>
<ToastProvider> <TooltipProvider>
<WebSocketProvider> <ToastProvider>
<App /> <WebSocketProvider>
</WebSocketProvider> <App />
</ToastProvider> </WebSocketProvider>
</TooltipProvider> </ToastProvider>
</ThemeColorsProvider> </TooltipProvider>
</PlatformProvider> </ThemeColorsProvider>
</PlatformProvider>
</BrowserRouter>
); );
// Use Telegram navigator in Mini App, BrowserRouter elsewhere
if (isTelegram) {
return <TelegramRouter>{appContent}</TelegramRouter>;
}
return <BrowserRouter>{appContent}</BrowserRouter>;
} }

View File

@@ -1,4 +1,5 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { retrieveRawInitData } from '@telegram-apps/sdk-react';
import { import {
tokenStorage, tokenStorage,
isTokenExpired, isTokenExpired,
@@ -41,10 +42,14 @@ function ensureCsrfToken(): string {
const getTelegramInitData = (): string | null => { const getTelegramInitData = (): string | null => {
if (typeof window === 'undefined') return null; if (typeof window === 'undefined') return null;
const initData = window.Telegram?.WebApp?.initData; try {
if (initData) { const raw = retrieveRawInitData();
tokenStorage.setTelegramInitData(initData); if (raw) {
return initData; tokenStorage.setTelegramInitData(raw);
return raw;
}
} catch {
// Not in Telegram or SDK not initialized
} }
return tokenStorage.getTelegramInitData(); return tokenStorage.getTelegramInitData();

View File

@@ -1,5 +1,6 @@
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { isInTelegramWebApp } from '@/hooks/useTelegramSDK';
interface ColorPickerProps { interface ColorPickerProps {
value: string; value: string;
@@ -9,11 +10,6 @@ interface ColorPickerProps {
disabled?: boolean; disabled?: boolean;
} }
// Check if running in Telegram WebApp
const isTelegramWebApp = (): boolean => {
return !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp;
};
// Convert hex to RGB // Convert hex to RGB
const hexToRgb = (hex: string): { r: number; g: number; b: number } => { const hexToRgb = (hex: string): { r: number; g: number; b: number } => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -117,7 +113,7 @@ export function ColorPicker({ value, onChange, label, description, disabled }: C
const pickerRef = useRef<HTMLDivElement>(null); const pickerRef = useRef<HTMLDivElement>(null);
const colorInputRef = useRef<HTMLInputElement>(null); const colorInputRef = useRef<HTMLInputElement>(null);
const isTelegram = useMemo(() => isTelegramWebApp(), []); const isTelegram = useMemo(() => isInTelegramWebApp(), []);
// Sync with external value // Sync with external value
useEffect(() => { useEffect(() => {

View File

@@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
import { subscriptionApi } from '../api/subscription'; import { subscriptionApi } from '../api/subscription';
import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
import { useBackButton, useHaptic } from '@/platform'; import { useBackButton, useHaptic } from '@/platform';
@@ -300,19 +301,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
// actual deep link URL. This works for both http(s) and custom schemes. // actual deep link URL. This works for both http(s) and custom schemes.
const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(deepLink)}&lang=${i18n.language || 'en'}`; const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(deepLink)}&lang=${i18n.language || 'en'}`;
const tg = ( try {
window as unknown as { sdkOpenLink(redirectUrl, { tryInstantView: false });
Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } }; return;
} } catch {
).Telegram?.WebApp; // SDK not available, fallback
if (tg?.openLink) {
try {
tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true });
return;
} catch {
/* fallback */
}
} }
window.location.href = redirectUrl; window.location.href = redirectUrl;
}; };

View File

@@ -2,6 +2,7 @@ import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { initDataUser } from '@telegram-apps/sdk-react';
import { useAuthStore } from '@/store/auth'; import { useAuthStore } from '@/store/auth';
import { useTheme } from '@/hooks/useTheme'; import { useTheme } from '@/hooks/useTheme';
@@ -113,17 +114,12 @@ export function AppHeader({
// Get user photo from Telegram // Get user photo from Telegram
useEffect(() => { useEffect(() => {
try { try {
const tg = ( const user = initDataUser();
window as unknown as { if (user?.photo_url) {
Telegram?: { WebApp?: { initDataUnsafe?: { user?: { photo_url?: string } } } }; setUserPhotoUrl(user.photo_url);
}
).Telegram?.WebApp;
const photoUrl = tg?.initDataUnsafe?.user?.photo_url;
if (photoUrl) {
setUserPhotoUrl(photoUrl);
} }
} catch (e) { } catch {
console.warn('Failed to get Telegram user photo:', e); // Not in Telegram or init data not available
} }
}, []); }, []);

View File

@@ -1,4 +1,20 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import {
useSignal,
isFullscreen as isFullscreenSignal,
viewportHeight as viewportHeightSignal,
viewportStableHeight as viewportStableHeightSignal,
isViewportExpanded as isViewportExpandedSignal,
viewportSafeAreaInsets,
viewportContentSafeAreaInsets,
requestFullscreen as sdkRequestFullscreen,
exitFullscreen as sdkExitFullscreen,
disableVerticalSwipes as sdkDisableVerticalSwipes,
enableVerticalSwipes as sdkEnableVerticalSwipes,
expandViewport,
retrieveLaunchParams,
retrieveRawInitData,
} from '@telegram-apps/sdk-react';
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled'; const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
@@ -24,60 +40,47 @@ export const setCachedFullscreenEnabled = (enabled: boolean) => {
} }
}; };
// Cached detection result (evaluated once at module load)
let _isInTelegram: boolean | null = null;
function detectTelegram(): boolean {
if (_isInTelegram === null) {
try {
retrieveLaunchParams();
_isInTelegram = true;
} catch {
_isInTelegram = false;
}
}
return _isInTelegram;
}
/** /**
* Check if we're actually running inside Telegram Mini App * Check if we're actually running inside Telegram Mini App
*/ */
export function isInTelegramWebApp(): boolean { export function isInTelegramWebApp(): boolean {
const webApp = window.Telegram?.WebApp; return detectTelegram();
return Boolean(webApp?.initData && webApp.initData.length > 0);
} }
/** /**
* Check if running on mobile Telegram client (iOS/Android) * Check if running on mobile Telegram client (iOS/Android)
*/ */
export function isTelegramMobile(): boolean { export function isTelegramMobile(): boolean {
const webApp = window.Telegram?.WebApp; try {
if (!webApp?.platform) return false; const { tgWebAppPlatform } = retrieveLaunchParams();
return webApp.platform === 'ios' || webApp.platform === 'android'; return tgWebAppPlatform === 'ios' || tgWebAppPlatform === 'android';
} catch {
return false;
}
} }
/** /**
* Get Telegram init data for authentication * Get Telegram init data for authentication
*/ */
export function getTelegramInitData(): string | null { export function getTelegramInitData(): string | null {
const webApp = window.Telegram?.WebApp; try {
return webApp?.initData || null; return retrieveRawInitData() || null;
} } catch {
return null;
/**
* Initialize Telegram WebApp (basic setup without SDK)
*/
export function initTelegramSDK() {
if (!isInTelegramWebApp()) {
return;
}
const tg = window.Telegram?.WebApp;
if (!tg) return;
// Basic initialization
tg.ready();
tg.expand();
// Disable closing confirmation by default
tg.disableClosingConfirmation?.();
// Disable vertical swipes to prevent accidental closures
tg.disableVerticalSwipes?.();
// Auto-enter fullscreen if enabled in settings (mobile only)
const fullscreenEnabled = getCachedFullscreenEnabled();
if (fullscreenEnabled && isTelegramMobile() && tg.requestFullscreen) {
setTimeout(() => {
if (!tg.isFullscreen) {
tg.requestFullscreen?.();
}
}, 100);
} }
} }
@@ -100,54 +103,72 @@ const defaultInsets = { top: 0, bottom: 0, left: 0, right: 0 };
/** /**
* Hook for Telegram WebApp integration * Hook for Telegram WebApp integration
* Uses native window.Telegram.WebApp API * Uses @telegram-apps/sdk-react v3 signals
*/ */
export function useTelegramSDK() { export function useTelegramSDK() {
const inTelegram = isInTelegramWebApp(); const inTelegram = detectTelegram();
const tg = window.Telegram?.WebApp;
const platform = useMemo<TelegramPlatform>(() => { const platform = useMemo<TelegramPlatform>(() => {
if (!inTelegram) return undefined; try {
return tg?.platform as TelegramPlatform; return retrieveLaunchParams().tgWebAppPlatform as TelegramPlatform;
}, [inTelegram, tg?.platform]); } catch {
return undefined;
}
}, []);
const isMobile = platform === 'ios' || platform === 'android'; const isMobile = platform === 'ios' || platform === 'android';
// Safe area insets from Telegram WebApp // Always call useSignal unconditionally (Rules of Hooks).
// When not in Telegram, the signals will have their default values.
const fullscreenValue = useSignal(isFullscreenSignal);
const heightValue = useSignal(viewportHeightSignal);
const stableHeightValue = useSignal(viewportStableHeightSignal);
const expandedValue = useSignal(isViewportExpandedSignal);
const safeInsets = useSignal(viewportSafeAreaInsets);
const contentSafeInsets = useSignal(viewportContentSafeAreaInsets);
const isFullscreen = inTelegram ? (fullscreenValue ?? false) : false;
const viewportHeight = inTelegram ? (heightValue ?? 0) : 0;
const viewportStableHeight = inTelegram ? (stableHeightValue ?? 0) : 0;
const isExpanded = inTelegram ? (expandedValue ?? true) : true;
const safeAreaInset = useMemo(() => { const safeAreaInset = useMemo(() => {
if (!inTelegram || !tg?.safeAreaInset) return defaultInsets; if (!inTelegram || !safeInsets) return defaultInsets;
return { return {
top: tg.safeAreaInset.top || 0, top: safeInsets.top || 0,
bottom: tg.safeAreaInset.bottom || 0, bottom: safeInsets.bottom || 0,
left: tg.safeAreaInset.left || 0, left: safeInsets.left || 0,
right: tg.safeAreaInset.right || 0, right: safeInsets.right || 0,
}; };
}, [inTelegram, tg?.safeAreaInset]); }, [inTelegram, safeInsets]);
const contentSafeAreaInset = useMemo(() => { const contentSafeAreaInset = useMemo(() => {
if (!inTelegram || !tg?.contentSafeAreaInset) return defaultInsets; if (!inTelegram || !contentSafeInsets) return defaultInsets;
return { return {
top: tg.contentSafeAreaInset.top || 0, top: contentSafeInsets.top || 0,
bottom: tg.contentSafeAreaInset.bottom || 0, bottom: contentSafeInsets.bottom || 0,
left: tg.contentSafeAreaInset.left || 0, left: contentSafeInsets.left || 0,
right: tg.contentSafeAreaInset.right || 0, right: contentSafeInsets.right || 0,
}; };
}, [inTelegram, tg?.contentSafeAreaInset]); }, [inTelegram, contentSafeInsets]);
const isFullscreen = tg?.isFullscreen ?? false;
const viewportHeight = tg?.viewportHeight ?? 0;
const viewportStableHeight = tg?.viewportStableHeight ?? 0;
const isExpanded = tg?.isExpanded ?? true;
const requestFullscreen = useCallback(() => { const requestFullscreen = useCallback(() => {
if (!inTelegram || !tg?.requestFullscreen) return; if (!inTelegram) return;
tg.requestFullscreen(); try {
}, [inTelegram, tg]); sdkRequestFullscreen();
} catch {
// Not supported
}
}, [inTelegram]);
const exitFullscreen = useCallback(() => { const exitFullscreen = useCallback(() => {
if (!inTelegram || !tg?.exitFullscreen) return; if (!inTelegram) return;
tg.exitFullscreen(); try {
}, [inTelegram, tg]); sdkExitFullscreen();
} catch {
// Not supported
}
}, [inTelegram]);
const toggleFullscreen = useCallback(() => { const toggleFullscreen = useCallback(() => {
if (isFullscreen) { if (isFullscreen) {
@@ -158,21 +179,33 @@ export function useTelegramSDK() {
}, [isFullscreen, requestFullscreen, exitFullscreen]); }, [isFullscreen, requestFullscreen, exitFullscreen]);
const expand = useCallback(() => { const expand = useCallback(() => {
if (!inTelegram || !tg?.expand) return; if (!inTelegram) return;
tg.expand(); try {
}, [inTelegram, tg]); expandViewport();
} catch {
// Not supported
}
}, [inTelegram]);
const disableVerticalSwipes = useCallback(() => { const disableVerticalSwipes = useCallback(() => {
if (!inTelegram || !tg?.disableVerticalSwipes) return; if (!inTelegram) return;
tg.disableVerticalSwipes(); try {
}, [inTelegram, tg]); sdkDisableVerticalSwipes();
} catch {
// Not supported
}
}, [inTelegram]);
const enableVerticalSwipes = useCallback(() => { const enableVerticalSwipes = useCallback(() => {
if (!inTelegram || !tg?.enableVerticalSwipes) return; if (!inTelegram) return;
tg.enableVerticalSwipes(); try {
}, [inTelegram, tg]); sdkEnableVerticalSwipes();
} catch {
// Not supported
}
}, [inTelegram]);
const isFullscreenSupported = inTelegram && typeof tg?.requestFullscreen === 'function'; const isFullscreenSupported = inTelegram;
return { return {
isTelegramWebApp: inTelegram, isTelegramWebApp: inTelegram,
@@ -182,7 +215,7 @@ export function useTelegramSDK() {
contentSafeAreaInset, contentSafeAreaInset,
viewportHeight, viewportHeight,
viewportStableHeight, viewportStableHeight,
viewportWidth: 0, // Not available in native API viewportWidth: 0,
isExpanded, isExpanded,
platform, platform,
isMobile, isMobile,

View File

@@ -11,7 +11,6 @@ export {
setCachedFullscreenEnabled, setCachedFullscreenEnabled,
isInTelegramWebApp, isInTelegramWebApp,
isTelegramMobile, isTelegramMobile,
initTelegramSDK as initTelegramWebApp, // Alias for backward compatibility
} from './useTelegramSDK'; } from './useTelegramSDK';
/** /**
@@ -32,7 +31,6 @@ export function useTelegramWebApp() {
toggleFullscreen: sdk.toggleFullscreen, toggleFullscreen: sdk.toggleFullscreen,
disableVerticalSwipes: sdk.disableVerticalSwipes, disableVerticalSwipes: sdk.disableVerticalSwipes,
enableVerticalSwipes: sdk.enableVerticalSwipes, enableVerticalSwipes: sdk.enableVerticalSwipes,
// For backward compatibility, expose webApp as the raw Telegram object webApp: null,
webApp: window.Telegram?.WebApp ?? null,
}; };
} }

View File

@@ -1,14 +1,66 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
init,
restoreInitData,
mountMiniApp,
miniAppReady,
mountThemeParams,
mountViewport,
expandViewport,
mountSwipeBehavior,
disableVerticalSwipes,
mountClosingBehavior,
disableClosingConfirmation,
mountBackButton,
mountMainButton,
bindThemeParamsCssVars,
bindViewportCssVars,
} from '@telegram-apps/sdk-react';
import { AppWithNavigator } from './AppWithNavigator'; import { AppWithNavigator } from './AppWithNavigator';
import { initLogoPreload } from './api/branding'; import { initLogoPreload } from './api/branding';
import { initTelegramSDK } from './hooks/useTelegramSDK'; import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK';
import './i18n'; import './i18n';
import './styles/globals.css'; import './styles/globals.css';
// Initialize Telegram SDK (init, viewport mount, CSS vars binding, swipe control) // Initialize Telegram SDK v3
initTelegramSDK(); try {
init();
restoreInitData();
mountMiniApp();
mountThemeParams();
bindThemeParamsCssVars();
mountSwipeBehavior();
disableVerticalSwipes();
mountClosingBehavior();
disableClosingConfirmation();
mountBackButton();
mountMainButton();
mountViewport()
.then(() => {
bindViewportCssVars();
expandViewport();
})
.catch(() => {});
miniAppReady();
// Auto-enter fullscreen if enabled in settings (mobile only)
if (getCachedFullscreenEnabled() && isTelegramMobile()) {
import('@telegram-apps/sdk-react').then(({ requestFullscreen, isFullscreen }) => {
setTimeout(() => {
if (!isFullscreen()) {
requestFullscreen();
}
}, 100);
});
}
} catch {
// Not in Telegram — ok
}
// Preload logo from cache immediately on page load // Preload logo from cache immediately on page load
initLogoPreload(); initLogoPreload();

View File

@@ -115,7 +115,7 @@ export default function Login() {
const initData = getTelegramInitData(); const initData = getTelegramInitData();
if (isInTelegramWebApp() && initData) { if (isInTelegramWebApp() && initData) {
setIsTelegramWebApp(true); setIsTelegramWebApp(true);
// Note: ready() and expand() are already called by initTelegramSDK in main.tsx // Note: ready() and expand() are already called by SDK init in main.tsx
setIsLoading(true); setIsLoading(true);
try { try {
await loginWithTelegram(initData); await loginWithTelegram(initData);

View File

@@ -74,7 +74,7 @@ export default function TelegramRedirect() {
return; return;
} }
// Note: ready(), expand(), and theme CSS vars are already handled by initTelegramSDK in main.tsx // Note: ready(), expand(), and theme CSS vars are already handled by SDK init in main.tsx
try { try {
await loginWithTelegram(initData); await loginWithTelegram(initData);

View File

@@ -1,4 +1,31 @@
import { isInTelegramWebApp } from '@/hooks/useTelegramSDK'; import { isInTelegramWebApp } from '@/hooks/useTelegramSDK';
import {
showBackButton,
hideBackButton,
onBackButtonClick,
offBackButtonClick,
isBackButtonVisible,
setMainButtonParams,
onMainButtonClick,
offMainButtonClick,
hapticFeedbackImpactOccurred,
hapticFeedbackNotificationOccurred,
hapticFeedbackSelectionChanged,
showPopup,
setMiniAppHeaderColor,
setMiniAppBottomBarColor,
themeParamsState,
getCloudStorageItem,
setCloudStorageItem,
deleteCloudStorageItem,
getCloudStorageKeys,
openInvoice,
openLink,
openTelegramLink,
shareURL,
enableClosingConfirmation,
disableClosingConfirmation,
} from '@telegram-apps/sdk-react';
import type { import type {
PlatformContext, PlatformContext,
PlatformCapabilities, PlatformCapabilities,
@@ -15,383 +42,342 @@ import type {
HapticNotificationType, HapticNotificationType,
} from '@/platform/types'; } from '@/platform/types';
// Use raw Telegram WebApp API directly - SDK has initialization issues
function getTelegram(): TelegramWebApp | null {
return window.Telegram?.WebApp ?? null;
}
function createCapabilities(): PlatformCapabilities { function createCapabilities(): PlatformCapabilities {
const tg = getTelegram();
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
return { return {
hasBackButton: inTelegram && !!tg?.BackButton, hasBackButton: inTelegram,
hasMainButton: inTelegram && !!tg?.MainButton, hasMainButton: inTelegram,
hasHapticFeedback: inTelegram && !!tg?.HapticFeedback, hasHapticFeedback: inTelegram,
hasNativeDialogs: inTelegram && !!tg?.showPopup, hasNativeDialogs: inTelegram,
hasThemeSync: inTelegram, hasThemeSync: inTelegram,
hasInvoice: !!tg?.openInvoice, hasInvoice: inTelegram,
hasCloudStorage: inTelegram && !!tg?.CloudStorage, hasCloudStorage: inTelegram,
hasShare: true, hasShare: true,
version: tg?.version, version: undefined,
}; };
} }
function createBackButtonController(): BackButtonController { function createBackButtonController(): BackButtonController {
const tg = getTelegram();
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
let currentCallback: (() => void) | null = null; let currentCallback: (() => void) | null = null;
return { return {
get isVisible() { get isVisible() {
if (!inTelegram || !tg?.BackButton) return false; if (!inTelegram) return false;
return tg.BackButton.isVisible; try {
return isBackButtonVisible();
} catch {
return false;
}
}, },
show(onClick: () => void) { show(onClick: () => void) {
if (!inTelegram || !tg?.BackButton) return; if (!inTelegram) return;
// Remove previous callback if exists
if (currentCallback) { if (currentCallback) {
tg.BackButton.offClick(currentCallback); try {
offBackButtonClick(currentCallback);
} catch {
// ignore
}
} }
currentCallback = onClick; currentCallback = onClick;
tg.BackButton.onClick(onClick); try {
tg.BackButton.show(); onBackButtonClick(onClick);
showBackButton();
} catch {
// Back button not mounted
}
}, },
hide() { hide() {
if (!inTelegram || !tg?.BackButton) return; if (!inTelegram) return;
if (currentCallback) { if (currentCallback) {
tg.BackButton.offClick(currentCallback); try {
offBackButtonClick(currentCallback);
} catch {
// ignore
}
currentCallback = null; currentCallback = null;
} }
tg.BackButton.hide(); try {
hideBackButton();
} catch {
// Back button not mounted
}
}, },
}; };
} }
function createMainButtonController(): MainButtonController { function createMainButtonController(): MainButtonController {
const tg = getTelegram();
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
let currentCallback: (() => void) | null = null; let currentCallback: (() => void) | null = null;
return { return {
get isVisible() { get isVisible() {
if (!inTelegram || !tg?.MainButton) return false; return false; // SDK v3 doesn't expose this as a simple getter
return tg.MainButton.isVisible;
}, },
show(config: MainButtonConfig) { show(config: MainButtonConfig) {
if (!inTelegram || !tg?.MainButton) return; if (!inTelegram) return;
// Remove previous callback if exists
if (currentCallback) { if (currentCallback) {
tg.MainButton.offClick(currentCallback); try {
} offMainButtonClick(currentCallback);
} catch {
// Set button parameters // ignore
tg.MainButton.setText(config.text); }
if (config.color) {
tg.MainButton.color = config.color;
}
if (config.textColor) {
tg.MainButton.textColor = config.textColor;
}
if (config.isActive === false) {
tg.MainButton.disable();
} else {
tg.MainButton.enable();
}
if (config.isLoading) {
tg.MainButton.showProgress();
} else {
tg.MainButton.hideProgress();
} }
currentCallback = config.onClick; currentCallback = config.onClick;
tg.MainButton.onClick(config.onClick);
tg.MainButton.show(); try {
setMainButtonParams({
text: config.text,
isVisible: true,
isEnabled: config.isActive !== false,
isLoaderVisible: config.isLoading ?? false,
backgroundColor: config.color as `#${string}` | undefined,
textColor: config.textColor as `#${string}` | undefined,
});
onMainButtonClick(config.onClick);
} catch {
// Main button not mounted
}
}, },
hide() { hide() {
if (!inTelegram || !tg?.MainButton) return; if (!inTelegram) return;
if (currentCallback) { if (currentCallback) {
tg.MainButton.offClick(currentCallback); try {
offMainButtonClick(currentCallback);
} catch {
// ignore
}
currentCallback = null; currentCallback = null;
} }
tg.MainButton.hideProgress(); try {
tg.MainButton.hide(); setMainButtonParams({ isVisible: false, isLoaderVisible: false });
} catch {
// Main button not mounted
}
}, },
showProgress(show: boolean) { showProgress(show: boolean) {
if (!inTelegram || !tg?.MainButton) return; if (!inTelegram) return;
try {
if (show) { setMainButtonParams({ isLoaderVisible: show });
tg.MainButton.showProgress(); } catch {
} else { // Main button not mounted
tg.MainButton.hideProgress();
} }
}, },
setText(text: string) { setText(text: string) {
if (!inTelegram || !tg?.MainButton) return; if (!inTelegram) return;
tg.MainButton.setText(text); try {
setMainButtonParams({ text });
} catch {
// Main button not mounted
}
}, },
setActive(active: boolean) { setActive(active: boolean) {
if (!inTelegram || !tg?.MainButton) return; if (!inTelegram) return;
try {
if (active) { setMainButtonParams({ isEnabled: active });
tg.MainButton.enable(); } catch {
} else { // Main button not mounted
tg.MainButton.disable();
} }
}, },
}; };
} }
function createHapticController(): HapticController { function createHapticController(): HapticController {
const tg = getTelegram();
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
const haptic = tg?.HapticFeedback;
return { return {
impact(style: HapticImpactStyle = 'medium') { impact(style: HapticImpactStyle = 'medium') {
if (!inTelegram || !haptic) return; if (!inTelegram) return;
haptic.impactOccurred(style); try {
hapticFeedbackImpactOccurred(style);
} catch {
// Haptic not available
}
}, },
notification(type: HapticNotificationType) { notification(type: HapticNotificationType) {
if (!inTelegram || !haptic) return; if (!inTelegram) return;
haptic.notificationOccurred(type); try {
hapticFeedbackNotificationOccurred(type);
} catch {
// Haptic not available
}
}, },
selection() { selection() {
if (!inTelegram || !haptic) return; if (!inTelegram) return;
haptic.selectionChanged(); try {
hapticFeedbackSelectionChanged();
} catch {
// Haptic not available
}
}, },
}; };
} }
function createDialogController(): DialogController { function createDialogController(): DialogController {
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
let popupOpen = false;
// Persistent listener: resets popupOpen whenever Telegram reports popup closed,
// even if our per-call listener was already removed.
if (inTelegram) {
const tg = getTelegram();
if (tg?.onEvent) {
tg.onEvent('popupClosed', (() => {
popupOpen = false;
}) as (...args: unknown[]) => void);
}
}
function showPopupSafe<T>(
params: Parameters<NonNullable<TelegramWebApp['showPopup']>>[0],
mapResult: (buttonId: string) => T,
fallback: () => T,
): Promise<T> {
const tg = getTelegram();
if (!inTelegram || !tg?.showPopup) {
return Promise.resolve(fallback());
}
if (popupOpen) {
return Promise.resolve(mapResult(''));
}
return new Promise((resolve) => {
popupOpen = true;
let settled = false;
const cleanup = () => {
if (settled) return;
settled = true;
popupOpen = false;
clearTimeout(timer);
tg.offEvent?.('popupClosed', onPopupClosed);
};
const onPopupClosed = ((...args: unknown[]) => {
if (settled) return;
const event = args[0] as { button_id?: string } | undefined;
const buttonId = event?.button_id ?? '';
cleanup();
resolve(mapResult(buttonId));
}) as (...args: unknown[]) => void;
const timer = setTimeout(() => {
if (settled) return;
cleanup();
resolve(mapResult(''));
}, 30_000);
tg.onEvent?.('popupClosed', onPopupClosed);
try {
tg.showPopup(params);
} catch (e) {
// Telegram SDK says a popup is already open — don't reset popupOpen,
// so subsequent calls are silently blocked until the popup actually closes.
const isAlreadyOpen = e instanceof Error && e.message?.includes('WebAppPopupOpened');
if (isAlreadyOpen) {
settled = true;
clearTimeout(timer);
tg.offEvent?.('popupClosed', onPopupClosed);
// popupOpen stays true — the persistent listener will reset it
resolve(mapResult(''));
return;
}
cleanup();
resolve(fallback());
}
});
}
return { return {
alert(message: string, _title?: string): Promise<void> { async alert(message: string, _title?: string): Promise<void> {
return showPopupSafe( if (!inTelegram) {
{ message, buttons: [{ type: 'ok' }] }, window.alert(message);
() => undefined, return;
() => { }
window.alert(message); try {
}, await showPopup({
); message,
buttons: [{ type: 'ok', id: 'ok' }],
});
} catch {
window.alert(message);
}
}, },
confirm(message: string, _title?: string): Promise<boolean> { async confirm(message: string, _title?: string): Promise<boolean> {
return showPopupSafe( if (!inTelegram) {
{ return window.confirm(message);
}
try {
const buttonId = await showPopup({
message, message,
buttons: [ buttons: [
{ id: 'ok', type: 'ok' }, { type: 'ok', id: 'ok' },
{ id: 'cancel', type: 'cancel' }, { type: 'cancel', id: 'cancel' },
], ],
}, });
(buttonId) => buttonId === 'ok', return buttonId === 'ok';
() => window.confirm(message), } catch {
); return window.confirm(message);
}
}, },
popup(options: PopupOptions): Promise<string | null> { async popup(options: PopupOptions): Promise<string | null> {
return showPopupSafe( if (!inTelegram) {
{ return window.confirm(options.message) ? 'ok' : null;
}
try {
const buttons = options.buttons?.map((btn) => {
// For 'ok', 'close', 'cancel' types: do NOT include text
// For 'default', 'destructive' types: text is required
if (btn.type === 'ok' || btn.type === 'close' || btn.type === 'cancel') {
return { type: btn.type, id: btn.id };
}
return { type: btn.type ?? ('default' as const), id: btn.id, text: btn.text };
});
const buttonId = await showPopup({
title: options.title, title: options.title,
message: options.message, message: options.message,
buttons: options.buttons?.map((btn) => ({ buttons: buttons as Parameters<typeof showPopup>[0]['buttons'],
id: btn.id, });
type: btn.type, return buttonId || null;
text: btn.text, } catch {
})), return window.confirm(options.message) ? 'ok' : null;
}, }
(buttonId) => buttonId || null,
() => (window.confirm(options.message) ? 'ok' : null),
);
}, },
}; };
} }
function createThemeController(): ThemeController { function createThemeController(): ThemeController {
const tg = getTelegram();
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
return { return {
setHeaderColor(color: string) { setHeaderColor(color: string) {
if (!inTelegram || !tg?.setHeaderColor) return; if (!inTelegram) return;
tg.setHeaderColor(color as `#${string}`); try {
setMiniAppHeaderColor(color as `#${string}`);
} catch {
// Not supported
}
}, },
setBottomBarColor(color: string) { setBottomBarColor(color: string) {
if (!inTelegram || !tg?.setBottomBarColor) return; if (!inTelegram) return;
try { try {
tg.setBottomBarColor(color as `#${string}`); setMiniAppBottomBarColor(color as `#${string}`);
} catch { } catch {
// Not supported in this version // Not supported
} }
}, },
getThemeParams() { getThemeParams() {
if (!inTelegram || !tg?.themeParams) return null; if (!inTelegram) return null;
const params = tg.themeParams; try {
return { const params = themeParamsState();
bg_color: params.bg_color, if (!params) return null;
text_color: params.text_color, // SDK v3 uses camelCase — convert to snake_case for our interface
hint_color: params.hint_color, return {
link_color: params.link_color, bg_color: params.bgColor,
button_color: params.button_color, text_color: params.textColor,
button_text_color: params.button_text_color, hint_color: params.hintColor,
secondary_bg_color: params.secondary_bg_color, link_color: params.linkColor,
header_bg_color: params.header_bg_color, button_color: params.buttonColor,
bottom_bar_bg_color: params.bottom_bar_bg_color, button_text_color: params.buttonTextColor,
accent_text_color: params.accent_text_color, secondary_bg_color: params.secondaryBgColor,
section_bg_color: params.section_bg_color, header_bg_color: params.headerBgColor,
section_header_text_color: params.section_header_text_color, bottom_bar_bg_color: params.bottomBarBgColor,
subtitle_text_color: params.subtitle_text_color, accent_text_color: params.accentTextColor,
destructive_text_color: params.destructive_text_color, section_bg_color: params.sectionBgColor,
}; section_header_text_color: params.sectionHeaderTextColor,
subtitle_text_color: params.subtitleTextColor,
destructive_text_color: params.destructiveTextColor,
};
} catch {
return null;
}
}, },
}; };
} }
function createCloudStorageController(): CloudStorageController | null { function createCloudStorageController(): CloudStorageController | null {
const tg = getTelegram();
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
const storage = tg?.CloudStorage; if (!inTelegram) return null;
if (!inTelegram || !storage) return null;
return { return {
async getItem(key: string): Promise<string | null> { async getItem(key: string): Promise<string | null> {
return new Promise((resolve) => { try {
storage.getItem(key, (error, value) => { const value = await getCloudStorageItem(key);
resolve(error ? null : value || null); return value === '' ? null : value;
}); } catch {
}); return null;
}
}, },
async setItem(key: string, value: string): Promise<void> { async setItem(key: string, value: string): Promise<void> {
return new Promise((resolve, reject) => { await setCloudStorageItem(key, value);
storage.setItem(key, value, (error) => {
if (error) reject(new Error(String(error)));
else resolve();
});
});
}, },
async removeItem(key: string): Promise<void> { async removeItem(key: string): Promise<void> {
return new Promise((resolve, reject) => { await deleteCloudStorageItem(key);
storage.removeItem(key, (error) => {
if (error) reject(new Error(String(error)));
else resolve();
});
});
}, },
async getKeys(): Promise<string[]> { async getKeys(): Promise<string[]> {
return new Promise((resolve) => { try {
storage.getKeys((error, keys) => { return await getCloudStorageKeys();
resolve(error ? [] : keys || []); } catch {
}); return [];
}); }
}, },
}; };
} }
export function createTelegramAdapter(): PlatformContext { export function createTelegramAdapter(): PlatformContext {
const tg = getTelegram();
return { return {
platform: 'telegram', platform: 'telegram',
capabilities: createCapabilities(), capabilities: createCapabilities(),
@@ -403,28 +389,26 @@ export function createTelegramAdapter(): PlatformContext {
cloudStorage: createCloudStorageController(), cloudStorage: createCloudStorageController(),
openInvoice(url: string): Promise<InvoiceStatus> { openInvoice(url: string): Promise<InvoiceStatus> {
return new Promise((resolve) => { try {
if (tg?.openInvoice) { return openInvoice(url, 'url') as Promise<InvoiceStatus>;
tg.openInvoice(url, (status) => resolve(status)); } catch {
} else { window.open(url, '_blank');
window.open(url, '_blank'); return Promise.resolve('pending');
resolve('pending'); }
}
});
}, },
openLink(url: string, options?: { tryInstantView?: boolean }) { openLink(url: string, options?: { tryInstantView?: boolean }) {
if (tg?.openLink) { try {
tg.openLink(url, { try_instant_view: options?.tryInstantView }); openLink(url, { tryInstantView: options?.tryInstantView });
} else { } catch {
window.open(url, '_blank'); window.open(url, '_blank');
} }
}, },
openTelegramLink(url: string) { openTelegramLink(url: string) {
if (tg?.openTelegramLink) { try {
tg.openTelegramLink(url); openTelegramLink(url);
} else { } catch {
window.open(url, '_blank'); window.open(url, '_blank');
} }
}, },
@@ -441,24 +425,24 @@ export function createTelegramAdapter(): PlatformContext {
} }
} }
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME; try {
if (botUsername && tg?.openTelegramLink) { shareURL(url || shareText, text);
const encoded = encodeURIComponent(shareText);
tg.openTelegramLink(`https://t.me/share/url?url=${encoded}`);
return true; return true;
} catch {
return false;
} }
return false;
}, },
setClosingConfirmation(enabled: boolean) { setClosingConfirmation(enabled: boolean) {
if (enabled) { try {
tg?.enableClosingConfirmation?.(); if (enabled) {
} else { enableClosingConfirmation();
tg?.disableClosingConfirmation?.(); } else {
disableClosingConfirmation();
}
} catch {
// Not supported
} }
}, },
telegram: tg,
}; };
} }

View File

@@ -271,7 +271,5 @@ export function createWebAdapter(): PlatformContext {
window.onbeforeunload = null; window.onbeforeunload = null;
} }
}, },
telegram: null,
}; };
} }

View File

@@ -131,10 +131,4 @@ export interface PlatformContext {
// Closing confirmation // Closing confirmation
setClosingConfirmation: (enabled: boolean) => void; setClosingConfirmation: (enabled: boolean) => void;
// Raw Telegram WebApp access (null in web)
telegram: TelegramWebApp | null;
} }
// Note: TelegramWebApp types are defined in vite-env.d.ts
// This file only re-exports the interface from the global scope

369
src/vite-env.d.ts vendored
View File

@@ -10,372 +10,3 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }
// Telegram WebApp types - comprehensive type definitions for Bot API 8.0+
interface TelegramWebApp {
// Init data
initData: string;
initDataUnsafe: {
query_id?: string;
user?: {
id: number;
is_bot?: boolean;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
is_premium?: boolean;
added_to_attachment_menu?: boolean;
allows_write_to_pm?: boolean;
photo_url?: string;
};
receiver?: {
id: number;
is_bot?: boolean;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
is_premium?: boolean;
added_to_attachment_menu?: boolean;
allows_write_to_pm?: boolean;
photo_url?: string;
};
chat?: {
id: number;
type: 'group' | 'supergroup' | 'channel';
title: string;
username?: string;
photo_url?: string;
};
chat_type?: 'sender' | 'private' | 'group' | 'supergroup' | 'channel';
chat_instance?: string;
start_param?: string;
can_send_after?: number;
auth_date: number;
hash: string;
};
// Platform info
version: string;
platform: string;
colorScheme: 'light' | 'dark';
// App state
isExpanded: boolean;
isClosingConfirmationEnabled: boolean;
isVerticalSwipesEnabled: boolean;
isFullscreen: boolean;
isOrientationLocked: boolean;
isActive: boolean;
// Viewport
viewportHeight: number;
viewportStableHeight: number;
// Safe areas
safeAreaInset: { top: number; bottom: number; left: number; right: number };
contentSafeAreaInset: { top: number; bottom: number; left: number; right: number };
// Theme colors (settable)
headerColor: string;
backgroundColor: string;
bottomBarColor?: string;
// Theme params (from Telegram)
themeParams: {
bg_color?: string;
text_color?: string;
hint_color?: string;
link_color?: string;
button_color?: string;
button_text_color?: string;
secondary_bg_color?: string;
header_bg_color?: string;
bottom_bar_bg_color?: string;
accent_text_color?: string;
section_bg_color?: string;
section_header_text_color?: string;
section_separator_color?: string;
subtitle_text_color?: string;
destructive_text_color?: string;
};
// Lifecycle methods
ready: () => void;
expand: () => void;
close: () => void;
// Links
openLink: (url: string, options?: { try_instant_view?: boolean; try_browser?: boolean }) => void;
openTelegramLink: (url: string) => void;
// Invoice
openInvoice: (
url: string,
callback?: (status: 'paid' | 'cancelled' | 'failed' | 'pending') => void,
) => void;
// Fullscreen API (Bot API 8.0+)
requestFullscreen: () => void;
exitFullscreen: () => void;
lockOrientation: () => void;
unlockOrientation: () => void;
// Vertical swipes control
disableVerticalSwipes: () => void;
enableVerticalSwipes: () => void;
// Closing confirmation
enableClosingConfirmation: () => void;
disableClosingConfirmation: () => void;
// Theme color control
setHeaderColor: (color: string) => void;
setBackgroundColor: (color: string) => void;
setBottomBarColor?: (color: string) => void;
// Event handlers
onEvent: (eventType: string, callback: (...args: unknown[]) => void) => void;
offEvent: (eventType: string, callback: (...args: unknown[]) => void) => void;
// Data sending
sendData: (data: string) => void;
// QR Scanner
showScanQrPopup: (params: { text?: string }, callback?: (text: string) => boolean | void) => void;
closeScanQrPopup: () => void;
// Clipboard
readTextFromClipboard: (callback?: (text: string | null) => void) => void;
// Write access
requestWriteAccess: (callback?: (granted: boolean) => void) => void;
// Contact
requestContact: (callback?: (granted: boolean) => void) => void;
// Emoji status (Bot API 8.0+)
requestEmojiStatusAccess: (callback?: (granted: boolean) => void) => void;
setEmojiStatus: (
customEmojiId: string,
params?: { duration?: number },
callback?: (success: boolean) => void,
) => void;
// Download file (Bot API 8.0+)
downloadFile: (
params: { url: string; file_name: string },
callback?: (success: boolean) => void,
) => void;
// Share story (Bot API 7.8+)
shareToStory?: (
mediaUrl: string,
params?: { text?: string; widget_link?: { url: string; name?: string } },
) => void;
// Version check helper
isVersionAtLeast: (version: string) => boolean;
// Main Button
MainButton: {
text: string;
color: string;
textColor: string;
isVisible: boolean;
isActive: boolean;
isProgressVisible: boolean;
show: () => void;
hide: () => void;
enable: () => void;
disable: () => void;
showProgress: (leaveActive?: boolean) => void;
hideProgress: () => void;
setText: (text: string) => void;
setParams: (params: {
text?: string;
color?: string;
text_color?: string;
has_shine_effect?: boolean;
is_active?: boolean;
is_visible?: boolean;
}) => void;
onClick: (callback: () => void) => void;
offClick: (callback: () => void) => void;
};
// Secondary Button (Bot API 7.10+)
SecondaryButton?: {
text: string;
color: string;
textColor: string;
isVisible: boolean;
isActive: boolean;
isProgressVisible: boolean;
position: 'left' | 'right' | 'top' | 'bottom';
show: () => void;
hide: () => void;
enable: () => void;
disable: () => void;
showProgress: (leaveActive?: boolean) => void;
hideProgress: () => void;
setText: (text: string) => void;
setParams: (params: {
text?: string;
color?: string;
text_color?: string;
has_shine_effect?: boolean;
is_active?: boolean;
is_visible?: boolean;
position?: 'left' | 'right' | 'top' | 'bottom';
}) => void;
onClick: (callback: () => void) => void;
offClick: (callback: () => void) => void;
};
// Back Button
BackButton: {
isVisible: boolean;
show: () => void;
hide: () => void;
onClick: (callback: () => void) => void;
offClick: (callback: () => void) => void;
};
// Settings Button (Bot API 7.0+)
SettingsButton?: {
isVisible: boolean;
show: () => void;
hide: () => void;
onClick: (callback: () => void) => void;
offClick: (callback: () => void) => void;
};
// Haptic Feedback (Bot API 6.1+)
HapticFeedback: {
impactOccurred: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => void;
notificationOccurred: (type: 'success' | 'warning' | 'error') => void;
selectionChanged: () => void;
};
// Popups (Bot API 6.2+)
showPopup: (
params: {
title?: string;
message: string;
buttons?: Array<{
id?: string;
type?: 'default' | 'ok' | 'close' | 'cancel' | 'destructive';
text?: string;
}>;
},
callback?: (buttonId: string) => void,
) => void;
showAlert: (message: string, callback?: () => void) => void;
showConfirm: (message: string, callback?: (confirmed: boolean) => void) => void;
// Cloud Storage (Bot API 6.9+)
CloudStorage: {
setItem: (
key: string,
value: string,
callback?: (error: Error | null, stored: boolean) => void,
) => void;
getItem: (key: string, callback: (error: Error | null, value: string) => void) => void;
getItems: (
keys: string[],
callback: (error: Error | null, values: Record<string, string>) => void,
) => void;
removeItem: (key: string, callback?: (error: Error | null, removed: boolean) => void) => void;
removeItems: (
keys: string[],
callback?: (error: Error | null, removed: boolean) => void,
) => void;
getKeys: (callback: (error: Error | null, keys: string[]) => void) => void;
};
// Biometric (Bot API 7.2+)
BiometricManager?: {
isInited: boolean;
isBiometricAvailable: boolean;
biometricType: 'finger' | 'face' | 'unknown';
isAccessRequested: boolean;
isAccessGranted: boolean;
isBiometricTokenSaved: boolean;
deviceId: string;
init: (callback?: () => void) => void;
requestAccess: (params: { reason?: string }, callback?: (granted: boolean) => void) => void;
authenticate: (
params: { reason?: string },
callback?: (success: boolean, token?: string) => void,
) => void;
updateBiometricToken: (token: string, callback?: (success: boolean) => void) => void;
openSettings: () => void;
};
// Accelerometer (Bot API 8.0+)
Accelerometer?: {
isStarted: boolean;
x: number | null;
y: number | null;
z: number | null;
start: (params?: { refresh_rate?: number }, callback?: (started: boolean) => void) => void;
stop: (callback?: (stopped: boolean) => void) => void;
};
// DeviceOrientation (Bot API 8.0+)
DeviceOrientation?: {
isStarted: boolean;
absolute: boolean;
alpha: number | null;
beta: number | null;
gamma: number | null;
start: (
params?: { refresh_rate?: number; need_absolute?: boolean },
callback?: (started: boolean) => void,
) => void;
stop: (callback?: (stopped: boolean) => void) => void;
};
// Gyroscope (Bot API 8.0+)
Gyroscope?: {
isStarted: boolean;
x: number | null;
y: number | null;
z: number | null;
start: (params?: { refresh_rate?: number }, callback?: (started: boolean) => void) => void;
stop: (callback?: (stopped: boolean) => void) => void;
};
// Location Manager (Bot API 8.0+)
LocationManager?: {
isInited: boolean;
isLocationAvailable: boolean;
isAccessRequested: boolean;
isAccessGranted: boolean;
init: (callback?: () => void) => void;
getLocation: (
callback: (
location: {
latitude: number;
longitude: number;
altitude?: number;
course?: number;
speed?: number;
horizontal_accuracy?: number;
vertical_accuracy?: number;
course_accuracy?: number;
speed_accuracy?: number;
} | null,
) => void,
) => void;
openSettings: () => void;
};
}
interface Window {
Telegram?: {
WebApp: TelegramWebApp;
};
}

View File

@@ -49,11 +49,7 @@ export default defineConfig({
'@radix-ui/react-visually-hidden', '@radix-ui/react-visually-hidden',
], ],
'vendor-dnd': ['@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities'], 'vendor-dnd': ['@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities'],
'vendor-telegram': [ 'vendor-telegram': ['@telegram-apps/sdk-react'],
'@telegram-apps/sdk',
'@telegram-apps/sdk-react',
'@telegram-apps/react-router-integration',
],
'vendor-utils': [ 'vendor-utils': [
'axios', 'axios',
'zustand', 'zustand',