Migrate to Telegram Navigator integration

Replace manual BackButton management with official @telegram-apps/react-router-integration
to fix navigation issues (back button cycling and double-click).

Changes:
- Migrate from @tma.js/sdk-react to @telegram-apps/sdk-react
- Downgrade react-router-dom to v6 for package compatibility
- Add AppWithNavigator component with Telegram navigator integration
- Use browser history mode (hashMode: null) to prevent hash URLs
- Add multi-level platform detection (user data, platform, version)
- Add try-catch error handling for navigator attach/detach
- Simplify useBackButton hook to no-op (navigator handles automatically)
- Update vite config with new Telegram packages

The navigator automatically manages BackButton based on router history,
falling back to BrowserRouter in regular browser environment.
This commit is contained in:
c0mrade
2026-02-04 07:18:43 +03:00
parent 482bdd0b0d
commit 483f3f4d0f
6 changed files with 268 additions and 180 deletions

102
src/AppWithNavigator.tsx Normal file
View File

@@ -0,0 +1,102 @@
import { useEffect, useMemo } from 'react';
import { BrowserRouter, Router } from 'react-router-dom';
import { useIntegration } from '@telegram-apps/react-router-integration';
import { initNavigator } from '@telegram-apps/sdk';
import App from './App';
import { PlatformProvider } from './platform/PlatformProvider';
import { ThemeColorsProvider } from './providers/ThemeColorsProvider';
import { WebSocketProvider } from './providers/WebSocketProvider';
import { ToastProvider } from './components/Toast';
import { TooltipProvider } from './components/primitives/Tooltip';
/**
* Check if running inside Telegram Mini App
* Uses multiple checks to reliably detect Telegram environment
*/
function isTelegramMiniApp(): boolean {
if (typeof window === 'undefined') return false;
const webApp = window.Telegram?.WebApp;
if (!webApp) return false;
// Check 1: initDataUnsafe should have user data in real Telegram
const hasUserData = webApp.initDataUnsafe?.user?.id !== undefined;
// Check 2: Platform should not be 'unknown' (which is default in browser)
const validPlatform = webApp.platform !== 'unknown' && webApp.platform !== '';
// Check 3: Version should be present (SDK loads in Telegram only)
const hasVersion = webApp.version !== undefined && webApp.version !== '';
return hasUserData || (validPlatform && hasVersion);
}
/**
* Component wrapper for Telegram navigator setup.
* Integrates Telegram Mini Apps navigator with React Router to provide
* automatic BackButton management based on navigation history.
* Falls back to BrowserRouter when not in Telegram Mini App.
*/
/**
* Navigator-based router for Telegram Mini App
*/
function TelegramRouter({ children }: { children: React.ReactNode }) {
const navigator = useMemo(() => initNavigator('app-navigation-state', { hashMode: null }), []);
const [location, reactNavigator] = useIntegration(navigator);
useEffect(() => {
try {
navigator.attach();
return () => {
try {
navigator.detach();
} catch (err) {
console.warn('Failed to detach navigator:', err);
}
};
} catch (err) {
console.warn('Failed to attach navigator:', err);
}
}, [navigator]);
return (
<Router location={location} navigator={reactNavigator}>
{children}
</Router>
);
}
export function AppWithNavigator() {
const isTelegram = useMemo(() => {
const result = isTelegramMiniApp();
console.log('[AppWithNavigator] Platform detection:', {
isTelegram: result,
platform: window.Telegram?.WebApp?.platform,
hasUser: window.Telegram?.WebApp?.initDataUnsafe?.user?.id !== undefined,
version: window.Telegram?.WebApp?.version,
});
return result;
}, []);
// Common app content
const appContent = (
<PlatformProvider>
<ThemeColorsProvider>
<TooltipProvider>
<ToastProvider>
<WebSocketProvider>
<App />
</WebSocketProvider>
</ToastProvider>
</TooltipProvider>
</ThemeColorsProvider>
</PlatformProvider>
);
// Use Telegram navigator in Mini App, BrowserRouter elsewhere
if (isTelegram) {
return <TelegramRouter>{appContent}</TelegramRouter>;
}
return <BrowserRouter>{appContent}</BrowserRouter>;
}

