fix connect flow in tray

This commit is contained in:
Eduard Gert
2026-05-22 10:16:13 +02:00
parent 580cfa0dc5
commit 577ce6deb5
5 changed files with 22 additions and 16 deletions

View File

@@ -51,7 +51,7 @@ All services live in `services/` and assume a build tag `!android && !ios && !fr
`main.go` registers five typed events for the frontend: `netbird:status` (`Status`), `netbird:event` (`SystemEvent`), `netbird:profile:changed` (`ProfileRef`), `netbird:update:available` (`UpdateAvailable`), `netbird:update:progress` (`UpdateProgress`). `netbird:profile:changed` fires from `ProfileSwitcher.SwitchActive` after a successful daemon-side switch — both the React `ProfileContext` and the tray subscribe so a flip driven from one surface paints in the others (the daemon itself does not emit a profile event). Plus three plain-string events:
- `EventTriggerLogin = "trigger-login"` — tray asking the frontend's `startLogin()` to begin an SSO flow.
- `EventTriggerLogin = "trigger-login"` — tray asking the frontend's `startLogin()` to begin an SSO flow. The tray does **not** show the main window when emitting — the hidden webview is alive and subscribed, so `startLogin` runs and the only visible surface is the BrowserLogin popup it opens.
- `EventBrowserLoginCancel = "browser-login:cancel"` — the `BrowserLogin` window's Cancel button or red-X close. `startLogin()` listens and tears down the daemon's pending `WaitSSOLogin`.
- `preferences.EventPreferencesChanged = "netbird:preferences:changed"` — emitted after every successful `SetLanguage` (payload `{language}`). Both the tray menu rebuild and the React `i18next.changeLanguage` subscribe so a flip from any window paints everywhere.

View File

@@ -132,8 +132,8 @@ The SSO flow is centralised in a module-level `startLogin()` with a `loginInFlig
1. `Connection.Login({})` with empty fields — Go fills in active profile + OS user.
2. If the daemon needs SSO (`needsSsoLogin`):
- `Connection.OpenURL(uri)` opens the verification page in the system browser (honors `$BROWSER`).
- `WindowManager.OpenBrowserLogin(uri)` opens the auxiliary "waiting for sign-in" window.
- `WindowManager.OpenBrowserLogin(uri)` opens the auxiliary "waiting for sign-in" window (Hidden until React mounts and `useAutoSizeWindow` calls `Window.Show`).
- `WaitingForBrowserDialog` mounts, gets shown by `useAutoSizeWindow`, then fires `Connection.OpenURL(uri)` from its mount effect — opens the verification page in the system browser (honors `$BROWSER`). Done from the dialog (not `startLogin`) so the browser doesn't race the still-hidden NetBird popup and land on top.
- `Promise.race(WaitSSOLogin, EVENT_BROWSER_LOGIN_CANCEL)` — whichever resolves first.
- On cancel: `Connection.Down()` to dislodge the daemon's pending `WaitSSOLogin` so the next Login starts fresh (see `services/connection.go:74`).
3. `Connection.Up({})` to bring the new session up.

View File

@@ -62,16 +62,15 @@ async function startLogin(): Promise<void> {
if (result.needsSsoLogin) {
const uri = result.verificationUriComplete || result.verificationUri;
if (uri) {
// Open the in-app sign-in popup first so it's already on
// screen when the system browser steals focus; otherwise
// the browser lands on top and the user has to dig the
// NetBird window back out.
// Open the in-app sign-in popup first; the dialog itself
// fires Connection.OpenURL after it's actually on screen
// (see WaitingForBrowserDialog) so the system browser
// doesn't land on top of a still-hidden NetBird window.
try {
await WindowManager.OpenBrowserLogin(uri);
} catch (e) {
console.error(e);
}
Connection.OpenURL(uri).catch(console.error);
}
const cancelPromise = new Promise<void>((resolve) => {

View File

@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { Events } from "@wailsio/runtime";
@@ -21,6 +21,15 @@ export default function WaitingForBrowserDialog() {
const uri = params.get("uri") ?? "";
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH);
// Open the system browser only after the dialog has mounted (which
// means useAutoSizeWindow has called Window.Show). startLogin used to
// fire OpenURL itself but the browser typically beat React's mount
// and landed on top of the still-hidden NetBird popup.
useEffect(() => {
if (!uri) return;
Connection.OpenURL(uri).catch(console.error);
}, [uri]);
const tryAgain = useCallback(() => {
if (!uri) return;
Connection.OpenURL(uri).catch(console.error);

View File

@@ -426,18 +426,17 @@ func (t *Tray) openRoute(route string) {
func (t *Tray) handleConnect() {
// NeedsLogin/SessionExpired/LoginFailed mean the daemon won't honor a
// plain Up RPC ("up already in progress: current status NeedsLogin") —
// it needs the Login → WaitSSOLogin → Up sequence instead. Hand off
// to the React-side startLogin() (which owns the browser-login window
// and SSO orchestration) by showing the main window and emitting
// EventTriggerLogin. The frontend subscribes in
// layouts/ConnectionStatusSwitch.tsx.
// it needs the Login → WaitSSOLogin → Up sequence instead. Emit
// EventTriggerLogin so the React-side startLogin() (which owns the
// BrowserLogin popup) drives the flow. The main window's webview is
// alive even while hidden, so we don't surface it — only the popup
// appears.
t.mu.Lock()
needsLogin := strings.EqualFold(t.lastStatus, services.StatusNeedsLogin) ||
strings.EqualFold(t.lastStatus, services.StatusSessionExpired) ||
strings.EqualFold(t.lastStatus, services.StatusLoginFailed)
t.mu.Unlock()
if needsLogin {
t.ShowWindow()
t.app.Event.Emit(services.EventTriggerLogin)
return
}
@@ -625,7 +624,6 @@ func (t *Tray) applyStatus(st services.Status) {
t.mu.Unlock()
if triggerLogin {
t.ShowWindow()
t.app.Event.Emit(services.EventTriggerLogin)
}