From 6bfbb5397044130a6d662f80064b26261675f945 Mon Sep 17 00:00:00 2001 From: Karem Date: Sat, 6 Jun 2026 22:01:53 +0300 Subject: [PATCH] fix(notify): register Windows AppUserModelID so toasts appear (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On installed Windows builds, tauri-plugin-notification sets the toast's System.AppUserModel.ID to the bundle identifier (com.karem.whatrust). A WinRT toast only renders if that AUMID is registered on the system; when the registration isn't effective at toast-time — the Desktop shortcut carries no AUMID, a per-user vs per-machine path mismatch, a regenerated shortcut that dropped the property, or a raw-exe run — CreateToastNotifier /Show fails, and that error was swallowed at three layers (notify.rs, the command, and bridge.js). The result: no toast and nothing logged, exactly the "payload never reaches Action Center, no drop logs" symptom in #3. Register the AUMID at startup under HKCU\Software\Classes\AppUserModelId\ (DisplayName) and call SetCurrentProcessExplicitAppUserModelID, both Windows-only, per-user (no admin), and idempotent — so toasts render regardless of installer (NSIS/MSI) or launch path, and a shortcut that lost its AUMID self-heals on next launch. The AUMID is read from the live Tauri config identifier, the same value the plugin passes to app_id(), so the two can't drift. Also stop discarding the Result in notify::show and log it instead, so any residual toast failure is diagnosable. Co-Authored-By: Claude Opus 4.8 (1M context) --- src-tauri/Cargo.toml | 3 ++ src-tauri/src/aumid.rs | 100 ++++++++++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 7 +++ src-tauri/src/notify.rs | 7 ++- 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src-tauri/src/aumid.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6b2f2af..9ae3c22 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,9 @@ windows = { version = "0.61", features = [ "Security_Credentials_UI", # UserConsentVerifier + result/availability enums "Win32_System_WinRT", # IUserConsentVerifierInterop "Foundation", # async return-type plumbing + "Win32_Security", # SECURITY_ATTRIBUTES (referenced by RegCreateKeyExW) + "Win32_System_Registry", # RegCreateKeyExW/RegSetValueExW: register the toast AUMID in HKCU + "Win32_UI_Shell", # SetCurrentProcessExplicitAppUserModelID ] } windows-core = "0.61" # In windows-rs 0.61 IAsyncOperation moved out of windows::Foundation into the diff --git a/src-tauri/src/aumid.rs b/src-tauri/src/aumid.rs new file mode 100644 index 0000000..2746e78 --- /dev/null +++ b/src-tauri/src/aumid.rs @@ -0,0 +1,100 @@ +//! Windows-only: register the app's AppUserModelID (AUMID) at runtime so WinRT +//! toast notifications actually render for the installed app (issue #3). +//! +//! On an installed build `tauri-plugin-notification` sets the toast's +//! `System.AppUserModel.ID` to the bundle identifier (`com.karem.whatrust`). +//! Windows only renders a toast whose AUMID is *registered* on the system. The +//! NSIS/MSI installers do tag their Start-Menu shortcut with the AUMID, but +//! that registration is fragile: the Desktop shortcut carries no AUMID, a +//! per-user vs per-machine path mismatch or a regenerated shortcut can drop the +//! property, and a raw-exe run has none at all. When the AUMID is unregistered +//! the WinRT call fails *silently* — and whatRust discards the error (see +//! `notify.rs`), so no notification ever appears. +//! +//! Registering the AUMID under HKCU on every launch makes toast delivery +//! self-sufficient regardless of installer or launch path. Both steps below are +//! per-user (no admin), idempotent, and best-effort: a failure only means +//! toasts may not render, so we log and never panic or block startup. +//! +//! The AUMID is read from the live Tauri config `identifier`, i.e. the exact +//! value the notification plugin passes to `app_id()`, so the two can never +//! drift apart. + +use tauri::AppHandle; + +/// No-op on every platform except Windows. +#[allow(unused_variables)] +pub fn register(app: &AppHandle) { + #[cfg(windows)] + { + use tauri::Manager; + let config = app.config(); + let aumid = &config.identifier; + let display = config.product_name.as_deref().unwrap_or("whatRust"); + win::register(aumid, display); + } +} + +#[cfg(windows)] +mod win { + use windows::core::PCWSTR; + use windows::Win32::System::Registry::{ + RegCloseKey, RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CURRENT_USER, KEY_WRITE, + REG_OPTION_NON_VOLATILE, REG_SZ, + }; + use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; + + /// UTF-16, NUL-terminated — suitable for `PCWSTR` args and `REG_SZ` data. + fn wide(s: &str) -> Vec { + s.encode_utf16().chain(std::iter::once(0)).collect() + } + + pub fn register(aumid: &str, display_name: &str) { + // Step 1 — the registry entry is what makes the Action Center render + // the toast for an unpackaged desktop app. + if let Err(e) = write_registry(aumid, display_name) { + eprintln!("[whatrust] AUMID registry registration failed: {e:?}"); + } + // Step 2 — pin this process to the AUMID (taskbar grouping + toast + // attribution). Harmless if it fails; we just log. + let id = wide(aumid); + if let Err(e) = unsafe { SetCurrentProcessExplicitAppUserModelID(PCWSTR(id.as_ptr())) } { + eprintln!("[whatrust] SetCurrentProcessExplicitAppUserModelID failed: {e:?}"); + } + } + + /// Writes `HKCU\Software\Classes\AppUserModelId\` with a `DisplayName` + /// (REG_SZ). Idempotent — overwritten on every launch, which also self-heals + /// a Start-Menu shortcut that lost its AUMID property. + fn write_registry(aumid: &str, display_name: &str) -> windows::core::Result<()> { + let subkey = wide(&format!("Software\\Classes\\AppUserModelId\\{aumid}")); + let mut hkey = HKEY(std::ptr::null_mut()); + unsafe { + RegCreateKeyExW( + HKEY_CURRENT_USER, + PCWSTR(subkey.as_ptr()), + None, + PCWSTR::null(), + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + None, + &mut hkey, + None, + ) + .ok()?; + } + + // REG_SZ data is the NUL-terminated UTF-16 bytes of the display name. + let name = wide("DisplayName"); + let data = wide(display_name); + let bytes = unsafe { + std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(&data[..])) + }; + let set = unsafe { RegSetValueExW(hkey, PCWSTR(name.as_ptr()), None, REG_SZ, Some(bytes)) }; + // Always close the key, then surface any set error. + unsafe { + let _ = RegCloseKey(hkey); + } + set.ok() + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 234e413..fcb2bb8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ mod settings; mod tray; mod commands; mod notify; +mod aumid; use tauri::Manager; @@ -92,6 +93,12 @@ pub fn run() { ]) .setup(|app| { let handle = app.handle(); + + // Windows: register our AppUserModelID so WinRT toast notifications + // actually render for the installed app (no-op elsewhere). Must run + // before any account window can fire a notification. See aumid.rs. + aumid::register(handle); + let s = settings::load(handle); let args: Vec = std::env::args().collect(); let start_hidden = s.start_minimized || args.iter().any(|a| a == "--minimized"); diff --git a/src-tauri/src/notify.rs b/src-tauri/src/notify.rs index a7c3fdf..dd9ee91 100644 --- a/src-tauri/src/notify.rs +++ b/src-tauri/src/notify.rs @@ -2,5 +2,10 @@ use tauri::AppHandle; use tauri_plugin_notification::NotificationExt; pub fn show(app: &AppHandle, title: &str, body: &str) { - let _ = app.notification().builder().title(title).body(body).show(); + // Don't silently swallow failures: on Windows a toast can fail (e.g. an + // unregistered AppUserModelID — see aumid.rs) and the only signal is this + // Result. Logging it makes such failures diagnosable from the console. + if let Err(e) = app.notification().builder().title(title).body(body).show() { + eprintln!("[whatrust] failed to show notification: {e:?}"); + } }