commit e09adaa044f648e5d0e5b7a74756b5944f2dd6e2
parent 1d03bc4793dc2d78694a88eee9067339bac5ade5
Author: Florian Dold <dold@taler.net>
Date: Wed, 26 Aug 2026 01:49:23 +0200
wallet-webui: preserve terminal payment error details
Diffstat:
3 files changed, 51 insertions(+), 13 deletions(-)
diff --git a/packages/wallet-webui/src/routes/App.tsx b/packages/wallet-webui/src/routes/App.tsx
@@ -123,6 +123,7 @@ import {
} from "./withdrawal-model.js";
import {
transactionDetailView,
+ transactionErrorPresentation,
transactionHistoryView,
transactionStateView,
type TransactionUiAction,
@@ -3749,6 +3750,10 @@ function PaymentRoute() {
return <PaymentDialog transaction={transaction} actionId={actionId} />;
const done = transaction.txState.major === TransactionMajorState.Done;
const pending = transaction.txState.major === TransactionMajorState.Pending;
+ const terminalError = transactionErrorPresentation(
+ transaction,
+ i18n.str`Payment is ${transactionStateView(transaction).label}.`,
+ );
return (
<PaymentScreen
state={done ? `done` : pending ? "pending" : "error"}
@@ -3764,14 +3769,10 @@ function PaymentRoute() {
error={
done || pending
? undefined
- : transaction.error
- ? walletCoreError(
- transaction.error,
- i18n.str`Payment is ${transactionStateView(transaction).label}.`,
- )
- : localError(
- i18n.str`Payment is ${transactionStateView(transaction).label}.`,
- )
+ : (terminalError ??
+ localError(
+ i18n.str`Payment is ${transactionStateView(transaction).label}.`,
+ ))
}
onSelectChoice={() => {}}
onConfirm={() => {}}
diff --git a/packages/wallet-webui/src/routes/transaction-model.ts b/packages/wallet-webui/src/routes/transaction-model.ts
@@ -344,13 +344,18 @@ function timestampView(
};
}
-function errorPresentation(
+/** Preserve the structured reason for any wallet-core transaction failure. */
+export function transactionErrorPresentation(
transaction: Transaction,
+ fallback?: string,
): ErrorPresentation | undefined {
const detail =
transaction.error ?? transaction.failReason ?? transaction.abortReason;
if (!detail) return undefined;
- return walletCoreError(detail, i18n.str`Wallet error ${detail.code}`);
+ return walletCoreError(
+ detail,
+ fallback ?? i18n.str`Wallet error ${detail.code}`,
+ );
}
function fieldsFor(transaction: Transaction): TransactionDetailField[] {
@@ -779,6 +784,7 @@ export function transactionDetailView(
transaction.txState.minor === TransactionMinorState.Ready
? transaction.talerUri
: undefined;
+ const error = transactionErrorPresentation(transaction);
return {
...base,
...(transaction.localTransactionId
@@ -788,9 +794,7 @@ export function transactionDetailView(
fields: fieldsFor(transaction),
...(technicalFields.length ? { technicalFields } : {}),
actions,
- ...(errorPresentation(transaction)
- ? { error: errorPresentation(transaction) }
- : {}),
+ ...(error ? { error } : {}),
...(externalAction ? { externalAction } : {}),
...(continuationAction ? { continuationAction } : {}),
...(peerShareUri ? { peerShareUri } : {}),
diff --git a/packages/wallet-webui/test/transaction-model.test.ts b/packages/wallet-webui/test/transaction-model.test.ts
@@ -16,6 +16,7 @@ import {
} from "@gnu-taler/taler-util";
import {
transactionDetailView,
+ transactionErrorPresentation,
transactionHistoryView,
} from "../src/routes/transaction-model.js";
@@ -232,6 +233,38 @@ test("transaction details preserve complete wallet-core errors", () => {
assert.strictEqual(view.error?.detail, detail);
});
+test("transaction errors preserve every wallet-core terminal reason", () => {
+ const active = {
+ code: TalerErrorCode.WALLET_NETWORK_ERROR,
+ hint: "active retry error",
+ };
+ const failed = {
+ code: TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
+ hint: "terminal failure",
+ nestedFutureField: { kept: true },
+ };
+ const aborted = {
+ code: TalerErrorCode.WALLET_ORDER_ALREADY_PAID,
+ hint: "terminal abort",
+ };
+
+ assert.strictEqual(
+ transactionErrorPresentation(payment({ failReason: failed }))?.detail,
+ failed,
+ );
+ assert.strictEqual(
+ transactionErrorPresentation(payment({ abortReason: aborted }))?.detail,
+ aborted,
+ );
+ assert.strictEqual(
+ transactionErrorPresentation(
+ payment({ error: active, failReason: failed, abortReason: aborted }),
+ )?.detail,
+ active,
+ );
+ assert.equal(transactionErrorPresentation(payment()), undefined);
+});
+
test("payment details include the Penpot order summary when contract terms are requested", () => {
const view = transactionDetailView(
payment({