import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { invoke } from "@tauri-apps/api/core"; import { useTranslation } from "../i18n"; interface DeviceAuthorization { user_code: string; device_code: string; verification_uri: string | null; verification_uri_complete: string | null; expires_in: number | null; interval: number | null; } type PollStatus = | { status: "pending"; interval: number } | { status: "slow_down"; interval: number } | { status: "success" } | { status: "expired" } | { status: "access_denied" } | { status: "timeout" }; interface KimiOAuthDialogProps { open: boolean; onClose: () => void; } /** Kimi device-code sign-in dialog (in-app `kimi login`). */ export function KimiOAuthDialog({ open, onClose }: KimiOAuthDialogProps) { const { t } = useTranslation(); const [auth, setAuth] = useState(null); const [polling, setPolling] = useState(false); const [done, setDone] = useState(false); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); const activeRef = useRef(false); const intervalRef = useRef(5); // Start a fresh device authorization when the dialog opens. useEffect(() => { if (!open) return; activeRef.current = true; setAuth(null); setPolling(false); setDone(false); setError(null); setCopied(false); intervalRef.current = 5; invoke("kimi_oauth_start") .then((a) => { if (!activeRef.current) return; setAuth(a); if (a.interval && a.interval > 0) intervalRef.current = a.interval; setPolling(true); }) .catch((e) => { if (!activeRef.current) return; setError(e instanceof Error ? e.message : String(e)); }); return () => { activeRef.current = false; }; }, [open]); // Poll the token endpoint while the user authorizes in the browser. useEffect(() => { if (!open || !polling || !auth) return; let cancelled = false; const tick = async () => { if (cancelled || !activeRef.current) return; try { const res = await invoke("kimi_oauth_poll", { deviceCode: auth.device_code, interval: intervalRef.current, }); if (cancelled) return; switch (res.status) { case "pending": case "slow_down": if (res.interval > 0) intervalRef.current = res.interval; break; case "success": setPolling(false); setDone(true); setTimeout(() => { if (activeRef.current) onClose(); }, 1600); return; case "expired": case "access_denied": case "timeout": setPolling(false); setError( res.status === "expired" ? t("kimiOAuthExpired") : res.status === "access_denied" ? t("kimiOAuthDenied") : t("kimiOAuthTimeout") ); return; } } catch { if (!cancelled) setError(t("kimiOAuthNetworkError")); } // Schedule the next poll (also on network errors — keep retrying). setTimeout(() => { if (!cancelled && activeRef.current && polling) void tick(); }, intervalRef.current * 1000); }; void tick(); return () => { cancelled = true; }; // `polling` and `auth` gate the loop; restart polling explicitly via // startPolling when retrying. // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, polling, auth]); const restart = () => { setAuth(null); setPolling(false); setDone(false); setError(null); intervalRef.current = 5; invoke("kimi_oauth_start") .then((a) => { if (!activeRef.current) return; setAuth(a); if (a.interval && a.interval > 0) intervalRef.current = a.interval; setPolling(true); }) .catch((e) => { if (!activeRef.current) return; setError(e instanceof Error ? e.message : String(e)); }); }; const copyCode = () => { if (!auth) return; navigator.clipboard.writeText(auth.user_code).catch(() => {}); setCopied(true); setTimeout(() => setCopied(false), 1500); }; // Open the authorization page via the Rust-side opener command (bypasses the // JS plugin scope, same reliable path the provider website links use); // fall back to a new tab when even that fails. const openAuthPage = () => { if (!auth) return; const url = auth.verification_uri_complete || auth.verification_uri; if (!url) return; invoke("open_external_url", { url }).catch(() => { const w = window.open(url, "_blank"); if (!w) console.error(`failed to open ${url}`); }); }; if (!open) return null; return createPortal(

{t("kimiOAuthLogin")}

{done && (
✓ {t("kimiOAuthSuccess")}
)} {error && !done && (
{error}
)} {auth && !done && ( <> {/* user code + copy */}
{auth.user_code}

{t("kimiOAuthWaiting")}

)} {!auth && !done && (
)}
{error && !done && ( )}
, document.body ); }