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:
| M | crates/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());
+ }
}