Skip to main content

tauri_plugin_notifications/
lib.rs

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