app

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

commit b711153efeb5403eeef5ce6dc0f3908efa140e1b
parent a7b87697b3e0b7c2f93ba294c78058cb369de709
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 20:40:45 +0000

app: add setup flow state machine

- add setup flow draft and validation types
- centralize step transition helpers
- encode role selection logic
- add unit tests for transitions

Diffstat:
Mapp/src/lib.rs | 12++++++++++++
Aapp/src/setup_flow.rs | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 237 insertions(+), 0 deletions(-)

diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -14,6 +14,7 @@ mod logs; mod notifications; mod settings; mod setup; +mod setup_flow; mod setup_lock; mod setup_status; mod theme; @@ -144,6 +145,17 @@ pub use setup::{ app_setup_step_default, RadrootsAppSetupStep, }; +pub use setup_flow::{ + app_setup_flow_next_step, + app_setup_flow_prev_step, + app_setup_flow_role_from_choices, + app_setup_flow_validate, + RadrootsAppSetupBusinessChoice, + RadrootsAppSetupFarmerChoice, + RadrootsAppSetupFlowDraft, + RadrootsAppSetupFlowValidation, + RadrootsAppSetupKeyChoice, +}; pub use setup_lock::{ app_setup_lock_acquire, app_setup_lock_enabled, diff --git a/app/src/setup_flow.rs b/app/src/setup_flow.rs @@ -0,0 +1,225 @@ +#![forbid(unsafe_code)] + +use crate::{app_setup_step_default, RadrootsAppRole, RadrootsAppSetupStep}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppSetupKeyChoice { + Generate, + AddExisting, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppSetupFarmerChoice { + Yes, + No, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppSetupBusinessChoice { + Yes, + No, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsAppSetupFlowDraft { + pub step: RadrootsAppSetupStep, + pub key_choice: Option<RadrootsAppSetupKeyChoice>, + pub farmer_choice: Option<RadrootsAppSetupFarmerChoice>, + pub business_choice: Option<RadrootsAppSetupBusinessChoice>, + pub profile_name: String, + pub profile_nip05: bool, +} + +impl Default for RadrootsAppSetupFlowDraft { + fn default() -> Self { + Self { + step: app_setup_step_default(), + key_choice: None, + farmer_choice: None, + business_choice: None, + profile_name: String::new(), + profile_nip05: true, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsAppSetupFlowValidation { + pub can_continue: bool, + pub can_back: bool, + pub next_step: RadrootsAppSetupStep, + pub prev_step: RadrootsAppSetupStep, +} + +pub fn app_setup_flow_role_from_choices( + farmer_choice: Option<RadrootsAppSetupFarmerChoice>, + business_choice: Option<RadrootsAppSetupBusinessChoice>, +) -> Option<RadrootsAppRole> { + match farmer_choice? { + RadrootsAppSetupFarmerChoice::Yes => Some(RadrootsAppRole::Farm), + RadrootsAppSetupFarmerChoice::No => match business_choice? { + RadrootsAppSetupBusinessChoice::Yes => Some(RadrootsAppRole::Business), + RadrootsAppSetupBusinessChoice::No => Some(RadrootsAppRole::Individual), + }, + } +} + +pub fn app_setup_flow_next_step(draft: &RadrootsAppSetupFlowDraft) -> RadrootsAppSetupStep { + match draft.step { + RadrootsAppSetupStep::Intro => RadrootsAppSetupStep::KeyChoice, + RadrootsAppSetupStep::KeyChoice => match draft.key_choice { + Some(RadrootsAppSetupKeyChoice::Generate) => RadrootsAppSetupStep::Profile, + Some(RadrootsAppSetupKeyChoice::AddExisting) => RadrootsAppSetupStep::KeyAddExisting, + None => RadrootsAppSetupStep::KeyChoice, + }, + RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::Profile, + RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::FarmerSetup, + RadrootsAppSetupStep::FarmerSetup => match draft.farmer_choice { + Some(RadrootsAppSetupFarmerChoice::Yes) => RadrootsAppSetupStep::Eula, + Some(RadrootsAppSetupFarmerChoice::No) => RadrootsAppSetupStep::BusinessSetup, + None => RadrootsAppSetupStep::FarmerSetup, + }, + RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::Eula, + RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::Eula, + } +} + +pub fn app_setup_flow_prev_step(draft: &RadrootsAppSetupFlowDraft) -> RadrootsAppSetupStep { + match draft.step { + RadrootsAppSetupStep::Intro => RadrootsAppSetupStep::Intro, + RadrootsAppSetupStep::KeyChoice => RadrootsAppSetupStep::Intro, + RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::KeyChoice, + RadrootsAppSetupStep::Profile => match draft.key_choice { + Some(RadrootsAppSetupKeyChoice::AddExisting) => RadrootsAppSetupStep::KeyAddExisting, + _ => RadrootsAppSetupStep::KeyChoice, + }, + RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Profile, + RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::FarmerSetup, + RadrootsAppSetupStep::Eula => match draft.farmer_choice { + Some(RadrootsAppSetupFarmerChoice::No) => RadrootsAppSetupStep::BusinessSetup, + _ => RadrootsAppSetupStep::FarmerSetup, + }, + } +} + +pub fn app_setup_flow_validate(draft: &RadrootsAppSetupFlowDraft) -> RadrootsAppSetupFlowValidation { + let can_continue = match draft.step { + RadrootsAppSetupStep::KeyChoice => draft.key_choice.is_some(), + RadrootsAppSetupStep::FarmerSetup => draft.farmer_choice.is_some(), + RadrootsAppSetupStep::BusinessSetup => draft.business_choice.is_some(), + RadrootsAppSetupStep::Profile => { + !(draft.profile_nip05 && draft.profile_name.trim().is_empty()) + } + _ => true, + }; + let can_back = !matches!(draft.step, RadrootsAppSetupStep::Intro); + RadrootsAppSetupFlowValidation { + can_continue, + can_back, + next_step: app_setup_flow_next_step(draft), + prev_step: app_setup_flow_prev_step(draft), + } +} + +#[cfg(test)] +mod tests { + use super::{ + app_setup_flow_next_step, + app_setup_flow_prev_step, + app_setup_flow_role_from_choices, + app_setup_flow_validate, + RadrootsAppSetupBusinessChoice, + RadrootsAppSetupFarmerChoice, + RadrootsAppSetupFlowDraft, + RadrootsAppSetupKeyChoice, + }; + use crate::{RadrootsAppRole, RadrootsAppSetupStep}; + + #[test] + fn flow_defaults_to_intro() { + let draft = RadrootsAppSetupFlowDraft::default(); + assert_eq!(draft.step, RadrootsAppSetupStep::Intro); + assert!(draft.profile_nip05); + } + + #[test] + fn flow_role_from_choices_maps_roles() { + assert_eq!( + app_setup_flow_role_from_choices( + Some(RadrootsAppSetupFarmerChoice::Yes), + None, + ), + Some(RadrootsAppRole::Farm) + ); + assert_eq!( + app_setup_flow_role_from_choices( + Some(RadrootsAppSetupFarmerChoice::No), + Some(RadrootsAppSetupBusinessChoice::Yes), + ), + Some(RadrootsAppRole::Business) + ); + assert_eq!( + app_setup_flow_role_from_choices( + Some(RadrootsAppSetupFarmerChoice::No), + Some(RadrootsAppSetupBusinessChoice::No), + ), + Some(RadrootsAppRole::Individual) + ); + assert_eq!( + app_setup_flow_role_from_choices( + Some(RadrootsAppSetupFarmerChoice::No), + None, + ), + None + ); + } + + #[test] + fn flow_next_step_respects_choices() { + let mut draft = RadrootsAppSetupFlowDraft::default(); + draft.step = RadrootsAppSetupStep::KeyChoice; + draft.key_choice = Some(RadrootsAppSetupKeyChoice::Generate); + assert_eq!(app_setup_flow_next_step(&draft), RadrootsAppSetupStep::Profile); + draft.key_choice = Some(RadrootsAppSetupKeyChoice::AddExisting); + assert_eq!( + app_setup_flow_next_step(&draft), + RadrootsAppSetupStep::KeyAddExisting + ); + draft.step = RadrootsAppSetupStep::FarmerSetup; + draft.farmer_choice = Some(RadrootsAppSetupFarmerChoice::No); + assert_eq!( + app_setup_flow_next_step(&draft), + RadrootsAppSetupStep::BusinessSetup + ); + } + + #[test] + fn flow_prev_step_respects_choices() { + let mut draft = RadrootsAppSetupFlowDraft::default(); + draft.step = RadrootsAppSetupStep::Profile; + draft.key_choice = Some(RadrootsAppSetupKeyChoice::AddExisting); + assert_eq!( + app_setup_flow_prev_step(&draft), + RadrootsAppSetupStep::KeyAddExisting + ); + draft.step = RadrootsAppSetupStep::Eula; + draft.farmer_choice = Some(RadrootsAppSetupFarmerChoice::No); + assert_eq!( + app_setup_flow_prev_step(&draft), + RadrootsAppSetupStep::BusinessSetup + ); + } + + #[test] + fn flow_validation_disables_continue_for_missing_name() { + let mut draft = RadrootsAppSetupFlowDraft::default(); + draft.step = RadrootsAppSetupStep::Profile; + draft.profile_name = String::new(); + draft.profile_nip05 = true; + let validation = app_setup_flow_validate(&draft); + assert!(!validation.can_continue); + draft.profile_nip05 = false; + let validation = app_setup_flow_validate(&draft); + assert!(validation.can_continue); + } +}