fix(pwa): keep PWA alive during TG login (anchor opens Telegram, app polls on /tg-wait)

window.location.href=deep_link navigated the whole PWA to t.me, destroying the
/tg-wait polling screen -> the authenticated token was never fetched. Prefetch
the pairing session and open Telegram via an <a href> (OS app-switch, PWA stays
mounted polling); add an Open-Telegram fallback link on the wait screen.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-19 23:44:57 +10:00
co-authored by claude-flow
parent 5ab4e9e74e
commit d2cd7632a5
2 changed files with 50 additions and 18 deletions
+29 -13
View File
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { authRequest, authVerify, tgSession } from "@/api/driver"; import { authRequest, authVerify, tgSession } from "@/api/driver";
@@ -15,19 +15,23 @@ export function LoginPage() {
const [channel, setChannel] = useState<string>(""); const [channel, setChannel] = useState<string>("");
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [mode, setMode] = useState<"choose" | "code">("choose"); const [mode, setMode] = useState<"choose" | "code">("choose");
// Prefetched pairing session so the «Войти через Telegram» control is a real
// <a href> the OS opens in Telegram (a synchronous user gesture) WITHOUT
// navigating the PWA away — the app stays mounted on /tg-wait and polls.
const [tg, setTg] = useState<{ pair_session: string; deep_link: string | null } | null>(null);
const e164 = "7" + phone; const e164 = "7" + phone;
async function loginViaTelegram() { useEffect(() => {
setBusy(true); let alive = true;
try { tgSession().then((r) => { if (alive) setTg(r); }).catch(() => {});
const r = await tgSession(); return () => { alive = false; };
sessionStorage.setItem("pp-pair-session", r.pair_session); }, []);
if (r.deep_link) { window.location.href = r.deep_link; }
function startTg() {
if (!tg) return;
sessionStorage.setItem("pp-pair-session", tg.pair_session);
if (tg.deep_link) sessionStorage.setItem("pp-pair-deeplink", tg.deep_link);
nav("/tg-wait"); nav("/tg-wait");
} catch {
toast.error("Не удалось начать вход. Попробуйте «по коду».");
setBusy(false);
}
} }
async function requestCode() { async function requestCode() {
@@ -75,9 +79,21 @@ export function LoginPage() {
{mode === "choose" ? ( {mode === "choose" ? (
<> <>
<button className="btn-primary" disabled={busy} onClick={loginViaTelegram}> {tg?.deep_link ? (
{busy ? <Spinner /> : "Войти через Telegram"} <a
href={tg.deep_link}
target="_blank"
rel="noopener noreferrer"
className="btn-primary block text-center"
onClick={startTg}
>
Войти через Telegram
</a>
) : (
<button className="btn-primary" disabled>
<Spinner />
</button> </button>
)}
<p className="text-muted text-xs mt-3">Telegram подтвердит ваш номер автоматически.</p> <p className="text-muted text-xs mt-3">Telegram подтвердит ваш номер автоматически.</p>
<button className="btn-ghost mt-4" onClick={() => setMode("code")}>Войти по коду</button> <button className="btn-ghost mt-4" onClick={() => setMode("code")}>Войти по коду</button>
</> </>
+19 -3
View File
@@ -6,6 +6,7 @@ import { Spinner } from "@/components/Spinner";
export function TgWaitPage() { export function TgWaitPage() {
const nav = useNavigate(); const nav = useNavigate();
const pairSession = sessionStorage.getItem("pp-pair-session"); const pairSession = sessionStorage.getItem("pp-pair-session");
const deepLink = sessionStorage.getItem("pp-pair-deeplink");
const { status } = usePairPoll(pairSession); const { status } = usePairPoll(pairSession);
useEffect(() => { useEffect(() => {
@@ -13,14 +14,29 @@ export function TgWaitPage() {
}, [pairSession, nav]); }, [pairSession, nav]);
useEffect(() => { useEffect(() => {
if (status === "authenticated") { sessionStorage.removeItem("pp-pair-session"); nav("/", { replace: true }); } if (status === "authenticated") {
if (status === "expired") { sessionStorage.removeItem("pp-pair-session"); nav("/login", { replace: true, state: { expired: true } }); } sessionStorage.removeItem("pp-pair-session");
sessionStorage.removeItem("pp-pair-deeplink");
nav("/", { replace: true });
}
if (status === "expired") {
sessionStorage.removeItem("pp-pair-session");
sessionStorage.removeItem("pp-pair-deeplink");
nav("/login", { replace: true, state: { expired: true } });
}
}, [status, nav]); }, [status, nav]);
return ( return (
<div className="pt-16 text-center"> <div className="pt-16 text-center">
<h1 className="text-xl font-extrabold mb-2">Премиум Водитель</h1> <h1 className="text-xl font-extrabold mb-2">Премиум Водитель</h1>
<p className="text-muted text-sm mb-8">Подтвердите номер в Telegram нажмите «Поделиться номером», затем вернитесь сюда.</p> <p className="text-muted text-sm mb-6">
В Telegram нажмите «Поделиться номером для входа», затем вернитесь сюда вход произойдёт автоматически.
</p>
{deepLink && (
<a href={deepLink} target="_blank" rel="noopener noreferrer" className="btn-primary block text-center mb-6">
Открыть Telegram
</a>
)}
<Spinner /> <Spinner />
</div> </div>
); );