diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 2a0d1d453092..94bfbe44c368 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -107,6 +107,29 @@ describe("DesktopUpdates", () => { }), ); + it.effect("keeps the disabled reason available after configuration and channel changes", () => { + const harness = makeHarness({ env: { T3CODE_DISABLE_AUTO_UPDATE: "true" } }); + const reason = "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting."; + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + + const configured = yield* updates.getState; + assert.equal(configured.message, reason); + + const changed = yield* updates.setChannel("nightly"); + assert.equal(changed.channel, "nightly"); + assert.equal(changed.enabled, false); + assert.equal(changed.status, "disabled"); + assert.equal(changed.message, reason); + assert.equal(harness.sentStates.at(-1)?.message, reason); + assert.equal(harness.checkCount(), 0); + }), + ).pipe(Effect.provide(harness.layer)); + }); + it.effect("subscribe delivers the latest state plus subsequent changes", () => { const harness = makeHarness(); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index b2d93f407ac7..ae3c2588fab2 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -273,13 +273,15 @@ function parseAppUpdateYml(raw: string): Effect.Effect, environment: DesktopEnvironment.DesktopEnvironment["Service"], ): DesktopUpdateState { + const enabled = Option.isNone(disabledReason); return { ...createInitialDesktopUpdateState(environment.appVersion, environment.runtimeInfo, channel), enabled, status: enabled ? "idle" : "disabled", + message: Option.getOrNull(disabledReason), }; } @@ -468,8 +470,6 @@ export const make = Effect.gen(function* () { }); }); - const shouldEnableAutoUpdates = resolveDisabledReason.pipe(Effect.map(Option.isNone)); - const checkForUpdates = Effect.fn("desktop.updates.checkForUpdates")(function* ( reason: string, actionReservation: "acquire" | "held" = "acquire", @@ -631,10 +631,10 @@ export const make = Effect.gen(function* () { ), ); - const baseUpdateState = (channel: DesktopUpdateChannel, enabled: boolean) => + const baseUpdateState = (channel: DesktopUpdateChannel, disabledReason: Option.Option) => Ref.get(notAppliedVersionRef).pipe( Effect.map((notAppliedVersion) => ({ - ...createBaseUpdateState(channel, enabled, environment), + ...createBaseUpdateState(channel, disabledReason, environment), ...Option.match(notAppliedVersion, { onNone: () => ({}), onSome: (version) => ({ notAppliedVersion: version }), @@ -1056,9 +1056,9 @@ export const make = Effect.gen(function* () { } const settings = yield* desktopSettings.get; - const enabled = yield* shouldEnableAutoUpdates; - yield* setState(yield* baseUpdateState(settings.updateChannel, enabled)); - if (!enabled) { + const disabledReason = yield* resolveDisabledReason; + yield* setState(yield* baseUpdateState(settings.updateChannel, disabledReason)); + if (Option.isSome(disabledReason)) { return; } yield* Ref.set(updaterConfiguredRef, true); @@ -1127,10 +1127,10 @@ export const make = Effect.gen(function* () { ), ); - const enabled = yield* shouldEnableAutoUpdates; - yield* setState(yield* baseUpdateState(nextChannel, enabled)); + const disabledReason = yield* resolveDisabledReason; + yield* setState(yield* baseUpdateState(nextChannel, disabledReason)); - if (!enabled || !(yield* Ref.get(updaterConfiguredRef))) { + if (Option.isSome(disabledReason) || !(yield* Ref.get(updaterConfiguredRef))) { return yield* Ref.get(updateStateRef); } diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts index 3a42348381de..6d9ce611c909 100644 --- a/apps/web/src/components/desktopUpdate.logic.test.ts +++ b/apps/web/src/components/desktopUpdate.logic.test.ts @@ -6,6 +6,8 @@ import { getArm64IntelBuildWarningDescription, getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, + getDesktopUpdateDisabledReason, + getDesktopUpdateIdleTooltip, getDesktopUpdateInstallConfirmationMessage, getDesktopUpdateReleaseHistoryUrl, getDesktopUpdateReleaseUrl, @@ -391,3 +393,34 @@ describe("getDesktopUpdateButtonTooltip", () => { ); }); }); + +describe("disabled update reason", () => { + const disabledState: DesktopUpdateState = { + ...baseState, + enabled: false, + status: "disabled", + message: "Automatic updates are only available in packaged production builds.", + }; + + it("explains why checking is unavailable", () => { + expect(getDesktopUpdateDisabledReason(disabledState)).toBe( + "Automatic updates are only available in packaged production builds.", + ); + expect(getDesktopUpdateIdleTooltip(disabledState)).toEqual({ + title: "Updates unavailable", + detail: "Automatic updates are only available in packaged production builds.", + }); + expect(getDesktopUpdateDisabledReason({ ...disabledState, message: null })).toBe( + "Automatic updates are not available in this build.", + ); + }); + + it("keeps the check prompt when updates are available", () => { + expect(getDesktopUpdateDisabledReason(baseState)).toBeNull(); + expect(getDesktopUpdateDisabledReason(null)).toBeNull(); + expect(getDesktopUpdateIdleTooltip(baseState)).toEqual({ + title: "Check for updates", + detail: null, + }); + }); +}); diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index bbef4bf5a4e3..f6d5bc90d60b 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -148,3 +148,19 @@ export function canCheckForUpdate(state: DesktopUpdateState | null): boolean { state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled" ); } + +export function getDesktopUpdateDisabledReason( + state: Pick | null, +): string | null { + if (!state || (state.enabled && state.status !== "disabled")) return null; + return state.message?.trim() || "Automatic updates are not available in this build."; +} + +export function getDesktopUpdateIdleTooltip( + state: Pick | null, +) { + const disabledReason = getDesktopUpdateDisabledReason(state); + return disabledReason + ? { title: "Updates unavailable", detail: disabledReason } + : { title: "Check for updates", detail: null }; +} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 949ad3ea1338..5c4cdcaffd73 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -60,6 +60,7 @@ import { MobileAppSettingsRow } from "./MobileAppSettingsRow"; import { canCheckForUpdate, getDesktopUpdateButtonTooltip, + getDesktopUpdateDisabledReason, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, @@ -419,7 +420,7 @@ function AboutVersionSection() { const description = action === "download" || action === "install" ? "Update available." - : "Current version of the application."; + : (getDesktopUpdateDisabledReason(updateState) ?? "Current version of the application."); return ( <> diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index cfe0ba456829..b7b329de37c9 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -15,6 +15,7 @@ import { getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, getDesktopUpdateDownloadedVersion, + getDesktopUpdateIdleTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, @@ -236,6 +237,8 @@ function SidebarUpdateControl() { isDownloading, showCheckIcon, }); + const idleTooltip = + !showUpdateDetails && !showCheckIcon ? getDesktopUpdateIdleTooltip(state) : null; const tooltip = runsEverything ? everything.summary : showUpdateDetails @@ -244,7 +247,7 @@ function SidebarUpdateControl() { : "Update available" : showCheckIcon ? "Checking for updates…" - : "Check for updates"; + : (idleTooltip?.title ?? "Check for updates"); const disabled = showCheckIcon ? true : runsEverything @@ -441,7 +444,7 @@ function SidebarUpdateControl() { const updateButton = (