diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 85c7093b5..8b0641fd5 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -231,6 +231,45 @@ jobs:
cd f\e
dart pub get
dart run msix:create
+ build_example_web_stable:
+ name: Build Web example app (stable channel)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ cache: true
+ cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
+ - name: Install Tools
+ run: |
+ dart pub global activate melos
+ melos bootstrap
+ - name: Build
+ run: |
+ cd flutter_local_notifications/example
+ dart pub get
+ flutter build web
+ build_example_web_3_22:
+ name: Build Web example app (3.22)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ flutter-version: 3.22.0
+ cache: true
+ cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
+ - name: Install Tools
+ run: |
+ dart pub global activate melos
+ melos bootstrap
+ - name: Build
+ run: |
+ cd flutter_local_notifications/example
+ dart pub get
+ flutter build web
unit_tests_dart:
name: Run all unit tests except for Windows (Dart)
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index acd67a5a8..cd766ff28 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@ This repository consists hosts the following packages
- [`flutter_local_notifications_platform_interface`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface): the code for the common platform interface
- [`flutter_local_notifications_linux`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_linux): the Linux implementation of [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications)
- [`flutter_local_notifications_windows`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_windows): the Windows implementation of [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications).
+- [`flutter_local_notifications_web`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_web): the Web implementation of [`flutter_local_notifications`](https://pub.dev/packages/flutter_local_notifications).
These can be found in the corresponding directories within the same name. Most developers are likely here as they are looking to use the `flutter_local_notifications` plugin. There is a readme file within each directory with more information.
diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md
index b3eca84eb..1ae90ffc0 100644
--- a/flutter_local_notifications/README.md
+++ b/flutter_local_notifications/README.md
@@ -61,6 +61,7 @@ A cross platform plugin for displaying local notifications.
* **macOS** Uses the [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework)
* **Linux**. Uses the [Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/)
* **Windows** Uses the [C++/WinRT](https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/) implementation of [Toast Notifications](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-notifications-overview)
+* **Web** Uses the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API)
Note: the plugin requires Flutter SDK 3.22 at a minimum. The list of support platforms for Flutter 3.22 itself can be found [here](https://github.com/flutter/website/blob/4fa26a1e909a2243fa18e4d101192bb5d400fcf2/src/_data/platforms.yml)
@@ -165,19 +166,28 @@ The `onDidReceiveNotificationResponse` callback runs on the main isolate of the
- Windows does not support repeating notifications, so [`periodicallyShow`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/periodicallyShow.html) and [`periodicallyShowWithDuration`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/periodicallyShowWithDuration.html) will throw `UnsupportedError`s.
- Windows only allows apps with package identity to retrieve previously shown notifications. This means that on an app that was not packaged as an [MSIX](https://learn.microsoft.com/en-us/windows/msix/overview) installer, [`cancel`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/cancel.html) does nothing and [`getActiveNotifications`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/getActiveNotifications.html) will return an empty list. To package your app as an MSIX, see [`package:msix`](https://pub.dev/packages/msix) and the `msix` section in [the example's `pubspec.yaml`](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/pubspec.yaml).
+### Web limitations
+
+- If you are debugging with `flutter run -d chrome`, you will see notifications but they will not respond to being clicked! This is due to the private debugging window that Flutter opens, and they will respond properly in release builds. To test notification handlers, make sure to use `flutter run -d web-server`. If you find that hot reload is broken with `-d web-server`, try to test as much as possible with `-d chrome`.
+- **You must request notification permissions before showing notifications -- but only in response to a user interaction.** If you try to request permissions automatically, like on loading a page, not only may your request be automatically denied, but the browser may deem your site as abusive and no longer show any more prompts to the user, and just block everything going forward.
+- Notification actions are supported by Chrome and Edge, but not Firefox or Safari. They may catch up soon, but text input fields use a standards _proposal_, not an accepted standard, and so may only work on Chrome for a while.
+
+- Browsers don't support scheduled or repeating notifications, and browsers on Android do not support custom vibration.
+
### Notification payload
Due to some limitations on iOS with how it treats null values in dictionaries, a null notification payload is coalesced to an empty string behind the scenes on all platforms for consistency.
## 📷 Screenshots
-| Platform | Screenshot |
-| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Android | |
-| iOS | |
-| macOS | |
+| Platform | Screenshot |
+| -------- | ------------------------------------------------------------ |
+| Android | |
+| iOS | |
+| macOS | |
| Linux | |
-| Windows | |
+| Windows | |
+| Web | |
## 👏 Acknowledgements
@@ -186,7 +196,8 @@ Due to some limitations on iOS with how it treats null values in dictionaries, a
* [Jeff Scaturro](https://github.com/JeffScaturro) for submitting the PR to fix the iOS issue around showing daily and weekly notifications and migrating the plugin to AndroidX
* [Ian Cavanaugh](https://github.com/icavanaugh95) for helping create a sample to reproduce the problem reported in [issue #88](https://github.com/MaikuB/flutter_local_notifications/issues/88)
* [Zhang Jing](https://github.com/byrdkm17) for adding 'ticker' support for Android notifications
-* [Kenneth](https://github.com/kennethnym), [lightrabbit](https://github.com/lightrabbit), and [Levi Lesches](https://github.com/Levi-Lesches) for adding Windows support
+* [Levi Lesches](https://github.com/Levi-Lesches) for adding Windows and Web support
+* [Kenneth](https://github.com/kennethnym) and [lightrabbit](https://github.com/lightrabbit) for their contributions to Windows support
* ...and everyone else for their contributions. They are greatly appreciated
## 🔧 Android Setup
@@ -473,6 +484,22 @@ By design, iOS applications *do not* display notifications while the app is in t
For iOS 10+, use the presentation options to control the behaviour for when a notification is triggered while the app is in the foreground. The default settings of the plugin will configure these such that a notification will be displayed when the app is in the foreground.
+## Web Setup
+
+No modifications to the HTML or JavaScript are necessary. But you must make sure to request permissions at runtime properly!
+
+```dart
+final plugin = FlutterLocalNotificationsPlugin();
+await plugin.initialize();
+
+if (!plugin.hasPermission) {
+ // IMPORTANT: Only call this after a button press!
+ await plugin.requestNotificationsPermission();
+}
+```
+
+Everything else works like the other platforms.
+
## ❓ Usage
Before going on to copy-paste the code snippets in this section, double-check you have configured your application correctly.
diff --git a/flutter_local_notifications/example/.gitignore b/flutter_local_notifications/example/.gitignore
index d16b8eba3..a981f586a 100644
--- a/flutter_local_notifications/example/.gitignore
+++ b/flutter_local_notifications/example/.gitignore
@@ -30,8 +30,5 @@
.pub/
/build/
-# Web related
-lib/generated_plugin_registrant.dart
-
# Exceptions to above rules.
-!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
\ No newline at end of file
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/flutter_local_notifications/example/.metadata b/flutter_local_notifications/example/.metadata
index aa7708c21..b603e3b8b 100644
--- a/flutter_local_notifications/example/.metadata
+++ b/flutter_local_notifications/example/.metadata
@@ -1,11 +1,11 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
-# This file should be version controlled.
+# This file should be version controlled and should not be manually edited.
version:
- revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
- channel: stable
+ revision: "603104015dd692ea3403755b55d07813d5cf8965"
+ channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
- base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
- - platform: macos
- create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
- base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8
+ create_revision: 603104015dd692ea3403755b55d07813d5cf8965
+ base_revision: 603104015dd692ea3403755b55d07813d5cf8965
+ - platform: web
+ create_revision: 603104015dd692ea3403755b55d07813d5cf8965
+ base_revision: 603104015dd692ea3403755b55d07813d5cf8965
# User provided section
diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart
index 6167847a0..cef457a34 100644
--- a/flutter_local_notifications/example/lib/main.dart
+++ b/flutter_local_notifications/example/lib/main.dart
@@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
+import 'dart:io' hide Platform;
// ignore: unnecessary_import
import 'dart:typed_data';
@@ -15,12 +15,17 @@ import 'package:image/image.dart' as image;
import 'package:path_provider/path_provider.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
+import 'package:universal_platform/universal_platform.dart';
import 'padded_button.dart';
import 'plugin.dart';
import 'repeating.dart' as repeating;
+import 'web_stub.dart' if (dart.library.js_interop) 'web.dart';
+
import 'windows.dart' as windows;
+typedef Platform = UniversalPlatform;
+
/// Streams are created so that app can respond to notification-related events
/// since the plugin is initialized in the `main` function
final StreamController selectNotificationStream =
@@ -47,8 +52,6 @@ class ReceivedNotification {
final Map? data;
}
-String? selectedNotificationPayload;
-
/// A notification action which triggers a url launch event
const String urlLaunchActionId = 'id_1';
@@ -61,6 +64,8 @@ const String darwinNotificationCategoryText = 'textCategory';
/// Defines a iOS/MacOS notification category for plain actions.
const String darwinNotificationCategoryPlain = 'plainCategory';
+bool? hasPermission;
+
@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) {
// ignore: avoid_print
@@ -163,14 +168,21 @@ Future main() async {
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
- final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb &&
- Platform.isLinux
+ if (kIsWeb) {
+ hasPermission = flutterLocalNotificationsPlugin
+ .resolvePlatformSpecificImplementation<
+ WebFlutterLocalNotificationsPlugin>()
+ ?.hasPermission;
+ }
+
+ final NotificationAppLaunchDetails? notificationAppLaunchDetails = Platform
+ .isLinux
? null
: await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
String initialRoute = HomePage.routeName;
+ NotificationResponse? initialNotification;
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
- selectedNotificationPayload =
- notificationAppLaunchDetails!.notificationResponse?.payload;
+ initialNotification = notificationAppLaunchDetails!.notificationResponse;
initialRoute = SecondPage.routeName;
}
@@ -179,7 +191,9 @@ Future main() async {
initialRoute: initialRoute,
routes: {
HomePage.routeName: (_) => HomePage(notificationAppLaunchDetails),
- SecondPage.routeName: (_) => SecondPage(selectedNotificationPayload)
+ SecondPage.routeName: (_) => SecondPage.withResponse(
+ initialNotification,
+ )
},
),
);
@@ -270,6 +284,11 @@ class _HomePageState extends State {
setState(() {
_notificationsEnabled = grantedNotificationPermission ?? false;
});
+ } else if (kIsWeb) {
+ await flutterLocalNotificationsPlugin
+ .resolvePlatformSpecificImplementation<
+ WebFlutterLocalNotificationsPlugin>()
+ ?.requestNotificationsPermission();
}
}
@@ -307,8 +326,7 @@ class _HomePageState extends State {
selectNotificationStream.stream
.listen((NotificationResponse? response) async {
await Navigator.of(context).push(MaterialPageRoute(
- builder: (BuildContext context) =>
- SecondPage(response?.payload, data: response?.data),
+ builder: (BuildContext context) => SecondPage.withResponse(response),
));
});
}
@@ -381,13 +399,14 @@ class _HomePageState extends State {
await _showNotificationWithNoBody();
},
),
- PaddedElevatedButton(
- buttonText: 'Show notification with custom sound',
- onPressed: () async {
- await _showNotificationCustomSound();
- },
- ),
- if (kIsWeb || !Platform.isLinux) ...[
+ if (Platform.isAndroid)
+ PaddedElevatedButton(
+ buttonText: 'Show notification with custom sound',
+ onPressed: () async {
+ await _showNotificationCustomSound();
+ },
+ ),
+ if (!kIsWeb && !Platform.isLinux) ...[
PaddedElevatedButton(
buttonText:
'Schedule notification to appear in 5 seconds '
@@ -410,26 +429,28 @@ class _HomePageState extends State {
await _checkPendingNotificationRequests();
},
),
+ ],
+ if (!Platform.isLinux)
PaddedElevatedButton(
buttonText: 'Get active notifications',
onPressed: () async {
await _getActiveNotifications();
},
),
- ],
PaddedElevatedButton(
buttonText: 'Show notification from silent channel',
onPressed: () async {
await _showNotificationWithNoSound();
},
),
- PaddedElevatedButton(
- buttonText:
- 'Show silent notification from channel with sound',
- onPressed: () async {
- await _showNotificationSilently();
- },
- ),
+ if (!kIsWeb)
+ PaddedElevatedButton(
+ buttonText:
+ 'Show silent notification from channel with sound',
+ onPressed: () async {
+ await _showNotificationSilently();
+ },
+ ),
PaddedElevatedButton(
buttonText: 'Cancel latest notification',
onPressed: () async {
@@ -449,7 +470,8 @@ class _HomePageState extends State {
await _cancelAllPendingNotifications();
},
),
- if (!Platform.isWindows) ...repeating.examples(context),
+ if (!Platform.isWindows && !kIsWeb)
+ ...repeating.examples(context),
const Divider(),
const Text(
'Notifications with actions',
@@ -476,7 +498,7 @@ class _HomePageState extends State {
await _showNotificationWithTextAction();
},
),
- if (!Platform.isLinux)
+ if (!Platform.isLinux && !kIsWeb)
PaddedElevatedButton(
buttonText: 'Show notification with text choice',
onPressed: () async {
@@ -1029,6 +1051,7 @@ class _HomePageState extends State {
),
],
if (!kIsWeb && Platform.isWindows) ...windows.examples(),
+ if (kIsWeb) ...webExamples(hasPermission),
],
),
),
@@ -1110,24 +1133,35 @@ class _HomePageState extends State {
],
);
- final WindowsNotificationDetails windowsNotificationsDetails =
- WindowsNotificationDetails(
- subtitle: 'Click the three dots for another button',
- actions: [
- const WindowsAction(
- content: 'Text',
- arguments: 'text',
- ),
- WindowsAction(
- content: 'Image',
- arguments: 'image',
- imageUri: WindowsImage.getAssetUri('icons/coworker.png'),
- ),
- const WindowsAction(
- content: 'Context',
- arguments: 'context',
- placement: WindowsActionPlacement.contextMenu,
- ),
+ final WindowsNotificationDetails? windowsNotificationsDetails = kIsWeb
+ ? null
+ : WindowsNotificationDetails(
+ subtitle: 'Click the three dots for another button',
+ actions: [
+ const WindowsAction(
+ content: 'Text',
+ arguments: 'text',
+ ),
+ WindowsAction(
+ content: 'Image',
+ arguments: 'image',
+ imageUri: WindowsImage.getAssetUri('icons/coworker.png'),
+ ),
+ const WindowsAction(
+ content: 'Context',
+ arguments: 'context',
+ placement: WindowsActionPlacement.contextMenu,
+ ),
+ ],
+ );
+
+ final WebNotificationDetails webDetails = WebNotificationDetails(
+ actions: [
+ const WebNotificationAction(action: 'Text', title: 'text'),
+ WebNotificationAction(
+ action: 'Image',
+ title: 'image',
+ icon: Uri.parse('https://picsum.photos/200')),
],
);
@@ -1137,6 +1171,7 @@ class _HomePageState extends State {
macOS: macOSNotificationDetails,
linux: linuxNotificationDetails,
windows: windowsNotificationsDetails,
+ web: webDetails,
);
await flutterLocalNotificationsPlugin.show(
id++, 'plain title', 'plain body', notificationDetails,
@@ -1184,11 +1219,21 @@ class _HomePageState extends State {
],
);
+ const WebNotificationDetails webDetails = WebNotificationDetails(
+ actions: [
+ WebNotificationAction(
+ action: 'text',
+ title: 'Send a reply',
+ type: WebNotificationActionType.textInput),
+ ],
+ );
+
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
macOS: darwinNotificationDetails,
windows: windowsNotificationDetails,
+ web: webDetails,
);
await flutterLocalNotificationsPlugin.show(id++, 'Text Input Notification',
@@ -1479,11 +1524,14 @@ class _HomePageState extends State {
);
final WindowsNotificationDetails windowsDetails =
WindowsNotificationDetails(audio: WindowsNotificationAudio.silent());
+ const WebNotificationDetails webDetails =
+ WebNotificationDetails(isSilent: true);
final NotificationDetails notificationDetails = NotificationDetails(
windows: windowsDetails,
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
- macOS: darwinNotificationDetails);
+ macOS: darwinNotificationDetails,
+ web: webDetails);
await flutterLocalNotificationsPlugin.show(
id++, 'silent title', 'silent body', notificationDetails);
}
@@ -1899,6 +1947,7 @@ class _HomePageState extends State {
Future _cancelAllNotifications() async {
await flutterLocalNotificationsPlugin.cancelAll();
+ id = 0;
}
Future _cancelAllPendingNotifications() async {
@@ -3053,32 +3102,17 @@ Future getLinuxCapabilities() =>
.getCapabilities();
class SecondPage extends StatefulWidget {
- const SecondPage(
- this.payload, {
- this.data,
- Key? key,
- }) : super(key: key);
+ const SecondPage.withResponse(this.response, {Key? key}) : super(key: key);
static const String routeName = '/secondPage';
- final String? payload;
- final Map? data;
+ final NotificationResponse? response;
@override
State createState() => SecondPageState();
}
class SecondPageState extends State {
- String? _payload;
- Map? _data;
-
- @override
- void initState() {
- super.initState();
- _payload = widget.payload;
- _data = widget.data;
- }
-
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
@@ -3088,8 +3122,11 @@ class SecondPageState extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- Text('payload ${_payload ?? ''}'),
- Text('data ${_data ?? ''}'),
+ Text('Notification ID: ${widget.response?.id}'),
+ Text('Payload: ${widget.response?.payload}'),
+ Text('Action ID: ${widget.response?.actionId}'),
+ Text('Input: ${widget.response?.input}'),
+ Text('Data (Windows only): ${widget.response?.data}'),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
diff --git a/flutter_local_notifications/example/lib/web.dart b/flutter_local_notifications/example/lib/web.dart
new file mode 100644
index 000000000..2cd9cb344
--- /dev/null
+++ b/flutter_local_notifications/example/lib/web.dart
@@ -0,0 +1,122 @@
+import 'dart:ui_web';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+import 'padded_button.dart';
+import 'plugin.dart';
+
+List webExamples(bool? hasPermission) => [
+ const Text(
+ 'Windows-specific examples',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ if (hasPermission ?? false)
+ const Text('Notification permissions granted')
+ else
+ const Text(
+ 'WARNING: The user did not grant permissions to show notifications',
+ ),
+ const PaddedElevatedButton(
+ buttonText: 'Notification with images',
+ onPressed: _showImages,
+ ),
+ const PaddedElevatedButton(
+ buttonText: 'Right-to-left notification',
+ onPressed: _showRtl,
+ ),
+ const PaddedElevatedButton(
+ buttonText: 'Require interaction',
+ onPressed: _showInteraction,
+ ),
+ const PaddedElevatedButton(
+ buttonText: 'Replace notification',
+ onPressed: _showRenotify,
+ ),
+ const PaddedElevatedButton(
+ buttonText: 'Notification with an older timestamp',
+ onPressed: _showTimestamp,
+ ),
+ const PaddedElevatedButton(
+ buttonText: 'Notification with vibration',
+ onPressed: _showVibrate,
+ ),
+ ];
+
+final Uri imageUrl = Uri.parse('https://picsum.photos/500');
+final Uri iconUrl = Uri.parse('https://picsum.photos/100');
+final Uri badgeUrl =
+ Uri.parse(AssetManager().getAssetUrl('icons/app_icon_density.png'));
+
+Future showDetails(WebNotificationDetails details) =>
+ flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is a title',
+ 'This is a body',
+ NotificationDetails(web: details),
+ );
+
+void _showTimestamp() =>
+ showDetails(WebNotificationDetails(timestamp: DateTime(2020, 1, 5)));
+
+Future _showRenotify() async {
+ final int id2 = id++;
+ await flutterLocalNotificationsPlugin.show(
+ id2,
+ 'This is the original notification!',
+ 'Wait for it...',
+ const NotificationDetails(web: WebNotificationDetails(renotify: true)),
+ );
+ await Future.delayed(const Duration(seconds: 1));
+ await flutterLocalNotificationsPlugin.show(
+ id2,
+ 'This is the replacement!',
+ 'Notice there is no animation!',
+ null,
+ );
+}
+
+void _showInteraction() =>
+ showDetails(const WebNotificationDetails(requireInteraction: true));
+
+void _showImages() => showDetails(WebNotificationDetails(
+ imageUrl: imageUrl,
+ iconUrl: iconUrl,
+ badgeUrl: iconUrl,
+ ));
+
+void _showRtl() => flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is in a right-to-left language',
+ 'שלום חביבי!',
+ const NotificationDetails(
+ web: WebNotificationDetails(
+ direction: WebNotificationDirection.rightToLeft,
+ ),
+ ),
+ );
+
+// Star wars theme!
+// See: https://tests.peter.sh/notification-generator/
+final List vibrationPattern = [
+ 500,
+ 110,
+ 500,
+ 110,
+ 450,
+ 110,
+ 200,
+ 110,
+ 170,
+ 40,
+ 450,
+ 110,
+ 200,
+ 110,
+ 170,
+ 40,
+ 500,
+];
+void _showVibrate() => showDetails(WebNotificationDetails(
+ vibrationPattern: vibrationPattern,
+ ));
diff --git a/flutter_local_notifications/example/lib/web_stub.dart b/flutter_local_notifications/example/lib/web_stub.dart
new file mode 100644
index 000000000..0394ea46d
--- /dev/null
+++ b/flutter_local_notifications/example/lib/web_stub.dart
@@ -0,0 +1,3 @@
+import 'package:flutter/widgets.dart';
+
+List webExamples(bool? hasPermission) => [];
diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml
index 21edacde6..f8f86216e 100644
--- a/flutter_local_notifications/example/pubspec.yaml
+++ b/flutter_local_notifications/example/pubspec.yaml
@@ -1,6 +1,7 @@
name: flutter_local_notifications_example
description: Demonstrates how to use the flutter_local_notifications plugin.
publish_to: none
+version: 1.0.0+0
dependencies:
cupertino_icons: ^1.0.8
diff --git a/flutter_local_notifications/example/web/favicon.png b/flutter_local_notifications/example/web/favicon.png
new file mode 100644
index 000000000..8aaa46ac1
Binary files /dev/null and b/flutter_local_notifications/example/web/favicon.png differ
diff --git a/flutter_local_notifications/example/web/icons/Icon-192.png b/flutter_local_notifications/example/web/icons/Icon-192.png
new file mode 100644
index 000000000..b749bfef0
Binary files /dev/null and b/flutter_local_notifications/example/web/icons/Icon-192.png differ
diff --git a/flutter_local_notifications/example/web/icons/Icon-512.png b/flutter_local_notifications/example/web/icons/Icon-512.png
new file mode 100644
index 000000000..88cfd48df
Binary files /dev/null and b/flutter_local_notifications/example/web/icons/Icon-512.png differ
diff --git a/flutter_local_notifications/example/web/icons/Icon-maskable-192.png b/flutter_local_notifications/example/web/icons/Icon-maskable-192.png
new file mode 100644
index 000000000..eb9b4d76e
Binary files /dev/null and b/flutter_local_notifications/example/web/icons/Icon-maskable-192.png differ
diff --git a/flutter_local_notifications/example/web/icons/Icon-maskable-512.png b/flutter_local_notifications/example/web/icons/Icon-maskable-512.png
new file mode 100644
index 000000000..d69c56691
Binary files /dev/null and b/flutter_local_notifications/example/web/icons/Icon-maskable-512.png differ
diff --git a/flutter_local_notifications/example/web/index.html b/flutter_local_notifications/example/web/index.html
new file mode 100644
index 000000000..1aa025dd6
--- /dev/null
+++ b/flutter_local_notifications/example/web/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ example
+
+
+
+
+
+
diff --git a/flutter_local_notifications/example/web/manifest.json b/flutter_local_notifications/example/web/manifest.json
new file mode 100644
index 000000000..096edf8fe
--- /dev/null
+++ b/flutter_local_notifications/example/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "example",
+ "short_name": "example",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart
index c42e2ff4c..b6eda92c2 100644
--- a/flutter_local_notifications/lib/flutter_local_notifications.dart
+++ b/flutter_local_notifications/lib/flutter_local_notifications.dart
@@ -2,6 +2,7 @@ export 'package:flutter_local_notifications_linux/flutter_local_notifications_li
// Exports what's defined in platform interface but hide helper methods
export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'
hide validateId, validateRepeatDurationInterval;
+export 'package:flutter_local_notifications_web/flutter_local_notifications_web.dart';
export 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart';
export 'src/flutter_local_notifications_plugin.dart';
diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart
index 4a9650131..080fa0c66 100644
--- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart
+++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
+import 'package:flutter_local_notifications_web/flutter_local_notifications_web.dart';
import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart';
import 'package:timezone/timezone.dart';
@@ -45,35 +46,31 @@ class FlutterLocalNotificationsPlugin {
'The type argument must be a concrete subclass of '
'FlutterLocalNotificationsPlatform');
}
- if (kIsWeb) {
- return null;
- }
- if (defaultTargetPlatform == TargetPlatform.android &&
+ final FlutterLocalNotificationsPlatform instance =
+ FlutterLocalNotificationsPlatform.instance;
+ if (kIsWeb && T == WebFlutterLocalNotificationsPlugin && instance is T) {
+ return instance;
+ } else if (defaultTargetPlatform == TargetPlatform.android &&
T == AndroidFlutterLocalNotificationsPlugin &&
- FlutterLocalNotificationsPlatform.instance
- is AndroidFlutterLocalNotificationsPlugin) {
- return FlutterLocalNotificationsPlatform.instance as T?;
+ instance is T) {
+ return instance;
} else if (defaultTargetPlatform == TargetPlatform.iOS &&
T == IOSFlutterLocalNotificationsPlugin &&
- FlutterLocalNotificationsPlatform.instance
- is IOSFlutterLocalNotificationsPlugin) {
- return FlutterLocalNotificationsPlatform.instance as T?;
+ instance is T) {
+ return instance;
} else if (defaultTargetPlatform == TargetPlatform.macOS &&
T == MacOSFlutterLocalNotificationsPlugin &&
- FlutterLocalNotificationsPlatform.instance
- is MacOSFlutterLocalNotificationsPlugin) {
- return FlutterLocalNotificationsPlatform.instance as T?;
+ instance is T) {
+ return instance;
} else if (defaultTargetPlatform == TargetPlatform.linux &&
T == LinuxFlutterLocalNotificationsPlugin &&
- FlutterLocalNotificationsPlatform.instance
- is LinuxFlutterLocalNotificationsPlugin) {
- return FlutterLocalNotificationsPlatform.instance as T?;
+ instance is T) {
+ return instance;
} else if (defaultTargetPlatform == TargetPlatform.windows &&
T == FlutterLocalNotificationsWindows &&
- FlutterLocalNotificationsPlatform.instance
- is FlutterLocalNotificationsWindows) {
- return FlutterLocalNotificationsPlatform.instance as T?;
+ instance is T) {
+ return instance;
}
return null;
@@ -117,7 +114,11 @@ class FlutterLocalNotificationsPlugin {
onDidReceiveBackgroundNotificationResponse,
}) async {
if (kIsWeb) {
- return true;
+ return resolvePlatformSpecificImplementation<
+ WebFlutterLocalNotificationsPlugin>()
+ ?.initialize(
+ onNotificationReceived: onDidReceiveNotificationResponse,
+ );
}
if (defaultTargetPlatform == TargetPlatform.android) {
@@ -203,9 +204,10 @@ class FlutterLocalNotificationsPlugin {
Future
getNotificationAppLaunchDetails() async {
if (kIsWeb) {
- return null;
- }
- if (defaultTargetPlatform == TargetPlatform.android) {
+ return await resolvePlatformSpecificImplementation<
+ WebFlutterLocalNotificationsPlugin>()
+ ?.getNotificationAppLaunchDetails();
+ } else if (defaultTargetPlatform == TargetPlatform.android) {
return await resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.getNotificationAppLaunchDetails();
@@ -238,7 +240,15 @@ class FlutterLocalNotificationsPlugin {
String? payload,
}) async {
if (kIsWeb) {
- return;
+ await resolvePlatformSpecificImplementation<
+ WebFlutterLocalNotificationsPlugin>()
+ ?.show(
+ id,
+ title,
+ body,
+ payload: payload,
+ details: notificationDetails?.web,
+ );
}
if (defaultTargetPlatform == TargetPlatform.android) {
await resolvePlatformSpecificImplementation<
@@ -283,9 +293,10 @@ class FlutterLocalNotificationsPlugin {
/// be canceled. `tag` has no effect on other platforms.
Future cancel(int id, {String? tag}) async {
if (kIsWeb) {
- return;
- }
- if (defaultTargetPlatform == TargetPlatform.android) {
+ await resolvePlatformSpecificImplementation<
+ WebFlutterLocalNotificationsPlugin>()
+ ?.cancel(id, tag: tag);
+ } else if (defaultTargetPlatform == TargetPlatform.android) {
await resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.cancel(id, tag: tag);
diff --git a/flutter_local_notifications/lib/src/notification_details.dart b/flutter_local_notifications/lib/src/notification_details.dart
index 070b03ae1..5e6919e49 100644
--- a/flutter_local_notifications/lib/src/notification_details.dart
+++ b/flutter_local_notifications/lib/src/notification_details.dart
@@ -1,4 +1,5 @@
import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
+import 'package:flutter_local_notifications_web/flutter_local_notifications_web.dart';
import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart';
import 'platform_specifics/android/notification_details.dart';
@@ -13,8 +14,12 @@ class NotificationDetails {
this.macOS,
this.linux,
this.windows,
+ this.web,
});
+ /// Notification details for web.
+ final WebNotificationDetails? web;
+
/// Notification details for Android.
final AndroidNotificationDetails? android;
diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml
index 71273770f..96cb84720 100644
--- a/flutter_local_notifications/pubspec.yaml
+++ b/flutter_local_notifications/pubspec.yaml
@@ -12,8 +12,10 @@ dependencies:
sdk: flutter
flutter_local_notifications_linux: ^6.0.0
flutter_local_notifications_windows: ^1.0.3
+ flutter_local_notifications_web: ^1.0.0
flutter_local_notifications_platform_interface: ^9.1.0
timezone: ^0.10.0
+ universal_platform: ^1.1.0
dev_dependencies:
flutter_driver:
@@ -40,6 +42,8 @@ flutter:
default_package: flutter_local_notifications_linux
windows:
default_package: flutter_local_notifications_windows
+ web:
+ default_package: flutter_local_notifications_web
environment:
sdk: ^3.4.0
diff --git a/flutter_local_notifications_web/.gitignore b/flutter_local_notifications_web/.gitignore
new file mode 100644
index 000000000..e7d347d9d
--- /dev/null
+++ b/flutter_local_notifications_web/.gitignore
@@ -0,0 +1,33 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+build/
diff --git a/flutter_local_notifications_web/.metadata b/flutter_local_notifications_web/.metadata
new file mode 100644
index 000000000..a374771a5
--- /dev/null
+++ b/flutter_local_notifications_web/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "edada7c56edf4a183c1735310e123c7f923584f1"
+ channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+ base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+ - platform: web
+ create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+ base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/flutter_local_notifications_web/CHANGELOG.md b/flutter_local_notifications_web/CHANGELOG.md
new file mode 100644
index 000000000..b335a3157
--- /dev/null
+++ b/flutter_local_notifications_web/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+Initial web release
diff --git a/flutter_local_notifications_web/LICENSE b/flutter_local_notifications_web/LICENSE
new file mode 100644
index 000000000..0861b3c81
--- /dev/null
+++ b/flutter_local_notifications_web/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2020 Michael Bui. 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.
diff --git a/flutter_local_notifications_web/README.md b/flutter_local_notifications_web/README.md
new file mode 100644
index 000000000..f6bc825f2
--- /dev/null
+++ b/flutter_local_notifications_web/README.md
@@ -0,0 +1,59 @@
+# flutter_local_notifications_web
+
+The web implementation of the `flutter_local_notifications` package.
+
+## Notes
+
+- If you are debugging with `flutter run -d chrome`, you will see notifications but they will not respond to being clicked! This is due to the private debugging window that Flutter opens, and they will respond properly in release builds. To test notification handlers, make sure to use `flutter run -d web-server`. If you find that hot reload is broken with `-d web-server`, try to test as much as possible with `-d chrome`.
+- **You must request notification permissions before showing notifications -- but only in response to a user interaction.** If you try to request permissions automatically, like on loading a page, not only may your request be automatically denied, but the browser may deem your site as abusive and no longer show any more prompts to the user, and just block everything going forward.
+- Notification actions are supported by Chrome and Edge, but not Firefox or Safari. They may catch up soon, but text input fields use a standards _proposal_, not an accepted standard, and so may only work on Chrome for a while.
+- Browsers don't support scheduled or repeating notifications, and browsers on Android do not support custom vibration.
+
+## Using the plugin
+
+### Initialize the plugin
+
+```dart
+final plugin = FlutterLocalNotificationsPlugin();
+await plugin.initialize();
+
+if (!plugin.hasPermission) {
+ // IMPORTANT: Only call this after a button press!
+ await plugin.requestNotificationsPermission();
+}
+```
+
+### Show a notification
+
+```dart
+var id = 0; // increment this every time you show a notification
+await plugin.show(id, "Title", "Body text", null);
+```
+
+### Use web-specific details
+```dart
+final webDetails = WebNotificationDetails(requireInteraction: true);
+final details = NotificationDetails(web: webDetails);
+await plugin.show(id, "Title", "Body text", details);
+```
+
+### Respond to notifications
+```dart
+// Handle incoming notifications when your site is open
+void handleNotification(NotificationResponse notification) {
+ print("User clicked on notification: ${notification.id}");
+}
+
+final plugin = FlutterLocalNotificationsPlugin();
+await plugin.initialize(onNotificationReceived: handleNotification);
+
+// When your site is closed, clicking the notification will launch your site
+// with special query parameters that include the notification details.
+// When your site is opened, check if it was because of a notification:
+final launchDetails = await plugin.getNotificationAppLaunchDetails();
+if (launchDetails != null) {
+ // The site was launched because a notification was clicked
+ final notification = launchDetails.notificationResponse;
+ print("User clicked on notification: ${notification.id}")
+}
+```
diff --git a/flutter_local_notifications_web/lib/flutter_local_notifications_web.dart b/flutter_local_notifications_web/lib/flutter_local_notifications_web.dart
new file mode 100644
index 000000000..0844b9c7f
--- /dev/null
+++ b/flutter_local_notifications_web/lib/flutter_local_notifications_web.dart
@@ -0,0 +1,3 @@
+export 'src/details.dart';
+
+export 'src/stub.dart' if (dart.library.js_interop) 'src/plugin.dart';
diff --git a/flutter_local_notifications_web/lib/src/action.dart b/flutter_local_notifications_web/lib/src/action.dart
new file mode 100644
index 000000000..a354974fa
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/action.dart
@@ -0,0 +1,49 @@
+/// The type of action for a web notification.
+///
+/// This is a non-standard extension of `showNotification()`s `options.actions`
+/// parameter that allows for buttons and text inputs.
+///
+/// The proposal can be found [here](https://github.com/whatwg/notifications/pull/132).
+///
+/// Web actions themselves are hardly supported at the time of writing. See
+/// [WebNotificationAction] for more details.
+enum WebNotificationActionType {
+ /// This action is a button the user can press.
+ button('button'),
+
+ /// This action is an input field the user can type into.
+ textInput('text');
+
+ const WebNotificationActionType(this.jsValue);
+
+ /// A string to pass to the JavaScript functions that indicates this type.
+ final String jsValue;
+}
+
+/// A means for a user to interact with a web notification.
+///
+/// See: [`options.actions`](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#actions)
+///
+/// Note: This is not standard yet, see: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#browser_compatibility
+/// Notably, Firefox and Safari do not support this yet.
+class WebNotificationAction {
+ /// A const constructor.
+ const WebNotificationAction({
+ required this.action,
+ required this.title,
+ this.icon,
+ this.type = WebNotificationActionType.button,
+ });
+
+ /// The type of action (button by default).
+ final WebNotificationActionType type;
+
+ /// A developer-facing string representing this action.
+ final String action;
+
+ /// A user-facing string to be shown next to this action.
+ final String title;
+
+ /// An optional icon to display next to the action.
+ final Uri? icon;
+}
diff --git a/flutter_local_notifications_web/lib/src/details.dart b/flutter_local_notifications_web/lib/src/details.dart
new file mode 100644
index 000000000..d89626f7e
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/details.dart
@@ -0,0 +1,83 @@
+import 'action.dart';
+import 'direction.dart';
+
+export 'action.dart';
+export 'direction.dart';
+
+/// The web-specific details of a notification.
+///
+/// See: https://developer.mozilla.org/en-US/docs/Web/API/Notification
+///
+/// Note: The `tag` field is reserved for the notification ID.
+class WebNotificationDetails {
+ /// A const constructor.
+ const WebNotificationDetails({
+ this.actions = const [],
+ this.direction = WebNotificationDirection.auto,
+ this.badgeUrl,
+ this.iconUrl,
+ this.lang,
+ this.requireInteraction = false,
+ this.isSilent = false,
+ this.imageUrl,
+ this.renotify = false,
+ this.timestamp,
+ this.vibrationPattern,
+ });
+
+ /// A list of actions to send with the notification.
+ final List actions;
+
+ /// The text direction of the notification.
+ final WebNotificationDirection direction;
+
+ /// An optional URL to a monochrome icon to show next to the title.
+ final Uri? badgeUrl;
+
+ /// An optional URL to an image to show as the app icon.
+ final Uri? iconUrl;
+
+ /// An optional URL to an image to show in the notification.
+ final Uri? imageUrl;
+
+ /// The language code of the notification's content, eg `en-US`.
+ final String? lang;
+
+ /// Whether the notification should remain visible until manually dismissed.
+ final bool requireInteraction;
+
+ /// Whether the notification should be silent.
+ final bool isSilent;
+
+ /// Whether the user should be notified if this notification is replaced.
+ ///
+ /// If this is false, the new notification will replace this one silently. If
+ /// this is true, the device will notify the user any time the notification is
+ /// updated.
+ ///
+ /// For exmaple, when updating a loading notification, you want each update to
+ /// occur silently. But if you have a notification that represents a chat
+ /// thread, you'd want it to update when a new message is sent.
+ final bool renotify;
+
+ /// The timestamp of the event that caused this notification.
+ ///
+ /// This doesn't always have to be the time the notification was sent. For
+ /// exmaple, if the notification represents a message that was sent a while
+ /// ago, you can back-date this timestamp to when the message was originally
+ /// sent instead of when it was received.
+ final DateTime? timestamp;
+
+ /// An optional vibration pattern
+ ///
+ /// This should be a list of milliseconds, starting with how long the device
+ /// should vibrate for, followed by how long it should pause.
+ ///
+ /// For examples, see:
+ /// - https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns
+ /// - https://web.dev/articles/push-notifications-display-a-notification#vibrate
+ ///
+ /// Note: This is not supported on Android as vibration patterns are set by
+ /// the notification channel, not an individual notification.
+ final List? vibrationPattern;
+}
diff --git a/flutter_local_notifications_web/lib/src/direction.dart b/flutter_local_notifications_web/lib/src/direction.dart
new file mode 100644
index 000000000..2f163d001
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/direction.dart
@@ -0,0 +1,18 @@
+/// The text direction of a web notification.
+///
+/// Note: This may be ignored by browsers. See the [docs](https://developer.mozilla.org/en-US/docs/Web/API/Notification/dir)
+enum WebNotificationDirection {
+ /// Adopt the browser's setting.
+ auto('auto'),
+
+ /// Left to right.
+ leftToRight('ltr'),
+
+ /// Right to left.
+ rightToLeft('rtl');
+
+ const WebNotificationDirection(this.jsValue);
+
+ /// A string value to pass to JavaScript functions.
+ final String jsValue;
+}
diff --git a/flutter_local_notifications_web/lib/src/handler.dart b/flutter_local_notifications_web/lib/src/handler.dart
new file mode 100644
index 000000000..198ba06d1
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/handler.dart
@@ -0,0 +1,46 @@
+@JS()
+library;
+
+import 'dart:js_interop';
+
+import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
+import 'package:web/web.dart';
+
+import 'utils.dart';
+
+/// An object representing a clicked notification.
+///
+/// Notification clicks are handled by service workers. See `web/notification_service_worker.js`
+/// for the source code there. When the service worker receives the
+/// [NotificationEvent], it uses [Client.postMessage] to send a message back to
+/// the currently open window/tab, if there is any.
+///
+/// This is the object sent by the service worker via [Client.postMessage].
+extension type JSNotificationData(JSObject _) implements JSObject {
+ /// A string rperesenting the [NotificationAction.action], if any.
+ external String? action;
+
+ /// A string representing the [Notification.tag];
+ external String id;
+
+ /// The original payload passed to [FlutterLocalNotificationsPlatform.show].
+ external String? payload;
+
+ /// The reply entered by the user in the text field, if any.
+ external String? reply;
+
+ /// The [NotificationResponse] that corresponds to this object.
+ NotificationResponse get response {
+ final NotificationResponseType type = (action?.isEmpty ?? true)
+ ? NotificationResponseType.selectedNotification
+ : NotificationResponseType.selectedNotificationAction;
+
+ return NotificationResponse(
+ id: int.parse(id),
+ input: reply?.nullIfEmpty,
+ payload: payload?.nullIfEmpty,
+ actionId: action?.nullIfEmpty,
+ notificationResponseType: type,
+ );
+ }
+}
diff --git a/flutter_local_notifications_web/lib/src/plugin.dart b/flutter_local_notifications_web/lib/src/plugin.dart
new file mode 100644
index 000000000..a7c98b429
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/plugin.dart
@@ -0,0 +1,204 @@
+import 'dart:js_interop';
+
+import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
+import 'package:web/web.dart';
+
+import 'details.dart';
+import 'handler.dart';
+import 'utils.dart';
+
+/// Called when a notification has been clicked.
+///
+/// Notification clicks are handled by service workers. See `web/notification_service_worker.js`
+/// for the source code there. When the service worker receives the
+/// [NotificationEvent], it uses [Client.postMessage] to send a message back to
+/// the currently open window/tab, if there is any.
+///
+/// This function creates a [NotificationResponse] object and calls the user's
+/// callback they provided to [WebFlutterLocalNotificationsPlugin.initialize].
+void _onNotificationClicked(MessageEvent event) {
+ final JSNotificationData data = event.data as JSNotificationData;
+ final NotificationResponse response = data.response;
+ WebFlutterLocalNotificationsPlugin.instance?._userCallback?.call(response);
+}
+
+/// Web implementation of the local notifications plugin.
+class WebFlutterLocalNotificationsPlugin
+ extends FlutterLocalNotificationsPlatform {
+ /// Registers the web plugin with the platform interface.
+ static void registerWith(_) {
+ FlutterLocalNotificationsPlatform.instance =
+ WebFlutterLocalNotificationsPlugin();
+ }
+
+ /// The currently loaded web plugin object, if any.
+ static WebFlutterLocalNotificationsPlugin? instance;
+
+ DidReceiveNotificationResponseCallback? _userCallback;
+ ServiceWorkerRegistration? _registration;
+
+ @override
+ Future show(int id, String? title, String? body,
+ {String? payload, WebNotificationDetails? details}) async {
+ if (_registration == null) {
+ throw StateError(
+ 'FlutterLocalNotifications.show(): You must call initialize() before '
+ 'calling this method',
+ );
+ } else if (!hasPermission) {
+ throw StateError(
+ 'FlutterLocalNotifications.show(): You must request notifications '
+ 'permissions first',
+ );
+ } else if (details?.isSilent == true && details?.vibrationPattern != null) {
+ throw ArgumentError(
+ 'WebNotificationDetails: Cannot specify both silent and a vibration '
+ 'pattern',
+ );
+ } else if (_registration!.active == null) {
+ throw StateError(
+ 'FlutterLocalNotifications.show(): There is no active service worker. '
+ 'Call initialize() first',
+ );
+ }
+
+ await _registration!
+ .showNotification(
+ title ?? '',
+ details.toJs(id, body, payload),
+ )
+ .toDart;
+ }
+
+ /// Initializes the plugin.
+ Future initialize({
+ DidReceiveNotificationResponseCallback? onNotificationReceived,
+ }) async {
+ instance = this;
+ _userCallback = onNotificationReceived;
+
+ // Replace the default Flutter service worker with our own.
+ // This isn't supported at build time yet and so must be done at runtime.
+ // See: https://github.com/flutter/flutter/issues/145828
+ final ServiceWorkerContainer serviceWorker = window.navigator.serviceWorker;
+ _registration = await serviceWorker.getRegistration().toDart;
+ const String jsPath =
+ './assets/packages/flutter_local_notifications_web/web/notifications_service_worker.js';
+ _registration = await serviceWorker.register(jsPath.toJS).toDart;
+
+ // Subscribe to messages from the service worker
+ serviceWorker.onmessage = _onNotificationClicked.toJS;
+
+ return true;
+ }
+
+ /// Requests notification permission from the browser.
+ ///
+ /// Be sure to only request permissions in response to a user gesture, or it
+ /// may be automatically rejected.
+ Future requestNotificationsPermission() async {
+ final JSString result = await Notification.requestPermission().toDart;
+ return result.toDart == 'granted';
+ }
+
+ /// Whether the user has granted permission to show notifications.
+ ///
+ /// If this is false, you must call [requestNotificationsPermission]. Be sure
+ /// to only request permissions in response to a user gesture, or it may be
+ /// automatically rejected.
+ bool get hasPermission => Notification.permission == 'granted';
+
+ @override
+ Future
+ getNotificationAppLaunchDetails() async {
+ final Uri uri = Uri.parse(window.location.toString());
+ final Map query = uri.queryParameters;
+ final String? id = query['notification_id'];
+ final String? payload = query['notification_payload'];
+ final String? action = query['notification_action'];
+ final String? reply = query['notification_reply'];
+ window.history.replaceState(null, '', '/');
+ if (id == null || payload == null || action == null || reply == null) {
+ return null;
+ } else {
+ return NotificationAppLaunchDetails(
+ true,
+ notificationResponse: NotificationResponse(
+ notificationResponseType: action.isEmpty
+ ? NotificationResponseType.selectedNotification
+ : NotificationResponseType.selectedNotificationAction,
+ id: int.parse(id),
+ input: reply.nullIfEmpty,
+ payload: payload.nullIfEmpty,
+ actionId: action.nullIfEmpty,
+ ),
+ );
+ }
+ }
+
+ @override
+ Future> getActiveNotifications() async {
+ if (_registration == null) {
+ return [];
+ }
+ final List result = [];
+ final Set ids = {};
+ final List jsNotifs =
+ await _registration!.getDartNotifications();
+ for (final Notification jsNotification in jsNotifs) {
+ final int? id = jsNotification.id;
+ if (id == null) {
+ continue;
+ }
+ final ActiveNotification notif = ActiveNotification(id: id);
+ ids.add(id);
+ result.add(notif);
+ }
+ return result;
+ }
+
+ @override
+ Future cancel(int id, {String? tag}) async {
+ if (_registration == null) {
+ return;
+ }
+ final List notifs =
+ await _registration!.getDartNotifications();
+ for (final Notification notification in notifs) {
+ if (notification.id == id || (tag != null && tag == notification.tag)) {
+ notification.close();
+ }
+ }
+ }
+
+ @override
+ Future cancelAll() async {
+ if (_registration == null) {
+ return;
+ }
+ final List notifs =
+ await _registration!.getDartNotifications();
+ for (final Notification notification in notifs) {
+ notification.close();
+ }
+ }
+
+ @override
+ Future>
+ pendingNotificationRequests() async => [];
+
+ @override
+ Future periodicallyShow(
+ int id, String? title, String? body, RepeatInterval repeatInterval) {
+ throw UnsupportedError('periodicallyShow() is not supported on the web');
+ }
+
+ @override
+ Future periodicallyShowWithDuration(
+ int id, String? title, String? body, Duration repeatDurationInterval) {
+ throw UnsupportedError(
+ 'periodicallyShowWithDuration() is not supported '
+ 'on the web',
+ );
+ }
+}
diff --git a/flutter_local_notifications_web/lib/src/stub.dart b/flutter_local_notifications_web/lib/src/stub.dart
new file mode 100644
index 000000000..97d8122cc
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/stub.dart
@@ -0,0 +1,29 @@
+import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
+
+import 'details.dart';
+
+/// A stub implementation of the web plugin, for non-web platforms.
+class WebFlutterLocalNotificationsPlugin
+ extends FlutterLocalNotificationsPlatform {
+ /// Initializes the plugin.
+ Future initialize({
+ DidReceiveNotificationResponseCallback? onNotificationReceived,
+ }) async =>
+ null;
+
+ /// Requests notification permission from the browser.
+ ///
+ /// Be sure to only request permissions in response to a user gesture, or it
+ /// may be automatically rejected.
+ Future requestNotificationsPermission() async => false;
+
+ @override
+ Future cancel(int id, {String? tag}) async {}
+
+ @override
+ Future show(int id, String? title, String? body,
+ {String? payload, WebNotificationDetails? details}) async {}
+
+ /// Whether the user has granted permission to show notifications.
+ bool get hasPermission => false;
+}
diff --git a/flutter_local_notifications_web/lib/src/utils.dart b/flutter_local_notifications_web/lib/src/utils.dart
new file mode 100644
index 000000000..f3f6bcb37
--- /dev/null
+++ b/flutter_local_notifications_web/lib/src/utils.dart
@@ -0,0 +1,102 @@
+import 'dart:js_interop';
+import 'dart:js_interop_unsafe';
+
+import 'package:web/web.dart';
+
+import 'details.dart';
+
+/// Utility methods on JavaScript [Notification] objects.
+extension JSNotificationUtils on Notification {
+ /// The ID of the notification.
+ int? get id => int.tryParse(tag);
+}
+
+/// Utility methods on [ServiceWorkerRegistration] objects.
+extension ServiceWorkerUtils on ServiceWorkerRegistration {
+ /// Gets a list of notifications (as Dart objects).
+ Future> getDartNotifications() async =>
+ (await getNotifications().toDart).toDart;
+}
+
+/// Utility methods on [WebNotificationDetails] objects.
+extension WebNotificationDetailsUtils on WebNotificationDetails {
+ /// A JavaScript array with the number of milliseconds in between vibrations.
+ JSArray? get vibrationPatternMs => vibrationPattern == null
+ ? null
+ : [
+ for (final int duration in vibrationPattern!) duration.toJS,
+ ].toJS;
+}
+
+/// Utility methods on [WebNotificationAction] objects.
+extension on WebNotificationAction {
+ /// Converts this object to the format expected by `showNotifications()`
+ ///
+ /// See: https://developer.mozilla.org/en-US/docs/Web/API/Notification/actions.
+ ///
+ /// Note: There is a WHATWG proposal to add button and text input actions.
+ /// This requires setting an additional `type` parameter, which is not yet
+ /// standardized, so is not a part of the Dart APIs. That's why we cannot rely
+ /// on `NotificationAction.toJs` for this function.
+ ///
+ /// The proposal can be found here: https://github.com/whatwg/notifications/pull/132
+ NotificationAction toJs() => {
+ 'action': action,
+ 'title': title,
+ 'icon': icon?.toString(),
+ 'type': type.jsValue,
+ }.jsify() as NotificationAction;
+}
+
+/// A useful way to convert a nullable details object to a JS payload.
+extension NullableWebNotificationDetailsUtils on WebNotificationDetails? {
+ /// Converts the list of actions into a JS array (empty if needed).
+ List get _actions =>
+ this?.actions ?? [];
+
+ /// Converts these nullable details to a JS [NotificationOptions] object.
+ NotificationOptions toJs(int id, String? body, String? payload) {
+ final NotificationOptions options = NotificationOptions(
+ data: {'payload': payload}.jsify(),
+ tag: id.toString(),
+ // -----------------------------
+ actions: [
+ for (final WebNotificationAction action in _actions) action.toJs(),
+ ].toJS,
+ badge: this?.badgeUrl.toString() ?? '',
+ body: body ?? '',
+ dir: this?.direction.jsValue ?? WebNotificationDirection.auto.jsValue,
+ icon: this?.iconUrl.toString() ?? '',
+ image: this?.imageUrl.toString() ?? '',
+ lang: this?.lang ?? '',
+ renotify: this?.renotify ?? false,
+ requireInteraction: this?.requireInteraction ?? false,
+ silent: this?.isSilent,
+ timestamp: (this?.timestamp ?? DateTime.now()).millisecondsSinceEpoch,
+ );
+
+ final JSArray? vibration = this?.vibrationPatternMs;
+ if (vibration != null) {
+ options.vibrate = vibration;
+ }
+
+ return options;
+ }
+}
+
+/// Utility methods on JavaScript [NotificationEvent] objects.
+extension WebNotificationEventUtils on NotificationEvent {
+ /// Gets text input from the action, if any.
+ ///
+ /// See [WebNotificationActionType] for details.
+ String? get reply => (this['reply'] as JSString).toDart;
+}
+
+/// Helpful methods on strings.
+extension StringUtils on String {
+ /// Returns null if this string is empty.
+ ///
+ /// Useful because JavaScript might prefer to represent a blank option with an
+ /// empty string, while Dart users would prefer `null` instead.
+ String? get nullIfEmpty => isEmpty ? null : this;
+}
diff --git a/flutter_local_notifications_web/pubspec.yaml b/flutter_local_notifications_web/pubspec.yaml
new file mode 100644
index 000000000..87dc4f783
--- /dev/null
+++ b/flutter_local_notifications_web/pubspec.yaml
@@ -0,0 +1,29 @@
+name: flutter_local_notifications_web
+description: Web implementation of the flutter_local_notifications plugin
+version: 1.0.0
+homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_web
+issue_tracker: https://github.com/MaikuB/flutter_local_notifications/issues
+
+environment:
+ sdk: ^3.4.0
+ flutter: ">=3.22.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ flutter_local_notifications_platform_interface: ^9.0.0
+ web: ^1.1.1
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+ plugin:
+ platforms:
+ web:
+ pluginClass: WebFlutterLocalNotificationsPlugin
+ fileName: "flutter_local_notifications_web.dart"
+
+ assets:
+ - web/notifications_service_worker.js
diff --git a/flutter_local_notifications_web/web/notifications_service_worker.js b/flutter_local_notifications_web/web/notifications_service_worker.js
new file mode 100644
index 000000000..7acb95265
--- /dev/null
+++ b/flutter_local_notifications_web/web/notifications_service_worker.js
@@ -0,0 +1,62 @@
+'use strict';
+
+// Experimental function to save a notification to the IndexedDB database
+//
+// This function isn't currently used but left because it may be helpful in other
+// implementations of this plugin or custom needs for some apps.
+function saveMessage(message) {
+ let request = indexedDB.open("flutter_local_notifications", 1);
+ request.onupgradeneeded = (event) => {
+ let db = event.target.result;
+ db.createObjectStore("notifications", {keyPath: "idb_id"});
+ }
+ request.onsuccess = (event) => {
+ let db = event.target.result;
+ let transaction = db.transaction(["notifications"], "readwrite");
+ transaction.objectStore("notifications").add(message);
+ }
+}
+
+// Handles a clicked notification.
+//
+// If the site is open, sends the information to the site using postMessage().
+// Otherwise, open the site with notification details as query parameters.
+async function _handleNotif(event) {
+ // We have to use `event.waitUntil()` to tell the browser that we're still
+ // processing this event. Without this, the event "expires" after the first
+ // `await`, and trying to call `clients.openWindow()` later results in a
+ // permission error, because the browser thinks they are unrelated events.
+ let allClientsPromise = clients.matchAll({includeUncontrolled: true});
+ event.waitUntil(allClientsPromise);
+ let allClients = await allClientsPromise;
+
+ let message = {
+ id: event.notification.tag,
+ payload: event.notification.data.payload,
+ action: event.action,
+ reply: event.reply,
+ // This is only used for saveMessage() above.
+ idb_id: "flutter_local_notifications",
+ };
+ // If you need to hold onto the message:
+ // saveMessage(message)
+
+ if (allClients.length == 0) {
+ let url = `/?notification_id=${encodeURIComponent(message.id)}`
+ + `¬ification_payload=${encodeURIComponent(message.payload)}`
+ + `¬ification_action=${encodeURIComponent(message.action)}`
+ + `¬ification_reply=${encodeURIComponent(message.reply)}`;
+ await clients.openWindow(url);
+ } else {
+ let client = allClients[0];
+ await client.postMessage(message);
+ }
+}
+
+// Listen for notification events.
+self.addEventListener("notificationclick", _handleNotif);
+
+// Normally, a service worker only takes effect the _next_ time it is installed.
+// These next lines make sure it takes effect the first time,
+self.addEventListener("install", event => { self.skipWaiting(); });
+self.addEventListener("activate", event => { event.waitUntil(clients.claim()); });
diff --git a/images/web_notifications.png b/images/web_notifications.png
new file mode 100644
index 000000000..76a108dfa
Binary files /dev/null and b/images/web_notifications.png differ
diff --git a/melos.yaml b/melos.yaml
index 69d137022..63d7b52ff 100644
--- a/melos.yaml
+++ b/melos.yaml
@@ -4,6 +4,7 @@ packages:
- flutter_local_notifications
- flutter_local_notifications_linux
- flutter_local_notifications_windows
+ - flutter_local_notifications_web
- flutter_local_notifications_platform_interface
- flutter_local_notifications/example/
@@ -88,6 +89,15 @@ scripts:
dirExists:
- windows
scope: "*example*"
+ build:example_web:
+ run: |
+ melos exec -c 1 -- \
+ "flutter build web"
+ description: Build a specific example app for Web.
+ packageFilters:
+ dirExists:
+ - web
+ scope: "*example*"
clean:
run: git clean -x -d -f -q
diff --git a/pubspec.yaml b/pubspec.yaml
index f913619b0..80c3a0f60 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,8 @@
name: flutter_local_notifications_workspace
environment:
- sdk: '>=3.0.0 <4.0.0'
+ sdk: ^3.4.0
+ flutter: ">=3.22.0"
+
dev_dependencies:
melos: ^6.1.0