Skip to main content

fission_shell_winit/
notifications.rs

1use fission_core::{
2    CancelNotificationRequest, NotificationError, NotificationPermission,
3    NotificationPermissionRequest, NotificationReceipt, NotificationRequest, NotificationSchedule,
4    NotificationSettings, PushPlatform, PushRegistration, PushRegistrationRequest,
5    SetBadgeCountRequest, CANCEL_ALL_NOTIFICATIONS, CANCEL_NOTIFICATION, GET_NOTIFICATION_SETTINGS,
6    REGISTER_PUSH_NOTIFICATIONS, REQUEST_NOTIFICATION_PERMISSION, SCHEDULE_NOTIFICATION,
7    SET_BADGE_COUNT, SHOW_NOTIFICATION, UNREGISTER_PUSH_NOTIFICATIONS,
8};
9use fission_shell::async_host::AsyncRegistry;
10use std::sync::Arc;
11
12/// Host-side notification provider used by the shell capability registry.
13pub trait NotificationHost: Send + Sync + 'static {
14    /// Requests permission for notification features such as alerts, badges, or sound.
15    ///
16    /// Implementations should map the typed request to the platform prompt and
17    /// return the resulting settings without assuming permission was granted.
18    fn request_permission(
19        &self,
20        request: NotificationPermissionRequest,
21    ) -> Result<NotificationSettings, NotificationError>;
22
23    /// Returns current notification settings without showing a platform prompt.
24    ///
25    /// Use this to report permission state, delivery support, scheduling support,
26    /// badge support, and push support to reducers.
27    fn settings(&self) -> Result<NotificationSettings, NotificationError>;
28
29    /// Displays an immediate local notification.
30    ///
31    /// `request` contains the stable id, visible text, badge, sound, deep link,
32    /// and action buttons. Return a receipt only after the host accepted the
33    /// notification request.
34    fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError>;
35
36    /// Schedules a local notification for later delivery.
37    ///
38    /// Implementations should persist or hand off the schedule according to the
39    /// platform notification model and return an error when scheduled delivery is
40    /// unavailable.
41    fn schedule(
42        &self,
43        request: NotificationRequest,
44    ) -> Result<NotificationReceipt, NotificationError>;
45
46    /// Cancels one notification by id.
47    ///
48    /// `request.id` is the id originally used to show or schedule the
49    /// notification. Hosts may treat an already-missing notification as success.
50    fn cancel(&self, request: CancelNotificationRequest) -> Result<(), NotificationError>;
51
52    /// Cancels all notifications owned by this app where the platform allows it.
53    fn cancel_all(&self) -> Result<(), NotificationError>;
54
55    /// Sets or clears the app badge count.
56    ///
57    /// `None` clears the badge. `Some(count)` asks the host to show the supplied
58    /// count using the target platform badge mechanism.
59    fn set_badge_count(&self, request: SetBadgeCountRequest) -> Result<(), NotificationError>;
60
61    /// Registers this app instance for remote or push notification delivery.
62    ///
63    /// Provider credentials remain in host configuration. The request carries
64    /// public registration inputs and the result returns token or endpoint data.
65    fn register_push(
66        &self,
67        request: PushRegistrationRequest,
68    ) -> Result<PushRegistration, NotificationError>;
69
70    /// Removes or invalidates this app instance from remote notification delivery.
71    fn unregister_push(&self) -> Result<(), NotificationError>;
72}
73
74/// Default provider used until a shell installs a platform-specific host.
75#[derive(Debug, Default)]
76pub struct UnsupportedNotificationHost;
77
78impl NotificationHost for UnsupportedNotificationHost {
79    fn request_permission(
80        &self,
81        _request: NotificationPermissionRequest,
82    ) -> Result<NotificationSettings, NotificationError> {
83        Ok(NotificationSettings {
84            permission: NotificationPermission::Unsupported,
85            ..Default::default()
86        })
87    }
88
89    fn settings(&self) -> Result<NotificationSettings, NotificationError> {
90        Ok(NotificationSettings {
91            permission: NotificationPermission::Unsupported,
92            ..Default::default()
93        })
94    }
95
96    fn show(
97        &self,
98        _request: NotificationRequest,
99    ) -> Result<NotificationReceipt, NotificationError> {
100        Err(NotificationError::unsupported("show"))
101    }
102
103    fn schedule(
104        &self,
105        _request: NotificationRequest,
106    ) -> Result<NotificationReceipt, NotificationError> {
107        Err(NotificationError::unsupported("schedule"))
108    }
109
110    fn cancel(&self, _request: CancelNotificationRequest) -> Result<(), NotificationError> {
111        Err(NotificationError::unsupported("cancel"))
112    }
113
114    fn cancel_all(&self) -> Result<(), NotificationError> {
115        Err(NotificationError::unsupported("cancel_all"))
116    }
117
118    fn set_badge_count(&self, _request: SetBadgeCountRequest) -> Result<(), NotificationError> {
119        Err(NotificationError::unsupported("set_badge_count"))
120    }
121
122    fn register_push(
123        &self,
124        _request: PushRegistrationRequest,
125    ) -> Result<PushRegistration, NotificationError> {
126        Err(NotificationError::unsupported("register_push"))
127    }
128
129    fn unregister_push(&self) -> Result<(), NotificationError> {
130        Err(NotificationError::unsupported("unregister_push"))
131    }
132}
133
134/// Minimal in-process host useful for smoke tests and non-OS environments.
135#[derive(Debug, Default)]
136pub struct MemoryNotificationHost;
137
138impl NotificationHost for MemoryNotificationHost {
139    fn request_permission(
140        &self,
141        request: NotificationPermissionRequest,
142    ) -> Result<NotificationSettings, NotificationError> {
143        Ok(NotificationSettings {
144            permission: NotificationPermission::Granted,
145            alerts: request.alerts,
146            badge: request.badge,
147            sound: request.sound,
148            scheduling: true,
149            push: false,
150        })
151    }
152
153    fn settings(&self) -> Result<NotificationSettings, NotificationError> {
154        Ok(NotificationSettings {
155            permission: NotificationPermission::Granted,
156            alerts: true,
157            badge: true,
158            sound: true,
159            scheduling: true,
160            push: false,
161        })
162    }
163
164    fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError> {
165        Ok(NotificationReceipt {
166            id: request.id,
167            scheduled: false,
168            delivered: true,
169        })
170    }
171
172    fn schedule(
173        &self,
174        request: NotificationRequest,
175    ) -> Result<NotificationReceipt, NotificationError> {
176        Ok(NotificationReceipt {
177            id: request.id,
178            scheduled: !matches!(request.schedule, NotificationSchedule::Immediate),
179            delivered: matches!(request.schedule, NotificationSchedule::Immediate),
180        })
181    }
182
183    fn cancel(&self, _request: CancelNotificationRequest) -> Result<(), NotificationError> {
184        Ok(())
185    }
186
187    fn cancel_all(&self) -> Result<(), NotificationError> {
188        Ok(())
189    }
190
191    fn set_badge_count(&self, _request: SetBadgeCountRequest) -> Result<(), NotificationError> {
192        Ok(())
193    }
194
195    fn register_push(
196        &self,
197        _request: PushRegistrationRequest,
198    ) -> Result<PushRegistration, NotificationError> {
199        Ok(PushRegistration {
200            platform: PushPlatform::Other("memory".into()),
201            token: "memory-push-token".into(),
202            endpoint: None,
203            p256dh_key: None,
204            auth_secret: None,
205        })
206    }
207
208    fn unregister_push(&self) -> Result<(), NotificationError> {
209        Ok(())
210    }
211}
212
213pub(crate) fn register_notification_capabilities(
214    async_registry: &mut AsyncRegistry,
215    host: Arc<dyn NotificationHost>,
216) {
217    let request_host = host.clone();
218    async_registry.register_operation_capability(
219        REQUEST_NOTIFICATION_PERMISSION,
220        move |request, _| {
221            let host = request_host.clone();
222            async move { host.request_permission(request) }
223        },
224    );
225
226    let settings_host = host.clone();
227    async_registry.register_operation_capability(GET_NOTIFICATION_SETTINGS, move |(), _| {
228        let host = settings_host.clone();
229        async move { host.settings() }
230    });
231
232    let show_host = host.clone();
233    async_registry.register_operation_capability(SHOW_NOTIFICATION, move |request, _| {
234        let host = show_host.clone();
235        async move { host.show(request) }
236    });
237
238    let schedule_host = host.clone();
239    async_registry.register_operation_capability(SCHEDULE_NOTIFICATION, move |request, _| {
240        let host = schedule_host.clone();
241        async move { host.schedule(request) }
242    });
243
244    let cancel_host = host.clone();
245    async_registry.register_operation_capability(CANCEL_NOTIFICATION, move |request, _| {
246        let host = cancel_host.clone();
247        async move { host.cancel(request) }
248    });
249
250    let cancel_all_host = host.clone();
251    async_registry.register_operation_capability(CANCEL_ALL_NOTIFICATIONS, move |(), _| {
252        let host = cancel_all_host.clone();
253        async move { host.cancel_all() }
254    });
255
256    let badge_host = host.clone();
257    async_registry.register_operation_capability(SET_BADGE_COUNT, move |request, _| {
258        let host = badge_host.clone();
259        async move { host.set_badge_count(request) }
260    });
261
262    let push_host = host.clone();
263    async_registry.register_operation_capability(REGISTER_PUSH_NOTIFICATIONS, move |request, _| {
264        let host = push_host.clone();
265        async move { host.register_push(request) }
266    });
267
268    async_registry.register_operation_capability(UNREGISTER_PUSH_NOTIFICATIONS, move |(), _| {
269        let host = host.clone();
270        async move { host.unregister_push() }
271    });
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use fission_core::NotificationId;
278
279    #[test]
280    fn unsupported_host_reports_permission_without_panicking() {
281        let host = UnsupportedNotificationHost;
282        let settings = host
283            .request_permission(NotificationPermissionRequest::default())
284            .unwrap();
285        assert_eq!(settings.permission, NotificationPermission::Unsupported);
286        assert_eq!(
287            host.show(NotificationRequest::default()).unwrap_err().code,
288            "unsupported"
289        );
290    }
291
292    #[test]
293    fn memory_host_returns_receipts() {
294        let host = MemoryNotificationHost;
295        let receipt = host
296            .show(NotificationRequest {
297                id: NotificationId::new("n1"),
298                title: "Title".into(),
299                body: "Body".into(),
300                ..Default::default()
301            })
302            .unwrap();
303        assert_eq!(receipt.id, NotificationId::new("n1"));
304        assert!(receipt.delivered);
305    }
306}