feat: migrate to @tma.js/sdk-react for Telegram Mini App

- Add useTelegramSDK hook with reactive signals for viewport, safe area, fullscreen
- Migrate TelegramAdapter to use SDK components (backButton, mainButton, hapticFeedback, cloudStorage, themeParams, popup)
- Update Login, TelegramRedirect to use SDK helpers
- Update PlatformProvider, api/client to use centralized SDK functions
- Simplify useTelegramWebApp as backward-compatible wrapper
- Add automatic CSS variable binding for theme and viewport
This commit is contained in:
c0mrade
2026-02-01 20:13:50 +03:00
parent 55ae55f4af
commit edb5be09ae
14 changed files with 736 additions and 513 deletions

273
src/hooks/useTelegramSDK.ts Normal file
View File

@@ -0,0 +1,273 @@
import { useCallback, useMemo, useSyncExternalStore } from 'react';
import { init, viewport, miniApp, swipeBehavior, themeParams } from '@tma.js/sdk-react';
// SDK initialization state
let sdkInitialized = false;
let initCleanup: VoidFunction | null = null;
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
/**
* Get cached fullscreen setting
*/
export const getCachedFullscreenEnabled = (): boolean => {
try {
return localStorage.getItem(FULLSCREEN_CACHE_KEY) === 'true';
} catch {
return false;
}
};
/**
* Set cached fullscreen setting
*/
export const setCachedFullscreenEnabled = (enabled: boolean) => {
try {
localStorage.setItem(FULLSCREEN_CACHE_KEY, String(enabled));
} catch {
// localStorage not available
}
};
/**
* 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);
}
/**
* 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';
}
/**
* Get Telegram init data for authentication
* Returns the raw initData string used for backend authentication
*/
export function getTelegramInitData(): string | null {
const webApp = window.Telegram?.WebApp;
return webApp?.initData || null;
}
/**
* Initialize Telegram SDK
* Call this once at app startup (in main.tsx)
*/
export function initTelegramSDK() {
// Only run in Telegram context
if (!isInTelegramWebApp()) {
return;
}
// Prevent double initialization
if (sdkInitialized) {
return;
}
try {
// Initialize the SDK
initCleanup = init();
sdkInitialized = true;
// Mount viewport and bind CSS variables
viewport
.mount()
.then(() => {
viewport.bindCssVars();
// Expand the mini app
viewport.expand();
})
.catch((err) => {
console.warn('Viewport mount failed:', err);
});
// Mount mini app and call ready
miniApp.mount();
miniApp.ready();
// Mount theme params and bind CSS variables
try {
themeParams.mount();
themeParams.bindCssVars();
} catch {
// Theme params may already be mounted or not supported
}
// Disable vertical swipes if supported
if (swipeBehavior.isSupported()) {
swipeBehavior.mount();
swipeBehavior.disableVertical();
}
// Auto-enter fullscreen if enabled in settings (mobile only)
const fullscreenEnabled = getCachedFullscreenEnabled();
if (fullscreenEnabled && isTelegramMobile()) {
// Wait for viewport to be mounted
setTimeout(() => {
const isFullscreen = viewport.isFullscreen();
if (!isFullscreen) {
viewport.requestFullscreen().catch((e) => {
console.warn('Auto-fullscreen failed:', e);
});
}
}, 100);
}
} catch (e) {
console.warn('Telegram SDK initialization failed:', e);
}
}
/**
* Cleanup SDK (call on app unmount if needed)
*/
export function cleanupTelegramSDK() {
if (initCleanup) {
initCleanup();
initCleanup = null;
sdkInitialized = false;
}
}
/**
* Type for platform values
*/
export type TelegramPlatform =
| 'android'
| 'ios'
| 'tdesktop'
| 'macos'
| 'weba'
| 'webk'
| 'unigram'
| 'unknown'
| undefined;
// Default values for when SDK is not available
const defaultInsets = { top: 0, bottom: 0, left: 0, right: 0 };
/**
* Helper to subscribe to SDK signals using useSyncExternalStore pattern
*/
function useSDKSignal<T>(
signal: { (): T; sub(fn: VoidFunction): VoidFunction } | null,
defaultValue: T,
): T {
return useSyncExternalStore(
(callback) => {
if (!signal) return () => {};
return signal.sub(callback);
},
() => (signal ? signal() : defaultValue),
() => defaultValue,
);
}
/**
* Hook for Telegram SDK integration
* Provides fullscreen mode, safe area insets, platform info, and other features
*/
export function useTelegramSDK() {
const inTelegram = isInTelegramWebApp();
const isReady = inTelegram && sdkInitialized;
// Get platform from window.Telegram.WebApp (always available in Telegram)
const platform = useMemo<TelegramPlatform>(() => {
if (!inTelegram) return undefined;
return window.Telegram?.WebApp?.platform as TelegramPlatform;
}, [inTelegram]);
// Use SDK signals for reactive state
const isFullscreen = useSDKSignal(isReady ? viewport.isFullscreen : null, false);
const safeAreaInsets = useSDKSignal(isReady ? viewport.safeAreaInsets : null, defaultInsets);
const contentSafeAreaInsets = useSDKSignal(
isReady ? viewport.contentSafeAreaInsets : null,
defaultInsets,
);
const viewportHeight = useSDKSignal(isReady ? viewport.height : null, 0);
const viewportStableHeight = useSDKSignal(isReady ? viewport.stableHeight : null, 0);
const viewportWidth = useSDKSignal(isReady ? viewport.width : null, 0);
const isExpanded = useSDKSignal(isReady ? viewport.isExpanded : null, true);
const requestFullscreen = useCallback(() => {
if (!isReady) return;
viewport.requestFullscreen().catch((e) => {
console.warn('Fullscreen not supported:', e);
});
}, [isReady]);
const exitFullscreen = useCallback(() => {
if (!isReady) return;
viewport.exitFullscreen().catch((e) => {
console.warn('Exit fullscreen failed:', e);
});
}, [isReady]);
const toggleFullscreen = useCallback(() => {
if (isFullscreen) {
exitFullscreen();
} else {
requestFullscreen();
}
}, [isFullscreen, requestFullscreen, exitFullscreen]);
const expand = useCallback(() => {
if (!isReady) return;
viewport.expand();
}, [isReady]);
const disableVerticalSwipes = useCallback(() => {
if (!isReady) return;
if (swipeBehavior.isSupported()) {
swipeBehavior.disableVertical();
}
}, [isReady]);
const enableVerticalSwipes = useCallback(() => {
if (!isReady) return;
if (swipeBehavior.isSupported()) {
swipeBehavior.enableVertical();
}
}, [isReady]);
// Check if fullscreen is supported (Bot API 8.0+)
const isFullscreenSupported = useMemo(() => {
if (!isReady) return false;
try {
return typeof viewport.requestFullscreen === 'function';
} catch {
return false;
}
}, [isReady]);
// Check if it's mobile platform
const isMobile = platform === 'ios' || platform === 'android';
return {
isTelegramWebApp: inTelegram,
isFullscreen,
isFullscreenSupported,
safeAreaInset: safeAreaInsets,
contentSafeAreaInset: contentSafeAreaInsets,
viewportHeight,
viewportStableHeight,
viewportWidth,
isExpanded,
platform,
isMobile,
requestFullscreen,
exitFullscreen,
toggleFullscreen,
expand,
disableVerticalSwipes,
enableVerticalSwipes,
// Expose raw SDK components for advanced usage
viewport: isReady ? viewport : null,
miniApp: isReady ? miniApp : null,
};
}

