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