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:
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",