app

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

commit ef7f887bb0f7202a9e106d175a7671cf525297be
parent 39d1494d7b1e72971f21128932764427e7dced2f
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 00:14:03 +0000

ios: enable local nostr key generation

- map the iOS identity gate through the accounts manager as missing or ready
- enable the Generate New Key setup action in the iOS backend
- persist new local identities through the Apple Keychain vault adapter
- add unit tests for the initial setup state and post-generation ready transition

Diffstat:
Mcrates/ios/src/lib.rs | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
1 file changed, 56 insertions(+), 25 deletions(-)

diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -2,11 +2,11 @@ #[cfg(target_os = "ios")] use eframe::egui::ViewportBuilder; +#[cfg(any(target_os = "ios", test))] +use radroots_app_core::IdentityGateState; #[cfg(target_os = "ios")] -use radroots_app_core::{ - APP_NAME, IdentityGateState, RadrootsApp, RadrootsAppBackend, SetupActionState, -}; -#[cfg(target_os = "ios")] +use radroots_app_core::{APP_NAME, RadrootsApp, RadrootsAppBackend, SetupActionState}; +#[cfg(any(target_os = "ios", test))] use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, }; @@ -18,57 +18,64 @@ mod storage; #[cfg(any(target_os = "ios", test))] mod vault; -#[cfg(target_os = "ios")] +#[cfg(any(target_os = "ios", test))] struct IosBackend; -#[cfg(target_os = "ios")] +#[cfg(any(target_os = "ios", test))] impl IosBackend { - fn unsupported_reason() -> String { - "Secure onboarding is not yet available on iOS.".to_owned() - } - + #[cfg(target_os = "ios")] fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { storage::accounts_manager() } fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState { match status { + RadrootsNostrSelectedAccountStatus::NotConfigured => IdentityGateState::Missing, + RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing, RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready { account_id: account.account_id.to_string(), npub: account.public_identity.public_key_npub, }, - RadrootsNostrSelectedAccountStatus::NotConfigured - | RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => { - IdentityGateState::Unsupported { - reason: Self::unsupported_reason(), - } - } } } + + fn identity_state_from_manager( + manager: &RadrootsNostrAccountsManager, + ) -> Result<IdentityGateState, String> { + let status = manager + .selected_account_status() + .map_err(|source| source.to_string())?; + Ok(Self::map_status(status)) + } + + fn generate_local_identity( + manager: &RadrootsNostrAccountsManager, + ) -> Result<IdentityGateState, String> { + manager + .generate_identity(Some("local".to_owned()), true) + .map_err(|source| source.to_string())?; + Self::identity_state_from_manager(manager) + } } #[cfg(target_os = "ios")] impl RadrootsAppBackend for IosBackend { fn load_identity_state(&self) -> Result<IdentityGateState, String> { let manager = Self::accounts_manager()?; - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - Ok(Self::map_status(status)) + Self::identity_state_from_manager(&manager) } fn setup_action_state(&self) -> SetupActionState { SetupActionState { - label: "Not Yet Available".to_owned(), - enabled: false, + label: "Generate New Key".to_owned(), + enabled: true, pending: false, } } fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - Ok(Some(IdentityGateState::Unsupported { - reason: Self::unsupported_reason(), - })) + let manager = Self::accounts_manager()?; + Self::generate_local_identity(&manager).map(Some) } } @@ -111,6 +118,7 @@ pub extern "C" fn radroots_ios_run() -> i32 { #[cfg(test)] mod tests { use super::*; + use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; #[test] fn non_ios_run_is_rejected() { @@ -125,4 +133,27 @@ mod tests { fn exported_entrypoint_symbol_is_stable() { assert_eq!(ENTRYPOINT_SYMBOL, "radroots_ios_run"); } + + #[test] + fn new_ios_manager_starts_in_setup_state() { + let manager = RadrootsNostrAccountsManager::new_in_memory(); + + assert_eq!( + IosBackend::identity_state_from_manager(&manager), + Ok(IdentityGateState::Missing) + ); + } + + #[test] + fn local_identity_generation_transitions_to_ready() { + let manager = RadrootsNostrAccountsManager::new_in_memory(); + + let state = IosBackend::generate_local_identity(&manager).expect("generate identity"); + let IdentityGateState::Ready { account_id, npub } = state else { + panic!("expected ready identity state"); + }; + + assert!(!account_id.is_empty()); + assert!(npub.starts_with("npub1")); + } }