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:
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,
}