mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(navigation): надёжная кнопка «Назад» через тип навигации, без петли (#436)
На iOS Telegram кнопка «Назад» на /subscriptions/:id казалась мёртвой: fallback вёл на /subscriptions, который авто-редиректит обратно на деталь (single-tariff, 1 подписка) — молчаливая петля. Причина — ненадёжный window.history.state.idx, который мутируют собственные redirect'ы. idx заменён на надёжный счётчик глубины по useNavigationType (PUSH +1, POP −1, REPLACE без изменений, дедуп по location.key). Кнопка корректно показывается/прячется, navigate(-1) берётся только при реальной истории. Плюс fail-closed fallback: с детали уходим в '/' если список не доказанно безопасен (multi-tariff или >1 подписки) — петля невозможна. GitHub-issue #436.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { BrowserRouter, useLocation, useNavigate } from 'react-router';
|
||||
import { BrowserRouter, useLocation, useNavigate, useNavigationType } from 'react-router';
|
||||
import {
|
||||
showBackButton,
|
||||
hideBackButton,
|
||||
@@ -16,7 +16,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';
|
||||
import { getFallbackParentPath } from './utils/navigation';
|
||||
import { subscriptionApi } from './api/subscription';
|
||||
|
||||
const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const;
|
||||
@@ -28,21 +28,38 @@ const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as
|
||||
/** Pages reachable from bottom nav — treat as top-level (no back button). */
|
||||
const BOTTOM_NAV_PATHS = ['/', '/subscriptions', '/balance', '/referral', '/support', '/wheel'];
|
||||
|
||||
/** Matches /subscriptions/:numericId — single-tariff users land here from
|
||||
* bot deep-links, but their /subscriptions list is empty (the list view
|
||||
* auto-redirects them straight back to this page). Pressing Back would loop
|
||||
* back to detail, so on a deep-link entry (idx=0) we hide the back button
|
||||
* and let Telegram surface its native Close (X) button instead. */
|
||||
/** Matches /subscriptions/:numericId. Single-tariff users land here straight
|
||||
* from bot deep-links, and their /subscriptions list auto-redirects right back
|
||||
* to this page (Subscriptions.tsx). So on a genuine deep-link entry (in-app
|
||||
* navigation depth 0) we hide the back button and let Telegram surface its
|
||||
* native Close (X); when there IS in-app history we show it and navigate back. */
|
||||
const SUBSCRIPTION_DETAIL_RE = /^\/subscriptions\/\d+\/?$/;
|
||||
|
||||
function TelegramBackButton() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const navType = useNavigationType();
|
||||
const navigateRef = useRef(navigate);
|
||||
navigateRef.current = navigate;
|
||||
const pathnameRef = useRef(location.pathname);
|
||||
pathnameRef.current = location.pathname;
|
||||
|
||||
// Reliable in-app navigation depth (the app's entry point is 0). Driven by
|
||||
// React Router's navigation TYPE — NOT window.history.state.idx, which the
|
||||
// app's own redirects mutate unpredictably and which is the root flake behind
|
||||
// issue #436 (the back button shows/acts on the wrong state). PUSH goes
|
||||
// deeper, POP unwinds, REPLACE (e.g. the Subscriptions.tsx auto-redirect) is
|
||||
// flat. De-duped by location.key so StrictMode's double-effect can't miscount.
|
||||
const depthRef = useRef(0);
|
||||
const lastKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (lastKeyRef.current === location.key) return;
|
||||
lastKeyRef.current = location.key;
|
||||
if (navType === 'PUSH') depthRef.current += 1;
|
||||
else if (navType === 'POP') depthRef.current = Math.max(0, depthRef.current - 1);
|
||||
// REPLACE: depth unchanged (replaces the current entry, adds no history)
|
||||
}, [location.key, navType]);
|
||||
|
||||
// Share the subscriptions-list query with the page-level components.
|
||||
// React Query dedupes by key so this does not cause an extra fetch when
|
||||
// Subscriptions/Subscription/Dashboard pages mount.
|
||||
@@ -55,10 +72,18 @@ function TelegramBackButton() {
|
||||
});
|
||||
const isMultiTariff = subData?.multi_tariff_enabled ?? false;
|
||||
|
||||
// Refs so the stable back handler (memoised with []) reads fresh values
|
||||
// without re-subscribing — re-subscription lets a component's local handler
|
||||
// overwrite ours via Telegram's singleton onBackButtonClick (issue #436).
|
||||
const isMultiTariffRef = useRef(isMultiTariff);
|
||||
isMultiTariffRef.current = isMultiTariff;
|
||||
const subsCountRef = useRef(subData?.subscriptions?.length ?? 0);
|
||||
subsCountRef.current = subData?.subscriptions?.length ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname);
|
||||
const isSingleTariffDetailDeepLink =
|
||||
!isMultiTariff && SUBSCRIPTION_DETAIL_RE.test(location.pathname) && !hasInAppHistory();
|
||||
!isMultiTariff && SUBSCRIPTION_DETAIL_RE.test(location.pathname) && depthRef.current === 0;
|
||||
try {
|
||||
if (isTopLevel || isSingleTariffDetailDeepLink) {
|
||||
hideBackButton();
|
||||
@@ -70,14 +95,31 @@ function TelegramBackButton() {
|
||||
|
||||
// Stable handler — ref prevents re-subscription on every render
|
||||
const handler = useCallback(() => {
|
||||
// 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()) {
|
||||
// Real in-app history (depth > 0): a normal back. Otherwise we were opened
|
||||
// directly on this route via a deep-link — navigate(-1) is a no-op, so fall
|
||||
// back to a sensible parent route instead.
|
||||
if (depthRef.current > 0) {
|
||||
navigateRef.current(-1);
|
||||
} else {
|
||||
navigateRef.current(getFallbackParentPath(pathnameRef.current), { replace: true });
|
||||
return;
|
||||
}
|
||||
// /subscriptions/:id is special: the /subscriptions list auto-redirects
|
||||
// straight back to a detail page when single-tariff with exactly one
|
||||
// subscription (Subscriptions.tsx), so falling back there loops silently and
|
||||
// the back button looks dead (issue #436). Land on the list ONLY when it is
|
||||
// PROVABLY safe (multi-tariff, or more than one subscription — neither of
|
||||
// which auto-redirects); otherwise escape to root.
|
||||
//
|
||||
// Fail-closed on purpose: `subsCount <= 1` is treated as not-safe, which
|
||||
// also covers the stale default 0 before the shared subscriptions query
|
||||
// resolves — so a fast tap on a cold cache can never route into the
|
||||
// redirecting list and re-open the loop.
|
||||
const pathname = pathnameRef.current;
|
||||
const listIsSafe = isMultiTariffRef.current || subsCountRef.current > 1;
|
||||
const fallback =
|
||||
SUBSCRIPTION_DETAIL_RE.test(pathname) && !listIsSafe
|
||||
? '/'
|
||||
: getFallbackParentPath(pathnameRef.current);
|
||||
navigateRef.current(fallback, { replace: true });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user