View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012080 | Taler | wallet (Android App) | public | 2026-11-11 01:39 | 2026-11-11 01:39 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0012080: Android never shows when a peer payment link expires (receive / pay-request prompt, pending send/request) -- iOS and web do (patch attached) | ||||
| Description | The Android wallet never tells the user when a peer-to-peer payment link expires: not on the "Receive payment" prompt of a taler://pay-push link, not on the "Pay request" prompt of a taler://pay-pull link, and not on the detail page of a pending Send / Request. iOS shows "Expires: <date>" on both prompts and on pending peer transactions; the web wallet shows "Expires" on the pay-request screen and in the transaction details. The date matters: it is how long the user has to accept or pay, and for a link the user sent, when the money comes back if nobody takes it. Screenshots (demo backend; links created with expiry 1 day): - before-android-1-receive.png, before-android-2-payrequest.png: Android prompts: summary, total, button -- no date. - before-android-3-send-detail.png: Android pending Send: QR, URI, amounts, purpose -- no date. - before-ios-1-receive.png, before-ios-2-payrequest.png: iOS "Expires: November 12, 2026 at 2:21 AM". - before-web-2-payrequest.png, before-web-3-send-detail.png: web "Expires" row. - after-android-1-receive.png, after-android-2-payrequest.png, after-android-3-send-detail.png: with the patch. wallet-core delivers the date already: PreparePeerPushCreditResponse / PreparePeerPullDebitResponse .contractTerms.purse_expiration and TransactionPeer*.info.expiration. Code (taler-android origin/master 4c483527, wallet/src/main/java/net/taler/wallet/): - peer/IncomingState.kt:58-62: PeerContractTerms(summary, amount) -- purse_expiration is not decoded - peer/IncomingComposable.kt:115-195: PeerPullTermsComposable (both prompts) renders summary, total, fee, button - transactions/Transactions.kt:577-581: PeerInfoShort.expiration is decoded, but none of peer/TransactionPeer{PushDebit,PullCredit,PushCredit,PullDebit}.kt reads it iOS: Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift:88, P2pPayURIView.swift:51-52, Views/Transactions/TransactionTypeDetail.swift:228-230. Web: screens/PeerReceiveScreen.tsx:118-123, routes/transaction-model.ts:532-585. Patch (attached, android-peer-expiration.patch): decode purse_expiration into PeerContractTerms; a small PeerExpirationComposable (TransactionInfoComposable with the absolute date, nothing for "never") shown on the incoming prompt and, while the transaction is pending or in dialog state, in the four peer transaction details. One new string, peer_expiration = "Expires". Verification: built :wallet:assembleFdroidSim; after-android-1/2 show "Expires / November 12, 2026 at 2:21 AM" on both prompts; after-android-3-send-detail.png shows "Expires / December 9, 2026 at 2:37 AM" (the exchange's default push expiration) on a pending Send. Assisted by LLM | ||||
| Steps To Reproduce | 1. Fund the wallet from https://bank.demo.taler.net/. 2. Let another wallet create a pay-push link and a pay-pull link (expiry e.g. 1 day) and open them: no expiry on either prompt. 3. Send money: the pending transaction's detail shows no expiry either. | ||||
| Tags | No tags attached. | ||||
| Attached Files | android-peer-expiration.patch (8,700 bytes)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
index 4393e470..32858701 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/ExpirationComposable.kt
@@ -26,13 +26,30 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import net.taler.common.Timestamp
+import net.taler.lib.android.toAbsoluteTime
import net.taler.wallet.R
import net.taler.wallet.compose.NumericInputField
import net.taler.wallet.compose.SelectionChip
import net.taler.wallet.compose.TalerSurface
+import net.taler.wallet.transactions.TransactionInfoComposable
+
+/**
+ * When a peer payment link (purse) expires: until then the other party can
+ * accept/pay it, afterwards the money goes back. Nothing is shown for "never".
+ */
+@Composable
+fun PeerExpirationComposable(expiration: Timestamp?) {
+ if (expiration == null || expiration.ms < 0) return
+ TransactionInfoComposable(
+ label = stringResource(R.string.peer_expiration),
+ info = expiration.ms.toAbsoluteTime(LocalContext.current).toString(),
+ )
+}
enum class ExpirationOption(val hours: Long) {
DAYS_1(24),
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
index 2171f445..fbc0ec17 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingComposable.kt
@@ -125,6 +125,7 @@ fun ColumnScope.PeerPullTermsComposable(
text = terms.contractTerms.summary,
style = MaterialTheme.typography.headlineSmall,
)
+ PeerExpirationComposable(terms.contractTerms.purseExpiration)
Spacer(modifier = Modifier.weight(1f))
Card(modifier = Modifier.fillMaxWidth()) {
Column(
diff --git a/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
index 6d0ae065..0e3eff51 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/IncomingState.kt
@@ -16,8 +16,10 @@
package net.taler.wallet.peer
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.taler.common.Amount
+import net.taler.common.Timestamp
import net.taler.wallet.backend.TalerErrorInfo
sealed class IncomingState
@@ -59,6 +61,8 @@ data class IncomingError(
data class PeerContractTerms(
val summary: String,
val amount: Amount,
+ @SerialName("purse_expiration")
+ val purseExpiration: Timestamp? = null,
)
@Serializable
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
index b14d4f5d..a5675889 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt
@@ -38,6 +38,7 @@ import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
import net.taler.wallet.transactions.TransactionMajorState.Done
+import net.taler.wallet.transactions.TransactionMajorState.Dialog
import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.TransactionMinorState.CreatePurse
import net.taler.wallet.transactions.TransactionMinorState.Ready
@@ -92,6 +93,10 @@ fun ColumnScope.TransactionPeerPullCreditComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
)
+
+ if (t.txState.major == Pending || t.txState.major == Dialog) {
+ PeerExpirationComposable(t.info.expiration)
+ }
}
@Preview
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
index ecc0377e..21ddc4b7 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt
@@ -33,6 +33,7 @@ import net.taler.wallet.transactions.TransactionAction.Retry
import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Dialog
import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.TransactionPeerComposable
import net.taler.wallet.transactions.TransactionPeerPullDebit
@@ -65,6 +66,10 @@ fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit) {
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
)
+
+ if (t.txState.major == Pending || t.txState.major == Dialog) {
+ PeerExpirationComposable(t.info.expiration)
+ }
}
@Preview
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
index aa7659a6..813707e2 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt
@@ -36,6 +36,7 @@ import net.taler.wallet.transactions.TransactionAction.Retry
import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
+import net.taler.wallet.transactions.TransactionMajorState.Dialog
import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.TransactionPeerComposable
import net.taler.wallet.transactions.TransactionPeerPushCredit
@@ -73,6 +74,10 @@ fun ColumnScope.TransactionPeerPushCreditComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
)
+
+ if (t.txState.major == Pending || t.txState.major == Dialog) {
+ PeerExpirationComposable(t.info.expiration)
+ }
}
@Preview
diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
index 4dc712b5..cc1f35fa 100644
--- a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
+++ b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt
@@ -47,6 +47,7 @@ import net.taler.wallet.transactions.TransactionAction.Suspend
import net.taler.wallet.transactions.TransactionAmountComposable
import net.taler.wallet.transactions.TransactionInfoComposable
import net.taler.wallet.transactions.TransactionMajorState.Done
+import net.taler.wallet.transactions.TransactionMajorState.Dialog
import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.TransactionMinorState.CreatePurse
import net.taler.wallet.transactions.TransactionMinorState.KycRequired
@@ -99,6 +100,10 @@ fun ColumnScope.TransactionPeerPushDebitComposable(
label = stringResource(id = R.string.send_peer_purpose),
info = t.info.summary ?: "",
)
+
+ if (t.txState.major == Pending || t.txState.major == Dialog) {
+ PeerExpirationComposable(t.info.expiration)
+ }
}
@Composable
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 89750709..ff02094e 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -347,6 +347,7 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<string name="send_peer_expiration_days">Days</string>
<string name="send_peer_expiration_hours">Hours</string>
<string name="send_peer_expiration_period">Expires in</string>
+ <string name="peer_expiration">Expires</string>
<string name="send_peer_payment_instruction">Scan this QR code to receive %1$s</string>
<string name="send_peer_purpose">Purpose</string>
<string name="send_peer_title">Send money to another wallet</string>
| ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-11 01:39 | bot-cg | New Issue | |
| 2026-11-11 01:39 | bot-cg | File Added: before-android-1-receive.png | |
| 2026-11-11 01:39 | bot-cg | File Added: before-android-2-payrequest.png | |
| 2026-11-11 01:39 | bot-cg | File Added: before-android-3-send-detail.png | |
| 2026-11-11 01:39 | bot-cg | File Added: before-ios-1-receive.png | |
| 2026-11-11 01:39 | bot-cg | File Added: before-ios-2-payrequest.png | |
| 2026-11-11 01:39 | bot-cg | File Added: before-web-2-payrequest.png | |
| 2026-11-11 01:39 | bot-cg | File Added: before-web-3-send-detail.png | |
| 2026-11-11 01:39 | bot-cg | File Added: after-android-1-receive.png | |
| 2026-11-11 01:39 | bot-cg | File Added: after-android-2-payrequest.png | |
| 2026-11-11 01:39 | bot-cg | File Added: after-android-3-send-detail.png | |
| 2026-11-11 01:39 | bot-cg | File Added: android-peer-expiration.patch |