You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
7.1 KiB
TypeScript

/**
* 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<string, unknown>;
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<string> {
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<boolean> {
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);
};
}