Files
fusion/packages/desktop/src/deep-link.ts
gsxdsm ce017cca98 feat(FN-1075): add desktop native and deep-link APIs
- Add electron-updater dependency and implement native desktop integration utilities for dialogs, notifications, auto-update wiring, and window state persistence
- Add fusion:// deep-link protocol registration, URL parsing, single-instance handlers, and renderer event forwarding for task/project routes
- Add comprehensive Vitest coverage for native integrations and deep-link handling across success, edge, and failure cases
- Document native and deep-link API contracts, behavior, and usage details in the desktop package README
2026-04-07 23:36:29 -07:00

100 lines
2.2 KiB
TypeScript

import { app, BrowserWindow } from "electron";
export interface DeepLinkResult {
type: "task" | "project" | "unknown";
id: string;
raw: string;
}
const DEEP_LINK_EVENT = "deep-link";
const FUSION_SCHEME = "fusion:";
export function registerDeepLinkProtocol(): void {
try {
const isRegistered = app.setAsDefaultProtocolClient("fusion");
if (!isRegistered) {
console.warn("[desktop/deep-link] Failed to register fusion:// protocol");
return;
}
console.log("[desktop/deep-link] Registered fusion:// protocol handler");
} catch (error) {
console.error("[desktop/deep-link] Error while registering fusion:// protocol", error);
}
}
export function parseDeepLink(rawUrl: string): DeepLinkResult | null {
if (!rawUrl) {
return null;
}
try {
const parsedUrl = new URL(rawUrl);
if (parsedUrl.protocol !== FUSION_SCHEME) {
return null;
}
const type = parsedUrl.hostname;
if (type !== "task" && type !== "project") {
return null;
}
const pathSegments = parsedUrl.pathname
.split("/")
.filter((segment) => segment.length > 0);
const decodedId = pathSegments.length > 0 ? decodeURIComponent(pathSegments[0]) : "";
return {
type,
id: decodedId,
raw: rawUrl,
};
} catch {
return null;
}
}
export function handleDeepLink(mainWindow: BrowserWindow, url: string): void {
const parsed = parseDeepLink(url);
if (!parsed || parsed.type === "unknown") {
console.warn(`[desktop/deep-link] Ignoring unsupported deep link: ${url}`);
return;
}
if (!mainWindow.isVisible()) {
mainWindow.show();
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.focus();
mainWindow.webContents.send(DEEP_LINK_EVENT, parsed);
}
export function setupDeepLinkHandler(mainWindow: BrowserWindow): void {
const hasLock = app.requestSingleInstanceLock();
if (!hasLock) {
app.quit();
return;
}
app.on("open-url", (event, url) => {
event.preventDefault();
handleDeepLink(mainWindow, url);
});
app.on("second-instance", (_event, argv) => {
const deepLink = argv.find((arg) => arg.startsWith("fusion://"));
if (!deepLink) {
return;
}
handleDeepLink(mainWindow, deepLink);
});
}