diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3d61571df9..d0576e908f 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -30,6 +30,7 @@ import { NetService } from "@t3tools/shared/Net"; import { RotatingFileSink } from "@t3tools/shared/logging"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { showDesktopConfirmDialog } from "./confirmDialog"; +import { normalizeDesktopUpdateError } from "./updateErrors"; import { syncShellEnvironment } from "./syncShellEnvironment"; import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; import { @@ -806,11 +807,16 @@ async function checkForUpdates(reason: string): Promise { await autoUpdater.checkForUpdates(); return true; } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); + const normalizedError = normalizeDesktopUpdateError(error, "check"); setUpdateState( - reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString()), + reduceDesktopUpdateStateOnCheckFailure( + updateState, + normalizedError.message, + new Date().toISOString(), + normalizedError.toastAction, + ), ); - console.error(`[desktop-updater] Failed to check for updates: ${message}`); + console.error(`[desktop-updater] Failed to check for updates: ${normalizedError.rawMessage}`); return true; } finally { updateCheckInFlight = false; @@ -830,9 +836,15 @@ async function downloadAvailableUpdate(): Promise<{ accepted: boolean; completed await autoUpdater.downloadUpdate(); return { accepted: true, completed: true }; } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - setUpdateState(reduceDesktopUpdateStateOnDownloadFailure(updateState, message)); - console.error(`[desktop-updater] Failed to download update: ${message}`); + const normalizedError = normalizeDesktopUpdateError(error, "download"); + setUpdateState( + reduceDesktopUpdateStateOnDownloadFailure( + updateState, + normalizedError.message, + normalizedError.toastAction, + ), + ); + console.error(`[desktop-updater] Failed to download update: ${normalizedError.rawMessage}`); return { accepted: true, completed: false }; } finally { updateDownloadInFlight = false; @@ -859,11 +871,17 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed autoUpdater.quitAndInstall(true, true); return { accepted: true, completed: false }; } catch (error: unknown) { - const message = formatErrorMessage(error); + const normalizedError = normalizeDesktopUpdateError(error, "install"); updateInstallInFlight = false; isQuitting = false; - setUpdateState(reduceDesktopUpdateStateOnInstallFailure(updateState, message)); - console.error(`[desktop-updater] Failed to install update: ${message}`); + setUpdateState( + reduceDesktopUpdateStateOnInstallFailure( + updateState, + normalizedError.message, + normalizedError.toastAction, + ), + ); + console.error(`[desktop-updater] Failed to install update: ${normalizedError.rawMessage}`); return { accepted: true, completed: false }; } } @@ -939,25 +957,33 @@ function configureAutoUpdater(): void { console.info("[desktop-updater] No updates available."); }); autoUpdater.on("error", (error) => { - const message = formatErrorMessage(error); + const errorContext = resolveUpdaterErrorContext(); + const normalizedError = normalizeDesktopUpdateError(error, errorContext); if (updateInstallInFlight) { updateInstallInFlight = false; isQuitting = false; - setUpdateState(reduceDesktopUpdateStateOnInstallFailure(updateState, message)); - console.error(`[desktop-updater] Updater error: ${message}`); + setUpdateState( + reduceDesktopUpdateStateOnInstallFailure( + updateState, + normalizedError.message, + normalizedError.toastAction, + ), + ); + console.error(`[desktop-updater] Updater error: ${normalizedError.rawMessage}`); return; } if (!updateCheckInFlight && !updateDownloadInFlight) { setUpdateState({ status: "error", - message, + message: normalizedError.message, checkedAt: new Date().toISOString(), downloadPercent: null, - errorContext: resolveUpdaterErrorContext(), + errorContext, canRetry: updateState.availableVersion !== null || updateState.downloadedVersion !== null, + toastAction: normalizedError.toastAction, }); } - console.error(`[desktop-updater] Updater error: ${message}`); + console.error(`[desktop-updater] Updater error: ${normalizedError.rawMessage}`); }); autoUpdater.on("download-progress", (progress) => { const percent = Math.floor(progress.percent); diff --git a/apps/desktop/src/updateErrors.test.ts b/apps/desktop/src/updateErrors.test.ts new file mode 100644 index 0000000000..e62029a1f2 --- /dev/null +++ b/apps/desktop/src/updateErrors.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeDesktopUpdateError } from "./updateErrors"; + +describe("normalizeDesktopUpdateError", () => { + it("maps network-style check errors to a friendly message", () => { + expect(normalizeDesktopUpdateError(new Error("net::ERR_CONNECTION_REFUSED"), "check")).toEqual({ + message: "Couldn't reach the update server. Check your connection and try again.", + rawMessage: "net::ERR_CONNECTION_REFUSED", + toastAction: null, + }); + }); + + it("maps checksum download failures to a retryable message", () => { + expect( + normalizeDesktopUpdateError( + new Error("sha512 checksum mismatch, expected abc but got def"), + "download", + ), + ).toEqual({ + message: "The downloaded update could not be verified. Try downloading it again.", + rawMessage: "sha512 checksum mismatch, expected abc but got def", + toastAction: { + kind: "desktop-update.retry-download", + label: "Retry download", + }, + }); + }); + + it("falls back to the raw message when it is already useful", () => { + expect( + normalizeDesktopUpdateError(new Error("Release feed returned malformed JSON"), "check"), + ).toEqual({ + message: "Release feed returned malformed JSON", + rawMessage: "Release feed returned malformed JSON", + toastAction: null, + }); + }); +}); diff --git a/apps/desktop/src/updateErrors.ts b/apps/desktop/src/updateErrors.ts new file mode 100644 index 0000000000..f3df9b2a63 --- /dev/null +++ b/apps/desktop/src/updateErrors.ts @@ -0,0 +1,76 @@ +import type { DesktopToastAction, DesktopUpdateState } from "@t3tools/contracts"; + +type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"]; + +const RETRY_DOWNLOAD_TOAST_ACTION: DesktopToastAction = { + kind: "desktop-update.retry-download", + label: "Retry download", +}; + +const NETWORK_ERROR_PATTERN = + /\b(EAI_AGAIN|ECONNABORTED|ECONNREFUSED|ECONNRESET|ENETUNREACH|ENOTFOUND|ERR_CONNECTION_(?:CLOSED|REFUSED|RESET|TIMED_OUT)|ERR_INTERNET_DISCONNECTED|ERR_NAME_NOT_RESOLVED|ETIMEDOUT|socket hang up)\b/i; +const CHECKSUM_ERROR_PATTERN = + /\b(checksum|sha(?:256|512)?|hash)\b.*\b(mismatch|invalid|failed|different)\b|\b(mismatch|invalid|failed|different)\b.*\b(checksum|sha(?:256|512)?|hash)\b/i; + +function formatDesktopUpdateRawError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +function getFallbackDesktopUpdateErrorMessage(context: DesktopUpdateErrorContext): string { + if (context === "check") return "Couldn't check for updates."; + if (context === "download") return "Couldn't download the update."; + if (context === "install") return "Couldn't install the update."; + return "Update failed."; +} + +function isNetworkErrorMessage(message: string): boolean { + return NETWORK_ERROR_PATTERN.test(message); +} + +function isChecksumErrorMessage(message: string): boolean { + return CHECKSUM_ERROR_PATTERN.test(message); +} + +export function normalizeDesktopUpdateError( + error: unknown, + context: DesktopUpdateErrorContext, +): { + message: string; + rawMessage: string; + toastAction: DesktopToastAction | null; +} { + const rawMessage = formatDesktopUpdateRawError(error).trim(); + + if (context === "download" && isChecksumErrorMessage(rawMessage)) { + return { + message: "The downloaded update could not be verified. Try downloading it again.", + rawMessage, + toastAction: RETRY_DOWNLOAD_TOAST_ACTION, + }; + } + + if (isNetworkErrorMessage(rawMessage)) { + if (context === "download") { + return { + message: + "Couldn't download the update because the update server is unavailable. Try again in a moment.", + rawMessage, + toastAction: null, + }; + } + return { + message: "Couldn't reach the update server. Check your connection and try again.", + rawMessage, + toastAction: null, + }; + } + + return { + message: rawMessage.length > 0 ? rawMessage : getFallbackDesktopUpdateErrorMessage(context), + rawMessage, + toastAction: null, + }; +} diff --git a/apps/desktop/src/updateMachine.test.ts b/apps/desktop/src/updateMachine.test.ts index 7fbc982eff..1dd665f5b9 100644 --- a/apps/desktop/src/updateMachine.test.ts +++ b/apps/desktop/src/updateMachine.test.ts @@ -53,6 +53,7 @@ describe("updateMachine", () => { expect(state.status).toBe("error"); expect(state.errorContext).toBe("check"); expect(state.canRetry).toBe(true); + expect(state.toastAction).toBeNull(); }); it("preserves available version on download failure for retry", () => { @@ -65,12 +66,20 @@ describe("updateMachine", () => { downloadPercent: 43, }, "checksum mismatch", + { + kind: "desktop-update.retry-download", + label: "Retry download", + }, ); expect(state.status).toBe("available"); expect(state.availableVersion).toBe("1.1.0"); expect(state.errorContext).toBe("download"); expect(state.canRetry).toBe(true); + expect(state.toastAction).toEqual({ + kind: "desktop-update.retry-download", + label: "Retry download", + }); }); it("transitions to downloaded and then preserves install retry state", () => { @@ -133,7 +142,9 @@ describe("updateMachine", () => { expect(available.status).toBe("available"); expect(downloading.status).toBe("downloading"); expect(downloading.downloadPercent).toBe(0); + expect(downloading.toastAction).toBeNull(); expect(progress.downloadPercent).toBe(55.5); expect(progress.errorContext).toBeNull(); + expect(progress.toastAction).toBeNull(); }); }); diff --git a/apps/desktop/src/updateMachine.ts b/apps/desktop/src/updateMachine.ts index f13b420281..bda7b87c2f 100644 --- a/apps/desktop/src/updateMachine.ts +++ b/apps/desktop/src/updateMachine.ts @@ -1,4 +1,8 @@ -import type { DesktopRuntimeInfo, DesktopUpdateState } from "@t3tools/contracts"; +import type { + DesktopRuntimeInfo, + DesktopToastAction, + DesktopUpdateState, +} from "@t3tools/contracts"; import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState"; @@ -20,6 +24,7 @@ export function createInitialDesktopUpdateState( message: null, errorContext: null, canRetry: false, + toastAction: null, }; } @@ -35,6 +40,7 @@ export function reduceDesktopUpdateStateOnCheckStart( downloadPercent: null, errorContext: null, canRetry: false, + toastAction: null, }; } @@ -42,6 +48,7 @@ export function reduceDesktopUpdateStateOnCheckFailure( state: DesktopUpdateState, message: string, checkedAt: string, + toastAction: DesktopToastAction | null = null, ): DesktopUpdateState { return { ...state, @@ -51,6 +58,7 @@ export function reduceDesktopUpdateStateOnCheckFailure( downloadPercent: null, errorContext: "check", canRetry: true, + toastAction, }; } @@ -69,6 +77,7 @@ export function reduceDesktopUpdateStateOnUpdateAvailable( message: null, errorContext: null, canRetry: false, + toastAction: null, }; } @@ -86,6 +95,7 @@ export function reduceDesktopUpdateStateOnNoUpdate( message: null, errorContext: null, canRetry: false, + toastAction: null, }; } @@ -99,12 +109,14 @@ export function reduceDesktopUpdateStateOnDownloadStart( message: null, errorContext: null, canRetry: false, + toastAction: null, }; } export function reduceDesktopUpdateStateOnDownloadFailure( state: DesktopUpdateState, message: string, + toastAction: DesktopToastAction | null = null, ): DesktopUpdateState { return { ...state, @@ -113,6 +125,7 @@ export function reduceDesktopUpdateStateOnDownloadFailure( downloadPercent: null, errorContext: "download", canRetry: getCanRetryAfterDownloadFailure(state), + toastAction, }; } @@ -127,6 +140,7 @@ export function reduceDesktopUpdateStateOnDownloadProgress( message: null, errorContext: null, canRetry: false, + toastAction: null, }; } @@ -143,12 +157,14 @@ export function reduceDesktopUpdateStateOnDownloadComplete( message: null, errorContext: null, canRetry: true, + toastAction: null, }; } export function reduceDesktopUpdateStateOnInstallFailure( state: DesktopUpdateState, message: string, + toastAction: DesktopToastAction | null = null, ): DesktopUpdateState { return { ...state, @@ -156,5 +172,6 @@ export function reduceDesktopUpdateStateOnInstallFailure( message, errorContext: "install", canRetry: true, + toastAction, }; } diff --git a/apps/desktop/src/updateState.test.ts b/apps/desktop/src/updateState.test.ts index 43b718bd00..75adcbc400 100644 --- a/apps/desktop/src/updateState.test.ts +++ b/apps/desktop/src/updateState.test.ts @@ -22,6 +22,7 @@ const baseState: DesktopUpdateState = { message: null, errorContext: null, canRetry: false, + toastAction: null, }; describe("shouldBroadcastDownloadProgress", () => { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 89ced46454..3baf5e608d 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -81,13 +81,16 @@ import { formatRelativeTimeLabel } from "../timestampFormat"; import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; import { getArm64IntelBuildWarningDescription, - getDesktopUpdateActionError, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, shouldShowArm64IntelBuildWarning, - shouldToastDesktopUpdateActionResult, } from "./desktopUpdate.logic"; +import { + toastDesktopUpdateDownloadResult, + toastDesktopUpdateInstallResult, + toastDesktopUpdateUnexpectedError, +} from "./desktopUpdateToast"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; import { Button } from "./ui/button"; import { Menu, MenuGroup, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; @@ -1893,28 +1896,10 @@ export default function Sidebar() { void bridge .downloadUpdate() .then((result) => { - if (result.completed) { - toastManager.add({ - type: "success", - title: "Update downloaded", - description: "Restart the app from the update button to install it.", - }); - } - if (!shouldToastDesktopUpdateActionResult(result)) return; - const actionError = getDesktopUpdateActionError(result); - if (!actionError) return; - toastManager.add({ - type: "error", - title: "Could not download update", - description: actionError, - }); + toastDesktopUpdateDownloadResult(result); }) .catch((error) => { - toastManager.add({ - type: "error", - title: "Could not start update download", - description: error instanceof Error ? error.message : "An unexpected error occurred.", - }); + toastDesktopUpdateUnexpectedError("download", error); }); return; } @@ -1927,21 +1912,10 @@ export default function Sidebar() { void bridge .installUpdate() .then((result) => { - if (!shouldToastDesktopUpdateActionResult(result)) return; - const actionError = getDesktopUpdateActionError(result); - if (!actionError) return; - toastManager.add({ - type: "error", - title: "Could not install update", - description: actionError, - }); + toastDesktopUpdateInstallResult(result); }) .catch((error) => { - toastManager.add({ - type: "error", - title: "Could not install update", - description: error instanceof Error ? error.message : "An unexpected error occurred.", - }); + toastDesktopUpdateUnexpectedError("install", error); }); } }, [desktopUpdateButtonAction, desktopUpdateButtonDisabled, desktopUpdateState]); diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts index 84bde53048..71e7386813 100644 --- a/apps/web/src/components/desktopUpdate.logic.test.ts +++ b/apps/web/src/components/desktopUpdate.logic.test.ts @@ -28,6 +28,7 @@ const baseState: DesktopUpdateState = { message: null, errorContext: null, canRetry: false, + toastAction: null, }; describe("desktop update button state", () => { diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index 38983c810b..9b95ab75b2 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -94,11 +94,6 @@ export function shouldToastDesktopUpdateActionResult(result: DesktopUpdateAction return getDesktopUpdateActionError(result) !== null; } -export function shouldHighlightDesktopUpdateError(state: DesktopUpdateState | null): boolean { - if (!state || state.status !== "error") return false; - return state.errorContext === "download" || state.errorContext === "install"; -} - export function canCheckForUpdate(state: DesktopUpdateState | null): boolean { if (!state || !state.enabled) return false; return ( diff --git a/apps/web/src/components/desktopUpdateToast.ts b/apps/web/src/components/desktopUpdateToast.ts new file mode 100644 index 0000000000..5c3db1f166 --- /dev/null +++ b/apps/web/src/components/desktopUpdateToast.ts @@ -0,0 +1,116 @@ +import type { + DesktopToastAction, + DesktopUpdateActionResult, + DesktopUpdateCheckResult, +} from "@t3tools/contracts"; +import { getDesktopUpdateActionError } from "./desktopUpdate.logic"; +import { toastManager } from "./ui/toast"; + +function formatUnexpectedDesktopUpdateError(error: unknown, fallback: string): string { + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + return fallback; +} + +function getDesktopUpdateActionErrorToastAction( + action: DesktopToastAction | null, +): { children: string; onClick: () => void } | undefined { + if (!action) return undefined; + return { + children: action.label, + onClick: () => { + void runDesktopToastAction(action); + }, + }; +} + +function showDesktopUpdateDownloadResultToast(result: DesktopUpdateActionResult): void { + if (result.completed) { + toastManager.add({ + type: "success", + title: "Update downloaded", + description: "Restart the app from the update button to install it.", + }); + } + + const actionError = getDesktopUpdateActionError(result); + if (!actionError) return; + toastManager.add({ + type: "error", + title: "Could not download update", + description: actionError, + actionProps: getDesktopUpdateActionErrorToastAction(result.state.toastAction), + }); +} + +function showDesktopUpdateInstallResultToast(result: DesktopUpdateActionResult): void { + const actionError = getDesktopUpdateActionError(result); + if (!actionError) return; + toastManager.add({ + type: "error", + title: "Could not install update", + description: actionError, + actionProps: getDesktopUpdateActionErrorToastAction(result.state.toastAction), + }); +} + +async function runDesktopToastAction(action: DesktopToastAction): Promise { + const bridge = window.desktopBridge; + if (!bridge) return; + + switch (action.kind) { + case "desktop-update.retry-download": { + try { + const result = await bridge.downloadUpdate(); + showDesktopUpdateDownloadResultToast(result); + } catch (error) { + toastManager.add({ + type: "error", + title: "Could not start update download", + description: formatUnexpectedDesktopUpdateError(error, "An unexpected error occurred."), + }); + } + return; + } + } +} + +export function toastDesktopUpdateDownloadResult(result: DesktopUpdateActionResult): void { + showDesktopUpdateDownloadResultToast(result); +} + +export function toastDesktopUpdateInstallResult(result: DesktopUpdateActionResult): void { + showDesktopUpdateInstallResultToast(result); +} + +export function toastDesktopUpdateCheckFailure(result: DesktopUpdateCheckResult): void { + if (result.checked) return; + toastManager.add({ + type: "error", + title: "Could not check for updates", + description: result.state.message ?? "Automatic updates are not available in this build.", + actionProps: getDesktopUpdateActionErrorToastAction(result.state.toastAction), + }); +} + +export function toastDesktopUpdateUnexpectedError( + phase: "check" | "download" | "install", + error: unknown, +): void { + const titles: Record<"check" | "download" | "install", string> = { + check: "Could not check for updates", + download: "Could not start update download", + install: "Could not install update", + }; + const fallbacks: Record<"check" | "download" | "install", string> = { + check: "Update check failed.", + download: "An unexpected error occurred.", + install: "An unexpected error occurred.", + }; + toastManager.add({ + type: "error", + title: titles[phase], + description: formatUnexpectedDesktopUpdateError(error, fallbacks[phase]), + }); +} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index d534eefaa4..ff8e642cf4 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -29,6 +29,12 @@ import { isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, } from "../../components/desktopUpdate.logic"; +import { + toastDesktopUpdateCheckFailure, + toastDesktopUpdateDownloadResult, + toastDesktopUpdateInstallResult, + toastDesktopUpdateUnexpectedError, +} from "../../components/desktopUpdateToast"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { resolveAndPersistPreferredEditor } from "../../editorPreferences"; @@ -341,13 +347,10 @@ function AboutVersionSection() { .downloadUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); + toastDesktopUpdateDownloadResult(result); }) .catch((error: unknown) => { - toastManager.add({ - type: "error", - title: "Could not download update", - description: error instanceof Error ? error.message : "Download failed.", - }); + toastDesktopUpdateUnexpectedError("download", error); }); return; } @@ -363,13 +366,10 @@ function AboutVersionSection() { .installUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); + toastDesktopUpdateInstallResult(result); }) .catch((error: unknown) => { - toastManager.add({ - type: "error", - title: "Could not install update", - description: error instanceof Error ? error.message : "Install failed.", - }); + toastDesktopUpdateUnexpectedError("install", error); }); return; } @@ -379,21 +379,10 @@ function AboutVersionSection() { .checkForUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); - if (!result.checked) { - toastManager.add({ - type: "error", - title: "Could not check for updates", - description: - result.state.message ?? "Automatic updates are not available in this build.", - }); - } + toastDesktopUpdateCheckFailure(result); }) .catch((error: unknown) => { - toastManager.add({ - type: "error", - title: "Could not check for updates", - description: error instanceof Error ? error.message : "Update check failed.", - }); + toastDesktopUpdateUnexpectedError("check", error); }); }, [queryClient, updateState]); diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index 2f9aec112a..23e37181f2 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -6,18 +6,20 @@ import { setDesktopUpdateStateQueryData, useDesktopUpdateState, } from "../../lib/desktopUpdateReactQuery"; -import { toastManager } from "../ui/toast"; import { getArm64IntelBuildWarningDescription, - getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, shouldShowArm64IntelBuildWarning, shouldShowDesktopUpdateButton, - shouldToastDesktopUpdateActionResult, } from "../desktopUpdate.logic"; +import { + toastDesktopUpdateDownloadResult, + toastDesktopUpdateInstallResult, + toastDesktopUpdateUnexpectedError, +} from "../desktopUpdateToast"; import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; @@ -45,28 +47,10 @@ export function SidebarUpdatePill() { .downloadUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); - if (result.completed) { - toastManager.add({ - type: "success", - title: "Update downloaded", - description: "Restart the app from the update button to install it.", - }); - } - if (!shouldToastDesktopUpdateActionResult(result)) return; - const actionError = getDesktopUpdateActionError(result); - if (!actionError) return; - toastManager.add({ - type: "error", - title: "Could not download update", - description: actionError, - }); + toastDesktopUpdateDownloadResult(result); }) .catch((error) => { - toastManager.add({ - type: "error", - title: "Could not start update download", - description: error instanceof Error ? error.message : "An unexpected error occurred.", - }); + toastDesktopUpdateUnexpectedError("download", error); }); return; } @@ -78,21 +62,10 @@ export function SidebarUpdatePill() { .installUpdate() .then((result) => { setDesktopUpdateStateQueryData(queryClient, result.state); - if (!shouldToastDesktopUpdateActionResult(result)) return; - const actionError = getDesktopUpdateActionError(result); - if (!actionError) return; - toastManager.add({ - type: "error", - title: "Could not install update", - description: actionError, - }); + toastDesktopUpdateInstallResult(result); }) .catch((error) => { - toastManager.add({ - type: "error", - title: "Could not install update", - description: error instanceof Error ? error.message : "An unexpected error occurred.", - }); + toastDesktopUpdateUnexpectedError("install", error); }); } }, [action, disabled, queryClient, state]); diff --git a/apps/web/src/lib/desktopUpdateReactQuery.test.ts b/apps/web/src/lib/desktopUpdateReactQuery.test.ts index a0f4755918..4275c2e818 100644 --- a/apps/web/src/lib/desktopUpdateReactQuery.test.ts +++ b/apps/web/src/lib/desktopUpdateReactQuery.test.ts @@ -21,6 +21,7 @@ const baseState: DesktopUpdateState = { message: null, errorContext: null, canRetry: false, + toastAction: null, }; describe("desktopUpdateStateQueryOptions", () => { diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 57a1c4c3dc..9bd46e44c3 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -67,6 +67,11 @@ export type DesktopUpdateStatus = | "downloaded" | "error"; +export type DesktopToastAction = { + kind: "desktop-update.retry-download"; + label: string; +}; + export type DesktopRuntimeArch = "arm64" | "x64" | "other"; export type DesktopTheme = "light" | "dark" | "system"; @@ -90,6 +95,7 @@ export interface DesktopUpdateState { message: string | null; errorContext: "check" | "download" | "install" | null; canRetry: boolean; + toastAction: DesktopToastAction | null; } export interface DesktopUpdateActionResult {