diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index 92783ee2be..cacf318deb 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -9,12 +9,13 @@ import { shouldUseCustomWindowTitlebar, } from "@/components/tauri/window-titlebar"; import { Toaster } from "@/components/ui/sonner"; +import { getTauriAuthFailure, tauriAutoAuth } from "@/features/auth"; import { useTauriBackend } from "@/hooks/use-tauri-backend"; import { useTauriUpdate } from "@/hooks/use-tauri-update"; import { isTauri } from "@/lib/api-base"; import { useRouterState } from "@tanstack/react-router"; import { ThemeProvider } from "next-themes"; -import { useEffect, useRef, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; interface AppProviderProps { children: ReactNode; @@ -141,6 +142,8 @@ function TauriWrapper({ children }: { children: ReactNode }) { const hasResized = useRef(false); const abortRef = useRef(false); + const [desktopAuthReady, setDesktopAuthReady] = useState(!isTauri); + const [desktopAuthRetry, setDesktopAuthRetry] = useState(0); // Show the window once the frontend mounts (for pre-running states) useEffect(() => { @@ -163,20 +166,56 @@ function TauriWrapper({ children }: { children: ReactNode }) { return () => { abortRef.current = true; }; }, [status]); + useEffect(() => { + if (!isTauri) { + setDesktopAuthReady(true); + return; + } + if (status !== "running") { + setDesktopAuthReady(false); + setDesktopAuthRetry(0); + return; + } + + let disposed = false; + setDesktopAuthReady(false); + tauriAutoAuth({ force: true }).then((authenticated) => { + if (disposed) return; + if (authenticated) { + setDesktopAuthReady(true); + return; + } + if (!getTauriAuthFailure()) { + window.setTimeout(() => { + if (!disposed) setDesktopAuthRetry((value) => value + 1); + }, 500); + } + }); + + return () => { disposed = true; }; + }, [status, desktopAuthRetry]); + if (!isTauri) return <>{children}; - const content = status === "running" ? ( + const showApp = status === "running" && desktopAuthReady; + const startupStatus = status === "running" ? "starting" : status; + const startupProgressDetail = + status === "running" && !desktopAuthReady + ? "Signing in to desktop session..." + : progressDetail; + + const content = showApp ? ( <> {children} ) : ( diff --git a/studio/frontend/src/features/auth/tauri-auto-auth.ts b/studio/frontend/src/features/auth/tauri-auto-auth.ts index d67730d2d6..a0199f9ac3 100644 --- a/studio/frontend/src/features/auth/tauri-auto-auth.ts +++ b/studio/frontend/src/features/auth/tauri-auto-auth.ts @@ -15,9 +15,13 @@ type DesktopAuthResponse = { refresh_token: string; }; +type TauriAutoAuthOptions = { + force?: boolean; +}; + // Concurrency guard: multiple route guards can call tauriAutoAuth simultaneously. // Without this, the first-launch password-change could race with itself. -let pending: Promise | null = null; +let pending: { promise: Promise; force: boolean } | null = null; let lastTauriAuthFailure: string | null = null; const TAURI_AUTH_FAILURE_FALLBACK = @@ -49,15 +53,15 @@ function isBackendNotReady(error: unknown): boolean { return authFailureMessage(error).includes(BACKEND_NOT_READY_MESSAGE); } -async function doTauriAutoAuth(): Promise { +async function doTauriAutoAuth(options: TauriAutoAuthOptions): Promise { // Desktop must handle password-change state internally in Rust. - if (hasAuthToken() && !mustChangePassword()) { + if (!options.force && hasAuthToken() && !mustChangePassword()) { clearTauriAuthFailure(); return true; } // Try refreshing existing session - if (hasRefreshToken()) { + if (!options.force && hasRefreshToken()) { const refreshed = await refreshSession(); if (refreshed && hasAuthToken() && !mustChangePassword()) { clearTauriAuthFailure(); @@ -86,10 +90,17 @@ async function doTauriAutoAuth(): Promise { * Returns true if authentication succeeded. * Concurrent calls are coalesced into a single in-flight attempt. */ -export function tauriAutoAuth(): Promise { +export function tauriAutoAuth( + options: TauriAutoAuthOptions = {}, +): Promise { if (!isTauri) return Promise.resolve(false); - if (!pending) { - pending = doTauriAutoAuth().finally(() => { pending = null; }); + const force = options.force === true; + if (!pending || (force && !pending.force)) { + let promise: Promise; + promise = doTauriAutoAuth({ force }).finally(() => { + if (pending?.promise === promise) pending = null; + }); + pending = { promise, force }; } - return pending; + return pending.promise; }