app

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

commit c27c28e8ced192b22103dad2b62595e16ed8b7af
parent 97826a1e663d4413863d2ad99813deca11236a6e
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 02:30:33 +0000

app: add setup draft and eula metadata

- add setup draft model and datastore helpers
- extend app state with eula version and hash
- add setup draft key map and accessors
- update config and bootstrap tests for draft storage

Diffstat:
Mapp/src/bootstrap.rs | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/config.rs | 30++++++++++++++++++++++++++++++
Mapp/src/data.rs | 14++++++++++++++
Mapp/src/lib.rs | 6++++++
Mapp/src/setup.rs | 2++
5 files changed, 264 insertions(+), 0 deletions(-)

diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs @@ -5,11 +5,13 @@ use radroots_app_core::notifications::RadrootsClientNotificationsPermission; use crate::{ app_datastore_obj_key_state, + app_datastore_obj_key_setup_draft, app_log_debug_emit, app_state_record_new, app_state_record_validate, app_state_timestamp_ms, RadrootsAppState, + RadrootsAppSetupDraft, RadrootsAppStateError, RadrootsAppStateRecord, RadrootsAppInitError, @@ -135,6 +137,43 @@ pub async fn app_datastore_clear_bootstrap<T: RadrootsClientDatastore>( Ok(()) } +pub async fn app_datastore_read_setup_draft<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &RadrootsAppKeyMapConfig, +) -> RadrootsAppInitResult<Option<RadrootsAppSetupDraft>> { + let key = app_datastore_obj_key_setup_draft(key_maps).map_err(RadrootsAppInitError::Config)?; + match datastore.get_obj::<RadrootsAppSetupDraft>(key).await { + Ok(draft) => Ok(Some(draft)), + Err(RadrootsClientDatastoreError::NoResult) => Ok(None), + Err(err) => Err(RadrootsAppInitError::Datastore(err)), + } +} + +pub async fn app_datastore_write_setup_draft<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &RadrootsAppKeyMapConfig, + draft: &RadrootsAppSetupDraft, +) -> RadrootsAppInitResult<RadrootsAppSetupDraft> { + let key = app_datastore_obj_key_setup_draft(key_maps).map_err(RadrootsAppInitError::Config)?; + let value = datastore + .set_obj(key, draft) + .await + .map_err(RadrootsAppInitError::Datastore)?; + Ok(value) +} + +pub async fn app_datastore_clear_setup_draft<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &RadrootsAppKeyMapConfig, +) -> RadrootsAppInitResult<()> { + let key = app_datastore_obj_key_setup_draft(key_maps).map_err(RadrootsAppInitError::Config)?; + match datastore.del_obj(key).await { + Ok(_) => Ok(()), + Err(RadrootsClientDatastoreError::NoResult) => Ok(()), + Err(err) => Err(RadrootsAppInitError::Datastore(err)), + } +} + pub async fn app_state_set_notifications_permission<T: RadrootsClientDatastore>( datastore: &T, key_maps: &RadrootsAppKeyMapConfig, @@ -166,10 +205,13 @@ pub async fn app_state_set_notifications_permission_value<T: RadrootsClientDatas mod tests { use super::{ app_datastore_clear_bootstrap, + app_datastore_clear_setup_draft, app_datastore_create_state, app_datastore_has_state, app_datastore_read_state, + app_datastore_read_setup_draft, app_datastore_update_state, + app_datastore_write_setup_draft, app_state_set_notifications_permission, app_state_set_notifications_permission_value, app_state_notifications_permission_value, @@ -178,9 +220,11 @@ mod tests { use crate::{ app_key_maps_default, RadrootsAppInitError, + RadrootsAppRole, RadrootsAppState, RadrootsAppStateError, RadrootsAppStateRecord, + RadrootsAppSetupDraft, }; use async_trait::async_trait; use radroots_app_core::backup::RadrootsClientBackupDatastorePayload; @@ -197,6 +241,136 @@ mod tests { use serde::Serialize; use std::cell::RefCell; + struct SetupDraftDatastore { + draft: RefCell<Option<RadrootsAppSetupDraft>>, + } + + #[async_trait(?Send)] + impl RadrootsClientDatastore for SetupDraftDatastore { + fn get_config(&self) -> RadrootsClientIdbConfig { + IDB_CONFIG_DATASTORE + } + + fn get_store_id(&self) -> &str { + "test" + } + + async fn init(&self) -> RadrootsClientDatastoreResult<()> { + Ok(()) + } + + async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn set_obj<T>( + &self, + _key: &str, + value: &T, + ) -> RadrootsClientDatastoreResult<T> + where + T: Serialize + DeserializeOwned + Clone, + { + let encoded = serde_json::to_string(value) + .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?; + if let Ok(parsed) = serde_json::from_str::<RadrootsAppSetupDraft>(&encoded) { + *self.draft.borrow_mut() = Some(parsed); + return Ok(value.clone()); + } + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn update_obj<T>( + &self, + _key: &str, + _value: &T, + ) -> RadrootsClientDatastoreResult<T> + where + T: Serialize + DeserializeOwned + Clone, + { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T> + where + T: DeserializeOwned, + { + if let Some(draft) = self.draft.borrow().as_ref() { + let encoded = serde_json::to_string(draft) + .map_err(|_| RadrootsClientDatastoreError::NoResult)?; + return serde_json::from_str(&encoded) + .map_err(|_| RadrootsClientDatastoreError::NoResult); + } + Err(RadrootsClientDatastoreError::NoResult) + } + + async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + *self.draft.borrow_mut() = None; + Ok("cleared".to_string()) + } + + async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn set_param( + &self, + _key: &str, + _key_param: &str, + _value: &str, + ) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn get_param( + &self, + _key: &str, + _key_param: &str, + ) -> RadrootsClientDatastoreResult<String> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn entries_pref( + &self, + _key_prefix: &str, + ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn reset(&self) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn export_backup( + &self, + ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + + async fn import_backup( + &self, + _payload: RadrootsClientBackupDatastorePayload, + ) -> RadrootsClientDatastoreResult<()> { + Err(RadrootsClientDatastoreError::IdbUndefined) + } + } + struct TestDatastore { state: Option<RadrootsAppState>, record: RefCell<Option<RadrootsAppStateRecord>>, @@ -514,4 +688,42 @@ mod tests { .expect_err("missing"); assert_eq!(err, RadrootsAppInitError::State(RadrootsAppStateError::Missing)); } + + #[test] + fn setup_draft_roundtrip() { + let datastore = SetupDraftDatastore { + draft: RefCell::new(None), + }; + let key_maps = app_key_maps_default(); + let draft = RadrootsAppSetupDraft { + nostr_public_key: Some("pub".to_string()), + profile_name: Some("radroots".to_string()), + role: Some(RadrootsAppRole::Public), + nip05_request: Some(true), + }; + let stored = futures::executor::block_on(app_datastore_write_setup_draft( + &datastore, + &key_maps, + &draft, + )) + .expect("store draft"); + assert_eq!(stored, draft); + let loaded = futures::executor::block_on(app_datastore_read_setup_draft( + &datastore, + &key_maps, + )) + .expect("read draft"); + assert_eq!(loaded, Some(draft)); + let cleared = futures::executor::block_on(app_datastore_clear_setup_draft( + &datastore, + &key_maps, + )); + assert!(cleared.is_ok()); + let loaded = futures::executor::block_on(app_datastore_read_setup_draft( + &datastore, + &key_maps, + )) + .expect("read draft"); + assert!(loaded.is_none()); + } } diff --git a/app/src/config.rs b/app/src/config.rs @@ -17,6 +17,7 @@ pub type RadrootsAppKeystoreKeyMap = BTreeMap<&'static str, &'static str>; pub const APP_DATASTORE_KEY_NOSTR_KEY: &str = "nostr:key"; pub const APP_DATASTORE_KEY_EULA_DATE: &str = "app:eula:date"; pub const APP_DATASTORE_KEY_OBJ_STATE: &str = "app:data"; +pub const APP_DATASTORE_KEY_OBJ_SETUP_DRAFT: &str = "setup:draft"; pub const APP_DATASTORE_KEY_LOG_ENTRY: &str = "log:entry"; pub const APP_KEYSTORE_KEY_NOSTR_DEFAULT: &str = "nostr:default"; @@ -62,6 +63,7 @@ pub fn app_key_maps_default() -> RadrootsAppKeyMapConfig { param_map.insert("log_entry", app_datastore_param_log_entry as RadrootsAppDatastoreKeyParam); let mut obj_map = BTreeMap::new(); obj_map.insert("state", APP_DATASTORE_KEY_OBJ_STATE); + obj_map.insert("setup_draft", APP_DATASTORE_KEY_OBJ_SETUP_DRAFT); RadrootsAppKeyMapConfig { key_map, param_map, @@ -117,6 +119,9 @@ pub fn app_key_maps_validate(config: &RadrootsAppKeyMapConfig) -> RadrootsAppCon if !config.obj_map.contains_key("state") { return Err(RadrootsAppConfigError::MissingObjMap("state")); } + if !config.obj_map.contains_key("setup_draft") { + return Err(RadrootsAppConfigError::MissingObjMap("setup_draft")); + } Ok(()) } @@ -169,6 +174,12 @@ pub fn app_datastore_obj_key_state(config: &RadrootsAppKeyMapConfig) -> Radroots app_datastore_obj_key(config, "state") } +pub fn app_datastore_obj_key_setup_draft( + config: &RadrootsAppKeyMapConfig, +) -> RadrootsAppConfigResult<&'static str> { + app_datastore_obj_key(config, "setup_draft") +} + pub fn app_keystore_key( config: &RadrootsAppKeystoreKeyMap, key: &'static str, @@ -280,9 +291,11 @@ mod tests { app_config_from_env, app_datastore_param_nostr_profile, app_datastore_param_log_entry, + app_datastore_param_radroots_profile, app_datastore_key_eula_date, app_datastore_key_nostr_key, app_datastore_obj_key_state, + app_datastore_obj_key_setup_draft, app_key_maps_validate, app_keystore_key_maps_default, app_keystore_key_maps_validate, @@ -295,12 +308,14 @@ mod tests { RadrootsAppConfig, RadrootsAppConfigError, RadrootsAppDatastoreConfig, + RadrootsAppDatastoreKeyParam, RadrootsAppKeyMapConfig, RadrootsAppKeystoreConfig, RadrootsAppKeystoreKeyMap, APP_DATASTORE_KEY_EULA_DATE, APP_DATASTORE_KEY_NOSTR_KEY, APP_DATASTORE_KEY_OBJ_STATE, + APP_DATASTORE_KEY_OBJ_SETUP_DRAFT, APP_DATASTORE_KEY_LOG_ENTRY, APP_KEYSTORE_KEY_NOSTR_DEFAULT, }; @@ -385,6 +400,10 @@ mod tests { config.obj_map.get("state"), Some(&APP_DATASTORE_KEY_OBJ_STATE) ); + assert_eq!( + config.obj_map.get("setup_draft"), + Some(&APP_DATASTORE_KEY_OBJ_SETUP_DRAFT) + ); assert_eq!(app_datastore_param_nostr_profile("abc"), "nostr:abc:profile"); assert_eq!( app_datastore_param_log_entry("entry"), @@ -400,6 +419,13 @@ mod tests { missing.key_map.insert("nostr_key", APP_DATASTORE_KEY_NOSTR_KEY); let err = app_key_maps_validate(&missing).expect_err("missing keys"); assert_eq!(err, RadrootsAppConfigError::MissingKeyMap("eula_date")); + missing.key_map.insert("eula_date", APP_DATASTORE_KEY_EULA_DATE); + missing.obj_map.insert("state", APP_DATASTORE_KEY_OBJ_STATE); + missing.param_map.insert("nostr_profile", app_datastore_param_nostr_profile as RadrootsAppDatastoreKeyParam); + missing.param_map.insert("radroots_profile", app_datastore_param_radroots_profile as RadrootsAppDatastoreKeyParam); + missing.param_map.insert("log_entry", app_datastore_param_log_entry as RadrootsAppDatastoreKeyParam); + let err = app_key_maps_validate(&missing).expect_err("missing draft"); + assert_eq!(err, RadrootsAppConfigError::MissingObjMap("setup_draft")); } #[test] @@ -427,6 +453,10 @@ mod tests { app_datastore_obj_key_state(&config).expect("state key"), APP_DATASTORE_KEY_OBJ_STATE ); + assert_eq!( + app_datastore_obj_key_setup_draft(&config).expect("draft key"), + APP_DATASTORE_KEY_OBJ_SETUP_DRAFT + ); let nostr_param = app_datastore_param_key(&config, "nostr_profile").expect("param"); assert_eq!(nostr_param("abc"), "nostr:abc:profile"); let log_param = app_datastore_param_key(&config, "log_entry").expect("param"); diff --git a/app/src/data.rs b/app/src/data.rs @@ -20,6 +20,8 @@ pub struct RadrootsAppState { pub active_key: String, pub role: RadrootsAppRole, pub eula_date: String, + pub eula_version: String, + pub eula_hash: String, pub nip05_key: Option<String>, pub notifications_permission: Option<String>, } @@ -30,12 +32,22 @@ impl Default for RadrootsAppState { active_key: String::new(), role: RadrootsAppRole::default(), eula_date: String::new(), + eula_version: String::from("0.1.0"), + eula_hash: String::from("unknown"), nip05_key: None, notifications_permission: None, } } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RadrootsAppSetupDraft { + pub nostr_public_key: Option<String>, + pub profile_name: Option<String>, + pub role: Option<RadrootsAppRole>, + pub nip05_request: Option<bool>, +} + pub const APP_STATE_SCHEMA_VERSION: u32 = 1; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -176,6 +188,8 @@ mod tests { assert_eq!(data.active_key, ""); assert_eq!(data.role, RadrootsAppRole::Public); assert_eq!(data.eula_date, ""); + assert_eq!(data.eula_version, "0.1.0"); + assert_eq!(data.eula_hash, "unknown"); assert!(data.nip05_key.is_none()); assert!(data.notifications_permission.is_none()); } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -24,6 +24,9 @@ pub use bootstrap::{ app_datastore_create_state, app_datastore_has_state, app_datastore_read_state, + app_datastore_read_setup_draft, + app_datastore_write_setup_draft, + app_datastore_clear_setup_draft, app_datastore_update_state, app_state_set_notifications_permission, app_state_set_notifications_permission_value, @@ -38,6 +41,7 @@ pub use data::{ app_state_timestamp_ms, RadrootsAppRole, RadrootsAppState, + RadrootsAppSetupDraft, RadrootsAppStateError, RadrootsAppStateRecord, APP_STATE_SCHEMA_VERSION, @@ -138,6 +142,7 @@ pub use config::{ app_datastore_param_key, app_datastore_obj_key, app_datastore_obj_key_state, + app_datastore_obj_key_setup_draft, app_assets_geocoder_db_url, app_assets_sql_wasm_url, app_keystore_key_maps_default, @@ -162,6 +167,7 @@ pub use config::{ APP_DATASTORE_KEY_LOG_ENTRY, APP_DATASTORE_KEY_NOSTR_KEY, APP_DATASTORE_KEY_OBJ_STATE, + APP_DATASTORE_KEY_OBJ_SETUP_DRAFT, APP_KEYSTORE_KEY_NOSTR_DEFAULT, }; pub use init::{ diff --git a/app/src/setup.rs b/app/src/setup.rs @@ -37,6 +37,8 @@ pub fn app_setup_state_new(active_key: String, eula_date: String) -> RadrootsApp active_key, role: RadrootsAppRole::default(), eula_date, + eula_version: String::from("0.1.0"), + eula_hash: String::from("unknown"), nip05_key: None, notifications_permission: None, }