commit bf0c1860fc716efff04319d2f3a4fe79ae8a7633
parent 48b774fc535a3f6a844fc18989fcc8b8dce112c6
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Thu, 16 Jul 2026 14:37:50 +0200
Implement Alias registration first draft
Diffstat:
6 files changed, 329 insertions(+), 0 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt b/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt
@@ -39,6 +39,7 @@ import net.taler.wallet.accounts.BankAccountsScreen
import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.contacts.AliasRegistrationScreen
import net.taler.wallet.contacts.ContactScreen
import net.taler.wallet.mailbox.MailboxScreen
import net.taler.wallet.deposit.DepositScreen
@@ -392,5 +393,11 @@ fun WalletNavHost(
onNavigateBack = onNavigateBack,
)
}
+ composable<WalletDestination.AliasRegistration> {
+ AliasRegistrationScreen(
+ model = model,
+ onNavigateBack = onNavigateBack,
+ )
+ }
}
}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt b/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt
@@ -114,6 +114,9 @@ sealed interface WalletDestination {
@Serializable
data object Mailbox : WalletDestination
+ @Serializable
+ data object AliasRegistration : WalletDestination
+
// Transaction Details
@Serializable
data object TransactionWithdrawal : WalletDestination
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/AliasRegistrationScreen.kt b/wallet/src/main/java/net/taler/wallet/contacts/AliasRegistrationScreen.kt
@@ -0,0 +1,221 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2026 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.contacts
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import net.taler.wallet.R
+import net.taler.wallet.mailbox.MailboxResult
+import net.taler.wallet.main.MainViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AliasRegistrationScreen(
+ model: MainViewModel,
+ onNavigateBack: () -> Unit,
+) {
+ var aliasType by remember { mutableStateOf("") }
+ var alias by remember { mutableStateOf("") }
+ var durationMonths by remember { mutableStateOf("12") }
+ var solution by remember { mutableStateOf("") }
+ val taldirBaseUri = "https://taldir.gnunet.org"
+
+ val mailboxState by model.mailboxManager.mailbox.collectAsState()
+ val registerResult by model.contactManager.registerResult.collectAsState()
+
+ DisposableEffect(Unit) {
+ onDispose {
+ model.contactManager.resetRegistration()
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.alias_registration_title)) },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ )
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ val mailboxAddress = (mailboxState as? MailboxResult.Success)?.let {
+ "${it.config.mailboxBaseUrl}/${it.config.hAddress}"
+ }
+
+ if (registerResult is RegisterResult.Idle || registerResult is RegisterResult.Loading || registerResult is RegisterResult.Error) {
+ OutlinedTextField(
+ value = aliasType,
+ onValueChange = { aliasType = it },
+ label = { Text(stringResource(R.string.alias_registration_alias_type)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp),
+ enabled = registerResult !is RegisterResult.Loading
+ )
+ OutlinedTextField(
+ value = alias,
+ onValueChange = { alias = it },
+ label = { Text(stringResource(R.string.alias_registration_alias)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp),
+ enabled = registerResult !is RegisterResult.Loading
+ )
+ OutlinedTextField(
+ value = durationMonths,
+ onValueChange = { durationMonths = it },
+ label = { Text(stringResource(R.string.alias_registration_duration)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp),
+ enabled = registerResult !is RegisterResult.Loading
+ )
+
+ Button(
+ onClick = {
+ mailboxAddress?.let { targetUri ->
+ model.contactManager.taldirRegister(
+ alias = alias,
+ aliasType = aliasType,
+ taldirBaseUri = taldirBaseUri,
+ targetUri = targetUri,
+ durationMonths = durationMonths.toIntOrNull() ?: 12
+ )
+ }
+ },
+ enabled = aliasType.isNotBlank() && alias.isNotBlank() && mailboxAddress != null && registerResult !is RegisterResult.Loading,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(stringResource(R.string.alias_registration_button))
+ }
+ }
+
+ when (val res = registerResult) {
+ is RegisterResult.Loading -> {
+ Text(stringResource(R.string.loading), modifier = Modifier.padding(top = 8.dp))
+ }
+ is RegisterResult.Error -> {
+ Text(
+ text = stringResource(R.string.alias_registration_error, res.error.userFacingMsg),
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ is RegisterResult.Success -> {
+ if (res.validFor != null) {
+ Text(
+ text = stringResource(R.string.alias_registration_exists),
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ } else {
+ OutlinedTextField(
+ value = solution,
+ onValueChange = { solution = it },
+ label = { Text(stringResource(R.string.alias_registration_solution)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ )
+ Button(
+ onClick = {
+ mailboxAddress?.let { targetUri ->
+ model.contactManager.completeTaldirRegister(
+ alias = alias,
+ aliasType = aliasType,
+ taldirBaseUri = taldirBaseUri,
+ targetUri = targetUri,
+ solution = solution
+ )
+ }
+ },
+ enabled = solution.isNotBlank() && mailboxAddress != null,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(stringResource(R.string.ok))
+ }
+ }
+ }
+ is RegisterResult.CompletionLoading -> {
+ Text(stringResource(R.string.loading), modifier = Modifier.padding(top = 8.dp))
+ }
+ is RegisterResult.CompletionSuccess -> {
+ Text(
+ text = stringResource(R.string.alias_registration_success),
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ Button(
+ onClick = onNavigateBack,
+ modifier = Modifier.fillMaxWidth().padding(top = 16.dp)
+ ) {
+ Text(stringResource(R.string.ok))
+ }
+ }
+ else -> {}
+ }
+
+ if (mailboxAddress == null) {
+ Text(
+ text = "Mailbox not configured",
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt b/wallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
+import net.taler.common.RelativeTime
import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
@@ -50,6 +51,20 @@ data class LookupAliasResponse(
val targetUri: String?,
)
+@Serializable
+data class TaldirRegisterRequest(
+ val alias: String,
+ val aliasType: String,
+ val taldirBaseUrl: String,
+ val targetUri: String,
+ val duration: RelativeTime,
+)
+
+@Serializable
+data class TaldirRegisterResponse(
+ val validFor: RelativeTime? = null,
+)
+
sealed class ContactResult {
data object Loading : ContactResult()
data class Error(val error: TalerErrorInfo) : ContactResult()
@@ -63,6 +78,15 @@ sealed class LookupResult {
data class Success(val targetUri: String, val source: String) : LookupResult()
}
+sealed class RegisterResult {
+ data object Idle : RegisterResult()
+ data object Loading : RegisterResult()
+ data class Error(val error: TalerErrorInfo) : RegisterResult()
+ data class Success(val validFor: RelativeTime?) : RegisterResult()
+ data object CompletionLoading : RegisterResult()
+ data object CompletionSuccess : RegisterResult()
+}
+
class ContactManager(
private val api: WalletBackendApi,
private val scope: CoroutineScope,
@@ -73,10 +97,17 @@ class ContactManager(
private val _lookupResult = MutableStateFlow<LookupResult>(LookupResult.Idle)
val lookupResult = _lookupResult.asStateFlow()
+ private val _registerResult = MutableStateFlow<RegisterResult>(RegisterResult.Idle)
+ val registerResult = _registerResult.asStateFlow()
+
fun resetLookup() {
_lookupResult.value = LookupResult.Idle
}
+ fun resetRegistration() {
+ _registerResult.value = RegisterResult.Idle
+ }
+
fun loadContacts() {
_contacts.value = ContactResult.Loading
scope.launch {
@@ -158,4 +189,55 @@ class ContactManager(
}
}
}
+
+ fun taldirRegister(
+ alias: String,
+ aliasType: String,
+ taldirBaseUri: String = "https://taldir.gnunet.org",
+ targetUri: String,
+ durationMonths: Int,
+ ) {
+ val durationUs = durationMonths.toLong() * 30 * 24 * 3600 * 1000000L
+ _registerResult.value = RegisterResult.Loading
+ scope.launch {
+ api.request("registerAlias", TaldirRegisterResponse.serializer()) {
+ put("alias", alias)
+ put("aliasType", aliasType)
+ put("taldirBaseUrl", taldirBaseUri)
+ put("targetUri", targetUri)
+ put("duration", JSONObject(BackendManager.json.encodeToString(RelativeTime.serializer(), RelativeTime(durationUs))))
+ }.onSuccess {
+ Log.d(TAG, "Taldir registration initiated for $alias: $it")
+ _registerResult.value = RegisterResult.Success(it.validFor)
+ }.onError {
+ Log.e(TAG, "Error initiating Taldir registration: $it")
+ _registerResult.value = RegisterResult.Error(it)
+ }
+ }
+ }
+
+ fun completeTaldirRegister(
+ alias: String,
+ aliasType: String,
+ taldirBaseUri: String = "https://taldir.gnunet.org",
+ targetUri: String,
+ solution: String,
+ ) {
+ _registerResult.value = RegisterResult.CompletionLoading
+ scope.launch {
+ api.request<Unit>("completeRegisterAlias") {
+ put("alias", alias)
+ put("aliasType", aliasType)
+ put("taldirBaseUrl", taldirBaseUri)
+ put("targetUri", targetUri)
+ put("challenge", solution)
+ }.onSuccess {
+ Log.d(TAG, "Taldir registration completed for $alias")
+ _registerResult.value = RegisterResult.CompletionSuccess
+ }.onError {
+ Log.e(TAG, "Error completing Taldir registration: $it")
+ _registerResult.value = RegisterResult.Error(it)
+ }
+ }
+ }
}
diff --git a/wallet/src/main/java/net/taler/wallet/settings/SettingsScreen.kt b/wallet/src/main/java/net/taler/wallet/settings/SettingsScreen.kt
@@ -254,6 +254,13 @@ fun SettingsScreen(
)
SettingsItem(
+ title = stringResource(R.string.alias_registration_title),
+ summary = "Taldir Registration",
+ icon = Icons.Default.Person,
+ onClick = { onNavigate(WalletDestination.AliasRegistration, false) }
+ )
+
+ SettingsItem(
title = stringResource(R.string.settings_db_export),
summary = stringResource(R.string.settings_db_export_summary),
icon = ImageVector.vectorResource(R.drawable.ic_unarchive),
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -100,6 +100,15 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<string name="contact_mailbox_base_uri">Mailbox Base URI</string>
<string name="contact_add_button">Add Contact</string>
<string name="contacts_title">Contacts</string>
+ <string name="alias_registration_title">Alias Registration</string>
+ <string name="alias_registration_alias_type">Alias Type</string>
+ <string name="alias_registration_alias">Alias</string>
+ <string name="alias_registration_duration">Duration (months)</string>
+ <string name="alias_registration_button">Register Alias</string>
+ <string name="alias_registration_exists">Registration already exists.</string>
+ <string name="alias_registration_solution">Challenge Solution</string>
+ <string name="alias_registration_success">Registration successful!</string>
+ <string name="alias_registration_error">Registration failed: %1$s</string>
<string name="wallet">Wallet</string>
<string name="warning">Warning</string>
<string name="wire_transfer">Wire transfer</string>