UI changes to update and support auto updating (#6075)

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Anthony Stirling
2026-06-02 23:11:37 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 919f0ade99
commit 256d1a86d2
31 changed files with 3602 additions and 488 deletions
@@ -25,6 +25,8 @@ import { SaaSTeamProvider } from "@app/contexts/SaaSTeamContext";
import { SaasBillingProvider } from "@app/contexts/SaasBillingContext";
import { SaaSCheckoutProvider } from "@app/contexts/SaaSCheckoutContext";
import { CreditModalBootstrap } from "@app/components/shared/modals/CreditModalBootstrap";
import UpdateModal from "@core/components/shared/UpdateModal";
import { useDesktopUpdatePopup } from "@app/hooks/useDesktopUpdatePopup";
// Common tool endpoints to preload for faster first-use
const COMMON_TOOL_ENDPOINTS = [
@@ -48,6 +50,7 @@ const COMMON_TOOL_ENDPOINTS = [
*/
export function AppProviders({ children }: { children: ReactNode }) {
const { isFirstLaunch, setupComplete } = useFirstLaunchCheck();
const updatePopup = useDesktopUpdatePopup();
const [connectionMode, setConnectionMode] = useState<
"saas" | "selfhosted" | "local" | null
>(null);
@@ -264,6 +267,38 @@ export function AppProviders({ children }: { children: ReactNode }) {
.catch(() => {});
}, [authChecked]);
// Desktop auto-update popup (shown on startup if update available)
const { state: popupState, actions: popupActions } = updatePopup;
const updatePopupModal = popupState.updateSummary && (
<UpdateModal
opened={popupState.showModal}
onClose={popupActions.dismissModal}
onRemindLater={popupActions.remindLater}
currentVersion={popupState.currentVersion}
updateSummary={popupState.updateSummary}
machineInfo={{
machineType: navigator.platform?.toLowerCase().includes("mac")
? "Client-mac"
: navigator.platform?.toLowerCase().includes("linux")
? "Client-unix"
: "Client-win",
activeSecurity: false,
licenseType: "NORMAL",
}}
desktopInstall={
popupState.tauriInstallReady
? {
state: popupState.state,
progress: popupState.progress,
errorMessage: popupState.errorMessage,
canInstall: popupState.canInstall,
actions: popupActions,
}
: undefined
}
/>
);
if (!authChecked) {
return (
<ProprietaryAppProviders
@@ -278,6 +313,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
}}
>
<div style={{ minHeight: "100vh" }} />
{updatePopupModal}
</ProprietaryAppProviders>
);
}
@@ -313,6 +349,8 @@ export function AppProviders({ children }: { children: ReactNode }) {
<DesktopOnboardingModal />
{/* Global sign-in modal, opened via stirling:open-sign-in event */}
<SignInModal />
{/* Desktop auto-update popup */}
{updatePopupModal}
</SaaSCheckoutProvider>
</SaasBillingProvider>
</SaaSTeamProvider>
@@ -1,16 +1,114 @@
import React from "react";
import { Stack } from "@mantine/core";
import React, { useCallback, useEffect, useState } from "react";
import { Stack, Alert } from "@mantine/core";
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 {
desktopUpdateService,
type UpdateMode,
type UpdateModeInfo,
} from "@app/services/desktopUpdateService";
/**
* Desktop extension of GeneralSection that adds default PDF editor settings
* Desktop extension of GeneralSection.
*
* Adds default PDF editor settings, wires up the Tauri auto-updater install
* flow, and exposes the user-facing update-mode control (prompt / auto /
* disabled). When the mode is locked by a provisioning file the control is
* still rendered but disabled, with a "Managed by administrator" hint, so
* managed-deployment users can see what policy is in effect.
*/
const GeneralSection: React.FC = () => {
const { t } = useTranslation();
const install = useDesktopInstall();
const [updateModeInfo, setUpdateModeInfo] = useState<UpdateModeInfo>({
mode: "prompt",
locked: false,
});
const [updateModeError, setUpdateModeError] = useState<string | null>(null);
// Check for Tauri updater availability on mount
useEffect(() => {
void install.checkTauriUpdate();
}, [install.checkTauriUpdate]);
// Load the current update mode + lock status on mount. We intentionally
// re-fetch on every mount so that a provisioning file dropped while the
// app is running (admin re-pushes config via MDM) is reflected the next
// time the user opens Settings — the Rust side re-reads the store on
// every call, so this is essentially a fresh read.
useEffect(() => {
let cancelled = false;
desktopUpdateService.getUpdateModeInfo().then((info) => {
if (!cancelled) setUpdateModeInfo(info);
});
return () => {
cancelled = true;
};
}, []);
const handleUpdateModeChange = useCallback(
async (mode: UpdateMode) => {
setUpdateModeError(null);
try {
await desktopUpdateService.setUpdateMode(mode);
// Refresh rather than optimistically updating — the Rust command
// can refuse the change (locked) and we want the UI to reflect
// the authoritative stored value.
const fresh = await desktopUpdateService.getUpdateModeInfo();
setUpdateModeInfo(fresh);
} catch (err) {
console.error("[GeneralSection] setUpdateMode failed:", err);
const msg =
err instanceof Error
? err.message
: typeof err === "string"
? err
: t(
"settings.general.updates.updateBehaviorErrorLocked",
"This setting is locked by your administrator.",
);
setUpdateModeError(msg);
}
},
[t],
);
return (
<Stack gap="lg">
<DefaultAppSettings />
<CoreGeneralSection />
{updateModeError && (
<Alert
color="red"
title={t(
"settings.general.updates.updateBehaviorError",
"Could not change update behavior",
)}
withCloseButton
onClose={() => setUpdateModeError(null)}
>
{updateModeError}
</Alert>
)}
<CoreGeneralSection
hideUpdateSection={
updateModeInfo.mode === "disabled" && updateModeInfo.locked
}
desktopInstall={{
state: install.state,
progress: install.progress,
errorMessage: install.errorMessage,
tauriInstallReady: install.tauriInstallReady,
canInstall: install.canInstall,
actions: install.actions,
}}
desktopUpdateMode={{
mode: updateModeInfo.mode,
locked: updateModeInfo.locked,
onChange: handleUpdateModeChange,
}}
/>
</Stack>
);
};