send app store builds to the store for updates

This commit is contained in:
pj committed 2026-09-01 23:17:33 +05:30
1 parent e8772e6699
commit e8555c7ef9
10 files changed
+199 -54

No files matched your search

+1
View File
@@ -4723,6 +4723,7 @@ dependencies = [
"objc2-foundation",
"rand 0.8.6",
"reqwest 0.12.28",
"semver",
"serde",
"serde_json",
"sha2",
+1
View File
@@ -38,6 +38,7 @@ typst-as-lib = "0.15.5"
harper-core = { version = "=2.5.0", features = ["concurrent"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
sha2 = "0.10"
semver = "1"
rand = "0.8"
url = "2"
+2 -1
View File
@@ -5,7 +5,8 @@
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Google Drive backup talks to googleapis.com. Nothing else leaves the machine. -->
<!-- Google Drive backup talks to googleapis.com, and the update check asks itunes.apple.com
which version is live on the store. Nothing else leaves the machine. -->
<key>com.apple.security.network.client</key>
<true/>
+6 -6
View File
@@ -7,6 +7,7 @@ mod macspell;
mod pdf;
mod project;
mod proofing;
mod updates;
mod writingtools;
#[cfg(desktop)]
@@ -30,11 +31,7 @@ fn build_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>
let export_epub = MenuItemBuilder::with_id("export-epub", "Export as EPUB…")
.accelerator("CmdOrCtrl+Shift+E")
.build(handle)?;
let check_updates = handle
.config()
.plugins
.0
.contains_key("updater")
let check_updates = (updates::channel(handle) != "none")
.then(|| MenuItemBuilder::with_id("check-updates", "Check for Updates…").build(handle))
.transpose()?;
let settings = MenuItemBuilder::with_id("settings", "Settings…")
@@ -223,7 +220,10 @@ pub fn run() {
gdrive::gdrive_backup,
gdrive::gdrive_sync,
gdrive::gdrive_restore,
gdrive::gdrive_list_backups
gdrive::gdrive_list_backups,
updates::update_channel,
updates::appstore_latest,
updates::open_appstore
])
.build(context)
.expect("error while building margin");
+90
View File
@@ -0,0 +1,90 @@
use std::sync::LazyLock;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Runtime};
static HTTP: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreRelease {
version: String,
track_id: u64,
}
// The build flavour decides this, but a Mac App Store receipt overrides it either way: a bundle
// carrying one must never self-update, whatever config it was built against.
pub fn channel<R: Runtime>(handle: &AppHandle<R>) -> &'static str {
let plugins = &handle.config().plugins.0;
if plugins.contains_key("updater") && !mas_receipt() {
"direct"
} else if mas_receipt() || plugins.contains_key("appstore") {
"appstore"
} else {
"none"
}
}
#[cfg(target_os = "macos")]
fn mas_receipt() -> bool {
let Ok(exe) = std::env::current_exe() else {
return false;
};
exe.parent()
.and_then(|macos| macos.parent())
.map(|contents| contents.join("_MASReceipt").join("receipt").exists())
.unwrap_or(false)
}
#[cfg(not(target_os = "macos"))]
fn mas_receipt() -> bool {
false
}
#[tauri::command]
pub fn update_channel(app: AppHandle) -> &'static str {
channel(&app)
}
// Apple's lookup endpoint is edge cached, so a release can take hours to show up here. The
// timestamp is the usual way past that.
#[tauri::command]
pub async fn appstore_latest(app: AppHandle) -> Result<Option<AppStoreRelease>, String> {
let bundle_id = app.config().identifier.clone();
let current = app.package_info().version.clone();
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let url = format!("https://itunes.apple.com/lookup?bundleId={bundle_id}&t={stamp}");
let body: serde_json::Value = HTTP
.get(url)
.send()
.await
.map_err(|e| e.to_string())?
.json()
.await
.map_err(|e| e.to_string())?;
let Some(result) = body["results"].get(0) else {
return Ok(None);
};
let (Some(version), Some(track_id)) = (result["version"].as_str(), result["trackId"].as_u64())
else {
return Ok(None);
};
let Ok(latest) = semver::Version::parse(version) else {
return Ok(None);
};
if latest <= current {
return Ok(None);
}
Ok(Some(AppStoreRelease { version: version.to_string(), track_id }))
}
#[tauri::command]
pub fn open_appstore(track_id: u64) -> Result<(), String> {
let url = format!("macappstore://apps.apple.com/app/id{track_id}");
tauri_plugin_opener::open_url(url, None::<&str>).map_err(|e| e.to_string())
}
+3
View File
@@ -5,5 +5,8 @@
"macOS": {
"hardenedRuntime": true
}
},
"plugins": {
"appstore": {}
}
}