From 2bcba3b60f883afc5fbad980fd067b0939a32069 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Fri, 22 May 2026 15:02:56 +0300 Subject: [PATCH] fix(navigation): make Telegram back button work on deep-link entry Bot deep-links open the Mini App directly on nested routes (/admin, /balance/top-up, /info, /profile, ...) where React Router history has a single entry, so navigate(-1) was a no-op and the native back button looked dead. Fall back to the derived parent route when there is no in-app history (window.history.state.idx === 0). --- src/AppWithNavigator.tsx | 12 +++++++++++- src/utils/navigation.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/utils/navigation.ts diff --git a/src/AppWithNavigator.tsx b/src/AppWithNavigator.tsx index 823cce9..f0f20cc 100644 --- a/src/AppWithNavigator.tsx +++ b/src/AppWithNavigator.tsx @@ -15,6 +15,7 @@ import { WebSocketProvider } from './providers/WebSocketProvider'; import { ToastProvider } from './components/Toast'; import { TooltipProvider } from './components/primitives/Tooltip'; import { isInTelegramWebApp } from './hooks/useTelegramSDK'; +import { hasInAppHistory, getFallbackParentPath } from './utils/navigation'; const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const; @@ -30,6 +31,8 @@ function TelegramBackButton() { const navigate = useNavigate(); const navigateRef = useRef(navigate); navigateRef.current = navigate; + const pathnameRef = useRef(location.pathname); + pathnameRef.current = location.pathname; useEffect(() => { const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname); @@ -44,7 +47,14 @@ function TelegramBackButton() { // Stable handler — ref prevents re-subscription on every render const handler = useCallback(() => { - navigateRef.current(-1); + // When opened via a bot deep-link directly on a nested route, there is no + // in-app history and navigate(-1) is a no-op — the back button looks dead. + // Fall back to the parent route so it always navigates somewhere sensible. + if (hasInAppHistory()) { + navigateRef.current(-1); + } else { + navigateRef.current(getFallbackParentPath(pathnameRef.current), { replace: true }); + } }, []); useEffect(() => { diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts new file mode 100644 index 0000000..6af9455 --- /dev/null +++ b/src/utils/navigation.ts @@ -0,0 +1,38 @@ +/** + * Navigation helpers for back-button behavior. + * + * The Telegram Mini App (and any deep-link entry) can be opened directly on a + * nested route — e.g. a bot button that opens `/admin` or `/balance/top-up`. + * In that case React Router's history stack holds a single entry, so a plain + * `navigate(-1)` has nothing to go back to and the back button appears dead. + * These helpers let callers detect that situation and fall back to a sensible + * parent route instead. + */ + +/** + * React Router stores the current position in the history stack on + * `window.history.state.idx`. The first/entry record is `0`. When `idx === 0` + * there is no in-app history to go back to (the page is the deep-link entry). + */ +export function hasInAppHistory(): boolean { + const idx = (window.history.state as { idx?: number } | null)?.idx ?? 0; + return idx > 0; +} + +/** + * Derive a parent route by dropping the last path segment. + * + * /admin/users/123 → /admin/users + * /balance/top-up → /balance + * /info → / + * / → / + * + * Used as the back-button target when there is no in-app history. If the + * derived path is not a real route the app's catch-all redirects to `/`, + * so the user is never left stuck. + */ +export function getFallbackParentPath(pathname: string): string { + const segments = pathname.replace(/\/+$/, '').split('/').filter(Boolean); + const parent = segments.slice(0, -1); + return parent.length ? '/' + parent.join('/') : '/'; +}