From 954c90f9e5695d9a353a59955a7c6f633c0dc6a5 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 3 Sep 2026 13:23:14 +0200 Subject: [PATCH] [client] Show the SSO login popup and open the browser from Go The browser-login popup was created hidden and relied on its own webview to size and show itself and to launch the external browser. On macOS a hidden WKWebView gets throttled or suspended (App Nap / hidden-window throttling), so on the first-use path nothing appeared and the browser never opened, leaving the session-expiration dialog disabled until the PKCE flow timed out. Reproduced by freezing the popup's WebContent process: the old code showed nothing, the new code shows the popup and opens the browser within 30 ms regardless of the webview state. Show and focus the popup from Go right after creation and launch the browser from Go on both the create and reuse paths. The popup's frontend no longer shows or focuses itself, so the browser keeps the foreground once it activates. This also fixes the reuse path, where a fragment-only SetURL kept the mounted React tree and the once-only guard skipped opening the browser for the new URI. Browser launch failures surface in the error dialog instead of being swallowed. --- .../frontend/src/hooks/useAutoSizeWindow.ts | 10 +++++-- .../login/LoginWaitingForBrowserDialog.tsx | 12 ++------ client/ui/services/connection.go | 30 +++++++++++-------- client/ui/services/windowmanager.go | 20 ++++++++++++- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts index d4f4d80b2..693f5725f 100644 --- a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts +++ b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts @@ -5,7 +5,11 @@ import { isLinux } from "@/lib/platform"; // Sizes the current Wails window to the measured content height (keeping `width`), // then shows it. Re-applies on content resize and language change. -export function useAutoSizeWindow(width: number, ready: boolean = true) { +export function useAutoSizeWindow( + width: number, + ready: boolean = true, + showWhenSized: boolean = true, +) { const ref = useRef(null); useLayoutEffect(() => { const el = ref.current; @@ -33,7 +37,7 @@ export function useAutoSizeWindow(width: number, ready: b await Window.SetMaxSize(width, targetH); } await Window.SetSize(width, targetH); - showOnce(); + if (showWhenSized) showOnce(); } catch { // window gone / not ready — ignore } @@ -55,6 +59,6 @@ export function useAutoSizeWindow(width: number, ready: b cancelAnimationFrame(raf2); i18next.off("languageChanged", scheduleApply); }; - }, [width, ready]); + }, [width, ready, showWhenSized]); return ref; } diff --git a/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx b/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx index efbd1ee84..d1a5da1d0 100644 --- a/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx +++ b/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { Events } from "@wailsio/runtime"; @@ -20,8 +20,7 @@ export default function LoginWaitingForBrowserDialog() { const { t } = useTranslation(); const [params] = useSearchParams(); const uri = params.get("uri") ?? ""; - const contentRef = useAutoSizeWindow(WINDOW_WIDTH); - const openedRef = useRef(false); + const contentRef = useAutoSizeWindow(WINDOW_WIDTH, true, false); const reportOpenFailure = useCallback( (e: unknown) => { @@ -33,13 +32,6 @@ export default function LoginWaitingForBrowserDialog() { [t], ); - // Open the browser only after mount, or it lands on top of the still-hidden popup. - useEffect(() => { - if (!uri || openedRef.current) return; - openedRef.current = true; - Connection.OpenURL(uri).catch(reportOpenFailure); - }, [uri, reportOpenFailure]); - const tryAgain = useCallback(() => { if (!uri) return; Connection.OpenURL(uri).catch(reportOpenFailure); diff --git a/client/ui/services/connection.go b/client/ui/services/connection.go index f78ce4c0f..f6a8eca72 100644 --- a/client/ui/services/connection.go +++ b/client/ui/services/connection.go @@ -205,19 +205,7 @@ func (s *Connection) Down(ctx context.Context) error { // window.open, so the SSO verification page can't pop inline. Honors $BROWSER // before the platform default. func (s *Connection) OpenURL(url string) error { - if browser := os.Getenv("BROWSER"); browser != "" { - return exec.Command(browser, url).Start() - } - switch runtime.GOOS { - case "windows": - return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - return exec.Command("open", url).Start() - case "linux": - return exec.Command("xdg-open", url).Start() - default: - return fmt.Errorf("unsupported platform") - } + return openURL(url) } func (s *Connection) Logout(ctx context.Context, p LogoutParams) error { @@ -288,3 +276,19 @@ func (s *Connection) waitSSOLogin(ctx context.Context, p WaitSSOParams) (string, func (s *Connection) classifyDaemonError(err error) *ClientError { return s.classifier.classify(err) } + +func openURL(url string) error { + if browser := os.Getenv("BROWSER"); browser != "" { + return exec.Command(browser, url).Start() + } + switch runtime.GOOS { + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + return exec.Command("open", url).Start() + case "linux": + return exec.Command("xdg-open", url).Start() + default: + return fmt.Errorf("unsupported platform") + } +} diff --git a/client/ui/services/windowmanager.go b/client/ui/services/windowmanager.go index 94dba6038..1a707df17 100644 --- a/client/ui/services/windowmanager.go +++ b/client/ui/services/windowmanager.go @@ -245,7 +245,11 @@ func (s *WindowManager) OpenBrowserLogin(uri string) { s.app.Event.Emit(EventBrowserLoginCancel) } }) - s.centerOnCursorScreen(s.browserLogin) + s.centerOnCursorScreen(bl) + bl.Show() + bl.Focus() + log.Debugf("browser-login popup created and shown") + s.openBrowser(uri) return } if uri != "" { @@ -254,6 +258,20 @@ func (s *WindowManager) OpenBrowserLogin(uri string) { s.centerOnCursorScreen(s.browserLogin) s.browserLogin.Show() s.browserLogin.Focus() + log.Debugf("browser-login popup reused") + s.openBrowser(uri) +} + +func (s *WindowManager) openBrowser(uri string) { + if uri == "" { + return + } + go func() { + if err := openURL(uri); err != nil { + log.Errorf("open browser for SSO login: %v", err) + s.OpenError(s.title("browserLogin.openFailedTitle"), err.Error(), "") + } + }() } // BrowserLoginWindow returns the live SSO popup, or nil. While non-nil it is the