taler-typescript-core

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

commit b8c8a914fc31918fa8e8ed40359bf168476a946a
parent 17020c9cb78963269de13b2e715eb23bfaf78615
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon,  8 Dec 2025 15:10:23 -0300

fix #9681

Diffstat:
Mpackages/merchant-backoffice-ui/src/hooks/instance.ts | 19++++++++-----------
Mpackages/merchant-backoffice-ui/src/hooks/order.ts | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 4++++
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 24++++++++++++++----------
4 files changed, 82 insertions(+), 22 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -29,6 +29,7 @@ import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; import { useState, useMemo } from "preact/hooks"; import { useEffect } from "preact/compat"; +import { delayMs } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export function revalidateInstanceDetails() { @@ -88,7 +89,7 @@ export function useInstanceKYCDetails() { } // FIXME: move to web-utils and reuse -function useAsyncWithRetry<Res>( +export function useAsyncWithRetry<Res>( fetcher: (() => Promise<Res>) | undefined, retry?: (res: Res | undefined, err?: TalerHttpError | undefined) => boolean, ): { result: Res | undefined; error: TalerHttpError | undefined } { @@ -96,8 +97,8 @@ function useAsyncWithRetry<Res>( const [error, setError] = useState<TalerHttpError>(); const [retryCounter, setRetryCounter] = useState(0); + let unloaded = false; useEffect(() => { - let unloaded = false; if (fetcher) { fetcher() .then((resp) => { @@ -116,23 +117,19 @@ function useAsyncWithRetry<Res>( }, [fetcher, retryCounter]); // retry on result or error + // FIXME: why we need a secund useEffect? this should be merged with the one above useEffect(() => { if (retry && retry(result, error)) { setRetryCounter((c) => c + 1); } + return () => { + unloaded = true; + }; }, [result, error]); return { result, error }; } -async function delay(t_ms: number) { - return new Promise((res) => { - setTimeout(() => { - res(0); - }, t_ms); - }); -} - let latestRequestTime = 0; /** * FIXME: this will not wait for the fist KYC event to be triggered @@ -160,7 +157,7 @@ export function useInstanceKYCDetailsLongPolling() { const diff = now - latestRequestTime; if (diff < DEFAULT_WAIT) { console.log("PREVENT REQUEST BURST"); - await delay(DEFAULT_WAIT); + await delayMs(DEFAULT_WAIT); } } latestRequestTime = new Date().getTime(); diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -19,12 +19,19 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; import { AbsoluteTime, AccessToken, + HttpStatusCode, + MerchantOrderStatusResponse, + OperationFail, + OperationOk, + TalerErrorCode, TalerHttpError, TalerMerchantManagementResultByMethod, } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; -import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; +import { buildPaginatedResult, delayMs } from "@gnu-taler/web-util/browser"; +import { useMemo } from "preact/hooks"; +import { useAsyncWithRetry } from "./instance.js"; const useSWR = _useSWR as unknown as SWRHook; export function revalidateOrderDetails() { @@ -51,6 +58,54 @@ export function useOrderDetails(oderId: string) { return undefined; } +let latestRequestTime = 0; +export function useWaitForOrderPayment( + orderId: string, + current?: MerchantOrderStatusResponse, +) { + const { state, lib } = useSessionContext(); + + const token = state.status === "loggedIn" ? state.token : undefined; + const evictWhenPaid = useMemo(() => { + return async () => { + const DEFAULT_WAIT = 5000; + const now = new Date().getTime(); + const diff = now - latestRequestTime; + if (diff < DEFAULT_WAIT) { + console.log("PREVENT REQUEST BURST"); + await delayMs(DEFAULT_WAIT); + } + latestRequestTime = new Date().getTime(); + const result = await lib.instance.getOrderDetails(token, orderId, { + timeout: DEFAULT_WAIT, + }); + if (result.type === "ok" && result.body.order_status === "paid") { + await revalidateOrderDetails(); + } + return result; + }; + }, [token]); + + const fetcher = !current || current.order_status === "paid" ? undefined : evictWhenPaid; + + useAsyncWithRetry(fetcher, (r, err) => { + // halt if error + if (err !== undefined) return false; + // loading, just wait + if (r == undefined) return false; + // error, we don't know the kyc status + if (r.type === "fail") { + return false; + } + // order already paid, no need to retry + if (r.body.order_status === "paid") { + return false; + } + + return true; + }); +} + export interface InstanceOrderFilter { paid?: boolean; refunded?: boolean; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -50,6 +50,7 @@ import { import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; +import { useWaitForOrderPayment } from "../../../../hooks/order.js"; const TALER_SCREEN_ID = 44; @@ -264,6 +265,7 @@ function ClaimedPage({ id: string; order: TalerMerchantApi.CheckPaymentClaimedResponse; }) { + useWaitForOrderPayment(id, order); const now = new Date(); const refundable = order.contract_terms.refund_deadline.t_s !== "never" && @@ -769,6 +771,8 @@ function UnpaidPage({ const [value, valueHandler] = useState<Partial<Unpaid>>(order); const { i18n } = useTranslationContext(); const [settings] = usePreference(); + useWaitForOrderPayment(id, order); + return ( <div> <section class="hero is-hero-bar"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -25,7 +25,7 @@ import { TalerError, TalerErrorCode, TalerMerchantApi, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { LocalNotificationBannerBulma, @@ -41,14 +41,13 @@ import { useSessionContext } from "../../../../context/session.js"; import { InstanceOrderFilter, useInstanceOrders, - useOrderDetails, + useOrderDetails } from "../../../../hooks/order.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; import { RefundModal } from "./Table.js"; - const TALER_SCREEN_ID = 46; interface Props { @@ -100,18 +99,20 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { const isWiredActive = filter.wired === true ? "is-active" : ""; const isAllActive = filter.paid === undefined && - filter.refunded === undefined && - filter.wired === undefined + filter.refunded === undefined && + filter.wired === undefined ? "is-active" : ""; - const data = {} as TalerMerchantApi.RefundRequest + const data = {} as TalerMerchantApi.RefundRequest; const refund = safeFunctionHandler( lib.instance.addRefund.bind(lib.instance), - !session.token || !orderToBeRefunded ? undefined : [session.token, orderToBeRefunded.order_id, data], + !session.token || !orderToBeRefunded + ? undefined + : [session.token, orderToBeRefunded.order_id, data], ); refund.onSuccess = () => { - setOrderToBeRefunded(undefined) + setOrderToBeRefunded(undefined); }; refund.onFail = (fail) => { switch (fail.case) { @@ -179,10 +180,13 @@ interface RefundProps { onConfirmed: () => void; } -function RefundModalForTable({ id, onConfirmed, onCancel }: RefundProps): VNode { +function RefundModalForTable({ + id, + onConfirmed, + onCancel, +}: RefundProps): VNode { const { i18n } = useTranslationContext(); const result = useOrderDetails(id); - if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />;