app

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

commit 0cb6b1bb232f037313544d0807d3a4eb737a461f
parent 312e6e8568777a429a6845a8efc0cc58496a9af0
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 16:26:21 +0000

android: add local account roster support

- surface android local accounts through the shared roster summary contract
- allow selecting an existing android account from the home roster
- reuse android local key generation as the home add-account action
- keep android secret key backup remove and reset actions bound to the selected account

Diffstat:
Mcrates/android/src/lib.rs | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 67 insertions(+), 9 deletions(-)

diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs @@ -9,10 +9,11 @@ use radroots_app_core::{APP_NAME, RadrootsApp}; #[cfg(any(target_os = "android", test))] use radroots_app_core::{ HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, - RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult, - RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsLocationResolverError, - RadrootsLocationReverseOptions, RadrootsOfflineGeocoderState, RadrootsResolvedLocation, - RadrootsReverseLocationLookupResult, SetupActionState, + RadrootsAccountCustody, RadrootsAccountSummary, RadrootsLocationCountry, + RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, + RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, + RadrootsOfflineGeocoderState, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, + SetupActionState, }; #[cfg(any(target_os = "android", test))] use radroots_identity::RadrootsIdentity; @@ -65,6 +66,19 @@ impl RadrootsAppBackend for AndroidBackend { } } + fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { + #[cfg(target_os = "android")] + { + let manager = Self::accounts_manager()?; + return Self::account_roster_from_manager(&manager); + } + + #[cfg(not(target_os = "android"))] + { + Ok(Vec::new()) + } + } + fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { Some(self.offline_geocoder.current_state()) } @@ -223,6 +237,14 @@ impl RadrootsAppBackend for AndroidBackend { } } + fn home_setup_action_state(&self) -> Option<SetupActionState> { + Some(self.setup_action_state()) + } + + fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { + self.request_setup_action() + } + fn import_action_state(&self) -> Option<ImportActionState> { #[cfg(target_os = "android")] { @@ -253,6 +275,28 @@ impl RadrootsAppBackend for AndroidBackend { } } + fn request_select_account( + &self, + account_id: &str, + ) -> Result<Option<IdentityGateState>, String> { + #[cfg(target_os = "android")] + { + let manager = Self::accounts_manager()?; + let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id) + .map_err(|_| "invalid account id".to_owned())?; + manager + .select_account(&account_id) + .map_err(|source| source.to_string())?; + return self.load_identity_state().map(Some); + } + + #[cfg(not(target_os = "android"))] + { + let _ = account_id; + Ok(None) + } + } + fn home_action_states(&self) -> Vec<HomeActionState> { #[cfg(target_os = "android")] { @@ -386,7 +430,6 @@ impl AndroidBackend { match status { RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready { account_id: account.account_id.to_string(), - npub: account.public_identity.public_key_npub, }, RadrootsNostrSelectedAccountStatus::NotConfigured | RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing, @@ -402,6 +445,24 @@ impl AndroidBackend { Ok(Self::map_status(status)) } + fn account_roster_from_manager( + manager: &RadrootsNostrAccountsManager, + ) -> Result<Vec<RadrootsAccountSummary>, String> { + manager + .list_accounts() + .map_err(|source| source.to_string())? + .into_iter() + .map(|record| { + Ok(RadrootsAccountSummary { + account_id: record.account_id.to_string(), + npub: record.public_identity.public_key_npub, + label: record.label, + custody: RadrootsAccountCustody::LocalManaged, + }) + }) + .collect() + } + fn generate_local_identity( manager: &RadrootsNostrAccountsManager, ) -> Result<IdentityGateState, String> { @@ -626,7 +687,6 @@ mod tests { state, IdentityGateState::Ready { account_id: account.account_id.to_string(), - npub: account.public_identity.public_key_npub, } ); } @@ -664,12 +724,11 @@ mod tests { let manager = RadrootsNostrAccountsManager::new_in_memory(); let state = AndroidBackend::generate_local_identity(&manager).expect("generate identity"); - let IdentityGateState::Ready { account_id, npub } = state else { + let IdentityGateState::Ready { account_id } = state else { panic!("expected ready identity state"); }; assert!(!account_id.is_empty()); - assert!(npub.starts_with("npub1")); } #[test] @@ -733,7 +792,6 @@ mod tests { state, IdentityGateState::Ready { account_id: identity.id().to_string(), - npub: identity.npub(), } ); assert_eq!(