app

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

commit f51778d5757a8b7ad8ef881a4f251dd54d3cae15
parent daee3e6648b63b1e3180c2ed7150515b5c063884
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 20:20:20 +0000

android: add secret-key import for local identities

- add the setup-side secret-key import action to the android backend
- import a provided nsec through the existing accounts manager and select the local identity
- keep the android build and run lane green for the import flow
- cover the android import path with a local unit test

Diffstat:
Mcrates/android/src/lib.rs | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 74 insertions(+), 1 deletion(-)

diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs @@ -10,7 +10,8 @@ use radroots_app_core::RadrootsAppBackend; use radroots_app_core::{APP_NAME, RadrootsApp}; #[cfg(any(target_os = "android", test))] use radroots_app_core::{ - HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, SetupActionState, + HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, + SetupActionState, }; #[cfg(any(target_os = "android", test))] use radroots_identity::RadrootsIdentity; @@ -77,6 +78,36 @@ impl RadrootsAppBackend for AndroidBackend { } } + fn import_action_state(&self) -> Option<ImportActionState> { + #[cfg(target_os = "android")] + { + return Some(ImportActionState { + label: "Import Secret Key".to_owned(), + enabled: true, + pending: false, + }); + } + + #[cfg(not(target_os = "android"))] + { + None + } + } + + fn request_import_action(&self, secret_key: &str) -> Result<Option<IdentityGateState>, String> { + #[cfg(target_os = "android")] + { + let manager = Self::accounts_manager()?; + return Self::import_local_identity(&manager, secret_key).map(Some); + } + + #[cfg(not(target_os = "android"))] + { + let _ = secret_key; + Ok(None) + } + } + fn home_action_states(&self) -> Vec<HomeActionState> { #[cfg(target_os = "android")] { @@ -237,6 +268,20 @@ impl AndroidBackend { Ok(identity.nsec()) } + fn import_local_identity( + manager: &RadrootsNostrAccountsManager, + secret_key: &str, + ) -> Result<IdentityGateState, String> { + let identity = RadrootsIdentity::from_secret_key_str(secret_key) + .map_err(|_| "invalid secret key".to_owned())?; + + manager + .upsert_identity(&identity, None, true) + .map_err(|source| source.to_string())?; + + Self::identity_state_from_manager(manager) + } + #[cfg(target_os = "android")] fn begin_secret_key_export() -> Result<(), String> { security::begin_user_presence_verification("reveal the current secret key") @@ -510,6 +555,34 @@ mod tests { } #[test] + fn import_local_identity_imports_nsec_and_selects_account() { + let manager = RadrootsNostrAccountsManager::new_in_memory(); + let identity = RadrootsIdentity::generate(); + + let state = AndroidBackend::import_local_identity(&manager, identity.nsec().as_str()) + .expect("import"); + + assert_eq!( + state, + IdentityGateState::Ready { + account_id: identity.id().to_string(), + npub: identity.npub(), + } + ); + assert_eq!( + manager.selected_account_id().expect("selected"), + Some(identity.id()) + ); + assert_eq!(manager.list_accounts().expect("list").len(), 1); + assert_eq!( + manager + .export_secret_hex(&identity.id()) + .expect("export secret"), + Some(identity.secret_key_hex()) + ); + } + + #[test] fn remove_accounts_file_if_present_deletes_existing_file() { let unique = format!( "radroots-android-reset-{}-{}.json",