commit 15420f0e27a5a1a1b6109dfd594eefc2a8f9a22f
parent 05ffc0dbc7649c977f1b11f0d4373841e4501bcc
Author: Florian Dold <dold@taler.net>
Date: Tue, 3 Nov 2026 10:08:55 +0100
bank web UI: prevent completed withdrawals from becoming active again
Diffstat:
4 files changed, 84 insertions(+), 49 deletions(-)
diff --git a/packages/libeufin-bank-webui/src/pages/ActiveWithdrawal.tsx b/packages/libeufin-bank-webui/src/pages/ActiveWithdrawal.tsx
@@ -72,11 +72,7 @@ export function ActiveWithdrawal({
openOperationId !== undefined &&
openOperationId !== operationId;
const watchedOperationId = operationRouteOpen ? undefined : operationId;
- const polledOperationId = useRef(watchedOperationId);
- const polledResult = useWithdrawalDetails(watchedOperationId);
- const result =
- polledOperationId.current === watchedOperationId ? polledResult : undefined;
- polledOperationId.current = watchedOperationId;
+ const result = useWithdrawalDetails(watchedOperationId);
useEffect(() => {
if (active || !bankState.currentWithdrawalOperationId) return;
@@ -101,30 +97,6 @@ export function ActiveWithdrawal({
}, [active, activeMatches, updateBankState]);
useEffect(() => {
- if (
- active ||
- bankState.currentWithdrawalOperationId ||
- openOperationId === undefined
- ) {
- return;
- }
- updateBankState("activeWithdrawal", {
- operationId: openOperationId,
- username,
- backendBaseUrl: backendUrl.href,
- confirmationDeferred: false,
- kind: "standard",
- });
- }, [
- active,
- backendUrl.href,
- bankState.currentWithdrawalOperationId,
- openOperationId,
- updateBankState,
- username,
- ]);
-
- useEffect(() => {
if (!routeHasConflict || !operationId) return;
navigateTo(routeOperation.url({ wopid: operationId }));
}, [navigateTo, operationId, routeHasConflict, routeOperation]);
@@ -185,7 +157,8 @@ export function ActiveWithdrawal({
}
const terminal = result !== undefined && isTerminalWithdrawalResult(result);
- const showNotice = !!operationId && !operationRouteOpen && !terminal;
+ const showNotice =
+ !!operationId && !operationRouteOpen && result !== undefined && !terminal;
return (
<Fragment>
diff --git a/packages/libeufin-bank-webui/src/pages/OperationState/index.ts b/packages/libeufin-bank-webui/src/pages/OperationState/index.ts
@@ -51,6 +51,7 @@ export interface Props {
export interface WithdrawalOperationProps extends Props {
operationId: string;
clearWhenTerminal?: boolean;
+ recoverActiveWithdrawal?: boolean;
}
export type State =
diff --git a/packages/libeufin-bank-webui/src/pages/OperationState/state.ts b/packages/libeufin-bank-webui/src/pages/OperationState/state.ts
@@ -30,7 +30,13 @@ import {
useTranslationContext,
utils,
} from "@gnu-taler/web-util/browser";
-import { useCallback, useEffect, useRef, useState } from "preact/hooks";
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "preact/hooks";
import { useSettingsContext } from "../../context/settings.js";
import {
revalidateWithdrawalDetails,
@@ -47,7 +53,7 @@ export type WithdrawalDetailsResult = Exclude<
interface WithdrawalStateInput extends Omit<
WithdrawalOperationProps,
- "clearWhenTerminal"
+ "clearWhenTerminal" | "recoverActiveWithdrawal"
> {
result: WithdrawalDetailsResult;
bankIntegrationApiBaseUrl: HostPortPath;
@@ -304,34 +310,76 @@ export function useWithdrawalOperationState({
onOperationCreated: _onOperationCreated,
focus,
clearWhenTerminal = false,
+ recoverActiveWithdrawal = false,
}: WithdrawalOperationProps): State {
- const polledOperationId = useRef(operationId);
- const polledResult = useWithdrawalDetails(operationId);
- const result =
- polledOperationId.current === operationId ? polledResult : undefined;
- polledOperationId.current = operationId;
+ const result = useWithdrawalDetails(operationId);
const { i18n } = useTranslationContext();
const [bankState, updateBankState] = useBankState();
+ const { state: credentials } = useSessionState();
+ const username =
+ credentials.status === "loggedIn" ? credentials.username : undefined;
const {
+ url: backendUrl,
lib: { bank },
} = useBankCoreApiContext();
-
+ const recovery = useMemo(
+ () => ({
+ username,
+ backendBaseUrl: backendUrl.href,
+ operationId,
+ handled: false,
+ }),
+ [username, backendUrl.href, operationId],
+ );
+ const active = bankState.activeWithdrawal;
+ const activeMatches =
+ active?.operationId === operationId &&
+ active.username === username &&
+ active.backendBaseUrl === backendUrl.href;
const terminal = result !== undefined && isTerminalWithdrawalResult(result);
useEffect(() => {
- if (
- clearWhenTerminal &&
- terminal &&
- bankState.activeWithdrawal?.operationId === operationId
- ) {
- updateBankState("activeWithdrawal", undefined);
+ if (terminal) {
+ recovery.handled = true;
+ if (clearWhenTerminal && activeMatches) {
+ updateBankState("activeWithdrawal", undefined);
+ }
+ return;
}
+ if (
+ !recoverActiveWithdrawal ||
+ recovery.handled ||
+ !username ||
+ !result ||
+ result instanceof TalerError ||
+ result.type !== "ok" ||
+ result.body.username !== username ||
+ (result.body.status !== "pending" && result.body.status !== "selected")
+ )
+ return;
+ // An existing record counts as recovered, too: clearing it after a
+ // successful action must not let the last pending response resurrect it.
+ recovery.handled = true;
+ if (active) return;
+ updateBankState("activeWithdrawal", {
+ operationId,
+ username,
+ backendBaseUrl: backendUrl.href,
+ confirmationDeferred: false,
+ kind: result.body.no_amount_to_wallet ? "cash-acceptor" : "standard",
+ });
}, [
- bankState.activeWithdrawal?.operationId,
+ active,
+ activeMatches,
+ backendUrl.href,
clearWhenTerminal,
operationId,
+ recoverActiveWithdrawal,
+ recovery,
+ result,
terminal,
updateBankState,
+ username,
]);
if (!result) {
diff --git a/packages/libeufin-bank-webui/src/pages/WithdrawalOperationPage.tsx b/packages/libeufin-bank-webui/src/pages/WithdrawalOperationPage.tsx
@@ -14,8 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import {
+ RouteDefinition,
+ useBankCoreApiContext,
+} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
+import { useSessionState } from "../hooks/session.js";
import { useBankState } from "../hooks/bank-state.js";
import { WithdrawalOperation } from "./OperationState/index.js";
@@ -31,12 +35,21 @@ export function WithdrawalOperationPage({
routeClose: RouteDefinition;
}): VNode {
const [bankState, updateBankState] = useBankState();
+ const { url: backendUrl } = useBankCoreApiContext();
+ const { state: credentials } = useSessionState();
+ const active = bankState.activeWithdrawal;
+ const activeMatches =
+ active?.operationId === operationId &&
+ credentials.status === "loggedIn" &&
+ active.username === credentials.username &&
+ active.backendBaseUrl === backendUrl.href;
return (
<WithdrawalOperation
operationId={operationId}
clearWhenTerminal
+ recoverActiveWithdrawal
onAbort={() => {
- if (bankState.activeWithdrawal?.operationId === operationId) {
+ if (activeMatches) {
updateBankState("activeWithdrawal", undefined);
}
onOperationAborted();
@@ -44,9 +57,9 @@ export function WithdrawalOperationPage({
onContinueLater={
onContinueLater
? () => {
- if (bankState.activeWithdrawal?.operationId === operationId) {
+ if (activeMatches) {
updateBankState("activeWithdrawal", {
- ...bankState.activeWithdrawal,
+ ...active,
confirmationDeferred: true,
});
}