mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
273
src/hooks/useTelegramSDK.ts
Normal file
273
src/hooks/useTelegramSDK.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user