View Issue Details

IDProjectCategoryView StatusLast Update
0012093Talerwallet (Android App)public2026-11-11 01:50
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityhave not tried
Status newResolutionopen 
Summary0012093: Android: no "time to pay" indication when an order expires within minutes; iOS counts down (patch attached)
DescriptionWhen an order is about to expire (contract pay_deadline less than five minutes away -- typical for point-of-sale and vending orders), iOS shows "Time to pay: m:ss" counting down above the Pay button and withdraws the button when it runs out. Android and the WebExtension show nothing; the screen simply changes under the user when wallet-core expires the proposal.

What the user sees (order created with pay_deadline = now + 4 minutes; origin/master builds of 2026-11-11):
- iOS: "Time to pay:" with the remaining minutes and seconds next to "Pay KUDOS 1.00 now" (before-ios-1.png).
- Android: the normal prompt, no time shown (before-android-1.png).
- WebExtension: the normal "Review payment", no time shown (before-web-1.png).

References: iOS Views/Sheets/Payment/PaymentScan.swift:255-316 (PaySafeArea, TimeView "Time to pay:"), Model/Model+Payment.swift:286-306 (timeToPay from payDeadline), Views/Transactions/TransactionSummaryList.swift:359-372.

Android (origin/master 4c483527): taler-kotlin-android/.../ContractTerms.kt:82-83,134-135 decodes payDeadline, but nothing in the wallet module uses it; PromptPaymentComposable.kt:185-246 shows only total, fee and buttons.

Patch (android-time-to-pay.patch): PromptPaymentComposable shows "Time to pay: m:ss" in the bottom box once the deadline is less than five minutes away, ticking every second (a LaunchedEffect sleeps until the five-minute mark, then updates once per second). One new English string payment_time_to_pay. The Pay button is left alone; wallet-core expires the proposal at the deadline.
Verified: :wallet:assembleFdroidSim builds; after-android-1.png shows "Time to pay: 3:58" for the same order (3:51 three seconds later).

Related: 0008378 (iOS: payment detail should show deadlines; the maintainer note there says the pay deadline matters before payment), WebExtension counterpart.

Assisted by LLM
Steps To Reproduce1. Create an order whose pay_deadline is a few minutes from now (POST /private/orders with "pay_deadline": {"t_s": <now+240>}).
2. Open its taler://pay/ link: iOS shows a countdown, Android and the WebExtension do not.
(Automated: wc-stack pay-uri --amount KUDOS:1 --pay-deadline 240.)
TagsNo tags attached.
Attached Files
before-ios-1.png (264,738 bytes)
before-android-1.png (64,104 bytes)   
before-android-1.png (64,104 bytes)   
before-web-1.png (78,811 bytes)   
before-web-1.png (78,811 bytes)   
after-android-1.png (70,471 bytes)   
after-android-1.png (70,471 bytes)   
android-time-to-pay.patch (4,131 bytes)   
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentComposable.kt
index 3663d1d3..9fb20a0e 100644
--- a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentComposable.kt
@@ -58,6 +58,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
@@ -71,6 +72,7 @@ import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import net.taler.common.Amount
 import net.taler.common.ContractChoice
@@ -83,6 +85,7 @@ import net.taler.common.ContractTokenFamily
 import net.taler.common.Exchange
 import net.taler.common.Merchant
 import net.taler.common.TalerUtils
+import net.taler.common.Timestamp
 import net.taler.lib.android.base64Bitmap
 import net.taler.wallet.R
 import net.taler.wallet.cleanExchange
@@ -182,8 +185,12 @@ fun PromptPaymentComposable(
             }
         }
 
+        val timeToPay = rememberTimeToPay(contractTerms.payDeadline)
         BottomButtonBox(Modifier.fillMaxWidth(),
-            heading = if (contractTerms is ContractTerms.V0) { ->
+            heading = if (contractTerms is ContractTerms.V0 || timeToPay != null) { ->
+                timeToPay?.let { TimeToPay(it) }
+
+                if (contractTerms !is ContractTerms.V0) return@BottomButtonBox
                 val choice = status.choices.firstOrNull()
                     ?: error("no v0 choice")
 
@@ -247,6 +254,40 @@ fun PromptPaymentComposable(
     }
 }
 
+/** Orders due within this time show a countdown, as in the iOS wallet. */
+private const val TIME_TO_PAY_WARNING_MS = 5 * 60 * 1000L
+
+/**
+ * Milliseconds left until the payment deadline once it is less than
+ * [TIME_TO_PAY_WARNING_MS] away, updated every second; null before that.
+ */
+@Composable
+private fun rememberTimeToPay(payDeadline: Timestamp?): Long? {
+    val deadlineMs = payDeadline?.ms?.takeIf { it > 0 } ?: return null
+    var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
+    val remaining = deadlineMs - now
+    LaunchedEffect(now) {
+        if (remaining > 0) {
+            delay(if (remaining > TIME_TO_PAY_WARNING_MS) remaining - TIME_TO_PAY_WARNING_MS else 1000L)
+            now = System.currentTimeMillis()
+        }
+    }
+    return if (remaining > TIME_TO_PAY_WARNING_MS) null else remaining.coerceAtLeast(0)
+}
+
+@Composable
+private fun TimeToPay(remainingMs: Long) {
+    val seconds = (remainingMs + 999) / 1000
+    Text(
+        modifier = Modifier.padding(bottom = 3.dp),
+        text = stringResource(
+            R.string.payment_time_to_pay,
+            "%d:%02d".format(seconds / 60, seconds % 60),
+        ),
+        style = MaterialTheme.typography.labelLarge,
+    )
+}
+
 @Composable
 fun MerchantSection(
     contractTerms: ContractTerms,
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 89750709..77b6b67c 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -320,6 +320,7 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
     <string name="payment_tokens_unexpected">Unexpected merchant %1$s, your token could be stolen.</string>
     <string name="payment_tokens_untrusted">Merchant %1$s is untrusted, cannot spend this token.</string>
     <string name="payment_template_error">Error creating order</string>
+    <string name="payment_time_to_pay">Time to pay: %1$s</string>
     <string name="payment_title">Payment</string>
 
     <!-- P2P receive -->
android-time-to-pay.patch (4,131 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

related to 0008378 implementing Payment: Detail view should show deadlines 
related to 0012095 new WebExtension: no "time to pay" indication when an order expires within minutes; iOS counts down (patch attached) 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-11 01:50bot-cgNew Issue
2026-11-11 01:50bot-cgRelationship addedrelated to 0008378
2026-11-11 01:50bot-cgFile Added: before-ios-1.png
2026-11-11 01:50bot-cgFile Added: before-android-1.png
2026-11-11 01:50bot-cgFile Added: before-web-1.png
2026-11-11 01:50bot-cgFile Added: after-android-1.png
2026-11-11 01:50bot-cgFile Added: android-time-to-pay.patch