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 ( +

+ { + // Translators: Placeholder is the remaining time (minutes:seconds) before the merchant's payment deadline. + i18n.str`Time to pay: ${time}` + } +

+ ); +} + 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…`}

)} + {props.payDeadline !== undefined && ( + + )}
{props.onUnclaim && (