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