commit 4f9cbd0785983bfe5fda1dfdf33ade840016b2ab
parent c327f11ada394ca92d2e2f705d884447d055fafe
Author: Florian Dold <dold@taler.net>
Date: Tue, 1 Sep 2026 22:53:29 +0200
wallet web UI: show peer-share delays and retries
Diffstat:
2 files changed, 93 insertions(+), 15 deletions(-)
diff --git a/packages/wallet-webui/src/routes/App.tsx b/packages/wallet-webui/src/routes/App.tsx
@@ -9,6 +9,7 @@ import {
TalerUriAction,
TalerUris,
TemplateType,
+ TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionType,
@@ -1849,6 +1850,7 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) {
function PeerShareRoute() {
const { connection } = useServices();
+ const callMutation = useWalletMutation(connection);
const [, params] = useRoute("/peer/share/:transactionId/:mode");
const [, navigate] = useLocation();
const transactionId = (params?.transactionId ?? "") as TransactionIdStr;
@@ -1859,6 +1861,8 @@ function PeerShareRoute() {
{ transactionId, includeContractTerms: false },
);
const transaction = query.data;
+ const [actionError, setActionError] = useState<ErrorPresentation>();
+ const [retrying, setRetrying] = useState(false);
const uri =
transaction &&
(transaction.type === TransactionType.PeerPushDebit ||
@@ -1875,28 +1879,84 @@ function PeerShareRoute() {
TransactionMajorState.Failed,
TransactionMajorState.Expired,
].includes(transaction.txState.major);
- const error = query.error
- ? errorFromException(
- query.error,
- i18n.str`The peer share link could not be loaded.`,
- )
- : transaction &&
- transaction.type !== TransactionType.PeerPushDebit &&
- transaction.type !== TransactionType.PeerPullCredit
- ? localError(i18n.str`This transaction does not have a peer share link.`)
- : terminalWithoutUri
- ? localError(i18n.str`This peer link is no longer available.`)
- : undefined;
+ const transactionProblem =
+ transaction && !terminalWithoutUri && transaction.error
+ ? transactionErrorPresentation(
+ transaction,
+ i18n.str`The exchange could not prepare the peer share link yet.`,
+ )
+ : undefined;
+ const retryable = Boolean(
+ transactionProblem &&
+ transaction?.txActions.includes(TransactionAction.Retry),
+ );
+ const error = actionError
+ ? actionError
+ : query.error
+ ? errorFromException(
+ query.error,
+ i18n.str`The peer share link could not be loaded.`,
+ )
+ : transaction &&
+ transaction.type !== TransactionType.PeerPushDebit &&
+ transaction.type !== TransactionType.PeerPullCredit
+ ? localError(
+ i18n.str`This transaction does not have a peer share link.`,
+ )
+ : terminalWithoutUri
+ ? localError(i18n.str`This peer link is no longer available.`)
+ : transactionProblem;
+ const loading = query.isLoading || (!uri && !error);
+ const [delayed, setDelayed] = useState(false);
+ useEffect(() => {
+ setDelayed(false);
+ if (!loading) return;
+ const timer = window.setTimeout(() => setDelayed(true), 5_000);
+ return () => window.clearTimeout(timer);
+ }, [loading, transactionId]);
+ const retry = async () => {
+ if (retrying) return;
+ setRetrying(true);
+ setActionError(undefined);
+ try {
+ const result = await callMutation(
+ WalletApiOperation.RetryTransaction,
+ { transactionId },
+ ["balances", "transactions"],
+ );
+ if (Result.isError(result)) {
+ setActionError(
+ walletCoreError(
+ result.detail,
+ i18n.str`The wallet could not retry preparing the peer share link.`,
+ ),
+ );
+ }
+ } catch (cause) {
+ setActionError(
+ errorFromException(
+ cause,
+ i18n.str`Retrying the peer share link failed.`,
+ ),
+ );
+ } finally {
+ setRetrying(false);
+ }
+ };
return (
<PeerShareScreen
mode={mode}
uri={uri}
- loading={query.isLoading || (!uri && !error)}
+ loading={loading}
+ delayed={delayed}
error={error}
+ retryable={retryable}
+ retrying={retrying}
onCopy={() => {
if (uri) void navigator.clipboard.writeText(uri);
}}
onDone={() => navigate(`/transaction/${transactionId}`)}
+ onRetry={() => void retry()}
/>
);
}
diff --git a/packages/wallet-webui/src/screens/PeerShareScreen.tsx b/packages/wallet-webui/src/screens/PeerShareScreen.tsx
@@ -9,9 +9,13 @@ export function PeerShareScreen(props: {
mode: "send" | "request";
uri?: string;
loading: boolean;
+ delayed?: boolean;
error?: ErrorPresentation;
+ retryable?: boolean;
+ retrying?: boolean;
onCopy: () => void;
onDone: () => void;
+ onRetry?: () => void;
}) {
return (
<div class="mx-auto w-full max-w-2xl space-y-6">
@@ -27,16 +31,30 @@ export function PeerShareScreen(props: {
</div>
{props.loading && (
<Card>
- <p role="status">{i18n.str`Preparing a secure share link…`}</p>
+ <p role="status">
+ {props.delayed
+ ? i18n.str`Waiting for a response from the exchange…`
+ : i18n.str`Preparing a secure share link…`}
+ </p>
</Card>
)}
{props.error && (
<ErrorCard error={props.error}>
- <div class="mt-4">
+ {props.retryable && (
+ <p class="mt-3 text-sm text-onErrorContainer">
+ {i18n.str`The wallet will retry automatically. You can also try again now.`}
+ </p>
+ )}
+ <div class="mt-4 flex flex-wrap justify-end gap-3">
<Button
tone="secondary"
onClick={props.onDone}
>{i18n.str`View transaction`}</Button>
+ {props.retryable && props.onRetry && (
+ <Button disabled={props.retrying} onClick={props.onRetry}>
+ {props.retrying ? i18n.str`Trying…` : i18n.str`Try now`}
+ </Button>
+ )}
</div>
</ErrorCard>
)}