* fix: prevent crash when clicking external links with no default browser (#663) On systems like Tiny11 where no default browser is associated with http/https URLs, shell.openExternal() rejects with Windows error 0x483 ("No application is associated..."). The main process treated that rejection as an unhandledRejection, which the global handler re-throws as fatal, crashing the entire app. Root cause: windowManager.cjs used `void shell?.openExternal?.(url)` inside a try/catch, assuming the try would cover the call. `void` only discards the returned Promise — it does not catch async rejections, so when openExternal rejected, the error escaped as a floating unhandledRejection. The IPC handler in main.cjs (`netcatty:openExternal`) also awaited shell.openExternal() without any try/catch. Electron's ipcMain.handle forwards rejections to the renderer over IPC, but the renderer-side fallback called `window.open()`, which re-entered the same buggy windowManager path — and that is where the process actually died. Changes: - windowManager.cjs: attach an explicit `.catch` on the openExternal Promise in both createExternalOnlyWindowOpenHandler and createAppWindowOpenHandler so rejections cannot propagate. - main.cjs: wrap the IPC handler in try/catch and return a structured { success, error } result instead of throwing. This lets the renderer render an informative message. - global.d.ts: update the openExternal return type to match. - useApplicationBackend.ts: read the structured result and throw on failure so callers can react; drop the now-redundant window.open() fallback for the Electron branch (kept only for non-Electron envs). - SettingsApplicationTab.tsx: show a friendly toast ("No default browser configured — please set one in system settings") when openExternal fails, instead of the previous silent failure. - i18n: add en + zh-CN strings for the toast. Closes #663 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: fall back to in-app browser window when system has no default browser Instead of showing a toast when shell.openExternal() fails (e.g. Tiny11 with no default browser), open the URL in a minimal in-app BrowserWindow so users can still read the linked page. windowManager.cjs now exposes: - openFallbackBrowser(url, opts): creates a stripped-down BrowserWindow that loads the URL. No preload script (remote content must never touch contextBridge), contextIsolation/nodeIntegration/sandbox all set to safe defaults, and an isolated persist:netcatty-fallback-browser session so cookies and storage do not leak into the main app. Basic Alt+Left / Alt+Right / Ctrl-or-Cmd+R shortcuts for navigation and reload. - tryOpenExternalWithFallback(shell, url, opts): tries shell.openExternal first; on rejection, falls back to openFallbackBrowser. Returns { success, fallback?: "in-app-browser" }. All three external-URL call paths now route through this helper: - main.cjs netcatty:openExternal IPC handler - createExternalOnlyWindowOpenHandler (popup blocker for child windows) - createAppWindowOpenHandler (main/settings window window-open handler) The renderer-side toast is retained as a last-resort for the rare case that both system and in-app browsers fail (e.g. BrowserWindow creation error). Copy updated to reflect the new behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: preserve rejection semantics for failed external opens Per Codex review on PR #676: returning { success, error } from bridge.openExternal changed the contract from "reject on failure" to "resolve with a failure object on failure", which silently broke callers that rely on rejection to abort flows. useCloudSync's OAuth path is the clearest example: it wraps bridge.openExternal in a try/catch and rejects browserPromise inside the catch. With the resolved-failure contract, that catch never fires, so Promise.race([callbackPromise, browserPromise]) can hang indefinitely when no browser is available. Revert the contract: - tryOpenExternalWithFallback resolves void on success (system browser or in-app fallback) and throws on total failure - main.cjs IPC handler awaits and lets rejections propagate - global.d.ts openExternal is Promise<void> again - useApplicationBackend just awaits — rejections propagate naturally - SettingsApplicationTab's existing try/catch + toast continues to work as before Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: propagate fallback browser loadURL failures Per Codex P2: openFallbackBrowser swallowed loadURL rejections by attaching a .catch that only logged, so any caller using tryOpenExternalWithFallback as a success signal saw an opened window as success even when the page failed to load. OAuth flows would then wait for the downstream callback timeout instead of canceling early on malformed or unreachable URLs. openFallbackBrowser now returns { window, loaded } where `loaded` is the raw loadURL Promise, and tryOpenExternalWithFallback awaits it in the fallback path. On initial load failure, the broken window is closed and the original shell.openExternal error is re-thrown. The internal popup handler inside the fallback window keeps its fire-and-forget behavior (it must return synchronously) but now explicitly catches the loaded rejection to avoid unhandledRejection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
45 lines
1.4 KiB
TypeScript
45 lines
1.4 KiB
TypeScript
import { useCallback } from "react";
|
|
import { netcattyBridge } from "../../infrastructure/services/netcattyBridge";
|
|
|
|
export type ApplicationInfo = {
|
|
name: string;
|
|
version: string;
|
|
platform: string;
|
|
};
|
|
|
|
export type SshAgentStatus = {
|
|
running: boolean;
|
|
startupType: string | null;
|
|
error: string | null;
|
|
};
|
|
|
|
export const useApplicationBackend = () => {
|
|
const openExternal = useCallback(async (url: string) => {
|
|
const bridge = netcattyBridge.get();
|
|
if (bridge?.openExternal) {
|
|
// Bridge resolves on success (either via system browser or in-app
|
|
// fallback window) and rejects only when both paths fail. Let the
|
|
// rejection propagate so callers can present a user-facing message.
|
|
await bridge.openExternal(url);
|
|
return;
|
|
}
|
|
// Fallback for non-Electron environments (tests, dev server, etc.).
|
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
}, []);
|
|
|
|
const getApplicationInfo = useCallback(async (): Promise<ApplicationInfo | null> => {
|
|
const bridge = netcattyBridge.get();
|
|
const info = await bridge?.getAppInfo?.();
|
|
return info ?? null;
|
|
}, []);
|
|
|
|
const checkSshAgent = useCallback(async (): Promise<SshAgentStatus | null> => {
|
|
const bridge = netcattyBridge.get();
|
|
const status = await bridge?.checkSshAgent?.();
|
|
return status ?? null;
|
|
}, []);
|
|
|
|
return { openExternal, getApplicationInfo, checkSshAgent };
|
|
};
|
|
|