Add auto-updater (Phase 1 #4)

- tauri-plugin-updater + tauri-plugin-process registered (Rust + npm)
- Updater endpoint: github.com/hoornet/wrystr/releases/latest/download/latest.json
- Ed25519 signing keypair generated; public key in tauri.conf.json;
  private key added to TAURI_SIGNING_PRIVATE_KEY GitHub secret
- Release workflow passes TAURI_SIGNING_PRIVATE_KEY env var so
  tauri-action signs artifacts and generates latest.json manifest
- useUpdater hook: checks for updates 5 s after startup (silent on
  error); exposes available, version, install(), dismiss()
- UpdateBanner: dismissible top-of-app bar shown when an update is
  available — "Update & restart" downloads, installs, and relaunches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-10 21:00:14 +01:00
parent 4cde2fe4c7
commit 5659f18099
10 changed files with 481 additions and 5 deletions

64
src/hooks/useUpdater.ts Normal file
View File

@@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
interface UpdateState {
available: boolean;
version: string | null;
body: string | null;
installing: boolean;
error: string | null;
install: () => Promise<void>;
dismiss: () => void;
}
export function useUpdater(): UpdateState {
const [available, setAvailable] = useState(false);
const [version, setVersion] = useState<string | null>(null);
const [body, setBody] = useState<string | null>(null);
const [installing, setInstalling] = useState(false);
const [error, setError] = useState<string | null>(null);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Check for updates ~5 s after startup (non-blocking)
const t = setTimeout(async () => {
try {
const update = await check();
if (update?.available) {
setAvailable(true);
setVersion(update.version);
setBody(update.body ?? null);
}
} catch {
// Update check failure is silent — network may be unavailable
}
}, 5000);
return () => clearTimeout(t);
}, []);
const install = async () => {
setInstalling(true);
setError(null);
try {
const update = await check();
if (update?.available) {
await update.downloadAndInstall();
await relaunch();
}
} catch (err) {
setError(String(err));
setInstalling(false);
}
};
return {
available: available && !dismissed,
version,
body,
installing,
error,
install,
dismiss: () => setDismissed(true),
};
}