app

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

commit e4433c33db4566ac37b203d3716bb3b274110a71
parent f11101f749ffdc4cfcbcefad7e0e7635f5fd6d02
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 02:46:34 +0000

app-core: add web geolocation adapter

- implement web geolocation position resolver with permissions checks
- expose web geolocation module in public exports
- expand web-sys features for geolocation and permissions APIs
- add non-wasm geolocation coverage test

Diffstat:
MCargo.toml | 10++++++++++
Mcrates/core/src/geolocation/mod.rs | 2++
Acrates/core/src/geolocation/web.rs | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 183 insertions(+), 0 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -44,10 +44,20 @@ web-sys = { version = "0.3.77", features = [ "IdbRequest", "IdbTransaction", "IdbTransactionMode", + "Coordinates", + "Geolocation", "Navigator", "Notification", "NotificationOptions", "NotificationPermission", + "PermissionDescriptor", + "PermissionName", + "PermissionState", + "PermissionStatus", + "Permissions", + "Position", + "PositionError", + "PositionOptions", ] } wasm-bindgen-futures = "0.4" base64 = "0.22" diff --git a/crates/core/src/geolocation/mod.rs b/crates/core/src/geolocation/mod.rs @@ -1,5 +1,6 @@ pub mod error; pub mod types; +pub mod web; pub use error::{RadrootsClientGeolocationError, RadrootsClientGeolocationErrorMessage}; pub use types::{ @@ -7,3 +8,4 @@ pub use types::{ RadrootsClientGeolocationPosition, RadrootsClientGeolocationResult, }; +pub use web::RadrootsClientWebGeolocation; diff --git a/crates/core/src/geolocation/web.rs b/crates/core/src/geolocation/web.rs @@ -0,0 +1,171 @@ +use async_trait::async_trait; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{JsCast, JsValue}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::JsFuture; + +use super::{ + RadrootsClientGeolocation, + RadrootsClientGeolocationError, + RadrootsClientGeolocationPosition, + RadrootsClientGeolocationResult, +}; + +pub struct RadrootsClientWebGeolocation; + +impl RadrootsClientWebGeolocation { + #[cfg(target_arch = "wasm32")] + fn policy_allows_geolocation( + document: &web_sys::Document, + ) -> Option<bool> { + let policy = js_sys::Reflect::get(document.as_ref(), &JsValue::from_str("permissionsPolicy")) + .ok()?; + if policy.is_null() || policy.is_undefined() { + return None; + } + let allows = js_sys::Reflect::get(&policy, &JsValue::from_str("allowsFeature")) + .ok()?; + let allows = allows.dyn_into::<js_sys::Function>().ok()?; + let result = allows + .call1(&policy, &JsValue::from_str("geolocation")) + .ok()?; + result.as_bool() + } + + #[cfg(target_arch = "wasm32")] + async fn permission_state( + navigator: &web_sys::Navigator, + ) -> Option<web_sys::PermissionState> { + let permissions = navigator.permissions().ok()?; + let descriptor = web_sys::PermissionDescriptor::new(web_sys::PermissionName::Geolocation); + let promise = permissions.query(&descriptor).ok()?; + let status = JsFuture::from(promise).await.ok()?; + let status: web_sys::PermissionStatus = status.dyn_into().ok()?; + Some(status.state()) + } + + #[cfg(target_arch = "wasm32")] + async fn get_current_position( + geolocation: &web_sys::Geolocation, + ) -> Result<web_sys::Position, JsValue> { + let geolocation = geolocation.clone(); + let promise = js_sys::Promise::new(&mut |resolve, reject| { + let success = wasm_bindgen::closure::Closure::once( + move |position: web_sys::Position| { + let _ = resolve.call1(&JsValue::NULL, &position); + }, + ); + let failure = wasm_bindgen::closure::Closure::once( + move |error: web_sys::PositionError| { + let _ = reject.call1(&JsValue::NULL, &error); + }, + ); + let mut options = web_sys::PositionOptions::new(); + options.enable_high_accuracy(true); + options.timeout(10000.0); + options.maximum_age(30000.0); + if geolocation + .get_current_position_with_error_callback_and_options( + success.as_ref().unchecked_ref(), + Some(failure.as_ref().unchecked_ref()), + &options, + ) + .is_err() + { + let _ = reject.call0(&JsValue::NULL); + } + success.forget(); + failure.forget(); + }); + JsFuture::from(promise).await + } + + #[cfg(target_arch = "wasm32")] + fn map_error( + policy_allows: Option<bool>, + error: &web_sys::PositionError, + ) -> RadrootsClientGeolocationError { + match error.code() { + web_sys::PositionError::PERMISSION_DENIED => { + if policy_allows == Some(false) { + RadrootsClientGeolocationError::BlockedByPermissionsPolicy + } else { + RadrootsClientGeolocationError::PermissionDenied + } + } + web_sys::PositionError::POSITION_UNAVAILABLE => { + RadrootsClientGeolocationError::PositionUnavailable + } + web_sys::PositionError::TIMEOUT => RadrootsClientGeolocationError::Timeout, + _ => RadrootsClientGeolocationError::UnknownError, + } + } +} + +#[async_trait(?Send)] +impl RadrootsClientGeolocation for RadrootsClientWebGeolocation { + async fn current( + &self, + ) -> RadrootsClientGeolocationResult<RadrootsClientGeolocationPosition> { + #[cfg(not(target_arch = "wasm32"))] + { + return Err(RadrootsClientGeolocationError::LocationUnavailable); + } + #[cfg(target_arch = "wasm32")] + { + let window = + web_sys::window().ok_or(RadrootsClientGeolocationError::LocationUnavailable)?; + let document = window + .document() + .ok_or(RadrootsClientGeolocationError::LocationUnavailable)?; + let navigator = window.navigator(); + let geolocation = navigator + .geolocation() + .map_err(|_| RadrootsClientGeolocationError::LocationUnavailable)?; + + let policy_allows = Self::policy_allows_geolocation(&document); + let _ = Self::permission_state(&navigator).await; + + if policy_allows == Some(false) { + return Err(RadrootsClientGeolocationError::BlockedByPermissionsPolicy); + } + + match Self::get_current_position(&geolocation).await { + Ok(position) => { + let coords = position.coords(); + Ok(RadrootsClientGeolocationPosition { + lat: coords.latitude(), + lng: coords.longitude(), + accuracy: Some(coords.accuracy()), + altitude: coords.altitude(), + altitude_accuracy: coords.altitude_accuracy(), + }) + } + Err(err) => { + if let Ok(position_error) = err.dyn_into::<web_sys::PositionError>() { + return Err(Self::map_error(policy_allows, &position_error)); + } + Err(RadrootsClientGeolocationError::UnknownError) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::RadrootsClientWebGeolocation; + use crate::geolocation::{ + RadrootsClientGeolocation, + RadrootsClientGeolocationError, + }; + + #[test] + fn non_wasm_current_errors() { + let geo = RadrootsClientWebGeolocation; + let err = futures::executor::block_on(geo.current()) + .expect_err("location unavailable"); + assert_eq!(err, RadrootsClientGeolocationError::LocationUnavailable); + } +}