commit d46567d46be1b0d7cf6bb10cda8296309623a698
parent 0624030217a5543e8b1edd1156592509e3538779
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 21:18:14 +0000
app: add notifications service wrapper
- add app notifications adapter around core client
- expose permission check and request helpers
- keep wrapper side-effect free until invoked
- add unit tests for error mapping
Diffstat:
2 files changed, 140 insertions(+), 0 deletions(-)
diff --git a/app/src/lib.rs b/app/src/lib.rs
@@ -8,6 +8,7 @@ mod data;
mod health;
mod init;
mod keystore;
+mod notifications;
mod entry;
pub use app::App;
@@ -40,6 +41,7 @@ pub use keystore::{
AppKeystoreError,
AppKeystoreResult,
};
+pub use notifications::{AppNotifications, AppNotificationsError, AppNotificationsResult};
pub use config::{
app_config_default,
app_config_from_env,
diff --git a/app/src/notifications.rs b/app/src/notifications.rs
@@ -0,0 +1,138 @@
+#![forbid(unsafe_code)]
+
+use radroots_app_core::notifications::{
+ RadrootsClientNotifications,
+ RadrootsClientNotificationsConfig,
+ RadrootsClientNotificationsError,
+ RadrootsClientNotificationsPermission,
+ RadrootsClientWebNotifications,
+};
+
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::JsValue;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AppNotificationsError {
+ Notifications(RadrootsClientNotificationsError),
+}
+
+pub type AppNotificationsResult<T> = Result<T, AppNotificationsError>;
+
+impl AppNotificationsError {
+ pub const fn message(self) -> &'static str {
+ match self {
+ AppNotificationsError::Notifications(err) => err.message(),
+ }
+ }
+}
+
+impl std::fmt::Display for AppNotificationsError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(self.message())
+ }
+}
+
+impl std::error::Error for AppNotificationsError {}
+
+impl From<RadrootsClientNotificationsError> for AppNotificationsError {
+ fn from(err: RadrootsClientNotificationsError) -> Self {
+ AppNotificationsError::Notifications(err)
+ }
+}
+
+pub struct AppNotifications {
+ client: RadrootsClientWebNotifications,
+}
+
+impl AppNotifications {
+ pub fn new(config: Option<RadrootsClientNotificationsConfig>) -> Self {
+ Self {
+ client: RadrootsClientWebNotifications::new(config),
+ }
+ }
+
+ pub fn get_config(&self) -> &RadrootsClientNotificationsConfig {
+ self.client.get_config()
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ fn notification_available(window: &web_sys::Window) -> bool {
+ js_sys::Reflect::has(window.as_ref(), &JsValue::from_str("Notification"))
+ .unwrap_or(false)
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ fn permission_from_web(
+ permission: web_sys::NotificationPermission,
+ ) -> RadrootsClientNotificationsPermission {
+ match permission {
+ web_sys::NotificationPermission::Granted => {
+ RadrootsClientNotificationsPermission::Granted
+ }
+ web_sys::NotificationPermission::Denied => RadrootsClientNotificationsPermission::Denied,
+ web_sys::NotificationPermission::Default => {
+ RadrootsClientNotificationsPermission::Default
+ }
+ _ => RadrootsClientNotificationsPermission::Unavailable,
+ }
+ }
+
+ pub async fn permission(
+ &self,
+ ) -> AppNotificationsResult<RadrootsClientNotificationsPermission> {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ return Ok(RadrootsClientNotificationsPermission::Unavailable);
+ }
+ #[cfg(target_arch = "wasm32")]
+ {
+ let window =
+ web_sys::window().ok_or(RadrootsClientNotificationsError::Unavailable)?;
+ if !Self::notification_available(&window) {
+ return Ok(RadrootsClientNotificationsPermission::Unavailable);
+ }
+ Ok(Self::permission_from_web(web_sys::Notification::permission()))
+ }
+ }
+
+ pub async fn request_permission(
+ &self,
+ ) -> AppNotificationsResult<RadrootsClientNotificationsPermission> {
+ self.client
+ .notify_init()
+ .await
+ .map_err(AppNotificationsError::from)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{AppNotifications, AppNotificationsError};
+ use radroots_app_core::notifications::{
+ RadrootsClientNotificationsConfig,
+ RadrootsClientNotificationsError,
+ RadrootsClientNotificationsPermission,
+ };
+
+ #[test]
+ fn permission_is_unavailable_on_native() {
+ let app = AppNotifications::new(Some(RadrootsClientNotificationsConfig {
+ app_name: String::from("Radroots"),
+ }));
+ let permission = futures::executor::block_on(app.permission())
+ .expect("permission");
+ assert_eq!(permission, RadrootsClientNotificationsPermission::Unavailable);
+ }
+
+ #[test]
+ fn request_permission_maps_errors() {
+ let app = AppNotifications::new(None);
+ let err = futures::executor::block_on(app.request_permission())
+ .expect_err("permission request error");
+ assert_eq!(
+ err,
+ AppNotificationsError::Notifications(RadrootsClientNotificationsError::Unavailable)
+ );
+ assert_eq!(err.to_string(), "error.client.notifications.unavailable");
+ }
+}