diff --git a/driver-pwa/frontend/src/hooks/usePairPoll.test.tsx b/driver-pwa/frontend/src/hooks/usePairPoll.test.tsx new file mode 100644 index 0000000..a0b878d --- /dev/null +++ b/driver-pwa/frontend/src/hooks/usePairPoll.test.tsx @@ -0,0 +1,29 @@ +// src/hooks/usePairPoll.test.tsx +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { usePairPoll } from "./usePairPoll"; +import * as driver from "@/api/driver"; +import { useAuth } from "@/store/auth"; + +beforeEach(() => { useAuth.getState().clear(); vi.restoreAllMocks(); }); + +describe("usePairPoll", () => { + it("sets session on authenticated", async () => { + vi.spyOn(driver, "tgPoll").mockResolvedValue({ status: "authenticated", token: "T", driver_id: 7 } as any); + const { result } = renderHook(() => usePairPoll("SESS")); + await waitFor(() => expect(result.current.status).toBe("authenticated")); + expect(useAuth.getState().token).toBe("T"); + expect(useAuth.getState().driverId).toBe(7); + }); + it("stays pending then expired", async () => { + vi.spyOn(driver, "tgPoll").mockResolvedValue({ status: "expired" } as any); + const { result } = renderHook(() => usePairPoll("SESS")); + await waitFor(() => expect(result.current.status).toBe("expired")); + }); + it("does nothing when session is null", () => { + const spy = vi.spyOn(driver, "tgPoll"); + const { result } = renderHook(() => usePairPoll(null)); + expect(result.current.status).toBe("pending"); + expect(spy).not.toHaveBeenCalled(); + }); +}); diff --git a/driver-pwa/frontend/src/hooks/usePairPoll.ts b/driver-pwa/frontend/src/hooks/usePairPoll.ts new file mode 100644 index 0000000..b56107a --- /dev/null +++ b/driver-pwa/frontend/src/hooks/usePairPoll.ts @@ -0,0 +1,43 @@ +// src/hooks/usePairPoll.ts +import { useEffect, useRef, useState } from "react"; +import { tgPoll } from "@/api/driver"; +import { useAuth } from "@/store/auth"; + +type Status = "pending" | "authenticated" | "expired"; +const INTERVAL_MS = 2000; +const TIMEOUT_MS = 5 * 60 * 1000; + +export function usePairPoll(pairSession: string | null): { status: Status } { + const [status, setStatus] = useState("pending"); + const setSession = useAuth((s) => s.setSession); + const startedAt = useRef(0); + + useEffect(() => { + if (!pairSession) return; + let alive = true; + let timer: ReturnType; + startedAt.current = Date.now(); + + const tick = async () => { + if (!alive) return; + try { + const r = await tgPoll(pairSession); + if (!alive) return; + if (r.status === "authenticated") { + setSession(r.token, r.driver_id); + setStatus("authenticated"); + return; + } + if (r.status === "expired") { setStatus("expired"); return; } + } catch { + // transient — keep polling until timeout + } + if (Date.now() - startedAt.current > TIMEOUT_MS) { setStatus("expired"); return; } + timer = setTimeout(tick, INTERVAL_MS); + }; + tick(); + return () => { alive = false; clearTimeout(timer); }; + }, [pairSession, setSession]); + + return { status }; +}