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:
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();
+ }
+});