feat(desktop): update failures say what happened and what to do
CI / Secret scan (gitleaks) (push) Successful in 15s
CI / Build & Quality Checks (push) Canceled after 53s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Successful in 15s
CI / Build & Quality Checks (push) Canceled after 53s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
A friend's update failed ten times in a row ("Update check failed: error
sending request for url (…nsis.zip)") before the 11th went through, and
he didn't know what to do. The label was also wrong: the check had
worked; the download failed.
- Progress: "Downloading update… 28% (14.3 MB of 49.9 MB)", "The update
server didn't respond. Trying again in 3 s (attempt 2 of 4)…", from the
native `lotus-update-progress` events (cinny-desktop retries itself).
- Failures name the step (check / download / install, from the native
error prefix) in plain language, with Try again and a Download
installer button (Windows: the setup .exe; else the release page), and
the raw error under "Details".
- Installing from the update toast now shows a "Downloading update"
toast, and on failure a sticky "Update didn't install" toast that
retries on click and points at Settings → General → App Updates.
Before, the toast vanished and the failure was only visible in Settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23649f1255
commit
568f218fe9
@@ -1,17 +1,33 @@
|
||||
import { useCallback } from 'react';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { useTauriEvent } from './useTauri';
|
||||
import { UpdatePhase, parseUpdateError } from '../utils/updateErrors';
|
||||
|
||||
type TauriInternals = { invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> };
|
||||
const tauriInvoke = (): TauriInternals['invoke'] | undefined =>
|
||||
(window as unknown as { __TAURI_INTERNALS__?: TauriInternals }).__TAURI_INTERNALS__?.invoke;
|
||||
|
||||
type UpdateStatus =
|
||||
/** Detail of the native `lotus-update-progress` event (see lib.rs `install_update`). */
|
||||
export type UpdateProgress =
|
||||
| {
|
||||
phase: 'downloading';
|
||||
attempt: number;
|
||||
maxAttempts: number;
|
||||
downloaded: number;
|
||||
total: number | null;
|
||||
}
|
||||
| { phase: 'retrying'; attempt: number; maxAttempts: number; waitSecs: number; error: string }
|
||||
| { phase: 'installing' };
|
||||
|
||||
export type UpdateStatus =
|
||||
| { state: 'idle' }
|
||||
| { state: 'checking' }
|
||||
| { state: 'up-to-date' }
|
||||
| { state: 'available'; version: string }
|
||||
| { state: 'installing' }
|
||||
| { state: 'error'; message: string };
|
||||
| { state: 'installing'; progress?: UpdateProgress }
|
||||
| { state: 'error'; phase: UpdatePhase; message: string };
|
||||
|
||||
export type UpdateFailure = { phase: UpdatePhase; message: string };
|
||||
|
||||
// Module-level so the result of a manual "Check for updates" survives closing
|
||||
// and reopening Settings (it used to be component state, so the "update
|
||||
@@ -20,6 +36,17 @@ type UpdateStatus =
|
||||
// mid-request by the resolve/reject below, so nothing gets stuck.
|
||||
const updateStatusAtom = atom<UpdateStatus>({ state: 'idle' });
|
||||
|
||||
/**
|
||||
* Mirror native download progress into the status. Mounted once (in
|
||||
* TauriUpdateFeature) so the listener isn't duplicated per consumer.
|
||||
*/
|
||||
export function useTauriUpdateProgress(): void {
|
||||
const setStatus = useSetAtom(updateStatusAtom);
|
||||
useTauriEvent<UpdateProgress>('lotus-update-progress', (progress) =>
|
||||
setStatus((prev) => (prev.state === 'installing' ? { state: 'installing', progress } : prev)),
|
||||
);
|
||||
}
|
||||
|
||||
export function useTauriUpdater() {
|
||||
const isTauri = !!tauriInvoke();
|
||||
const [status, setStatus] = useAtom(updateStatusAtom);
|
||||
@@ -36,22 +63,29 @@ export function useTauriUpdater() {
|
||||
: { state: 'up-to-date' },
|
||||
);
|
||||
} catch (e) {
|
||||
setStatus({ state: 'error', message: String(e) });
|
||||
setStatus({ state: 'error', ...parseUpdateError(String(e), 'check') });
|
||||
}
|
||||
}, [setStatus]);
|
||||
|
||||
const install = useCallback(async () => {
|
||||
/**
|
||||
* Resolves `undefined` when nothing needed installing (a successful install
|
||||
* restarts the app instead), or the failure.
|
||||
*/
|
||||
const install = useCallback(async (): Promise<UpdateFailure | undefined> => {
|
||||
const invoke = tauriInvoke();
|
||||
if (!invoke) return;
|
||||
if (!invoke) return undefined;
|
||||
setStatus({ state: 'installing' });
|
||||
try {
|
||||
await invoke('install_update');
|
||||
// On a successful install the native side calls app.restart(), so this
|
||||
// On a successful install the native side restarts the app, so this
|
||||
// resolve is only reached when nothing was installed (no update found) —
|
||||
// don't leave the UI stuck on "installing".
|
||||
setStatus({ state: 'up-to-date' });
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
setStatus({ state: 'error', message: String(e) });
|
||||
const failure = parseUpdateError(String(e), 'download');
|
||||
setStatus({ state: 'error', ...failure });
|
||||
return failure;
|
||||
}
|
||||
}, [setStatus]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user