diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f9ef166a9..1010ca75b 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,6 +8,10 @@ updates:
directory: "/flutter_local_notifications_linux"
schedule:
interval: "daily"
+ - package-ecosystem: "pub"
+ directory: "/flutter_local_notifications_windows"
+ schedule:
+ interval: "daily"
- package-ecosystem: "pub"
directory: "/flutter_local_notifications"
schedule:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 31ffa6bec..83b5b5a8a 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -61,15 +61,15 @@ jobs:
run: ./.github/workflows/scripts/install-tools.sh
- name: Build
run: melos run build:example_android
- build_example_android_3_13:
- name: Build Android example app (3.13)
+ build_example_android_3_19:
+ name: Build Android example app (3.19)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- flutter-version: 3.13.0
+ flutter-version: 3.19.0
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- name: Install Tools
@@ -90,15 +90,15 @@ jobs:
run: ./.github/workflows/scripts/install-tools.sh
- name: Build
run: melos run build:example_ios
- build_example_ios_3_13:
- name: Build iOS example app (3.13)
+ build_example_ios_3_19:
+ name: Build iOS example app (3.19)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- flutter-version: 3.13.0
+ flutter-version: 3.19.0
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- name: Install Tools
@@ -119,15 +119,15 @@ jobs:
run: ./.github/workflows/scripts/install-tools.sh
- name: Build
run: melos run build:example_macos
- build_example_macos_3_13:
- name: Build macOS example app (3.13)
+ build_example_macos_3_19:
+ name: Build macOS example app (3.19)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- flutter-version: 3.13.0
+ flutter-version: 3.19.0
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- name: Install Tools
@@ -152,15 +152,15 @@ jobs:
- run: flutter config --enable-linux-desktop
- name: Build
run: melos run build:example_linux
- build_example_linux_3_13:
- name: Build Linux example app (3.13)
+ build_example_linux_3_19:
+ name: Build Linux example app (3.19)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- flutter-version: 3.13.0
+ flutter-version: 3.19.0
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- name: Install Tools
@@ -171,6 +171,57 @@ jobs:
- run: flutter config --enable-linux-desktop
- name: Build
run: melos run build:example_linux
+ build_example_windows_stable:
+ name: Build Windows example app (stable channel)
+ runs-on: windows-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
+ # Windows has a filename length limit, which this repo just hits
+ # This saves us precious characters during the compilation
+ - name: Rename directory
+ run: |
+ move flutter_local_notifications f
+ move f\example f\e
+ - name: Build
+ run: |
+ cd f\e
+ dart pub get
+ dart run msix:create
+ build_example_windows_3_19:
+ name: Build Windows example app (3.19)
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ flutter-version: 3.19.0
+ cache: true
+ cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
+ - name: Install Tools
+ run: |
+ dart pub global activate melos
+ melos bootstrap
+ # Windows has a filename length limit, which this repo just hits
+ # This saves us precious characters during the compilation
+ - name: Rename directory
+ run: |
+ move flutter_local_notifications f
+ move f\example f\e
+ - name: Build
+ run: |
+ cd f\e
+ dart pub get
+ dart run msix:create
unit_tests_dart:
name: Run all unit tests (Dart)
runs-on: ubuntu-latest
@@ -199,6 +250,22 @@ jobs:
run: ./.github/workflows/scripts/install-tools.sh
- name: Run Tests
run: melos run test:unit:android
+ unit_tests_windows:
+ name: Run all unit tests (Windows)
+ runs-on: windows-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: Run Tests
+ run: melos run test:unit:windows
integration_tests_android:
name: Run integration tests (Android)
runs-on: ubuntu-latest
@@ -245,6 +312,3 @@ jobs:
brew install applesimutils
applesimutils --byId ${{ steps.simulator-action.outputs.udid}} --bundle com.dexterous.flutterLocalNotificationsExample --setPermissions notifications=YES
- run: melos run test:integration
-
-
-
diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md
index c27d778ea..0716a91dd 100644
--- a/flutter_local_notifications/README.md
+++ b/flutter_local_notifications/README.md
@@ -5,7 +5,9 @@
A cross platform plugin for displaying local notifications.
->[!IMPORTANT]
+>[!IMPORTANT]
+> Given how both quickly both Flutter ecosystem and Android ecosystem evolves, the minimum Flutter SDK version will be bumped to make it easier to maintain the plugin. Note that official plugins already follow a similar approach e.g. have a minimum Flutter SDK version of 3.13. This is being called out as if this affects your applications (e.g. supported OS versions) then you may need to consider maintaining your own fork in the future
+>[!IMPORTANT]
> Given how both quickly both Flutter ecosystem and Android ecosystem evolves, the minimum Flutter SDK version will occasionally be bumped to make it easier to maintain the plugin. Note that official plugins already follow a similar approach. This is being called out as if this affects your applications (e.g. supported OS versions) then you may need to consider maintaining your own fork in the future
## Table of contents
@@ -59,6 +61,7 @@ A cross platform plugin for displaying local notifications.
* **iOS** Uses the [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework)
* **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)
Note: the plugin requires Flutter SDK 3.13 at a minimum. The list of support platforms for Flutter 3.13 itself can be found [here](https://github.com/flutter/website/blob/3d18ab48218101493af84953b71eac0cc6781fdd/src/reference/supported-platforms.md)
@@ -109,6 +112,10 @@ Note: the plugin requires Flutter SDK 3.13 at a minimum. The list of support pla
* [Linux] Ability to set custom hints
* [Linux] Ability to suppress sound
* [Linux] Resident and transient notifications
+* [Windows] Can show raw XML (see the [Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer))
+* [Windows] A full Dart API for all the options supported by toast notifications
+* [Windows] Can configure images, buttons, dropdowns, text input, and launch behavior
+* [Windows] Can dynamically update notifications after they've been shown
## ⚠ Caveats and limitations
@@ -154,6 +161,11 @@ Scheduled/pending notifications is currently not supported due to the lack of a
The `onDidReceiveNotificationResponse` callback runs on the main isolate of the running application and cannot be launched in the background if the application is not running. To respond to notification after the application is terminated, your application should be registered as DBus activatable (please see [DBusApplicationLaunching](https://wiki.gnome.org/HowDoI/DBusApplicationLaunching) for more information), and register action before activating the application. This is difficult to do in a plugin because plugins instantiate during application activation, so `getNotificationAppLaunchDetails` can't be implemented without changing the main user application.
+### Windows limitations
+
+- 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).
+
### 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.
@@ -166,6 +178,7 @@ Due to some limitations on iOS with how it treats null values in dictionaries, a
| iOS | |
| macOS | |
| Linux | |
+| Windows | |
## 👏 Acknowledgements
@@ -174,6 +187,7 @@ 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
* ...and everyone else for their contributions. They are greatly appreciated
## 🔧 Android Setup
@@ -270,7 +284,7 @@ For apps that need the following functionality please complete the following in
* Declare the service exposed by the plugin by adding the following between `` tags. An example of what this looks like is below where `` should be replaced with the foreground service type(s) your app needs. If you want your foreground service to be stopped if your app is stopped, set `android:stopWithTask` to `true`
```xml
@@ -386,7 +400,7 @@ then extend `didFinishLaunchingWithOptions` and register the callback:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
- // Add this method
+ // Add this method
[FlutterLocalNotificationsPlugin setPluginRegistrantCallback:registerPlugins];
}
```
@@ -529,11 +543,18 @@ final DarwinInitializationSettings initializationSettingsDarwin =
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(
defaultActionName: 'Open notification');
+final WindowsInitializationSettings initializationSettingsWindows =
+ WindowsInitializationSettings(
+ appName: 'Flutter Local Notifications Example',
+ appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample',
+ // Search online for GUID generators to make your own
+ guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb')
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
- linux: initializationSettingsLinux);
+ linux: initializationSettingsLinux,
+ windows: initializationSettingsWindows);
await flutterLocalNotificationsPlugin.initialize(initializationSettings,
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse);
```
@@ -557,9 +578,9 @@ void onDidReceiveNotificationResponse(NotificationResponse notificationResponse)
In the real world, this payload could represent the id of the item you want to display the details of. Once the initialisation is complete, then you can manage the displaying of notifications. Note that this callback is only intended to work when the app is running. For scenarios where your application needs to handle when a notification launched the app refer to [here](#getting-details-on-if-the-app-was-launched-via-a-notification-created-by-this-plugin)
-The `DarwinInitializationSettings` class provides default settings on how the notification be presented when it is triggered and the application is in the foreground on iOS/macOS. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled.
+The `DarwinInitializationSettings` class provides default settings on how the notification be presented when it is triggered and the application is in the foreground on iOS/macOS. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled.
-The `LinuxInitializationSettings` class requires a name for the default action that calls the `onDidReceiveNotificationResponse` callback when the notification is clicked.
+The `LinuxInitializationSettings` class requires a name for the default action that calls the `onDidReceiveNotificationResponse` callback when the notification is clicked.
On iOS and macOS, initialisation may show a prompt to requires users to give the application permission to display notifications (note: permissions don't need to be requested on Android). Depending on when this happens, this may not be the ideal user experience for your application. If so, please refer to the next section on how to work around this.
@@ -647,7 +668,7 @@ The details specific to the Android platform are also specified. This includes t
### Scheduling a notification
-Starting in version 2.0 of the plugin, scheduling notifications now requires developers to specify a date and time relative to a specific time zone. This is to solve issues with daylight saving time that existed in the `schedule` method that is now deprecated. A new `zonedSchedule` method is provided that expects an instance `TZDateTime` class provided by the [`timezone`](https://pub.dev/packages/timezone) package. Even though the `timezone` package is be a transitive dependency via this plugin, it is recommended based on [this lint rule](https://dart-lang.github.io/linter/lints/depend_on_referenced_packages.html) that you also add the `timezone` package as a direct dependency.
+Starting in version 2.0 of the plugin, scheduling notifications now requires developers to specify a date and time relative to a specific time zone. This is to solve issues with daylight saving time that existed in the `schedule` method that is now deprecated. A new `zonedSchedule` method is provided that expects an instance `TZDateTime` class provided by the [`timezone`](https://pub.dev/packages/timezone) package. Even though the `timezone` package is be a transitive dependency via this plugin, it is recommended based on [this lint rule](https://dart-lang.github.io/linter/lints/depend_on_referenced_packages.html) that you also add the `timezone` package as a direct dependency.
Once the depdendency as been added, usage of the `timezone` package requires initialisation that is covered in the package's readme. For convenience the following are code snippets used by the example app.
@@ -699,6 +720,8 @@ If you are trying to update your code so it doesn't use the deprecated methods f
### Periodically show a notification with a specified interval
+**Note** This is not supported on Windows
+
```dart
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
@@ -720,8 +743,7 @@ final List pendingNotificationRequests =
### Retrieving active notifications
-
-
+**Note** On Windows, your app must be packaged as an MSIX to do this. See the limitations section.
```dart
final List activeNotifications =
@@ -805,6 +827,8 @@ await flutterLocalNotificationsPlugin.show(
### Cancelling/deleting a notification
+**Note** On Windows, your app must be packaged as an MSIX to do this. See the limitations section.
+
```dart
// cancel the notification with id value of zero
await flutterLocalNotificationsPlugin.cancel(0);
diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart
index b80c04fc2..bcfc0b06a 100644
--- a/flutter_local_notifications/example/lib/main.dart
+++ b/flutter_local_notifications/example/lib/main.dart
@@ -16,13 +16,15 @@ import 'package:path_provider/path_provider.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
-int id = 0;
+import 'padded_button.dart';
+import 'plugin.dart';
+import 'repeating.dart' as repeating;
+import 'windows.dart' as windows;
-final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
- FlutterLocalNotificationsPlugin();
-
-final StreamController selectNotificationStream =
- StreamController.broadcast();
+/// Streams are created so that app can respond to notification-related events
+/// since the plugin is initialized in the `main` function
+final StreamController selectNotificationStream =
+ StreamController.broadcast();
const MethodChannel platform =
MethodChannel('dexterx.dev/flutter_local_notifications_example');
@@ -35,12 +37,14 @@ class ReceivedNotification {
required this.title,
required this.body,
required this.payload,
+ this.data,
});
final int id;
final String? title;
final String? body;
final String? payload;
+ final Map? data;
}
String? selectedNotificationPayload;
@@ -81,17 +85,6 @@ Future main() async {
await _configureLocalTimeZone();
- final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb &&
- Platform.isLinux
- ? null
- : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
- String initialRoute = HomePage.routeName;
- if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
- selectedNotificationPayload =
- notificationAppLaunchDetails!.notificationResponse?.payload;
- initialRoute = SecondPage.routeName;
- }
-
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('app_icon');
@@ -149,34 +142,38 @@ Future main() async {
requestSoundPermission: false,
notificationCategories: darwinNotificationCategories,
);
+
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(
defaultActionName: 'Open notification',
defaultIcon: AssetsLinuxIcon('icons/app_icon.png'),
);
+
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
linux: initializationSettingsLinux,
+ windows: windows.initSettings,
);
+
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
- onDidReceiveNotificationResponse:
- (NotificationResponse notificationResponse) {
- switch (notificationResponse.notificationResponseType) {
- case NotificationResponseType.selectedNotification:
- selectNotificationStream.add(notificationResponse.payload);
- break;
- case NotificationResponseType.selectedNotificationAction:
- if (notificationResponse.actionId == navigationActionId) {
- selectNotificationStream.add(notificationResponse.payload);
- }
- break;
- }
- },
+ onDidReceiveNotificationResponse: selectNotificationStream.add,
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
+
+ final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb &&
+ Platform.isLinux
+ ? null
+ : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
+ String initialRoute = HomePage.routeName;
+ if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
+ selectedNotificationPayload =
+ notificationAppLaunchDetails!.notificationResponse?.payload;
+ initialRoute = SecondPage.routeName;
+ }
+
runApp(
MaterialApp(
initialRoute: initialRoute,
@@ -193,30 +190,13 @@ Future _configureLocalTimeZone() async {
return;
}
tz.initializeTimeZones();
+ if (Platform.isWindows) {
+ return;
+ }
final String? timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName!));
}
-class PaddedElevatedButton extends StatelessWidget {
- const PaddedElevatedButton({
- required this.buttonText,
- required this.onPressed,
- Key? key,
- }) : super(key: key);
-
- final String buttonText;
- final VoidCallback onPressed;
-
- @override
- Widget build(BuildContext context) => Padding(
- padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
- child: ElevatedButton(
- onPressed: onPressed,
- child: Text(buttonText),
- ),
- );
-}
-
class HomePage extends StatefulWidget {
const HomePage(
this.notificationAppLaunchDetails, {
@@ -238,6 +218,9 @@ class _HomePageState extends State {
final TextEditingController _linuxIconPathController =
TextEditingController();
+ final TextEditingController _windowsRawXmlController =
+ TextEditingController();
+
bool _notificationsEnabled = false;
@override
@@ -294,9 +277,11 @@ class _HomePageState extends State {
}
void _configureSelectNotificationSubject() {
- selectNotificationStream.stream.listen((String? payload) async {
+ selectNotificationStream.stream
+ .listen((NotificationResponse? response) async {
await Navigator.of(context).push(MaterialPageRoute(
- builder: (BuildContext context) => SecondPage(payload),
+ builder: (BuildContext context) =>
+ SecondPage(response?.payload, data: response?.data),
));
});
}
@@ -392,50 +377,6 @@ class _HomePageState extends State {
await _zonedScheduleAlarmClockNotification();
},
),
- PaddedElevatedButton(
- buttonText: 'Repeat notification every minute',
- onPressed: () async {
- await _repeatNotification();
- },
- ),
- PaddedElevatedButton(
- buttonText: 'Repeat notification every 5 minutes',
- onPressed: () async {
- await _repeatPeriodicallyWithDurationNotification();
- },
- ),
- PaddedElevatedButton(
- buttonText:
- 'Schedule daily 10:00:00 am notification in your '
- 'local time zone',
- onPressed: () async {
- await _scheduleDailyTenAMNotification();
- },
- ),
- PaddedElevatedButton(
- buttonText:
- 'Schedule daily 10:00:00 am notification in your '
- "local time zone using last year's date",
- onPressed: () async {
- await _scheduleDailyTenAMLastYearNotification();
- },
- ),
- PaddedElevatedButton(
- buttonText:
- 'Schedule weekly 10:00:00 am notification in your '
- 'local time zone',
- onPressed: () async {
- await _scheduleWeeklyTenAMNotification();
- },
- ),
- PaddedElevatedButton(
- buttonText:
- 'Schedule weekly Monday 10:00:00 am notification '
- 'in your local time zone',
- onPressed: () async {
- await _scheduleWeeklyMondayTenAMNotification();
- },
- ),
PaddedElevatedButton(
buttonText: 'Check pending notifications',
onPressed: () async {
@@ -449,22 +390,6 @@ class _HomePageState extends State {
},
),
],
- PaddedElevatedButton(
- buttonText:
- 'Schedule monthly Monday 10:00:00 am notification in '
- 'your local time zone',
- onPressed: () async {
- await _scheduleMonthlyMondayTenAMNotification();
- },
- ),
- PaddedElevatedButton(
- buttonText:
- 'Schedule yearly Monday 10:00:00 am notification in '
- 'your local time zone',
- onPressed: () async {
- await _scheduleYearlyMondayTenAMNotification();
- },
- ),
PaddedElevatedButton(
buttonText: 'Show notification from silent channel',
onPressed: () async {
@@ -490,6 +415,7 @@ class _HomePageState extends State {
await _cancelAllNotifications();
},
),
+ if (!Platform.isWindows) ...repeating.examples(context),
const Divider(),
const Text(
'Notifications with actions',
@@ -1040,6 +966,11 @@ class _HomePageState extends State {
},
),
],
+ if (!kIsWeb && Platform.isWindows)
+ ...windows.examples(
+ xmlController: _windowsRawXmlController,
+ showXmlNotification: _showWindowsNotificationWithRawXml,
+ ),
],
),
),
@@ -1119,11 +1050,33 @@ class _HomePageState extends State {
],
);
- const NotificationDetails notificationDetails = NotificationDetails(
+ final WindowsNotificationDetails windowsNotificationsDetails =
+ WindowsNotificationDetails(
+ subtitle: 'Click the three dots for another button',
+ actions: [
+ const WindowsAction(
+ content: 'Text',
+ arguments: 'text',
+ ),
+ WindowsAction(
+ content: 'Image',
+ arguments: 'image',
+ image: File('icons/coworker.png').absolute,
+ ),
+ const WindowsAction(
+ content: 'Context',
+ arguments: 'context',
+ placement: WindowsActionPlacement.contextMenu,
+ ),
+ ],
+ );
+
+ final NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: iosNotificationDetails,
macOS: macOSNotificationDetails,
linux: linuxNotificationDetails,
+ windows: windowsNotificationsDetails,
);
await flutterLocalNotificationsPlugin.show(
id++, 'plain title', 'plain body', notificationDetails,
@@ -1158,10 +1111,23 @@ class _HomePageState extends State {
categoryIdentifier: darwinNotificationCategoryText,
);
+ const WindowsNotificationDetails windowsNotificationDetails =
+ WindowsNotificationDetails(
+ actions: [
+ WindowsAction(
+ content: 'Send', arguments: 'send-reply', inputId: 'text'),
+ ],
+ inputs: [
+ WindowsTextInput(
+ id: 'text', title: 'Send a reply?', placeHolderContent: 'Message'),
+ ],
+ );
+
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
macOS: darwinNotificationDetails,
+ windows: windowsNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(id++, 'Text Input Notification',
@@ -1218,10 +1184,29 @@ class _HomePageState extends State {
categoryIdentifier: darwinNotificationCategoryText,
);
+ const WindowsNotificationDetails windowsNotificationDetails =
+ WindowsNotificationDetails(
+ actions: [
+ WindowsAction(
+ content: 'Submit', arguments: 'submit', inputId: 'choice'),
+ ],
+ inputs: [
+ WindowsSelectionInput(
+ id: 'choice',
+ defaultItem: 'abc',
+ items: [
+ WindowsSelection(id: 'abc', content: 'abc'),
+ WindowsSelection(id: 'def', content: 'def'),
+ ],
+ ),
+ ],
+ );
+
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
macOS: darwinNotificationDetails,
+ windows: windowsNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
id++, 'plain title', 'plain body', notificationDetails,
@@ -1346,11 +1331,17 @@ class _HomePageState extends State {
LinuxNotificationDetails(
sound: AssetsLinuxSound('sound/slow_spring_board.mp3'),
);
+ final WindowsNotificationDetails windowsNotificationDetails =
+ WindowsNotificationDetails(
+ audio: WindowsNotificationAudio.preset(
+ sound: WindowsNotificationSound.alarm5),
+ );
final NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
macOS: darwinNotificationDetails,
linux: linuxPlatformChannelSpecifics,
+ windows: windowsNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
id++,
@@ -1429,7 +1420,10 @@ class _HomePageState extends State {
DarwinNotificationDetails(
presentSound: false,
);
- const NotificationDetails notificationDetails = NotificationDetails(
+ final WindowsNotificationDetails windowsDetails =
+ WindowsNotificationDetails(audio: WindowsNotificationAudio.silent());
+ final NotificationDetails notificationDetails = NotificationDetails(
+ windows: windowsDetails,
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
macOS: darwinNotificationDetails);
@@ -1449,7 +1443,10 @@ class _HomePageState extends State {
DarwinNotificationDetails(
presentSound: false,
);
- const NotificationDetails notificationDetails = NotificationDetails(
+ final WindowsNotificationDetails windowsDetails =
+ WindowsNotificationDetails(audio: WindowsNotificationAudio.silent());
+ final NotificationDetails notificationDetails = NotificationDetails(
+ windows: windowsDetails,
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
macOS: darwinNotificationDetails);
@@ -1864,166 +1861,6 @@ class _HomePageState extends State {
notificationDetails);
}
- Future _repeatNotification() async {
- const AndroidNotificationDetails androidNotificationDetails =
- AndroidNotificationDetails(
- 'repeating channel id', 'repeating channel name',
- channelDescription: 'repeating description');
- const NotificationDetails notificationDetails =
- NotificationDetails(android: androidNotificationDetails);
- await flutterLocalNotificationsPlugin.periodicallyShow(
- id++,
- 'repeating title',
- 'repeating body',
- RepeatInterval.everyMinute,
- notificationDetails,
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- );
- }
-
- Future _repeatPeriodicallyWithDurationNotification() async {
- const AndroidNotificationDetails androidNotificationDetails =
- AndroidNotificationDetails(
- 'repeating channel id', 'repeating channel name',
- channelDescription: 'repeating description');
- const NotificationDetails notificationDetails =
- NotificationDetails(android: androidNotificationDetails);
- await flutterLocalNotificationsPlugin.periodicallyShowWithDuration(
- id++,
- 'repeating period title',
- 'repeating period body',
- const Duration(minutes: 5),
- notificationDetails,
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- );
- }
-
- Future _scheduleDailyTenAMNotification() async {
- await flutterLocalNotificationsPlugin.zonedSchedule(
- 0,
- 'daily scheduled notification title',
- 'daily scheduled notification body',
- _nextInstanceOfTenAM(),
- const NotificationDetails(
- android: AndroidNotificationDetails('daily notification channel id',
- 'daily notification channel name',
- channelDescription: 'daily notification description'),
- ),
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- uiLocalNotificationDateInterpretation:
- UILocalNotificationDateInterpretation.absoluteTime,
- matchDateTimeComponents: DateTimeComponents.time);
- }
-
- /// To test we don't validate past dates when using `matchDateTimeComponents`
- Future _scheduleDailyTenAMLastYearNotification() async {
- await flutterLocalNotificationsPlugin.zonedSchedule(
- 0,
- 'daily scheduled notification title',
- 'daily scheduled notification body',
- _nextInstanceOfTenAMLastYear(),
- const NotificationDetails(
- android: AndroidNotificationDetails('daily notification channel id',
- 'daily notification channel name',
- channelDescription: 'daily notification description'),
- ),
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- uiLocalNotificationDateInterpretation:
- UILocalNotificationDateInterpretation.absoluteTime,
- matchDateTimeComponents: DateTimeComponents.time);
- }
-
- Future _scheduleWeeklyTenAMNotification() async {
- await flutterLocalNotificationsPlugin.zonedSchedule(
- 0,
- 'weekly scheduled notification title',
- 'weekly scheduled notification body',
- _nextInstanceOfTenAM(),
- const NotificationDetails(
- android: AndroidNotificationDetails('weekly notification channel id',
- 'weekly notification channel name',
- channelDescription: 'weekly notificationdescription'),
- ),
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- uiLocalNotificationDateInterpretation:
- UILocalNotificationDateInterpretation.absoluteTime,
- matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime);
- }
-
- Future _scheduleWeeklyMondayTenAMNotification() async {
- await flutterLocalNotificationsPlugin.zonedSchedule(
- 0,
- 'weekly scheduled notification title',
- 'weekly scheduled notification body',
- _nextInstanceOfMondayTenAM(),
- const NotificationDetails(
- android: AndroidNotificationDetails('weekly notification channel id',
- 'weekly notification channel name',
- channelDescription: 'weekly notificationdescription'),
- ),
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- uiLocalNotificationDateInterpretation:
- UILocalNotificationDateInterpretation.absoluteTime,
- matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime);
- }
-
- Future _scheduleMonthlyMondayTenAMNotification() async {
- await flutterLocalNotificationsPlugin.zonedSchedule(
- 0,
- 'monthly scheduled notification title',
- 'monthly scheduled notification body',
- _nextInstanceOfMondayTenAM(),
- const NotificationDetails(
- android: AndroidNotificationDetails('monthly notification channel id',
- 'monthly notification channel name',
- channelDescription: 'monthly notificationdescription'),
- ),
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- uiLocalNotificationDateInterpretation:
- UILocalNotificationDateInterpretation.absoluteTime,
- matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime);
- }
-
- Future _scheduleYearlyMondayTenAMNotification() async {
- await flutterLocalNotificationsPlugin.zonedSchedule(
- 0,
- 'yearly scheduled notification title',
- 'yearly scheduled notification body',
- _nextInstanceOfMondayTenAM(),
- const NotificationDetails(
- android: AndroidNotificationDetails('yearly notification channel id',
- 'yearly notification channel name',
- channelDescription: 'yearly notification description'),
- ),
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
- uiLocalNotificationDateInterpretation:
- UILocalNotificationDateInterpretation.absoluteTime,
- matchDateTimeComponents: DateTimeComponents.dateAndTime);
- }
-
- tz.TZDateTime _nextInstanceOfTenAM() {
- final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
- tz.TZDateTime scheduledDate =
- tz.TZDateTime(tz.local, now.year, now.month, now.day, 10);
- if (scheduledDate.isBefore(now)) {
- scheduledDate = scheduledDate.add(const Duration(days: 1));
- }
- return scheduledDate;
- }
-
- tz.TZDateTime _nextInstanceOfTenAMLastYear() {
- final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
- return tz.TZDateTime(tz.local, now.year - 1, now.month, now.day, 10);
- }
-
- tz.TZDateTime _nextInstanceOfMondayTenAM() {
- tz.TZDateTime scheduledDate = _nextInstanceOfTenAM();
- while (scheduledDate.weekday != DateTime.monday) {
- scheduledDate = scheduledDate.add(const Duration(days: 1));
- }
- return scheduledDate;
- }
-
Future _showNotificationWithNoBadge() async {
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('no badge channel', 'no badge name',
@@ -2869,6 +2706,16 @@ class _HomePageState extends State {
platformChannelSpecifics,
);
}
+
+ Future? _showWindowsNotificationWithRawXml() =>
+ flutterLocalNotificationsPlugin
+ .resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>()
+ ?.showRawXml(
+ id: id++,
+ xml: _windowsRawXmlController.text,
+ bindings: {'message': 'Hello, World!'},
+ );
}
Future _showLinuxNotificationWithBodyMarkup() async {
@@ -2905,7 +2752,7 @@ Future _showLinuxNotificationWithByteDataIcon() async {
'icons/app_icon_density.png',
);
final image.Image? iconData = image.decodePng(
- assetIcon.buffer.asUint8List().toList(),
+ assetIcon.buffer.asUint8List(),
);
final Uint8List iconBytes = iconData!.getBytes();
final LinuxNotificationDetails linuxPlatformChannelSpecifics =
@@ -3083,12 +2930,14 @@ Future getLinuxCapabilities() =>
class SecondPage extends StatefulWidget {
const SecondPage(
this.payload, {
+ this.data,
Key? key,
}) : super(key: key);
static const String routeName = '/secondPage';
final String? payload;
+ final Map? data;
@override
State createState() => SecondPageState();
@@ -3096,11 +2945,13 @@ class SecondPage extends StatefulWidget {
class SecondPageState extends State {
String? _payload;
+ Map? _data;
@override
void initState() {
super.initState();
_payload = widget.payload;
+ _data = widget.data;
}
@override
@@ -3113,6 +2964,7 @@ class SecondPageState extends State {
mainAxisSize: MainAxisSize.min,
children: [
Text('payload ${_payload ?? ''}'),
+ Text('data ${_data ?? ''}'),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
diff --git a/flutter_local_notifications/example/lib/padded_button.dart b/flutter_local_notifications/example/lib/padded_button.dart
new file mode 100644
index 000000000..5867d5e3e
--- /dev/null
+++ b/flutter_local_notifications/example/lib/padded_button.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+
+class PaddedElevatedButton extends StatelessWidget {
+ const PaddedElevatedButton({
+ required this.buttonText,
+ required this.onPressed,
+ Key? key,
+ }) : super(key: key);
+
+ final String buttonText;
+ final VoidCallback onPressed;
+
+ @override
+ Widget build(BuildContext context) => Padding(
+ padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
+ child: ElevatedButton(
+ onPressed: onPressed,
+ child: Text(buttonText),
+ ),
+ );
+}
diff --git a/flutter_local_notifications/example/lib/plugin.dart b/flutter_local_notifications/example/lib/plugin.dart
new file mode 100644
index 000000000..b64f21809
--- /dev/null
+++ b/flutter_local_notifications/example/lib/plugin.dart
@@ -0,0 +1,6 @@
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
+ FlutterLocalNotificationsPlugin();
+
+int id = 0;
diff --git a/flutter_local_notifications/example/lib/repeating.dart b/flutter_local_notifications/example/lib/repeating.dart
new file mode 100644
index 000000000..8eea896a4
--- /dev/null
+++ b/flutter_local_notifications/example/lib/repeating.dart
@@ -0,0 +1,228 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:timezone/timezone.dart' as tz;
+
+import 'padded_button.dart';
+import 'plugin.dart';
+
+List examples(BuildContext context) => [
+ const Divider(),
+ const Text(
+ 'Repeating notifications',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Repeat notification every minute',
+ onPressed: () async {
+ await _repeatNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Repeat notification every 5 minutes',
+ onPressed: () async {
+ await _repeatPeriodicallyWithDurationNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Schedule daily 10:00:00 am notification in your '
+ 'local time zone',
+ onPressed: () async {
+ await _scheduleDailyTenAMNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Schedule daily 10:00:00 am notification in your '
+ "local time zone using last year's date",
+ onPressed: () async {
+ await _scheduleDailyTenAMLastYearNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Schedule weekly 10:00:00 am notification in your '
+ 'local time zone',
+ onPressed: () async {
+ await _scheduleWeeklyTenAMNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Schedule weekly Monday 10:00:00 am notification '
+ 'in your local time zone',
+ onPressed: () async {
+ await _scheduleWeeklyMondayTenAMNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Schedule monthly Monday 10:00:00 am notification in '
+ 'your local time zone',
+ onPressed: () async {
+ await _scheduleMonthlyMondayTenAMNotification();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Schedule yearly Monday 10:00:00 am notification in '
+ 'your local time zone',
+ onPressed: () async {
+ await _scheduleYearlyMondayTenAMNotification();
+ },
+ ),
+ ];
+
+/// To test we don't validate past dates when using `matchDateTimeComponents`
+Future _scheduleDailyTenAMLastYearNotification() async {
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ 0,
+ 'daily scheduled notification title',
+ 'daily scheduled notification body',
+ _nextInstanceOfTenAMLastYear(),
+ const NotificationDetails(
+ android: AndroidNotificationDetails(
+ 'daily notification channel id', 'daily notification channel name',
+ channelDescription: 'daily notification description'),
+ ),
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ uiLocalNotificationDateInterpretation:
+ UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.time);
+}
+
+Future _scheduleWeeklyTenAMNotification() async {
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ 0,
+ 'weekly scheduled notification title',
+ 'weekly scheduled notification body',
+ _nextInstanceOfTenAM(),
+ const NotificationDetails(
+ android: AndroidNotificationDetails('weekly notification channel id',
+ 'weekly notification channel name',
+ channelDescription: 'weekly notificationdescription'),
+ ),
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ uiLocalNotificationDateInterpretation:
+ UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime);
+}
+
+Future _scheduleWeeklyMondayTenAMNotification() async {
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ 0,
+ 'weekly scheduled notification title',
+ 'weekly scheduled notification body',
+ _nextInstanceOfMondayTenAM(),
+ const NotificationDetails(
+ android: AndroidNotificationDetails('weekly notification channel id',
+ 'weekly notification channel name',
+ channelDescription: 'weekly notificationdescription'),
+ ),
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ uiLocalNotificationDateInterpretation:
+ UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime);
+}
+
+Future _scheduleMonthlyMondayTenAMNotification() async {
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ 0,
+ 'monthly scheduled notification title',
+ 'monthly scheduled notification body',
+ _nextInstanceOfMondayTenAM(),
+ const NotificationDetails(
+ android: AndroidNotificationDetails('monthly notification channel id',
+ 'monthly notification channel name',
+ channelDescription: 'monthly notificationdescription'),
+ ),
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ uiLocalNotificationDateInterpretation:
+ UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime);
+}
+
+Future _scheduleYearlyMondayTenAMNotification() async {
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ 0,
+ 'yearly scheduled notification title',
+ 'yearly scheduled notification body',
+ _nextInstanceOfMondayTenAM(),
+ const NotificationDetails(
+ android: AndroidNotificationDetails('yearly notification channel id',
+ 'yearly notification channel name',
+ channelDescription: 'yearly notification description'),
+ ),
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ uiLocalNotificationDateInterpretation:
+ UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.dateAndTime);
+}
+
+Future _repeatNotification() async {
+ const AndroidNotificationDetails androidNotificationDetails =
+ AndroidNotificationDetails(
+ 'repeating channel id', 'repeating channel name',
+ channelDescription: 'repeating description');
+ const NotificationDetails notificationDetails =
+ NotificationDetails(android: androidNotificationDetails);
+ await flutterLocalNotificationsPlugin.periodicallyShow(
+ id++,
+ 'repeating title',
+ 'repeating body',
+ RepeatInterval.everyMinute,
+ notificationDetails,
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ );
+}
+
+Future _repeatPeriodicallyWithDurationNotification() async {
+ const AndroidNotificationDetails androidNotificationDetails =
+ AndroidNotificationDetails(
+ 'repeating channel id', 'repeating channel name',
+ channelDescription: 'repeating description');
+ const NotificationDetails notificationDetails =
+ NotificationDetails(android: androidNotificationDetails);
+ await flutterLocalNotificationsPlugin.periodicallyShowWithDuration(
+ id++,
+ 'repeating period title',
+ 'repeating period body',
+ const Duration(minutes: 5),
+ notificationDetails,
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ );
+}
+
+Future _scheduleDailyTenAMNotification() async {
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ 0,
+ 'daily scheduled notification title',
+ 'daily scheduled notification body',
+ _nextInstanceOfTenAM(),
+ const NotificationDetails(
+ android: AndroidNotificationDetails(
+ 'daily notification channel id', 'daily notification channel name',
+ channelDescription: 'daily notification description'),
+ ),
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ uiLocalNotificationDateInterpretation:
+ UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.time);
+}
+
+tz.TZDateTime _nextInstanceOfTenAM() {
+ final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
+ tz.TZDateTime scheduledDate =
+ tz.TZDateTime(tz.local, now.year, now.month, now.day, 10);
+ if (scheduledDate.isBefore(now)) {
+ scheduledDate = scheduledDate.add(const Duration(days: 1));
+ }
+ return scheduledDate;
+}
+
+tz.TZDateTime _nextInstanceOfTenAMLastYear() {
+ final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
+ return tz.TZDateTime(tz.local, now.year - 1, now.month, now.day, 10);
+}
+
+tz.TZDateTime _nextInstanceOfMondayTenAM() {
+ tz.TZDateTime scheduledDate = _nextInstanceOfTenAM();
+ while (scheduledDate.weekday != DateTime.monday) {
+ scheduledDate = scheduledDate.add(const Duration(days: 1));
+ }
+ return scheduledDate;
+}
diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart
new file mode 100644
index 000000000..c80a4e70d
--- /dev/null
+++ b/flutter_local_notifications/example/lib/windows.dart
@@ -0,0 +1,408 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+import 'padded_button.dart';
+import 'plugin.dart';
+
+const WindowsInitializationSettings initSettings =
+ WindowsInitializationSettings(
+ appName: 'Flutter Local Notifications Example',
+ appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample',
+ guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb',
+);
+
+List examples({
+ required TextEditingController xmlController,
+ required VoidCallback showXmlNotification,
+}) =>
+ [
+ const Text(
+ 'Windows-specific examples',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show short and long notifications notification',
+ onPressed: () async {
+ await _showWindowsNotificationWithDuration();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show different scenarios',
+ onPressed: () async {
+ await _showWindowsNotificationWithScenarios();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notifications with some detail',
+ onPressed: () async {
+ await _showWindowsNotificationWithDetails();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notifications with image',
+ onPressed: () async {
+ await _showWindowsNotificationWithImages();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notifications with columns',
+ onPressed: () async {
+ await _showWindowsNotificationWithGroups();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notifications with progress bar',
+ onPressed: () async {
+ await _showWindowsNotificationWithProgress();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notifications with dynamic content',
+ onPressed: () async {
+ await _showWindowsNotificationWithDynamic();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notification with activation',
+ onPressed: () async {
+ await _showWindowsNotificationWithActivation();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notification with button styles',
+ onPressed: () async {
+ await _showWindowsNotificationWithButtonStyle();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notifications in a group',
+ onPressed: () async {
+ await _showWindowsNotificationWithHeader();
+ },
+ ),
+ PaddedElevatedButton(
+ buttonText: 'Show notification with raw XML',
+ onPressed: showXmlNotification,
+ ),
+ const SizedBox(height: 8),
+ SizedBox(
+ width: 500,
+ child: ExpansionTile(
+ title: const Text('Click to expand raw XML'),
+ children: [
+ TextField(
+ maxLines: 20,
+ style: const TextStyle(fontFamily: 'RobotoMono'),
+ controller: xmlController,
+ decoration: InputDecoration(
+ hintText: 'Enter the raw xml',
+ helperText: 'Bindings: {message} --> Hello, World!',
+ constraints:
+ const BoxConstraints.tightFor(width: 600, height: 480),
+ suffixIcon: IconButton(
+ icon: const Icon(Icons.clear),
+ onPressed: () => xmlController.clear(),
+ ),
+ ),
+ ),
+ ]),
+ ),
+ ];
+
+Future _showWindowsNotificationWithDuration() async {
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is a short notification',
+ 'This will last about 7 seconds',
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ duration: WindowsNotificationDuration.short),
+ ),
+ );
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is a long notification',
+ 'This will last about 25 seconds',
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ duration: WindowsNotificationDuration.long),
+ ),
+ );
+}
+
+Future _showWindowsNotificationWithScenarios() async {
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is an alarm',
+ null,
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ scenario: WindowsNotificationScenario.alarm,
+ actions: [
+ WindowsAction(content: 'Button', arguments: 'button')
+ ]),
+ ),
+ );
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is an incoming call',
+ null,
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ scenario: WindowsNotificationScenario.incomingCall,
+ actions: [
+ WindowsAction(content: 'Button', arguments: 'button')
+ ]),
+ ),
+ );
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is a reminder',
+ null,
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ scenario: WindowsNotificationScenario.reminder,
+ actions: [
+ WindowsAction(content: 'Button', arguments: 'button')
+ ]),
+ ),
+ );
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is an urgent notification',
+ null,
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ scenario: WindowsNotificationScenario.urgent,
+ actions: [
+ WindowsAction(content: 'Button', arguments: 'button')
+ ]),
+ ),
+ );
+}
+
+Future _showWindowsNotificationWithDetails() =>
+ flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This one has more details',
+ 'And a different timestamp!',
+ NotificationDetails(
+ windows: WindowsNotificationDetails(
+ subtitle: 'This is the subtitle',
+ timestamp:
+ DateTime.now().subtract(const Duration(hours: 2, minutes: 5)),
+ ),
+ ),
+ );
+
+Future _showWindowsNotificationWithImages() =>
+ flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This notification has an image',
+ 'You can only show images from files',
+ NotificationDetails(
+ windows: WindowsNotificationDetails(
+ images: [
+ WindowsImage.file(
+ File('./icons/4.0x/app_icon_density.png').absolute,
+ altText: 'A beautiful image',
+ ),
+ ],
+ ),
+ ),
+ );
+
+Future _showWindowsNotificationWithGroups() =>
+ flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This notification has many groups',
+ 'Each group stays together',
+ NotificationDetails(
+ windows: WindowsNotificationDetails(
+ subtitle: 'Caption text is fainter',
+ rows: [
+ WindowsRow([
+ WindowsColumn([
+ WindowsImage.file(File('icons/coworker.png').absolute,
+ altText: 'A coworker'),
+ const WindowsNotificationText(
+ text: 'A coworker', isCaption: true),
+ ]),
+ WindowsColumn([
+ WindowsImage.file(
+ File('icons/4.0x/app_icon_density.png').absolute,
+ altText: 'The icon'),
+ const WindowsNotificationText(text: 'The icon'),
+ ]),
+ ]),
+ ],
+ ),
+ ),
+ );
+
+Future _showWindowsNotificationWithProgress() async {
+ final WindowsProgressBar fastProgress = WindowsProgressBar(
+ id: 'fast-progress', status: 'Updating quickly...', value: 0);
+ final WindowsProgressBar slowProgress = WindowsProgressBar(
+ id: 'slow-progress',
+ status: 'Updating slowly...',
+ value: 0,
+ label: '0 / 10');
+ final int notificationId = id++;
+ final FlutterLocalNotificationsWindows? windows =
+ flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>();
+ await flutterLocalNotificationsPlugin.show(
+ notificationId,
+ 'This notification has progress bars',
+ 'You can have precise or indeterminate',
+ NotificationDetails(
+ windows: WindowsNotificationDetails(
+ progressBars: [
+ WindowsProgressBar(
+ id: 'indeterminate',
+ title: 'This has indeterminate progress',
+ status: 'Downloading...',
+ value: null,
+ ),
+ WindowsProgressBar(
+ id: 'continuous',
+ title: 'This has continuous progress',
+ status: 'Uploading...',
+ value: 0.75,
+ ),
+ WindowsProgressBar(
+ id: 'discrete',
+ title: 'This has discrete progress',
+ status: 'Syncing...',
+ value: 0.75,
+ label: '9/12 complete'),
+ fastProgress,
+ slowProgress,
+ ],
+ ),
+ ),
+ );
+
+ int count = 0;
+ Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
+ fastProgress.value = fastProgress.value! + 0.05;
+ slowProgress.value = count++ / 50;
+ fastProgress.value = fastProgress.value!.clamp(0, 1);
+ slowProgress.value = slowProgress.value!.clamp(0, 1);
+ if (fastProgress.value == 1 && slowProgress.value == 1) {
+ return timer.cancel();
+ }
+ count = count.clamp(0, 50);
+ slowProgress.label = '$count / 50';
+ await windows?.updateProgressBar(
+ notificationId: notificationId, progressBar: fastProgress);
+ await windows?.updateProgressBar(
+ notificationId: notificationId, progressBar: slowProgress);
+ });
+}
+
+Future _showWindowsNotificationWithDynamic() async {
+ final DateTime start = DateTime.now();
+ final int notificationId = id++;
+ final FlutterLocalNotificationsWindows? windows =
+ flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>();
+ await flutterLocalNotificationsPlugin.show(
+ notificationId,
+ 'Dynamic content',
+ 'This notification will be updated from Dart code',
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ subtitle: '{stopwatch}',
+ ),
+ ),
+ );
+ Map getBindings() => {
+ 'stopwatch':
+ 'Elapsed time: ${DateTime.now().difference(start).inSeconds} seconds',
+ };
+ await windows?.updateBindings(id: notificationId, bindings: getBindings());
+ Timer.periodic(const Duration(seconds: 1), (Timer timer) async {
+ if (timer.tick > 10) {
+ timer.cancel();
+ await flutterLocalNotificationsPlugin.cancel(notificationId);
+ return;
+ }
+ await windows?.updateBindings(id: notificationId, bindings: getBindings());
+ });
+}
+
+Future _showWindowsNotificationWithActivation() =>
+ flutterLocalNotificationsPlugin.show(
+ id++,
+ 'These buttons do different things',
+ 'Click on each one!',
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ actions: [
+ WindowsAction(
+ content: 'Loading',
+ arguments: 'loading',
+ activationBehavior: WindowsNotificationBehavior.pendingUpdate,
+ ),
+ WindowsAction(
+ content: 'Google',
+ arguments: 'https://google.com',
+ activationType: WindowsActivationType.protocol,
+ activationBehavior: WindowsNotificationBehavior.pendingUpdate,
+ ),
+ ],
+ ),
+ ),
+ );
+
+Future _showWindowsNotificationWithButtonStyle() =>
+ flutterLocalNotificationsPlugin.show(
+ id++,
+ 'Incoming call',
+ 'Your best friend',
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(
+ actions: [
+ WindowsAction(
+ content: 'Accept',
+ arguments: 'accept',
+ buttonStyle: WindowsButtonStyle.success,
+ ),
+ WindowsAction(
+ content: 'Reject',
+ arguments: 'reject',
+ buttonStyle: WindowsButtonStyle.critical,
+ ),
+ ],
+ ),
+ ),
+ );
+
+Future _showWindowsNotificationWithHeader() async {
+ const WindowsHeader header = WindowsHeader(
+ id: 'header',
+ title: 'Cool notifications',
+ arguments: 'header-clicked',
+ );
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is the first notification',
+ null,
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(header: header),
+ ),
+ );
+ await flutterLocalNotificationsPlugin.show(
+ id++,
+ 'This is the second notification',
+ null,
+ const NotificationDetails(
+ windows: WindowsNotificationDetails(header: header),
+ ),
+ );
+}
diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml
index 57dfa6023..ade28741c 100644
--- a/flutter_local_notifications/example/pubspec.yaml
+++ b/flutter_local_notifications/example/pubspec.yaml
@@ -10,9 +10,10 @@ dependencies:
flutter_local_notifications:
path: ../
flutter_timezone: ^1.0.4
- http: ^0.13.4
- image: ^3.0.8
+ http: ^1.2.1
+ image: ^4.2.0
path_provider: ^2.0.0
+ timezone: ^0.9.4
dev_dependencies:
flutter_driver:
@@ -21,6 +22,7 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
+ msix: ^3.16.7
flutter:
uses-material-design: true
@@ -31,3 +33,15 @@ flutter:
environment:
sdk: ^3.1.0
flutter: ">=3.1.3"
+
+msix_config:
+ display_name: Flutter Local Notifications Example
+ identity_name: Com.Example.FlutterLocalNotificationsExample
+ msix_version: 1.0.0.0
+ store: false
+ install_certificate: false
+ output_name: example
+ toast_activator:
+ clsid: "d49b0314-ee7a-4626-bf79-97cdb8a991bb"
+ arguments: "msix-args"
+ display_name: "Flutter Local Notifications"
diff --git a/flutter_local_notifications/example/windows/.gitignore b/flutter_local_notifications/example/windows/.gitignore
new file mode 100644
index 000000000..571c3131e
--- /dev/null
+++ b/flutter_local_notifications/example/windows/.gitignore
@@ -0,0 +1,21 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+.vs
+out
+flutter/ephemeral
diff --git a/flutter_local_notifications/example/windows/CMakeLists.txt b/flutter_local_notifications/example/windows/CMakeLists.txt
new file mode 100644
index 000000000..1633297a0
--- /dev/null
+++ b/flutter_local_notifications/example/windows/CMakeLists.txt
@@ -0,0 +1,95 @@
+cmake_minimum_required(VERSION 3.14)
+project(example LANGUAGES CXX)
+
+set(BINARY_NAME "example")
+
+cmake_policy(SET CMP0063 NEW)
+
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Configure build options.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
+endfunction()
+
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+
+# Flutter library and tool build rules.
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build
+add_subdirectory("runner")
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/flutter_local_notifications/example/windows/flutter/CMakeLists.txt b/flutter_local_notifications/example/windows/flutter/CMakeLists.txt
new file mode 100644
index 000000000..4f2af69bb
--- /dev/null
+++ b/flutter_local_notifications/example/windows/flutter/CMakeLists.txt
@@ -0,0 +1,108 @@
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 000000000..8b6d4680a
--- /dev/null
+++ b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+}
diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 000000000..dc139d85a
--- /dev/null
+++ b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake
new file mode 100644
index 000000000..93f25e155
--- /dev/null
+++ b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ flutter_local_notifications_windows
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/flutter_local_notifications/example/windows/runner/CMakeLists.txt b/flutter_local_notifications/example/windows/runner/CMakeLists.txt
new file mode 100644
index 000000000..de2d8916b
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+apply_standard_settings(${BINARY_NAME})
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/flutter_local_notifications/example/windows/runner/RCa04060 b/flutter_local_notifications/example/windows/runner/RCa04060
new file mode 100644
index 000000000..a890c5b8c
Binary files /dev/null and b/flutter_local_notifications/example/windows/runner/RCa04060 differ
diff --git a/flutter_local_notifications/example/windows/runner/RCb04060 b/flutter_local_notifications/example/windows/runner/RCb04060
new file mode 100644
index 000000000..a890c5b8c
Binary files /dev/null and b/flutter_local_notifications/example/windows/runner/RCb04060 differ
diff --git a/flutter_local_notifications/example/windows/runner/Runner.rc b/flutter_local_notifications/example/windows/runner/Runner.rc
new file mode 100644
index 000000000..f377c3592
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.dexterous" "\0"
+ VALUE "FileDescription", "example" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "example" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2022 com.dexterous. All rights reserved." "\0"
+ VALUE "OriginalFilename", "example.exe" "\0"
+ VALUE "ProductName", "example" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/flutter_local_notifications/example/windows/runner/flutter_window.cpp b/flutter_local_notifications/example/windows/runner/flutter_window.cpp
new file mode 100644
index 000000000..b43b9095e
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/flutter_window.cpp
@@ -0,0 +1,61 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/flutter_local_notifications/example/windows/runner/flutter_window.h b/flutter_local_notifications/example/windows/runner/flutter_window.h
new file mode 100644
index 000000000..6da0652f0
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/flutter_local_notifications/example/windows/runner/main.cpp b/flutter_local_notifications/example/windows/runner/main.cpp
new file mode 100644
index 000000000..87602243e
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/main.cpp
@@ -0,0 +1,44 @@
+#include
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.CreateAndShow(L"example", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/flutter_local_notifications/example/windows/runner/resource.h b/flutter_local_notifications/example/windows/runner/resource.h
new file mode 100644
index 000000000..66a65d1e4
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/flutter_local_notifications/example/windows/runner/resources/app_icon.ico b/flutter_local_notifications/example/windows/runner/resources/app_icon.ico
new file mode 100644
index 000000000..c04e20caf
Binary files /dev/null and b/flutter_local_notifications/example/windows/runner/resources/app_icon.ico differ
diff --git a/flutter_local_notifications/example/windows/runner/runner.exe.manifest b/flutter_local_notifications/example/windows/runner/runner.exe.manifest
new file mode 100644
index 000000000..c977c4a42
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/runner.exe.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flutter_local_notifications/example/windows/runner/utils.cpp b/flutter_local_notifications/example/windows/runner/utils.cpp
new file mode 100644
index 000000000..d19bdbbcc
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/utils.cpp
@@ -0,0 +1,64 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr);
+ if (target_length == 0) {
+ return std::string();
+ }
+ std::string utf8_string;
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, utf8_string.data(),
+ target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/flutter_local_notifications/example/windows/runner/utils.h b/flutter_local_notifications/example/windows/runner/utils.h
new file mode 100644
index 000000000..3879d5475
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/flutter_local_notifications/example/windows/runner/win32_window.cpp b/flutter_local_notifications/example/windows/runner/win32_window.cpp
new file mode 100644
index 000000000..c10f08dc7
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/win32_window.cpp
@@ -0,0 +1,245 @@
+#include "win32_window.h"
+
+#include
+
+#include "resource.h"
+
+namespace {
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ FreeLibrary(user32_module);
+ }
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::CreateAndShow(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ return OnCreate();
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
diff --git a/flutter_local_notifications/example/windows/runner/win32_window.h b/flutter_local_notifications/example/windows/runner/win32_window.h
new file mode 100644
index 000000000..17ba43112
--- /dev/null
+++ b/flutter_local_notifications/example/windows/runner/win32_window.h
@@ -0,0 +1,98 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates and shows a win32 window with |title| and position and size using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size to will treat the width height passed in to this function
+ // as logical pixels and scale to appropriate for the default monitor. Returns
+ // true if the window was created successfully.
+ bool CreateAndShow(const std::wstring& title,
+ const Point& origin,
+ const Size& size);
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responsponds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart
index 21f06768e..c97d57460 100644
--- a/flutter_local_notifications/lib/flutter_local_notifications.dart
+++ b/flutter_local_notifications/lib/flutter_local_notifications.dart
@@ -1,14 +1,6 @@
export 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
-export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'
- show
- DidReceiveBackgroundNotificationResponseCallback,
- DidReceiveNotificationResponseCallback,
- PendingNotificationRequest,
- ActiveNotification,
- RepeatInterval,
- NotificationAppLaunchDetails,
- NotificationResponse,
- NotificationResponseType;
+export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
+export 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart';
export 'src/flutter_local_notifications_plugin.dart';
export 'src/initialization_settings.dart';
@@ -44,5 +36,6 @@ export 'src/platform_specifics/darwin/notification_category_option.dart';
export 'src/platform_specifics/darwin/notification_details.dart';
export 'src/platform_specifics/darwin/notification_enabled_options.dart';
export 'src/platform_specifics/ios/enums.dart';
+
export 'src/typedefs.dart';
export 'src/types.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 aae3c05d9..bc0265fe1 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_windows/flutter_local_notifications_windows.dart';
import 'package:timezone/timezone.dart';
import 'initialization_settings.dart';
@@ -69,6 +70,11 @@ class FlutterLocalNotificationsPlugin {
FlutterLocalNotificationsPlatform.instance
is LinuxFlutterLocalNotificationsPlugin) {
return FlutterLocalNotificationsPlatform.instance as T?;
+ } else if (defaultTargetPlatform == TargetPlatform.windows &&
+ T == FlutterLocalNotificationsWindows &&
+ FlutterLocalNotificationsPlatform.instance
+ is FlutterLocalNotificationsWindows) {
+ return FlutterLocalNotificationsPlatform.instance as T?;
}
return null;
@@ -167,6 +173,18 @@ class FlutterLocalNotificationsPlugin {
initializationSettings.linux!,
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
);
+ } else if (defaultTargetPlatform == TargetPlatform.windows) {
+ if (initializationSettings.windows == null) {
+ throw ArgumentError(
+ 'Windows settings must be set when targeting Windows platform.');
+ }
+
+ return await resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>()
+ ?.initialize(
+ initializationSettings.windows!,
+ onNotificationReceived: onDidReceiveNotificationResponse,
+ );
}
return true;
}
@@ -200,6 +218,10 @@ class FlutterLocalNotificationsPlugin {
return await resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()
?.getNotificationAppLaunchDetails();
+ } else if (defaultTargetPlatform == TargetPlatform.windows) {
+ return await resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>()
+ ?.getNotificationAppLaunchDetails();
} else {
return await FlutterLocalNotificationsPlatform.instance
.getNotificationAppLaunchDetails() ??
@@ -242,6 +264,11 @@ class FlutterLocalNotificationsPlugin {
?.show(id, title, body,
notificationDetails: notificationDetails?.linux,
payload: payload);
+ } else if (defaultTargetPlatform == TargetPlatform.windows) {
+ await resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>()
+ ?.show(id, title, body,
+ details: notificationDetails?.windows, payload: payload);
} else {
await FlutterLocalNotificationsPlatform.instance.show(id, title, body);
}
@@ -307,6 +334,9 @@ class FlutterLocalNotificationsPlugin {
/// On Android, this will also require additional setup for the app,
/// especially in the app's `AndroidManifest.xml` file. Please see check the
/// readme for further details.
+ ///
+ /// On Windows, this will only set a notification on the [scheduledDate], and
+ /// not repeat, regardless of the value for [matchDateTimeComponents].
Future zonedSchedule(
int id,
String? title,
@@ -346,6 +376,17 @@ class FlutterLocalNotificationsPlugin {
id, title, body, scheduledDate, notificationDetails.macOS,
payload: payload,
matchDateTimeComponents: matchDateTimeComponents);
+ } else if (defaultTargetPlatform == TargetPlatform.windows) {
+ await resolvePlatformSpecificImplementation<
+ FlutterLocalNotificationsWindows>()
+ ?.zonedSchedule(
+ id,
+ title,
+ body,
+ scheduledDate,
+ notificationDetails.windows,
+ payload: payload,
+ );
} else {
throw UnimplementedError('zonedSchedule() has not been implemented');
}
@@ -389,6 +430,8 @@ class FlutterLocalNotificationsPlugin {
MacOSFlutterLocalNotificationsPlugin>()
?.periodicallyShow(id, title, body, repeatInterval,
notificationDetails: notificationDetails.macOS, payload: payload);
+ } else if (defaultTargetPlatform == TargetPlatform.windows) {
+ throw UnsupportedError('Notifications do not repeat on Windows');
} else {
await FlutterLocalNotificationsPlatform.instance
.periodicallyShow(id, title, body, repeatInterval);
@@ -457,6 +500,10 @@ class FlutterLocalNotificationsPlugin {
/// - macOS: macOS 10.14 or newer
///
/// On Linux it will throw an [UnimplementedError].
+ ///
+ /// On Windows, your application must be packaged as an MSIX to be able
+ /// to use this API. If not, this function will return an empty list.
+ /// For more details, see: https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/modernize-wpf-tutorial-5
Future> getActiveNotifications() =>
FlutterLocalNotificationsPlatform.instance.getActiveNotifications();
}
diff --git a/flutter_local_notifications/lib/src/initialization_settings.dart b/flutter_local_notifications/lib/src/initialization_settings.dart
index edad98426..ddd7b356d 100644
--- a/flutter_local_notifications/lib/src/initialization_settings.dart
+++ b/flutter_local_notifications/lib/src/initialization_settings.dart
@@ -1,7 +1,4 @@
-import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
-
-import 'platform_specifics/android/initialization_settings.dart';
-import 'platform_specifics/darwin/initialization_settings.dart';
+import '../flutter_local_notifications.dart';
/// Settings for initializing the plugin for each platform.
class InitializationSettings {
@@ -11,6 +8,7 @@ class InitializationSettings {
this.iOS,
this.macOS,
this.linux,
+ this.windows,
});
/// Settings for Android.
@@ -36,4 +34,7 @@ class InitializationSettings {
/// It is nullable, because we don't want to force users to specify settings
/// for platforms that they don't target.
final LinuxInitializationSettings? linux;
+
+ /// Settings for Windows.
+ final WindowsInitializationSettings? windows;
}
diff --git a/flutter_local_notifications/lib/src/notification_details.dart b/flutter_local_notifications/lib/src/notification_details.dart
index feb9b1a79..070b03ae1 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_windows/flutter_local_notifications_windows.dart';
import 'platform_specifics/android/notification_details.dart';
import 'platform_specifics/darwin/notification_details.dart';
@@ -11,6 +12,7 @@ class NotificationDetails {
this.iOS,
this.macOS,
this.linux,
+ this.windows,
});
/// Notification details for Android.
@@ -24,4 +26,7 @@ class NotificationDetails {
/// Notification details for Linux.
final LinuxNotificationDetails? linux;
+
+ /// Notification details for Windows.
+ final WindowsNotificationDetails? windows;
}
diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart
index fcc059825..d5e69b4ca 100644
--- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart
+++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart
@@ -52,8 +52,9 @@ class MethodChannelFlutterLocalNotificationsPlugin
result != null && result.containsKey('notificationResponse')
? result['notificationResponse']
: null;
- return result != null
- ? NotificationAppLaunchDetails(
+ return result == null
+ ? null
+ : NotificationAppLaunchDetails(
result['notificationLaunchedApp'],
notificationResponse: notificationResponse == null
? null
@@ -66,9 +67,11 @@ class MethodChannelFlutterLocalNotificationsPlugin
payload: notificationResponse.containsKey('payload')
? notificationResponse['payload']
: null,
+ data: Map.from(
+ notificationResponse['data'] ?? {},
+ ),
),
- )
- : null;
+ );
}
@override
@@ -113,7 +116,7 @@ class AndroidFlutterLocalNotificationsPlugin
AndroidFlutterLocalNotificationsPlugin();
}
- DidReceiveNotificationResponseCallback? _ondidReceiveNotificationResponse;
+ DidReceiveNotificationResponseCallback? _onDidReceiveNotificationResponse;
/// Initializes the plugin.
///
@@ -137,7 +140,7 @@ class AndroidFlutterLocalNotificationsPlugin
DidReceiveBackgroundNotificationResponseCallback?
onDidReceiveBackgroundNotificationResponse,
}) async {
- _ondidReceiveNotificationResponse = onDidReceiveNotificationResponse;
+ _onDidReceiveNotificationResponse = onDidReceiveNotificationResponse;
_channel.setMethodCallHandler(_handleMethod);
final Map arguments = initializationSettings.toMap();
@@ -230,7 +233,7 @@ class AndroidFlutterLocalNotificationsPlugin
/// a foreground service with a notification id of 0.
///
/// Since not all users of this plugin need such a service, it was not
- /// added to this plugins Android manifest. Thie means you have to add
+ /// added to this plugins Android manifest. This means you have to add
/// it if you want to use the foreground service functionality. Add the
/// foreground service permission to your apps `AndroidManifest.xml` like
/// described in the [official Android documentation](https://developer.android.com/guide/components/foreground-services#request-foreground-service-permissions):
@@ -257,11 +260,11 @@ class AndroidFlutterLocalNotificationsPlugin
/// The notification of the foreground service can be updated by
/// simply calling this method multiple times.
///
- /// Information on selecting an appropriate `startType` for your app's usecase
- /// should be taken from the official Android documentation, check [`Service.onStartCommand`](https://developer.android.com/reference/android/app/Service#onStartCommand(android.content.Intent,%20int,%20int)).
+ /// Information on selecting an appropriate `startType` for your app's use
+ /// case should be taken from the official Android documentation, check [`Service.onStartCommand`](https://developer.android.com/reference/android/app/Service#onStartCommand(android.content.Intent,%20int,%20int)).
/// The there mentioned constants can be found in [AndroidServiceStartType].
///
- /// The notification for the foreground service will not be dismissable
+ /// The notification for the foreground service will not be dismissible
/// and automatically removed when using [stopForegroundService].
///
/// `foregroundServiceType` is a set of foreground service types to apply to
@@ -581,7 +584,7 @@ class AndroidFlutterLocalNotificationsPlugin
Future _handleMethod(MethodCall call) async {
switch (call.method) {
case 'didReceiveNotificationResponse':
- _ondidReceiveNotificationResponse?.call(
+ _onDidReceiveNotificationResponse?.call(
NotificationResponse(
id: call.arguments['notificationId'],
actionId: call.arguments['actionId'],
@@ -613,7 +616,7 @@ class IOSFlutterLocalNotificationsPlugin
///
/// Call this method on application before using the plugin further.
///
- /// Initialisation may also request notification permissions where users will
+ /// Initialization may also request notification permissions where users will
/// see a permissions prompt. This may be fine in cases where it's acceptable
/// to do this when the application runs for the first time. However, if your
/// application needs to do this at a later point in time, set the
@@ -841,7 +844,7 @@ class MacOSFlutterLocalNotificationsPlugin
/// Call this method on application before using the plugin further.
/// This should only be done once.
///
- /// Initialisation may also request notification permissions where users will
+ /// Initialization may also request notification permissions where users will
/// see a permissions prompt. This may be fine in cases where it's acceptable
/// to do this when the application runs for the first time. However, if your
/// application needs to do this at a later point in time, set the
@@ -1035,7 +1038,7 @@ void _evaluateBackgroundNotificationCallback(
final CallbackHandle? callback = PluginUtilities.getCallbackHandle(
didReceiveBackgroundNotificationResponseCallback);
assert(callback != null, '''
- The backgroundHandler needs to be either a static function or a top
+ The backgroundHandler needs to be either a static function or a top
level function to be accessible as a Flutter entry point.''');
final CallbackHandle? dispatcher =
diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml
index 5c365b0b7..f96d9f61c 100644
--- a/flutter_local_notifications/pubspec.yaml
+++ b/flutter_local_notifications/pubspec.yaml
@@ -1,6 +1,6 @@
name: flutter_local_notifications
description: A cross platform plugin for displaying and scheduling local
- notifications for Flutter applications with the ability to customise for each
+ notifications for Flutter applications with the ability to customize for each
platform.
version: 18.0.0
homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications
@@ -11,7 +11,8 @@ dependencies:
flutter:
sdk: flutter
flutter_local_notifications_linux: ^5.0.0
- flutter_local_notifications_platform_interface: ^8.0.0
+ flutter_local_notifications_windows: ^1.0.0
+ flutter_local_notifications_platform_interface: ^8.1.0
timezone: ^0.9.0
dev_dependencies:
@@ -37,7 +38,9 @@ flutter:
dartPluginClass: MacOSFlutterLocalNotificationsPlugin
linux:
default_package: flutter_local_notifications_linux
+ windows:
+ default_package: flutter_local_notifications_windows
environment:
- sdk: ^3.1.0
- flutter: ">=3.13.0"
+ sdk: ^3.3.0
+ flutter: ">=3.19.0"
diff --git a/flutter_local_notifications/test/flutter_local_notifications_test.dart b/flutter_local_notifications/test/flutter_local_notifications_test.dart
index c9fdfbc84..b47635843 100644
--- a/flutter_local_notifications/test/flutter_local_notifications_test.dart
+++ b/flutter_local_notifications/test/flutter_local_notifications_test.dart
@@ -1,6 +1,5 @@
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
-import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
diff --git a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart
index 5521a30c0..387968afb 100644
--- a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart
+++ b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart
@@ -87,10 +87,10 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface {
/// Returns the list of active notifications shown by the application that
/// haven't been dismissed/removed.
///
- /// Throws a [PlatformException] with an `unsupported_os_version` error code
- /// when the OS version is older than what is supported to have results
- /// returned. On platforms that don't support the method at all,
- /// it will throw an [UnimplementedError].
+ /// Throws a [PlatformException](https://api.flutter.dev/flutter/services/PlatformException-class.html)
+ /// with an `unsupported_os_version` error code when the OS version is older
+ /// than what is supported to have results returned. On platforms that don't
+ /// support the method at all, it will throw an [UnimplementedError].
Future> getActiveNotifications() {
throw UnimplementedError(
'getActiveNotifications() has not been implemented');
diff --git a/flutter_local_notifications_platform_interface/lib/src/types.dart b/flutter_local_notifications_platform_interface/lib/src/types.dart
index d4b94a25e..e97734f47 100644
--- a/flutter_local_notifications_platform_interface/lib/src/types.dart
+++ b/flutter_local_notifications_platform_interface/lib/src/types.dart
@@ -93,6 +93,7 @@ class NotificationResponse {
this.actionId,
this.input,
this.payload,
+ this.data = const {},
});
/// The notification's id.
@@ -106,11 +107,17 @@ class NotificationResponse {
/// The value of the input field if the notification action had an input
/// field.
+ ///
+ /// On Windows, this is always null. Instead, [data] holds the values of
+ /// each input with the input's ID as the key.
final String? input;
/// The notification's payload.
final String? payload;
+ /// Any other data returned by the platform.
+ final Map data;
+
/// The notification response type.
final NotificationResponseType notificationResponseType;
}
diff --git a/flutter_local_notifications_platform_interface/pubspec.yaml b/flutter_local_notifications_platform_interface/pubspec.yaml
index e3b2bd3b2..181f73c6e 100644
--- a/flutter_local_notifications_platform_interface/pubspec.yaml
+++ b/flutter_local_notifications_platform_interface/pubspec.yaml
@@ -1,6 +1,6 @@
name: flutter_local_notifications_platform_interface
description: A common platform interface for the flutter_local_notifications plugin.
-version: 8.0.0
+version: 8.1.0
homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface
environment:
@@ -8,8 +8,6 @@ environment:
flutter: ">=3.13.0"
dependencies:
- flutter:
- sdk: flutter
plugin_platform_interface: ^2.0.0
dev_dependencies:
diff --git a/flutter_local_notifications_windows/.gitignore b/flutter_local_notifications_windows/.gitignore
new file mode 100644
index 000000000..ac5aa9893
--- /dev/null
+++ b/flutter_local_notifications_windows/.gitignore
@@ -0,0 +1,29 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+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/
+build/
diff --git a/flutter_local_notifications_windows/.metadata b/flutter_local_notifications_windows/.metadata
new file mode 100644
index 000000000..fc2d1c605
--- /dev/null
+++ b/flutter_local_notifications_windows/.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: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
+ channel: "stable"
+
+project_type: plugin_ffi
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
+ base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
+ - platform: windows
+ create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
+ base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
+
+ # 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_windows/CHANGELOG.md b/flutter_local_notifications_windows/CHANGELOG.md
new file mode 100644
index 000000000..89dfc0ddb
--- /dev/null
+++ b/flutter_local_notifications_windows/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+* Initial release for Windows
diff --git a/flutter_local_notifications_windows/LICENSE b/flutter_local_notifications_windows/LICENSE
new file mode 100644
index 000000000..ba75c69f7
--- /dev/null
+++ b/flutter_local_notifications_windows/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/flutter_local_notifications_windows/README.md b/flutter_local_notifications_windows/README.md
new file mode 100644
index 000000000..09b1d0c21
--- /dev/null
+++ b/flutter_local_notifications_windows/README.md
@@ -0,0 +1,48 @@
+# flutter_local_notifications_windows
+
+The Windows implementation of `package:flutter_local_notifications` as an FFI package that can be run in plain Dart or as a Flutter plugin. See [the docs on FFI](https://dart.dev/interop/c-interop).
+
+## Limitations
+
+- 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).
+
+## Project structure
+
+This template uses the following structure:
+
+- `src`: Contains the native source code, and a CmakeFile.txt file for building
+ that source code into a dynamic library. Within this folder, there are three C++ files:
+ - `ffi_api.h`/`ffi_api.cpp`: A C-compatible header file with the API that will be used by Dart, and the C++ implementation of that API
+ - `plugin.hpp`/`plugin.cpp`: A C++ class holding handles to the [C++/WinRT](https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/) SDK, along with some Windows-heavy logic. `ffi_api.cpp` implements its features using this class.
+ - `utils.hpp`/`utils.cpp` handle copying and allocating data from C structs to WinRT classes and vice-versa. Since FFI is done over C-based APIs, C++ types like strings, maps, and vectors need to be translated.
+
+- `lib`: Contains the Dart code that defines the API of the plugin, and which
+ calls into the native code using `dart:ffi`.
+ - The `details` folder holds all the Windows-specific notification configurations such as `WindowsAction`, `WindowsImage`, etc.
+ - The `ffi` folder holds the generated bindings (see below) and other FFI utilities.
+ - The `plugin` folder implements `package:flutter_local_notifications_platform_interface` in two ways: a stub for platforms that don't support FFI, and an FFI-based implementation.
+
+- The `windows` folder contains the build files for building and bundling the native code library with the platform application.
+
+## Building and bundling native code
+
+The code in `src` can be built with CMake. A `build.bat` file is included, which has the following code:
+
+```batch
+@echo off
+cd build
+cmake ../windows
+cmake --build .
+cd ..
+copy build\shared\Debug\flutter_local_notifications_windows.dll .
+```
+
+This generates a DLL from the native code and copies it to the current directory. This is useful for testing locally without Flutter. When using Flutter, this step is unnecessary as Flutter will build and bundle the assets for you.
+
+## Binding to native code
+
+To use the native code, bindings in Dart are needed.
+To avoid writing these by hand, they are generated from the header file
+`src/ffi_api.h` by `package:ffigen`.
+Regenerate the bindings by running `dart run ffigen --config ffigen.yaml`.
diff --git a/flutter_local_notifications_windows/bin/crash.dart b/flutter_local_notifications_windows/bin/crash.dart
new file mode 100644
index 000000000..7354403cd
--- /dev/null
+++ b/flutter_local_notifications_windows/bin/crash.dart
@@ -0,0 +1,72 @@
+// This file demonstrates how the plugin is _not_ thread safe.
+//
+// This crash can happen when running `dart test -j 1`, which would otherwise
+// fix other concurrency issues with the tests. This crash is not significant
+// for users as it depends on having two plugins instantiated at the same time,
+// which is not recommended, but I left it here as a demonstration if needed.
+//
+// The experimental function `enableMultithreading()` can fix the issues
+// demonstrated by this file, but when testing with `dart test -j 1`, a crash
+// occurs as `XmlDocument doc;`, a seemingly harmless statement. I have not
+// been able to deduce the cause, and `enableMultithreading()` does not fix it.
+// If we can figure that out, tests can be run with `-j 1` and race conditions
+// would be eliminated from the tests.
+
+// ignore_for_file: avoid_print
+
+import 'dart:isolate';
+
+import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart';
+import 'package:timezone/standalone.dart';
+
+const WindowsInitializationSettings settings = WindowsInitializationSettings(
+ appName: 'Test app',
+ appUserModelId: 'com.test.test',
+ guid: 'a8c22b55-049e-422f-b30f-863694de08c8',
+);
+
+void main() async {
+ print('Starting tests');
+ await Isolate.spawn(bindingsTest, null);
+ await Isolate.spawn(scheduledTest, null);
+
+ // This is the critical line. Removing this causes crashes in the Windows SDK
+ // ignore: invalid_use_of_visible_for_testing_member
+ FlutterLocalNotificationsWindows().enableMultithreading();
+
+ await Future.delayed(const Duration(seconds: 5));
+ print('Done. Scheduled and binding tests should have completed');
+}
+
+Future scheduledTest(_) async {
+ print('Starting scheduled test');
+ await Future.delayed(const Duration(seconds: 4));
+ final FlutterLocalNotificationsWindows plugin =
+ FlutterLocalNotificationsWindows();
+ await plugin.initialize(settings);
+ await initializeTimeZone();
+ final Location location = getLocation('US/Eastern');
+ final TZDateTime now = TZDateTime.now(location);
+ final TZDateTime later = now.add(const Duration(days: 1));
+ await plugin.zonedSchedule(300, null, null, later, null);
+ await plugin.zonedSchedule(301, null, null, later, null);
+ await plugin.zonedSchedule(302, null, null, later, null);
+ print('Scheduled test complete');
+}
+
+Future bindingsTest(_) async {
+ print('Starting bindings test');
+ final Map bindings = {
+ 'title': 'Bindings title',
+ 'body': 'Bindings body'
+ };
+ await Future.delayed(const Duration(seconds: 1));
+ final FlutterLocalNotificationsWindows plugin =
+ FlutterLocalNotificationsWindows();
+ await plugin.initialize(settings);
+ await plugin.show(503, '{title}', '{body}');
+ await Future.delayed(const Duration(milliseconds: 100));
+ await plugin.updateBindings(id: 503, bindings: bindings);
+ await plugin.updateBindings(id: 503, bindings: bindings);
+ print('Bindings test complete');
+}
diff --git a/flutter_local_notifications_windows/build.bat b/flutter_local_notifications_windows/build.bat
new file mode 100644
index 000000000..bf7478293
--- /dev/null
+++ b/flutter_local_notifications_windows/build.bat
@@ -0,0 +1,6 @@
+@echo off
+cd build
+cmake ../windows
+cmake --build .
+cd ..
+copy build\shared\Debug\flutter_local_notifications_windows.dll .
diff --git a/flutter_local_notifications_windows/dart_test.yaml b/flutter_local_notifications_windows/dart_test.yaml
new file mode 100644
index 000000000..2305630e4
--- /dev/null
+++ b/flutter_local_notifications_windows/dart_test.yaml
@@ -0,0 +1,3 @@
+platforms: [vm]
+test_on: windows
+retry: 5 # These tests have concurrency issues. See bin/crash.dart
diff --git a/flutter_local_notifications_windows/ffigen.yaml b/flutter_local_notifications_windows/ffigen.yaml
new file mode 100644
index 000000000..aad4b5e0c
--- /dev/null
+++ b/flutter_local_notifications_windows/ffigen.yaml
@@ -0,0 +1,31 @@
+# Run with `dart run ffigen --config ffigen.yaml`.
+name: NotificationsPluginBindings
+description: |
+ Bindings for `src/ffi_api.h`.
+
+ Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
+output: 'lib/src/ffi/bindings.dart'
+
+silence-enum-warning: true
+
+headers:
+ entry-points:
+ - 'src/ffi_api.h'
+ include-directives:
+ - 'src/ffi_api.h'
+
+preamble: |
+ // ignore_for_file: always_specify_types
+ // ignore_for_file: camel_case_types
+ // ignore_for_file: non_constant_identifier_names
+
+comments:
+ style: any
+ length: full
+
+type-map:
+ native-types:
+ 'char': # Converts `char` to `Utf8` instead of `Char`
+ 'lib': 'pkg_ffi'
+ 'c-type': 'Utf8'
+ 'dart-type': 'Utf8'
diff --git a/flutter_local_notifications_windows/flutter_local_notifications_windows.dll b/flutter_local_notifications_windows/flutter_local_notifications_windows.dll
new file mode 100644
index 000000000..5f9a8629a
Binary files /dev/null and b/flutter_local_notifications_windows/flutter_local_notifications_windows.dll differ
diff --git a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart
new file mode 100644
index 000000000..06a875848
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart
@@ -0,0 +1,2 @@
+export 'src/details.dart';
+export 'src/plugin/stub.dart' if (dart.library.ffi) 'src/plugin/ffi.dart';
diff --git a/flutter_local_notifications_windows/lib/src/details.dart b/flutter_local_notifications_windows/lib/src/details.dart
new file mode 100644
index 000000000..a69027094
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details.dart
@@ -0,0 +1,21 @@
+export 'details/initialization_settings.dart';
+export 'details/notification_action.dart';
+export 'details/notification_audio.dart';
+export 'details/notification_details.dart';
+export 'details/notification_header.dart';
+export 'details/notification_input.dart';
+export 'details/notification_parts.dart';
+export 'details/notification_progress.dart';
+export 'details/notification_row.dart';
+
+/// The result of updating a notification.
+enum NotificationUpdateResult {
+ /// The update was successful.
+ success,
+
+ /// There was an unexpected error updating the notification.
+ error,
+
+ /// No notification with the provided ID could be found.
+ notFound,
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart b/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart
new file mode 100644
index 000000000..507d85929
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart
@@ -0,0 +1,26 @@
+/// Plugin initialization settings for Windows.
+class WindowsInitializationSettings {
+ /// Creates a new settings object for initializing this plugin on Windows.
+ const WindowsInitializationSettings({
+ required this.appName,
+ required this.appUserModelId,
+ required this.guid,
+ this.iconPath,
+ });
+
+ /// The name of the app that should be shown in the notification toast.
+ final String appName;
+
+ /// The unique app user model ID that identifies the app,
+ /// in the form of CompanyName.ProductName.SubProduct.VersionInformation.
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/shell/appids
+ /// for more information.
+ final String appUserModelId;
+
+ /// The GUID that identifies the notification activation callback.
+ final String guid;
+
+ /// The path to the icon of the notification.
+ final String? iconPath;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart
new file mode 100644
index 000000000..946785470
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart
@@ -0,0 +1,104 @@
+import 'dart:io';
+
+// NOTE: All enum values in this file have Windows RT-specific names.
+// If you change their Dart names, be sure to override [Enum.name].
+
+/// Decides how the [WindowsAction] will launch the app.
+enum WindowsActivationType {
+ /// The application will launch in the foreground (the default).
+ foreground,
+
+ /// Any application can be launched using its protocol.
+ protocol,
+}
+
+/// Decides how a [WindowsAction] will react to being pressed.
+enum WindowsNotificationBehavior {
+ /// The notification will be dismissed.
+ dismiss('default'),
+
+ /// The notification will remain on screen and show a loading status.
+ pendingUpdate('pendingUpdate');
+
+ const WindowsNotificationBehavior(this.name);
+
+ /// The Windows API name for this choice.
+ final String name;
+}
+
+/// Decides how a [WindowsAction] will be styled.
+enum WindowsButtonStyle {
+ /// A green button.
+ success('Success'),
+
+ /// A red button.
+ critical('Critical');
+
+ const WindowsButtonStyle(this.name);
+
+ /// The Windows API name for this choice.
+ final String name;
+}
+
+/// Decides how a [WindowsAction] is placed on a notification.
+enum WindowsActionPlacement {
+ /// Instead of a separate button, the action is part of the context menu.
+ contextMenu,
+}
+
+/// A button in a Windows notification.
+///
+/// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#attributes
+class WindowsAction {
+ /// Constructs a Windows notification button from parameters.
+ const WindowsAction({
+ required this.content,
+ required this.arguments,
+ this.activationType = WindowsActivationType.foreground,
+ this.activationBehavior = WindowsNotificationBehavior.dismiss,
+ this.placement,
+ this.image,
+ this.inputId,
+ this.buttonStyle,
+ this.tooltip,
+ });
+
+ /// The body text of the button.
+ final String content;
+
+ /// An app-defined string that will be passed back if the button is pressed.
+ final String arguments;
+
+ /// How the application should open if the button is pressed.
+ ///
+ /// The default value is [WindowsActivationType.foreground].
+ final WindowsActivationType activationType;
+
+ /// How the notification should react when the button is pressed.
+ ///
+ /// The default value is [WindowsNotificationBehavior.dismiss].
+ final WindowsNotificationBehavior activationBehavior;
+
+ /// How the button should be placed on the notification.
+ ///
+ /// Null indicates a regular button.
+ final WindowsActionPlacement? placement;
+
+ /// An image to show on the button.
+ ///
+ /// Images must be white with a transparent background, and should be
+ /// 16x16 pixels with no padding. If you provide an image for one button,
+ /// you should provide images for all your buttons.
+ final File? image;
+
+ /// The ID of an input box.
+ ///
+ /// If provided, this button will be placed next to the specified input.
+ final String? inputId;
+
+ /// The style of the button. Null indicates a plain button.
+ final WindowsButtonStyle? buttonStyle;
+
+ /// The tooltip, useful if [content] is empty.
+ final String? tooltip;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart
new file mode 100644
index 000000000..cd2c2e8a9
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart
@@ -0,0 +1,148 @@
+extension on Uri {
+ String get filename => pathSegments.last;
+ String get extension => pathSegments.last.split('.').last;
+}
+
+/// A preset sound for a Windows notification.
+enum WindowsNotificationSound {
+ /// The default sound.
+ defaultSound('ms-winsoundevent:Notification.Default'),
+
+ /// The IM sound.
+ im('ms-winsoundevent:Notification.IM'),
+
+ /// The Mail sound.
+ mail('ms-winsoundevent:Notification.Mail'),
+
+ /// The Reminder sound.
+ reminder('ms-winsoundevent:Notification.Reminder'),
+
+ /// The SMS sound.
+ sms('ms-winsoundevent:Notification.SMS'),
+
+ /// Alarm sound 1.
+ alarm1('ms-winsoundevent:Notification.Looping.Alarm1'),
+
+ /// Alarm sound 2.
+ alarm2('ms-winsoundevent:Notification.Looping.Alarm2'),
+
+ /// Alarm sound 3.
+ alarm3('ms-winsoundevent:Notification.Looping.Alarm3'),
+
+ /// Alarm sound 4.
+ alarm4('ms-winsoundevent:Notification.Looping.Alarm4'),
+
+ /// Alarm sound 5.
+ alarm5('ms-winsoundevent:Notification.Looping.Alarm5'),
+
+ /// Alarm sound 6.
+ alarm6('ms-winsoundevent:Notification.Looping.Alarm6'),
+
+ /// Alarm sound 7.
+ alarm7('ms-winsoundevent:Notification.Looping.Alarm7'),
+
+ /// Alarm sound 8.
+ alarm8('ms-winsoundevent:Notification.Looping.Alarm8'),
+
+ /// Alarm sound 9.
+ alarm9('ms-winsoundevent:Notification.Looping.Alarm9'),
+
+ /// Alarm sound 10.
+ alarm10('ms-winsoundevent:Notification.Looping.Alarm10'),
+
+ /// Call sound 1.
+ call1('ms-winsoundevent:Notification.Looping.Call1'),
+
+ /// Call sound 2.
+ call2('ms-winsoundevent:Notification.Looping.Call2'),
+
+ /// Call sound 3.
+ call3('ms-winsoundevent:Notification.Looping.Call3'),
+
+ /// Call sound 4.
+ call4('ms-winsoundevent:Notification.Looping.Call4'),
+
+ /// Call sound 5.
+ call5('ms-winsoundevent:Notification.Looping.Call5'),
+
+ /// Call sound 6.
+ call6('ms-winsoundevent:Notification.Looping.Call6'),
+
+ /// Call sound 7.
+ call7('ms-winsoundevent:Notification.Looping.Call7'),
+
+ /// Call sound 8.
+ call8('ms-winsoundevent:Notification.Looping.Call8'),
+
+ /// Call sound 9.
+ call9('ms-winsoundevent:Notification.Looping.Call9'),
+
+ /// Call sound 10.
+ call10('ms-winsoundevent:Notification.Looping.Call10');
+
+ const WindowsNotificationSound(this.name);
+
+ /// The Windows API name for this sound.
+ final String name;
+}
+
+/// Specifies custom audio to play during a notification.
+class WindowsNotificationAudio {
+ /// No sound will play during this notification.
+ WindowsNotificationAudio.silent()
+ : source = WindowsNotificationSound.defaultSound.name,
+ shouldLoop = false,
+ isSilent = true;
+
+ /// Audio from a Windows preset. See [WindowsNotificationSound] for options.
+ WindowsNotificationAudio.preset({
+ required WindowsNotificationSound sound,
+ this.shouldLoop = false,
+ }) : isSilent = false,
+ source = sound.name;
+
+ /// Audio from a file. See [allowedSchemes] and [allowedExtensions].
+ WindowsNotificationAudio.fromFile({
+ required Uri file,
+ this.shouldLoop = false,
+ }) : isSilent = false,
+ source = file.toFilePath() {
+ if (!allowedSchemes.contains(file.scheme)) {
+ throw ArgumentError.value(
+ file.toString(),
+ 'WindowsNotificationAudio.file',
+ 'URI scheme must be one of the following schemes: $allowedSchemes',
+ );
+ }
+ if (!file.filename.contains('.') ||
+ !allowedExtensions.contains(file.extension)) {
+ throw ArgumentError.value(
+ file.toString(),
+ 'WindowsNotificationAudio.file',
+ 'File extension must be one of the following: $allowedExtensions',
+ );
+ }
+ }
+
+ /// Allowed Uri schemes for [WindowsNotificationAudio.fromFile].
+ static const Set allowedSchemes = {'ms-appx', 'ms-resource'};
+
+ /// Allowed file extensions for [WindowsNotificationAudio.fromFile].
+ static const Set allowedExtensions = {
+ 'aac',
+ 'flac',
+ 'm4a',
+ 'mp3',
+ 'wav',
+ 'wma'
+ };
+
+ /// Whether this audio should loop.
+ final bool shouldLoop;
+
+ /// Whether this notification should be silent.
+ final bool isSilent;
+
+ /// The source of the audio.
+ final String source;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart
new file mode 100644
index 000000000..255224d5d
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart
@@ -0,0 +1,102 @@
+import 'notification_action.dart';
+import 'notification_audio.dart';
+import 'notification_header.dart';
+import 'notification_input.dart';
+import 'notification_parts.dart';
+import 'notification_progress.dart';
+import 'notification_row.dart';
+
+export 'notification_parts.dart';
+
+/// The duration for a Windows notification.
+enum WindowsNotificationDuration {
+ /// The notification will stay for a long time.
+ long,
+
+ /// The notification will stay for a short time.
+ short,
+}
+
+/// The scenario a notification is being used for.
+enum WindowsNotificationScenario {
+ /// Reminders are expanded and remain until manually dismissed.
+ ///
+ /// This will be ignored unless the notification also has at least one
+ /// [WindowsAction] that activates a background task.
+ reminder,
+
+ /// Alarms are expanded and remain until manually dismissed.
+ ///
+ /// By default, alarm notifications loop the standard "alarm" sound.
+ alarm,
+
+ /// Calls are expanded and show in a special format.
+ ///
+ /// By default, call notifications loop the standard "call" sound.
+ incomingCall,
+
+ /// Urgent notifications can break through Do Not Disturb settings.
+ urgent,
+}
+
+/// Contains notification details specific to Windows.
+///
+/// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts
+class WindowsNotificationDetails {
+ /// Creates a Windows notification from the given options.
+ const WindowsNotificationDetails({
+ this.actions = const [],
+ this.inputs = const [],
+ this.images = const [],
+ this.rows = const [],
+ this.progressBars = const [],
+ this.bindings = const {},
+ this.header,
+ this.audio,
+ this.duration,
+ this.scenario,
+ this.timestamp,
+ this.subtitle,
+ });
+
+ /// A list of at most five action buttons.
+ final List actions;
+
+ /// A list of at most five input elements.
+ final List inputs;
+
+ /// A custom audio to play during this notification.
+ final WindowsNotificationAudio? audio;
+
+ /// The duration for this notification.
+ final WindowsNotificationDuration? duration;
+
+ /// The scenario for this notification. Sets some defaults based on the value.
+ final WindowsNotificationScenario? scenario;
+
+ /// The header for this group of notifications.
+ final WindowsHeader? header;
+
+ /// Overrides the timestamp to show on the notification.
+ final DateTime? timestamp;
+
+ /// A third line to show under the notification body.
+ final String? subtitle;
+
+ /// A list of images to show.
+ final List images;
+
+ /// A list of rows to show.
+ final List rows;
+
+ /// A list of progress bars to show.
+ final List progressBars;
+
+ /// Custom bindings in the notification.
+ ///
+ /// Text elements can contains "bindings", which are entered as
+ /// `{bindingName}` directly into the string values. You can then update them
+ /// while or after the notification is launched by using the binding name as
+ /// the key here, and the value as any string you want.
+ final Map bindings;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_header.dart b/flutter_local_notifications_windows/lib/src/details/notification_header.dart
new file mode 100644
index 000000000..a5fbd14f8
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_header.dart
@@ -0,0 +1,31 @@
+/// Decides how the application will open when the header is pressed.
+enum WindowsHeaderActivation {
+ /// Opens the app in the foreground.
+ foreground,
+
+ /// Opens any app using a custom protocol.
+ protocol,
+}
+
+/// A header that groups multiple Windows notifications.
+class WindowsHeader {
+ /// Creates a Windows header.
+ const WindowsHeader({
+ required this.id,
+ required this.title,
+ required this.arguments,
+ this.activation,
+ });
+
+ /// A unique ID for this header.
+ final String id;
+
+ /// The title of the header.
+ final String title;
+
+ /// An application-defined payload that will be passed back when pressed.
+ final String arguments;
+
+ /// Specifies how the application will open.
+ final WindowsHeaderActivation? activation;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart
new file mode 100644
index 000000000..f13291efb
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart
@@ -0,0 +1,74 @@
+/// The type of a [WindowsInput].
+enum WindowsInputType {
+ /// A text input.
+ text,
+
+ /// A multiple choice input.
+ selection,
+}
+
+/// A text or multiple choice input element in a Windows notification.
+sealed class WindowsInput {
+ /// Creates an input field in a notification.
+ const WindowsInput({
+ required this.id,
+ required this.type,
+ this.title,
+ });
+
+ /// A unique ID for this input.
+ ///
+ /// Can be used by buttons to be placed next to this input.
+ final String id;
+
+ /// The type of this input.
+ final WindowsInputType type;
+
+ /// The title of this input.
+ final String? title;
+}
+
+/// A text input.
+class WindowsTextInput extends WindowsInput {
+ /// Creates an input field in a notification.
+ const WindowsTextInput({
+ required super.id,
+ this.placeHolderContent,
+ super.title,
+ }) : super(type: WindowsInputType.text);
+
+ /// A placeholder shown before the user enters input, like a hint text.
+ final String? placeHolderContent;
+}
+
+/// A multiple choice input.
+class WindowsSelectionInput extends WindowsInput {
+ /// Creates a selection input.
+ const WindowsSelectionInput({
+ required super.id,
+ required this.items,
+ this.defaultItem,
+ super.title,
+ }) : super(type: WindowsInputType.selection);
+
+ /// The items that can be selected.
+ final List items;
+
+ /// The default item that is selected.
+ final String? defaultItem;
+}
+
+/// An option that can be selected by a [WindowsSelectionInput].
+class WindowsSelection {
+ /// Creates a selectable choice.
+ const WindowsSelection({
+ required this.id,
+ required this.content,
+ });
+
+ /// A unique ID for this item.
+ final String id;
+
+ /// The content of this item in the UI.
+ final String content;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart
new file mode 100644
index 000000000..43dc29d69
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart
@@ -0,0 +1,93 @@
+import 'dart:io';
+
+/// A text or image element in a Windows notification.
+///
+/// Note: This should not be used for anything else as notification
+/// groups can only contain text and images.
+// This class needs to be abstract so [WindowsNotificationText] and
+// [WindowsImage] can extend it. Specifically, this class is a marker
+// type for classes that are valid as part of a [WindowsColumn].
+// ignore: one_member_abstracts
+sealed class WindowsNotificationPart {
+ /// A const constructor.
+ const WindowsNotificationPart();
+}
+
+/// Where a Windows notification image can be placed.
+enum WindowsImagePlacement {
+ /// The image replaces the app logo.
+ appLogoOverride,
+
+ /// The image is shown on top of the notification body.
+ hero,
+}
+
+/// How a Windows notification image can be cropped.
+enum WindowsImageCrop {
+ /// The image is cropped into a circle.
+ circle,
+}
+
+/// An image in a Windows notification.
+class WindowsImage extends WindowsNotificationPart {
+ /// Creates a Windows notification image.
+ const WindowsImage.file(
+ this.file, {
+ required this.altText,
+ this.addQueryParams = false,
+ this.placement,
+ this.crop,
+ });
+
+ /// Whether Windows should add URL query parameters when fetching the image.
+ final bool addQueryParams;
+
+ /// A description of the image to be used by assistive technology.
+ final String altText;
+
+ /// The source of the image.
+ final File file;
+
+ /// Where this image will be placed. Null indicates below the notification.
+ final WindowsImagePlacement? placement;
+
+ /// How the image will be cropped. Null indicates uncropped.
+ final WindowsImageCrop? crop;
+}
+
+/// Where text can be placed in a Windows notification.
+enum WindowsTextPlacement {
+ /// Shown at the bottom of the notification body in smaller text.
+ attribution,
+}
+
+/// Text in a Windows notification.
+///
+/// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text
+class WindowsNotificationText extends WindowsNotificationPart {
+ /// Creates text for a Windows notification.
+ const WindowsNotificationText({
+ required this.text,
+ this.centerIfCall = false,
+ this.isCaption = false,
+ this.placement,
+ this.languageCode,
+ });
+
+ /// The text being displayed.
+ final String text;
+
+ /// Whether to center this text. Only relevant if in an incoming call.
+ final bool centerIfCall;
+
+ /// Whether the text should be smaller like a caption.
+ final bool isCaption;
+
+ /// The placement of this text.
+ ///
+ /// The default placement (null) is in the main body of the notification.
+ final WindowsTextPlacement? placement;
+
+ /// The language of this text.
+ final String? languageCode;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart
new file mode 100644
index 000000000..f7847a422
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart
@@ -0,0 +1,35 @@
+import '../../flutter_local_notifications_windows.dart';
+
+/// A progress bar in a Windows notification.
+///
+/// To update the progress after the notification has been shown,
+/// use [FlutterLocalNotificationsWindows.updateProgressBar].
+class WindowsProgressBar {
+ /// Creates a progress bar for a Windows notification.
+ WindowsProgressBar({
+ required this.id,
+ required this.status,
+ required this.value,
+ this.title,
+ this.label,
+ });
+
+ /// A unique ID for this progress bar.
+ final String id;
+
+ /// An optional title.
+ final String? title;
+
+ /// Describes what's happening, like `Downloading...` or `Installing...`
+ final String status;
+
+ /// The value of the progress, from 0.0 to 1.0.
+ ///
+ /// Setting this to null indicates a indeterminate progress bar.
+ double? value;
+
+ /// Overrides the default reading as a percent with a different text.
+ ///
+ /// Useful for indicating discrete progress, like `3/10` instead of `30%`.
+ String? label;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_row.dart b/flutter_local_notifications_windows/lib/src/details/notification_row.dart
new file mode 100644
index 000000000..8e3f6a9f6
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_row.dart
@@ -0,0 +1,21 @@
+import 'notification_parts.dart';
+
+/// A group of notification content that must be displayed as a whole row.
+///
+/// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group
+class WindowsRow {
+ /// Makes a group of multiple columns.
+ const WindowsRow(this.columns);
+
+ /// The different columns being grouped together.
+ final List columns;
+}
+
+/// A vertical column of text and images in a Windows notification.
+class WindowsColumn {
+ /// A const constructor.
+ const WindowsColumn(this.parts);
+
+ /// A list of text or images in this column.
+ final List parts;
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart
new file mode 100644
index 000000000..67184c1b1
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart
@@ -0,0 +1,47 @@
+import 'package:xml/xml.dart';
+
+import '../../flutter_local_notifications_windows.dart';
+import 'xml/details.dart';
+
+export 'xml/progress.dart';
+
+/// Converts a notification with [WindowsNotificationDetails] into XML.
+///
+/// For more details, refer to the [Toast Notification XML schema](https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root).
+String notificationToXml({
+ String? title,
+ String? body,
+ String? payload,
+ WindowsNotificationDetails? details,
+}) {
+ final XmlBuilder builder = XmlBuilder();
+ builder.element(
+ 'toast',
+ attributes: {
+ ...details?.attributes ?? {},
+ if (payload != null) 'launch': payload,
+ if (details?.scenario == null) 'useButtonStyle': 'true',
+ },
+ nest: () {
+ builder.element(
+ 'visual',
+ nest: () {
+ builder.element(
+ 'binding',
+ attributes: {'template': 'ToastGeneric'},
+ nest: () {
+ builder
+ ..element('text', nest: title)
+ ..element('text', nest: body);
+ details?.generateBinding(builder);
+ },
+ );
+ },
+ );
+ details?.buildXml(builder);
+ },
+ );
+ return builder
+ .buildDocument()
+ .toXmlString(pretty: true, indentAttribute: (_) => true);
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/action.dart b/flutter_local_notifications_windows/lib/src/details/xml/action.dart
new file mode 100644
index 000000000..67a39d024
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/action.dart
@@ -0,0 +1,34 @@
+import 'package:xml/xml.dart';
+import '../notification_action.dart';
+
+/// Converts a [WindowsAction] to XML
+extension ActionToXml on WindowsAction {
+ /// Serializes this notification action as Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax
+ void buildXml(XmlBuilder builder) {
+ if (image != null && !image!.isAbsolute) {
+ throw ArgumentError.value(
+ image!.path,
+ 'WindowsImage.file',
+ 'File path must be absolute',
+ );
+ }
+ builder.element(
+ 'action',
+ attributes: {
+ 'content': content,
+ 'arguments': arguments,
+ 'activationType': activationType.name,
+ 'afterActivationBehavior': activationBehavior.name,
+ if (placement != null) 'placement': placement!.name,
+ if (image != null)
+ 'imageUri':
+ Uri.file(image!.absolute.path, windows: true).toFilePath(),
+ if (inputId != null) 'hint-inputId': inputId!,
+ if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name,
+ if (tooltip != null) 'hint-toolTip': tooltip!,
+ },
+ );
+ }
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/audio.dart b/flutter_local_notifications_windows/lib/src/details/xml/audio.dart
new file mode 100644
index 000000000..586a53f0d
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/audio.dart
@@ -0,0 +1,17 @@
+import 'package:xml/xml.dart';
+import '../notification_audio.dart';
+
+/// Converts a [WindowsNotificationAudio] to XML
+extension AudioToXml on WindowsNotificationAudio {
+ /// Serializes this audio to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'audio',
+ attributes: {
+ 'src': source,
+ 'silent': isSilent.toString(),
+ 'loop': shouldLoop.toString(),
+ },
+ );
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/details.dart b/flutter_local_notifications_windows/lib/src/details/xml/details.dart
new file mode 100644
index 000000000..98ba8d6a7
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/details.dart
@@ -0,0 +1,91 @@
+import 'package:xml/xml.dart';
+
+import '../notification_action.dart';
+import '../notification_details.dart';
+import '../notification_input.dart';
+import '../notification_progress.dart';
+import '../notification_row.dart';
+
+import 'action.dart';
+import 'audio.dart';
+import 'header.dart';
+import 'image.dart';
+import 'input.dart';
+import 'progress.dart';
+import 'row.dart';
+
+extension on DateTime {
+ String toIso8601StringTz() {
+ // Get offset
+ final Duration offset = timeZoneOffset;
+ final String sign = offset.isNegative ? '-' : '+';
+ final String hours = offset.inHours.abs().toString().padLeft(2, '0');
+ final String minutes =
+ offset.inMinutes.abs().remainder(60).toString().padLeft(2, '0');
+ final String offsetString = '$sign$hours:$minutes';
+ // Get first part of properly formatted ISO 8601 date
+ final String formattedDate = toIso8601String().split('.').first;
+ return '$formattedDate$offsetString';
+ }
+}
+
+/// Converts a [WindowsNotificationDetails] to XML
+extension DetailsToXml on WindowsNotificationDetails {
+ /// Builds all relevant XML parts under the root `` element.
+ void buildXml(XmlBuilder builder) {
+ if (actions.length > 5) {
+ throw ArgumentError(
+ 'WindowsNotificationDetails can only have up to 5 actions',
+ );
+ }
+ if (inputs.length > 5) {
+ throw ArgumentError(
+ 'WindowsNotificationDetails can only have up to 5 inputs',
+ );
+ }
+ builder.element(
+ 'actions',
+ nest: () {
+ for (final WindowsInput input in inputs) {
+ switch (input) {
+ case WindowsTextInput():
+ input.buildXml(builder);
+ case WindowsSelectionInput():
+ input.buildXml(builder);
+ }
+ }
+ for (final WindowsAction action in actions) {
+ action.buildXml(builder);
+ }
+ },
+ );
+ audio?.buildXml(builder);
+ header?.buildXml(builder);
+ }
+
+ /// Generates the `` element of the notification.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding
+ void generateBinding(XmlBuilder builder) {
+ if (subtitle != null) {
+ builder.element('text', nest: subtitle);
+ }
+ for (final WindowsImage image in images) {
+ image.buildXml(builder);
+ }
+ for (final WindowsRow row in rows) {
+ row.buildXml(builder);
+ }
+ for (final WindowsProgressBar progressBar in progressBars) {
+ progressBar.buildXml(builder);
+ }
+ }
+
+ /// XML attributes for the toast notification as a whole.
+ Map get attributes => {
+ if (duration != null) 'duration': duration!.name,
+ if (timestamp != null)
+ 'displayTimestamp': timestamp!.toIso8601StringTz(),
+ if (scenario != null) 'scenario': scenario!.name,
+ };
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/header.dart b/flutter_local_notifications_windows/lib/src/details/xml/header.dart
new file mode 100644
index 000000000..b7de790d4
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/header.dart
@@ -0,0 +1,19 @@
+import 'package:xml/xml.dart';
+
+import '../notification_header.dart';
+
+/// Converts a [WindowsHeader] to XML
+extension HeaderToXml on WindowsHeader {
+ /// Serializes this header to XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-header
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'header',
+ attributes: {
+ 'id': id,
+ 'title': title,
+ 'arguments': arguments,
+ if (activation != null) 'activationType': activation!.name,
+ },
+ );
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/image.dart b/flutter_local_notifications_windows/lib/src/details/xml/image.dart
new file mode 100644
index 000000000..e652f3db5
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/image.dart
@@ -0,0 +1,29 @@
+import 'package:xml/xml.dart';
+
+import '../notification_parts.dart';
+
+/// Converts a [WindowsImage] to XML
+extension ImageToXml on WindowsImage {
+ /// Serializes this image to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image
+ void buildXml(XmlBuilder builder) {
+ if (!file.isAbsolute) {
+ throw ArgumentError.value(
+ file.path,
+ 'WindowsImage.file',
+ 'File path must be absolute',
+ );
+ }
+ builder.element(
+ 'image',
+ attributes: {
+ 'src': Uri.file(file.absolute.path, windows: true).toFilePath(),
+ 'alt': altText,
+ 'addImageQuery': addQueryParams.toString(),
+ if (placement != null) 'placement': placement!.name,
+ if (crop != null) 'hint-crop': crop!.name,
+ },
+ );
+ }
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/input.dart b/flutter_local_notifications_windows/lib/src/details/xml/input.dart
new file mode 100644
index 000000000..407421a5a
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/input.dart
@@ -0,0 +1,55 @@
+import 'package:xml/xml.dart';
+
+import '../notification_input.dart';
+
+/// Converts a [WindowsTextInput] to XML
+extension TextInputToXml on WindowsTextInput {
+ /// Serializes this input to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'input',
+ attributes: {
+ 'id': id,
+ 'type': type.name,
+ if (title != null) 'title': title!,
+ if (placeHolderContent != null)
+ 'placeHolderContent': placeHolderContent!,
+ },
+ );
+}
+
+/// Converts a [WindowsSelectionInput] to XML
+extension SelectionInputToXml on WindowsSelectionInput {
+ /// Serializes this input to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'input',
+ attributes: {
+ 'id': id,
+ 'type': type.name,
+ if (title != null) 'title': title!,
+ if (defaultItem != null) 'defaultInput': defaultItem!,
+ },
+ nest: () {
+ for (final WindowsSelection item in items) {
+ item.buildXml(builder);
+ }
+ },
+ );
+}
+
+/// Converts a [WindowsSelection] to XML
+extension SelectionToXml on WindowsSelection {
+ /// Serializes this selection to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-selection
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'selection',
+ attributes: {
+ 'id': id,
+ 'content': content,
+ },
+ );
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/progress.dart b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart
new file mode 100644
index 000000000..ee8ef441b
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart
@@ -0,0 +1,30 @@
+import 'package:xml/xml.dart';
+
+import '../notification_progress.dart';
+
+/// Converts a [WindowsProgressBar] to XML
+extension ProgressBarToXml on WindowsProgressBar {
+ /// Serializes this progress bar to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-progress
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'progress',
+ attributes: {
+ 'status': status,
+ 'value': '{$id-progressValue}',
+ if (title != null) 'title': title!,
+ if (label != null) 'valueStringOverride': '{$id-progressString}',
+ },
+ );
+
+ /// The data bindings for this progress bar.
+ ///
+ /// To support dynamic updates, [buildXml] will inject placeholder strings
+ /// called data bindings instead of actual values. This can then be updated
+ /// dynamically later by calling
+ /// [FlutterLocalNotificationsWindows.updateProgressBar].
+ Map get data => {
+ '$id-progressValue': value?.toString() ?? 'indeterminate',
+ if (label != null) '$id-progressString': label!,
+ };
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/row.dart b/flutter_local_notifications_windows/lib/src/details/xml/row.dart
new file mode 100644
index 000000000..cada3be4a
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/row.dart
@@ -0,0 +1,35 @@
+import 'package:xml/xml.dart';
+
+import '../notification_parts.dart';
+import '../notification_row.dart';
+
+import 'image.dart';
+import 'text.dart';
+
+/// Converts a [WindowsRow] to XML
+extension RowToXml on WindowsRow {
+ /// Serializes this group to XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'group',
+ nest: () {
+ for (final WindowsColumn column in columns) {
+ builder.element(
+ 'subgroup',
+ attributes: {'hint-weight': '1'},
+ nest: () {
+ for (final WindowsNotificationPart part in column.parts) {
+ switch (part) {
+ case WindowsImage():
+ part.buildXml(builder);
+ case WindowsNotificationText():
+ part.buildXml(builder);
+ }
+ }
+ },
+ );
+ }
+ },
+ );
+}
diff --git a/flutter_local_notifications_windows/lib/src/details/xml/text.dart b/flutter_local_notifications_windows/lib/src/details/xml/text.dart
new file mode 100644
index 000000000..419e66422
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/details/xml/text.dart
@@ -0,0 +1,21 @@
+import 'package:xml/xml.dart';
+
+import '../notification_parts.dart';
+
+/// Converts a [WindowsNotificationText] to XML
+extension TextToXml on WindowsNotificationText {
+ /// Serializes this text to Windows-compatible XML.
+ ///
+ /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text
+ void buildXml(XmlBuilder builder) => builder.element(
+ 'text',
+ attributes: {
+ if (languageCode != null) 'lang': languageCode!,
+ if (placement != null) 'placement': placement!.name,
+ 'hint-callScenarioCenterAlign': centerIfCall.toString(),
+ 'hint-align': 'center',
+ if (isCaption) 'hint-style': 'captionsubtle',
+ },
+ nest: text,
+ );
+}
diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart
new file mode 100644
index 000000000..90c788061
--- /dev/null
+++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart
@@ -0,0 +1,367 @@
+// ignore_for_file: always_specify_types
+// ignore_for_file: camel_case_types
+// ignore_for_file: non_constant_identifier_names
+
+// AUTO GENERATED FILE, DO NOT EDIT.
+//
+// Generated by `package:ffigen`.
+// ignore_for_file: type=lint
+import 'dart:ffi' as ffi;
+import 'package:ffi/ffi.dart' as pkg_ffi;
+
+/// Bindings for `src/ffi_api.h`.
+///
+/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
+///
+class NotificationsPluginBindings {
+ /// Holds the symbol lookup function.
+ final ffi.Pointer Function(String symbolName)
+ _lookup;
+
+ /// The symbols are looked up in [dynamicLibrary].
+ NotificationsPluginBindings(ffi.DynamicLibrary dynamicLibrary)
+ : _lookup = dynamicLibrary.lookup;
+
+ /// The symbols are looked up with [lookup].
+ NotificationsPluginBindings.fromLookup(
+ ffi.Pointer Function(String symbolName)
+ lookup)
+ : _lookup = lookup;
+
+ /// Allocates a new plugin that must be released with [disposePlugin].
+ ffi.Pointer createPlugin() {
+ return _createPlugin();
+ }
+
+ late final _createPluginPtr =
+ _lookup Function()>>(
+ 'createPlugin');
+ late final _createPlugin =
+ _createPluginPtr.asFunction Function()>();
+
+ /// Releases the plugin and any resources it was holding onto.
+ void disposePlugin(
+ ffi.Pointer ptr,
+ ) {
+ return _disposePlugin(
+ ptr,
+ );
+ }
+
+ late final _disposePluginPtr =
+ _lookup)>>(
+ 'disposePlugin');
+ late final _disposePlugin =
+ _disposePluginPtr.asFunction)>();
+
+ /// Initializes the plugin and registers the callback to be run when a notification is pressed.
+ bool init(
+ ffi.Pointer plugin,
+ ffi.Pointer appName,
+ ffi.Pointer aumId,
+ ffi.Pointer guid,
+ ffi.Pointer iconPath,
+ NativeNotificationCallback callback,
+ ) {
+ return _init(
+ plugin,
+ appName,
+ aumId,
+ guid,
+ iconPath,
+ callback,
+ );
+ }
+
+ late final _initPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.Bool Function(
+ ffi.Pointer,
+ ffi.Pointer,
+ ffi.Pointer,
+ ffi.Pointer,
+ ffi.Pointer,
+ NativeNotificationCallback)>>('init');
+ late final _init = _initPtr.asFunction<
+ bool Function(
+ ffi.Pointer,
+ ffi.Pointer,
+ ffi.Pointer,
+ ffi.Pointer,
+ ffi.Pointer,
+ NativeNotificationCallback)>();
+
+ /// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings.
+ bool showNotification(
+ ffi.Pointer plugin,
+ int id,
+ ffi.Pointer xml,
+ NativeStringMap bindings,
+ ) {
+ return _showNotification(
+ plugin,
+ id,
+ xml,
+ bindings,
+ );
+ }
+
+ late final _showNotificationPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.Bool Function(ffi.Pointer, ffi.Int,
+ ffi.Pointer, NativeStringMap)>>('showNotification');
+ late final _showNotification = _showNotificationPtr.asFunction<
+ bool Function(ffi.Pointer, int, ffi.Pointer,
+ NativeStringMap)>();
+
+ /// Schedules the notification to be shown at the given time (as a [time_t]).
+ bool scheduleNotification(
+ ffi.Pointer plugin,
+ int id,
+ ffi.Pointer xml,
+ int time,
+ ) {
+ return _scheduleNotification(
+ plugin,
+ id,
+ xml,
+ time,
+ );
+ }
+
+ late final _scheduleNotificationPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.Bool Function(ffi.Pointer, ffi.Int,
+ ffi.Pointer, ffi.Int)>>('scheduleNotification');
+ late final _scheduleNotification = _scheduleNotificationPtr.asFunction<
+ bool Function(
+ ffi.Pointer, int, ffi.Pointer, int)>();
+
+ /// Updates a notification with the provided bindings after it's been shown.
+ ///
+ /// String values in the `` element of the XML can be placeholders instead of values,
+ /// for example, `{name}` and then call this function with a map with a `name` key,
+ /// and any string value, and the notification will be updated with that value where `name` was.
+ NativeUpdateResult updateNotification(
+ ffi.Pointer plugin,
+ int id,
+ NativeStringMap bindings,
+ ) {
+ return NativeUpdateResult.fromValue(_updateNotification(
+ plugin,
+ id,
+ bindings,
+ ));
+ }
+
+ late final _updateNotificationPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.UnsignedInt Function(ffi.Pointer, ffi.Int,
+ NativeStringMap)>>('updateNotification');
+ late final _updateNotification = _updateNotificationPtr.asFunction<
+ int Function(ffi.Pointer, int, NativeStringMap)>();
+
+ /// Cancels all notifications.
+ void cancelAll(
+ ffi.Pointer plugin,
+ ) {
+ return _cancelAll(
+ plugin,
+ );
+ }
+
+ late final _cancelAllPtr =
+ _lookup)>>(
+ 'cancelAll');
+ late final _cancelAll =
+ _cancelAllPtr.asFunction)>();
+
+ /// Cancels a notification with the given ID.
+ ///
+ /// Only applications with "package identity" (ie, installed with an MSIX installer), can use this.
+ void cancelNotification(
+ ffi.Pointer plugin,
+ int id,
+ ) {
+ return _cancelNotification(
+ plugin,
+ id,
+ );
+ }
+
+ late final _cancelNotificationPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.Void Function(
+ ffi.Pointer, ffi.Int)>>('cancelNotification');
+ late final _cancelNotification = _cancelNotificationPtr
+ .asFunction, int)>();
+
+ /// Gets all notifications that have already been shown but are still in the Action center.
+ ///
+ /// Only applications with "package identity" (ie, installed with an MSIX installer), can use this.
+ /// When your app does not have identity, such as in debug mode, this will return an empty array.
+ ffi.Pointer getActiveNotifications(
+ ffi.Pointer plugin,
+ ffi.Pointer size,
+ ) {
+ return _getActiveNotifications(
+ plugin,
+ size,
+ );
+ }
+
+ late final _getActiveNotificationsPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.Pointer Function(
+ ffi.Pointer,
+ ffi.Pointer)>>('getActiveNotifications');
+ late final _getActiveNotifications = _getActiveNotificationsPtr.asFunction<
+ ffi.Pointer Function(
+ ffi.Pointer, ffi.Pointer)>();
+
+ /// Gets all notifications that have been scheduled but not yet shown.
+ ffi.Pointer getPendingNotifications(
+ ffi.Pointer plugin,
+ ffi.Pointer size,
+ ) {
+ return _getPendingNotifications(
+ plugin,
+ size,
+ );
+ }
+
+ late final _getPendingNotificationsPtr = _lookup<
+ ffi.NativeFunction<
+ ffi.Pointer Function(
+ ffi.Pointer,
+ ffi.Pointer)>>('getPendingNotifications');
+ late final _getPendingNotifications = _getPendingNotificationsPtr.asFunction<
+ ffi.Pointer Function(
+ ffi.Pointer, ffi.Pointer