refactor: move ConnectionQR into standard Layout with proper navigation

- Wrap /connection/qr route in ProtectedRoute with Layout
- Remove fullscreen overlay and custom close button
- Add AdminBackButton with replace prop to avoid history cycles
- Use replace navigation from Connection to QR page
- Add replace prop support to AdminBackButton component
This commit is contained in:
c0mrade
2026-03-07 23:25:33 +03:00
parent eed077b019
commit 8ce4b1a24a
4 changed files with 42 additions and 77 deletions

View File

@@ -406,9 +406,11 @@ function App() {
<Route <Route
path="/connection/qr" path="/connection/qr"
element={ element={
<ProtectedRoute>
<LazyPage> <LazyPage>
<ConnectionQR /> <ConnectionQR />
</LazyPage> </LazyPage>
</ProtectedRoute>
} }
/> />
<Route <Route

View File

@@ -4,6 +4,7 @@ import { BackIcon } from './icons';
interface AdminBackButtonProps { interface AdminBackButtonProps {
to?: string; to?: string;
replace?: boolean;
className?: string; className?: string;
} }
@@ -11,7 +12,7 @@ interface AdminBackButtonProps {
* Back button for admin pages. * Back button for admin pages.
* Hidden in Telegram Mini App since native back button is used instead. * Hidden in Telegram Mini App since native back button is used instead.
*/ */
export function AdminBackButton({ to = '/admin', className }: AdminBackButtonProps) { export function AdminBackButton({ to = '/admin', replace, className }: AdminBackButtonProps) {
const { platform } = usePlatform(); const { platform } = usePlatform();
// In Telegram Mini App, we use native back button // In Telegram Mini App, we use native back button
@@ -22,6 +23,7 @@ export function AdminBackButton({ to = '/admin', className }: AdminBackButtonPro
return ( return (
<Link <Link
to={to} to={to}
replace={replace}
className={ className={
className || className ||
'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600' 'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600'

View File

@@ -37,6 +37,7 @@ export default function Connection() {
const handleOpenQR = useCallback(() => { const handleOpenQR = useCallback(() => {
navigate('/connection/qr', { navigate('/connection/qr', {
replace: true,
state: { state: {
url: appConfig?.subscriptionUrl, url: appConfig?.subscriptionUrl,
hideLink: appConfig?.hideLink ?? false, hideLink: appConfig?.hideLink ?? false,

View File

@@ -2,8 +2,8 @@ import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router'; import { useLocation, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { useAuthStore } from '../store/auth';
import { useBranding } from '../hooks/useBranding'; import { useBranding } from '../hooks/useBranding';
import { AdminBackButton } from '@/components/admin';
interface ConnectionQRState { interface ConnectionQRState {
url: string; url: string;
@@ -20,80 +20,40 @@ export default function ConnectionQR() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const isLoading = useAuthStore((state) => state.isLoading);
const { appName } = useBranding(); const { appName } = useBranding();
const state = location.state as unknown; const state = location.state as unknown;
const validState = isValidState(state) ? state : null; const validState = isValidState(state) ? state : null;
useEffect(() => { useEffect(() => {
if (!isLoading && !isAuthenticated) { if (!validState) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, isLoading, navigate]);
useEffect(() => {
if (!isLoading && isAuthenticated && !validState) {
navigate('/connection', { replace: true }); navigate('/connection', { replace: true });
} }
}, [isLoading, isAuthenticated, validState, navigate]); }, [validState, navigate]);
useEffect(() => { if (!validState) {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
navigate(-1);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [navigate]);
if (isLoading || !validState) {
return null; return null;
} }
return ( return (
<div className="fixed inset-0 z-50 flex min-h-dvh flex-col items-center justify-center bg-gradient-to-b from-dark-900 to-dark-950"> <div className="animate-fade-in">
{/* Close button */} <div className="mb-6 flex items-center gap-3">
<button <AdminBackButton to="/connection" replace />
type="button" <h1 className="text-2xl font-bold text-dark-100">{t('subscription.connection.qrTitle')}</h1>
onClick={() => navigate(-1)} </div>
className="absolute left-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-dark-800/60 backdrop-blur-sm transition-colors hover:bg-dark-700/80"
aria-label={t('common.close')}
>
<svg
className="h-5 w-5 text-dark-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Content */} <div className="flex flex-col items-center">
<div className="flex w-full max-w-sm animate-scale-in flex-col items-center px-6"> <div className="flex w-full max-w-sm flex-col items-center px-6">
{/* Branding name */}
{appName && ( {appName && (
<p className="mb-3 text-sm font-medium uppercase tracking-wider text-dark-400"> <p className="mb-3 text-sm font-medium uppercase tracking-wider text-dark-400">
{appName} {appName}
</p> </p>
)} )}
{/* Title */}
<h1 className="mb-2 text-center text-xl font-bold text-dark-100">
{t('subscription.connection.qrTitle')}
</h1>
{/* Hint */}
<p className="mb-8 text-center text-sm text-dark-400"> <p className="mb-8 text-center text-sm text-dark-400">
{t('subscription.connection.qrScanHint')} {t('subscription.connection.qrScanHint')}
</p> </p>
{/* QR code container */}
<div className="rounded-3xl bg-white p-6"> <div className="rounded-3xl bg-white p-6">
<QRCodeSVG <QRCodeSVG
value={validState.url} value={validState.url}
@@ -104,7 +64,6 @@ export default function ConnectionQR() {
/> />
</div> </div>
{/* URL display */}
{!validState.hideLink && ( {!validState.hideLink && (
<p className="mt-6 max-w-full truncate text-center font-mono text-xs text-dark-500"> <p className="mt-6 max-w-full truncate text-center font-mono text-xs text-dark-500">
{validState.url} {validState.url}
@@ -112,5 +71,6 @@ export default function ConnectionQR() {
)} )}
</div> </div>
</div> </div>
</div>
); );
} }