app

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

commit 7f181b47a59fd214ca52f86c4e461bd52aefa422
parent 21f4bbd6c4dad28c59eb18655b1a10c6866da8d4
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 16:50:03 +0000

ios: add device-reset action for local state

- expose `Reset This Device` from the ios backend in this slice
- remove all local identities and the ios accounts file before returning the app to setup
- add ios tests for clearing every local identity and deleting the accounts file
- keep the change scoped to the ios launcher crate with the existing shared home reset contract

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

diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -12,6 +12,8 @@ use radroots_app_core::{ use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, }; +#[cfg(any(target_os = "ios", test))] +use std::path::Path; #[cfg(any(target_os = "ios", test))] mod storage; @@ -70,6 +72,43 @@ impl IosBackend { .map_err(|source| source.to_string())?; Self::identity_state_from_manager(manager) } + + fn remove_all_local_identities( + manager: &RadrootsNostrAccountsManager, + ) -> Result<IdentityGateState, String> { + let account_ids = manager + .list_accounts() + .map_err(|source| source.to_string())? + .into_iter() + .map(|record| record.account_id) + .collect::<Vec<_>>(); + + for account_id in account_ids { + manager + .remove_account(&account_id) + .map_err(|source| source.to_string())?; + } + + Self::identity_state_from_manager(manager) + } + + fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> { + match std::fs::remove_file(accounts_path) { + Ok(()) => Ok(()), + Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(source) => Err(format!("failed to remove ios accounts file: {source}")), + } + } + + #[cfg(target_os = "ios")] + fn reset_local_device_state( + manager: &RadrootsNostrAccountsManager, + accounts_path: &Path, + ) -> Result<IdentityGateState, String> { + let state = Self::remove_all_local_identities(manager)?; + Self::remove_accounts_file_if_present(accounts_path)?; + Ok(state) + } } #[cfg(target_os = "ios")] @@ -104,6 +143,20 @@ impl RadrootsAppBackend for IosBackend { let manager = Self::accounts_manager()?; Self::remove_selected_local_identity(&manager).map(Some) } + + fn home_reset_action_state(&self) -> Option<HomeActionState> { + Some(HomeActionState { + label: "Reset This Device".to_owned(), + enabled: true, + pending: false, + }) + } + + fn request_home_reset_action(&self) -> Result<Option<IdentityGateState>, String> { + let manager = Self::accounts_manager()?; + let accounts_path = storage::accounts_path()?; + Self::reset_local_device_state(&manager, accounts_path.as_path()).map(Some) + } } #[cfg(target_os = "ios")] @@ -198,4 +251,40 @@ mod tests { None ); } + + #[test] + fn remove_all_local_identities_clears_every_account() { + let manager = RadrootsNostrAccountsManager::new_in_memory(); + + manager + .generate_identity(Some("first".into()), true) + .expect("generate first"); + manager + .generate_identity(Some("second".into()), false) + .expect("generate second"); + + let state = IosBackend::remove_all_local_identities(&manager).expect("reset state"); + + assert_eq!(state, IdentityGateState::Missing); + assert_eq!(manager.list_accounts().expect("list accounts").len(), 0); + assert_eq!(manager.selected_account_id().expect("selected"), None); + } + + #[test] + fn remove_accounts_file_if_present_deletes_existing_file() { + let unique = format!( + "radroots-ios-reset-{}-{}.json", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time") + .as_nanos() + ); + let path = std::env::temp_dir().join(unique); + std::fs::write(&path, b"{}").expect("write accounts file"); + + IosBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file"); + + assert!(!path.exists()); + } }