/** * Telegram Mini App React Hooks * Copy-paste into your project */ import { useState, useEffect, useCallback } from 'react'; // ============================================================================ // Types // ============================================================================ interface SafeAreaInset { top: number; bottom: number; left: number; right: number; } interface TelegramWebApp { version: string; platform: string; isExpanded: boolean; viewportHeight: number; viewportStableHeight: number; isFullscreen: boolean; safeAreaInset: SafeAreaInset; contentSafeAreaInset: SafeAreaInset; themeParams: Record; colorScheme: 'light' | 'dark'; requestFullscreen: () => void; exitFullscreen: () => void; onEvent: (eventType: string, callback: () => void) => void; offEvent: (eventType: string, callback: () => void) => void; } declare global { interface Window { Telegram?: { WebApp?: TelegramWebApp }; } } // ============================================================================ // Utilities // ============================================================================ export function getWebApp(): TelegramWebApp | null { if (typeof window === 'undefined') return null; return window.Telegram?.WebApp ?? null; } export function isDevMode(): boolean { if (typeof window === 'undefined') return false; const url = new URL(window.location.href); return ['localhost', '127.0.0.1'].includes(url.hostname) || url.searchParams.get('debug') === '1'; } // ============================================================================ // useSafeAreaInset // ============================================================================ export interface SafeAreaInsets { device: SafeAreaInset; content: SafeAreaInset; totalTop: number; totalBottom: number; } /** * Reactive safe area insets - updates when Telegram sends events * * @example * const { totalTop, totalBottom } = useSafeAreaInset(); *
Header
*/ export function useSafeAreaInset(): SafeAreaInsets { const [insets, setInsets] = useState(() => { const webApp = getWebApp(); const device = webApp?.safeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 }; const content = webApp?.contentSafeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 }; return { device, content, totalTop: device.top + content.top, totalBottom: device.bottom + content.bottom, }; }); const updateInsets = useCallback(() => { const webApp = getWebApp(); const device = webApp?.safeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 }; const content = webApp?.contentSafeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 }; // Add minimum fallbacks for fullscreen mode let totalTop = device.top + content.top; if (webApp?.isFullscreen && totalTop < 80) { totalTop = webApp.platform === 'ios' ? 100 : 80; } setInsets({ device, content, totalTop, totalBottom: device.bottom + content.bottom, }); }, []); useEffect(() => { const webApp = getWebApp(); if (!webApp) return; updateInsets(); webApp.onEvent('safeAreaChanged', updateInsets); webApp.onEvent('contentSafeAreaChanged', updateInsets); webApp.onEvent('fullscreenChanged', updateInsets); // Fallback: poll every 500ms (some events may not fire) const interval = setInterval(updateInsets, 500); return () => { webApp.offEvent('safeAreaChanged', updateInsets); webApp.offEvent('contentSafeAreaChanged', updateInsets); webApp.offEvent('fullscreenChanged', updateInsets); clearInterval(interval); }; }, [updateInsets]); return insets; } // ============================================================================ // useFullscreen // ============================================================================ export interface FullscreenState { isFullscreen: boolean; isSupported: boolean; requestFullscreen: () => void; exitFullscreen: () => void; toggleFullscreen: () => void; } /** * Manage Telegram Mini App fullscreen mode (requires version 8.0+) * * @example * const { isFullscreen, toggleFullscreen } = useFullscreen(); * */ export function useFullscreen(): FullscreenState { const [isFullscreen, setIsFullscreen] = useState(() => { return getWebApp()?.isFullscreen ?? false; }); const isSupported = (() => { const webApp = getWebApp(); if (!webApp) return false; return parseFloat(webApp.version) >= 8.0; })(); const requestFullscreen = useCallback(() => { getWebApp()?.requestFullscreen?.(); }, []); const exitFullscreen = useCallback(() => { getWebApp()?.exitFullscreen?.(); }, []); const toggleFullscreen = useCallback(() => { if (isFullscreen) exitFullscreen(); else requestFullscreen(); }, [isFullscreen, requestFullscreen, exitFullscreen]); useEffect(() => { const webApp = getWebApp(); if (!webApp) return; const handleChange = () => setIsFullscreen(webApp.isFullscreen); webApp.onEvent('fullscreenChanged', handleChange); webApp.onEvent('fullscreenFailed', handleChange); return () => { webApp.offEvent('fullscreenChanged', handleChange); webApp.offEvent('fullscreenFailed', handleChange); }; }, []); return { isFullscreen, isSupported, requestFullscreen, exitFullscreen, toggleFullscreen }; } // ============================================================================ // useTelegramTheme // ============================================================================ export interface TelegramTheme { params: Record; colorScheme: 'light' | 'dark'; isDark: boolean; } /** * Reactive Telegram theme params * * @example * const { params, isDark } = useTelegramTheme(); *
...
*/ export function useTelegramTheme(): TelegramTheme { const [theme, setTheme] = useState(() => { const webApp = getWebApp(); return { params: webApp?.themeParams ?? {}, colorScheme: webApp?.colorScheme ?? 'light', isDark: webApp?.colorScheme === 'dark', }; }); useEffect(() => { const webApp = getWebApp(); if (!webApp) return; const handleChange = () => { setTheme({ params: webApp.themeParams, colorScheme: webApp.colorScheme, isDark: webApp.colorScheme === 'dark', }); }; webApp.onEvent('themeChanged', handleChange); return () => webApp.offEvent('themeChanged', handleChange); }, []); return theme; }