From 023c3ef6b356b38ef5169c38a01e5b2b8bc4891d Mon Sep 17 00:00:00 2001 From: c0mrade Date: Wed, 4 Feb 2026 11:06:59 +0300 Subject: [PATCH] refactor: migrate to @telegram-apps/sdk-react v3, remove all window.Telegram.WebApp usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Полный переход с нативного 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 --- package-lock.json | 25 - package.json | 1 - src/AppWithNavigator.tsx | 138 ++--- src/api/client.ts | 13 +- src/components/ColorPicker.tsx | 8 +- src/components/ConnectionModal.tsx | 19 +- src/components/layout/AppShell/AppHeader.tsx | 16 +- src/hooks/useTelegramSDK.ts | 191 ++++--- src/hooks/useTelegramWebApp.ts | 4 +- src/main.tsx | 58 ++- src/pages/Login.tsx | 2 +- src/pages/TelegramRedirect.tsx | 2 +- src/platform/adapters/TelegramAdapter.ts | 510 +++++++++---------- src/platform/adapters/WebAdapter.ts | 2 - src/platform/types.ts | 6 - src/vite-env.d.ts | 369 -------------- vite.config.ts | 6 +- 17 files changed, 497 insertions(+), 873 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b676a4..113a599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-visually-hidden": "^1.2.4", "@tanstack/react-query": "^5.8.0", - "@telegram-apps/react-router-integration": "^1.0.1", "@telegram-apps/sdk-react": "^3.3.9", "axios": "^1.6.0", "class-variance-authority": "^0.7.1", @@ -2640,30 +2639,6 @@ "integrity": "sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==", "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": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/@telegram-apps/sdk-react/-/sdk-react-3.3.9.tgz", diff --git a/package.json b/package.json index 9a92f4e..b10e970 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-visually-hidden": "^1.2.4", "@tanstack/react-query": "^5.8.0", - "@telegram-apps/react-router-integration": "^1.0.1", "@telegram-apps/sdk-react": "^3.3.9", "axios": "^1.6.0", "class-variance-authority": "^0.7.1", diff --git a/src/AppWithNavigator.tsx b/src/AppWithNavigator.tsx index fb05276..11ae689 100644 --- a/src/AppWithNavigator.tsx +++ b/src/AppWithNavigator.tsx @@ -1,102 +1,76 @@ -import { useEffect, useMemo } from 'react'; -import { BrowserRouter, Router } from 'react-router-dom'; -import { useIntegration } from '@telegram-apps/react-router-integration'; -import { initNavigator } from '@telegram-apps/sdk'; +import { useEffect } from 'react'; +import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom'; +import { + showBackButton, + hideBackButton, + onBackButtonClick, + offBackButtonClick, +} from '@telegram-apps/sdk-react'; import App from './App'; import { PlatformProvider } from './platform/PlatformProvider'; 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'; /** - * Check if running inside Telegram Mini App - * Uses multiple checks to reliably detect Telegram environment + * Manages Telegram BackButton visibility based on navigation location. + * Shows back button on non-root routes, hides on root. */ -function isTelegramMiniApp(): boolean { - if (typeof window === 'undefined') return false; - - 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); +function TelegramBackButton() { + const location = useLocation(); + const navigate = useNavigate(); useEffect(() => { + const isRoot = location.pathname === '/' || location.pathname === ''; try { - navigator.attach(); - return () => { - try { - navigator.detach(); - } catch (err) { - console.warn('Failed to detach navigator:', err); - } - }; - } catch (err) { - console.warn('Failed to attach navigator:', err); + if (isRoot) { + hideBackButton(); + } else { + showBackButton(); + } + } catch { + // Not in Telegram or back button not mounted } - }, [navigator]); + }, [location]); - return ( - - {children} - - ); + useEffect(() => { + const handler = () => navigate(-1); + try { + onBackButtonClick(handler); + } catch { + // Not in Telegram + } + return () => { + try { + offBackButtonClick(handler); + } catch { + // Not in Telegram + } + }; + }, [navigate]); + + return null; } export function AppWithNavigator() { - const isTelegram = useMemo(() => { - 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; - }, []); + const isTelegram = isInTelegramWebApp(); - // Common app content - const appContent = ( - - - - - - - - - - - + return ( + + {isTelegram && } + + + + + + + + + + + + ); - - // Use Telegram navigator in Mini App, BrowserRouter elsewhere - if (isTelegram) { - return {appContent}; - } - - return {appContent}; } diff --git a/src/api/client.ts b/src/api/client.ts index 40775ac..49d7b0a 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,4 +1,5 @@ import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; +import { retrieveRawInitData } from '@telegram-apps/sdk-react'; import { tokenStorage, isTokenExpired, @@ -41,10 +42,14 @@ function ensureCsrfToken(): string { const getTelegramInitData = (): string | null => { if (typeof window === 'undefined') return null; - const initData = window.Telegram?.WebApp?.initData; - if (initData) { - tokenStorage.setTelegramInitData(initData); - return initData; + try { + const raw = retrieveRawInitData(); + if (raw) { + tokenStorage.setTelegramInitData(raw); + return raw; + } + } catch { + // Not in Telegram or SDK not initialized } return tokenStorage.getTelegramInitData(); diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 69de3e5..080e96e 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { createPortal } from 'react-dom'; +import { isInTelegramWebApp } from '@/hooks/useTelegramSDK'; interface ColorPickerProps { value: string; @@ -9,11 +10,6 @@ interface ColorPickerProps { 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 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); @@ -117,7 +113,7 @@ export function ColorPicker({ value, onChange, label, description, disabled }: C const pickerRef = useRef(null); const colorInputRef = useRef(null); - const isTelegram = useMemo(() => isTelegramWebApp(), []); + const isTelegram = useMemo(() => isInTelegramWebApp(), []); // Sync with external value useEffect(() => { diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 57254a1..7818ce3 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; +import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react'; import { subscriptionApi } from '../api/subscription'; import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; 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. const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(deepLink)}&lang=${i18n.language || 'en'}`; - const tg = ( - window as unknown as { - Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } }; - } - ).Telegram?.WebApp; - - if (tg?.openLink) { - try { - tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true }); - return; - } catch { - /* fallback */ - } + try { + sdkOpenLink(redirectUrl, { tryInstantView: false }); + return; + } catch { + // SDK not available, fallback } window.location.href = redirectUrl; }; diff --git a/src/components/layout/AppShell/AppHeader.tsx b/src/components/layout/AppShell/AppHeader.tsx index 002b32d..68fc13d 100644 --- a/src/components/layout/AppShell/AppHeader.tsx +++ b/src/components/layout/AppShell/AppHeader.tsx @@ -2,6 +2,7 @@ import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { useState, useEffect } from 'react'; +import { initDataUser } from '@telegram-apps/sdk-react'; import { useAuthStore } from '@/store/auth'; import { useTheme } from '@/hooks/useTheme'; @@ -113,17 +114,12 @@ export function AppHeader({ // Get user photo from Telegram useEffect(() => { try { - const tg = ( - window as unknown as { - Telegram?: { WebApp?: { initDataUnsafe?: { user?: { photo_url?: string } } } }; - } - ).Telegram?.WebApp; - const photoUrl = tg?.initDataUnsafe?.user?.photo_url; - if (photoUrl) { - setUserPhotoUrl(photoUrl); + const user = initDataUser(); + if (user?.photo_url) { + setUserPhotoUrl(user.photo_url); } - } catch (e) { - console.warn('Failed to get Telegram user photo:', e); + } catch { + // Not in Telegram or init data not available } }, []); diff --git a/src/hooks/useTelegramSDK.ts b/src/hooks/useTelegramSDK.ts index 73cdcbe..162dc54 100644 --- a/src/hooks/useTelegramSDK.ts +++ b/src/hooks/useTelegramSDK.ts @@ -1,4 +1,20 @@ 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'; @@ -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 */ export function isInTelegramWebApp(): boolean { - const webApp = window.Telegram?.WebApp; - return Boolean(webApp?.initData && webApp.initData.length > 0); + return detectTelegram(); } /** * Check if running on mobile Telegram client (iOS/Android) */ export function isTelegramMobile(): boolean { - const webApp = window.Telegram?.WebApp; - if (!webApp?.platform) return false; - return webApp.platform === 'ios' || webApp.platform === 'android'; + try { + const { tgWebAppPlatform } = retrieveLaunchParams(); + return tgWebAppPlatform === 'ios' || tgWebAppPlatform === 'android'; + } catch { + return false; + } } /** * Get Telegram init data for authentication */ export function getTelegramInitData(): string | null { - const webApp = window.Telegram?.WebApp; - return webApp?.initData || 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); + try { + return retrieveRawInitData() || null; + } catch { + return null; } } @@ -100,54 +103,72 @@ const defaultInsets = { top: 0, bottom: 0, left: 0, right: 0 }; /** * Hook for Telegram WebApp integration - * Uses native window.Telegram.WebApp API + * Uses @telegram-apps/sdk-react v3 signals */ export function useTelegramSDK() { - const inTelegram = isInTelegramWebApp(); - const tg = window.Telegram?.WebApp; + const inTelegram = detectTelegram(); const platform = useMemo(() => { - if (!inTelegram) return undefined; - return tg?.platform as TelegramPlatform; - }, [inTelegram, tg?.platform]); + try { + return retrieveLaunchParams().tgWebAppPlatform as TelegramPlatform; + } catch { + return undefined; + } + }, []); 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(() => { - if (!inTelegram || !tg?.safeAreaInset) return defaultInsets; + if (!inTelegram || !safeInsets) return defaultInsets; return { - top: tg.safeAreaInset.top || 0, - bottom: tg.safeAreaInset.bottom || 0, - left: tg.safeAreaInset.left || 0, - right: tg.safeAreaInset.right || 0, + top: safeInsets.top || 0, + bottom: safeInsets.bottom || 0, + left: safeInsets.left || 0, + right: safeInsets.right || 0, }; - }, [inTelegram, tg?.safeAreaInset]); + }, [inTelegram, safeInsets]); const contentSafeAreaInset = useMemo(() => { - if (!inTelegram || !tg?.contentSafeAreaInset) return defaultInsets; + if (!inTelegram || !contentSafeInsets) return defaultInsets; return { - top: tg.contentSafeAreaInset.top || 0, - bottom: tg.contentSafeAreaInset.bottom || 0, - left: tg.contentSafeAreaInset.left || 0, - right: tg.contentSafeAreaInset.right || 0, + top: contentSafeInsets.top || 0, + bottom: contentSafeInsets.bottom || 0, + left: contentSafeInsets.left || 0, + right: contentSafeInsets.right || 0, }; - }, [inTelegram, tg?.contentSafeAreaInset]); - - const isFullscreen = tg?.isFullscreen ?? false; - const viewportHeight = tg?.viewportHeight ?? 0; - const viewportStableHeight = tg?.viewportStableHeight ?? 0; - const isExpanded = tg?.isExpanded ?? true; + }, [inTelegram, contentSafeInsets]); const requestFullscreen = useCallback(() => { - if (!inTelegram || !tg?.requestFullscreen) return; - tg.requestFullscreen(); - }, [inTelegram, tg]); + if (!inTelegram) return; + try { + sdkRequestFullscreen(); + } catch { + // Not supported + } + }, [inTelegram]); const exitFullscreen = useCallback(() => { - if (!inTelegram || !tg?.exitFullscreen) return; - tg.exitFullscreen(); - }, [inTelegram, tg]); + if (!inTelegram) return; + try { + sdkExitFullscreen(); + } catch { + // Not supported + } + }, [inTelegram]); const toggleFullscreen = useCallback(() => { if (isFullscreen) { @@ -158,21 +179,33 @@ export function useTelegramSDK() { }, [isFullscreen, requestFullscreen, exitFullscreen]); const expand = useCallback(() => { - if (!inTelegram || !tg?.expand) return; - tg.expand(); - }, [inTelegram, tg]); + if (!inTelegram) return; + try { + expandViewport(); + } catch { + // Not supported + } + }, [inTelegram]); const disableVerticalSwipes = useCallback(() => { - if (!inTelegram || !tg?.disableVerticalSwipes) return; - tg.disableVerticalSwipes(); - }, [inTelegram, tg]); + if (!inTelegram) return; + try { + sdkDisableVerticalSwipes(); + } catch { + // Not supported + } + }, [inTelegram]); const enableVerticalSwipes = useCallback(() => { - if (!inTelegram || !tg?.enableVerticalSwipes) return; - tg.enableVerticalSwipes(); - }, [inTelegram, tg]); + if (!inTelegram) return; + try { + sdkEnableVerticalSwipes(); + } catch { + // Not supported + } + }, [inTelegram]); - const isFullscreenSupported = inTelegram && typeof tg?.requestFullscreen === 'function'; + const isFullscreenSupported = inTelegram; return { isTelegramWebApp: inTelegram, @@ -182,7 +215,7 @@ export function useTelegramSDK() { contentSafeAreaInset, viewportHeight, viewportStableHeight, - viewportWidth: 0, // Not available in native API + viewportWidth: 0, isExpanded, platform, isMobile, diff --git a/src/hooks/useTelegramWebApp.ts b/src/hooks/useTelegramWebApp.ts index 0178b89..7fdf0be 100644 --- a/src/hooks/useTelegramWebApp.ts +++ b/src/hooks/useTelegramWebApp.ts @@ -11,7 +11,6 @@ export { setCachedFullscreenEnabled, isInTelegramWebApp, isTelegramMobile, - initTelegramSDK as initTelegramWebApp, // Alias for backward compatibility } from './useTelegramSDK'; /** @@ -32,7 +31,6 @@ export function useTelegramWebApp() { toggleFullscreen: sdk.toggleFullscreen, disableVerticalSwipes: sdk.disableVerticalSwipes, enableVerticalSwipes: sdk.enableVerticalSwipes, - // For backward compatibility, expose webApp as the raw Telegram object - webApp: window.Telegram?.WebApp ?? null, + webApp: null, }; } diff --git a/src/main.tsx b/src/main.tsx index ba8673c..d4ee76d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,66 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; 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 { initLogoPreload } from './api/branding'; -import { initTelegramSDK } from './hooks/useTelegramSDK'; +import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK'; import './i18n'; import './styles/globals.css'; -// Initialize Telegram SDK (init, viewport mount, CSS vars binding, swipe control) -initTelegramSDK(); +// Initialize Telegram SDK v3 +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 initLogoPreload(); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index d1231d7..6c8aada 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -115,7 +115,7 @@ export default function Login() { const initData = getTelegramInitData(); if (isInTelegramWebApp() && initData) { 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); try { await loginWithTelegram(initData); diff --git a/src/pages/TelegramRedirect.tsx b/src/pages/TelegramRedirect.tsx index 1342519..468e25e 100644 --- a/src/pages/TelegramRedirect.tsx +++ b/src/pages/TelegramRedirect.tsx @@ -74,7 +74,7 @@ export default function TelegramRedirect() { 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 { await loginWithTelegram(initData); diff --git a/src/platform/adapters/TelegramAdapter.ts b/src/platform/adapters/TelegramAdapter.ts index e6be1b7..07c19f2 100644 --- a/src/platform/adapters/TelegramAdapter.ts +++ b/src/platform/adapters/TelegramAdapter.ts @@ -1,4 +1,31 @@ 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 { PlatformContext, PlatformCapabilities, @@ -15,383 +42,342 @@ import type { HapticNotificationType, } 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 { - const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); return { - hasBackButton: inTelegram && !!tg?.BackButton, - hasMainButton: inTelegram && !!tg?.MainButton, - hasHapticFeedback: inTelegram && !!tg?.HapticFeedback, - hasNativeDialogs: inTelegram && !!tg?.showPopup, + hasBackButton: inTelegram, + hasMainButton: inTelegram, + hasHapticFeedback: inTelegram, + hasNativeDialogs: inTelegram, hasThemeSync: inTelegram, - hasInvoice: !!tg?.openInvoice, - hasCloudStorage: inTelegram && !!tg?.CloudStorage, + hasInvoice: inTelegram, + hasCloudStorage: inTelegram, hasShare: true, - version: tg?.version, + version: undefined, }; } function createBackButtonController(): BackButtonController { - const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); let currentCallback: (() => void) | null = null; return { get isVisible() { - if (!inTelegram || !tg?.BackButton) return false; - return tg.BackButton.isVisible; + if (!inTelegram) return false; + try { + return isBackButtonVisible(); + } catch { + return false; + } }, show(onClick: () => void) { - if (!inTelegram || !tg?.BackButton) return; + if (!inTelegram) return; - // Remove previous callback if exists if (currentCallback) { - tg.BackButton.offClick(currentCallback); + try { + offBackButtonClick(currentCallback); + } catch { + // ignore + } } currentCallback = onClick; - tg.BackButton.onClick(onClick); - tg.BackButton.show(); + try { + onBackButtonClick(onClick); + showBackButton(); + } catch { + // Back button not mounted + } }, hide() { - if (!inTelegram || !tg?.BackButton) return; + if (!inTelegram) return; if (currentCallback) { - tg.BackButton.offClick(currentCallback); + try { + offBackButtonClick(currentCallback); + } catch { + // ignore + } currentCallback = null; } - tg.BackButton.hide(); + try { + hideBackButton(); + } catch { + // Back button not mounted + } }, }; } function createMainButtonController(): MainButtonController { - const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); let currentCallback: (() => void) | null = null; return { get isVisible() { - if (!inTelegram || !tg?.MainButton) return false; - return tg.MainButton.isVisible; + return false; // SDK v3 doesn't expose this as a simple getter }, show(config: MainButtonConfig) { - if (!inTelegram || !tg?.MainButton) return; + if (!inTelegram) return; - // Remove previous callback if exists if (currentCallback) { - tg.MainButton.offClick(currentCallback); - } - - // Set button parameters - 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(); + try { + offMainButtonClick(currentCallback); + } catch { + // ignore + } } 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() { - if (!inTelegram || !tg?.MainButton) return; + if (!inTelegram) return; if (currentCallback) { - tg.MainButton.offClick(currentCallback); + try { + offMainButtonClick(currentCallback); + } catch { + // ignore + } currentCallback = null; } - tg.MainButton.hideProgress(); - tg.MainButton.hide(); + try { + setMainButtonParams({ isVisible: false, isLoaderVisible: false }); + } catch { + // Main button not mounted + } }, showProgress(show: boolean) { - if (!inTelegram || !tg?.MainButton) return; - - if (show) { - tg.MainButton.showProgress(); - } else { - tg.MainButton.hideProgress(); + if (!inTelegram) return; + try { + setMainButtonParams({ isLoaderVisible: show }); + } catch { + // Main button not mounted } }, setText(text: string) { - if (!inTelegram || !tg?.MainButton) return; - tg.MainButton.setText(text); + if (!inTelegram) return; + try { + setMainButtonParams({ text }); + } catch { + // Main button not mounted + } }, setActive(active: boolean) { - if (!inTelegram || !tg?.MainButton) return; - - if (active) { - tg.MainButton.enable(); - } else { - tg.MainButton.disable(); + if (!inTelegram) return; + try { + setMainButtonParams({ isEnabled: active }); + } catch { + // Main button not mounted } }, }; } function createHapticController(): HapticController { - const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - const haptic = tg?.HapticFeedback; return { impact(style: HapticImpactStyle = 'medium') { - if (!inTelegram || !haptic) return; - haptic.impactOccurred(style); + if (!inTelegram) return; + try { + hapticFeedbackImpactOccurred(style); + } catch { + // Haptic not available + } }, notification(type: HapticNotificationType) { - if (!inTelegram || !haptic) return; - haptic.notificationOccurred(type); + if (!inTelegram) return; + try { + hapticFeedbackNotificationOccurred(type); + } catch { + // Haptic not available + } }, selection() { - if (!inTelegram || !haptic) return; - haptic.selectionChanged(); + if (!inTelegram) return; + try { + hapticFeedbackSelectionChanged(); + } catch { + // Haptic not available + } }, }; } function createDialogController(): DialogController { 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( - params: Parameters>[0], - mapResult: (buttonId: string) => T, - fallback: () => T, - ): Promise { - 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 { - alert(message: string, _title?: string): Promise { - return showPopupSafe( - { message, buttons: [{ type: 'ok' }] }, - () => undefined, - () => { - window.alert(message); - }, - ); + async alert(message: string, _title?: string): Promise { + if (!inTelegram) { + window.alert(message); + return; + } + try { + await showPopup({ + message, + buttons: [{ type: 'ok', id: 'ok' }], + }); + } catch { + window.alert(message); + } }, - confirm(message: string, _title?: string): Promise { - return showPopupSafe( - { + async confirm(message: string, _title?: string): Promise { + if (!inTelegram) { + return window.confirm(message); + } + try { + const buttonId = await showPopup({ message, buttons: [ - { id: 'ok', type: 'ok' }, - { id: 'cancel', type: 'cancel' }, + { type: 'ok', id: 'ok' }, + { type: 'cancel', id: 'cancel' }, ], - }, - (buttonId) => buttonId === 'ok', - () => window.confirm(message), - ); + }); + return buttonId === 'ok'; + } catch { + return window.confirm(message); + } }, - popup(options: PopupOptions): Promise { - return showPopupSafe( - { + async popup(options: PopupOptions): Promise { + 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, message: options.message, - buttons: options.buttons?.map((btn) => ({ - id: btn.id, - type: btn.type, - text: btn.text, - })), - }, - (buttonId) => buttonId || null, - () => (window.confirm(options.message) ? 'ok' : null), - ); + buttons: buttons as Parameters[0]['buttons'], + }); + return buttonId || null; + } catch { + return window.confirm(options.message) ? 'ok' : null; + } }, }; } function createThemeController(): ThemeController { - const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); return { setHeaderColor(color: string) { - if (!inTelegram || !tg?.setHeaderColor) return; - tg.setHeaderColor(color as `#${string}`); + if (!inTelegram) return; + try { + setMiniAppHeaderColor(color as `#${string}`); + } catch { + // Not supported + } }, setBottomBarColor(color: string) { - if (!inTelegram || !tg?.setBottomBarColor) return; + if (!inTelegram) return; try { - tg.setBottomBarColor(color as `#${string}`); + setMiniAppBottomBarColor(color as `#${string}`); } catch { - // Not supported in this version + // Not supported } }, getThemeParams() { - if (!inTelegram || !tg?.themeParams) return null; - const params = tg.themeParams; - return { - bg_color: params.bg_color, - text_color: params.text_color, - hint_color: params.hint_color, - link_color: params.link_color, - button_color: params.button_color, - button_text_color: params.button_text_color, - secondary_bg_color: params.secondary_bg_color, - header_bg_color: params.header_bg_color, - bottom_bar_bg_color: params.bottom_bar_bg_color, - accent_text_color: params.accent_text_color, - section_bg_color: params.section_bg_color, - section_header_text_color: params.section_header_text_color, - subtitle_text_color: params.subtitle_text_color, - destructive_text_color: params.destructive_text_color, - }; + if (!inTelegram) return null; + try { + const params = themeParamsState(); + if (!params) return null; + // SDK v3 uses camelCase — convert to snake_case for our interface + return { + bg_color: params.bgColor, + text_color: params.textColor, + hint_color: params.hintColor, + link_color: params.linkColor, + button_color: params.buttonColor, + button_text_color: params.buttonTextColor, + secondary_bg_color: params.secondaryBgColor, + header_bg_color: params.headerBgColor, + bottom_bar_bg_color: params.bottomBarBgColor, + accent_text_color: params.accentTextColor, + 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 { - const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - const storage = tg?.CloudStorage; - - if (!inTelegram || !storage) return null; + if (!inTelegram) return null; return { async getItem(key: string): Promise { - return new Promise((resolve) => { - storage.getItem(key, (error, value) => { - resolve(error ? null : value || null); - }); - }); + try { + const value = await getCloudStorageItem(key); + return value === '' ? null : value; + } catch { + return null; + } }, async setItem(key: string, value: string): Promise { - return new Promise((resolve, reject) => { - storage.setItem(key, value, (error) => { - if (error) reject(new Error(String(error))); - else resolve(); - }); - }); + await setCloudStorageItem(key, value); }, async removeItem(key: string): Promise { - return new Promise((resolve, reject) => { - storage.removeItem(key, (error) => { - if (error) reject(new Error(String(error))); - else resolve(); - }); - }); + await deleteCloudStorageItem(key); }, async getKeys(): Promise { - return new Promise((resolve) => { - storage.getKeys((error, keys) => { - resolve(error ? [] : keys || []); - }); - }); + try { + return await getCloudStorageKeys(); + } catch { + return []; + } }, }; } export function createTelegramAdapter(): PlatformContext { - const tg = getTelegram(); - return { platform: 'telegram', capabilities: createCapabilities(), @@ -403,28 +389,26 @@ export function createTelegramAdapter(): PlatformContext { cloudStorage: createCloudStorageController(), openInvoice(url: string): Promise { - return new Promise((resolve) => { - if (tg?.openInvoice) { - tg.openInvoice(url, (status) => resolve(status)); - } else { - window.open(url, '_blank'); - resolve('pending'); - } - }); + try { + return openInvoice(url, 'url') as Promise; + } catch { + window.open(url, '_blank'); + return Promise.resolve('pending'); + } }, openLink(url: string, options?: { tryInstantView?: boolean }) { - if (tg?.openLink) { - tg.openLink(url, { try_instant_view: options?.tryInstantView }); - } else { + try { + openLink(url, { tryInstantView: options?.tryInstantView }); + } catch { window.open(url, '_blank'); } }, openTelegramLink(url: string) { - if (tg?.openTelegramLink) { - tg.openTelegramLink(url); - } else { + try { + openTelegramLink(url); + } catch { window.open(url, '_blank'); } }, @@ -441,24 +425,24 @@ export function createTelegramAdapter(): PlatformContext { } } - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME; - if (botUsername && tg?.openTelegramLink) { - const encoded = encodeURIComponent(shareText); - tg.openTelegramLink(`https://t.me/share/url?url=${encoded}`); + try { + shareURL(url || shareText, text); return true; + } catch { + return false; } - - return false; }, setClosingConfirmation(enabled: boolean) { - if (enabled) { - tg?.enableClosingConfirmation?.(); - } else { - tg?.disableClosingConfirmation?.(); + try { + if (enabled) { + enableClosingConfirmation(); + } else { + disableClosingConfirmation(); + } + } catch { + // Not supported } }, - - telegram: tg, }; } diff --git a/src/platform/adapters/WebAdapter.ts b/src/platform/adapters/WebAdapter.ts index 68c6e37..8f52b77 100644 --- a/src/platform/adapters/WebAdapter.ts +++ b/src/platform/adapters/WebAdapter.ts @@ -271,7 +271,5 @@ export function createWebAdapter(): PlatformContext { window.onbeforeunload = null; } }, - - telegram: null, }; } diff --git a/src/platform/types.ts b/src/platform/types.ts index 3c7ebab..1f01b10 100644 --- a/src/platform/types.ts +++ b/src/platform/types.ts @@ -131,10 +131,4 @@ export interface PlatformContext { // Closing confirmation 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 diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 3b70828..47caca1 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -10,372 +10,3 @@ interface ImportMetaEnv { interface ImportMeta { 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) => 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; - }; -} diff --git a/vite.config.ts b/vite.config.ts index 9d07f2e..f131284 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -49,11 +49,7 @@ export default defineConfig({ '@radix-ui/react-visually-hidden', ], 'vendor-dnd': ['@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities'], - 'vendor-telegram': [ - '@telegram-apps/sdk', - '@telegram-apps/sdk-react', - '@telegram-apps/react-router-integration', - ], + 'vendor-telegram': ['@telegram-apps/sdk-react'], 'vendor-utils': [ 'axios', 'zustand',