From 20fcb84fd40a7bae7d1e82ecb7b9bab7e42e5e08 Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Fri, 24 Apr 2026 11:35:09 -0700 Subject: [PATCH] fix React cloud sign-in and workspace reload state --- apps/app/src/app/lib/desktop.ts | 18 ++++- .../domains/cloud/den-auth-provider.tsx | 67 ++++++++++++++++++- .../react-app/shell/reload-coordinator.tsx | 2 +- .../app/src/react-app/shell/session-route.tsx | 33 +++++++-- .../src/react-app/shell/settings-route.tsx | 6 +- apps/desktop/electron/main.mjs | 16 +++++ 6 files changed, 131 insertions(+), 11 deletions(-) diff --git a/apps/app/src/app/lib/desktop.ts b/apps/app/src/app/lib/desktop.ts index 8824b34a..1ace9431 100644 --- a/apps/app/src/app/lib/desktop.ts +++ b/apps/app/src/app/lib/desktop.ts @@ -88,7 +88,23 @@ export const desktopBridge: DesktopBridge = new Proxy({} as DesktopBridge, { export const desktopFetch: typeof globalThis.fetch = (input, init) => { if (isElectronDesktopRuntime()) { - return globalThis.fetch(input, init); + return invokeElectronHelper<{ + status: number; + statusText: string; + headers: [string, string][]; + body: string; + }>("__fetch", typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url, { + method: init?.method, + headers: init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : undefined, + body: typeof init?.body === "string" ? init.body : undefined, + }).then( + (result) => + new Response(result.body, { + status: result.status, + statusText: result.statusText, + headers: result.headers, + }), + ); } return tauriBridge.desktopFetch(input, init); }; diff --git a/apps/app/src/react-app/domains/cloud/den-auth-provider.tsx b/apps/app/src/react-app/domains/cloud/den-auth-provider.tsx index 41c94ac2..ec8ab98c 100644 --- a/apps/app/src/react-app/domains/cloud/den-auth-provider.tsx +++ b/apps/app/src/react-app/domains/cloud/den-auth-provider.tsx @@ -16,9 +16,19 @@ import { DenApiError, ensureDenActiveOrganization, readDenSettings, + writeDenSettings, type DenUser, } from "../../../app/lib/den"; -import { denSessionUpdatedEvent } from "../../../app/lib/den-session-events"; +import { + denSessionUpdatedEvent, + dispatchDenSessionUpdated, +} from "../../../app/lib/den-session-events"; +import { + deepLinkBridgeEvent, + drainPendingDeepLinks, + type DeepLinkBridgeDetail, +} from "../../../app/lib/deep-link-bridge"; +import { parseDenAuthDeepLink } from "../../../app/lib/openwork-links"; export type DenAuthStatus = "checking" | "signed_in" | "signed_out"; @@ -48,6 +58,7 @@ export function DenAuthProvider({ children }: DenAuthProviderProps) { const [error, setError] = useState(null); // Monotonic token so stale async refreshes can't clobber a newer result. const refreshTokenRef = useRef(0); + const handledGrantsRef = useRef>(new Set()); const refresh = useCallback(async () => { const currentRun = ++refreshTokenRef.current; @@ -114,6 +125,60 @@ export function DenAuthProvider({ children }: DenAuthProviderProps) { }; }, [refresh]); + useEffect(() => { + if (typeof window === "undefined") return; + + const handleUrls = (urls: readonly string[]) => { + for (const rawUrl of urls) { + const parsed = parseDenAuthDeepLink(rawUrl); + if (!parsed || handledGrantsRef.current.has(parsed.grant)) continue; + handledGrantsRef.current.add(parsed.grant); + + void createDenClient({ baseUrl: parsed.denBaseUrl }) + .exchangeDesktopHandoff(parsed.grant) + .then((result) => { + if (!result.token) { + throw new Error("Failed to sign in to OpenWork Cloud."); + } + + writeDenSettings({ + baseUrl: parsed.denBaseUrl, + authToken: result.token, + activeOrgId: null, + activeOrgSlug: null, + activeOrgName: null, + }); + + dispatchDenSessionUpdated({ + status: "success", + baseUrl: parsed.denBaseUrl, + token: result.token, + user: result.user, + email: result.user?.email ?? null, + }); + }) + .catch((error) => { + handledGrantsRef.current.delete(parsed.grant); + dispatchDenSessionUpdated({ + status: "error", + message: + error instanceof Error + ? error.message + : "Failed to sign in to OpenWork Cloud.", + }); + }); + } + }; + + handleUrls(drainPendingDeepLinks(window)); + const handleDeepLink = (event: Event) => { + handleUrls(((event as CustomEvent).detail?.urls ?? []) as string[]); + }; + + window.addEventListener(deepLinkBridgeEvent, handleDeepLink); + return () => window.removeEventListener(deepLinkBridgeEvent, handleDeepLink); + }, []); + const value = useMemo( () => ({ status, diff --git a/apps/app/src/react-app/shell/reload-coordinator.tsx b/apps/app/src/react-app/shell/reload-coordinator.tsx index 2a5e38d5..576a5cac 100644 --- a/apps/app/src/react-app/shell/reload-coordinator.tsx +++ b/apps/app/src/react-app/shell/reload-coordinator.tsx @@ -115,7 +115,7 @@ export function ReloadCoordinatorProvider({ children }: { children: ReactNode })
((resolve, reject) => { const reader = new FileReader(); @@ -372,6 +380,21 @@ export function SessionRoute() { workspaceLabel, }); + const activeReloadBlockingSessions = useMemo( + () => + Object.values(sessionsByWorkspaceId) + .flat() + .filter((session) => isActiveSessionStatus(getSessionStatus(session))) + .map((session: any) => ({ + id: String(session?.id ?? ""), + title: + String(session?.title ?? session?.slug ?? session?.id ?? "").trim() || + t("session.untitled"), + })) + .filter((session) => session.id.length > 0), + [sessionsByWorkspaceId], + ); + const backgroundSessionLoadInFlight = useRef>(new Set()); const loadWorkspaceSessionsInBackground = useCallback( async (openworkClient: OpenworkServerClient, workspaces: RouteWorkspace[]) => { @@ -575,9 +598,9 @@ export function SessionRoute() { return reloadCoordinator.registerWorkspaceReloadControls({ canReloadWorkspaceEngine: () => Boolean(client && selectedWorkspaceId), reloadWorkspaceEngine: reloadWorkspaceEngineFromUi, - activeSessions: () => [], + activeSessions: () => activeReloadBlockingSessions, }); - }, [client, reloadCoordinator, reloadWorkspaceEngineFromUi, selectedWorkspaceId]); + }, [activeReloadBlockingSessions, client, reloadCoordinator, reloadWorkspaceEngineFromUi, selectedWorkspaceId]); useEffect(() => { if (!client || !selectedWorkspaceId) return; diff --git a/apps/app/src/react-app/shell/settings-route.tsx b/apps/app/src/react-app/shell/settings-route.tsx index c3b1ee72..dde96928 100644 --- a/apps/app/src/react-app/shell/settings-route.tsx +++ b/apps/app/src/react-app/shell/settings-route.tsx @@ -109,9 +109,9 @@ function mergeRouteWorkspaces( const merged = match ? { ...workspace, - displayName: match.displayName?.trim() - ? match.displayName - : workspace.displayName, + displayName: workspace.displayName?.trim() + ? workspace.displayName + : match.displayName, name: match.name?.trim() ? match.name : workspace.name, } : workspace; diff --git a/apps/desktop/electron/main.mjs b/apps/desktop/electron/main.mjs index 1b405ba0..1951c126 100644 --- a/apps/desktop/electron/main.mjs +++ b/apps/desktop/electron/main.mjs @@ -1140,6 +1140,22 @@ async function handleDesktopInvoke(event, command, ...args) { shell.showItemInFolder(target); return undefined; } + case "__fetch": { + const url = String(args[0] ?? "").trim(); + const init = args[1] ?? {}; + if (!url) throw new Error("URL is required."); + const response = await fetch(url, { + method: typeof init.method === "string" ? init.method : undefined, + headers: init.headers && typeof init.headers === "object" ? init.headers : undefined, + body: typeof init.body === "string" ? init.body : undefined, + }); + return { + status: response.status, + statusText: response.statusText, + headers: Array.from(response.headers.entries()), + body: await response.text(), + }; + } case "__homeDir": return os.homedir(); case "__joinPath":