Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ at the time; the last full run on the Simulator is under
| [`audioplayers_watchos`](packages/audioplayers_watchos) [![pub](https://img.shields.io/pub/v/audioplayers_watchos.svg)](https://pub.dev/packages/audioplayers_watchos) | [`audioplayers`](https://pub.dev/packages/audioplayers) | AVFoundation (`AVPlayer`) |
| [`in_app_purchase_watchos`](packages/in_app_purchase_watchos) [![pub](https://img.shields.io/pub/v/in_app_purchase_watchos.svg)](https://pub.dev/packages/in_app_purchase_watchos) | [`in_app_purchase`](https://pub.dev/packages/in_app_purchase) | StoreKit (`SKProductsRequest`, `SKPaymentQueue`) |
| [`games_services_watchos`](packages/games_services_watchos) [![pub](https://img.shields.io/pub/v/games_services_watchos.svg)](https://pub.dev/packages/games_services_watchos) | [`games_services`](https://pub.dev/packages/games_services) | GameKit (`GKLeaderboard`, `GKLocalPlayer`) — leaderboards only [†](#games-services-status) |
| [`flutter_local_notifications_watchos`](packages/flutter_local_notifications_watchos) | [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications) | UserNotifications (`UNUserNotificationCenter`), taps through the host's application delegate |
| [`firebase_core_watchos`](packages/firebase_core_watchos) [![pub](https://img.shields.io/pub/v/firebase_core_watchos.svg)](https://pub.dev/packages/firebase_core_watchos) | [`firebase_core`](https://pub.dev/packages/firebase_core) | Firebase Apple SDK (`FirebaseCore`) |
| [`firebase_auth_watchos`](packages/firebase_auth_watchos) [![pub](https://img.shields.io/pub/v/firebase_auth_watchos.svg)](https://pub.dev/packages/firebase_auth_watchos) | [`firebase_auth`](https://pub.dev/packages/firebase_auth) | Firebase Apple SDK (`FirebaseAuth`) |
| [`firebase_storage_watchos`](packages/firebase_storage_watchos) [![pub](https://img.shields.io/pub/v/firebase_storage_watchos.svg)](https://pub.dev/packages/firebase_storage_watchos) | [`firebase_storage`](https://pub.dev/packages/firebase_storage) | Firebase Apple SDK (`FirebaseStorage`) |
Expand Down
23 changes: 23 additions & 0 deletions packages/flutter_local_notifications_watchos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Dart / Flutter
.dart_tool/
build/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub/

# Written into the package by `flutter-watchos test` (GeneratedPluginRegistrant.swift)
watchos/Flutter/

# Xcode / SwiftPM (per-user, generated when watchos/Package.swift is opened)
**/.swiftpm/
**/.build/
**/xcuserdata/

# IDE
.idea/
.vscode/
*.iml

# macOS
.DS_Store
39 changes: 39 additions & 0 deletions packages/flutter_local_notifications_watchos/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## 0.0.1

First version, staged: checked on the watchOS 27.0 Simulator, not yet on a
physical watch.

* `FlutterLocalNotificationsWatchos` extends `IOSFlutterLocalNotificationsPlugin`,
which flutter_local_notifications requires on iOS-reporting platforms, and
answers every method through UNUserNotificationCenter over dart:ffi:
initialize with categories, permissions, show, zonedSchedule (calendar
triggers in the date's own time zone), periodicallyShow and
periodicallyShowWithDuration, cancel, pending and delivered lists, and the
launch tap.
* Taps reach `onDidReceiveNotificationResponse` through the flutter-watchos
host's application delegate, observed from a load-time constructor so the
launch tap is not missed. As upstream, only a tap on the notification or
on an action that opens the app is the launch tap, and a dismissal is
reported only for a notification whose details set `dismissIsolate`;
background actions and those dismissals reach
`onDidReceiveNotificationResponse`, since the watch has no background
isolate.
* As upstream 22.3.2, an error in `UNErrorDomain` from adding a request is
ignored: watchOS 27, like iOS 27, reports a missing permission that way.
* As upstream on iOS 14 and later, `presentAlert` is not read: the banner
and the list have flags of their own. `checkPermissions` reports
provisional authorization apart from `isEnabled`, and `initialize` answers
false when it asks for nothing.
* `providesAppNotificationSettings` is asked for and reported;
`criticalSoundVolume` plays the default critical sound at that volume.
* LICENSE carries Michael Bui's notice, as upstream's does.
* `DarwinNotificationDetails` without attachments no longer ends the app:
their null reached native as NSNull, which it walked as a list.
* As upstream, an error from asking for permission is not reported; the
answer is whether it was granted.
* The example's "With actions" buttons schedule a notification with an
action that opens the app, a background action, and a reported
dismissal, or one without it.
* The example's `lib/` is a small watch demo, because the upstream example
needs `flutter_timezone`; its official integration test is upstream's,
unchanged.
28 changes: 28 additions & 0 deletions packages/flutter_local_notifications_watchos/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright 2018 Michael Bui. All rights reserved.
Copyright 2026 The FlutterWatch Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
102 changes: 102 additions & 0 deletions packages/flutter_local_notifications_watchos/PORTING_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# flutter_local_notifications_watchos — porting report

## Verification status

| Aspect | Result |
|---|---|
| Implementation | ✅ Working (UserNotifications FFI) |
| watchOS capability | Partial — named sounds, typed action input and the background-isolate callback are dropped (see the README). Not yet checked on a physical watch |
| Host unit tests (`flutter test`) | ✅ pass, through the app-facing `FlutterLocalNotificationsPlugin` with the target platform set to iOS |
| Upstream integration test | ✅ passes verbatim on the watchOS 27.0 Simulator, after the permission prompt is answered |
| Internal unified demo | ○ not included yet |

Marking: ✅ full / passes · ◐ partial — reason given · ○ not applicable (no upstream test) · ✗ unsupported on watchOS.

On the watchOS 27.0 Simulator the example scheduled notifications ten
seconds out and these responses were checked by hand, after the review's
routing change:

| Case | Result |
|---|---|
| Tap on the notification, app not running | ✅ the app launches, `getNotificationAppLaunchDetails` reports the tap with its id and payload |
| "Resume" (an action that opens the app), app not running | ◐ the app launches and the action reaches `onDidReceiveNotificationResponse` with its id, action id and payload; not as the launch details, because the host delivers it about 0.2 s after Dart read them (seen in a log), so it takes the queue meant for a late launch response |
| "Skip" (a background action), app in the background | ✅ the app stays in the background; the action reaches `onDidReceiveNotificationResponse` |
| Dismissal (the ✕), with `dismissIsolate` | ✅ reaches `onDidReceiveNotificationResponse` as `notificationDismissed` |
| Dismissal (the ✕), without `dismissIsolate` | ✅ not reported, as upstream |

The unattended watchOS integration test shows a notification without
permission: on watchOS 27 the add fails in `UNErrorDomain` (code 2003,
"Source is not authorized"), which the old code turned into a
`PlatformException`; it now completes, as upstream. A notification shown
while the app is in front was added but not seen on screen; that path (the
host's `willPresent` and the presentation flags) is not yet checked.

Generated by `flutter-watchos plugin port` on 2026-10-21, then finished by
hand.

Source: `flutter_local_notifications` 22.3.2
Base platform: ios (Objective-C)

## How this differs from the iOS implementation

flutter_local_notifications is not fully federated: its iOS implementation,
`IOSFlutterLocalNotificationsPlugin`, lives in the app-facing package, and
the app-facing calls check that the platform instance *is* one before using
it. The watch reports iOS as its target platform, so this package's class
extends `IOSFlutterLocalNotificationsPlugin` and overrides every method that
would use the method channel. That makes it depend on
`flutter_local_notifications` itself, which does not depend back on it.

The iOS plugin is the notification-center delegate. On the watch the
flutter-watchos host is (`FlutterWatchOSAppDelegate`), and it rebroadcasts
"will present" and "did receive response" as NSNotifications. The native
file observes them from a load-time constructor, so the tap that launched
the app, which the host does not keep, is caught before Dart runs. As
upstream, the launch is a tap on the notification or on an action that
opens the app (kept with each notification, since the launch comes before
`initialize`), if it arrives before `initialize` and before Dart reads the
launch details; later ones queue for the response callback. Background
actions, and dismissals of notifications that set `dismissIsolate`, queue
for it too, since the watch app has no background isolate; a dismissal for
the main isolate that comes in before `initialize` is dropped, as upstream
drops it. Each notification carries its id, payload and presentation flags in
`userInfo` under keys of this package's own, and callbacks for
notifications without them (pushes, other plugins) are left alone.

The port tool flagged `UIApplication` (badge clearing and opening settings
on iOS); neither exists on watchOS.

## Example

flutter_local_notifications' own example app is a phone demo that needs
`flutter_timezone`, which has no watchOS implementation, so
`example/lib/main.dart` is a small watch demo instead.
`example/integration_test/flutter_local_notifications_test.dart` is
upstream's test, unchanged but for the formatting this repo's CI checks.
`watchos_notifications_test.dart` next to it
checks what upstream's does not reach on the watch: the permission flags,
the launch details, and scheduling, listing and cancelling against the real
notification center. The cases that need permission ask for it only with
`--dart-define=FLNW_PROMPT=true`, when someone answers the prompt; without it
they are skipped and the suite asks for nothing, so it cannot hang. That
unattended run instead shows a notification without permission, which
checks the `UNErrorDomain` case above. The demo's "With actions" and
"Actions, no dismissal" buttons schedule the `lap` notifications the routing
table above was checked with; its status line shows the last response.

## Interface coverage

Every member of `IOSFlutterLocalNotificationsPlugin` and the
`FlutterLocalNotificationsPlatform` methods it inherits, audited by hand.
✅ implemented · ◐ implemented with a limit · ✗ fails on watchOS.

| Member | watchOS |
|---|---|
| `initialize` | ◐ `onDidReceiveBackgroundNotificationResponse` is not called: background actions, and dismissals of notifications with `dismissIsolate` set, reach `onDidReceiveNotificationResponse` |
| `requestPermissions` | ✅ `providesAppNotificationSettings` (watchOS 5+) is asked for; `carPlay` has nothing to ask for. False when none of alert, sound, badge, critical and app settings is asked, as on iOS |
| `checkPermissions` | ◐ `isBadgeEnabled` is always false; `isEnabled` is for full authorization only, provisional is `isProvisionalEnabled`, as on iOS |
| `show` / `zonedSchedule` / `periodicallyShow` / `periodicallyShowWithDuration` | ◐ the default sound only; `criticalSoundVolume` plays the default critical sound at that volume, since watchOS has no named critical sound. A missing permission is ignored, as upstream 22.3.2 ignores `UNErrorDomain` errors |
| `cancel` / `cancelAll` / `cancelAllPendingNotifications` | ✅ |
| `pendingNotificationRequests` / `getActiveNotifications` | ✅ |
| `getNotificationAppLaunchDetails` | ✅ |
| `openAppNotificationSettings` | ✗ returns false |
94 changes: 94 additions & 0 deletions packages/flutter_local_notifications_watchos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# flutter_local_notifications_watchos

The watchOS implementation of [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications).

Notifications go through **UserNotifications** (`UNUserNotificationCenter`)
over dart:ffi: show one now, schedule one for a date (once or repeating), or
repeat one every interval; list, cancel, and get taps back.

> Scaffolded by [`flutter-watchos plugin port`](https://github.com/flutterwatch/flutter-watchos)
> from `flutter_local_notifications`, then implemented and verified by hand.

## Usage

This is a federated plugin implementation. Apps that already depend on
`flutter_local_notifications` and target watchOS only need to add this
package alongside it:

```yaml
dependencies:
flutter_local_notifications: ^22.3.2
flutter_local_notifications_watchos: ^0.0.1
```

The plugin registers automatically via Flutter's federated registry — no
explicit imports required from app code. The watch reports itself as iOS,
so pass `iOS:` settings and details, as on an iPhone:

```dart
await FlutterLocalNotificationsPlugin().initialize(
settings: const InitializationSettings(iOS: DarwinInitializationSettings()),
onDidReceiveNotificationResponse: (NotificationResponse r) => print(r.payload),
);
```

The app must install the flutter-watchos host's application delegate,
which owns the notification-center delegate and forwards its callbacks;
`flutter-watchos create` does (`@WKApplicationDelegateAdaptor(FlutterWatchOSAppDelegate.self)`
in `watchos/Runner/App.swift`), and the CLI warns at build time when it is
missing.

## Behaviour on watchOS

| Member | watchOS |
|---|---|
| `initialize` | supported: categories with their actions, default presentation, the permissions the settings request |
| `requestPermissions` / `checkPermissions` | supported; `isBadgeEnabled` is always false: watchOS has no badge setting |
| `show` | supported |
| `zonedSchedule` | supported: a calendar trigger in the date's own time zone, repeating by `matchDateTimeComponents` |
| `periodicallyShow` / `periodicallyShowWithDuration` | supported: a repeating interval trigger |
| `cancel` / `cancelAll` / `cancelAllPendingNotifications` | supported |
| `pendingNotificationRequests` / `getActiveNotifications` | supported, for this package's notifications |
| `getNotificationAppLaunchDetails` | supported: the tap that launched the app. A launch response that comes in only after this was asked is not lost: it reaches `onDidReceiveNotificationResponse` once `initialize` has run. On the Simulator a tap on the notification arrived in time, an action that opens the app did not, so read both |
| `onDidReceiveNotificationResponse` | taps and action taps, with the notification's id and payload; background actions too, since the watch app has no background isolate, and dismissals of notifications whose details set `dismissIsolate`. A banner shown while the app is in front closes when tapped and sends no response: watchOS does not report that tap |

Of `DarwinNotificationDetails`, these reach the notification: the
`present…` flags (for a notification arriving while the app is in front;
`presentBanner` and `presentList` decide, and `presentAlert` is not read, as
on iOS 14 and later), `subtitle`, `threadIdentifier`, `categoryIdentifier`,
`badgeNumber`, `interruptionLevel`, `criticalSoundVolume`, `dismissIsolate`
and `attachments`.

`InterruptionLevel.timeSensitive` needs the Time Sensitive Notifications
capability (`com.apple.developer.usernotifications.time-sensitive`) in the
watch app's entitlements, and critical alerts, `requestCriticalPermission`
and `criticalSoundVolume` need the Critical Alerts entitlement
(`com.apple.developer.usernotifications.critical-alerts`), which Apple grants
on request.

`flutter_timezone` has no watchOS implementation, so `tz.local` stays UTC on
the watch unless the app sets it: pass `zonedSchedule` a `TZDateTime` in the
zone you mean, for example from `tz.getLocation('Europe/Istanbul')`.

## Not supported on watchOS

| Member | On watchOS | Why |
|---|---|---|
| `DarwinNotificationDetails.sound` | the default sound plays instead | watchOS has no `UNNotificationSound soundNamed:` |
| `DarwinNotificationAction.text` | a plain action; `NotificationResponse.input` is null | the host's delegate forwards the action, not the typed text |
| `onDidReceiveBackgroundNotificationResponse` | not called; every tap reaches `onDidReceiveNotificationResponse` | the watch app handles taps in its own isolate |
| `openAppNotificationSettings` | returns false | watchOS has no settings page an app can open |

## Following upstream

This class extends upstream's concrete `IOSFlutterLocalNotificationsPlugin`,
as flutter_local_notifications requires on iOS-reporting platforms. A method
an upstream release adds to that class, a minor release included, reaches
its method channel here and throws `MissingPluginException` until this
package overrides it too. 23.0.0 is in development.

## License

BSD-3-Clause, as upstream flutter_local_notifications: `LICENSE` carries
Michael Bui's notice and The FlutterWatch Authors'. See `LICENSE` for the
full text.
19 changes: 19 additions & 0 deletions packages/flutter_local_notifications_watchos/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
language:
strict-casts: true
strict-inference: true
strict-raw-types: true

linter:
rules:
public_member_api_docs: true
14 changes: 14 additions & 0 deletions packages/flutter_local_notifications_watchos/example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.dart_tool/
.packages
build/
.flutter-plugins
.flutter-plugins-dependencies
*.iml
.idea/
.DS_Store

# watchOS / CocoaPods
watchos/Pods/
watchos/Podfile.lock
watchos/.symlinks/
watchos/Flutter/Flutter.framework
13 changes: 13 additions & 0 deletions packages/flutter_local_notifications_watchos/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# flutter_local_notifications_example

A watchOS-only example: show a notification now, schedule one ten seconds
out, and cancel them. Run it on an Apple Watch simulator with:

```sh
flutter-watchos run
```

`integration_test/flutter_local_notifications_test.dart` is
flutter_local_notifications' official integration test, unchanged. Its
first case asks for notification permission; answer the prompt on the watch
the first time it runs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
analyzer:
exclude:
- build/**
include: package:flutter_lints/flutter.yaml
Loading
Loading