tauri-apps / tauri-apps/plugins-workspace

[notification] iOS: actionPerformed is delivered only to the webview, so an action tapped on a cold launch is silently lost

Open
#3,534 0 comments 0 reactions 0 assignees View on GitHub
platform: ios 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 iOS, `didReceive(response:)` forwards a notification action to the app **only** through the webview:

https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/notification/ios/Sources/NotificationHandler.swift

```swift
try? self.plugin?.trigger("actionPerformed", data: ReceivedNotification(...))
```

iOS calls `didReceive(response:)` within milliseconds of launching the app to handle the response. The only consumer is a listener registered from JS via `onAction`, which cannot exist until the webview has loaded and app code has run. Tauri webview events are not buffered and not replayed, so the payload is emitted into a webview with no listener and discarded.

The result: **tapping a notification action on a cold or background launch silently does nothing.** No crash, no error — the app opens and the action is simply lost. That is the normal case for a scheduled reminder, since the app is usually not running when it fires.

This is not a race that careful app code can win. Registering `onAction` as early as possible still requires the webview to have booted, which is orders of magnitude later than `didFinishLaunchingWithOptions`.

### Reproduction

1. Register an action type with `registerActionTypes`
2. `sendNotification` with `schedule: Schedule.at(...)` and that `actionTypeId`
3. Force-quit the app
4. When the notification fires, tap one of its action buttons

The app launches; `onAction` never fires.

Verified on device with the OS-level evidence that the action *was* delivered:

```
[pro.example.app] Received response to … for action keep (7)
[pro.example.app] backgroundLaunchApplication: response:
Bootstrapping app with intent background
```

…followed by no `actionPerformed` reaching JS.

### Why the Rust side cannot compensate

`PluginHandle` exposes no `add_plugin_listener` in Tauri 2.8/2.9, so the Rust half of the plugin cannot observe `trigger` either. The webview is the only consumer, which is what makes this unwinnable from outside the plugin.

### Suggested fix

Persist responses that arrive with no consumer, and let the frontend drain them once its listener exists. Two shapes that both work:

1. **Native queue + drain command** — retain responses in the plugin and expose a command the JS side calls immediately after `onAction` is attached. Smallest change, but an in-memory queue dies if iOS suspends the process before the webview boots, which is common on a background launch.
2. **Persist to disk** — append each response to a file in the app's data directory before triggering, and drain it on startup. Survives the process never becoming interactive.

I am running (2) as a vendored patch: the handler appends a JSON line to `Application Support//…` before calling `trigger`, and the app drains it once its listener is up. On-device this now applies the action about two seconds after the tap, from a fully terminated start.

Treating the live `actionPerformed` event as a wake-up that prompts a drain — rather than as the payload — is what keeps it exactly-once when both paths are available.

### Full `tauri info` output

Plugin version 2.3.3; also present on `v2` HEAD as of today. Reproduced on an iOS 26 simulator with `tauri-cli` 2.9.5.

### Additional context

Related but distinct: #3533 (the same handler force-unwraps `notificationsMap`, which crashes before this code path is reached) and #3256 (scheduled notifications fire at the wrong time outside UTC). All three have to be fixed for iOS notification actions to work end to end — that was my experience getting there.

Happy to open a PR if a maintainer has a preference between the two shapes above.

Contributor guide

Open the contributing guide

Research direction

Start with plugins/notification/ios/Sources/NotificationHandler.swift and reproduce the cold-launch flow using a scheduled notification action. Trace how actionPerformed reaches the onAction listener, then evaluate the queue or disk-persistence approaches described in the issue. Done means an action tapped after force-quitting the app is delivered to the frontend once after startup.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, rust, swift
Domain
frontend, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.