Skip to content

fix(desktop): repair the Windows notification shim's false denial on read - #5600

Open
Joxyko wants to merge 1 commit into
block:mainfrom
Joxyko:fix/windows-notification-false-denial-at-startup
Open

fix(desktop): repair the Windows notification shim's false denial on read#5600
Joxyko wants to merge 1 commit into
block:mainfrom
Joxyko:fix/windows-notification-false-denial-at-startup

Conversation

@Joxyko

@Joxyko Joxyko commented Aug 11, 2026

Copy link
Copy Markdown

Addresses #2445. Closely related to #2483 -- see "Relationship to #2483" below; I am happy to close this in favour of that one.

The problem

On Windows the notification plugin's injected shim decides its permission at startup like this (guest-js/init.ts:12-18):

if (window.Notification.permission !== 'default' || __TEMPLATE_windows__) {
  return await Promise.resolve(window.Notification.permission === 'granted')
}
return await invoke('plugin:notification|is_permission_granted')

__TEMPLATE_windows__ is replaced with the literal true on Windows, so the || short-circuits and the shim compares its own freshly-initialised 'default' against 'granted' -- false -- and stamps denied. The line that would ask the backend is unreachable, and the backend would have said yes: permission_state() returns PermissionState::Granted unconditionally on desktop.

So on every launch of the Windows app, window.Notification.permission is denied and nothing in the OS is denying anything.

Why the toggle is only half of it

getDesktopNotificationPermissionState() trusts that value, so refreshPermission() reports denied at mount, and this effect in hooks.ts runs before the user touches anything:

React.useEffect(() => {
  if (settings.desktopEnabled && (permission === "denied" || permission === "unsupported")) {
    setSettings((current) => ({ ...current, desktopEnabled: false }));
  }
}, [permission, settings.desktopEnabled]);

It writes desktopEnabled: false and persists it. Measured on a real install: with alerts successfully enabled and desktopEnabled persisted as true, a clean relaunch comes back with it persisted as false.

That means fixing only the enable path leaves a Windows user re-enabling alerts after every restart.

What this changes

One place: getDesktopNotificationPermissionState() repairs the bogus value instead of reporting it.

if (
  window.Notification.permission === "denied" &&
  isTauri() &&
  isWindowsPlatform()
) {
  try {
    return await requestDesktopNotificationAccess();
  } catch {
    return window.Notification.permission;
  }
}

requestPermission() is the only call that reaches the backend, and on Windows it raises no prompt, returns Granted, and rewrites the shim's cached value -- so this is self-limiting: the next read short-circuits as granted and never requests again. There is a test for exactly that.

Repairing at the read rather than at the toggle fixes every consumer at once, including sendDesktopNotification(), which gates delivery on === "granted" and would otherwise drop notifications at boot.

  • denied stays terminal on macOS, on Linux, and on ordinary Windows web pages.
  • A denial that survives the request is still reported as denied.
  • A rejected request falls back to the current value instead of propagating; callers do not handle rejections today.
  • Adds isWindowsPlatform() to shared/lib/platform.ts, alongside the existing isMacPlatform() / isLinuxPlatform().
  • No new dependencies. No changes to hooks.ts or to notification delivery.

Verification on a real Windows 11 install

Buzz Desktop 0.5.9 release build, Windows 11 Home 10.0.26200, WebView2 151.0.4129.72, attached over CDP. Clean boot, nothing injected:

window.Notification.permission                -> "denied"
localStorage[...].desktopEnabled              -> false      (written by the effect above)

await window.Notification.requestPermission() -> "granted"
window.Notification.permission                -> "granted"

new window.Notification("Buzz", { body: ... })
ToastNotificationManager::History
  .GetHistory("xyz.block.buzz.app")           -> 3 -> 4     (real toast delivered)
CreateToastNotifier(...).Setting              -> Enabled

The OS was permissive throughout: no ToastEnabled override, no notification policy keys under HKCU or HKLM, and the app present under HKCU\...\Notifications\Settings\xyz.block.buzz.app. Full detail in this comment.

To be precise about scope: this confirms the mechanism the patch relies on, measured on a stock release build. The patched build itself is covered by the unit tests below, not by a packaged Windows run.

Tests

pnpm test -- full desktop suite. New cases in lib/desktop.test.mjs:

  • Windows Tauri repairs the shim's false denied state on read
  • the repaired state is cached, so later reads do not request again
  • a denial that survives the request is reported as denied
  • denied stays terminal outside the Windows Tauri app (Windows web, Linux Tauri, Linux web)
  • granted is returned untouched and never triggers a request

Plus shared/lib/platform.test.mjs for isWindowsPlatform() across Win32 / Win64 / Windows and MacIntel / Darwin / Linux x86_64.

Relationship to #2483

#2483 fixes the same root cause at the toggle: it retries the request inside setDesktopEnabled. That makes the toggle work, which is what #2445 reports, and its ensureDesktopNotificationPermission helper is a nicer shape than an inline condition.

The difference is where the repair happens. Because #2483 leaves the mount-time read as denied, the effect quoted above still turns the setting off on every launch, so the fix does not survive a restart. Repairing at the read covers the toggle path too, since setDesktopEnabled calls refreshPermission() first and finds granted.

The two overlap: both add isWindowsPlatform() to platform.ts, so whichever lands second needs a trivial rebase. If maintainers prefer #2483's shape, the equivalent of this PR is to call its helper from refreshPermission as well as from setDesktopEnabled -- I am glad to close this and send that instead. I did not want to push changes into someone else's open PR uninvited.

Once this lands

#2445 can close. I am running a per-machine launcher workaround for a small Windows team until then, which this makes unnecessary.

…read

On Windows the notification plugin's injected shim stamps `denied` at
startup without ever consulting the backend, whose `permission_state()`
returns `Granted` unconditionally on desktop. Its bootstrap short-circuits
on the `__TEMPLATE_windows__` literal and compares its own freshly
initialised `'default'` against `'granted'`.

`getDesktopNotificationPermissionState()` trusted that value, so the
mount-time read reported `denied` and the settings effect turned
`desktopEnabled` off and persisted it on every launch -- which is why
fixing only the enable path leaves a Windows user re-enabling alerts
after each restart.

Repair the value at the read instead of reporting it. Inside the Windows
Tauri app a `denied` reading is never a real denial, and
`requestPermission()` is the only call that reaches the backend and
rewrites the shim's cached value. It is self-limiting: the next read
short-circuits as `granted` and never requests again.

Repairing at the read rather than at the toggle also covers
`sendDesktopNotification()`, which gates delivery on `=== "granted"` and
would otherwise drop notifications at boot.

`denied` stays terminal on macOS, on Linux and on ordinary Windows web
pages, and a denial that survives the request is still reported as
`denied`.

Refs block#2445

Signed-off-by: VSCteam <144167621+Joxyko@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant