taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/wallet-webui/src/routes/App.tsx | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mpackages/wallet-webui/src/screens/PeerShareScreen.tsx | 22++++++++++++++++++++--
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> )}