app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mapp/src/lib.rs | 2++
Aapp/src/notifications.rs | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"); + } +}