tauri-apps / tauri-apps/plugins-workspace

[bug][notification] Windows: init script always sets `window.Notification.permission` to `"denied"` at startup

Open Beginner friendly
#3,512 1 comment 0 reactions 0 assignees View on GitHub
platform: windows plugin: notification type: bug
Dominant language
Rust
Stars
1.8k
Forks
602
Avg merge
4d 14h
Merged PRs (30d)
9

Description

## Describe the bug

On Windows, the notification plugin's injected init script stamps
`window.Notification.permission = "denied"` on every page load, even though the
desktop backend unconditionally reports `PermissionState::Granted` on Windows.
Any app that reads `window.Notification.permission` (or gates sending on it)
concludes notifications are blocked.

In [`guest-js/init.ts`](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/notification/guest-js/init.ts#L12-L18):

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

By the time this runs, `window.Notification` has already been replaced by the shim
defined earlier in the same IIFE, whose `permission` is initialized to `'default'`.
With `__TEMPLATE_windows__ === true` the function therefore always returns
`'default' === 'granted'` → `false`, and the bootstrap at the bottom of the file maps
`false` → `'denied'`:

```ts
void isPermissionGranted().then(function (response) {
if (response === null) {
setNotificationPermission('default')
} else {
setNotificationPermission(response ? 'granted' : 'denied')
}
})
```

The Windows short-circuit presumably predates the shim replacement (it would make
sense reading the *native* WebView2 permission), but as written it can only ever
observe the shim's own initial value.

## Reproduction

Any Tauri v2 app on Windows with the notification plugin registered:

```js
console.log(window.Notification.permission) // "denied" — always, fresh profile
await window.Notification.requestPermission() // "granted"
console.log(window.Notification.permission) // "granted" — until next launch
```

Observed in the wild in block/buzz (block/buzz#2445), where the settings UI
reads `window.Notification.permission`, sees `"denied"`, and tells users to enable
notifications in system settings (where nothing is disabled).

## Expected behavior

On Windows the bootstrap should reflect the backend (`permission_state()` returns
`Granted`), i.e. initialize to `"granted"` — or fall through to
`invoke('plugin:notification|is_permission_granted')` like other platforms:

```ts
async function isPermissionGranted(): Promise {
if (window.Notification.permission !== 'default') {
return window.Notification.permission === 'granted'
}
return await invoke('plugin:notification|is_permission_granted')
}
```

## Platform and versions

- OS: Windows 11 (10.0.26200), WebView2 Evergreen
- tauri-plugin-notification: 2.3.3 (bug still present on `v2` branch tip)
- tauri: 2.x

Contributor guide

Open the contributing guide

Research direction

Start in plugins/notification/guest-js/init.ts, reading the Notification shim, isPermissionGranted(), and the bootstrap that calls setNotificationPermission(). Compare this flow with the backend permission_state() behavior described in the issue. Done means a fresh Windows launch reflects the backend as granted and requestPermission() continues to report the expected state.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, typescript
Domain
desktop
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.