app

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

commit 201753744a978a2ca956cced626c8aef5f4df18d
parent 622aa11b1318bf8d8e9f8421fcef0cb52ca256af
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Apr 2026 02:45:38 +0000

app: add typed identity and activation projections

Diffstat:
Mcrates/launchers/desktop/src/app.rs | 4+++-
Mcrates/launchers/desktop/src/runtime.rs | 29++++++++++++++++++++---------
Mcrates/launchers/desktop/src/window.rs | 499++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/shared/i18n/src/keys.rs | 17+++++++++++++++--
Mcrates/shared/models/src/lib.rs | 318++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/shared/state/src/lib.rs | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/shared/ui/src/lib.rs | 2+-
Mcrates/shared/ui/src/text.rs | 13-------------
Mi18n/locales/en/messages.json | 17+++++++++++++++--
9 files changed, 947 insertions(+), 194 deletions(-)

diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs @@ -153,7 +153,7 @@ mod tests { APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeCapture, AppRuntimeMode, AppRuntimeSnapshot, }; - use radroots_app_models::TodayAgendaProjection; + use radroots_app_models::{AppStartupGate, SettingsAccountProjection, TodayAgendaProjection}; use radroots_app_state::AppShellProjection; use tracing::{ Event, Level, Subscriber, @@ -253,6 +253,8 @@ mod tests { }); let summary = DesktopAppRuntimeSummary { shell_projection: AppShellProjection::default(), + settings_account_projection: SettingsAccountProjection::default(), + startup_gate: AppStartupGate::SetupRequired, today_projection: TodayAgendaProjection::default(), startup_issue: Some("desktop runtime roots require HOME for macos".to_owned()), }; diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -3,8 +3,8 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use radroots_app_core::{AppRuntimePathsError, AppRuntimeRoots}; use radroots_app_models::{ - ActiveSurface, AppActivityContext, AppActivityKind, SettingsPreference, SettingsSection, - TodayAgendaProjection, + AppActivityContext, AppActivityKind, AppStartupGate, SettingsAccountProjection, + SettingsPreference, SettingsSection, TodayAgendaProjection, }; use radroots_app_sqlite::{ APP_ACTIVITY_CONTEXT_LIMIT, AppSqliteError, AppSqliteStore, DatabaseTarget, @@ -38,6 +38,8 @@ impl DesktopAppRuntime { DesktopAppRuntimeSummary { shell_projection: state.state_store.shell_projection().clone(), + settings_account_projection: state.state_store.settings_account_projection(), + startup_gate: state.state_store.startup_gate(), today_projection: state.state_store.today_projection().clone(), startup_issue: state.startup_issue.clone(), } @@ -140,6 +142,8 @@ impl DesktopAppRuntime { #[derive(Clone, Debug)] pub struct DesktopAppRuntimeSummary { pub shell_projection: AppShellProjection, + pub settings_account_projection: SettingsAccountProjection, + pub startup_gate: AppStartupGate, pub today_projection: TodayAgendaProjection, pub startup_issue: Option<String>, } @@ -183,10 +187,7 @@ impl DesktopAppRuntimeState { fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self { Self { - state_store: AppStateStore::in_memory(AppShellProjection { - active_surface: ActiveSurface::Farmer, - ..AppShellProjection::default() - }), + state_store: AppStateStore::in_memory(AppShellProjection::default()), sqlite_store: None, startup_issue: Some(error.to_string()), } @@ -216,7 +217,7 @@ mod tests { use radroots_app_core::{AppRuntimeHostEnvironment, AppRuntimePlatform, AppRuntimeRoots}; use radroots_app_models::{ - ActiveSurface, AppActivityKind, FarmReadiness, FarmSummary, SettingsPreference, + AppActivityKind, AppStartupGate, FarmReadiness, FarmSummary, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; @@ -283,6 +284,14 @@ mod tests { cloned_runtime.selected_settings_section(), SettingsSection::About ); + assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired); + assert!(summary.settings_account_projection.roster.is_empty()); + assert!( + summary + .settings_account_projection + .selected_account + .is_none() + ); } #[test] @@ -324,7 +333,7 @@ mod tests { assert_eq!(summary.today_projection, today_agenda); assert_eq!( summary.shell_projection.active_surface, - ActiveSurface::Farmer + radroots_app_models::ActiveSurface::Personal ); assert_eq!( summary.shell_projection.selected_section, @@ -349,7 +358,7 @@ mod tests { assert_eq!( summary.shell_projection.active_surface, - ActiveSurface::Farmer + radroots_app_models::ActiveSurface::Personal ); assert_eq!( summary.shell_projection.selected_section, @@ -359,6 +368,8 @@ mod tests { summary.shell_projection.settings.selected_section, SettingsSection::Account ); + assert_eq!(summary.startup_gate, AppStartupGate::SetupRequired); + assert!(summary.settings_account_projection.roster.is_empty()); assert_eq!(summary.today_projection, TodayAgendaProjection::default()); assert_eq!( summary.startup_issue.as_deref(), diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -7,8 +7,9 @@ use gpui_component::{IconName, Root}; use radroots_app_i18n::AppTextKey; pub use radroots_app_models::SettingsSection as SettingsPanelViewKey; use radroots_app_models::{ - FulfillmentWindowSummary, OrderListRow, ProductListRow, SettingsPreference, - TodayAgendaProjection, TodaySetupTaskKind, + AccountCustody, AccountSummary, ActiveSurface, AppStartupGate, FulfillmentWindowSummary, + IdentityReadiness, OrderListRow, ProductListRow, SelectedAccountProjection, + SettingsAccountProjection, SettingsPreference, TodayAgendaProjection, TodaySetupTaskKind, }; use radroots_app_ui::{ APP_UI_THEME, AppCheckboxFieldSpec, IconSegmentButtonSpec, LabelValueRow, action_button, @@ -193,7 +194,12 @@ impl SettingsWindowView { fn account_panel(&self, cx: &mut Context<Self>) -> impl IntoElement { let detail_text_px = APP_UI_THEME.typography.settings_account_detail_text_px; - let account_status_color = APP_UI_THEME.controls.status_indicator.online; + let runtime_summary = self.runtime.summary(); + let account_projection = &runtime_summary.settings_account_projection; + let selected_account = account_projection.selected_account.as_ref(); + let selected_account_id = + selected_account.map(|account| account.account.account_id.as_str()); + let account_status_color = settings_account_status_color(account_projection); div() .size_full() @@ -209,63 +215,72 @@ impl SettingsWindowView { .child( div() .w_full() - .h(px(APP_UI_THEME.layout.settings_account_sidebar_button_height_px)) - .bg(rgb(APP_UI_THEME.surfaces.chrome_background)) - .rounded(px( - APP_UI_THEME - .layout - .settings_account_sidebar_button_corner_radius_px, - )) - .p(px( - APP_UI_THEME.layout.settings_account_sidebar_button_padding_px, - )) .flex() - .flex_row() - .justify_start() - .items_center() + .flex_col() .gap(px(APP_UI_THEME.layout.settings_account_sidebar_button_gap_px)) - .child( - div() - .size(px(APP_UI_THEME.layout.settings_account_sidebar_avatar_size_px)) - .bg(rgb(APP_UI_THEME.surfaces.card_background)) - .rounded(px( - APP_UI_THEME.layout.settings_account_sidebar_avatar_size_px - / 2.0, - )), - ) - .child( - div() - .flex() - .flex_col() - .gap(px(APP_UI_THEME.layout.settings_account_identity_text_gap_px)) - .justify_center() - .child( - div() - .text_size(px( - APP_UI_THEME - .typography - .settings_account_identity_text_px, - )) - .font_weight(gpui::FontWeight::MEDIUM) - .text_color(rgb(APP_UI_THEME.text.primary)) - .child(app_shared_text( - AppTextKey::SettingsAccountPlaceholderName, - )), - ) - .child( - div() - .text_size(px( - APP_UI_THEME - .typography - .settings_account_identity_text_px, - )) - .font_weight(gpui::FontWeight::MEDIUM) - .text_color(rgb(APP_UI_THEME.text.secondary)) - .child(app_shared_text( - AppTextKey::SettingsAccountPlaceholderHandle, - )), - ), - ), + .when(account_projection.roster.is_empty(), |this| { + this.child( + div() + .w_full() + .bg(rgb(APP_UI_THEME.surfaces.chrome_background)) + .rounded(px( + APP_UI_THEME + .layout + .settings_account_sidebar_button_corner_radius_px, + )) + .p(px( + APP_UI_THEME + .layout + .settings_account_sidebar_button_padding_px, + )) + .child( + div() + .flex() + .flex_col() + .gap(px(2.0)) + .child( + div() + .text_size(px( + APP_UI_THEME + .typography + .settings_account_identity_text_px, + )) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(app_shared_text( + AppTextKey::SettingsAccountNoSelectionTitle, + )), + ) + .child( + div() + .text_size(px( + APP_UI_THEME + .typography + .settings_account_identity_text_px, + )) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .line_height(relative(1.2)) + .child(app_shared_text( + AppTextKey::SettingsAccountNoSelectionBody, + )), + ), + ), + ) + }) + .when(!account_projection.roster.is_empty(), |this| { + this.children( + account_projection + .roster + .iter() + .map(|account| { + settings_account_sidebar_row( + account, + selected_account_id, + ) + }) + .collect::<Vec<_>>(), + ) + }), ) .child( div() @@ -357,9 +372,19 @@ impl SettingsWindowView { .text_size(px(detail_text_px)) .font_weight(gpui::FontWeight::MEDIUM) .text_color(rgb(APP_UI_THEME.text.primary)) - .child(app_shared_text( - AppTextKey::SettingsAccountPlaceholderName, - )), + .child( + selected_account + .map(|account| { + settings_account_display_name( + &account.account, + ) + }) + .unwrap_or_else(|| { + app_shared_text( + AppTextKey::SettingsAccountNoSelectionTitle, + ) + }), + ), ), ) .child( @@ -368,110 +393,169 @@ impl SettingsWindowView { .flex() .flex_col() .gap(px(APP_UI_THEME.layout.settings_account_detail_row_gap_px)) - .child( + .child(self.settings_account_detail_row( + AppTextKey::SettingsAccountProfileLabel, + div() + .text_size(px(detail_text_px)) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child( + selected_account + .map(|account| account.account.npub.clone().into()) + .unwrap_or_else(|| { + app_shared_text(AppTextKey::ValueNone) + }), + ), + )) + .child(self.settings_account_detail_row( + AppTextKey::SettingsAccountStatusLabel, div() - .w_full() .flex() .items_center() .gap(px( APP_UI_THEME .layout - .settings_account_detail_value_gap_px, + .settings_account_status_gap_px, )) - .child( - div() - .text_size(px(detail_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.text.secondary)) - .child(app_shared_label_text( - AppTextKey::SettingsAccountProfileLabel, - )), - ) + .child(status_indicator(account_status_color)) .child( div() .text_size(px(detail_text_px)) .text_color(rgb(APP_UI_THEME.text.primary)) .child(app_shared_text( - AppTextKey::SettingsAccountPlaceholderHandle, + settings_account_status_key( + account_projection, + ), )), ), - ) - .child( + )) + .child(self.settings_account_detail_row( + AppTextKey::SettingsAccountCustodyLabel, div() - .w_full() - .flex() - .items_center() - .gap(px( - APP_UI_THEME - .layout - .settings_account_detail_value_gap_px, - )) - .child( - div() - .text_size(px(detail_text_px)) - .font_weight(gpui::FontWeight::SEMIBOLD) - .text_color(rgb(APP_UI_THEME.text.secondary)) - .child(app_shared_label_text( - AppTextKey::SettingsAccountStatusLabel, - )), - ) + .text_size(px(detail_text_px)) + .text_color(rgb(APP_UI_THEME.text.primary)) .child( - div() - .flex() - .items_center() - .gap(px( - APP_UI_THEME - .layout - .settings_account_status_gap_px, - )) - .child(status_indicator(account_status_color)) - .child( - div() - .text_size(px(detail_text_px)) - .text_color(rgb(APP_UI_THEME.text.primary)) - .child(app_shared_text( - AppTextKey::SettingsAccountStatusLoggedIn, - )), - ), + selected_account + .map(|account| { + app_shared_text( + settings_account_custody_key( + account.account.custody, + ), + ) + }) + .unwrap_or_else(|| { + app_shared_text(AppTextKey::ValueNone) + }), ), - ) - .child( + )) + .child(self.settings_account_detail_row( + AppTextKey::SettingsAccountSurfaceLabel, div() - .w_full() - .flex() - .min_w_0() - .items_center() - .gap(px( - APP_UI_THEME - .layout - .settings_account_action_row_gap_px, - )) + .text_size(px(detail_text_px)) + .text_color(rgb(APP_UI_THEME.text.primary)) .child( - div().child(action_button( - "account-log-out", - app_shared_text( - AppTextKey::SettingsAccountLogOutAction, - ), - |_, _, _| {}, - cx, - )), - ) + selected_account + .map(|account| { + app_shared_text( + settings_account_surface_key( + account.active_surface(), + ), + ) + }) + .unwrap_or_else(|| { + app_shared_text(AppTextKey::ValueNone) + }), + ), + )) + .child(self.settings_account_detail_row( + AppTextKey::SettingsAccountActivationLabel, + div() + .text_size(px(detail_text_px)) + .text_color(rgb(APP_UI_THEME.text.primary)) .child( - div().child(action_button( - "account-admin-console", - app_shared_text( - AppTextKey::SettingsAccountAdminConsoleAction, - ), - |_, _, _| {}, - cx, - )), + selected_account + .map(|account| { + app_shared_text( + settings_account_activation_key( + account, + ), + ) + }) + .unwrap_or_else(|| { + app_shared_text(AppTextKey::ValueNone) + }), ), - ), + )) + .when(selected_account.is_none(), |this| { + this.child( + div() + .w_full() + .text_size(px(detail_text_px)) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_text( + AppTextKey::SettingsAccountNoSelectionBody, + )), + ) + }) + .when(selected_account.is_some(), |this| { + this.child( + div() + .w_full() + .flex() + .min_w_0() + .items_center() + .gap(px( + APP_UI_THEME + .layout + .settings_account_action_row_gap_px, + )) + .child( + div().child(action_button( + "account-log-out", + app_shared_text( + AppTextKey::SettingsAccountLogOutAction, + ), + |_, _, _| {}, + cx, + )), + ) + .child( + div().child(action_button( + "account-admin-console", + app_shared_text( + AppTextKey::SettingsAccountAdminConsoleAction, + ), + |_, _, _| {}, + cx, + )), + ), + ) + }), ), ), ) } + fn settings_account_detail_row( + &self, + label_key: AppTextKey, + value: impl IntoElement, + ) -> impl IntoElement { + div() + .w_full() + .flex() + .items_center() + .gap(px(APP_UI_THEME.layout.settings_account_detail_value_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.settings_account_detail_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_label_text(label_key)), + ) + .child(value) + } + fn settings_checkbox_row( &mut self, id: &'static str, @@ -725,6 +809,124 @@ impl Render for SettingsWindowView { } } +fn settings_account_display_name(account: &AccountSummary) -> SharedString { + match account.label.as_deref() { + Some(label) if !label.trim().is_empty() => label.to_owned().into(), + _ => app_shared_text(settings_account_custody_key(account.custody)), + } +} + +fn settings_account_status_color(account_projection: &SettingsAccountProjection) -> u32 { + match account_projection.readiness { + IdentityReadiness::Ready => APP_UI_THEME.controls.status_indicator.online, + IdentityReadiness::MissingAccount => APP_UI_THEME.controls.status_indicator.offline, + IdentityReadiness::Blocked(_) => APP_UI_THEME.controls.status_indicator.attention, + } +} + +fn settings_account_status_key(account_projection: &SettingsAccountProjection) -> AppTextKey { + match account_projection.readiness { + IdentityReadiness::Ready => AppTextKey::SettingsAccountStatusLoggedIn, + IdentityReadiness::MissingAccount => AppTextKey::SettingsAccountStatusLoggedOut, + IdentityReadiness::Blocked(_) => AppTextKey::SettingsAccountStatusBlocked, + } +} + +fn settings_account_custody_key(custody: AccountCustody) -> AppTextKey { + match custody { + AccountCustody::LocalManaged => AppTextKey::SettingsAccountCustodyLocalManaged, + AccountCustody::BrowserSigner => AppTextKey::SettingsAccountCustodyBrowserSigner, + AccountCustody::RemoteSigner => AppTextKey::SettingsAccountCustodyRemoteSigner, + } +} + +fn settings_account_surface_key(surface: ActiveSurface) -> AppTextKey { + match surface { + ActiveSurface::Personal => AppTextKey::SettingsAccountSurfacePersonal, + ActiveSurface::Farmer => AppTextKey::SettingsAccountSurfaceFarmer, + } +} + +fn settings_account_activation_key(account: &SelectedAccountProjection) -> AppTextKey { + if account.farmer_activation.is_active() { + AppTextKey::SettingsAccountActivationActive + } else { + AppTextKey::SettingsAccountActivationInactive + } +} + +fn settings_account_sidebar_row( + account: &AccountSummary, + selected_account_id: Option<&str>, +) -> AnyElement { + let is_selected = selected_account_id + .map(|account_id| account_id == account.account_id.as_str()) + .unwrap_or(false); + + div() + .w_full() + .h(px(APP_UI_THEME + .layout + .settings_account_sidebar_button_height_px)) + .bg(rgb(if is_selected { + APP_UI_THEME.surfaces.card_background + } else { + APP_UI_THEME.surfaces.chrome_background + })) + .rounded(px(APP_UI_THEME + .layout + .settings_account_sidebar_button_corner_radius_px)) + .p(px(APP_UI_THEME + .layout + .settings_account_sidebar_button_padding_px)) + .flex() + .flex_row() + .justify_start() + .items_center() + .gap(px(APP_UI_THEME + .layout + .settings_account_sidebar_button_gap_px)) + .child( + div() + .size(px(APP_UI_THEME + .layout + .settings_account_sidebar_avatar_size_px)) + .bg(rgb(APP_UI_THEME.surfaces.window_background)) + .rounded(px(APP_UI_THEME + .layout + .settings_account_sidebar_avatar_size_px + / 2.0)), + ) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap(px(APP_UI_THEME + .layout + .settings_account_identity_text_gap_px)) + .justify_center() + .child( + div() + .text_size(px(APP_UI_THEME + .typography + .settings_account_identity_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(settings_account_display_name(account)), + ) + .child( + div() + .text_size(px(APP_UI_THEME + .typography + .settings_account_identity_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(account.npub.clone()), + ), + ) + .into_any_element() +} + fn settings_panel_label_key(view: SettingsPanelViewKey) -> AppTextKey { match view { SettingsPanelViewKey::Account => AppTextKey::SettingsNavAccounts, @@ -816,7 +1018,15 @@ fn home_view_content(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { ); } - if runtime.startup_issue.is_none() && projection.farm.is_none() { + if runtime.startup_issue.is_none() && runtime.startup_gate == AppStartupGate::SetupRequired { + sections.push( + home_empty_state_card( + AppTextKey::HomeTodayEmptySetupTitle, + AppTextKey::HomeTodayEmptySetupBody, + ) + .into_any_element(), + ); + } else if runtime.startup_issue.is_none() && projection.farm.is_none() { sections.push( home_empty_state_card( AppTextKey::HomeTodayEmptyNoFarmTitle, @@ -1159,13 +1369,20 @@ fn home_empty_state_card(title_key: AppTextKey, body_key: AppTextKey) -> impl In } fn home_status_presentation(runtime: &DesktopAppRuntimeSummary) -> HomeStatusPresentation { - if runtime.startup_issue.is_some() { + if runtime.startup_issue.is_some() || runtime.startup_gate == AppStartupGate::Blocked { return HomeStatusPresentation { indicator_color: APP_UI_THEME.controls.status_indicator.attention, label_key: AppTextKey::HomeTodayStatusStartupIssue, }; } + if runtime.startup_gate == AppStartupGate::SetupRequired { + return HomeStatusPresentation { + indicator_color: APP_UI_THEME.controls.status_indicator.offline, + label_key: AppTextKey::HomeTodayStatusSetup, + }; + } + if runtime.today_projection.farm.is_none() { return HomeStatusPresentation { indicator_color: APP_UI_THEME.controls.status_indicator.offline, diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -38,6 +38,8 @@ define_app_text_keys! { HomeTodayStockCountLabel => "home.today.stock_count.label", HomeTodaySetupAddFulfillmentWindow => "home.today.setup.add_fulfillment_window", HomeTodaySetupPublishProduct => "home.today.setup.publish_product", + HomeTodayEmptySetupTitle => "home.today.empty.setup.title", + HomeTodayEmptySetupBody => "home.today.empty.setup.body", HomeTodayEmptyNoFarmTitle => "home.today.empty.no_farm.title", HomeTodayEmptyNoFarmBody => "home.today.empty.no_farm.body", HomeTodayEmptyQuietTitle => "home.today.empty.quiet.title", @@ -49,12 +51,23 @@ define_app_text_keys! { SettingsNavAccounts => "settings.nav.accounts", SettingsNavSettings => "settings.nav.settings", SettingsNavAbout => "settings.nav.about", + SettingsAccountNoSelectionTitle => "settings.account.no_selection.title", + SettingsAccountNoSelectionBody => "settings.account.no_selection.body", SettingsAccountProfileLabel => "settings.account.profile.label", SettingsAccountStatusLabel => "settings.account.status.label", SettingsAccountStatusLoggedIn => "settings.account.status.logged_in", SettingsAccountStatusLoggedOut => "settings.account.status.logged_out", - SettingsAccountPlaceholderName => "settings.account.placeholder_name", - SettingsAccountPlaceholderHandle => "settings.account.placeholder_handle", + SettingsAccountStatusBlocked => "settings.account.status.blocked", + SettingsAccountCustodyLabel => "settings.account.custody.label", + SettingsAccountCustodyLocalManaged => "settings.account.custody.local_managed", + SettingsAccountCustodyBrowserSigner => "settings.account.custody.browser_signer", + SettingsAccountCustodyRemoteSigner => "settings.account.custody.remote_signer", + SettingsAccountSurfaceLabel => "settings.account.surface.label", + SettingsAccountSurfacePersonal => "settings.account.surface.personal", + SettingsAccountSurfaceFarmer => "settings.account.surface.farmer", + SettingsAccountActivationLabel => "settings.account.activation.label", + SettingsAccountActivationInactive => "settings.account.activation.inactive", + SettingsAccountActivationActive => "settings.account.activation.active", SettingsAccountAddAction => "settings.account.action.add_account", SettingsAccountLogOutAction => "settings.account.action.log_out", SettingsAccountAdminConsoleAction => "settings.account.action.admin_console", diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs @@ -208,6 +208,228 @@ typed_id!(ActivityEventId); #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] +pub enum AccountCustody { + LocalManaged, + BrowserSigner, + RemoteSigner, +} + +impl AccountCustody { + pub const fn storage_key(self) -> &'static str { + match self { + Self::LocalManaged => "local_managed", + Self::BrowserSigner => "browser_signer", + Self::RemoteSigner => "remote_signer", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IdentityBlockedReason { + RuntimeUnavailable, + HostVaultUnavailable, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status", content = "reason", rename_all = "snake_case")] +pub enum IdentityReadiness { + #[default] + MissingAccount, + Ready, + Blocked(IdentityBlockedReason), +} + +impl IdentityReadiness { + pub const fn storage_key(self) -> &'static str { + match self { + Self::MissingAccount => "missing_account", + Self::Ready => "ready", + Self::Blocked(IdentityBlockedReason::RuntimeUnavailable) => "runtime_unavailable", + Self::Blocked(IdentityBlockedReason::HostVaultUnavailable) => "host_vault_unavailable", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct SelectedSurfaceProjection { + pub active_surface: ActiveSurface, +} + +impl Default for SelectedSurfaceProjection { + fn default() -> Self { + Self::new(ActiveSurface::Personal) + } +} + +impl SelectedSurfaceProjection { + pub const fn new(active_surface: ActiveSurface) -> Self { + Self { active_surface } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct FarmerActivationProjection { + pub farm_id: Option<FarmId>, +} + +impl FarmerActivationProjection { + pub const fn inactive() -> Self { + Self { farm_id: None } + } + + pub fn active(farm_id: FarmId) -> Self { + Self { + farm_id: Some(farm_id), + } + } + + pub const fn is_active(&self) -> bool { + self.farm_id.is_some() + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct AccountSummary { + pub account_id: String, + pub npub: String, + pub label: Option<String>, + pub custody: AccountCustody, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct SelectedAccountProjection { + pub account: AccountSummary, + pub selected_surface: SelectedSurfaceProjection, + pub farmer_activation: FarmerActivationProjection, +} + +impl SelectedAccountProjection { + pub fn new( + account: AccountSummary, + selected_surface: SelectedSurfaceProjection, + farmer_activation: FarmerActivationProjection, + ) -> Self { + let active_surface = if farmer_activation.is_active() { + selected_surface.active_surface + } else { + ActiveSurface::Personal + }; + + Self { + account, + selected_surface: SelectedSurfaceProjection::new(active_surface), + farmer_activation, + } + } + + pub const fn active_surface(&self) -> ActiveSurface { + self.selected_surface.active_surface + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AppStartupGate { + Blocked, + #[default] + SetupRequired, + Personal, + Farmer, +} + +impl AppStartupGate { + pub const fn storage_key(self) -> &'static str { + match self { + Self::Blocked => "blocked", + Self::SetupRequired => "setup_required", + Self::Personal => "personal", + Self::Farmer => "farmer", + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct AppIdentityProjection { + pub readiness: IdentityReadiness, + pub roster: Vec<AccountSummary>, + pub selected_account: Option<SelectedAccountProjection>, +} + +impl AppIdentityProjection { + pub fn missing() -> Self { + Self::default() + } + + pub fn blocked(reason: IdentityBlockedReason) -> Self { + Self { + readiness: IdentityReadiness::Blocked(reason), + ..Self::default() + } + } + + pub fn ready( + mut roster: Vec<AccountSummary>, + selected_account: SelectedAccountProjection, + ) -> Self { + if !roster + .iter() + .any(|account| account.account_id == selected_account.account.account_id) + { + roster.insert(0, selected_account.account.clone()); + } + + Self { + readiness: IdentityReadiness::Ready, + roster, + selected_account: Some(selected_account), + } + } + + pub fn startup_gate(&self) -> AppStartupGate { + match self.readiness { + IdentityReadiness::MissingAccount => AppStartupGate::SetupRequired, + IdentityReadiness::Blocked(_) => AppStartupGate::Blocked, + IdentityReadiness::Ready => self + .selected_account + .as_ref() + .map(|account| { + if account.farmer_activation.is_active() + && account.active_surface() == ActiveSurface::Farmer + { + AppStartupGate::Farmer + } else { + AppStartupGate::Personal + } + }) + .unwrap_or(AppStartupGate::SetupRequired), + } + } + + pub fn settings_account(&self) -> SettingsAccountProjection { + self.into() + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct SettingsAccountProjection { + pub readiness: IdentityReadiness, + pub roster: Vec<AccountSummary>, + pub selected_account: Option<SelectedAccountProjection>, +} + +impl From<&AppIdentityProjection> for SettingsAccountProjection { + fn from(value: &AppIdentityProjection) -> Self { + Self { + readiness: value.readiness, + roster: value.roster.clone(), + selected_account: value.selected_account.clone(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum FarmReadiness { Incomplete, Ready, @@ -365,9 +587,12 @@ impl TodayAgendaProjection { #[cfg(test)] mod tests { use super::{ - ActiveSurface, ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, - FarmId, FarmerSection, OrderListRow, ProductListRow, SettingsPreference, SettingsSection, - ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, + AccountCustody, AccountSummary, ActiveSurface, ActivityEventId, AppActivityContext, + AppActivityEvent, AppActivityKind, AppIdentityProjection, AppStartupGate, FarmId, + FarmerActivationProjection, FarmerSection, IdentityBlockedReason, OrderListRow, + ProductListRow, SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, + SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + TodaySummary, }; use std::{collections::BTreeSet, str::FromStr}; use uuid::Uuid; @@ -425,6 +650,93 @@ mod tests { } #[test] + fn selected_surface_defaults_to_personal() { + assert_eq!( + SelectedSurfaceProjection::default().active_surface, + ActiveSurface::Personal + ); + } + + #[test] + fn selected_account_without_farmer_activation_falls_back_to_personal_surface() { + let projection = SelectedAccountProjection::new( + AccountSummary { + account_id: "acct_01".to_owned(), + npub: "npub1example".to_owned(), + label: Some("North field".to_owned()), + custody: AccountCustody::LocalManaged, + }, + SelectedSurfaceProjection::new(ActiveSurface::Farmer), + FarmerActivationProjection::inactive(), + ); + + assert_eq!(projection.active_surface(), ActiveSurface::Personal); + assert!(!projection.farmer_activation.is_active()); + } + + #[test] + fn startup_gate_tracks_setup_personal_farmer_and_blocked_states() { + let farmer_identity = AppIdentityProjection::ready( + Vec::new(), + SelectedAccountProjection::new( + AccountSummary { + account_id: "acct_02".to_owned(), + npub: "npub1farmer".to_owned(), + label: None, + custody: AccountCustody::LocalManaged, + }, + SelectedSurfaceProjection::new(ActiveSurface::Farmer), + FarmerActivationProjection::active(FarmId::new()), + ), + ); + let personal_identity = AppIdentityProjection::ready( + Vec::new(), + SelectedAccountProjection::new( + AccountSummary { + account_id: "acct_03".to_owned(), + npub: "npub1personal".to_owned(), + label: None, + custody: AccountCustody::LocalManaged, + }, + SelectedSurfaceProjection::new(ActiveSurface::Personal), + FarmerActivationProjection::inactive(), + ), + ); + + assert_eq!( + AppIdentityProjection::missing().startup_gate(), + AppStartupGate::SetupRequired + ); + assert_eq!(personal_identity.startup_gate(), AppStartupGate::Personal); + assert_eq!(farmer_identity.startup_gate(), AppStartupGate::Farmer); + assert_eq!( + AppIdentityProjection::blocked(IdentityBlockedReason::HostVaultUnavailable) + .startup_gate(), + AppStartupGate::Blocked + ); + } + + #[test] + fn ready_identity_keeps_selected_account_visible_in_roster() { + let selected_account = SelectedAccountProjection::new( + AccountSummary { + account_id: "acct_selected".to_owned(), + npub: "npub1selected".to_owned(), + label: None, + custody: AccountCustody::RemoteSigner, + }, + SelectedSurfaceProjection::new(ActiveSurface::Personal), + FarmerActivationProjection::inactive(), + ); + let projection = AppIdentityProjection::ready(Vec::new(), selected_account.clone()); + + assert_eq!(projection.readiness.storage_key(), "ready"); + assert_eq!(projection.roster.len(), 1); + assert_eq!(projection.roster[0], selected_account.account); + assert_eq!(projection.selected_account, Some(selected_account)); + } + + #[test] fn typed_ids_round_trip_through_strings() { let uuid = Uuid::parse_str("018f4d61-19b0-7cc4-9d4e-6d0df7c0aa11") .expect("test uuid should parse"); diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs @@ -1,7 +1,9 @@ #![forbid(unsafe_code)] use radroots_app_models::{ - ActiveSurface, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, + ActiveSurface, AppIdentityProjection, AppStartupGate, SelectedSurfaceProjection, + SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection, + TodayAgendaProjection, }; use thiserror::Error; @@ -73,7 +75,7 @@ pub struct AppShellProjection { impl Default for AppShellProjection { fn default() -> Self { - Self::new(ActiveSurface::Farmer, ShellSection::Home) + Self::new(ActiveSurface::Personal, ShellSection::Home) } } @@ -92,7 +94,10 @@ impl AppShellProjection { } pub fn for_surface(active_surface: ActiveSurface) -> Self { - Self::new(active_surface, ShellSection::default_for_surface(active_surface)) + Self::new( + active_surface, + ShellSection::default_for_surface(active_surface), + ) } pub fn for_settings(active_surface: ActiveSurface, selected_section: SettingsSection) -> Self { @@ -139,15 +144,39 @@ impl AppShellProjection { } } -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct AppProjection { pub shell: AppShellProjection, + pub identity: AppIdentityProjection, + pub startup_gate: AppStartupGate, pub today: TodayAgendaProjection, } impl AppProjection { - pub fn new(shell: AppShellProjection, today: TodayAgendaProjection) -> Self { - Self { shell, today } + pub fn new( + mut shell: AppShellProjection, + identity: AppIdentityProjection, + today: TodayAgendaProjection, + ) -> Self { + sync_shell_to_identity(&mut shell, &identity); + let startup_gate = identity.startup_gate(); + + Self { + shell, + identity, + startup_gate, + today, + } + } +} + +impl Default for AppProjection { + fn default() -> Self { + Self::new( + AppShellProjection::default(), + AppIdentityProjection::default(), + TodayAgendaProjection::default(), + ) } } @@ -156,6 +185,7 @@ pub enum AppStateCommand { SelectActiveSurface(ActiveSurface), SelectSection(ShellSection), SelectSettingsSection(SettingsSection), + ReplaceIdentityProjection(AppIdentityProjection), SetSettingsPreference { preference: SettingsPreference, enabled: bool, @@ -172,6 +202,10 @@ impl AppStateCommand { Self::SelectSettingsSection(section) } + pub fn replace_identity_projection(projection: AppIdentityProjection) -> Self { + Self::ReplaceIdentityProjection(projection) + } + pub fn replace_today_agenda(projection: TodayAgendaProjection) -> Self { Self::ReplaceTodayAgenda(projection) } @@ -263,6 +297,7 @@ impl<R: AppStateRepository> AppStateStore<R> { pub fn load(repository: R) -> Result<Self, AppStateStoreError> { let projection = AppProjection::new( repository.load_shell_projection()?, + AppIdentityProjection::default(), TodayAgendaProjection::default(), ); @@ -284,6 +319,18 @@ impl<R: AppStateRepository> AppStateStore<R> { &self.projection.today } + pub fn identity_projection(&self) -> &AppIdentityProjection { + &self.projection.identity + } + + pub fn settings_account_projection(&self) -> SettingsAccountProjection { + self.projection.identity.settings_account() + } + + pub fn startup_gate(&self) -> AppStartupGate { + self.projection.startup_gate + } + pub fn repository(&self) -> &R { &self.repository } @@ -313,7 +360,11 @@ impl AppStateStore<InMemoryAppStateRepository> { pub fn in_memory(projection: AppShellProjection) -> Self { Self { repository: InMemoryAppStateRepository::new(projection.clone()), - projection: AppProjection::new(projection, TodayAgendaProjection::default()), + projection: AppProjection::new( + projection, + AppIdentityProjection::default(), + TodayAgendaProjection::default(), + ), } } @@ -350,6 +401,15 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap match command { AppStateCommand::SelectActiveSurface(active_surface) => { projection.shell.select_active_surface(active_surface); + if let Some(selected_account) = projection.identity.selected_account.as_mut() { + let selected_surface = if selected_account.farmer_activation.is_active() { + active_surface + } else { + ActiveSurface::Personal + }; + selected_account.selected_surface = + SelectedSurfaceProjection::new(selected_surface); + } } AppStateCommand::SelectSection(selected_section) => { projection.shell.select_section(selected_section); @@ -357,6 +417,9 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap AppStateCommand::SelectSettingsSection(selected_section) => { projection.shell.select_settings_section(selected_section); } + AppStateCommand::ReplaceIdentityProjection(identity_projection) => { + projection.identity = identity_projection; + } AppStateCommand::SetSettingsPreference { preference, enabled, @@ -372,6 +435,8 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap } } + sync_projection(projection); + if *projection == before { AppStateMutation::NoChange } else if projection.shell != before.shell { @@ -381,6 +446,28 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap } } +fn sync_projection(projection: &mut AppProjection) { + sync_shell_to_identity(&mut projection.shell, &projection.identity); + projection.startup_gate = projection.identity.startup_gate(); +} + +fn sync_shell_to_identity(shell: &mut AppShellProjection, identity: &AppIdentityProjection) { + match identity.startup_gate() { + AppStartupGate::Blocked | AppStartupGate::SetupRequired | AppStartupGate::Personal => { + shell.active_surface = ActiveSurface::Personal; + if matches!(shell.selected_section, ShellSection::Farmer(_)) { + shell.selected_section = ShellSection::Home; + } + } + AppStartupGate::Farmer => { + shell.active_surface = ActiveSurface::Farmer; + if matches!(shell.selected_section, ShellSection::Home) { + shell.selected_section = ShellSection::default_for_surface(ActiveSurface::Farmer); + } + } + } +} + #[cfg(test)] mod tests { use super::{ @@ -389,7 +476,9 @@ mod tests { SettingsPreference, }; use radroots_app_models::{ - ActiveSurface, FarmerSection, SettingsSection, ShellSection, TodayAgendaProjection, + AccountCustody, AccountSummary, ActiveSurface, AppIdentityProjection, AppStartupGate, + FarmId, FarmerActivationProjection, FarmerSection, SelectedAccountProjection, + SelectedSurfaceProjection, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, }; @@ -408,12 +497,30 @@ mod tests { } } + fn ready_identity(surface: ActiveSurface) -> AppIdentityProjection { + AppIdentityProjection::ready( + Vec::new(), + SelectedAccountProjection::new( + AccountSummary { + account_id: "acct_surface".to_owned(), + npub: "npub1surface".to_owned(), + label: Some("North field".to_owned()), + custody: AccountCustody::LocalManaged, + }, + SelectedSurfaceProjection::new(surface), + FarmerActivationProjection::active(FarmId::new()), + ), + ) + } + #[test] - fn default_projection_starts_on_farmer_home() { + fn default_projection_starts_on_personal_setup_gate() { let projection = AppProjection::default(); - assert_eq!(projection.shell.active_surface, ActiveSurface::Farmer); + assert_eq!(projection.shell.active_surface, ActiveSurface::Personal); assert_eq!(projection.shell.selected_section, ShellSection::Home); + assert_eq!(projection.identity, AppIdentityProjection::default()); + assert_eq!(projection.startup_gate, AppStartupGate::SetupRequired); assert_eq!( projection.shell.settings.selected_section, SettingsSection::Account @@ -433,7 +540,10 @@ mod tests { )); let store = AppStateStore::load(repository).expect("in-memory repository should load"); - assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + assert_eq!( + store.projection().shell.active_surface, + ActiveSurface::Personal + ); assert_eq!( store.projection().shell.selected_section, ShellSection::Settings(SettingsSection::About) @@ -442,6 +552,7 @@ mod tests { store.projection().shell.settings.selected_section, SettingsSection::About ); + assert_eq!(store.startup_gate(), AppStartupGate::SetupRequired); assert_eq!(store.projection().today, TodayAgendaProjection::default()); } @@ -455,7 +566,10 @@ mod tests { )); assert_eq!(changed, Ok(true)); - assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + assert_eq!( + store.projection().shell.active_surface, + ActiveSurface::Personal + ); assert_eq!( store.projection().shell.selected_section, ShellSection::Home @@ -475,7 +589,7 @@ mod tests { } #[test] - fn select_section_still_updates_the_root_shell() { + fn select_farmer_section_without_identity_gate_is_rejected() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); @@ -483,16 +597,36 @@ mod tests { FarmerSection::Products, ))); - assert_eq!(changed, Ok(true)); + assert_eq!(changed, Ok(false)); assert_eq!( store.projection().shell.selected_section, - ShellSection::Farmer(FarmerSection::Products) + ShellSection::Home ); assert_eq!( - store.repository().projection().selected_section, - ShellSection::Farmer(FarmerSection::Products) + store.projection().shell.active_surface, + ActiveSurface::Personal + ); + } + + #[test] + fn replacing_identity_projection_with_farmer_activation_moves_home_to_farmer_today() { + let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) + .expect("in-memory repository should load"); + + let changed = store.apply(AppStateCommand::replace_identity_projection( + ready_identity(ActiveSurface::Farmer), + )); + + assert_eq!(changed, Ok(true)); + assert_eq!(store.startup_gate(), AppStartupGate::Farmer); + assert_eq!( + store.projection().shell.active_surface, + ActiveSurface::Farmer + ); + assert_eq!( + store.projection().shell.selected_section, + ShellSection::Farmer(FarmerSection::Today) ); - assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); } #[test] @@ -501,17 +635,35 @@ mod tests { ActiveSurface::Personal, )); let mut store = AppStateStore::load(repository).expect("in-memory repository should load"); + assert_eq!( + store.apply(AppStateCommand::replace_identity_projection( + ready_identity(ActiveSurface::Personal,) + )), + Ok(true) + ); let changed = store.apply(AppStateCommand::select_active_surface( ActiveSurface::Farmer, )); assert_eq!(changed, Ok(true)); - assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + assert_eq!( + store.projection().shell.active_surface, + ActiveSurface::Farmer + ); assert_eq!( store.projection().shell.selected_section, ShellSection::Farmer(FarmerSection::Today) ); + assert_eq!( + store + .identity_projection() + .selected_account + .as_ref() + .expect("selected account") + .active_surface(), + ActiveSurface::Farmer + ); } #[test] @@ -521,14 +673,27 @@ mod tests { ShellSection::Farmer(FarmerSection::Products), )); let mut store = AppStateStore::load(repository).expect("in-memory repository should load"); + assert_eq!( + store.apply(AppStateCommand::replace_identity_projection( + ready_identity(ActiveSurface::Farmer,) + )), + Ok(true) + ); let changed = store.apply(AppStateCommand::select_active_surface( ActiveSurface::Personal, )); assert_eq!(changed, Ok(true)); - assert_eq!(store.projection().shell.active_surface, ActiveSurface::Personal); - assert_eq!(store.projection().shell.selected_section, ShellSection::Home); + assert_eq!( + store.projection().shell.active_surface, + ActiveSurface::Personal + ); + assert_eq!( + store.projection().shell.selected_section, + ShellSection::Home + ); + assert_eq!(store.startup_gate(), AppStartupGate::Personal); } #[test] @@ -538,13 +703,22 @@ mod tests { SettingsSection::About, )); let mut store = AppStateStore::load(repository).expect("in-memory repository should load"); + assert_eq!( + store.apply(AppStateCommand::replace_identity_projection( + ready_identity(ActiveSurface::Personal,) + )), + Ok(true) + ); let changed = store.apply(AppStateCommand::select_active_surface( ActiveSurface::Farmer, )); assert_eq!(changed, Ok(true)); - assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + assert_eq!( + store.projection().shell.active_surface, + ActiveSurface::Farmer + ); assert_eq!( store.projection().shell.selected_section, ShellSection::Settings(SettingsSection::About) @@ -623,6 +797,30 @@ mod tests { } #[test] + fn replacing_identity_projection_surfaces_settings_account_state_without_touching_repository() { + let mut store = + AppStateStore::load(FailingRepository).expect("failing repository should still load"); + + let changed = store.apply(AppStateCommand::replace_identity_projection( + ready_identity(ActiveSurface::Personal), + )); + + assert_eq!(changed, Ok(true)); + assert_eq!(store.startup_gate(), AppStartupGate::Personal); + assert_eq!(store.settings_account_projection().roster.len(), 1); + assert_eq!( + store + .settings_account_projection() + .selected_account + .as_ref() + .expect("selected account") + .account + .account_id, + "acct_surface" + ); + } + + #[test] fn in_memory_store_construction_and_updates_are_infallible() { let mut store = AppStateStore::in_memory(AppShellProjection::for_settings( ActiveSurface::Farmer, diff --git a/crates/shared/ui/src/lib.rs b/crates/shared/ui/src/lib.rs @@ -12,7 +12,7 @@ pub use primitives::{ }; pub use text::{ app_shared_label_text, app_shared_text, runtime_metadata_rows, settings_about_status_rows, - settings_account_profile_rows, settings_preferences_general_rows, + settings_preferences_general_rows, }; pub use theme::{ APP_UI_THEME, ActionButtonColors, ActionButtonSizing, ActionButtonTokens, AppControlTokens, diff --git a/crates/shared/ui/src/text.rs b/crates/shared/ui/src/text.rs @@ -91,19 +91,6 @@ pub fn runtime_metadata_rows(snapshot: &AppRuntimeSnapshot) -> Vec<LabelValueRow ] } -pub fn settings_account_profile_rows() -> Vec<LabelValueRow> { - vec![ - text_row( - AppTextKey::SettingsAccountProfileLabel, - AppTextKey::SettingsAccountPlaceholderHandle, - ), - text_row( - AppTextKey::SettingsAccountStatusLabel, - AppTextKey::SettingsAccountStatusLoggedIn, - ), - ] -} - pub fn settings_preferences_general_rows() -> Vec<LabelValueRow> { vec![ text_row( diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -17,6 +17,8 @@ "home.today.stock_count.label": "Stock", "home.today.setup.add_fulfillment_window": "Add a fulfillment window", "home.today.setup.publish_product": "Publish a product", + "home.today.empty.setup.title": "Account setup required", + "home.today.empty.setup.body": "Add or import a local account to start using Radroots on this device.", "home.today.empty.no_farm.title": "No farm yet", "home.today.empty.no_farm.body": "Create a farm to start using the farmer workspace.", "home.today.empty.quiet.title": "Nothing urgent right now", @@ -28,12 +30,23 @@ "settings.nav.accounts": "Accounts", "settings.nav.settings": "Settings", "settings.nav.about": "About", + "settings.account.no_selection.title": "No account selected", + "settings.account.no_selection.body": "Add or import a local account to start using Radroots on this device.", "settings.account.profile.label": "Profile", "settings.account.status.label": "Status", "settings.account.status.logged_in": "Logged In", "settings.account.status.logged_out": "Logged Out", - "settings.account.placeholder_name": "Test User", - "settings.account.placeholder_handle": "test.user", + "settings.account.status.blocked": "Blocked", + "settings.account.custody.label": "Custody", + "settings.account.custody.local_managed": "Local managed", + "settings.account.custody.browser_signer": "Browser signer", + "settings.account.custody.remote_signer": "Remote signer", + "settings.account.surface.label": "Surface", + "settings.account.surface.personal": "Personal", + "settings.account.surface.farmer": "Farmer", + "settings.account.activation.label": "Farmer", + "settings.account.activation.inactive": "Not activated", + "settings.account.activation.active": "Activated", "settings.account.action.add_account": "Add Account...", "settings.account.action.log_out": "Log Out", "settings.account.action.admin_console": "Admin Console...",