feat(desktop): restore Tauri auto-updater support (#393)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-04-23 11:55:35 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 1863411b1e
commit c1a33e3ff1
10 changed files with 305 additions and 1 deletions
+1
View File
@@ -41,6 +41,7 @@
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.0",
"@tiptap/core": "^3.22.3",
"@tiptap/extension-link": "^3.22.3",
"@tiptap/extension-placeholder": "^3.22.3",
+10
View File
@@ -68,6 +68,9 @@ importers:
'@tauri-apps/plugin-process':
specifier: ^2.3.1
version: 2.3.1
'@tauri-apps/plugin-updater':
specifier: ^2.10.0
version: 2.10.1
'@tiptap/core':
specifier: ^3.22.3
version: 3.22.3(@tiptap/pm@3.22.3)
@@ -1359,6 +1362,9 @@ packages:
'@tauri-apps/plugin-process@2.3.1':
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
'@tauri-apps/plugin-updater@2.10.1':
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
'@tiptap/core@3.22.3':
resolution: {integrity: sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==}
peerDependencies:
@@ -3731,6 +3737,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/plugin-updater@2.10.1':
dependencies:
'@tauri-apps/api': 2.10.1
'@tiptap/core@3.22.3(@tiptap/pm@3.22.3)':
dependencies:
'@tiptap/pm': 3.22.3
+79 -1
View File
@@ -2937,6 +2937,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -3374,6 +3380,18 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -3529,6 +3547,20 @@ dependencies = [
"ureq 3.3.0",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -5278,6 +5310,7 @@ dependencies = [
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-updater",
"tauri-plugin-websocket",
"tauri-plugin-window-state",
"tempfile",
@@ -5288,7 +5321,7 @@ dependencies = [
"uuid",
"windows-sys 0.61.2",
"zeroize",
"zip",
"zip 2.4.2",
]
[[package]]
@@ -5931,6 +5964,39 @@ dependencies = [
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]]
name = "tauri-plugin-websocket"
version = "2.4.2"
@@ -8052,6 +8118,18 @@ dependencies = [
"zstd",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.21"
+1
View File
@@ -31,6 +31,7 @@ tauri-plugin-opener = "2"
tauri-plugin-window-state = "2"
tauri-plugin-websocket = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
infer = "0.19"
hex = "0.4"
+16
View File
@@ -1,6 +1,9 @@
fn main() {
println!("cargo:rerun-if-env-changed=SPROUT_RELAY_URL");
println!("cargo:rerun-if-env-changed=SPROUT_RELAY_HTTP");
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_PUBLIC_KEY");
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_ENDPOINT");
println!("cargo:rustc-check-cfg=cfg(sprout_updater_enabled)");
if let Ok(relay_url) = std::env::var("SPROUT_RELAY_URL") {
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_URL={relay_url}");
@@ -10,5 +13,18 @@ fn main() {
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_HTTP={relay_http}");
}
let updater_public_key = std::env::var("SPROUT_UPDATER_PUBLIC_KEY")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let updater_endpoint = std::env::var("SPROUT_UPDATER_ENDPOINT")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
if updater_public_key.is_some() && updater_endpoint.is_some() {
println!("cargo:rustc-cfg=sprout_updater_enabled");
}
tauri_build::build()
}
@@ -18,6 +18,8 @@
"websocket:default",
"window-state:default",
"dialog:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:allow-restart",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister",
+13
View File
@@ -252,6 +252,19 @@ pub fn run() {
.build()
});
// Only register the updater in release builds that were compiled with a
// real updater configuration. Local unsigned builds omit that config and
// should still launch for debugging.
#[cfg(sprout_updater_enabled)]
let builder = if cfg!(debug_assertions) {
builder
} else {
builder.plugin(tauri_plugin_updater::Builder::new().build())
};
#[cfg(not(sprout_updater_enabled))]
let builder = builder;
let shutdown_started = Arc::new(AtomicBool::new(false));
let restore_shutdown_started = Arc::clone(&shutdown_started);
let app = builder
+5
View File
@@ -33,6 +33,11 @@
"csp": null
}
},
"plugins": {
"updater": {
"endpoints": []
}
},
"bundle": {
"active": true,
"targets": "all",
@@ -0,0 +1,168 @@
import { useState, useRef, useCallback } from "react";
import { check, type Update } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
type UpdateStatus =
| { state: "idle" }
| { state: "checking" }
| { state: "up-to-date" }
| { state: "available"; version: string }
| { state: "downloading" }
| { state: "installing" }
| { state: "ready" }
| { state: "error"; message: string };
export function UpdateChecker() {
const [status, setStatus] = useState<UpdateStatus>({ state: "idle" });
const updateRef = useRef<Update | null>(null);
const closeUpdate = useCallback(async () => {
if (updateRef.current) {
await updateRef.current.close();
updateRef.current = null;
}
}, []);
async function checkForUpdate() {
try {
await closeUpdate();
setStatus({ state: "checking" });
const update = await check();
if (update) {
updateRef.current = update;
setStatus({ state: "available", version: update.version });
} else {
setStatus({ state: "up-to-date" });
}
} catch (err) {
setStatus({
state: "error",
message: err instanceof Error ? err.message : String(err),
});
}
}
async function downloadAndInstall() {
try {
const update = updateRef.current;
if (!update) {
setStatus({ state: "up-to-date" });
return;
}
setStatus({ state: "downloading" });
await update.downloadAndInstall((event) => {
if (event.event === "Finished") {
setStatus({ state: "installing" });
}
});
setStatus({ state: "ready" });
} catch (err) {
setStatus({
state: "error",
message: err instanceof Error ? err.message : String(err),
});
}
}
async function handleRelaunch() {
await relaunch();
}
return (
<div className="rounded-lg bg-zinc-900 p-4">
<h3 className="mb-3 text-sm font-medium text-zinc-200">
Software Updates
</h3>
{status.state === "idle" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-400">
Check if a new version is available.
</p>
<button
type="button"
onClick={checkForUpdate}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Check for Updates
</button>
</div>
)}
{status.state === "checking" && (
<p className="text-sm text-zinc-400">Checking for updates...</p>
)}
{status.state === "up-to-date" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-300">You're on the latest version.</p>
<button
type="button"
onClick={checkForUpdate}
className="rounded-lg bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700"
>
Check Again
</button>
</div>
)}
{status.state === "available" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-300">
Version <span className="font-semibold">{status.version}</span> is
available.
</p>
<button
type="button"
onClick={downloadAndInstall}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Download &amp; Install
</button>
</div>
)}
{status.state === "downloading" && (
<p className="text-sm text-zinc-400">Downloading update...</p>
)}
{status.state === "installing" && (
<p className="text-sm text-zinc-400">Installing update...</p>
)}
{status.state === "ready" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-300">
Update installed. Restart to apply.
</p>
<button
type="button"
onClick={handleRelaunch}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Restart Now
</button>
</div>
)}
{status.state === "error" && (
<div className="flex items-center justify-between">
<p className="text-sm text-red-400">
Update failed: {status.message}
</p>
<button
type="button"
onClick={checkForUpdate}
className="rounded-lg bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700"
>
Retry
</button>
</div>
)}
</div>
);
}
@@ -2,6 +2,7 @@ import { useState, useMemo, useRef } from "react";
import {
BellRing,
Check,
Download,
Keyboard,
KeyRound,
MonitorCog,
@@ -26,6 +27,7 @@ import { KeyboardShortcutsCard } from "./KeyboardShortcutsCard";
import { MobilePairingCard } from "./MobilePairingCard";
import { NotificationSettingsCard } from "./NotificationSettingsCard";
import { ProfileSettingsCard } from "./ProfileSettingsCard";
import { UpdateChecker } from "../UpdateChecker";
export type SettingsSection =
| "profile"
@@ -34,6 +36,7 @@ export type SettingsSection =
| "shortcuts"
| "tokens"
| "mobile"
| "updates"
| "doctor";
export const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile";
@@ -88,6 +91,11 @@ export const settingsSections: SettingsSectionDescriptor[] = [
label: "Mobile",
icon: Smartphone,
},
{
value: "updates",
label: "Updates",
icon: Download,
},
{
value: "doctor",
label: "Doctor",
@@ -248,6 +256,8 @@ export function renderSettingsSection(
return <TokenSettingsCard currentPubkey={props.currentPubkey} />;
case "mobile":
return <MobilePairingCard currentPubkey={props.currentPubkey} />;
case "updates":
return <UpdateChecker />;
case "doctor":
return <DoctorSettingsPanel />;
default: {