commit 7d7c271fae5c582b20331bd54af36e67bc806e63
parent 4ef136060c74e18458d55e3a59be31000a6345e7
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Wed, 15 Jul 2026 15:29:10 +0200
add taldir lookup integration into contact management
Diffstat:
5 files changed, 192 insertions(+), 28 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriScreen.kt b/wallet/src/main/java/net/taler/wallet/HandleUriScreen.kt
@@ -134,6 +134,9 @@ fun HandleUriScreen(
}
onNavigateBack()
}
+ action.startsWith("add-contact/", ignoreCase = true) -> {
+ onNavigate(WalletDestination.Contacts(addContactUri = u2), true)
+ }
else -> {
errorInfo = TalerErrorInfo.makeCustomError("Unsupported URI: $u2")
}
diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt b/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt
@@ -49,7 +49,6 @@ import net.taler.wallet.exchanges.ExchangeListScreen
import net.taler.wallet.exchanges.ExchangeShoppingScreen
import net.taler.wallet.exchanges.ReviewExchangeTosScreen
import net.taler.wallet.main.MainScreen
-import net.taler.wallet.contacts.PromptAddContactScreen
import net.taler.wallet.main.MainViewModel
import net.taler.wallet.payment.PayTemplateScreen
import net.taler.wallet.peer.IncomingPullPaymentScreen
@@ -380,20 +379,12 @@ fun WalletNavHost(
ContactScreen(
model = model,
sendUri = contacts.sendUri,
+ addContactUri = contacts.addContactUri,
scopeInfo = scopeInfo,
onNavigate = onNavigate,
onNavigateBack = onNavigateBack,
)
}
- composable<WalletDestination.PromptAddContact> { backStackEntry ->
- val dest: WalletDestination.PromptAddContact = backStackEntry.toRoute()
- PromptAddContactScreen(
- model = model,
- uriString = dest.uriString,
- onNavigate = onNavigate,
- onNavigateBack = onNavigateBack,
- )
- }
composable<WalletDestination.Mailbox> {
MailboxScreen(
model = model,
diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt b/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt
@@ -88,9 +88,6 @@ sealed interface WalletDestination {
data class PromptPayTemplate(val uri: String) : WalletDestination
@Serializable
- data class PromptAddContact(val uriString: String) : WalletDestination
-
- @Serializable
data class WireTransferDetails(
val showQrCodes: Boolean,
) : WalletDestination
@@ -111,6 +108,7 @@ sealed interface WalletDestination {
data class Contacts(
val sendUri: String? = null,
val scopeInfo: String? = null,
+ val addContactUri: String? = null,
) : WalletDestination
@Serializable
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt b/wallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt
@@ -24,6 +24,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import net.taler.wallet.backend.BackendManager
+import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.main.TAG
@@ -44,12 +45,24 @@ data class ContactListResponse(
val contacts: List<ContactEntry>,
)
+@Serializable
+data class LookupAliasResponse(
+ val targetUri: String?,
+)
+
sealed class ContactResult {
data object Loading : ContactResult()
data class Error(val error: TalerErrorInfo) : ContactResult()
data class Success(val contacts: List<ContactEntry>) : ContactResult()
}
+sealed class LookupResult {
+ data object Idle : LookupResult()
+ data object Loading : LookupResult()
+ data class Error(val error: TalerErrorInfo) : LookupResult()
+ data class Success(val targetUri: String) : LookupResult()
+}
+
class ContactManager(
private val api: WalletBackendApi,
private val scope: CoroutineScope,
@@ -57,6 +70,13 @@ class ContactManager(
private val _contacts = MutableStateFlow<ContactResult>(ContactResult.Loading)
val contacts = _contacts.asStateFlow()
+ private val _lookupResult = MutableStateFlow<LookupResult>(LookupResult.Idle)
+ val lookupResult = _lookupResult.asStateFlow()
+
+ fun resetLookup() {
+ _lookupResult.value = LookupResult.Idle
+ }
+
fun loadContacts() {
_contacts.value = ContactResult.Loading
scope.launch {
@@ -98,6 +118,35 @@ class ContactManager(
}
}
+ fun taldirLookupAlias(
+ alias: String,
+ aliasType: String,
+ taldirBaseUri: String = "https://taldir.gnunet.org",
+ ) {
+ _lookupResult.value = LookupResult.Loading
+ scope.launch {
+ api.request<LookupAliasResponse>("lookupAlias", LookupAliasResponse.serializer()) {
+ put("alias", alias)
+ put("aliasType", aliasType)
+ put("taldirBaseUrl", taldirBaseUri)
+ }.onSuccess { resp ->
+ Log.d(TAG, "Got Taldir target URI for $alias: ${resp.targetUri}")
+ val targetUri = resp.targetUri
+ if (targetUri.isNullOrBlank()) {
+ _lookupResult.value = LookupResult.Error(TalerErrorInfo(
+ code = TalerErrorCode.UNKNOWN,
+ message = "Alias not found",
+ ))
+ } else {
+ _lookupResult.value = LookupResult.Success(targetUri)
+ }
+ }.onError {
+ Log.e(TAG, "Error looking up contact: $it")
+ _lookupResult.value = LookupResult.Error(it)
+ }
+ }
+ }
+
fun deleteContact(contact: ContactEntry) {
scope.launch {
api.request<Unit>("deleteContact") {
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/ContactScreen.kt b/wallet/src/main/java/net/taler/wallet/contacts/ContactScreen.kt
@@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
@@ -31,6 +33,7 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
@@ -53,6 +56,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
import net.taler.wallet.NavigateCallback
import net.taler.wallet.R
import net.taler.wallet.WalletDestination
@@ -65,18 +69,62 @@ import net.taler.wallet.transactions.TransactionStateFilter.Nonfinal
fun ContactScreen(
model: MainViewModel,
sendUri: String? = null,
+ addContactUri: String? = null,
scopeInfo: ScopeInfo? = null,
onNavigate: NavigateCallback,
onNavigateBack: () -> Unit,
) {
val contactManager = model.contactManager
val contactResult by contactManager.contacts.collectAsState()
+ val lookupResult by contactManager.lookupResult.collectAsState()
var showAddDialog by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
var contactToConfirm by remember { mutableStateOf<ContactEntry?>(null) }
+ var initialAlias by remember { mutableStateOf("") }
+ var initialAliasType by remember { mutableStateOf("email") }
+ var initialMailboxBaseUri by remember { mutableStateOf("") }
+ var initialMailboxAddress by remember { mutableStateOf("") }
+ var initialSource by remember { mutableStateOf("") }
+ var initialPetname by remember { mutableStateOf("") }
+
+ fun fillFromUri(uriString: String) {
+ val uri = uriString.toUri()
+ val segments = uri.pathSegments
+ var mailboxAddressTmp = uriString
+ val aliasType = segments.getOrNull(0) ?: ""
+ mailboxAddressTmp = mailboxAddressTmp.removePrefix("taler://add-contact/")
+ mailboxAddressTmp = mailboxAddressTmp.removePrefix("""$aliasType/""")
+ val alias = segments.getOrNull(1) ?: ""
+ mailboxAddressTmp = mailboxAddressTmp.removePrefix("""$alias/""")
+ val mailboxAddressFull = mailboxAddressTmp.split("?")[0]
+ val mailboxAddress = mailboxAddressFull.split("/").last()
+ val mailboxBaseUri = buildString {
+ append("https://")
+ append(mailboxAddressFull.removeSuffix(mailboxAddress))
+ }
+ val petname = uri.getQueryParameter("petname") ?: ""
+
+ initialAlias = alias
+ initialAliasType = aliasType
+ initialMailboxBaseUri = mailboxBaseUri
+ initialMailboxAddress = mailboxAddress
+ initialSource = uriString
+ initialPetname = petname
+ showAddDialog = true
+ }
+
LaunchedEffect(Unit) {
contactManager.loadContacts()
+ addContactUri?.let { fillFromUri(it) }
+ }
+
+ LaunchedEffect(lookupResult) {
+ if (lookupResult is LookupResult.Success) {
+ val uri = (lookupResult as LookupResult.Success).targetUri
+ contactManager.resetLookup()
+ fillFromUri(uri)
+ }
}
Scaffold(
@@ -163,38 +211,113 @@ fun ContactScreen(
if (showAddDialog) {
AddContactDialog(
- onDismiss = { showAddDialog = false },
+ initialAlias = initialAlias,
+ initialAliasType = initialAliasType,
+ initialMailboxBaseUri = initialMailboxBaseUri,
+ initialMailboxAddress = initialMailboxAddress,
+ initialSource = initialSource,
+ initialPetname = initialPetname,
+ onDismiss = {
+ showAddDialog = false
+ contactManager.resetLookup()
+ initialAlias = ""
+ initialAliasType = "email"
+ initialMailboxBaseUri = ""
+ initialMailboxAddress = ""
+ initialSource = ""
+ initialPetname = ""
+ },
onAdd = { alias, aliasType, mailboxBaseUri, mailboxAddress, source, petname ->
contactManager.addContact(alias, aliasType, mailboxBaseUri, mailboxAddress, source, petname)
showAddDialog = false
- }
+ },
+ onLookup = { alias, aliasType ->
+ contactManager.taldirLookupAlias(alias, aliasType)
+ },
+ lookupResult = lookupResult,
)
}
}
@Composable
fun AddContactDialog(
+ initialAlias: String = "",
+ initialAliasType: String = "email",
+ initialMailboxBaseUri: String = "",
+ initialMailboxAddress: String = "",
+ initialSource: String = "",
+ initialPetname: String = "",
onDismiss: () -> Unit,
onAdd: (String, String, String, String, String, String) -> Unit,
+ onLookup: (String, String) -> Unit,
+ lookupResult: LookupResult,
) {
- var alias by remember { mutableStateOf("") }
- var aliasType by remember { mutableStateOf("") }
- var mailboxBaseUri by remember { mutableStateOf("") }
- var mailboxAddress by remember { mutableStateOf("") }
- var source by remember { mutableStateOf("") }
- var petname by remember { mutableStateOf("") }
+ var alias by remember { mutableStateOf(initialAlias) }
+ var aliasType by remember { mutableStateOf(initialAliasType) }
+ var mailboxBaseUri by remember { mutableStateOf(initialMailboxBaseUri) }
+ var mailboxAddress by remember { mutableStateOf(initialMailboxAddress) }
+ var source by remember { mutableStateOf(initialSource) }
+ var petname by remember { mutableStateOf(initialPetname) }
+
+ LaunchedEffect(initialAlias) { alias = initialAlias }
+ LaunchedEffect(initialAliasType) { aliasType = initialAliasType }
+ LaunchedEffect(initialMailboxBaseUri) { mailboxBaseUri = initialMailboxBaseUri }
+ LaunchedEffect(initialMailboxAddress) { mailboxAddress = initialMailboxAddress }
+ LaunchedEffect(initialSource) { source = initialSource }
+ LaunchedEffect(initialPetname) { petname = initialPetname }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add Contact") },
text = {
- Column {
- OutlinedTextField(value = alias, onValueChange = { alias = it }, label = { Text("Alias") })
- OutlinedTextField(value = aliasType, onValueChange = { aliasType = it }, label = { Text("Alias Type") })
- OutlinedTextField(value = mailboxBaseUri, onValueChange = { mailboxBaseUri = it }, label = { Text("Mailbox Base URI") })
- OutlinedTextField(value = mailboxAddress, onValueChange = { mailboxAddress = it }, label = { Text("Mailbox Address") })
- OutlinedTextField(value = source, onValueChange = { source = it }, label = { Text("Source") })
- OutlinedTextField(value = petname, onValueChange = { petname = it }, label = { Text("Petname") })
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ OutlinedTextField(
+ value = petname,
+ onValueChange = { petname = it },
+ label = { Text("Petname") },
+ modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
+ )
+ OutlinedTextField(
+ value = alias,
+ onValueChange = { alias = it },
+ label = { Text("Alias") },
+ modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
+ )
+ OutlinedTextField(
+ value = aliasType,
+ onValueChange = { aliasType = it },
+ label = { Text("Alias Type") },
+ modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
+ )
+
+ if (lookupResult is LookupResult.Loading) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ } else {
+ Button(
+ onClick = { onLookup(alias, aliasType) },
+ enabled = alias.isNotBlank() && aliasType.isNotBlank(),
+ modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
+ ) {
+ Text("Lookup with Taldir")
+ }
+ }
+
+ if (lookupResult is LookupResult.Error) {
+ Text(
+ text = "Lookup failed: ${lookupResult.error.userFacingMsg}",
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ }
+
+ OutlinedTextField(value = mailboxBaseUri, onValueChange = { mailboxBaseUri = it }, label = { Text("Mailbox Base URI") }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
+ OutlinedTextField(value = mailboxAddress, onValueChange = { mailboxAddress = it }, label = { Text("Mailbox Address") }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
+ OutlinedTextField(value = source, onValueChange = { source = it }, label = { Text("Source") }, modifier = Modifier.fillMaxWidth())
}
},
confirmButton = {