Skip to main content

fission_shell_winit/
notifications.rs

1#[cfg(target_os = "macos")]
2use block::ConcreteBlock;
3use fission_core::{
4    CancelNotificationRequest, NotificationError, NotificationPermission,
5    NotificationPermissionRequest, NotificationReceipt, NotificationRequest, NotificationSchedule,
6    NotificationSettings, PushPlatform, PushRegistration, PushRegistrationRequest,
7    SetBadgeCountRequest, CANCEL_ALL_NOTIFICATIONS, CANCEL_NOTIFICATION, GET_NOTIFICATION_SETTINGS,
8    REGISTER_PUSH_NOTIFICATIONS, REQUEST_NOTIFICATION_PERMISSION, SCHEDULE_NOTIFICATION,
9    SET_BADGE_COUNT, SHOW_NOTIFICATION, UNREGISTER_PUSH_NOTIFICATIONS,
10};
11use fission_shell::async_host::AsyncRegistry;
12#[cfg(target_os = "ios")]
13use objc::{class, msg_send, sel, sel_impl};
14#[cfg(target_os = "macos")]
15use objc::{class, msg_send, sel, sel_impl};
16#[cfg(target_os = "macos")]
17use std::ffi::CStr;
18#[cfg(any(target_os = "ios", target_os = "macos"))]
19use std::os::raw::c_void;
20#[cfg(not(target_os = "ios"))]
21use std::process::Command;
22use std::sync::Arc;
23#[cfg(target_os = "macos")]
24use std::sync::{Condvar, Mutex};
25
26#[cfg(target_os = "ios")]
27#[link(name = "UIKit", kind = "framework")]
28extern "C" {}
29
30#[cfg(target_os = "macos")]
31#[link(name = "AppKit", kind = "framework")]
32extern "C" {}
33
34#[cfg(target_os = "macos")]
35#[link(name = "Foundation", kind = "framework")]
36extern "C" {}
37
38#[cfg(target_os = "macos")]
39#[link(name = "UserNotifications", kind = "framework")]
40extern "C" {}
41
42/// Host-side notification provider used by the shell capability registry.
43pub trait NotificationHost: Send + Sync + 'static {
44    /// Requests permission for notification features such as alerts, badges, or sound.
45    ///
46    /// Implementations should map the typed request to the platform prompt and
47    /// return the resulting settings without assuming permission was granted.
48    fn request_permission(
49        &self,
50        request: NotificationPermissionRequest,
51    ) -> Result<NotificationSettings, NotificationError>;
52
53    /// Returns current notification settings without showing a platform prompt.
54    ///
55    /// Use this to report permission state, delivery support, scheduling support,
56    /// badge support, and push support to reducers.
57    fn settings(&self) -> Result<NotificationSettings, NotificationError>;
58
59    /// Displays an immediate local notification.
60    ///
61    /// `request` contains the stable id, visible text, badge, sound, deep link,
62    /// and action buttons. Return a receipt only after the host accepted the
63    /// notification request.
64    fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError>;
65
66    /// Schedules a local notification for later delivery.
67    ///
68    /// Implementations should persist or hand off the schedule according to the
69    /// platform notification model and return an error when scheduled delivery is
70    /// unavailable.
71    fn schedule(
72        &self,
73        request: NotificationRequest,
74    ) -> Result<NotificationReceipt, NotificationError>;
75
76    /// Cancels one notification by id.
77    ///
78    /// `request.id` is the id originally used to show or schedule the
79    /// notification. Hosts may treat an already-missing notification as success.
80    fn cancel(&self, request: CancelNotificationRequest) -> Result<(), NotificationError>;
81
82    /// Cancels all notifications owned by this app where the platform allows it.
83    fn cancel_all(&self) -> Result<(), NotificationError>;
84
85    /// Sets or clears the app badge count.
86    ///
87    /// `None` clears the badge. `Some(count)` asks the host to show the supplied
88    /// count using the target platform badge mechanism.
89    fn set_badge_count(&self, request: SetBadgeCountRequest) -> Result<(), NotificationError>;
90
91    /// Registers this app instance for remote or push notification delivery.
92    ///
93    /// Provider credentials remain in host configuration. The request carries
94    /// public registration inputs and the result returns token or endpoint data.
95    fn register_push(
96        &self,
97        request: PushRegistrationRequest,
98    ) -> Result<PushRegistration, NotificationError>;
99
100    /// Removes or invalidates this app instance from remote notification delivery.
101    fn unregister_push(&self) -> Result<(), NotificationError>;
102}
103
104/// Default provider used until a shell installs a platform-specific host.
105#[derive(Debug, Default)]
106pub struct UnsupportedNotificationHost;
107
108impl NotificationHost for UnsupportedNotificationHost {
109    fn request_permission(
110        &self,
111        _request: NotificationPermissionRequest,
112    ) -> Result<NotificationSettings, NotificationError> {
113        Ok(NotificationSettings {
114            permission: NotificationPermission::Unsupported,
115            ..Default::default()
116        })
117    }
118
119    fn settings(&self) -> Result<NotificationSettings, NotificationError> {
120        Ok(NotificationSettings {
121            permission: NotificationPermission::Unsupported,
122            ..Default::default()
123        })
124    }
125
126    fn show(
127        &self,
128        _request: NotificationRequest,
129    ) -> Result<NotificationReceipt, NotificationError> {
130        Err(NotificationError::unsupported("show"))
131    }
132
133    fn schedule(
134        &self,
135        _request: NotificationRequest,
136    ) -> Result<NotificationReceipt, NotificationError> {
137        Err(NotificationError::unsupported("schedule"))
138    }
139
140    fn cancel(&self, _request: CancelNotificationRequest) -> Result<(), NotificationError> {
141        Err(NotificationError::unsupported("cancel"))
142    }
143
144    fn cancel_all(&self) -> Result<(), NotificationError> {
145        Err(NotificationError::unsupported("cancel_all"))
146    }
147
148    fn set_badge_count(&self, _request: SetBadgeCountRequest) -> Result<(), NotificationError> {
149        Err(NotificationError::unsupported("set_badge_count"))
150    }
151
152    fn register_push(
153        &self,
154        _request: PushRegistrationRequest,
155    ) -> Result<PushRegistration, NotificationError> {
156        Err(NotificationError::unsupported("register_push"))
157    }
158
159    fn unregister_push(&self) -> Result<(), NotificationError> {
160        Err(NotificationError::unsupported("unregister_push"))
161    }
162}
163
164/// Minimal in-process host useful for smoke tests and non-OS environments.
165#[derive(Debug, Default)]
166pub struct MemoryNotificationHost;
167
168impl NotificationHost for MemoryNotificationHost {
169    fn request_permission(
170        &self,
171        request: NotificationPermissionRequest,
172    ) -> Result<NotificationSettings, NotificationError> {
173        Ok(NotificationSettings {
174            permission: NotificationPermission::Granted,
175            alerts: request.alerts,
176            badge: request.badge,
177            sound: request.sound,
178            scheduling: true,
179            push: false,
180        })
181    }
182
183    fn settings(&self) -> Result<NotificationSettings, NotificationError> {
184        Ok(NotificationSettings {
185            permission: NotificationPermission::Granted,
186            alerts: true,
187            badge: true,
188            sound: true,
189            scheduling: true,
190            push: false,
191        })
192    }
193
194    fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError> {
195        Ok(NotificationReceipt {
196            id: request.id,
197            scheduled: false,
198            delivered: true,
199        })
200    }
201
202    fn schedule(
203        &self,
204        request: NotificationRequest,
205    ) -> Result<NotificationReceipt, NotificationError> {
206        Ok(NotificationReceipt {
207            id: request.id,
208            scheduled: !matches!(request.schedule, NotificationSchedule::Immediate),
209            delivered: matches!(request.schedule, NotificationSchedule::Immediate),
210        })
211    }
212
213    fn cancel(&self, _request: CancelNotificationRequest) -> Result<(), NotificationError> {
214        Ok(())
215    }
216
217    fn cancel_all(&self) -> Result<(), NotificationError> {
218        Ok(())
219    }
220
221    fn set_badge_count(&self, _request: SetBadgeCountRequest) -> Result<(), NotificationError> {
222        Ok(())
223    }
224
225    fn register_push(
226        &self,
227        _request: PushRegistrationRequest,
228    ) -> Result<PushRegistration, NotificationError> {
229        Ok(PushRegistration {
230            platform: PushPlatform::Other("memory".into()),
231            token: "memory-push-token".into(),
232            endpoint: None,
233            p256dh_key: None,
234            auth_secret: None,
235        })
236    }
237
238    fn unregister_push(&self) -> Result<(), NotificationError> {
239        Ok(())
240    }
241}
242
243#[derive(Debug, Default)]
244pub struct NativeNotificationHost;
245
246pub(crate) fn native_notification_host() -> impl NotificationHost {
247    NativeNotificationHost
248}
249
250impl NativeNotificationHost {
251    #[cfg(any(test, not(target_os = "macos")))]
252    fn supported() -> bool {
253        cfg!(target_os = "ios")
254            || cfg!(target_os = "macos")
255            || (cfg!(target_os = "linux") && command_exists("notify-send"))
256    }
257
258    #[cfg(any(test, not(target_os = "macos")))]
259    fn native_settings() -> NotificationSettings {
260        if Self::supported() {
261            NotificationSettings {
262                permission: NotificationPermission::Granted,
263                alerts: true,
264                badge: cfg!(any(target_os = "ios", target_os = "macos")),
265                sound: true,
266                scheduling: cfg!(any(target_os = "ios", target_os = "macos"))
267                    || (cfg!(target_os = "linux") && command_exists("notify-send")),
268                push: false,
269            }
270        } else {
271            NotificationSettings {
272                permission: NotificationPermission::Unsupported,
273                ..Default::default()
274            }
275        }
276    }
277
278    fn show_now(&self, request: &NotificationRequest) -> Result<(), NotificationError> {
279        #[cfg(target_os = "ios")]
280        {
281            ios_register_local_notifications();
282            ios_show_local_notification(request, None);
283            return Ok(());
284        }
285
286        #[cfg(not(target_os = "ios"))]
287        {
288            if cfg!(target_os = "macos") {
289                #[cfg(target_os = "macos")]
290                {
291                    macos_deliver_notification(request, None)?;
292                    return Ok(());
293                }
294            }
295
296            if cfg!(target_os = "linux") {
297                if !command_exists("notify-send") {
298                    return Err(NotificationError::unsupported("show"));
299                }
300                Command::new("notify-send")
301                    .arg(&request.title)
302                    .arg(&request.body)
303                    .spawn()
304                    .map_err(notification_command_error)?
305                    .wait()
306                    .map_err(notification_command_error)?;
307                return Ok(());
308            }
309
310            if cfg!(target_os = "windows") {
311                return Err(NotificationError::unsupported("show_windows_toast"));
312            }
313
314            Err(NotificationError::unsupported("show"))
315        }
316    }
317}
318
319impl NotificationHost for NativeNotificationHost {
320    fn request_permission(
321        &self,
322        _request: NotificationPermissionRequest,
323    ) -> Result<NotificationSettings, NotificationError> {
324        #[cfg(target_os = "macos")]
325        {
326            macos_request_notification_permission()
327        }
328        #[cfg(not(target_os = "macos"))]
329        {
330            #[cfg(target_os = "ios")]
331            ios_register_local_notifications();
332            Ok(Self::native_settings())
333        }
334    }
335
336    fn settings(&self) -> Result<NotificationSettings, NotificationError> {
337        #[cfg(target_os = "macos")]
338        {
339            macos_notification_settings()
340        }
341        #[cfg(not(target_os = "macos"))]
342        {
343            Ok(Self::native_settings())
344        }
345    }
346
347    fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError> {
348        match request.schedule {
349            NotificationSchedule::Immediate => {
350                self.show_now(&request)?;
351                Ok(NotificationReceipt {
352                    id: request.id,
353                    scheduled: false,
354                    delivered: true,
355                })
356            }
357            _ => Err(NotificationError::unsupported("schedule")),
358        }
359    }
360
361    fn schedule(
362        &self,
363        request: NotificationRequest,
364    ) -> Result<NotificationReceipt, NotificationError> {
365        match request.schedule {
366            NotificationSchedule::Immediate => self.show(request),
367            #[cfg(target_os = "ios")]
368            NotificationSchedule::AfterMillis(ms) => {
369                ios_register_local_notifications();
370                ios_show_local_notification(&request, Some(ms as f64 / 1000.0));
371                Ok(NotificationReceipt {
372                    id: request.id,
373                    scheduled: true,
374                    delivered: false,
375                })
376            }
377            #[cfg(target_os = "ios")]
378            NotificationSchedule::AtUnixMillis(ms) => {
379                let now_ms = std::time::SystemTime::now()
380                    .duration_since(std::time::UNIX_EPOCH)
381                    .map(|duration| duration.as_millis() as u64)
382                    .unwrap_or(ms);
383                ios_register_local_notifications();
384                ios_show_local_notification(
385                    &request,
386                    Some(ms.saturating_sub(now_ms) as f64 / 1000.0),
387                );
388                Ok(NotificationReceipt {
389                    id: request.id,
390                    scheduled: true,
391                    delivered: false,
392                })
393            }
394            #[cfg(not(target_os = "ios"))]
395            NotificationSchedule::AfterMillis(ms) => {
396                if cfg!(target_os = "macos") {
397                    #[cfg(target_os = "macos")]
398                    {
399                        macos_deliver_notification(&request, Some(ms as f64 / 1000.0))?;
400                        return Ok(NotificationReceipt {
401                            id: request.id,
402                            scheduled: true,
403                            delivered: false,
404                        });
405                    }
406                }
407                if !(cfg!(target_os = "linux") && command_exists("notify-send")) {
408                    return Err(NotificationError::unsupported("schedule"));
409                }
410                let id = request.id.clone();
411                let request = request.clone();
412                std::thread::spawn(move || {
413                    std::thread::sleep(std::time::Duration::from_millis(ms));
414                    let host = NativeNotificationHost;
415                    let _ = host.show_now(&request);
416                });
417                Ok(NotificationReceipt {
418                    id,
419                    scheduled: true,
420                    delivered: false,
421                })
422            }
423            #[cfg(not(target_os = "ios"))]
424            NotificationSchedule::AtUnixMillis(ms) => {
425                let now_ms = std::time::SystemTime::now()
426                    .duration_since(std::time::UNIX_EPOCH)
427                    .map(|duration| duration.as_millis() as u64)
428                    .unwrap_or(ms);
429                if cfg!(target_os = "macos") {
430                    #[cfg(target_os = "macos")]
431                    {
432                        macos_deliver_notification(
433                            &request,
434                            Some(ms.saturating_sub(now_ms) as f64 / 1000.0),
435                        )?;
436                        return Ok(NotificationReceipt {
437                            id: request.id,
438                            scheduled: true,
439                            delivered: false,
440                        });
441                    }
442                }
443                if !(cfg!(target_os = "linux") && command_exists("notify-send")) {
444                    return Err(NotificationError::unsupported("schedule"));
445                }
446                let id = request.id.clone();
447                let request = request.clone();
448                std::thread::spawn(move || {
449                    std::thread::sleep(std::time::Duration::from_millis(ms.saturating_sub(now_ms)));
450                    let host = NativeNotificationHost;
451                    let _ = host.show_now(&request);
452                });
453                Ok(NotificationReceipt {
454                    id,
455                    scheduled: true,
456                    delivered: false,
457                })
458            }
459        }
460    }
461
462    fn cancel(&self, request: CancelNotificationRequest) -> Result<(), NotificationError> {
463        #[cfg(target_os = "macos")]
464        {
465            macos_cancel_notification(&request.id.0);
466            Ok(())
467        }
468        #[cfg(not(target_os = "macos"))]
469        {
470            let _ = request;
471            Err(NotificationError::unsupported("cancel"))
472        }
473    }
474
475    fn cancel_all(&self) -> Result<(), NotificationError> {
476        #[cfg(target_os = "macos")]
477        {
478            macos_cancel_all_notifications();
479            Ok(())
480        }
481        #[cfg(not(target_os = "macos"))]
482        {
483            Err(NotificationError::unsupported("cancel_all"))
484        }
485    }
486
487    fn set_badge_count(&self, request: SetBadgeCountRequest) -> Result<(), NotificationError> {
488        #[cfg(target_os = "ios")]
489        {
490            ios_set_badge_count(request.count);
491            return Ok(());
492        }
493        #[cfg(target_os = "macos")]
494        {
495            macos_set_badge_count(request.count);
496            return Ok(());
497        }
498        #[cfg(not(any(target_os = "ios", target_os = "macos")))]
499        {
500            let _ = request;
501            Err(NotificationError::unsupported("set_badge_count"))
502        }
503    }
504
505    fn register_push(
506        &self,
507        _request: PushRegistrationRequest,
508    ) -> Result<PushRegistration, NotificationError> {
509        Err(NotificationError::unsupported("register_push"))
510    }
511
512    fn unregister_push(&self) -> Result<(), NotificationError> {
513        Err(NotificationError::unsupported("unregister_push"))
514    }
515}
516
517#[cfg(target_os = "ios")]
518fn ios_register_local_notifications() {
519    unsafe {
520        let app: *mut objc::runtime::Object = msg_send![class!(UIApplication), sharedApplication];
521        if app.is_null() {
522            return;
523        }
524        let settings: *mut objc::runtime::Object = msg_send![
525            class!(UIUserNotificationSettings),
526            settingsForTypes: 7usize
527            categories: std::ptr::null_mut::<objc::runtime::Object>()
528        ];
529        if !settings.is_null() {
530            let _: () = msg_send![app, registerUserNotificationSettings: settings];
531        }
532    }
533}
534
535#[cfg(target_os = "ios")]
536fn ios_show_local_notification(request: &NotificationRequest, delay_seconds: Option<f64>) {
537    unsafe {
538        let notification: *mut objc::runtime::Object = msg_send![class!(UILocalNotification), new];
539        if notification.is_null() {
540            return;
541        }
542        let title = ns_string(&request.title);
543        let body = ns_string(&request.body);
544        let _: () = msg_send![notification, setAlertTitle: title];
545        let _: () = msg_send![notification, setAlertBody: body];
546        if !matches!(request.sound, fission_core::NotificationSound::Silent) {
547            let default_sound: *mut objc::runtime::Object =
548                msg_send![class!(UILocalNotification), defaultSoundName];
549            let _: () = msg_send![notification, setSoundName: default_sound];
550        }
551        if let Some(badge) = request.badge {
552            let _: () = msg_send![notification, setApplicationIconBadgeNumber: badge as isize];
553        }
554        let app: *mut objc::runtime::Object = msg_send![class!(UIApplication), sharedApplication];
555        if app.is_null() {
556            return;
557        }
558        if let Some(delay) = delay_seconds {
559            let date: *mut objc::runtime::Object =
560                msg_send![class!(NSDate), dateWithTimeIntervalSinceNow: delay.max(0.0)];
561            let _: () = msg_send![notification, setFireDate: date];
562            let _: () = msg_send![app, scheduleLocalNotification: notification];
563        } else {
564            let _: () = msg_send![app, presentLocalNotificationNow: notification];
565        }
566    }
567}
568
569#[cfg(target_os = "ios")]
570fn ios_set_badge_count(count: Option<u32>) {
571    unsafe {
572        let app: *mut objc::runtime::Object = msg_send![class!(UIApplication), sharedApplication];
573        if !app.is_null() {
574            let _: () = msg_send![app, setApplicationIconBadgeNumber: count.unwrap_or(0) as isize];
575        }
576    }
577}
578
579#[cfg(target_os = "macos")]
580fn macos_request_notification_permission() -> Result<NotificationSettings, NotificationError> {
581    let pair = Arc::new((Mutex::new(None), Condvar::new()));
582    let pair_for_block = pair.clone();
583    let block = ConcreteBlock::new(move |granted: bool, _error: *mut objc::runtime::Object| {
584        let (lock, cvar) = &*pair_for_block;
585        if let Ok(mut result) = lock.lock() {
586            *result = Some(granted);
587            cvar.notify_all();
588        }
589    })
590    .copy();
591    unsafe {
592        let center: *mut objc::runtime::Object =
593            msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
594        if center.is_null() {
595            return Err(NotificationError::unsupported("notifications"));
596        }
597        let options = 1usize | 2usize | 4usize;
598        let _: () = msg_send![
599            center,
600            requestAuthorizationWithOptions: options
601            completionHandler: &*block
602        ];
603    }
604    let (lock, cvar) = &*pair;
605    let guard = lock.lock().unwrap();
606    let (guard, _) = cvar
607        .wait_timeout_while(guard, std::time::Duration::from_secs(30), |value| {
608            value.is_none()
609        })
610        .unwrap();
611    let granted = (*guard).unwrap_or(false);
612    Ok(NotificationSettings {
613        permission: if granted {
614            NotificationPermission::Granted
615        } else {
616            NotificationPermission::Denied
617        },
618        alerts: granted,
619        badge: granted,
620        sound: granted,
621        scheduling: granted,
622        push: false,
623    })
624}
625
626#[cfg(target_os = "macos")]
627fn macos_notification_settings() -> Result<NotificationSettings, NotificationError> {
628    let pair = Arc::new((Mutex::new(None), Condvar::new()));
629    let pair_for_block = pair.clone();
630    let block = ConcreteBlock::new(move |settings: *mut objc::runtime::Object| {
631        let status: i64 = if settings.is_null() {
632            0
633        } else {
634            unsafe { msg_send![settings, authorizationStatus] }
635        };
636        let permission = match status {
637            2 => NotificationPermission::Granted,
638            3 | 4 => NotificationPermission::Provisional,
639            1 => NotificationPermission::Denied,
640            _ => NotificationPermission::NotDetermined,
641        };
642        let enabled = matches!(
643            permission,
644            NotificationPermission::Granted | NotificationPermission::Provisional
645        );
646        let (lock, cvar) = &*pair_for_block;
647        if let Ok(mut result) = lock.lock() {
648            *result = Some(NotificationSettings {
649                permission,
650                alerts: enabled,
651                badge: enabled,
652                sound: enabled,
653                scheduling: enabled,
654                push: false,
655            });
656            cvar.notify_all();
657        }
658    })
659    .copy();
660    unsafe {
661        let center: *mut objc::runtime::Object =
662            msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
663        if center.is_null() {
664            return Err(NotificationError::unsupported("notifications"));
665        }
666        let _: () = msg_send![center, getNotificationSettingsWithCompletionHandler: &*block];
667    }
668    let (lock, cvar) = &*pair;
669    let guard = lock.lock().unwrap();
670    let (guard, _) = cvar
671        .wait_timeout_while(guard, std::time::Duration::from_secs(30), |value| {
672            value.is_none()
673        })
674        .unwrap();
675    Ok(guard.clone().unwrap_or(NotificationSettings {
676        permission: NotificationPermission::NotDetermined,
677        ..Default::default()
678    }))
679}
680
681#[cfg(target_os = "macos")]
682fn macos_deliver_notification(
683    request: &NotificationRequest,
684    delay_seconds: Option<f64>,
685) -> Result<(), NotificationError> {
686    let settings = macos_request_notification_permission()?;
687    if !matches!(
688        settings.permission,
689        NotificationPermission::Granted | NotificationPermission::Provisional
690    ) {
691        return Err(NotificationError::new(
692            "permission_denied",
693            "macOS notification permission is not granted",
694        ));
695    }
696
697    let pair = Arc::new((Mutex::new(None), Condvar::new()));
698    let pair_for_block = pair.clone();
699    let block = ConcreteBlock::new(move |error: *mut objc::runtime::Object| {
700        let message = if error.is_null() {
701            None
702        } else {
703            Some(macos_error_description(error))
704        };
705        let (lock, cvar) = &*pair_for_block;
706        if let Ok(mut result) = lock.lock() {
707            *result = Some(message);
708            cvar.notify_all();
709        }
710    })
711    .copy();
712
713    unsafe {
714        let content: *mut objc::runtime::Object =
715            msg_send![class!(UNMutableNotificationContent), new];
716        if content.is_null() {
717            return Err(NotificationError::unsupported("notification_content"));
718        }
719        let title = ns_string(&request.title);
720        let body = ns_string(&request.body);
721        let _: () = msg_send![content, setTitle: title];
722        let _: () = msg_send![content, setBody: body];
723        if let Some(subtitle) = request.subtitle.as_deref() {
724            let subtitle = ns_string(subtitle);
725            let _: () = msg_send![content, setSubtitle: subtitle];
726        }
727        if !matches!(request.sound, fission_core::NotificationSound::Silent) {
728            let sound: *mut objc::runtime::Object =
729                msg_send![class!(UNNotificationSound), defaultSound];
730            let _: () = msg_send![content, setSound: sound];
731        }
732        if let Some(badge) = request.badge {
733            let badge: *mut objc::runtime::Object =
734                msg_send![class!(NSNumber), numberWithUnsignedInteger: badge as usize];
735            let _: () = msg_send![content, setBadge: badge];
736        }
737
738        let trigger: *mut objc::runtime::Object = if let Some(delay) = delay_seconds {
739            msg_send![
740                class!(UNTimeIntervalNotificationTrigger),
741                triggerWithTimeInterval: delay.max(1.0)
742                repeats: false
743            ]
744        } else {
745            std::ptr::null_mut()
746        };
747        let identifier = ns_string(&request.id.0);
748        let notification_request: *mut objc::runtime::Object = msg_send![
749            class!(UNNotificationRequest),
750            requestWithIdentifier: identifier
751            content: content
752            trigger: trigger
753        ];
754        if notification_request.is_null() {
755            return Err(NotificationError::unsupported("notification_request"));
756        }
757        let center: *mut objc::runtime::Object =
758            msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
759        if center.is_null() {
760            return Err(NotificationError::unsupported("notifications"));
761        }
762        let _: () = msg_send![center, addNotificationRequest: notification_request withCompletionHandler: &*block];
763    }
764
765    let (lock, cvar) = &*pair;
766    let guard = lock.lock().unwrap();
767    let (guard, _) = cvar
768        .wait_timeout_while(guard, std::time::Duration::from_secs(30), |value| {
769            value.is_none()
770        })
771        .unwrap();
772    if let Some(Some(message)) = guard.clone() {
773        Err(NotificationError::new("host_error", message))
774    } else {
775        Ok(())
776    }
777}
778
779#[cfg(target_os = "macos")]
780fn macos_error_description(error: *mut objc::runtime::Object) -> String {
781    unsafe {
782        let description: *mut objc::runtime::Object = msg_send![error, localizedDescription];
783        ns_string_to_string(description).unwrap_or_else(|| "macOS notification error".into())
784    }
785}
786
787#[cfg(target_os = "macos")]
788fn macos_cancel_notification(id: &str) {
789    unsafe {
790        let center: *mut objc::runtime::Object =
791            msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
792        if center.is_null() {
793            return;
794        }
795        let identifier = ns_string(id);
796        let ids: *mut objc::runtime::Object =
797            msg_send![class!(NSArray), arrayWithObject: identifier];
798        let _: () = msg_send![center, removePendingNotificationRequestsWithIdentifiers: ids];
799        let _: () = msg_send![center, removeDeliveredNotificationsWithIdentifiers: ids];
800    }
801}
802
803#[cfg(target_os = "macos")]
804fn macos_cancel_all_notifications() {
805    unsafe {
806        let center: *mut objc::runtime::Object =
807            msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
808        if center.is_null() {
809            return;
810        }
811        let _: () = msg_send![center, removeAllPendingNotificationRequests];
812        let _: () = msg_send![center, removeAllDeliveredNotifications];
813    }
814}
815
816#[cfg(target_os = "macos")]
817fn macos_set_badge_count(count: Option<u32>) {
818    unsafe {
819        let app: *mut objc::runtime::Object = msg_send![class!(NSApplication), sharedApplication];
820        if app.is_null() {
821            return;
822        }
823        let dock_tile: *mut objc::runtime::Object = msg_send![app, dockTile];
824        if dock_tile.is_null() {
825            return;
826        }
827        let label = count
828            .filter(|count| *count > 0)
829            .map(|count| ns_string(&count.to_string()))
830            .unwrap_or(std::ptr::null_mut());
831        let _: () = msg_send![dock_tile, setBadgeLabel: label];
832    }
833}
834
835#[cfg(any(target_os = "ios", target_os = "macos"))]
836fn ns_string(value: &str) -> *mut objc::runtime::Object {
837    unsafe {
838        let string: *mut objc::runtime::Object = msg_send![class!(NSString), alloc];
839        msg_send![
840            string,
841            initWithBytes: value.as_ptr() as *const c_void
842            length: value.len()
843            encoding: 4usize
844        ]
845    }
846}
847
848#[cfg(target_os = "macos")]
849fn ns_string_to_string(value: *mut objc::runtime::Object) -> Option<String> {
850    if value.is_null() {
851        return None;
852    }
853    unsafe {
854        let ptr: *const std::os::raw::c_char = msg_send![value, UTF8String];
855        (!ptr.is_null()).then(|| CStr::from_ptr(ptr).to_string_lossy().into_owned())
856    }
857}
858
859fn command_exists(name: &str) -> bool {
860    std::env::var_os("PATH")
861        .and_then(|paths| {
862            std::env::split_paths(&paths)
863                .map(|path| path.join(name))
864                .find(|path| path.is_file())
865        })
866        .is_some()
867}
868
869#[cfg(not(target_os = "ios"))]
870fn notification_command_error(error: std::io::Error) -> NotificationError {
871    NotificationError::new("host_error", error.to_string())
872}
873
874pub(crate) fn register_notification_capabilities(
875    async_registry: &mut AsyncRegistry,
876    host: Arc<dyn NotificationHost>,
877) {
878    let request_host = host.clone();
879    async_registry.register_operation_capability(
880        REQUEST_NOTIFICATION_PERMISSION,
881        move |request, _| {
882            let host = request_host.clone();
883            async move { host.request_permission(request) }
884        },
885    );
886
887    let settings_host = host.clone();
888    async_registry.register_operation_capability(GET_NOTIFICATION_SETTINGS, move |(), _| {
889        let host = settings_host.clone();
890        async move { host.settings() }
891    });
892
893    let show_host = host.clone();
894    async_registry.register_operation_capability(SHOW_NOTIFICATION, move |request, _| {
895        let host = show_host.clone();
896        async move { host.show(request) }
897    });
898
899    let schedule_host = host.clone();
900    async_registry.register_operation_capability(SCHEDULE_NOTIFICATION, move |request, _| {
901        let host = schedule_host.clone();
902        async move { host.schedule(request) }
903    });
904
905    let cancel_host = host.clone();
906    async_registry.register_operation_capability(CANCEL_NOTIFICATION, move |request, _| {
907        let host = cancel_host.clone();
908        async move { host.cancel(request) }
909    });
910
911    let cancel_all_host = host.clone();
912    async_registry.register_operation_capability(CANCEL_ALL_NOTIFICATIONS, move |(), _| {
913        let host = cancel_all_host.clone();
914        async move { host.cancel_all() }
915    });
916
917    let badge_host = host.clone();
918    async_registry.register_operation_capability(SET_BADGE_COUNT, move |request, _| {
919        let host = badge_host.clone();
920        async move { host.set_badge_count(request) }
921    });
922
923    let push_host = host.clone();
924    async_registry.register_operation_capability(REGISTER_PUSH_NOTIFICATIONS, move |request, _| {
925        let host = push_host.clone();
926        async move { host.register_push(request) }
927    });
928
929    async_registry.register_operation_capability(UNREGISTER_PUSH_NOTIFICATIONS, move |(), _| {
930        let host = host.clone();
931        async move { host.unregister_push() }
932    });
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use fission_core::NotificationId;
939
940    #[test]
941    fn unsupported_host_reports_permission_without_panicking() {
942        let host = UnsupportedNotificationHost;
943        let settings = host
944            .request_permission(NotificationPermissionRequest::default())
945            .unwrap();
946        assert_eq!(settings.permission, NotificationPermission::Unsupported);
947        assert_eq!(
948            host.show(NotificationRequest::default()).unwrap_err().code,
949            "unsupported"
950        );
951    }
952
953    #[test]
954    fn memory_host_returns_receipts() {
955        let host = MemoryNotificationHost;
956        let receipt = host
957            .show(NotificationRequest {
958                id: NotificationId::new("n1"),
959                title: "Title".into(),
960                body: "Body".into(),
961                ..Default::default()
962            })
963            .unwrap();
964        assert_eq!(receipt.id, NotificationId::new("n1"));
965        assert!(receipt.delivered);
966    }
967
968    #[test]
969    fn native_host_settings_are_honest_about_support() {
970        let settings = NativeNotificationHost::native_settings();
971        if NativeNotificationHost::supported() {
972            assert_eq!(settings.permission, NotificationPermission::Granted);
973            assert!(settings.alerts);
974            assert!(!settings.push);
975        } else {
976            assert_eq!(settings.permission, NotificationPermission::Unsupported);
977            assert!(!settings.alerts);
978        }
979    }
980}