Skip to main content

mac_usernotifications/
auth.rs

1//! Request macOS permission to display alerts and play sounds.
2//!
3//! Uses [`UNUserNotificationCenter`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter)
4//! and [`UNAuthorizationOptions`](https://developer.apple.com/documentation/usernotifications/unauthorizationoptions).
5
6use std::{cell::Cell, future::Future, ptr::NonNull};
7
8use block2::RcBlock;
9use futures_channel::oneshot;
10#[cfg(feature = "blocking-wrappers")]
11use futures_lite::future;
12use objc2::runtime::Bool;
13use objc2_user_notifications::{UNAuthorizationOptions, UNUserNotificationCenter};
14
15use crate::{Error, check_bundle, settings::NotificationSettings, worker};
16
17fn request_auth_inner() -> impl Future<Output = Result<bool, Error>> + Send + 'static {
18    log::trace!("dispatching to worker");
19    let (tx, rx) = oneshot::channel::<Result<bool, Error>>();
20    worker::dispatch(move || {
21        log::debug!("closure entered on worker");
22        let center = UNUserNotificationCenter::currentNotificationCenter();
23
24        let tx = Cell::new(Some(tx));
25        log::debug!("calling requestAuthorizationWithOptions");
26        center.requestAuthorizationWithOptions_completionHandler(
27            UNAuthorizationOptions::Alert | UNAuthorizationOptions::Sound,
28            &RcBlock::new(move |granted: Bool, err: *mut objc2_foundation::NSError| {
29                log::trace!(
30                    "authorization completed (granted={}, err.is_null={})",
31                    granted.as_bool(),
32                    err.is_null()
33                );
34                let Some(tx) = tx.take() else {
35                    log::warn!("completion fired twice");
36                    return;
37                };
38
39                if tx.send(Ok(granted.as_bool())).is_err() {
40                    log::warn!("receiver dropped before completion");
41                }
42            }),
43        );
44        log::debug!("requestAuthorizationWithOptions returned");
45    });
46    async move { rx.await.unwrap_or(Err(Error::NotificationRejected)) }
47}
48
49/// Ask for permission to display notifications.
50///
51/// Returns `Ok(true)` if granted, `Ok(false)` if denied.
52/// Prefer this over [`blocking::request_auth`](`crate::blocking::request_auth`) from async contexts.
53pub async fn request_auth() -> Result<bool, Error> {
54    check_bundle()?;
55    request_auth_inner().await
56}
57
58/// Ask for permission to display notifications (blocks caller).
59///
60/// Returns `Ok(true)` if granted, `Ok(false)` if denied.
61///
62/// Uses [`futures_lite::future::block_on`] rather than [`crate::block_on_main`] because the [`requestAuthorization`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:))
63/// completion handler fires on an Apple-internal dispatch queue, not the main run loop.
64/// There's no need to pump [`NSRunLoop`](https://developer.apple.com/documentation/foundation/nsrunloop) here; don't change this to `block_on_main`.
65#[cfg(feature = "blocking-wrappers")]
66pub fn request_auth_blocking() -> Result<bool, Error> {
67    check_bundle()?;
68    future::block_on(request_auth_inner())
69}
70
71fn get_settings_inner() -> impl Future<Output = Result<NotificationSettings, Error>> + Send + 'static
72{
73    let (tx, rx) = oneshot::channel::<NotificationSettings>();
74    let tx = Cell::new(Some(tx));
75    worker::dispatch(move || {
76        UNUserNotificationCenter::currentNotificationCenter()
77            .getNotificationSettingsWithCompletionHandler(&RcBlock::new(
78                move |settings: NonNull<objc2_user_notifications::UNNotificationSettings>| {
79                    let settings_ref = unsafe { settings.as_ref() };
80                    let result = NotificationSettings {
81                        authorization_status: settings_ref.authorizationStatus().into(),
82                        alert_enabled: settings_ref.alertSetting().into(),
83                        badge_enabled: settings_ref.badgeSetting().into(),
84                        sound_enabled: settings_ref.soundSetting().into(),
85                        lock_screen_enabled: settings_ref.lockScreenSetting().into(),
86                        notification_center_enabled: settings_ref
87                            .notificationCenterSetting()
88                            .into(),
89                    };
90                    if let Some(sender) = tx.take() {
91                        let _ = sender.send(result);
92                    }
93                },
94            ));
95    });
96    async move { rx.await.map_err(|_| Error::NotificationRejected) }
97}
98
99/// Query the current notification authorization settings without prompting.
100///
101/// Returns the current [`NotificationSettings`] for the app. Does not ask the user
102/// for permission; use [`request_auth`] for that.
103pub async fn get_notification_settings() -> Result<NotificationSettings, Error> {
104    check_bundle()?;
105    get_settings_inner().await
106}
107
108/// Query the current notification authorization settings, blocking the caller.
109///
110/// See [`get_notification_settings`] for details.
111#[cfg(feature = "blocking-wrappers")]
112pub fn get_notification_settings_blocking() -> Result<NotificationSettings, Error> {
113    check_bundle()?;
114    future::block_on(get_settings_inner())
115}