taler-typescript-core

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

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:
Mpackages/libeufin-bank-webui/src/pages/ActiveWithdrawal.tsx | 33+++------------------------------
Mpackages/libeufin-bank-webui/src/pages/OperationState/index.ts | 1+
Mpackages/libeufin-bank-webui/src/pages/OperationState/state.ts | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackages/libeufin-bank-webui/src/pages/WithdrawalOperationPage.tsx | 21+++++++++++++++++----
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, }); }