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
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);
|
|
};
|
|
}
|