Repository navigation
Add flutter_local_notifications_watchos: local notifications on the watch - #9
iamoguzhanarslantas wants to merge 6 commits into
Conversation
… watch flutter_local_notifications checks that the platform instance is an IOSFlutterLocalNotificationsPlugin on iOS-reporting platforms, which the watch is, so this class extends it and overrides every method that would use the method channel, answering through UNUserNotificationCenter over dart:ffi: initialize with categories and default presentation, requestPermissions and checkPermissions, show, zonedSchedule (calendar triggers in the date's own time zone, repeating by matchDateTimeComponents), periodicallyShow and periodicallyShowWithDuration, cancel, pending and delivered lists, and the launch tap. The flutter-watchos host owns the notification-center delegate and rebroadcasts its callbacks; the native file observes them from a load-time constructor, so the launch tap, which the host does not keep, is caught. Each notification carries its id, payload and presentation flags in userInfo, and callbacks for anyone else's notifications are left alone. Named sounds, typed action input and the background-isolate callback have no watchOS path and are documented as such. The upstream example needs flutter_timezone, which has no watchOS implementation, so the example's lib/ is a small watch demo; the official integration test is upstream's, unchanged. The root README lists the package, tool/upstream_versions.yaml gains flutter_local_notifications 22.3.2, and the word allow-list gains the package's C free(). Checked: 13 host tests through the app-facing plugin with the target platform set to iOS; the official integration test on the watchOS 27.0 Simulator after answering the permission prompt; and the example there scheduling a notification, showing it on the watch face in the background, and delivering the tap with its id and payload. A notification arriving while the app is in front was not seen on screen, and nothing is checked on a physical watch yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ags that read true A C comparison is an int, so `@(launch != nil)` and the checkPermissions flags reached JSON as 1, which Dart's `== true` reads as false: a launch tap was never reported and every alert setting read as off. They are cast to BOOL now. A launch tap that comes in after Dart has read the launch details queues for the response callback instead of being dropped, since the host posts it from the main thread while Dart runs on its own. A new integration test reads the flags, the launch details, and schedule/list/cancel against the real notification center; it fails on the old code. Checked on the Simulator: cold launch from a notification reports it, and a tap while the app runs reaches the callback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st as CI checks it Its content is upstream's; only the layout changes, so that CI's dart format step passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MAUstaoglu
left a comment
There was a problem hiding this comment.
Thank you, this is a careful port with good coverage (Simulator-tested, as the description says). Before merging:
Must-fix
LICENSE:1names only FlutterWatch. Sibling packages keep their upstream's notice, and this one ships upstream code and tests, so please add Michael Bui's BSD-3 notice.watchos/Classes/flutter_local_notifications_watchos_ffi.m:501-502: upstream 22.3.2 ignoresUNErrorDomainerrors when adding a request, because iOS 27 reports a missing permission that way. Here they become aPlatformException, soshow()without permission likely throws on watchOS 27. Please match upstream._ffi.m:208-210andlib/flutter_local_notifications_watchos.dart:140: any response that arrives beforeinitialize, including dismissals and background actions, becomes the launch tap. Dismissals are also always reported, while upstream reports them only whendismissIsolateis set, and that setting never reaches native here.example/integration_test/watchos_notifications_test.dart:25-29:setUpAllrequests permission, so the suite blocks on the prompt. Please gate it as AUTHORING §2c describes.
Nice-to-have
_ffi.m::169and:172hide banner and list whenpresentAlertis false; upstream ignores that flag on iOS 14+;:463-464counts provisional as enabled;:446returns true when nothing is requested, where upstream returns false.
PORTING_REPORT.md:71:providesAppNotificationSettingsexists on watchOS 5+, andcriticalSoundVolumeis dropped without a note.- README: mention the time-sensitive and critical-alert entitlements, and that
tz.localstays UTC on the watch. - Extending upstream's concrete iOS class means a method added in an upstream minor release will throw
MissingPluginException; 23.0.0 is at dev.4. Worth a note. - Allow-list:
free(is justified, since it is a C function. Please keep the list alphabetical, and fix the reason text: the export is_release.
We'll start the CI run after your update. Thanks!
Posted from the wrong account; superseded by the maintainer review.
From the review of PR flutterwatch#9: * LICENSE carries Michael Bui's BSD-3-Clause notice, as upstream's does, next to The FlutterWatch Authors'; the README says so. * An error in UNErrorDomain from adding a request is ignored, as upstream 22.3.2 ignores it: watchOS 27, like iOS 27, reports a missing permission that way, so show() without permission threw. * Only a tap on the notification, or on an action that opens the app, is the launch tap, as upstream. A dismissal is reported only for a notification whose details set dismissIsolate, which now reaches native in userInfo, and one for the main isolate that comes in before initialize is dropped, as upstream drops it. Background actions and the other dismissals reach onDidReceiveNotificationResponse once initialize has run. Which actions open the app is kept with each notification, since a launch response arrives before initialize. * The watchOS integration test asks for permission only with --dart-define=FLNW_PROMPT=true; without it the cases that need permission are skipped and the suite cannot hang on the prompt. * As upstream on iOS 14+: presentAlert is not read for the banner and the list; checkPermissions' isEnabled is full authorization only; initialize and requestPermissions answer false when they ask for nothing. * providesAppNotificationSettings (watchOS 5+) is asked for and reported, and criticalSoundVolume plays the default critical sound at that volume. * README: the Time Sensitive and Critical Alerts entitlements, tz.local staying UTC on the watch, and what extending upstream's concrete class means for methods a later upstream release adds. * The word allow-list entry is in alphabetical order, and its reason names the _release export. Checked: 15 host tests (2 new); on the watchOS 27.0 Simulator the watchOS integration test without the define (1 passed, 3 skipped, no prompt) and with it (4 passed after allowing the prompt), and upstream's integration test (5 passed after allowing it). Taps from the watch face were not run again after the routing change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tool/upstream_versions.yaml: main's new Firebase majors, and this branch's flutter_local_notifications line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. Pushed 7d903c9: Must-fix
Nice-to-have
Checked: 15 host tests. On the watchOS 27.0 Simulator:
I did not re-run taps from the watch face after the routing change. 🤖 Generated with Claude Code |
MAUstaoglu
left a comment
There was a problem hiding this comment.
Thanks! All four must-fixes and every nice-to-have are in, and they match upstream 22.3.2. Before merging:
-
A test for the UNErrorDomain fix: a
watchos_notifications_testcase whereshow()completes without permission, gated asskip: _prompt. It runs unattended and checks the watchOS 27 claim. -
A Simulator pass over the new response routing:
- a tap and a foreground action at cold launch;
- a background action;
- a dismissal, with and without
dismissIsolate.
Please note the results in PORTING_REPORT.
-
Docs: update the native header's JSON contract (
_ffi.h:41-55lacksprovidesAppNotificationSettingsandisProvidesAppNotificationSettingsEnabled; the host fake follows that header), and PORTING_REPORT lines 41–43 and 15–19, which still describe the old launch rule.
Optional: requestAuthorization errors throw here, while upstream ignores them and returns granted.
|
On the example, for the record. AUTHORING.md §3 asks for upstream's own example, unchanged. Here upstream's example cannot launch on the watch: That makes your watch demo the right call for this package, and the report's Example section says why. The official integration test is upstream's, as the rule asks. Nothing to change here beyond the round-2 items. |
From the second review of PR flutterwatch#9: * The watchOS integration test shows a notification without permission on an unattended run (skipped when someone answers the prompt, or when notifications are already allowed). On the watchOS 27.0 Simulator the add fails in UNErrorDomain, code 2003, "Source is not authorized": the test fails on the old error handling, which threw a PlatformException, and passes now. * The test passes iOS details without attachments, and that found a crash that predates the review: their null reached native as NSNull, which _content walked as a list, so show() with any DarwinNotificationDetails ended the app. The attachments, categories and actions are only walked when they are lists. * The response routing was run by hand on the Simulator with new demo buttons, a "lap" category with an action that opens the app, a background action and a reported dismissal. A tap at cold launch is the launch details; the opening action at cold launch reaches onDidReceiveNotificationResponse instead, since the host delivers it about 0.2 s after Dart read the launch details; a background action and a dismissal with dismissIsolate reach the callback; a dismissal without it is not reported. PORTING_REPORT has the table, and the README says to read both the launch details and the callback. * The native header's JSON contract names providesAppNotificationSettings, isProvidesAppNotificationSettingsEnabled, dismissIsolate and the new launch and queue rules; PORTING_REPORT no longer describes the old launch rule. * As upstream, an error from requestAuthorization is not reported; the answer is whether it was granted. Checked: 15 host tests; the watchOS integration test unattended from a fresh install (2 passed, 3 skipped) and with the prompt allowed (4 passed, 1 skipped); the five routing cases above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks! Pushed ced2c4c:
Optional: done. A Checked:
And thanks for the note on the example. 🤖 Generated with Claude Code |
Summary
Adds
flutter_local_notifications_watchos0.0.1, the watchOS implementation offlutter_local_notifications, over UserNotifications and dart:ffi.IOSFlutterLocalNotificationsPlugin. This package's class therefore extends it and overrides every method that would use the method channel. It depends onflutter_local_notificationsitself, which does not depend back on it.initialize(categories with their actions, default presentation, the permissions the settings ask for),requestPermissions/checkPermissions,show,zonedSchedule(a calendar trigger in the date's own time zone, repeating bymatchDateTimeComponents),periodicallyShow/periodicallyShowWithDuration, cancelling, the pending and delivered lists,getNotificationAppLaunchDetails, and taps throughonDidReceiveNotificationResponse.FlutterWatchOSAppDelegateowns the notification-center delegate and rebroadcasts its callbacks. The native file observes them from a load-time constructor, so the tap that launched the app is caught before Dart runs. Each notification carries its id, payload and presentation flags inuserInfounder keys of this package's own, and callbacks for anyone else's notifications (pushes, other plugins) are left alone.onDidReceiveBackgroundNotificationResponse, andopenAppNotificationSettings(returns false).isBadgeEnabledis always false.tool/upstream_versions.yamlgetsflutter_local_notifications; the word allow-list gets the package's Cfree(.Verified
On the watchOS 27.0 Simulator (Apple Watch Series 12, 46 mm):
example/integration_test/flutter_local_notifications_test.dartpasses, unchanged but for the formatting CI checks, after the permission prompt is allowed (flutter-watchos testreinstalls the app, so the prompt comes back each run).example/integration_test/watchos_notifications_test.dart(4 tests) passes: the permission flags, the launch details, and scheduling, listing and cancelling against the real notification center. It fails on the first commit's code (see the second commit).onDidReceiveNotificationResponsewith its id and payload. A cold launch from a notification is reported bygetNotificationAppLaunchDetails.ffiSymbolsentry in the export table.Off the watch:
FlutterLocalNotificationsPlugin, with the target platform set to iOS.tool/check,dart formatas CI runs it, andpub publish --dry-runare clean.Not verified
willPresentpath and the fivepresent…flags are unchecked.Notes for review
@WKApplicationDelegateAdaptor(FlutterWatchOSAppDelegate.self)), asflutter-watchos createdoes.example/lib/is a small watch demo, because upstream's example needsflutter_timezone, which has no watchOS implementation.@(a == b)boxed an int, so the launch flag and every permission flag reached Dart as 1 and read false) with the new integration test; and the formatting of upstream's test.tool/upstream_versions.yaml, so whichever merges first leaves the others a conflict there; I will rebase each one after a merge.🤖 Generated with Claude Code