diff --git a/desktop/src/ipc.ts b/desktop/src/ipc.ts index aecf639..c8147a5 100644 --- a/desktop/src/ipc.ts +++ b/desktop/src/ipc.ts @@ -23,6 +23,7 @@ export function registerIpc(opts: { session: () => SessionInfo; signOut: () => Promise; accountUrl: () => string; + shellAction: (action: string) => void; }): void { const navigate = opts.navigate; ipcMain.on("capscreen:bootstrap", (e) => { @@ -42,6 +43,12 @@ export function registerIpc(opts: { void opts.signOut(); }); + // Only the shell's data: pages (gate, boot error) may trigger these. + ipcMain.on("capscreen:shell-action", (e, action: unknown) => { + if (typeof action !== "string" || !e.senderFrame?.url.startsWith("data:")) return; + opts.shellAction(action); + }); + ipcMain.handle("capscreen:open-account", async () => { const url = opts.accountUrl(); if (url) await shell.openExternal(url); diff --git a/desktop/src/main.ts b/desktop/src/main.ts index b518af4..322bc40 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -101,20 +101,6 @@ function createWindow(): void { return { action: "deny" }; }); mainWindow.webContents.on("will-navigate", (e, url) => { - // Buttons on the sign-in gate. - const gateAction = /^about:blank#(signin|signup|reopen|cancel)$/.exec(url)?.[1]; - if (gateAction) { - e.preventDefault(); - void handleGateAction(gateAction); - return; - } - // Buttons on the boot error page. - if (url === "about:blank#retry" || url === "about:blank#logs") { - e.preventDefault(); - if (url.endsWith("#logs")) void shell.openPath(userData("logs")); - else void mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(splashHtml())}`).then(start); - return; - } const allowed = url.startsWith(APP_ORIGIN) || (runtime && url.startsWith(runtime.frontendUrl)) || url.startsWith("data:"); if (!allowed) { e.preventDefault(); @@ -267,6 +253,24 @@ function requireSignIn(): Promise { return new Promise((resolve) => (gateResolve = resolve)); } +/** Buttons on the shell pages (splash.ts), sent over IPC. */ +function handleShellAction(action: string): void { + switch (action) { + case "signin": + case "signup": + case "reopen": + case "cancel": + void handleGateAction(action); + break; + case "logs": + void shell.openPath(userData("logs")); + break; + case "retry": + void showPage(splashHtml()).then(start); + break; + } +} + async function handleGateAction(action: string): Promise { if (action === "reopen" && pendingSignIn) { await shell.openExternal(pendingSignIn.handle.authorizeUrl); @@ -364,6 +368,7 @@ void app.whenReady().then(() => { }, signOut: signOutAndGate, accountUrl: () => AUTH_URL, + shellAction: handleShellAction, }); buildMenu(); buildTray(); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index f7b40ee..9aae4df 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -24,6 +24,8 @@ contextBridge.exposeInMainWorld("capscreenDesktop", { printToPdf: (opts: unknown) => ipcRenderer.invoke("capscreen:print-pdf", opts), signOut: () => ipcRenderer.invoke("capscreen:sign-out"), openAccount: () => ipcRenderer.invoke("capscreen:open-account"), + // Buttons on the shell's own pages (sign-in gate, boot error); ignored from the app UI. + shellAction: (action: string) => ipcRenderer.send("capscreen:shell-action", action), onNavigate: (cb: (route: string) => void) => { const listener = (_e: unknown, route: string) => cb(route); ipcRenderer.on("capscreen:navigate", listener); diff --git a/desktop/src/splash.ts b/desktop/src/splash.ts index a9d554c..e889634 100644 --- a/desktop/src/splash.ts +++ b/desktop/src/splash.ts @@ -36,6 +36,20 @@ const BASE_CSS = ` a.btn.primary { background: var(--fg); color: var(--bg); border-color: var(--fg); } `; +/** + * Buttons carry data-action; clicks go to main.ts over the preload bridge. + * (Navigating to about:blank#… doesn't work: Chromium commits it without + * emitting will-navigate, so the window just went blank.) + */ +const ACTIONS_SCRIPT = ``; + export function splashHtml(): string { return `CapScreen
${markSvg()}

CapScreen

@@ -53,15 +67,15 @@ export function errorHtml(message: string, logsDir: string): string {

One of the local services failed to launch.

${escapeHtml(message)}

Logs: ${escapeHtml(logsDir)}

- -
`; + + ${ACTIONS_SCRIPT}`; } export type SignInView = { state: "idle" } | { state: "waiting" } | { state: "error"; message: string }; /** - * Gate shown until the user signs in. Buttons navigate to about:blank#… and - * main.ts intercepts them (the page itself has no privileges). + * Gate shown until the user signs in. Buttons send their data-action to + * main.ts (the page itself has no privileges). */ export function signInHtml(view: SignInView): string { const body = @@ -69,13 +83,13 @@ export function signInHtml(view: SignInView): string { ? `

Finish signing in in your browser.

Didn't see a browser window?

- ` + ` : `

Sign in with your CapScreen account to continue.

Your account controls access only. Cap tables, documents and screenings stay on this computer.

${view.state === "error" ? `
${escapeHtml(view.message)}
` : ""} - `; + `; return `CapScreen -
${markSvg()}

Welcome to CapScreen

${body}
`; +
${markSvg()}

Welcome to CapScreen

${body}
${ACTIONS_SCRIPT}`; } /** Shown in the browser tab after it hands the sign-in back to the app. */