View File

@@ -1,218 +1,38 @@
import { useEffect, useState, useCallback } from 'react';
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
/**
* Check if running on mobile Telegram client (iOS/Android)
* Fullscreen mode should only be applied on mobile platforms
* @deprecated This hook is deprecated. Use useTelegramSDK instead.
* This file is kept for backward compatibility and re-exports from useTelegramSDK.
*/
export function isTelegramMobile(): boolean {
const webApp = typeof window !== 'undefined' ? window.Telegram?.WebApp : undefined;
if (!webApp?.platform) return false;
// Only iOS and Android are mobile platforms
// tdesktop, macos, web, unknown - all are desktop/non-mobile
return webApp.platform === 'ios' || webApp.platform === 'android';
}
import { useTelegramSDK } from './useTelegramSDK';
// Get cached fullscreen setting
export const getCachedFullscreenEnabled = (): boolean => {
try {
return localStorage.getItem(FULLSCREEN_CACHE_KEY) === 'true';
} catch {
return false;
}
};
// Set cached fullscreen setting
export const setCachedFullscreenEnabled = (enabled: boolean) => {
try {
localStorage.setItem(FULLSCREEN_CACHE_KEY, String(enabled));
} catch {
// localStorage not available
}
};
// Re-export everything from useTelegramSDK for backward compatibility
export {
getCachedFullscreenEnabled,
setCachedFullscreenEnabled,
isInTelegramWebApp,
isTelegramMobile,
initTelegramSDK as initTelegramWebApp, // Alias for backward compatibility
} from './useTelegramSDK';
/**
* Hook for Telegram WebApp API integration
* Provides fullscreen mode, safe area insets, and other WebApp features
* @deprecated Use useTelegramSDK instead
* Hook for Telegram WebApp API integration - backward compatible wrapper
*/
export function useTelegramWebApp() {
// Initialize synchronously to avoid flash/flicker on first render
const webApp = typeof window !== 'undefined' ? window.Telegram?.WebApp : undefined;
const inTelegram = isInTelegramWebApp();
const [isFullscreen, setIsFullscreen] = useState(
() => (inTelegram && webApp?.isFullscreen) || false,
);
const [isTelegramWebApp, setIsTelegramWebApp] = useState(() => inTelegram);
const [safeAreaInset, setSafeAreaInset] = useState(
() => (inTelegram && webApp?.safeAreaInset) || { top: 0, bottom: 0, left: 0, right: 0 },
);
const [contentSafeAreaInset, setContentSafeAreaInset] = useState(
() =>
(inTelegram && webApp?.contentSafeAreaInset) || {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
);
useEffect(() => {
// Only run Telegram-specific code if we're actually in Telegram
const isActuallyInTelegram = isInTelegramWebApp();
if (!webApp || !isActuallyInTelegram) {
setIsTelegramWebApp(false);
return;
}
setIsTelegramWebApp(true);
setIsFullscreen(webApp.isFullscreen || false);
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 });
setContentSafeAreaInset(
webApp.contentSafeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 },
);
// Expand WebApp to full height
webApp.expand();
webApp.ready();
// Listen for fullscreen changes
const handleFullscreenChanged = () => {
setIsFullscreen(webApp.isFullscreen || false);
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 });
setContentSafeAreaInset(
webApp.contentSafeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 },
);
};
// Listen for safe area changes
const handleSafeAreaChanged = () => {
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 });
setContentSafeAreaInset(
webApp.contentSafeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 },
);
};
webApp.onEvent('fullscreenChanged', handleFullscreenChanged);
webApp.onEvent('safeAreaChanged', handleSafeAreaChanged);
webApp.onEvent('contentSafeAreaChanged', handleSafeAreaChanged);
return () => {
webApp.offEvent('fullscreenChanged', handleFullscreenChanged);
webApp.offEvent('safeAreaChanged', handleSafeAreaChanged);
webApp.offEvent('contentSafeAreaChanged', handleSafeAreaChanged);
};
}, [webApp]);
const requestFullscreen = useCallback(() => {
if (webApp?.requestFullscreen) {
try {
webApp.requestFullscreen();
} catch (e) {
console.warn('Fullscreen not supported:', e);
}
}
}, [webApp]);
const exitFullscreen = useCallback(() => {
if (webApp?.exitFullscreen) {
try {
webApp.exitFullscreen();
} catch (e) {
console.warn('Exit fullscreen failed:', e);
}
}
}, [webApp]);
const toggleFullscreen = useCallback(() => {
if (isFullscreen) {
exitFullscreen();
} else {
requestFullscreen();
}
}, [isFullscreen, requestFullscreen, exitFullscreen]);
const disableVerticalSwipes = useCallback(() => {
try {
if (webApp?.disableVerticalSwipes && webApp.version && webApp.version >= '7.7') {
webApp.disableVerticalSwipes();
}
} catch {
// Not supported in this version
}
}, [webApp]);
const enableVerticalSwipes = useCallback(() => {
try {
if (webApp?.enableVerticalSwipes && webApp.version && webApp.version >= '7.7') {
webApp.enableVerticalSwipes();
}
} catch {
// Not supported in this version
}
}, [webApp]);
// Check if fullscreen is supported (Bot API 8.0+)
const isFullscreenSupported = Boolean(webApp?.requestFullscreen);
const sdk = useTelegramSDK();
return {
isTelegramWebApp,
isFullscreen,
isFullscreenSupported,
safeAreaInset,
contentSafeAreaInset,
requestFullscreen,
exitFullscreen,
toggleFullscreen,
disableVerticalSwipes,
enableVerticalSwipes,
webApp,
isTelegramWebApp: sdk.isTelegramWebApp,
isFullscreen: sdk.isFullscreen,
isFullscreenSupported: sdk.isFullscreenSupported,
safeAreaInset: sdk.safeAreaInset,
contentSafeAreaInset: sdk.contentSafeAreaInset,
requestFullscreen: sdk.requestFullscreen,
exitFullscreen: sdk.exitFullscreen,
toggleFullscreen: sdk.toggleFullscreen,
disableVerticalSwipes: sdk.disableVerticalSwipes,
enableVerticalSwipes: sdk.enableVerticalSwipes,
// For backward compatibility, expose webApp as the raw Telegram object
webApp: window.Telegram?.WebApp ?? null,
};
}
/**
* Check if we're actually running inside Telegram Mini App
* (not just the script loaded on a regular webpage)
*/
export function isInTelegramWebApp(): boolean {
const webApp = window.Telegram?.WebApp;
// Check if initData exists - it's empty when not in Telegram
return Boolean(webApp?.initData && webApp.initData.length > 0);
}
/**
* Initialize Telegram WebApp on app start
* Call this in main.tsx or App.tsx
*/
export function initTelegramWebApp() {
const webApp = window.Telegram?.WebApp;
// Only initialize if we're actually in Telegram
if (!webApp || !isInTelegramWebApp()) {
return;
}
webApp.ready();
webApp.expand();
// Disable vertical swipes to prevent accidental closing (requires Bot API 7.7+)
try {
if (webApp.disableVerticalSwipes && webApp.version && webApp.version >= '7.7') {
webApp.disableVerticalSwipes();
}
} catch {
// Swipe control not supported in this version
}
// Auto-enter fullscreen if enabled in settings (use cached value for instant response)
// Only apply fullscreen on mobile Telegram (iOS/Android) - desktop doesn't need it
const fullscreenEnabled = getCachedFullscreenEnabled();
if (fullscreenEnabled && isTelegramMobile() && webApp.requestFullscreen && !webApp.isFullscreen) {
try {
webApp.requestFullscreen();
} catch (e) {
console.warn('Auto-fullscreen failed:', e);
}
}
}