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).
This commit is contained in:
c0mrade
2026-05-22 15:02:56 +03:00
parent e44a09312e
commit 2bcba3b60f
2 changed files with 49 additions and 1 deletions

View File

@@ -15,6 +15,7 @@ import { WebSocketProvider } from './providers/WebSocketProvider';
import { ToastProvider } from './components/Toast'; import { ToastProvider } from './components/Toast';
import { TooltipProvider } from './components/primitives/Tooltip'; import { TooltipProvider } from './components/primitives/Tooltip';
import { isInTelegramWebApp } from './hooks/useTelegramSDK'; import { isInTelegramWebApp } from './hooks/useTelegramSDK';
import { hasInAppHistory, getFallbackParentPath } from './utils/navigation';
const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const; const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const;
@@ -30,6 +31,8 @@ function TelegramBackButton() {
const navigate = useNavigate(); const navigate = useNavigate();
const navigateRef = useRef(navigate); const navigateRef = useRef(navigate);
navigateRef.current = navigate; navigateRef.current = navigate;
const pathnameRef = useRef(location.pathname);
pathnameRef.current = location.pathname;
useEffect(() => { useEffect(() => {
const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname); 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 // Stable handler — ref prevents re-subscription on every render
const handler = useCallback(() => { 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(() => { useEffect(() => {

38
src/utils/navigation.ts Normal file
View File

@@ -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('/') : '/';
}