'use client'; import * as React from 'react'; import { Loader2, QrCode } from 'lucide-react'; import { QRCodeSVG } from 'qrcode.react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { approveQrLoginSession, createDeviceLinkQrSession, getApiErrorMessage, pollQrLoginSession, type QrLoginSession } from '@/lib/api'; interface DeviceLinkQrDialogProps { open: boolean; userId: string; token: string; onOpenChange: (open: boolean) => void; onLinked?: () => void; } function formatCountdown(expiresAt: string) { const secondsLeft = Math.max(0, Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 1000)); const minutes = Math.floor(secondsLeft / 60); const seconds = secondsLeft % 60; return `${minutes}:${seconds.toString().padStart(2, '0')}`; } export function DeviceLinkQrDialog({ open, userId, token, onOpenChange, onLinked }: DeviceLinkQrDialogProps) { const [session, setSession] = React.useState(null); const [loading, setLoading] = React.useState(false); const [approving, setApproving] = React.useState(false); const [error, setError] = React.useState(null); const [successMessage, setSuccessMessage] = React.useState(null); const [countdown, setCountdown] = React.useState('5:00'); const approveStartedRef = React.useRef(false); const refreshInFlightRef = React.useRef(false); const refreshSession = React.useCallback(async () => { if (!userId || !token || refreshInFlightRef.current) return; refreshInFlightRef.current = true; setLoading(true); setError(null); setSuccessMessage(null); approveStartedRef.current = false; try { const created = await createDeviceLinkQrSession(userId, token); setSession(created); } catch (err) { setError(getApiErrorMessage(err, 'Не удалось создать QR-код') ?? 'Не удалось создать QR-код'); } finally { setLoading(false); refreshInFlightRef.current = false; } }, [token, userId]); React.useEffect(() => { if (!open) { setSession(null); setError(null); setSuccessMessage(null); approveStartedRef.current = false; return; } void refreshSession(); }, [open, refreshSession]); React.useEffect(() => { if (!open || !session?.expiresAt) return; const update = () => setCountdown(formatCountdown(session.expiresAt)); update(); const timer = window.setInterval(update, 1000); return () => window.clearInterval(timer); }, [open, session?.expiresAt]); React.useEffect(() => { if (!open || !session?.sessionId || !token || successMessage) return; let cancelled = false; const poll = async () => { try { const current = await pollQrLoginSession(session.sessionId); if (cancelled) return; if (current.status === 'EXPIRED') { if (!refreshInFlightRef.current) { await refreshSession(); } return; } if (current.claimed && !approveStartedRef.current) { approveStartedRef.current = true; setApproving(true); setError(null); try { await approveQrLoginSession(session.sessionId, token); if (cancelled) return; const label = current.claimedDeviceName?.trim(); setSuccessMessage(label ? `Устройство «${label}» подключено` : 'Новое устройство подключено'); onLinked?.(); } catch (err) { if (!cancelled) { approveStartedRef.current = false; setError(getApiErrorMessage(err, 'Не удалось подтвердить подключение') ?? 'Не удалось подтвердить подключение'); } } finally { if (!cancelled) setApproving(false); } } } catch (err) { if (!cancelled) { setError(getApiErrorMessage(err, 'Не удалось проверить статус QR-кода') ?? 'Не удалось проверить статус QR-кода'); } } }; void poll(); const timer = window.setInterval(() => void poll(), 2000); return () => { cancelled = true; window.clearInterval(timer); }; }, [onLinked, open, refreshSession, session?.sessionId, successMessage, token]); return ( Подключить новое устройство {loading && !session ? (

Генерируем QR-код...

) : null} {session?.qrPayload ? (

Отсканируйте QR-код камерой на новом устройстве. Откроется страница входа, и устройство подключится к вашему аккаунту.

Код действует {countdown} и обновится автоматически

{approving ? (

Подключаем устройство...

) : null} {successMessage ?

{successMessage}

: null} {error ?

{error}

: null}
) : null}
); }