View File

@@ -1,13 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import { PlatformProvider } from './platform/PlatformProvider';
import { ThemeColorsProvider } from './providers/ThemeColorsProvider';
import { WebSocketProvider } from './providers/WebSocketProvider';
import { ToastProvider } from './components/Toast';
import { TooltipProvider } from './components/primitives/Tooltip';
import { AppWithNavigator } from './AppWithNavigator';
import { initLogoPreload } from './api/branding';
import { initTelegramSDK } from './hooks/useTelegramSDK';
import './i18n';
@@ -31,19 +25,7 @@ const queryClient = new QueryClient({
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<PlatformProvider>
<ThemeColorsProvider>
<TooltipProvider>
<ToastProvider>
<WebSocketProvider>
<App />
</WebSocketProvider>
</ToastProvider>
</TooltipProvider>
</ThemeColorsProvider>
</PlatformProvider>
</BrowserRouter>
<AppWithNavigator />
</QueryClientProvider>
</React.StrictMode>,
);

View File

@@ -1,6 +1,3 @@
import { useEffect, useRef, useCallback } from 'react';
import { usePlatform } from '@/platform/hooks/usePlatform';
interface UseBackButtonOptions {
/**
* Whether the back button should be visible
@@ -11,65 +8,46 @@ interface UseBackButtonOptions {
/**
* Hook to manage the Telegram BackButton
* Automatically shows/hides based on component lifecycle
*
* @param onBack - Callback when back button is pressed
* @param options - Configuration options
* NOTE: This hook is now a no-op for backward compatibility.
* The Telegram Navigator (@telegram-apps/react-router-integration) handles
* BackButton visibility and navigation automatically based on router history.
*
* The navigator:
* - Shows BackButton when there's navigation history
* - Hides BackButton on root page
* - Handles back button clicks automatically
*
* @param onBack - (Ignored) Callback when back button is pressed
* @param options - (Ignored) Configuration options
*
* @example
* ```tsx
* function MyPage() {
* const navigate = useNavigate();
* useBackButton(() => navigate(-1));
* useBackButton(() => navigate('/admin')); // No-op: navigator handles this
* // ...
* }
* ```
*/
export function useBackButton(
onBack: (() => void) | null | undefined,
options: UseBackButtonOptions = {},
_onBack: (() => void) | null | undefined,
_options: UseBackButtonOptions = {},
): void {
const { backButton, capabilities } = usePlatform();
const { visible = true } = options;
// Use ref to prevent callback recreation issues
const callbackRef = useRef(onBack);
callbackRef.current = onBack;
// Stable callback wrapper
const handleBack = useCallback(() => {
callbackRef.current?.();
}, []);
useEffect(() => {
// If no native back button support, do nothing
if (!capabilities.hasBackButton) {
return;
}
// If callback is null/undefined or visible is false, hide button
if (!onBack || !visible) {
backButton.hide();
return;
}
// Show the back button with our handler
backButton.show(handleBack);
// Cleanup: hide button when component unmounts
return () => {
backButton.hide();
};
}, [backButton, capabilities.hasBackButton, handleBack, onBack, visible]);
// No-op: Navigator handles BackButton automatically
return;
}
/**
* Hook to conditionally show back button based on navigation depth
* Useful for showing back button only when there's history to go back to
*
* @param canGoBack - Whether navigation back is possible
* @param onBack - Callback when back button is pressed
* NOTE: This hook is now a no-op for backward compatibility.
* The navigator handles conditional visibility automatically.
*
* @param canGoBack - (Ignored) Whether navigation back is possible
* @param onBack - (Ignored) Callback when back button is pressed
*/
export function useConditionalBackButton(canGoBack: boolean, onBack: () => void): void {
useBackButton(canGoBack ? onBack : null);
export function useConditionalBackButton(_canGoBack: boolean, _onBack: () => void): void {
// No-op: Navigator handles BackButton automatically
return;
}