feat(desktop): split daemon and release settings to main process

Move daemon management and release channel settings out of the renderer's
AsyncStorage and into a desktop-owned settings store persisted by the main
process. Adds a "Keep daemon running after quit" option, a daemon lifecycle
section in settings, and a one-time migration of legacy renderer-owned
desktop settings.
This commit is contained in:
Mohamed Boudra
2026-04-25 13:59:07 +07:00
parent 2b37276577
commit ae3e88e7e1
20 changed files with 1792 additions and 187 deletions
@@ -0,0 +1,71 @@
/**
* @vitest-environment jsdom
*/
import { act, renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useDesktopAppUpdater } from "./use-desktop-app-updater";
const desktopUpdatesMock = vi.hoisted(() => ({
shouldShowDesktopUpdateSection: vi.fn(() => true),
checkDesktopAppUpdate: vi.fn(async () => ({
hasUpdate: false,
readyToInstall: false,
latestVersion: null,
})),
installDesktopAppUpdate: vi.fn(async () => ({
installed: false,
message: null,
})),
formatVersionWithPrefix: vi.fn((version: string | null) => version ?? "\u2014"),
}));
const settingsState = vi.hoisted(() => ({
releaseChannel: "stable" as "stable" | "beta",
}));
vi.mock("@/desktop/updates/desktop-updates", () => desktopUpdatesMock);
vi.mock("@/desktop/settings/desktop-settings", () => ({
useDesktopSettings: () => ({
settings: {
releaseChannel: settingsState.releaseChannel,
daemon: { manageBuiltInDaemon: true, keepRunningAfterQuit: true },
},
isLoading: false,
error: null,
updateSettings: vi.fn(async () => {}),
}),
}));
describe("useDesktopAppUpdater", () => {
beforeEach(() => {
settingsState.releaseChannel = "stable";
desktopUpdatesMock.checkDesktopAppUpdate.mockClear();
desktopUpdatesMock.installDesktopAppUpdate.mockClear();
});
it("uses the effective desktop release channel when checking for updates", async () => {
settingsState.releaseChannel = "beta";
renderHook(() => useDesktopAppUpdater());
await waitFor(() => {
expect(desktopUpdatesMock.checkDesktopAppUpdate).toHaveBeenCalledWith({
releaseChannel: "beta",
});
});
});
it("uses the effective desktop release channel when installing updates", async () => {
settingsState.releaseChannel = "beta";
const { result } = renderHook(() => useDesktopAppUpdater());
await act(async () => {
await result.current.installUpdate();
});
expect(desktopUpdatesMock.installDesktopAppUpdate).toHaveBeenCalledWith({
releaseChannel: "beta",
});
});
});
@@ -7,7 +7,7 @@ import {
type DesktopAppUpdateCheckResult,
type DesktopAppUpdateInstallResult,
} from "@/desktop/updates/desktop-updates";
import { useAppSettings } from "@/hooks/use-settings";
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
export type DesktopAppUpdateStatus =
| "idle"
@@ -84,8 +84,8 @@ function formatStatusText(input: {
export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
const isDesktopApp = shouldShowDesktopUpdateSection();
const { settings } = useAppSettings();
const releaseChannel = settings.releaseChannel;
const { settings: desktopSettings } = useDesktopSettings();
const releaseChannel = desktopSettings.releaseChannel;
const requestVersionRef = useRef(0);
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);