taler-typescript-core

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

commit 15ec20dde06ff7df065e29e78d3e6bfa724a6b61
parent 54e25259f70a3270be2c62a8e48d30e8406b27cd
Author: Florian Dold <dold@taler.net>
Date:   Wed,  4 Nov 2026 00:38:29 +0100

wallet-webui: add retry and cancel controls for demo cash

Issue: https://bugs.gnunet.org/view?id=11825

Diffstat:
Mpackages/wallet-webui/src/routes/App.tsx | 8++++++--
Mpackages/wallet-webui/test/progress.test.ts | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 72 insertions(+), 5 deletions(-)

diff --git a/packages/wallet-webui/src/routes/App.tsx b/packages/wallet-webui/src/routes/App.tsx @@ -668,6 +668,7 @@ function UriInputRoute() { function BalanceRoute() { const { connection, demo } = useServices(); + const { controller: demoCashRequest } = useWalletProgress(connection); const { language } = useLanguage(); const uiPreferences = useUiPreferences(); const [, navigate] = useLocation(); @@ -753,7 +754,7 @@ function BalanceRoute() { setGettingDemoCash(true); setDemoCashError(undefined); try { - const result = await callMutation( + const result = await demoCashRequest.callAndInvalidate( WalletApiOperation.WithdrawTestBalance, publicTestBalanceRequest("KUDOS"), ["balances", "transactions", "exchanges"], @@ -769,6 +770,7 @@ function BalanceRoute() { } navigate(`/transaction/${result.value.transactionId}`); } catch (cause) { + if (isProgressRequestCancelled(cause)) return; setDemoCashError( errorFromException(cause, i18n.str`Demo cash could not be withdrawn.`), ); @@ -5335,6 +5337,7 @@ async function downloadJson(filename: string, value: unknown): Promise<void> { function SettingsRoute() { const { connection, storage, demo, platform } = useServices(); + const { controller: testMoneyRequest } = useWalletProgress(connection); const language = useLanguage(); const uiPreferences = useUiPreferences(); const durability = useDurability(); @@ -5763,7 +5766,7 @@ function SettingsRoute() { setTestMoneyBusy(true); setTestMoneyError(undefined); try { - const result = await connection.client.callForResult( + const result = await testMoneyRequest.callForResult( WalletApiOperation.WithdrawTestBalance, publicTestBalanceRequest(currency), ); @@ -5779,6 +5782,7 @@ function SettingsRoute() { await mutate((key) => isWalletKey(key)); navigate(`/transaction/${result.value.transactionId}`); } catch (cause) { + if (isProgressRequestCancelled(cause)) return; setTestMoneyError( errorFromException(cause, i18n.str`Test money could not be withdrawn.`), ); diff --git a/packages/wallet-webui/test/progress.test.ts b/packages/wallet-webui/test/progress.test.ts @@ -24,10 +24,9 @@ function deferred<T>() { return { promise, resolve, reject }; } -function progressFixture() { +function progressFixture<T = { exchangeBaseUrl: string }>() { const notifications = new Set<(n: WalletNotification) => void>(); - const request = - deferred<ReturnType<typeof Result.of<{ exchangeBaseUrl: string }>>>(); + const request = deferred<ReturnType<typeof Result.of<T>>>(); const sent: { operation: string; request: unknown }[] = []; const connection = { client: { @@ -179,3 +178,67 @@ test("disposing an active controller cancels the owned request", async () => { }, }); }); + +test("demo cash progress retries, cancels quietly, and invalidates on success", async () => { + type Cash = { transactionId: string; accountPaytoUri: string }; + const request = { + amount: "KUDOS:5" as const, + corebankApiBaseUrl: "https://bank.example/", + exchangeBaseUrl: "https://exchange.example/", + }; + for (const cancel of [false, true]) { + const fixture = progressFixture<Cash>(); + const invalidated: string[][] = []; + const controller = new WalletProgressController( + fixture.connection, + async (families) => { + invalidated.push([...families]); + }, + false, + ); + const pending = controller.callAndInvalidate( + WalletApiOperation.WithdrawTestBalance, + request, + ["balances", "transactions", "exchanges"], + ); + const token = (fixture.sent[0].request as { progressToken: string }) + .progressToken; + assert.ok(token); + fixture.emit({ + type: NotificationType.RequestProgressPhase, + operation: "withdrawTestBalance", + progressToken: token, + phase: "stalled", + }); + assert.equal(controller.getState().phase, "stalled"); + await controller.retryNow(); + assert.deepEqual(fixture.sent.at(-1), { + operation: WalletApiOperation.RetryProgressTokenNow, + request: { operation: "withdrawTestBalance", progressToken: token }, + }); + if (cancel) { + await controller.cancel(); + assert.deepEqual(fixture.sent.at(-1), { + operation: WalletApiOperation.CancelProgressToken, + request: { operation: "withdrawTestBalance", progressToken: token }, + }); + fixture.request.reject( + TalerError.fromDetail(TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED, {}), + ); + await assert.rejects(pending, ProgressRequestCancelledError); + assert.deepEqual(invalidated, []); + } else { + const result = { + transactionId: "withdrawal:demo", + accountPaytoUri: "payto://x-taler-bank/bank.example/demo", + }; + fixture.request.resolve(Result.of(result)); + assert.deepEqual(await pending, Result.of(result)); + assert.deepEqual(invalidated, [ + ["balances", "transactions", "exchanges"], + ]); + } + assert.equal(controller.getState().phase, "idle"); + controller.dispose(); + } +});