diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 61ec4151..35f3df1e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2368,6 +2368,7 @@ function App() {
hideUpdateDownloadProgress,
isAboutOpen,
isBackgroundProgressForLatestUpdate,
+ isCheckingForUpdates,
isLatestUpdateDownloaded,
isUpdateChannelLoading,
isUpdateChannelSaving,
@@ -5162,7 +5163,14 @@ function App() {
lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? (
) : null,
- } onClick={() => checkForUpdates(false)}>{t('app.about.action.check_updates')},
+ }
+ loading={isCheckingForUpdates}
+ onClick={() => checkForUpdates(false)}
+ >
+ {t('app.about.action.check_updates')}
+ ,
closeAction ?? null,
lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? (
} onClick={() => downloadUpdate(lastUpdateInfo, false)}>{updateDownloadActionLabel}
diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx
index 88dea93d..200d463b 100644
--- a/frontend/src/hooks/useAppUpdateManager.test.tsx
+++ b/frontend/src/hooks/useAppUpdateManager.test.tsx
@@ -198,6 +198,38 @@ describe('useAppUpdateManager', () => {
expect(backendApp.CheckForUpdates).not.toHaveBeenCalled();
});
+ it('exposes a loading state while checking for updates', async () => {
+ let resolveCheck: ((result: Record) => void) | undefined;
+ const checkPromise = new Promise>((resolve) => {
+ resolveCheck = resolve;
+ });
+ backendApp.CheckForUpdates.mockReturnValue(checkPromise);
+
+ renderHook();
+
+ let pendingCheck: Promise | undefined;
+ act(() => {
+ pendingCheck = hook?.checkForUpdates(false);
+ });
+
+ expect(hook?.isCheckingForUpdates).toBe(true);
+ expect(hook?.aboutUpdateStatus).toBe('app.about.update_status.checking');
+
+ await act(async () => {
+ resolveCheck?.({
+ success: true,
+ data: {
+ hasUpdate: false,
+ currentVersion: '0.8.1',
+ latestVersion: '0.8.1',
+ },
+ });
+ await pendingCheck;
+ });
+
+ expect(hook?.isCheckingForUpdates).toBe(false);
+ });
+
it('merges complete MSI download metadata returned by the backend', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
diff --git a/frontend/src/hooks/useAppUpdateManager.ts b/frontend/src/hooks/useAppUpdateManager.ts
index 3eccbc71..75b496f4 100644
--- a/frontend/src/hooks/useAppUpdateManager.ts
+++ b/frontend/src/hooks/useAppUpdateManager.ts
@@ -221,6 +221,7 @@ export const useAppUpdateManager = ({
const [installMode, setInstallMode] = useState('unknown');
const [isUpdateChannelLoading, setIsUpdateChannelLoading] = useState(false);
const [isUpdateChannelSaving, setIsUpdateChannelSaving] = useState(false);
+ const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false);
const [aboutInfo, setAboutInfo] = useState(() => DEFAULT_ABOUT_INFO);
const [aboutUpdateStatus, setAboutUpdateStatus] = useState('');
const [lastUpdateInfo, setLastUpdateInfo] = useState(null);
@@ -470,6 +471,7 @@ export const useAppUpdateManager = ({
const checkForUpdates = useCallback(async (silent: boolean) => {
if (updateCheckInFlightRef.current) return;
updateCheckInFlightRef.current = true;
+ setIsCheckingForUpdates(true);
if (!silent) {
setAboutUpdateStatus(t('app.about.update_status.checking'));
}
@@ -477,8 +479,13 @@ export const useAppUpdateManager = ({
const checkFn = silent && typeof updateAPI.CheckForUpdatesSilently === 'function'
? updateAPI.CheckForUpdatesSilently
: updateAPI.CheckForUpdates;
- const res = await checkFn();
- updateCheckInFlightRef.current = false;
+ let res: any = null;
+ try {
+ res = await checkFn();
+ } finally {
+ updateCheckInFlightRef.current = false;
+ setIsCheckingForUpdates(false);
+ }
if (!res?.success) {
if (!silent) {
const error = res?.message || t('common.unknown');
@@ -785,6 +792,7 @@ export const useAppUpdateManager = ({
hideUpdateDownloadProgress,
isAboutOpen,
isBackgroundProgressForLatestUpdate,
+ isCheckingForUpdates,
isLatestUpdateDownloaded,
isUpdateChannelLoading,
isUpdateChannelSaving,