Disable update check and notification in SaaS mode (#6863)

# Description of Changes

In SaaS mode the self-hosted "Update Available" notification could still
appear and the update-check code (external call to
`supabase.stirling.com/functions/v1/updates`) still ran, even though the
cloud owns app versioning. The web `UpdateStartupPopup` was already
SaaS-gated via a null override, but two other paths were not:

- **Desktop app in SaaS connection mode** - `useDesktopUpdatePopup()`
ran its startup check and rendered the `UpdateModal` regardless of
connection mode, so a self-hosted update popup appeared while connected
to SaaS.
- **Settings → General** - the core `GeneralSection` fired
`checkForUpdate()` on mount unconditionally, even when the update
section was hidden (as SaaS does), so the external call still ran.

**What changed**

- `useDesktopUpdatePopup.ts` - the startup timer now bails out
immediately when `connectionModeService.getCurrentMode() === "saas"`. No
mode lookup, no external fetch, no modal.
- `core/GeneralSection.tsx` - the mount `checkForUpdate()` now returns
early when `hideUpdateSection` is set, so hiding the section (web SaaS,
managed-disabled desktop) also stops the external call.
- `desktop/GeneralSection.tsx` - passes `hideUpdateSection` when
`useSaaSMode()` is true, which (via the above) suppresses the settings
check in desktop-SaaS too.

**Why** - in SaaS the update check should never be called and no update
notification should be shown; the cloud handles versioning.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
This commit is contained in:
Anthony Stirling
2026-07-07 12:09:51 +01:00
parent 3c93457021
commit 8e4b2e2fc6
4 changed files with 40 additions and 4 deletions
@@ -112,12 +112,14 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
// falling back to the backend version
const currentVersion = appVersion ?? config?.appVersion ?? null;
// Check for updates on mount
// Check for updates on mount — skipped when the update UI is hidden (SaaS
// build, managed-disabled desktop) so no external update call ever fires.
useEffect(() => {
if (hideUpdateSection) return;
if (currentVersion) {
checkForUpdate();
}
}, [currentVersion, config?.machineType]);
}, [currentVersion, config?.machineType, hideUpdateSection]);
const checkForUpdate = async () => {
if (!currentVersion) return;
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import CoreGeneralSection from "@core/components/shared/config/configSections/GeneralSection";
import { DefaultAppSettings } from "@app/components/shared/config/configSections/DefaultAppSettings";
import { useDesktopInstall } from "@app/hooks/useDesktopInstall";
import { useSaaSMode } from "@app/hooks/useSaaSMode";
import {
desktopUpdateService,
type UpdateMode,
@@ -22,6 +23,9 @@ import {
const GeneralSection: React.FC = () => {
const { t } = useTranslation();
const install = useDesktopInstall();
// In SaaS connection mode the cloud owns app versioning — hide the update
// section (which also stops the core auto-check from firing).
const isSaaSMode = useSaaSMode();
const [updateModeInfo, setUpdateModeInfo] = useState<UpdateModeInfo>({
mode: "prompt",
locked: false,
@@ -93,7 +97,8 @@ const GeneralSection: React.FC = () => {
)}
<CoreGeneralSection
hideUpdateSection={
updateModeInfo.mode === "disabled" && updateModeInfo.locked
isSaaSMode ||
(updateModeInfo.mode === "disabled" && updateModeInfo.locked)
}
desktopInstall={{
state: install.state,
@@ -9,6 +9,7 @@ const getVersionMock = vi.fn();
const getUpdateModeMock = vi.fn();
const canInstallUpdatesMock = vi.fn();
const getUpdateSummaryMock = vi.fn();
const getCurrentModeMock = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
invoke: (cmd: string, args?: unknown) => invokeMock(cmd, args),
@@ -36,6 +37,12 @@ vi.mock("@app/services/desktopUpdateService", () => ({
},
}));
vi.mock("@app/services/connectionModeService", () => ({
connectionModeService: {
getCurrentMode: () => getCurrentModeMock(),
},
}));
import { useDesktopUpdatePopup } from "@app/hooks/useDesktopUpdatePopup";
/** Flush pending microtasks so awaited promises settle. */
@@ -74,8 +81,11 @@ describe("useDesktopUpdatePopup — auto mode", () => {
getUpdateModeMock.mockReset();
canInstallUpdatesMock.mockReset();
getUpdateSummaryMock.mockReset();
getCurrentModeMock.mockReset();
// Defaults: auto mode, update available, install permitted.
// Defaults: local (non-SaaS) connection, auto mode, update available,
// install permitted.
getCurrentModeMock.mockResolvedValue("local");
getUpdateModeMock.mockResolvedValue("auto");
getVersionMock.mockResolvedValue("1.0.0");
getUpdateSummaryMock.mockResolvedValue({ latest_version: "2.0.0" });
@@ -189,4 +199,18 @@ describe("useDesktopUpdatePopup — auto mode", () => {
expect(invocations).toContain("download_and_install_update");
expect(invocations).toContain("restart_app");
});
it("skips the update check entirely in SaaS connection mode", async () => {
// In SaaS mode the cloud owns versioning — the self-hosted update check
// must never run: no mode lookup, no external summary fetch, no install.
getCurrentModeMock.mockResolvedValue("saas");
await runStartup();
expect(getUpdateModeMock).not.toHaveBeenCalled();
expect(getUpdateSummaryMock).not.toHaveBeenCalled();
const invocations = invokeMock.mock.calls.map((c) => c[0]);
expect(invocations).not.toContain("download_and_install_update");
expect(invocations).not.toContain("restart_app");
});
});
@@ -6,6 +6,7 @@ import {
desktopUpdateService,
type CanInstallResult,
} from "@app/services/desktopUpdateService";
import { connectionModeService } from "@app/services/connectionModeService";
const SNOOZE_KEY = "stirling-pdf-updater:snoozedUntil";
const STARTUP_DELAY_MS = 15_000;
@@ -72,6 +73,10 @@ export function useDesktopUpdatePopup() {
hasChecked.current = true;
const timer = setTimeout(async () => {
// In SaaS connection mode the cloud owns app versioning — the self-hosted
// update check + popup must never run (no external call, no modal).
if ((await connectionModeService.getCurrentMode()) === "saas") return;
let mode: Awaited<ReturnType<typeof desktopUpdateService.getUpdateMode>> =
"prompt";
try {