app

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

commit 68865cead977121c6f81f2bd6c27ebddde5bc785
parent 980f478bcb207acaafd29d8c8ce028c357a94991
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 18:13:47 +0000

app: extend setup roles and eula ui

- add business setup step and role mapping
- expand role enum to individual farm business
- refresh eula actions layout and styles
- regenerate i18n build artifacts

Diffstat:
Mapp/i18n/build/i18n.catalog.json | 11+++++++++++
Mapp/i18n/build/id_map.json | 1+
Mapp/i18n/build/id_map_hash | 2+-
Mapp/i18n/build/manifest.json | 4++--
Mapp/i18n/build/packs/en.mf2pack | 0
Mapp/i18n/locales/en/messages.mf2 | 2++
Mapp/src/app.rs | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mapp/src/bootstrap.rs | 5+++--
Mapp/src/data.rs | 15++++++++++-----
Mapp/src/setup.rs | 48+++++++++++++++++++++++++++++++++++++++---------
Mapp/stylesheets/apps-ui.css | 30++++++++++++++++++++++++++++++
Mcrates/ui-components/src/button_layout.rs | 28+++++++++++++++++++++++-----
12 files changed, 312 insertions(+), 96 deletions(-)

diff --git a/app/i18n/build/i18n.catalog.json b/app/i18n/build/i18n.catalog.json @@ -1248,6 +1248,17 @@ } }, { + "key": "app.setup.business.title", + "id": 1801429027, + "args": [], + "features": { + "select": false, + "plural_cardinal": false, + "plural_ordinal": false, + "formatters": [] + } + }, + { "key": "app.setup.eula.acceptance.body", "id": 2382792662, "args": [], diff --git a/app/i18n/build/id_map.json b/app/i18n/build/id_map.json @@ -112,6 +112,7 @@ "app.settings.appearance.color_mode.option.system": 116103587, "app.settings.appearance.title": 410103833, "app.settings.title": 2068161917, + "app.setup.business.title": 1801429027, "app.setup.eula.acceptance.body": 2382792662, "app.setup.eula.acceptance.title": 3507789532, "app.setup.eula.changes.body": 951360382, diff --git a/app/i18n/build/id_map_hash b/app/i18n/build/id_map_hash @@ -1 +1 @@ -sha256:8fcc5219c69f901ff56bf3890c0127b9a7a3e324f1c4475efed91fd28a54bf64 +sha256:1e03c508c7169c9adae1a42d2067051774c96ed67e7396703a0ec3e56173a014 diff --git a/app/i18n/build/manifest.json b/app/i18n/build/manifest.json @@ -1 +1 @@ -{"schema":1,"release_id":"dev","generated_at":"2026-02-02T00:00:00Z","default_locale":"en","supported_locales":["en"],"id_map_hash":"sha256:8fcc5219c69f901ff56bf3890c0127b9a7a3e324f1c4475efed91fd28a54bf64","mf2_packs":{"en":{"kind":"base","url":"packs/en.mf2pack","hash":"sha256:4c050faaa27e4d6ed0203d1da1ecf344fa70eab12a747935ad918f752c3c930b","size":10709,"content_encoding":"identity","pack_schema":0}}} -\ No newline at end of file +{"schema":1,"release_id":"dev","generated_at":"2026-02-02T00:00:00Z","default_locale":"en","supported_locales":["en"],"id_map_hash":"sha256:1e03c508c7169c9adae1a42d2067051774c96ed67e7396703a0ec3e56173a014","mf2_packs":{"en":{"kind":"base","url":"packs/en.mf2pack","hash":"sha256:e78181c9838bd04d3863f9e047a1705d885ad3faab2d3d64893fa479f52ac8e3","size":10765,"content_encoding":"identity","pack_schema":0}}} +\ No newline at end of file diff --git a/app/i18n/build/packs/en.mf2pack b/app/i18n/build/packs/en.mf2pack Binary files differ. diff --git a/app/i18n/locales/en/messages.mf2 b/app/i18n/locales/en/messages.mf2 @@ -148,6 +148,8 @@ app.setup.profile.confirm_no_name = Your profile will be created without a name. app.setup.farmer.title = Setup for Farmer +app.setup.business.title = Setup for Business + # eula app.setup.eula.title = End User License Agreement diff --git a/app/src/app.rs b/app/src/app.rs @@ -42,7 +42,6 @@ use crate::{ app_config_default, app_datastore_clear_setup_draft, app_datastore_read_state, - app_datastore_read_setup_draft, app_datastore_write_profile_seed, app_datastore_write_setup_draft, app_keystore_nostr_ensure_key, @@ -175,6 +174,25 @@ enum RadrootsAppSetupFarmerChoice { No, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RadrootsAppSetupBusinessChoice { + Yes, + No, +} + +fn setup_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), + }, + } +} + fn active_key_label(value: Option<String>) -> String { let Some(value) = value else { return t!("app.common.missing"); @@ -310,7 +328,9 @@ fn SetupPage() -> impl IntoView { let setup_step = RwSignal::new_local(app_setup_step_default()); let setup_key_choice = RwSignal::new_local(None::<RadrootsAppSetupKeyChoice>); let setup_farmer_choice = RwSignal::new_local(None::<RadrootsAppSetupFarmerChoice>); + let setup_business_choice = RwSignal::new_local(None::<RadrootsAppSetupBusinessChoice>); let setup_eula_scrolled = RwSignal::new_local(false); + let setup_eula_scroll_ref: NodeRef<leptos::html::Div> = NodeRef::new(); let nostr_key_add = RwSignal::new_local(String::new()); let profile_name = RwSignal::new_local(String::new()); let profile_nip05 = RwSignal::new_local(true); @@ -326,6 +346,8 @@ fn SetupPage() -> impl IntoView { let backends = backends.clone(); let setup_draft_loaded = setup_draft_loaded.clone(); let setup_key_choice = setup_key_choice.clone(); + let setup_farmer_choice = setup_farmer_choice.clone(); + let setup_business_choice = setup_business_choice.clone(); let nostr_key_add = nostr_key_add.clone(); let profile_name = profile_name.clone(); let profile_nip05 = profile_nip05.clone(); @@ -338,20 +360,15 @@ fn SetupPage() -> impl IntoView { else { return; }; + setup_draft_loaded.set(true); + setup_key_choice.set(None); + setup_farmer_choice.set(None); + setup_business_choice.set(None); + nostr_key_add.set(String::new()); + profile_name.set(String::new()); + profile_nip05.set(true); spawn_local(async move { - if let Ok(Some(draft)) = app_datastore_read_setup_draft(datastore.as_ref(), &key_maps).await { - if let Some(public_key) = draft.nostr_public_key { - nostr_key_add.set(public_key); - setup_key_choice.set(Some(RadrootsAppSetupKeyChoice::AddExisting)); - } - if let Some(name) = draft.profile_name { - profile_name.set(name); - } - if let Some(nip05_request) = draft.nip05_request { - profile_nip05.set(nip05_request); - } - } - setup_draft_loaded.set(true); + let _ = app_datastore_clear_setup_draft(datastore.as_ref(), &key_maps).await; }); } }); @@ -360,6 +377,7 @@ fn SetupPage() -> impl IntoView { let setup_draft_loaded = setup_draft_loaded.clone(); let setup_key_choice = setup_key_choice.clone(); let setup_farmer_choice = setup_farmer_choice.clone(); + let setup_business_choice = setup_business_choice.clone(); let nostr_key_add = nostr_key_add.clone(); let profile_name = profile_name.clone(); let profile_nip05 = profile_nip05.clone(); @@ -390,10 +408,14 @@ fn SetupPage() -> impl IntoView { } else { Some(profile_value) }; + let role = setup_role_from_choices( + setup_farmer_choice.get(), + setup_business_choice.get(), + ); let draft = RadrootsAppSetupDraft { nostr_public_key, profile_name, - role: setup_farmer_choice.get().map(|_| RadrootsAppRole::default()), + role, nip05_request: Some(profile_nip05.get()), }; spawn_local(async move { @@ -405,6 +427,8 @@ fn SetupPage() -> impl IntoView { let backends = backends.clone(); let setup_step = setup_step.clone(); let setup_key_choice = setup_key_choice.clone(); + let setup_farmer_choice = setup_farmer_choice.clone(); + let setup_business_choice = setup_business_choice.clone(); let nostr_key_add = nostr_key_add.clone(); let profile_name = profile_name.clone(); let setup_required = setup_required.clone(); @@ -412,6 +436,11 @@ fn SetupPage() -> impl IntoView { let current_step = setup_step.get(); if matches!(current_step, RadrootsAppSetupStep::Eula) { let key_choice = setup_key_choice.get(); + let setup_role = setup_role_from_choices( + setup_farmer_choice.get(), + setup_business_choice.get(), + ) + .unwrap_or_else(RadrootsAppRole::default); let nostr_key_add = nostr_key_add.get(); let profile_name = profile_name.get(); let profile_nip05 = profile_nip05.get(); @@ -500,6 +529,7 @@ fn SetupPage() -> impl IntoView { active_key, eula_date, nip05_key, + setup_role, ) .await { @@ -513,6 +543,9 @@ fn SetupPage() -> impl IntoView { } if matches!(current_step, RadrootsAppSetupStep::Profile) { let profile_name = profile_name.get(); + if profile_nip05.get() && profile_name.trim().is_empty() { + return; + } if profile_name.trim().is_empty() { let setup_step = setup_step.clone(); let confirm_message = t!("app.setup.profile.confirm_no_name"); @@ -544,7 +577,12 @@ fn SetupPage() -> impl IntoView { } RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::Profile, RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::FarmerSetup, - RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Eula, + RadrootsAppSetupStep::FarmerSetup => match setup_farmer_choice.get() { + Some(RadrootsAppSetupFarmerChoice::Yes) => RadrootsAppSetupStep::Eula, + Some(RadrootsAppSetupFarmerChoice::No) => RadrootsAppSetupStep::BusinessSetup, + None => RadrootsAppSetupStep::FarmerSetup, + }, + RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::Eula, RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::Eula, }; }); @@ -559,6 +597,7 @@ fn SetupPage() -> impl IntoView { let rewind_step: Callback<MouseEvent> = { let setup_step = setup_step.clone(); let setup_key_choice = setup_key_choice.clone(); + let setup_farmer_choice = setup_farmer_choice.clone(); Callback::new(move |_| { let current_step = setup_step.get(); let next_step = match current_step { @@ -572,7 +611,11 @@ fn SetupPage() -> impl IntoView { _ => RadrootsAppSetupStep::KeyChoice, }, RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Profile, - RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::FarmerSetup, + RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::FarmerSetup, + RadrootsAppSetupStep::Eula => match setup_farmer_choice.get() { + Some(RadrootsAppSetupFarmerChoice::No) => RadrootsAppSetupStep::BusinessSetup, + _ => RadrootsAppSetupStep::FarmerSetup, + }, }; setup_step.set(next_step); if matches!(next_step, RadrootsAppSetupStep::Intro) { @@ -591,6 +634,22 @@ fn SetupPage() -> impl IntoView { } } }); + Effect::new({ + let setup_step = setup_step.clone(); + let setup_eula_scrolled = setup_eula_scrolled.clone(); + let setup_eula_scroll_ref = setup_eula_scroll_ref.clone(); + move |_| { + if !matches!(setup_step.get(), RadrootsAppSetupStep::Eula) { + return; + } + let Some(target) = setup_eula_scroll_ref.get() else { + return; + }; + if target.scroll_height() <= target.client_height() { + setup_eula_scrolled.set(true); + } + } + }); view! { <main id="app-setup" @@ -910,6 +969,81 @@ fn SetupPage() -> impl IntoView { </div> </section> }.into_any(), + RadrootsAppSetupStep::BusinessSetup => view! { + <section + id="app-setup-business" + class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16" + on:click=move |_| { + setup_business_choice.set(None); + } + > + <div + id="app-setup-business-body" + class="flex flex-1 w-full flex-col justify-center items-center" + > + <div + id="app-setup-business-card" + class="flex flex-col h-[16rem] w-full gap-10 justify-start items-center" + > + <div + id="app-setup-business-title" + class="flex flex-row w-full justify-center items-center" + > + <p class="font-sans font-[600] text-ly0-gl text-3xl"> + {t!("app.setup.business.title")} + </p> + </div> + <div + id="app-setup-business-actions" + class="flex flex-col w-full gap-5 justify-center items-center" + > + <button + id="app-setup-business-yes" + type="button" + class=move || { + if setup_business_choice.get() + == Some(RadrootsAppSetupBusinessChoice::Yes) + { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re" + } else { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re" + } + } + on:click=move |ev| { + ev.stop_propagation(); + setup_business_choice.set(Some(RadrootsAppSetupBusinessChoice::Yes)); + } + > + <span class="font-sans font-[600] text-ly0-gl text-xl"> + {t!("app.common.yes")} + </span> + </button> + <button + id="app-setup-business-no" + type="button" + class=move || { + if setup_business_choice.get() + == Some(RadrootsAppSetupBusinessChoice::No) + { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-selected-press el-re" + } else { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re" + } + } + on:click=move |ev| { + ev.stop_propagation(); + setup_business_choice.set(Some(RadrootsAppSetupBusinessChoice::No)); + } + > + <span class="font-sans font-[600] text-ly0-gl text-xl"> + {t!("app.common.no")} + </span> + </button> + </div> + </div> + </div> + </section> + }.into_any(), RadrootsAppSetupStep::Eula => view! { <section id="app-setup-eula" @@ -929,7 +1063,8 @@ fn SetupPage() -> impl IntoView { </header> <div id="app-setup-eula-scroll" - class="app-page-scroll scroll-hide flex flex-col flex-1 min-h-0 w-full gap-6 px-1 pb-6 overscroll-contain" + class="app-page-scroll scroll-hide flex flex-col flex-1 min-h-0 w-full gap-6 px-1 pb-20 se-compact:pb-12 overscroll-contain font-mono" + node_ref=setup_eula_scroll_ref on:scroll=move |ev| { if setup_eula_scrolled.get() { return; @@ -938,7 +1073,7 @@ fn SetupPage() -> impl IntoView { let scroll_top = target.scroll_top(); let scroll_height = target.scroll_height(); let client_height = target.client_height(); - if scroll_top + client_height >= scroll_height { + if scroll_top + client_height + 1 >= scroll_height { setup_eula_scrolled.set(true); } } @@ -1046,59 +1181,33 @@ fn SetupPage() -> impl IntoView { </div> <div id="app-setup-eula-actions" - class="flex flex-row w-full pt-4 justify-center items-center" + class="flex flex-col w-full pt-4 pb-2 justify-center items-center" > - <button - type="button" - class=move || { - if setup_eula_scrolled.get() { - "group flex flex-row basis-1/2 gap-3 justify-center items-center" - } else { - "group flex flex-row basis-1/2 gap-3 justify-center items-center opacity-80" - } - } - on:click=move |ev| { - ev.stop_propagation(); - rewind_step.run(ev); - } - > - <span class="font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl/80 el-re"> - "-" - </span> - <span class="font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl/80 el-re"> - {t!("app.common.disagree")} - </span> - <span class="font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl/80 el-re"> - "-" - </span> - </button> - <button - type="button" - aria-disabled=move || !setup_eula_scrolled.get() - class=move || { - if setup_eula_scrolled.get() { - "relative group flex flex-row basis-1/2 gap-3 justify-center items-center el-re" - } else { - "relative group flex flex-row basis-1/2 gap-3 justify-center items-center opacity-40 pointer-events-none" - } - } - on:click=move |ev| { - ev.stop_propagation(); - if setup_eula_scrolled.get() { - advance_step.run(()); - } - } - > - <span class="font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re"> - "-" - </span> - <span class="font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re"> - {t!("app.common.agree")} - </span> - <span class="font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re"> - "-" - </span> - </button> + {move || { + let continue_action = RadrootsAppUiButtonLayoutAction { + label: t!("app.common.agree"), + disabled: !setup_eula_scrolled.get(), + loading: false, + on_click: advance_step_click.clone(), + class: Some("button-layout-accent button-layout-compact".to_string()), + class_label: Some("text-base".to_string()), + style: None, + }; + let back_action = RadrootsAppUiButtonLayoutBackAction { + visible: true, + label: Some(t!("app.common.disagree")), + disabled: false, + on_click: rewind_step.clone(), + compact: true, + }; + view! { + <RadrootsAppUiButtonLayoutPair + continue_action=continue_action + back=back_action + class="gap-2".to_string() + /> + }.into_any() + }} </div> </section> }.into_any(), @@ -1115,7 +1224,12 @@ fn SetupPage() -> impl IntoView { let continue_disabled = (matches!(step, RadrootsAppSetupStep::KeyChoice) && setup_key_choice.get().is_none()) || (matches!(step, RadrootsAppSetupStep::FarmerSetup) - && setup_farmer_choice.get().is_none()); + && setup_farmer_choice.get().is_none()) + || (matches!(step, RadrootsAppSetupStep::BusinessSetup) + && setup_business_choice.get().is_none()) + || (matches!(step, RadrootsAppSetupStep::Profile) + && profile_nip05.get() + && profile_name.get().trim().is_empty()); let continue_label = t!("app.common.continue"); let back_label = t!("app.common.back"); let continue_action = RadrootsAppUiButtonLayoutAction { @@ -1123,12 +1237,16 @@ fn SetupPage() -> impl IntoView { disabled: continue_disabled, loading: false, on_click: advance_step_click.clone(), + class: None, + class_label: None, + style: None, }; let back_action = RadrootsAppUiButtonLayoutBackAction { visible: !matches!(step, RadrootsAppSetupStep::Intro), label: Some(back_label), disabled: false, on_click: rewind_step.clone(), + compact: false, }; view! { <RadrootsAppUiButtonLayoutPair diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs @@ -15,6 +15,7 @@ use crate::{ app_state_record_validate, app_state_timestamp_ms, RadrootsAppProfileSeed, + RadrootsAppRole, RadrootsAppState, RadrootsAppSetupDraft, RadrootsAppStateError, @@ -85,7 +86,7 @@ async fn app_datastore_migrate_legacy_state<T: RadrootsClientDatastore>( Ok(value) => value, Err(_) => return Ok(None), }; - let state = app_setup_state_new(active_key.clone(), eula_date); + let state = app_setup_state_new(active_key.clone(), eula_date, RadrootsAppRole::default()); let record = app_state_record_new(state, 1, app_state_timestamp_ms()); let stored = app_datastore_write_state_record(datastore, key_maps, &record).await?; let _ = datastore.del(key_nostr).await; @@ -1020,7 +1021,7 @@ mod tests { let draft = RadrootsAppSetupDraft { nostr_public_key: Some("pub".to_string()), profile_name: Some("radroots".to_string()), - role: Some(RadrootsAppRole::Public), + role: Some(RadrootsAppRole::Individual), nip05_request: Some(true), }; let stored = futures::executor::block_on(app_datastore_write_setup_draft( diff --git a/app/src/data.rs b/app/src/data.rs @@ -6,12 +6,17 @@ use sha2::{Digest, Sha256}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum RadrootsAppRole { - Public, + #[serde(rename = "individual", alias = "Public", alias = "public", alias = "Individual")] + Individual, + #[serde(rename = "farm")] + Farm, + #[serde(rename = "business")] + Business, } impl Default for RadrootsAppRole { fn default() -> Self { - RadrootsAppRole::Public + RadrootsAppRole::Individual } } @@ -188,15 +193,15 @@ mod tests { }; #[test] - fn role_defaults_to_public() { - assert_eq!(RadrootsAppRole::default(), RadrootsAppRole::Public); + fn role_defaults_to_individual() { + assert_eq!(RadrootsAppRole::default(), RadrootsAppRole::Individual); } #[test] fn state_defaults_empty() { let data = RadrootsAppState::default(); assert_eq!(data.active_key, ""); - assert_eq!(data.role, RadrootsAppRole::Public); + assert_eq!(data.role, RadrootsAppRole::Individual); assert_eq!(data.eula_date, ""); assert_eq!(data.eula_version, "0.1.0"); assert_eq!(data.eula_hash, "unknown"); diff --git a/app/src/setup.rs b/app/src/setup.rs @@ -34,10 +34,14 @@ pub fn app_setup_eula_date() -> String { Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) } -pub fn app_setup_state_new(active_key: String, eula_date: String) -> RadrootsAppState { +pub fn app_setup_state_new( + active_key: String, + eula_date: String, + role: RadrootsAppRole, +) -> RadrootsAppState { RadrootsAppState { active_key, - role: RadrootsAppRole::default(), + role, eula_date, eula_version: String::from("0.1.0"), eula_hash: String::from("unknown"), @@ -54,6 +58,7 @@ pub enum RadrootsAppSetupStep { KeyAddExisting, Profile, FarmerSetup, + BusinessSetup, Eula, } @@ -64,7 +69,8 @@ impl RadrootsAppSetupStep { RadrootsAppSetupStep::KeyChoice => RadrootsAppSetupStep::KeyAddExisting, RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::Profile, RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::FarmerSetup, - RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Eula, + RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::BusinessSetup, + RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::Eula, RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::Eula, } } @@ -76,7 +82,8 @@ impl RadrootsAppSetupStep { RadrootsAppSetupStep::KeyAddExisting => RadrootsAppSetupStep::KeyChoice, RadrootsAppSetupStep::Profile => RadrootsAppSetupStep::KeyAddExisting, RadrootsAppSetupStep::FarmerSetup => RadrootsAppSetupStep::Profile, - RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::FarmerSetup, + RadrootsAppSetupStep::BusinessSetup => RadrootsAppSetupStep::FarmerSetup, + RadrootsAppSetupStep::Eula => RadrootsAppSetupStep::BusinessSetup, } } @@ -110,7 +117,15 @@ pub async fn app_setup_initialize<T: RadrootsClientDatastore, K: RadrootsClientK RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey) } })?; - app_setup_finalize_with_key(datastore, key_maps, active_key, app_setup_eula_date(), None).await + app_setup_finalize_with_key( + datastore, + key_maps, + active_key, + app_setup_eula_date(), + None, + RadrootsAppRole::default(), + ) + .await } pub async fn app_setup_finalize_with_key<T: RadrootsClientDatastore>( @@ -119,8 +134,9 @@ pub async fn app_setup_finalize_with_key<T: RadrootsClientDatastore>( active_key: String, eula_date: String, nip05_key: Option<String>, + role: RadrootsAppRole, ) -> RadrootsAppInitResult<RadrootsAppState> { - let mut state = app_setup_state_new(active_key.clone(), eula_date); + let mut state = app_setup_state_new(active_key.clone(), eula_date, role); state.nip05_key = nip05_key; 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)?; @@ -334,9 +350,13 @@ mod tests { #[test] fn setup_state_new_populates_defaults() { - let state = app_setup_state_new("pub".to_string(), "2025-01-01T00:00:00Z".to_string()); + let state = app_setup_state_new( + "pub".to_string(), + "2025-01-01T00:00:00Z".to_string(), + RadrootsAppRole::default(), + ); assert_eq!(state.active_key, "pub"); - assert_eq!(state.role, RadrootsAppRole::Public); + assert_eq!(state.role, RadrootsAppRole::Individual); assert_eq!(state.eula_date, "2025-01-01T00:00:00Z"); assert!(!state.relays.is_empty()); assert!(state.nip05_key.is_none()); @@ -374,6 +394,10 @@ mod tests { ); assert_eq!( RadrootsAppSetupStep::FarmerSetup.next(), + RadrootsAppSetupStep::BusinessSetup + ); + assert_eq!( + RadrootsAppSetupStep::BusinessSetup.next(), RadrootsAppSetupStep::Eula ); assert_eq!( @@ -405,9 +429,13 @@ mod tests { RadrootsAppSetupStep::Profile ); assert_eq!( - RadrootsAppSetupStep::Eula.prev(), + RadrootsAppSetupStep::BusinessSetup.prev(), RadrootsAppSetupStep::FarmerSetup ); + assert_eq!( + RadrootsAppSetupStep::Eula.prev(), + RadrootsAppSetupStep::BusinessSetup + ); } #[test] @@ -417,6 +445,7 @@ mod tests { assert!(!RadrootsAppSetupStep::KeyAddExisting.is_terminal()); assert!(!RadrootsAppSetupStep::Profile.is_terminal()); assert!(!RadrootsAppSetupStep::FarmerSetup.is_terminal()); + assert!(!RadrootsAppSetupStep::BusinessSetup.is_terminal()); assert!(RadrootsAppSetupStep::Eula.is_terminal()); } @@ -462,6 +491,7 @@ mod tests { "pub".to_string(), "2025-01-01T00:00:00Z".to_string(), None, + RadrootsAppRole::default(), )) .expect("finalize"); assert_eq!(state.active_key, "pub"); diff --git a/app/stylesheets/apps-ui.css b/app/stylesheets/apps-ui.css @@ -40,6 +40,36 @@ @apply font-sans font-[600] tracking-wide text-ly1-gl-shade group-active:text-ly1-gl/40 el-re; } +@layer utilities { + .button-layout-compact { + height: 48px; + min-height: 48px; + } + + .button-layout-accent { + background: hsl(var(--ly0-gl-hl) / 1); + color: #fff; + } + + .button-layout-accent:active { + background: hsl(var(--ly0-gl-hl) / 0.85); + } + + .button-layout-accent:disabled { + opacity: 1; + background: hsl(var(--ly0-gl-hl) / 0.6); + color: #fff; + } + + .button-layout-accent .button-layout-label { + color: #fff; + } + + .button-layout-accent:active .button-layout-label { + color: rgba(255, 255, 255, 0.8); + } +} + @utility input-base { @apply flex w-full items-center rounded-touch border border-ly1-edge/60 bg-ly1 px-3 py-2 font-sans text-form_base text-ly1-gl placeholder:text-ly1-gl-label/70 transition-colors focus:outline-none focus:bg-ly1-focus focus:border-ly1-edge/80 disabled:opacity-60; @apply focus:shadow-[inset_0_0_0_1px_hsl(var(--ly1-edge)/0.6)]; diff --git a/crates/ui-components/src/button_layout.rs b/crates/ui-components/src/button_layout.rs @@ -28,6 +28,9 @@ pub struct RadrootsAppUiButtonLayoutAction { pub disabled: bool, pub loading: bool, pub on_click: Callback<MouseEvent>, + pub class: Option<String>, + pub class_label: Option<String>, + pub style: Option<String>, } #[derive(Clone)] @@ -36,6 +39,7 @@ pub struct RadrootsAppUiButtonLayoutBackAction { pub label: Option<String>, pub disabled: bool, pub on_click: Callback<MouseEvent>, + pub compact: bool, } #[component] @@ -46,6 +50,7 @@ pub fn RadrootsAppUiButtonLayout( #[prop(optional)] loading: bool, #[prop(optional)] class: Option<String>, #[prop(optional)] class_label: Option<String>, + #[prop(optional)] style: Option<String>, #[prop(optional)] hide_active: bool, ) -> impl IntoView { let allow_active = !disabled && !hide_active; @@ -67,6 +72,7 @@ pub fn RadrootsAppUiButtonLayout( <button type="button" class=button_class + style=style disabled=disabled on:click=move |ev| { ev.stop_propagation(); @@ -104,6 +110,9 @@ pub fn RadrootsAppUiButtonLayoutPair( disabled=continue_action.disabled loading=continue_action.loading on_click=continue_action.on_click + class=continue_action.class.unwrap_or_default() + class_label=continue_action.class_label.unwrap_or_default() + style=continue_action.style.unwrap_or_default() /> {back.map(|back_action| { view! { @@ -113,15 +122,24 @@ pub fn RadrootsAppUiButtonLayoutPair( let back_disabled = back_action.disabled; let back_on_click = back_action.on_click.clone(); let back_visible = back_action.visible; + let back_compact = back_action.compact; let back_text_class = radroots_app_ui_button_class_merge(&[ Some("font-sans font-[600] tracking-wide text-ly1-gl-shade"), if back_disabled { None } else { Some("group-active:text-ly1-gl/40") }, ]); - let back_button_class = radroots_app_ui_button_class_merge(&[ - if back_disabled { None } else { Some("group") }, - Some("flex flex-row h-12 w-lo_ios0 ios1:w-lo_ios1 justify-center items-center -translate-y-[2px] transition-opacity duration-[160ms] ease-[cubic-bezier(.2,.8,.2,1)]"), - if back_visible { Some("opacity-100") } else { Some("opacity-0 pointer-events-none") }, - ]); + let back_button_class = if back_compact { + radroots_app_ui_button_class_merge(&[ + if back_disabled { None } else { Some("group") }, + Some("flex flex-row w-fit justify-center items-center py-1 transition-opacity duration-[160ms] ease-[cubic-bezier(.2,.8,.2,1)]"), + if back_visible { Some("opacity-100") } else { Some("opacity-0 pointer-events-none") }, + ]) + } else { + radroots_app_ui_button_class_merge(&[ + if back_disabled { None } else { Some("group") }, + Some("flex flex-row h-12 w-lo_ios0 ios1:w-lo_ios1 justify-center items-center -translate-y-[2px] transition-opacity duration-[160ms] ease-[cubic-bezier(.2,.8,.2,1)]"), + if back_visible { Some("opacity-100") } else { Some("opacity-0 pointer-events-none") }, + ]) + }; view! { <button type="button"