app

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

commit 312e6e8568777a429a6845a8efc0cc58496a9af0
parent 96ee69fa0e9c3f1b773f0fbabafa00272cd283c6
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 16:26:08 +0000

ios: add local account roster support

- surface ios local accounts through the shared roster summary contract
- allow selecting an existing ios account from the home roster
- reuse ios local key generation as the home add-account action
- add roster coverage in ios tests alongside the existing lifecycle checks

Diffstat:
Mcrates/ios/src/lib.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 65 insertions(+), 4 deletions(-)

diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -15,6 +15,8 @@ use radroots_app_core::{ RadrootsReverseLocationLookupResult, SetupActionState, }; #[cfg(any(target_os = "ios", test))] +use radroots_app_core::{RadrootsAccountCustody, RadrootsAccountSummary}; +#[cfg(any(target_os = "ios", test))] use radroots_identity::RadrootsIdentity; #[cfg(any(target_os = "ios", test))] use radroots_nostr_accounts::prelude::{ @@ -82,7 +84,6 @@ impl IosBackend { RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing, RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready { account_id: account.account_id.to_string(), - npub: account.public_identity.public_key_npub, }, } } @@ -96,6 +97,24 @@ impl IosBackend { Ok(Self::map_status(status)) } + fn account_roster_from_manager( + manager: &RadrootsNostrAccountsManager, + ) -> Result<Vec<RadrootsAccountSummary>, String> { + manager + .list_accounts() + .map_err(|source| source.to_string())? + .into_iter() + .map(|record| { + Ok(RadrootsAccountSummary { + account_id: record.account_id.to_string(), + npub: record.public_identity.public_key_npub, + label: record.label, + custody: RadrootsAccountCustody::LocalManaged, + }) + }) + .collect() + } + fn generate_local_identity( manager: &RadrootsNostrAccountsManager, ) -> Result<IdentityGateState, String> { @@ -246,6 +265,11 @@ impl RadrootsAppBackend for IosBackend { Self::identity_state_from_manager(&manager) } + fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { + let manager = Self::accounts_manager()?; + Self::account_roster_from_manager(&manager) + } + fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { Some(self.offline_geocoder.current_state()) } @@ -385,6 +409,14 @@ impl RadrootsAppBackend for IosBackend { Self::generate_local_identity(&manager).map(Some) } + fn home_setup_action_state(&self) -> Option<SetupActionState> { + Some(self.setup_action_state()) + } + + fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { + self.request_setup_action() + } + fn import_action_state(&self) -> Option<ImportActionState> { Some(ImportActionState { label: "Import Secret Key".to_owned(), @@ -398,6 +430,19 @@ impl RadrootsAppBackend for IosBackend { Self::import_local_identity(&manager, secret_key).map(Some) } + fn request_select_account( + &self, + account_id: &str, + ) -> Result<Option<IdentityGateState>, String> { + let manager = Self::accounts_manager()?; + let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id) + .map_err(|_| "invalid account id".to_owned())?; + manager + .select_account(&account_id) + .map_err(|source| source.to_string())?; + self.load_identity_state().map(Some) + } + fn import_paste_action_state(&self) -> Option<PasteActionState> { Some(PasteActionState { label: "Paste Secret Key".to_owned(), @@ -523,12 +568,11 @@ mod tests { let manager = RadrootsNostrAccountsManager::new_in_memory(); let state = IosBackend::generate_local_identity(&manager).expect("generate identity"); - let IdentityGateState::Ready { account_id, npub } = state else { + let IdentityGateState::Ready { account_id } = state else { panic!("expected ready identity state"); }; assert!(!account_id.is_empty()); - assert!(npub.starts_with("npub1")); } #[test] @@ -591,7 +635,6 @@ mod tests { state, IdentityGateState::Ready { account_id: identity.id().to_string(), - npub: identity.npub(), } ); assert_eq!( @@ -608,6 +651,24 @@ mod tests { } #[test] + fn account_roster_from_manager_lists_local_managed_account() { + let manager = RadrootsNostrAccountsManager::new_in_memory(); + let identity = RadrootsIdentity::generate(); + + manager + .upsert_identity(&identity, Some("primary".into()), true) + .expect("store identity"); + + let roster = IosBackend::account_roster_from_manager(&manager).expect("account roster"); + + assert_eq!(roster.len(), 1); + assert_eq!(roster[0].account_id, identity.id().to_string()); + assert_eq!(roster[0].npub, identity.npub()); + assert_eq!(roster[0].label.as_deref(), Some("primary")); + assert_eq!(roster[0].custody, RadrootsAccountCustody::LocalManaged); + } + + #[test] fn normalize_clipboard_secret_key_text_trims_wrapping_whitespace() { let clipboard_text = format!(" {} \n", FIXTURE_ALICE.nsec); let normalized = IosBackend::normalize_clipboard_secret_key_text(clipboard_text.as_str())