taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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:
Mwallet/src/main/java/net/taler/wallet/WalletNavHost.kt | 7+++++++
Mwallet/src/main/java/net/taler/wallet/WalletNavigation.kt | 3+++
Awallet/src/main/java/net/taler/wallet/contacts/AliasRegistrationScreen.kt | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/java/net/taler/wallet/contacts/ContactManager.kt | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/java/net/taler/wallet/settings/SettingsScreen.kt | 7+++++++
Mwallet/src/main/res/values/strings.xml | 9+++++++++
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>