From 6f8bc4fca592f074d4705069a48bbaef3e5a105d Mon Sep 17 00:00:00 2001 From: c0mrade Date: Sun, 1 Feb 2026 20:37:23 +0300 Subject: [PATCH] fix: revert to native Telegram WebApp API, remove SDK usage --- src/hooks/useTelegramSDK.ts | 249 +++++------------- src/platform/adapters/TelegramAdapter.ts | 319 ++++++++++------------- 2 files changed, 207 insertions(+), 361 deletions(-) diff --git a/src/hooks/useTelegramSDK.ts b/src/hooks/useTelegramSDK.ts index 9994e5a..0ed1503 100644 --- a/src/hooks/useTelegramSDK.ts +++ b/src/hooks/useTelegramSDK.ts @@ -1,9 +1,4 @@ -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; +import { useCallback, useMemo } from 'react'; const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled'; @@ -48,7 +43,6 @@ export function isTelegramMobile(): boolean { /** * 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; @@ -56,86 +50,31 @@ export function getTelegramInitData(): string | null { } /** - * Initialize Telegram SDK - * Call this once at app startup (in main.tsx) + * Initialize Telegram WebApp (basic setup without SDK) */ export function initTelegramSDK() { - // Only run in Telegram context if (!isInTelegramWebApp()) { return; } - // Prevent double initialization - if (sdkInitialized) { - return; - } + const tg = window.Telegram?.WebApp; + if (!tg) return; - try { - // Initialize the SDK - initCleanup = init(); - sdkInitialized = true; + // Basic initialization + tg.ready(); + tg.expand(); - // Mount mini app and call ready (sync) - miniApp.mount(); - miniApp.ready(); + // Disable closing confirmation by default + tg.disableClosingConfirmation?.(); - // Mount theme params and bind CSS variables (sync) - try { - themeParams.mount(); - themeParams.bindCssVars(); - } catch { - // Theme params may already be mounted or not supported - } - - // Disable vertical swipes if supported (sync) - try { - if (swipeBehavior.isSupported()) { - swipeBehavior.mount(); - swipeBehavior.disableVertical(); + // Auto-enter fullscreen if enabled in settings (mobile only) + const fullscreenEnabled = getCachedFullscreenEnabled(); + if (fullscreenEnabled && isTelegramMobile() && tg.requestFullscreen) { + setTimeout(() => { + if (!tg.isFullscreen) { + tg.requestFullscreen?.(); } - } catch { - // Swipe behavior not available - } - - // Mount viewport and bind CSS variables (async) - viewport - .mount() - .then(() => { - viewport.bindCssVars(); - // Expand the mini app - viewport.expand(); - - // Auto-enter fullscreen if enabled in settings (mobile only) - const fullscreenEnabled = getCachedFullscreenEnabled(); - if (fullscreenEnabled && isTelegramMobile()) { - try { - const isFullscreen = viewport.isFullscreen(); - if (!isFullscreen) { - viewport.requestFullscreen().catch((e) => { - console.warn('Auto-fullscreen failed:', e); - }); - } - } catch { - // Viewport signal not available - } - } - }) - .catch((err) => { - console.warn('Viewport mount failed:', err); - }); - } 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; + }, 100); } } @@ -153,79 +92,59 @@ export type TelegramPlatform = | 'unknown' | undefined; -// Default values for when SDK is not available +// Default values const defaultInsets = { top: 0, bottom: 0, left: 0, right: 0 }; /** - * Helper to subscribe to SDK signals using useSyncExternalStore pattern - * Safely handles unmounted components by returning default values - */ -function useSDKSignal( - signal: { (): T; sub(fn: VoidFunction): VoidFunction } | null, - defaultValue: T, -): T { - return useSyncExternalStore( - (callback) => { - if (!signal) return () => {}; - try { - return signal.sub(callback); - } catch { - // Signal not available (component unmounted) - return () => {}; - } - }, - () => { - if (!signal) return defaultValue; - try { - return signal(); - } catch { - // Signal not available (component unmounted) - return defaultValue; - } - }, - () => defaultValue, - ); -} - -/** - * Hook for Telegram SDK integration - * Provides fullscreen mode, safe area insets, platform info, and other features + * Hook for Telegram WebApp integration + * Uses native window.Telegram.WebApp API */ export function useTelegramSDK() { const inTelegram = isInTelegramWebApp(); - const isReady = inTelegram && sdkInitialized; + const tg = window.Telegram?.WebApp; - // Get platform from window.Telegram.WebApp (always available in Telegram) const platform = useMemo(() => { if (!inTelegram) return undefined; - return window.Telegram?.WebApp?.platform as TelegramPlatform; - }, [inTelegram]); + return tg?.platform as TelegramPlatform; + }, [inTelegram, tg?.platform]); - // 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 isMobile = platform === 'ios' || platform === 'android'; + + // Safe area insets from Telegram WebApp + const safeAreaInset = useMemo(() => { + if (!inTelegram || !tg?.safeAreaInset) return defaultInsets; + return { + top: tg.safeAreaInset.top || 0, + bottom: tg.safeAreaInset.bottom || 0, + left: tg.safeAreaInset.left || 0, + right: tg.safeAreaInset.right || 0, + }; + }, [inTelegram, tg?.safeAreaInset]); + + const contentSafeAreaInset = useMemo(() => { + if (!inTelegram || !tg?.contentSafeAreaInset) return defaultInsets; + return { + top: tg.contentSafeAreaInset.top || 0, + bottom: tg.contentSafeAreaInset.bottom || 0, + left: tg.contentSafeAreaInset.left || 0, + right: tg.contentSafeAreaInset.right || 0, + }; + }, [inTelegram, tg?.contentSafeAreaInset]); + + const isFullscreen = tg?.isFullscreen ?? false; + const viewportHeight = tg?.viewportHeight ?? 0; + const viewportStableHeight = tg?.viewportStableHeight ?? 0; + const isExpanded = tg?.isExpanded ?? true; const requestFullscreen = useCallback(() => { - if (!isReady) return; - viewport.requestFullscreen().catch((e) => { - console.warn('Fullscreen not supported:', e); - }); - }, [isReady]); + if (!inTelegram || !tg?.requestFullscreen) return; + tg.requestFullscreen(); + }, [inTelegram, tg]); const exitFullscreen = useCallback(() => { - if (!isReady) return; - viewport.exitFullscreen().catch((e) => { - console.warn('Exit fullscreen failed:', e); - }); - }, [isReady]); + if (!inTelegram || !tg?.exitFullscreen) return; + tg.exitFullscreen(); + }, [inTelegram, tg]); const toggleFullscreen = useCallback(() => { if (isFullscreen) { @@ -236,58 +155,31 @@ export function useTelegramSDK() { }, [isFullscreen, requestFullscreen, exitFullscreen]); const expand = useCallback(() => { - if (!isReady) return; - try { - viewport.expand(); - } catch { - // Viewport not available - } - }, [isReady]); + if (!inTelegram || !tg?.expand) return; + tg.expand(); + }, [inTelegram, tg]); const disableVerticalSwipes = useCallback(() => { - if (!isReady) return; - try { - if (swipeBehavior.isSupported()) { - swipeBehavior.disableVertical(); - } - } catch { - // Swipe behavior not available - } - }, [isReady]); + if (!inTelegram || !tg?.disableVerticalSwipes) return; + tg.disableVerticalSwipes(); + }, [inTelegram, tg]); const enableVerticalSwipes = useCallback(() => { - if (!isReady) return; - try { - if (swipeBehavior.isSupported()) { - swipeBehavior.enableVertical(); - } - } catch { - // Swipe behavior not available - } - }, [isReady]); + if (!inTelegram || !tg?.enableVerticalSwipes) return; + tg.enableVerticalSwipes(); + }, [inTelegram, tg]); - // 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'; + const isFullscreenSupported = inTelegram && typeof tg?.requestFullscreen === 'function'; return { isTelegramWebApp: inTelegram, isFullscreen, isFullscreenSupported, - safeAreaInset: safeAreaInsets, - contentSafeAreaInset: contentSafeAreaInsets, + safeAreaInset, + contentSafeAreaInset, viewportHeight, viewportStableHeight, - viewportWidth, + viewportWidth: 0, // Not available in native API isExpanded, platform, isMobile, @@ -297,8 +189,7 @@ export function useTelegramSDK() { expand, disableVerticalSwipes, enableVerticalSwipes, - // Expose raw SDK components for advanced usage - viewport: isReady ? viewport : null, - miniApp: isReady ? miniApp : null, + viewport: null, + miniApp: null, }; } diff --git a/src/platform/adapters/TelegramAdapter.ts b/src/platform/adapters/TelegramAdapter.ts index 98843c3..9dc3e7e 100644 --- a/src/platform/adapters/TelegramAdapter.ts +++ b/src/platform/adapters/TelegramAdapter.ts @@ -1,12 +1,3 @@ -import { - backButton, - mainButton, - hapticFeedback, - cloudStorage, - themeParams, - popup, - miniApp, -} from '@tma.js/sdk-react'; import { isInTelegramWebApp } from '@/hooks/useTelegramSDK'; import type { PlatformContext, @@ -24,217 +15,180 @@ import type { HapticNotificationType, } from '@/platform/types'; -// Keep reference to raw Telegram WebApp for features not in SDK +// Use raw Telegram WebApp API directly - SDK has initialization issues function getTelegram(): TelegramWebApp | null { return window.Telegram?.WebApp ?? null; } -function safeIsSupported(checkFn: () => boolean): boolean { - try { - return checkFn(); - } catch { - return false; - } -} - function createCapabilities(): PlatformCapabilities { const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); return { - hasBackButton: inTelegram && safeIsSupported(() => backButton.isSupported()), - hasMainButton: inTelegram, - hasHapticFeedback: inTelegram && safeIsSupported(() => hapticFeedback.isSupported()), - hasNativeDialogs: inTelegram && safeIsSupported(() => popup.isSupported()), + hasBackButton: inTelegram && !!tg?.BackButton, + hasMainButton: inTelegram && !!tg?.MainButton, + hasHapticFeedback: inTelegram && !!tg?.HapticFeedback, + hasNativeDialogs: inTelegram && !!tg?.showPopup, hasThemeSync: inTelegram, hasInvoice: !!tg?.openInvoice, - hasCloudStorage: inTelegram && safeIsSupported(() => cloudStorage.isSupported()), + hasCloudStorage: inTelegram && !!tg?.CloudStorage, hasShare: true, version: tg?.version, }; } function createBackButtonController(): BackButtonController { + const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - let removeClickListener: VoidFunction | null = null; - - // Mount back button on first use - let mounted = false; - const ensureMounted = () => { - if (!mounted && inTelegram && safeIsSupported(() => backButton.isSupported())) { - try { - backButton.mount(); - mounted = true; - } catch { - // Already mounted or not supported - } - } - }; + let currentCallback: (() => void) | null = null; return { get isVisible() { - if (!inTelegram || !mounted) return false; - try { - return backButton.isVisible(); - } catch { - return false; - } + if (!inTelegram || !tg?.BackButton) return false; + return tg.BackButton.isVisible; }, show(onClick: () => void) { - if (!inTelegram || !safeIsSupported(() => backButton.isSupported())) return; - - ensureMounted(); + if (!inTelegram || !tg?.BackButton) return; // Remove previous callback if exists - if (removeClickListener) { - removeClickListener(); - removeClickListener = null; + if (currentCallback) { + tg.BackButton.offClick(currentCallback); } - removeClickListener = backButton.onClick(onClick); - backButton.show(); + currentCallback = onClick; + tg.BackButton.onClick(onClick); + tg.BackButton.show(); }, hide() { - if (!inTelegram || !safeIsSupported(() => backButton.isSupported())) return; + if (!inTelegram || !tg?.BackButton) return; - if (removeClickListener) { - removeClickListener(); - removeClickListener = null; + if (currentCallback) { + tg.BackButton.offClick(currentCallback); + currentCallback = null; } - backButton.hide(); + tg.BackButton.hide(); }, }; } function createMainButtonController(): MainButtonController { + const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - let removeClickListener: VoidFunction | null = null; - - // Mount main button on first use - let mounted = false; - const ensureMounted = () => { - if (!mounted && inTelegram) { - try { - mainButton.mount(); - mounted = true; - } catch { - // Already mounted or not supported - } - } - }; + let currentCallback: (() => void) | null = null; return { get isVisible() { - if (!inTelegram || !mounted) return false; - try { - return mainButton.isVisible(); - } catch { - return false; - } + if (!inTelegram || !tg?.MainButton) return false; + return tg.MainButton.isVisible; }, show(config: MainButtonConfig) { - if (!inTelegram) return; - - ensureMounted(); + if (!inTelegram || !tg?.MainButton) return; // Remove previous callback if exists - if (removeClickListener) { - removeClickListener(); - removeClickListener = null; + if (currentCallback) { + tg.MainButton.offClick(currentCallback); } // Set button parameters - mainButton.setParams({ - text: config.text, - bgColor: config.color as `#${string}` | undefined, - textColor: config.textColor as `#${string}` | undefined, - isEnabled: config.isActive !== false, - isVisible: true, - isLoaderVisible: config.isLoading || false, - }); + tg.MainButton.setText(config.text); + if (config.color) { + tg.MainButton.color = config.color; + } + if (config.textColor) { + tg.MainButton.textColor = config.textColor; + } - removeClickListener = mainButton.onClick(config.onClick); - mainButton.show(); + if (config.isActive === false) { + tg.MainButton.disable(); + } else { + tg.MainButton.enable(); + } + + if (config.isLoading) { + tg.MainButton.showProgress(); + } else { + tg.MainButton.hideProgress(); + } + + currentCallback = config.onClick; + tg.MainButton.onClick(config.onClick); + tg.MainButton.show(); }, hide() { - if (!inTelegram) return; + if (!inTelegram || !tg?.MainButton) return; - if (removeClickListener) { - removeClickListener(); - removeClickListener = null; + if (currentCallback) { + tg.MainButton.offClick(currentCallback); + currentCallback = null; } - mainButton.hideLoader(); - mainButton.hide(); + tg.MainButton.hideProgress(); + tg.MainButton.hide(); }, showProgress(show: boolean) { - if (!inTelegram) return; + if (!inTelegram || !tg?.MainButton) return; if (show) { - mainButton.showLoader(); + tg.MainButton.showProgress(); } else { - mainButton.hideLoader(); + tg.MainButton.hideProgress(); } }, setText(text: string) { - if (!inTelegram) return; - mainButton.setText(text); + if (!inTelegram || !tg?.MainButton) return; + tg.MainButton.setText(text); }, setActive(active: boolean) { - if (!inTelegram) return; + if (!inTelegram || !tg?.MainButton) return; if (active) { - mainButton.enable(); + tg.MainButton.enable(); } else { - mainButton.disable(); + tg.MainButton.disable(); } }, }; } function createHapticController(): HapticController { + const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - const isSupported = inTelegram && safeIsSupported(() => hapticFeedback.isSupported()); + const haptic = tg?.HapticFeedback; return { impact(style: HapticImpactStyle = 'medium') { - if (!isSupported) return; - hapticFeedback.impactOccurred(style); + if (!inTelegram || !haptic) return; + haptic.impactOccurred(style); }, notification(type: HapticNotificationType) { - if (!isSupported) return; - hapticFeedback.notificationOccurred(type); + if (!inTelegram || !haptic) return; + haptic.notificationOccurred(type); }, selection() { - if (!isSupported) return; - hapticFeedback.selectionChanged(); + if (!inTelegram || !haptic) return; + haptic.selectionChanged(); }, }; } function createDialogController(): DialogController { + const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - const isSupported = inTelegram && safeIsSupported(() => popup.isSupported()); return { alert(message: string, _title?: string): Promise { return new Promise((resolve) => { - if (isSupported) { - popup - .show({ - message, - buttons: [{ type: 'ok' }], - }) - .then(() => resolve()); + if (inTelegram && tg?.showPopup) { + tg.showPopup({ message, buttons: [{ type: 'ok' }] }, () => resolve()); } else { window.alert(message); resolve(); @@ -244,16 +198,17 @@ function createDialogController(): DialogController { confirm(message: string, _title?: string): Promise { return new Promise((resolve) => { - if (isSupported) { - popup - .show({ + if (inTelegram && tg?.showPopup) { + tg.showPopup( + { message, buttons: [ { id: 'ok', type: 'ok' }, { id: 'cancel', type: 'cancel' }, ], - }) - .then((buttonId) => resolve(buttonId === 'ok')); + }, + (buttonId) => resolve(buttonId === 'ok'), + ); } else { resolve(window.confirm(message)); } @@ -262,9 +217,9 @@ function createDialogController(): DialogController { popup(options: PopupOptions): Promise { return new Promise((resolve) => { - if (isSupported) { - popup - .show({ + if (inTelegram && tg?.showPopup) { + tg.showPopup( + { title: options.title, message: options.message, buttons: options.buttons?.map((btn) => ({ @@ -272,8 +227,9 @@ function createDialogController(): DialogController { type: btn.type, text: btn.text, })), - }) - .then((buttonId) => resolve(buttonId ?? null)); + }, + (buttonId) => resolve(buttonId ?? null), + ); } else { const confirmed = window.confirm(options.message); resolve(confirmed ? 'ok' : null); @@ -284,85 +240,87 @@ function createDialogController(): DialogController { } function createThemeController(): ThemeController { + const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - // Mount theme params on first use - let mounted = false; - const ensureMounted = () => { - if (!mounted && inTelegram) { - try { - themeParams.mount(); - mounted = true; - } catch { - // Already mounted - } - } - }; - return { setHeaderColor(color: string) { - if (!inTelegram) return; - miniApp.setHeaderColor(color as `#${string}`); + if (!inTelegram || !tg?.setHeaderColor) return; + tg.setHeaderColor(color as `#${string}`); }, setBottomBarColor(color: string) { - if (!inTelegram) return; + if (!inTelegram || !tg?.setBottomBarColor) return; try { - miniApp.setBottomBarColor(color as `#${string}`); + tg.setBottomBarColor(color as `#${string}`); } catch { // Not supported in this version } }, getThemeParams() { - if (!inTelegram) return null; - ensureMounted(); - try { - const state = themeParams.state(); - // Convert SDK format to our format - return { - bg_color: state.bgColor, - text_color: state.textColor, - hint_color: state.hintColor, - link_color: state.linkColor, - button_color: state.buttonColor, - button_text_color: state.buttonTextColor, - secondary_bg_color: state.secondaryBgColor, - header_bg_color: state.headerBgColor, - bottom_bar_bg_color: state.bottomBarBgColor, - accent_text_color: state.accentTextColor, - section_bg_color: state.sectionBgColor, - section_header_text_color: state.sectionHeaderTextColor, - subtitle_text_color: state.subtitleTextColor, - destructive_text_color: state.destructiveTextColor, - }; - } catch { - return null; - } + if (!inTelegram || !tg?.themeParams) return null; + const params = tg.themeParams; + return { + bg_color: params.bg_color, + text_color: params.text_color, + hint_color: params.hint_color, + link_color: params.link_color, + button_color: params.button_color, + button_text_color: params.button_text_color, + secondary_bg_color: params.secondary_bg_color, + header_bg_color: params.header_bg_color, + bottom_bar_bg_color: params.bottom_bar_bg_color, + accent_text_color: params.accent_text_color, + section_bg_color: params.section_bg_color, + section_header_text_color: params.section_header_text_color, + subtitle_text_color: params.subtitle_text_color, + destructive_text_color: params.destructive_text_color, + }; }, }; } function createCloudStorageController(): CloudStorageController | null { + const tg = getTelegram(); const inTelegram = isInTelegramWebApp(); - if (!inTelegram || !safeIsSupported(() => cloudStorage.isSupported())) return null; + const storage = tg?.CloudStorage; + + if (!inTelegram || !storage) return null; return { async getItem(key: string): Promise { - const value = await cloudStorage.getItem(key); - return value || null; + return new Promise((resolve) => { + storage.getItem(key, (error, value) => { + resolve(error ? null : value || null); + }); + }); }, async setItem(key: string, value: string): Promise { - await cloudStorage.setItem(key, value); + return new Promise((resolve, reject) => { + storage.setItem(key, value, (error) => { + if (error) reject(new Error(String(error))); + else resolve(); + }); + }); }, async removeItem(key: string): Promise { - await cloudStorage.deleteItem(key); + return new Promise((resolve, reject) => { + storage.removeItem(key, (error) => { + if (error) reject(new Error(String(error))); + else resolve(); + }); + }); }, async getKeys(): Promise { - return cloudStorage.getKeys(); + return new Promise((resolve) => { + storage.getKeys((error, keys) => { + resolve(error ? [] : keys || []); + }); + }); }, }; } @@ -385,7 +343,6 @@ export function createTelegramAdapter(): PlatformContext { if (tg?.openInvoice) { tg.openInvoice(url, (status) => resolve(status)); } else { - // Fallback: open in new window window.open(url, '_blank'); resolve('pending'); } @@ -411,17 +368,15 @@ export function createTelegramAdapter(): PlatformContext { async share(text: string, url?: string): Promise { const shareText = url ? `${text}\n${url}` : text; - // Try native share API first (if available on mobile) if (navigator.share) { try { await navigator.share({ text: shareText, url }); return true; } catch { - // User cancelled or share failed, continue to Telegram share + // User cancelled or share failed } } - // Use Telegram share const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME; if (botUsername && tg?.openTelegramLink) { const encoded = encodeURIComponent(shareText);