View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012095 | Taler | wallet (WebExtension) | 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 | 0012095: WebExtension: 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. WebExtension (origin/master bf2e571a2): packages/wallet-webui/src has no reference to pay_deadline; screens/PaymentScreen.tsx:324-384 renders the action row without any deadline. Patch (web-time-to-pay.patch): paymentReviewView passes the contract's pay_deadline (ms) as payDeadline; PaymentOptions shows a small TimeToPay element ("Time to pay: m:ss", role=timer) above the action buttons once the deadline is less than five minutes away, updated every second (a single timeout until the five-minute mark before that). One new i18n string. Verified: wallet-webui builds (esbuild), and tsc --build reports no error in the changed files (the 29 errors it reports are identical on unmodified origin/master); after-web-1.png shows "Time to pay: 3:59" for the same order, ticking (3:52 a few seconds later). Related: 0008378, 0012093 (Android 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 | web-time-to-pay.patch (4,488 bytes)
diff --git a/packages/wallet-webui/src/routes/App.tsx b/packages/wallet-webui/src/routes/App.tsx
index e649abaf4..57ca4883a 100644
--- a/packages/wallet-webui/src/routes/App.tsx
+++ b/packages/wallet-webui/src/routes/App.tsx
@@ -4889,6 +4889,7 @@ function PaymentDialog(props: {
summary={review.summary}
order={review.order}
minimumAge={review.minimumAge}
+ payDeadline={review.payDeadline}
choices={review.choices}
selectedChoice={activeChoice}
error={error}
diff --git a/packages/wallet-webui/src/routes/payment-model.ts b/packages/wallet-webui/src/routes/payment-model.ts
index 8629cf90f..09b8db19d 100644
--- a/packages/wallet-webui/src/routes/payment-model.ts
+++ b/packages/wallet-webui/src/routes/payment-model.ts
@@ -31,6 +31,8 @@ export interface PaymentReviewView {
summary: string;
order: OrderSummaryView;
minimumAge?: number;
+ /** Payment deadline in milliseconds since the epoch, if any. */
+ payDeadline?: number;
fulfillmentMessage?: string;
fulfillmentUrl?: string;
choices: PaymentChoiceView[];
@@ -270,6 +272,9 @@ export function paymentReviewView(
Amounts.parse(choices[0]?.amountRaw ?? "")?.currency,
),
...(terms.minimum_age ? { minimumAge: terms.minimum_age } : {}),
+ ...(typeof terms.pay_deadline?.t_s === "number"
+ ? { payDeadline: terms.pay_deadline.t_s * 1000 }
+ : {}),
...(terms.fulfillment_message
? { fulfillmentMessage: terms.fulfillment_message }
: {}),
diff --git a/packages/wallet-webui/src/screens/PaymentScreen.tsx b/packages/wallet-webui/src/screens/PaymentScreen.tsx
index da04c0f71..baaf4a407 100644
--- a/packages/wallet-webui/src/screens/PaymentScreen.tsx
+++ b/packages/wallet-webui/src/screens/PaymentScreen.tsx
@@ -7,7 +7,7 @@ import type { ErrorPresentation } from "../ui/error.js";
import type { PaymentInsufficientBalanceDetails } from "@gnu-taler/taler-util";
import { ConfirmationDialog } from "../ui/ConfirmationDialog.js";
import { QrFrame } from "../ui/QrFrame.js";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
export interface PaymentChoiceView {
index: number;
@@ -163,8 +163,37 @@ export type PaymentScreenState =
| "done"
| "error";
+/** Payments due within this time show a countdown, as in the iOS wallet. */
+const TIME_TO_PAY_WARNING_MS = 5 * 60 * 1000;
+
+function TimeToPay(props: { deadline: number }) {
+ const [now, setNow] = useState(() => Date.now());
+ const remaining = props.deadline - now;
+ const visible = remaining <= TIME_TO_PAY_WARNING_MS;
+ useEffect(() => {
+ if (remaining <= 0) return;
+ const timer = window.setTimeout(
+ () => setNow(Date.now()),
+ visible ? 1000 : remaining - TIME_TO_PAY_WARNING_MS,
+ );
+ return () => window.clearTimeout(timer);
+ }, [now, remaining, visible]);
+ if (!visible) return null;
+ const seconds = Math.max(0, Math.ceil(remaining / 1000));
+ const time = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
+ return (
+ <p role="timer" class="text-right text-sm font-medium text-onWarningContainer">
+ {
+ // Translators: Placeholder is the remaining time (minutes:seconds) before the merchant's payment deadline.
+ i18n.str`Time to pay: ${time}`
+ }
+ </p>
+ );
+}
+
export function PaymentOptions(props: {
state: "ready" | "paying";
+ payDeadline?: number;
choices?: PaymentChoiceView[];
selectedChoice?: number;
useDonau?: boolean;
@@ -321,6 +350,9 @@ export function PaymentOptions(props: {
: i18n.str`Preparing the payment for another wallet…`}
</p>
)}
+ {props.payDeadline !== undefined && (
+ <TimeToPay deadline={props.payDeadline} />
+ )}
<div class="flex flex-wrap justify-end gap-3 border-t border-outlineVariant pt-5">
{props.onUnclaim && (
<Button
@@ -442,6 +474,7 @@ export function PaymentScreen(props: {
summary?: string;
order?: OrderSummaryView;
minimumAge?: number;
+ payDeadline?: number;
choices?: PaymentChoiceView[];
selectedChoice?: number;
error?: ErrorPresentation;
@@ -591,6 +624,7 @@ export function PaymentScreen(props: {
/>
<PaymentOptions
state={props.state}
+ payDeadline={props.payDeadline}
choices={props.choices}
selectedChoice={props.selectedChoice}
useDonau={props.useDonau}
| ||||
| related to | 0012093 | new | Android: no "time to pay" indication when an order expires within minutes; iOS counts down (patch attached) |
| 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 0012093 |
| 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-web-1.png | |
| 2026-11-11 01:50 | bot-cg | File Added: web-time-to-pay.patch |