taler-android

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

commit 1a1753086266dd04ac8db12f3ae1198f2f4adbfb
parent a3a188cee788a599f127aa8a8844a8e1b4df7a6c
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Sat, 31 Oct 2026 15:33:45 +0100

mailbox: fix flow for subject hint previews

Diffstat:
Mwallet/src/main/java/net/taler/wallet/mailbox/MailboxManager.kt | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mwallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mwallet/src/main/res/values/strings.xml | 4++--
3 files changed, 143 insertions(+), 47 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/mailbox/MailboxManager.kt b/wallet/src/main/java/net/taler/wallet/mailbox/MailboxManager.kt @@ -18,12 +18,14 @@ package net.taler.wallet.mailbox import android.util.Log import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import net.taler.common.Amount @@ -56,6 +58,7 @@ data class MailboxMessageRecord( val originMailboxBaseUrl: String, val downloadedAt: Timestamp, val talerUri: String, + val subjectHint: String? = null, ) @Serializable @@ -121,37 +124,76 @@ class MailboxManager( } } - fun fetchDetailsForMessage(message: MailboxMessageRecord) { + fun prepareAndStoreSubject(message: MailboxMessageRecord, onReady: () -> Unit) { val uri = message.talerUri - if (_messageDetails.value.containsKey(uri)) return + if (!message.subjectHint.isNullOrBlank()) { + scope.launch(Dispatchers.Main) { onReady() } + return + } - scope.launch { + scope.launch(Dispatchers.IO) { try { + var summary: String? = null if (uri.startsWith("taler://pay-push")) { api.request("preparePeerPushCredit", PreparePeerPushCreditResponse.serializer()) { put("talerUri", uri) }.onSuccess { res -> - _messageDetails.update { - it + (uri to MailboxMessageDetails( - summary = res.contractTerms.summary.ifBlank { null }, - amount = res.amountRaw - )) - } + summary = res.contractTerms.summary.ifBlank { "Incoming payment" } + }.onError { + summary = "Incoming payment" } } else if (uri.startsWith("taler://pay-pull")) { api.request("preparePeerPullDebit", PreparePeerPullDebitResponse.serializer()) { put("talerUri", uri) }.onSuccess { res -> - _messageDetails.update { - it + (uri to MailboxMessageDetails( - summary = res.contractTerms.summary.ifBlank { null }, - amount = res.amountRaw - )) + summary = res.contractTerms.summary.ifBlank { "Payment request" } + }.onError { + summary = "Payment request" + } + } else { + summary = "Transaction" + } + + if (!summary.isNullOrBlank()) { + _messageDetails.update { + it + (uri to MailboxMessageDetails( + summary = summary, + amount = null + )) + } + val updated = message.copy(subjectHint = summary) + api.request<Unit>("upsertMailboxMessage") { + put("message", JSONObject(BackendManager.json.encodeToString(updated))) + }.onSuccess { + val state = _mailbox.value + if (state is MailboxResult.Success) { + getMessages(state.config) } + }.onError { + Log.e(TAG, "Error upserting mailbox message in prepareAndStoreSubject: $it") } } } catch (e: Exception) { - Log.e(TAG, "Error fetching details for message URI: $uri", e) + Log.e(TAG, "Error preparing transaction for summary: $uri", e) + } finally { + withContext(Dispatchers.Main) { + onReady() + } + } + } + } + + fun upsertMessage(message: MailboxMessageRecord) { + scope.launch { + api.request<Unit>("upsertMailboxMessage") { + put("message", JSONObject(BackendManager.json.encodeToString(message))) + }.onSuccess { + val state = _mailbox.value + if (state is MailboxResult.Success) { + getMessages(state.config) + } + }.onError { + Log.e(TAG, "Error upserting mailbox message: $it") } } } @@ -199,9 +241,6 @@ class MailboxManager( api.request("getMailboxMessages", MailboxMessagesResponse.serializer()) .onSuccess { response -> _mailbox.value = MailboxResult.Success(config, response.messages) - response.messages.forEach { msg -> - fetchDetailsForMessage(msg) - } }.onError { Log.e(TAG, "Error getting mailbox messages: $it") _mailbox.value = MailboxResult.Error(it) @@ -235,7 +274,7 @@ class MailboxManager( private fun addMessage(message: MailboxMessageRecord) { scope.launch { - api.request<Unit>("addMailboxMessage") { + api.request<Unit>("upsertMailboxMessage") { put("message", JSONObject(BackendManager.json.encodeToString(message))) }.onError { Log.e(TAG, "Error adding mailbox message: $it") diff --git a/wallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt b/wallet/src/main/java/net/taler/wallet/mailbox/MailboxScreen.kt @@ -16,17 +16,21 @@ package net.taler.wallet.mailbox +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Refresh @@ -58,6 +62,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext @@ -246,6 +251,23 @@ fun MailboxScreen( } @Composable +fun NewBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = "NEW", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onTertiaryContainer, + fontWeight = FontWeight.Bold, + ) + } +} + +@Composable fun MessageList( messages: List<MailboxMessageRecord>, mailboxManager: MailboxManager? = null, @@ -255,9 +277,20 @@ fun MessageList( if (messages.isEmpty()) { Text("No messages yet.", modifier = Modifier.padding(16.dp)) } else { + val initiallyNewUris = remember { + messages.filter { it.subjectHint.isNullOrBlank() }.map { it.talerUri }.toSet() + } + LazyColumn { items(messages) { message -> - MessageItem(message, mailboxManager, onNavigate, onDelete) + val isNew = initiallyNewUris.contains(message.talerUri) + MessageItem( + message = message, + isNew = isNew, + mailboxManager = mailboxManager, + onNavigate = onNavigate, + onDelete = onDelete + ) HorizontalDivider() } } @@ -267,6 +300,7 @@ fun MessageList( @Composable fun MessageItem( message: MailboxMessageRecord, + isNew: Boolean, mailboxManager: MailboxManager? = null, onNavigate: NavigateCallback, onDelete: (MailboxMessageRecord) -> Unit, @@ -277,12 +311,7 @@ fun MessageItem( message.talerUri.startsWith("taler://pay/") || message.talerUri.startsWith("taler://pay-template") - val messageDetailsState = mailboxManager?.messageDetails?.collectAsState() - val details = messageDetailsState?.value?.get(message.talerUri) - - LaunchedEffect(message.talerUri) { - mailboxManager?.fetchDetailsForMessage(message) - } + val hasSubject = !message.subjectHint.isNullOrBlank() Row( modifier = Modifier @@ -291,27 +320,49 @@ fun MessageItem( verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { - if (details?.summary != null) { + if (isNew) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 4.dp) + ) { + NewBadge() + if (hasSubject) { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = message.subjectHint, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + } + } else if (hasSubject) { Text( - text = details.summary, + text = message.subjectHint, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 2.dp) ) } - if (details?.amount != null) { - Text( - text = details.amount.toString(), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold, - color = if (isPayPush) Color(0xFF2E7D32) else Color(0xFFE65100), - modifier = Modifier.padding(bottom = 6.dp) - ) - } - if (isPayPush) { + if (hasSubject) { + Button( + onClick = { + onNavigate(WalletDestination.HandleUri(message.talerUri), false) + }, + modifier = Modifier.padding(bottom = 6.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + modifier = Modifier.padding(end = 6.dp) + ) + Text("Go to transaction") + } + } else if (isPayPush) { Button( - onClick = { onNavigate(WalletDestination.HandleUri(message.talerUri), false) }, + onClick = { + mailboxManager?.prepareAndStoreSubject(message) {} + }, colors = ButtonDefaults.buttonColors( containerColor = Color(0xFF2E7D32), contentColor = Color.White, @@ -327,7 +378,9 @@ fun MessageItem( } } else if (isPayPull) { Button( - onClick = { onNavigate(WalletDestination.HandleUri(message.talerUri), false) }, + onClick = { + mailboxManager?.prepareAndStoreSubject(message) {} + }, colors = ButtonDefaults.buttonColors( containerColor = Color(0xFFE65100), contentColor = Color.White, @@ -342,11 +395,14 @@ fun MessageItem( Text(stringResource(R.string.mailbox_payment_request)) } } else { - Text( - text = message.talerUri, - style = MaterialTheme.typography.bodyMedium, + Button( + onClick = { + mailboxManager?.prepareAndStoreSubject(message) {} + }, modifier = Modifier.padding(bottom = 6.dp) - ) + ) { + Text(message.talerUri) + } } Text( "From: ${message.originMailboxBaseUrl}", @@ -375,6 +431,7 @@ fun MessageItemPreview() { downloadedAt = Timestamp.now(), talerUri = "taler://pay-push/...", ), + isNew = true, onNavigate = { _, _ -> }, onDelete = {} ) diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -531,8 +531,8 @@ 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_petname">Nickname</string> <string name="contacts_title">Contacts</string> - <string name="mailbox_incoming_payment">Incoming payment</string> - <string name="mailbox_payment_request">Payment request</string> + <string name="mailbox_incoming_payment">Open incoming transaction message</string> + <string name="mailbox_payment_request">Open payment request message</string> <!-- Settings -->