/** * Telegram Mini Apps SDK wrapper. * * The SDK script is loaded in index.html before the app mounts, * so `window.Telegram.WebApp` should be available synchronously. */ // Extend Window interface for Telegram SDK declare global { interface Window { Telegram?: { WebApp: TelegramWebApp; }; } } export interface TelegramWebApp { ready: () => void; expand: () => void; close: () => void; initData: string; initDataUnsafe: Record; version: string; platform: string; colorScheme: 'light' | 'dark'; themeParams: { bg_color?: string; text_color?: string; hint_color?: string; link_color?: string; button_color?: string; button_text_color?: string; secondary_bg_color?: string; }; viewportHeight: number; viewportStableHeight: number; isExpanded: boolean; MainButton: { text: string; color: string; textColor: string; isVisible: boolean; isActive: boolean; show: () => void; hide: () => void; onClick: (cb: () => void) => void; offClick: (cb: () => void) => void; setText: (text: string) => void; enable: () => void; disable: () => void; }; BackButton: { isVisible: boolean; show: () => void; hide: () => void; onClick: (cb: () => void) => void; offClick: (cb: () => void) => void; }; HapticFeedback: { impactOccurred: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => void; notificationOccurred: (type: 'error' | 'success' | 'warning') => void; selectionChanged: () => void; }; showPopup: (params: { title?: string; message: string; buttons?: Array<{ id?: string; type?: 'default' | 'ok' | 'close' | 'cancel' | 'destructive'; text?: string }>; }, cb?: (buttonId: string) => void) => void; requestWriteAccess: (cb?: (granted: boolean) => void) => void; onEvent: (event: string, cb: (...args: unknown[]) => void) => void; offEvent: (event: string, cb: (...args: unknown[]) => void) => void; sendData: (data: string) => void; setHeaderColor: (color: string) => void; setBackgroundColor: (color: string) => void; } /** Returns the Telegram WebApp instance, or null if not inside Telegram */ export function getTelegramWebApp(): TelegramWebApp | null { return window.Telegram?.WebApp ?? null; } /** Returns true if running inside a Telegram client */ export function isTelegram(): boolean { return getTelegramWebApp() !== null; } /** Initialize Telegram Mini App: signal readiness, expand viewport */ export function initTelegramApp(): void { const tg = getTelegramWebApp(); if (!tg) { console.info('[Telegram] Not running inside Telegram, skipping init'); return; } tg.ready(); tg.expand(); // Apply Telegram theme to CSS variables for UI components const theme = tg.themeParams; const root = document.documentElement; if (theme.bg_color) root.style.setProperty('--tg-bg', theme.bg_color); if (theme.text_color) root.style.setProperty('--tg-text', theme.text_color); if (theme.button_color) root.style.setProperty('--tg-button', theme.button_color); if (theme.button_text_color) root.style.setProperty('--tg-button-text', theme.button_text_color); if (theme.secondary_bg_color) root.style.setProperty('--tg-secondary-bg', theme.secondary_bg_color); console.info(`[Telegram] Initialized v${tg.version} on ${tg.platform}, scheme=${tg.colorScheme}`); } /** Get Telegram initData for authenticating API requests */ export function getTelegramInitData(): string { return getTelegramWebApp()?.initData ?? ''; } /** Same host rules as backend resolveTelegramID dev fallback (query telegramId). */ function isDevTelegramIdQueryHost(): boolean { if (typeof window === 'undefined') return false; const h = window.location.hostname; // Mirror backend resolveTelegramID host checks (hostname has no port). return h === 'localhost' || h === '127.0.0.1' || h === '192.168.0.53'; } /** Parse ?telegramId= for local/browser dev; ignored on production hosts. */ function readDevTelegramIdFromQuery(): number | null { if (!isDevTelegramIdQueryHost()) return null; const raw = new URLSearchParams(window.location.search).get('telegramId'); if (raw == null || raw === '') return null; const id = Number.parseInt(raw, 10); if (!Number.isFinite(id) || id <= 0) return null; return id; } /** * Telegram user id for API/WS: dev query ?telegramId= on localhost/LAN, else initDataUnsafe.user.id. */ export function getTelegramUserId(): number | null { const fromQuery = readDevTelegramIdFromQuery(); if (fromQuery != null) return fromQuery; const tg = getTelegramWebApp(); if (!tg) return null; const user = tg.initDataUnsafe?.user as { id?: number } | undefined; return user?.id ?? null; } /** Get viewport dimensions accounting for Telegram safe areas */ export function getViewport(): { width: number; height: number } { const tg = getTelegramWebApp(); return { width: window.innerWidth, height: tg?.viewportStableHeight ?? window.innerHeight, }; } // ---- Haptic Feedback ---- /** Trigger haptic impact feedback (combat hits, UI taps) */ export function hapticImpact(style: 'light' | 'medium' | 'heavy'): void { getTelegramWebApp()?.HapticFeedback.impactOccurred(style); } /** Trigger haptic notification feedback (success/error/warning events) */ export function hapticNotification(type: 'error' | 'success' | 'warning'): void { getTelegramWebApp()?.HapticFeedback.notificationOccurred(type); } // ---- Popup ---- /** Show a native Telegram popup dialog */ export function showPopup( title: string, message: string, buttons?: Array<{ id?: string; type?: 'default' | 'ok' | 'close' | 'cancel' | 'destructive'; text?: string }>, ): Promise { return new Promise((resolve) => { const tg = getTelegramWebApp(); if (!tg) { resolve(''); return; } tg.showPopup({ title, message, buttons }, (buttonId) => { resolve(buttonId); }); }); } // ---- Write Access ---- /** Request write access permission from the user */ export function requestWriteAccess(): Promise { return new Promise((resolve) => { const tg = getTelegramWebApp(); if (!tg) { resolve(false); return; } tg.requestWriteAccess((granted) => { resolve(granted); }); }); } // ---- Theme Change Listener ---- /** Subscribe to Telegram theme changes and re-apply CSS variables */ export function onThemeChanged(cb?: () => void): () => void { const tg = getTelegramWebApp(); if (!tg) return () => {}; const handler = () => { const theme = tg.themeParams; const root = document.documentElement; if (theme.bg_color) root.style.setProperty('--tg-bg', theme.bg_color); if (theme.text_color) root.style.setProperty('--tg-text', theme.text_color); if (theme.button_color) root.style.setProperty('--tg-button', theme.button_color); if (theme.button_text_color) root.style.setProperty('--tg-button-text', theme.button_text_color); if (theme.secondary_bg_color) root.style.setProperty('--tg-secondary-bg', theme.secondary_bg_color); cb?.(); }; tg.onEvent('themeChanged', handler); // Return unsubscribe function return () => { tg.offEvent('themeChanged', handler); }; }