tauri_plugin_notifications/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
6
7use serde::Serialize;
8#[cfg(mobile)]
9use tauri::plugin::PluginHandle;
10#[cfg(desktop)]
11use tauri::AppHandle;
12use tauri::{
13    plugin::{Builder, TauriPlugin},
14    Manager, Runtime,
15};
16
17pub use models::*;
18pub use tauri::plugin::PermissionState;
19
20#[cfg(all(desktop, feature = "notify-rust"))]
21mod desktop;
22#[cfg(all(target_os = "macos", not(feature = "notify-rust")))]
23mod macos;
24#[cfg(mobile)]
25mod mobile;
26
27mod commands;
28mod error;
29#[cfg(desktop)]
30mod listeners;
31mod models;
32
33pub use error::{Error, Result};
34
35#[cfg(all(desktop, feature = "notify-rust"))]
36pub use desktop::Notifications;
37#[cfg(all(target_os = "macos", not(feature = "notify-rust")))]
38pub use macos::Notifications;
39#[cfg(mobile)]
40pub use mobile::Notifications;
41
42/// The notification builder.
43#[derive(Debug)]
44pub struct NotificationsBuilder<R: Runtime> {
45    #[cfg(desktop)]
46    #[allow(dead_code)]
47    app: AppHandle<R>,
48    #[cfg(all(target_os = "macos", not(feature = "notify-rust")))]
49    plugin: std::sync::Arc<macos::NotificationPlugin>,
50    #[cfg(mobile)]
51    handle: PluginHandle<R>,
52    pub(crate) data: NotificationData,
53}
54
55impl<R: Runtime> NotificationsBuilder<R> {
56    #[cfg(all(desktop, feature = "notify-rust"))]
57    fn new(app: AppHandle<R>) -> Self {
58        Self {
59            app,
60            data: Default::default(),
61        }
62    }
63
64    #[cfg(all(target_os = "macos", not(feature = "notify-rust")))]
65    fn new(app: AppHandle<R>, plugin: std::sync::Arc<macos::NotificationPlugin>) -> Self {
66        Self {
67            app,
68            plugin,
69            data: Default::default(),
70        }
71    }
72
73    #[cfg(mobile)]
74    fn new(handle: PluginHandle<R>) -> Self {
75        Self {
76            handle,
77            data: Default::default(),
78        }
79    }
80
81    /// Sets the notification identifier.
82    pub fn id(mut self, id: i32) -> Self {
83        self.data.id = id;
84        self
85    }
86
87    /// Identifier of the {@link Channel} that delivers this notification.
88    ///
89    /// If the channel does not exist, the notification won't fire.
90    /// Make sure the channel exists with {@link listChannels} and {@link createChannel}.
91    pub fn channel_id(mut self, id: impl Into<String>) -> Self {
92        self.data.channel_id.replace(id.into());
93        self
94    }
95
96    /// Sets the notification title.
97    pub fn title(mut self, title: impl Into<String>) -> Self {
98        self.data.title.replace(title.into());
99        self
100    }
101
102    /// Sets the notification body.
103    pub fn body(mut self, body: impl Into<String>) -> Self {
104        self.data.body.replace(body.into());
105        self
106    }
107
108    /// Schedule this notification to fire on a later time or a fixed interval.
109    pub fn schedule(mut self, schedule: Schedule) -> Self {
110        self.data.schedule.replace(schedule);
111        self
112    }
113
114    /// Multiline text.
115    /// Changes the notification style to big text.
116    /// Cannot be used with `inboxLines`.
117    pub fn large_body(mut self, large_body: impl Into<String>) -> Self {
118        self.data.large_body.replace(large_body.into());
119        self
120    }
121
122    /// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
123    pub fn summary(mut self, summary: impl Into<String>) -> Self {
124        self.data.summary.replace(summary.into());
125        self
126    }
127
128    /// Defines an action type for this notification.
129    pub fn action_type_id(mut self, action_type_id: impl Into<String>) -> Self {
130        self.data.action_type_id.replace(action_type_id.into());
131        self
132    }
133
134    /// Identifier used to group multiple notifications.
135    ///
136    /// <https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier>
137    pub fn group(mut self, group: impl Into<String>) -> Self {
138        self.data.group.replace(group.into());
139        self
140    }
141
142    /// Instructs the system that this notification is the summary of a group on Android.
143    pub fn group_summary(mut self) -> Self {
144        self.data.group_summary = true;
145        self
146    }
147
148    /// The sound resource name. Only available on mobile.
149    pub fn sound(mut self, sound: impl Into<String>) -> Self {
150        self.data.sound.replace(sound.into());
151        self
152    }
153
154    /// Append an inbox line to the notification.
155    /// Changes the notification style to inbox.
156    /// Cannot be used with `largeBody`.
157    ///
158    /// Only supports up to 5 lines.
159    pub fn inbox_line(mut self, line: impl Into<String>) -> Self {
160        self.data.inbox_lines.push(line.into());
161        self
162    }
163
164    /// Notification icon.
165    ///
166    /// On Android the icon must be placed in the app's `res/drawable` folder.
167    pub fn icon(mut self, icon: impl Into<String>) -> Self {
168        self.data.icon.replace(icon.into());
169        self
170    }
171
172    /// Notification large icon (Android).
173    ///
174    /// The icon must be placed in the app's `res/drawable` folder.
175    pub fn large_icon(mut self, large_icon: impl Into<String>) -> Self {
176        self.data.large_icon.replace(large_icon.into());
177        self
178    }
179
180    /// Icon color on Android.
181    pub fn icon_color(mut self, icon_color: impl Into<String>) -> Self {
182        self.data.icon_color.replace(icon_color.into());
183        self
184    }
185
186    /// Append an attachment to the notification.
187    pub fn attachment(mut self, attachment: Attachment) -> Self {
188        self.data.attachments.push(attachment);
189        self
190    }
191
192    /// Adds an extra payload to store in the notification.
193    pub fn extra(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
194        if let Ok(value) = serde_json::to_value(value) {
195            self.data.extra.insert(key.into(), value);
196        }
197        self
198    }
199
200    /// If true, the notification cannot be dismissed by the user on Android.
201    ///
202    /// An application service must manage the dismissal of the notification.
203    /// It is typically used to indicate a background task that is pending (e.g. a file download)
204    /// or the user is engaged with (e.g. playing music).
205    pub fn ongoing(mut self) -> Self {
206        self.data.ongoing = true;
207        self
208    }
209
210    /// Automatically cancel the notification when the user clicks on it.
211    pub fn auto_cancel(mut self) -> Self {
212        self.data.auto_cancel = true;
213        self
214    }
215
216    /// Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
217    pub fn silent(mut self) -> Self {
218        self.data.silent = true;
219        self
220    }
221}
222
223/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the notification APIs.
224pub trait NotificationsExt<R: Runtime> {
225    fn notifications(&self) -> &Notifications<R>;
226}
227
228impl<R: Runtime, T: Manager<R>> crate::NotificationsExt<R> for T {
229    fn notifications(&self) -> &Notifications<R> {
230        self.state::<Notifications<R>>().inner()
231    }
232}
233
234/// Initializes the plugin.
235pub fn init<R: Runtime>() -> TauriPlugin<R> {
236    Builder::new("notifications")
237        .invoke_handler(tauri::generate_handler![
238            commands::notify,
239            commands::request_permission,
240            commands::register_for_push_notifications,
241            commands::unregister_for_push_notifications,
242            commands::is_permission_granted,
243            commands::register_action_types,
244            commands::get_pending,
245            commands::get_active,
246            commands::set_click_listener_active,
247            commands::remove_active,
248            commands::cancel,
249            commands::cancel_all,
250            commands::create_channel,
251            commands::delete_channel,
252            commands::list_channels,
253            #[cfg(desktop)]
254            listeners::register_listener,
255            #[cfg(desktop)]
256            listeners::remove_listener,
257        ])
258        .setup(|app, api| {
259            #[cfg(desktop)]
260            listeners::init();
261            #[cfg(mobile)]
262            let notification = mobile::init(app, api)?;
263            #[cfg(all(desktop, feature = "notify-rust"))]
264            let notification = desktop::init(app, api)?;
265            #[cfg(all(target_os = "macos", not(feature = "notify-rust")))]
266            let notification = macos::init(app, api)?;
267            app.manage(notification);
268            Ok(())
269        })
270        .build()
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    // Helper function to create a test builder without needing a runtime
278    #[cfg(desktop)]
279    fn create_test_data() -> NotificationData {
280        NotificationData::default()
281    }
282
283    #[cfg(mobile)]
284    fn create_test_data() -> NotificationData {
285        NotificationData::default()
286    }
287
288    #[test]
289    fn test_notification_data_id() {
290        let mut data = create_test_data();
291        data.id = 42;
292        assert_eq!(data.id, 42);
293    }
294
295    #[test]
296    fn test_notification_data_channel_id() {
297        let mut data = create_test_data();
298        data.channel_id = Some("test_channel".to_string());
299        assert_eq!(data.channel_id, Some("test_channel".to_string()));
300    }
301
302    #[test]
303    fn test_notification_data_title() {
304        let mut data = create_test_data();
305        data.title = Some("Test Title".to_string());
306        assert_eq!(data.title, Some("Test Title".to_string()));
307    }
308
309    #[test]
310    fn test_notification_data_body() {
311        let mut data = create_test_data();
312        data.body = Some("Test Body".to_string());
313        assert_eq!(data.body, Some("Test Body".to_string()));
314    }
315
316    #[test]
317    fn test_notification_data_large_body() {
318        let mut data = create_test_data();
319        data.large_body = Some("Large Body Text".to_string());
320        assert_eq!(data.large_body, Some("Large Body Text".to_string()));
321    }
322
323    #[test]
324    fn test_notification_data_summary() {
325        let mut data = create_test_data();
326        data.summary = Some("Summary Text".to_string());
327        assert_eq!(data.summary, Some("Summary Text".to_string()));
328    }
329
330    #[test]
331    fn test_notification_data_action_type_id() {
332        let mut data = create_test_data();
333        data.action_type_id = Some("action_type".to_string());
334        assert_eq!(data.action_type_id, Some("action_type".to_string()));
335    }
336
337    #[test]
338    fn test_notification_data_group() {
339        let mut data = create_test_data();
340        data.group = Some("test_group".to_string());
341        assert_eq!(data.group, Some("test_group".to_string()));
342    }
343
344    #[test]
345    fn test_notification_data_group_summary() {
346        let mut data = create_test_data();
347        data.group_summary = true;
348        assert!(data.group_summary);
349    }
350
351    #[test]
352    fn test_notification_data_sound() {
353        let mut data = create_test_data();
354        data.sound = Some("notification_sound".to_string());
355        assert_eq!(data.sound, Some("notification_sound".to_string()));
356    }
357
358    #[test]
359    fn test_notification_data_inbox_lines() {
360        let mut data = create_test_data();
361        data.inbox_lines.push("Line 1".to_string());
362        data.inbox_lines.push("Line 2".to_string());
363        assert_eq!(data.inbox_lines.len(), 2);
364        assert_eq!(data.inbox_lines[0], "Line 1");
365        assert_eq!(data.inbox_lines[1], "Line 2");
366    }
367
368    #[test]
369    fn test_notification_data_icon() {
370        let mut data = create_test_data();
371        data.icon = Some("icon_name".to_string());
372        assert_eq!(data.icon, Some("icon_name".to_string()));
373    }
374
375    #[test]
376    fn test_notification_data_large_icon() {
377        let mut data = create_test_data();
378        data.large_icon = Some("large_icon_name".to_string());
379        assert_eq!(data.large_icon, Some("large_icon_name".to_string()));
380    }
381
382    #[test]
383    fn test_notification_data_icon_color() {
384        let mut data = create_test_data();
385        data.icon_color = Some("#FF0000".to_string());
386        assert_eq!(data.icon_color, Some("#FF0000".to_string()));
387    }
388
389    #[test]
390    fn test_notification_data_attachments() {
391        let mut data = create_test_data();
392        let url = url::Url::parse("https://example.com/image.png").expect("Failed to parse URL");
393        let attachment = Attachment::new("attachment1", url);
394        data.attachments.push(attachment);
395        assert_eq!(data.attachments.len(), 1);
396    }
397
398    #[test]
399    fn test_notification_data_extra() {
400        let mut data = create_test_data();
401        data.extra
402            .insert("key1".to_string(), serde_json::json!("value1"));
403        data.extra.insert("key2".to_string(), serde_json::json!(42));
404        assert_eq!(data.extra.len(), 2);
405        assert_eq!(data.extra.get("key1"), Some(&serde_json::json!("value1")));
406        assert_eq!(data.extra.get("key2"), Some(&serde_json::json!(42)));
407    }
408
409    #[test]
410    fn test_notification_data_ongoing() {
411        let mut data = create_test_data();
412        data.ongoing = true;
413        assert!(data.ongoing);
414    }
415
416    #[test]
417    fn test_notification_data_auto_cancel() {
418        let mut data = create_test_data();
419        data.auto_cancel = true;
420        assert!(data.auto_cancel);
421    }
422
423    #[test]
424    fn test_notification_data_silent() {
425        let mut data = create_test_data();
426        data.silent = true;
427        assert!(data.silent);
428    }
429
430    #[test]
431    fn test_notification_data_schedule() {
432        let mut data = create_test_data();
433        let schedule = Schedule::Every {
434            interval: ScheduleEvery::Day,
435            count: 1,
436            allow_while_idle: false,
437        };
438        data.schedule = Some(schedule);
439        assert!(data.schedule.is_some());
440        assert!(matches!(data.schedule, Some(Schedule::Every { .. })));
441    }
442}