app

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

commit c68f93ef61bea6916b2cbb836ace420430f25183
parent e0abab8c39b467921653d72f5d0e7e80369a93f0
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 03:18:48 +0000

app: finalize setup on eula accept

- add setup finalize helper for explicit keys
- persist setup state on eula agreement
- handle add-existing keys and clear draft
- extend setup tests for finalize path

Diffstat:
Mapp/src/app.rs | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mapp/src/lib.rs | 1+
Mapp/src/setup.rs | 33++++++++++++++++++++++++++++++++-
3 files changed, 112 insertions(+), 2 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -9,6 +9,11 @@ use web_sys::HtmlElement; use radroots_app_core::datastore::RadrootsClientDatastore; use radroots_app_core::idb::IDB_CONFIG_LOGS; +use radroots_app_core::keystore::{ + RadrootsClientKeystoreError, + RadrootsClientKeystoreNostr, + RadrootsClientWebKeystoreNostr, +}; use radroots_app_ui_components::{ RadrootsAppUiButtonLayoutAction, RadrootsAppUiButtonLayoutBackAction, @@ -33,11 +38,15 @@ use crate::{ app_log_error_emit, app_log_error_store, app_config_default, + app_datastore_clear_setup_draft, app_datastore_read_state, app_datastore_read_setup_draft, app_datastore_write_setup_draft, + app_keystore_nostr_ensure_key, app_state_notifications_permission_value, app_state_set_notifications_permission_value, + app_setup_eula_date, + app_setup_finalize_with_key, app_setup_step_default, app_health_check_all, RadrootsAppBackends, @@ -49,6 +58,7 @@ use crate::{ RadrootsAppInitStage, RadrootsAppNotifications, RadrootsAppLogsPage, + RadrootsAppKeystoreError, RadrootsAppRole, RadrootsAppSettingsPage, RadrootsAppSetupDraft, @@ -320,14 +330,82 @@ fn SetupPage() -> impl IntoView { } }); let advance_step: Callback<()> = { + let backends = backends.clone(); let setup_step = setup_step.clone(); let setup_key_choice = setup_key_choice.clone(); + let nostr_key_add = nostr_key_add.clone(); let profile_name = profile_name.clone(); let setup_required = setup_required.clone(); Callback::new(move |_| { let current_step = setup_step.get(); if matches!(current_step, RadrootsAppSetupStep::Eula) { - setup_required.set(Some(false)); + let key_choice = setup_key_choice.get(); + let nostr_key_add = nostr_key_add.get(); + let eula_date = app_setup_eula_date(); + let setup_required = setup_required.clone(); + let backends = backends.clone(); + spawn_local(async move { + let Some((datastore, key_maps, keystore_config)) = backends.with_untracked(|value| { + value.as_ref().map(|backends| { + ( + backends.datastore.clone(), + backends.config.datastore.key_maps.clone(), + backends.nostr_keystore.get_config(), + ) + }) + }) else { + return; + }; + let keystore = RadrootsClientWebKeystoreNostr::new(Some(keystore_config)); + let active_key = match key_choice { + Some(RadrootsAppSetupKeyChoice::AddExisting) => { + let secret_key = nostr_key_add.trim(); + if secret_key.is_empty() { + let err = RadrootsAppInitError::Keystore( + RadrootsClientKeystoreError::NostrInvalidSecretKey, + ); + let _ = app_log_error_emit(&err); + return; + } + match keystore.add(secret_key).await { + Ok(value) => value, + Err(err) => { + let init_err = RadrootsAppInitError::Keystore(err); + let _ = app_log_error_emit(&init_err); + return; + } + } + } + _ => match app_keystore_nostr_ensure_key(&keystore).await { + Ok(value) => value, + Err(err) => { + let init_err = match err { + RadrootsAppKeystoreError::Keystore(inner) => { + RadrootsAppInitError::Keystore(inner) + } + RadrootsAppKeystoreError::KeyMismatch => RadrootsAppInitError::Keystore( + RadrootsClientKeystoreError::NostrInvalidSecretKey, + ), + }; + let _ = app_log_error_emit(&init_err); + return; + } + }, + }; + if let Err(err) = app_setup_finalize_with_key( + datastore.as_ref(), + &key_maps, + active_key, + eula_date, + ) + .await + { + let _ = app_log_error_emit(&err); + return; + } + let _ = app_datastore_clear_setup_draft(datastore.as_ref(), &key_maps).await; + setup_required.set(Some(false)); + }); return; } if matches!(current_step, RadrootsAppSetupStep::Profile) { diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -125,6 +125,7 @@ pub use logging::{ pub use notifications::{RadrootsAppNotifications, RadrootsAppNotificationsError, RadrootsAppNotificationsResult}; pub use setup::{ app_setup_eula_date, + app_setup_finalize_with_key, app_setup_initialize, app_setup_state_new, app_setup_step_default, diff --git a/app/src/setup.rs b/app/src/setup.rs @@ -108,7 +108,16 @@ pub async fn app_setup_initialize<T: RadrootsClientDatastore, K: RadrootsClientK RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey) } })?; - let state = app_setup_state_new(active_key.clone(), app_setup_eula_date()); + app_setup_finalize_with_key(datastore, key_maps, active_key, app_setup_eula_date()).await +} + +pub async fn app_setup_finalize_with_key<T: RadrootsClientDatastore>( + datastore: &T, + key_maps: &RadrootsAppKeyMapConfig, + active_key: String, + eula_date: String, +) -> RadrootsAppInitResult<RadrootsAppState> { + let state = app_setup_state_new(active_key.clone(), eula_date); let stored_state = app_datastore_create_state(datastore, key_maps, &state).await?; let key_name = app_datastore_key_nostr_key(key_maps).map_err(RadrootsAppInitError::Config)?; datastore @@ -123,6 +132,7 @@ pub async fn app_setup_initialize<T: RadrootsClientDatastore, K: RadrootsClientK mod tests { use super::{ app_setup_eula_date, + app_setup_finalize_with_key, app_setup_initialize, app_setup_state_new, app_setup_step_default, @@ -433,4 +443,25 @@ mod tests { assert_eq!(stored, public_key); assert!(datastore.record.borrow().is_some()); } + + #[test] + fn setup_finalize_with_key_writes_state() { + let datastore = TestDatastore { + record: RefCell::new(None), + values: RefCell::new(BTreeMap::new()), + }; + let key_maps = app_key_maps_default(); + let state = futures::executor::block_on(app_setup_finalize_with_key( + &datastore, + &key_maps, + "pub".to_string(), + "2025-01-01T00:00:00Z".to_string(), + )) + .expect("finalize"); + assert_eq!(state.active_key, "pub"); + let key_name = app_datastore_key_nostr_key(&key_maps).expect("key name"); + let stored = futures::executor::block_on(datastore.get(key_name)).expect("stored"); + assert_eq!(stored, "pub"); + assert!(datastore.record.borrow().is_some()); + } }