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
+28 -1
View File
@@ -13,6 +13,30 @@ import { AboutView } from "./components/shared/AboutView";
import { ZapHistoryView } from "./components/zap/ZapHistoryView";
import { DMView } from "./components/dm/DMView";
import { useUIStore } from "./stores/ui";
import { useUpdater } from "./hooks/useUpdater";
function UpdateBanner() {
const { available, version, installing, error, install, dismiss } = useUpdater();
if (!available) return null;
return (
<div className="flex items-center justify-between px-4 py-2 bg-accent/10 border-b border-accent/30 text-[12px] shrink-0">
<span className="text-text">
Wrystr {version} is available.{" "}
{error && <span className="text-danger ml-1">{error}</span>}
</span>
<div className="flex items-center gap-3">
<button
onClick={install}
disabled={installing}
className="text-accent hover:text-accent-hover transition-colors disabled:opacity-50"
>
{installing ? "Installing…" : "Update & restart"}
</button>
<button onClick={dismiss} className="text-text-dim hover:text-text transition-colors">×</button>
</div>
</div>
);
}
function App() {
const currentView = useUIStore((s) => s.currentView);
@@ -25,7 +49,9 @@ function App() {
}
return (
<div className="flex h-screen w-screen bg-bg">
<div className="flex flex-col h-screen w-screen bg-bg">
<UpdateBanner />
<div className="flex flex-1 min-h-0">
<Sidebar />
<main className="flex-1 min-w-0">
{currentView === "feed" && <Feed />}
@@ -40,6 +66,7 @@ function App() {
{currentView === "zaps" && <ZapHistoryView />}
{currentView === "dm" && <DMView />}
</main>
</div>
</div>
);
}
+64
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),
};
}