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:
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())