fix(navigation): single-tariff /subscriptions/:id deep-link shows Close, not Back

Refines 3c2f650 — that commit broke the loop by suppressing the single-tariff
redirect on idx=0, but left the BackButton visible. The user still saw Back
on the detail page and needed two taps (Back → Close) to exit. Серёжа's
report was specifically that Back should not appear at all when you land on
your only subscription via a bot button.

Move the decision up to TelegramBackButton:

  - Subscribe to the existing ['subscriptions-list'] query (deduped by
    React Query, so no extra fetch).
  - On /subscriptions/:id, when multi_tariff_enabled is false and there is
    no in-app history (idx=0 deep-link entry), hide the BackButton. Telegram
    then surfaces its native Close (X) in the header.
  - Multi-tariff users still see BackButton on /subscriptions/:id deep-links
    because their /subscriptions list is meaningful (they have many subs).
    The 2bcba3b fallback-to-parent behaviour remains intact for unrelated
    deep-links like /balance/top-up, /admin/*, etc.

Revert the defensive guard in Subscriptions.tsx — the loop can no longer be
triggered because no BackButton is shown to trigger it.

Behaviour summary:

  flow                                    BackButton  exit cost
  --------------------------------------  ----------  ---------
  deep-link → /subscriptions/:id (1T)     hidden      Close, 1 tap
  / → /subscriptions → /subscriptions/:id shown       Back → /, 1 tap
  deep-link → /balance/top-up             shown       Back → /balance ✓
  deep-link → /subscriptions/:id (multi)  shown       Back → /subscriptions list

Reported by Серёжа, bugs topic #599679.
This commit is contained in:
c0mrade
2026-05-29 11:36:22 +03:00
parent 3c2f650c28
commit 0ed8bb1d48
2 changed files with 27 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ import {
onBackButtonClick, onBackButtonClick,
offBackButtonClick, offBackButtonClick,
} from '@telegram-apps/sdk-react'; } from '@telegram-apps/sdk-react';
import { useQuery } from '@tanstack/react-query';
import Twemoji from 'react-twemoji'; import Twemoji from 'react-twemoji';
import App from './App'; import App from './App';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
@@ -16,6 +17,7 @@ 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'; import { hasInAppHistory, getFallbackParentPath } from './utils/navigation';
import { subscriptionApi } from './api/subscription';
const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const; const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as const;
@@ -26,6 +28,13 @@ const TWEMOJI_OPTIONS = { className: 'twemoji', folder: 'svg', ext: '.svg' } as
/** Pages reachable from bottom nav — treat as top-level (no back button). */ /** Pages reachable from bottom nav — treat as top-level (no back button). */
const BOTTOM_NAV_PATHS = ['/', '/subscriptions', '/balance', '/referral', '/support', '/wheel']; 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. */
const SUBSCRIPTION_DETAIL_RE = /^\/subscriptions\/\d+\/?$/;
function TelegramBackButton() { function TelegramBackButton() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -34,16 +43,30 @@ function TelegramBackButton() {
const pathnameRef = useRef(location.pathname); const pathnameRef = useRef(location.pathname);
pathnameRef.current = location.pathname; pathnameRef.current = location.pathname;
// 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.
const { data: subData } = useQuery({
queryKey: ['subscriptions-list'],
queryFn: () => subscriptionApi.getSubscriptions(),
staleTime: 30_000,
// Don't fetch outside Telegram — the cabinet still loads on the web.
enabled: isInTelegramWebApp(),
});
const isMultiTariff = subData?.multi_tariff_enabled ?? false;
useEffect(() => { useEffect(() => {
const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname); const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname);
const isSingleTariffDetailDeepLink =
!isMultiTariff && SUBSCRIPTION_DETAIL_RE.test(location.pathname) && !hasInAppHistory();
try { try {
if (isTopLevel) { if (isTopLevel || isSingleTariffDetailDeepLink) {
hideBackButton(); hideBackButton();
} else { } else {
showBackButton(); showBackButton();
} }
} catch {} } catch {}
}, [location]); }, [location, isMultiTariff]);
// Stable handler — ref prevents re-subscription on every render // Stable handler — ref prevents re-subscription on every render
const handler = useCallback(() => { const handler = useCallback(() => {

View File

@@ -9,7 +9,6 @@ import { getGlassColors } from '../utils/glassTheme';
import { useAuthStore } from '../store/auth'; import { useAuthStore } from '../store/auth';
import SubscriptionListCard from '../components/subscription/SubscriptionListCard'; import SubscriptionListCard from '../components/subscription/SubscriptionListCard';
import TrialOfferCard from '../components/dashboard/TrialOfferCard'; import TrialOfferCard from '../components/dashboard/TrialOfferCard';
import { hasInAppHistory } from '../utils/navigation';
function EmptyState({ onBuy }: { onBuy: () => void }) { function EmptyState({ onBuy }: { onBuy: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -107,11 +106,8 @@ export default function Subscriptions() {
}, },
}); });
// Single-tariff mode with one subscription: skip list, go directly to detail. // Single-tariff mode with one subscription: skip list, go directly to detail
// Skip the redirect when the user just hit the Telegram BackButton from if (data && !isMultiTariff && subscriptions.length === 1) {
// /subscriptions/:id — that lands us here with idx=0, and redirecting back
// to the detail page creates a Back-button loop the user can't escape (#599679).
if (data && !isMultiTariff && subscriptions.length === 1 && hasInAppHistory()) {
return <Navigate to={`/subscriptions/${subscriptions[0].id}`} replace />; return <Navigate to={`/subscriptions/${subscriptions[0].id}`} replace />;
} }