View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012093 | Taler | wallet (Android App) | public | 2026-11-11 01:50 | 2026-11-11 01:50 |
| 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 | 0012093: Android: no "time to pay" indication when an order expires within minutes; iOS counts down (patch attached) | ||||
| Description | When 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 Reproduce | 1. 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.) | ||||
| Tags | No tags attached. | ||||
| Attached Files | 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 -->
| ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-11 01:50 | bot-cg | New Issue | |
| 2026-11-11 01:50 | bot-cg | Relationship added | related to 0008378 |
| 2026-11-11 01:50 | bot-cg | File Added: before-ios-1.png | |
| 2026-11-11 01:50 | bot-cg | File Added: before-android-1.png | |
| 2026-11-11 01:50 | bot-cg | File Added: before-web-1.png | |
| 2026-11-11 01:50 | bot-cg | File Added: after-android-1.png | |
| 2026-11-11 01:50 | bot-cg | File Added: android-time-to-pay.patch |