commit 247e16b9843a89e378cbb5e4c0ff7944671c6bb0
parent 406c8e1be44a2fd004372d668bf4f29717fce8da
Author: Sebastian <sebasjm@taler-systems.com>
Date: Sat, 18 Jul 2026 00:21:52 -0300
fix #11644
Diffstat:
7 files changed, 693 insertions(+), 663 deletions(-)
diff --git a/packages/libeufin-bank-webui/src/hooks/account.ts b/packages/libeufin-bank-webui/src/hooks/account.ts
@@ -90,13 +90,14 @@ export function useWithdrawalDetails(wid: string | undefined) {
const result = useLongPolling(
async (ct, res): Promise<Res> => {
+ if (!wid) return undefined;
const old_state =
!res || res instanceof TalerError || res.type === "fail"
? undefined
: res.body.status;
- const result = await api.getWithdrawalById(wid!, {
+ const result = await api.getWithdrawalById(wid, {
old_state,
- timeoutMs: LONG_POLL_DELAY,
+ timeoutMs: !old_state ? undefined : LONG_POLL_DELAY,
ct,
});
return result;
@@ -106,10 +107,11 @@ export function useWithdrawalDetails(wid: string | undefined) {
deps: [wid],
shouldStop(res) {
return (
- res !== undefined &&
- !(res instanceof TalerError) &&
- res.type !== "fail" &&
- (res.body.status === "confirmed" || res.body.status === "aborted")
+ wid === undefined ||
+ (res !== undefined &&
+ !(res instanceof TalerError) &&
+ res.type !== "fail" &&
+ (res.body.status === "confirmed" || res.body.status === "aborted"))
);
},
},
diff --git a/packages/taler-exchange-kyc-webui/src/hooks/kyc.ts b/packages/taler-exchange-kyc-webui/src/hooks/kyc.ts
@@ -23,7 +23,7 @@ import { dummyHttpResponse } from "@gnu-taler/taler-util/http";
import {
LONG_POLL_DELAY,
useExchangeApiContext,
- useLongPolling
+ useLongPolling,
} from "@gnu-taler/web-util/browser";
import _useSWR, { mutate, SWRHook } from "swr";
const useSWR = _useSWR as unknown as SWRHook;
@@ -44,18 +44,28 @@ export function useKycInfo(token?: AccessToken) {
type Res = undefined | Awaited<ReturnType<typeof api.checkKycInfoSpa>>;
const result = useLongPolling(
async (ct, result): Promise<Res> => {
+ if (!token) return undefined;
const etag =
!result || result instanceof TalerError || result.type === "fail"
? undefined
: result.body.etag;
- const res = await api.checkKycInfoSpa(token!, etag, {
- timeoutMs: LONG_POLL_DELAY,
- });
+ const res = await api.checkKycInfoSpa(
+ token,
+ etag,
+ !etag
+ ? undefined
+ : {
+ timeoutMs: LONG_POLL_DELAY,
+ ct,
+ },
+ );
if (res.type === "fail" && res.case === HttpStatusCode.NotModified) {
if (!result || result instanceof TalerError || result.type === "fail") {
// this should not happen, if we get at 304 is because
// we used a etag
- throw Error("server replied 304 not modified but we dont have previous response.")
+ throw Error(
+ "server replied 304 not modified but we dont have previous response.",
+ );
}
return opFixedSuccess(dummyHttpResponse, result.body);
}
@@ -67,10 +77,11 @@ export function useKycInfo(token?: AccessToken) {
// if error keep trying
// stop when no requirements left
return (
- result !== undefined &&
- !(result instanceof TalerError) &&
- result.type !== "fail" &&
- result.body.requirements.length === 0
+ token === undefined ||
+ (result !== undefined &&
+ !(result instanceof TalerError) &&
+ result.type !== "fail" &&
+ result.body.requirements.length === 0)
);
},
},
diff --git a/packages/taler-merchant-webui/src/Application.tsx b/packages/taler-merchant-webui/src/Application.tsx
@@ -137,39 +137,41 @@ function SubApp({ baseUrl }: { baseUrl: string }) {
}}
>
<SWRConfig
- value={{
- provider: WITH_LOCAL_STORAGE_CACHE ? localStorageProvider : undefined,
- // normally, do not revalidate
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- revalidateIfStale: false,
- revalidateOnMount: undefined,
- focusThrottleInterval: undefined,
+ value={
+ {
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
- // normally, do not refresh
- refreshInterval: undefined,
- dedupingInterval: 2000,
- refreshWhenHidden: false,
- refreshWhenOffline: false,
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
- // ignore errors
- shouldRetryOnError: false,
- errorRetryCount: 0,
- errorRetryInterval: undefined,
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
- // do not go to loading again if already has data
- keepPreviousData: true,
- } as Partial<FullConfiguration>}
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ } as Partial<FullConfiguration>
+ }
>
<MerchantChallengeHandlerProvider>
<SessionContextProvider>
<TalerWalletIntegrationBrowserProvider>
<BrowserHashNavigationProvider>
- <CurrenciesProvider>
- <ToastBannerBulma />
- <SolveChallengeDialog />
- <Routing />
- </CurrenciesProvider>
+ <ToastBannerBulma />
+ <SolveChallengeDialog />
+ <Routing />
</BrowserHashNavigationProvider>
</TalerWalletIntegrationBrowserProvider>
</SessionContextProvider>
diff --git a/packages/taler-merchant-webui/src/Routing.tsx b/packages/taler-merchant-webui/src/Routing.tsx
@@ -37,7 +37,10 @@ import { Route, Router, route } from "preact-router";
import { useEffect } from "preact/hooks";
import { Loading } from "./components/exception/loading.js";
import { Menu, NotConnectedAppMenu } from "./components/menu/index.js";
-import { useCurrenciesContext } from "./context/currency.js";
+import {
+ CurrenciesProvider,
+ useCurrenciesContext,
+} from "./context/currency.js";
import { useSessionContext } from "./context/session.js";
import { useInstanceBankAccounts } from "./hooks/bank.js";
import { usePreference } from "./hooks/preference.js";
@@ -267,586 +270,597 @@ export function Routing(_p: Props): VNode {
if (shouldWarnAboutMissingBankAccounts) {
return (
<Fragment>
- <Menu />
- <Router history={history}>
- <Route path={InstancePaths.interface} component={Settings} />
- <Route
- default
- component={() => {
- return (
- <Fragment>
- <BankAccountBanner />
- <BankAccountCreatePage
- onConfirm={() => {
- route(InstancePaths.bank_list);
- }}
- />
- </Fragment>
- );
- }}
- />
- </Router>
+ <CurrenciesProvider>
+ <Menu />
+ <Router history={history}>
+ <Route path={InstancePaths.interface} component={Settings} />
+ <Route
+ default
+ component={() => {
+ return (
+ <Fragment>
+ <BankAccountBanner />
+ <BankAccountCreatePage
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ </Fragment>
+ );
+ }}
+ />
+ </Router>
+ </CurrenciesProvider>
</Fragment>
);
}
return (
<Fragment>
- <Menu />
- <KycBanner />
- <ToastBannerBulma />
-
- <Router history={history}>
- <Route path="/" component={Redirect} to={InstancePaths.order_list} />
- {/**
- * Admin pages
- */}
- {state.isAdmin && (
- <Route
- path={AdminPaths.list_instances}
- component={InstanceListPage}
- onChangePassword={(id: string): void => {
- route(`/instance/${id}/token`);
+ <CurrenciesProvider>
+ <Menu />
+ <KycBanner />
+ <ToastBannerBulma />
+
+ <Router history={history}>
+ <Route path="/" component={Redirect} to={InstancePaths.order_list} />
+ {/**
+ * Admin pages
+ */}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.list_instances}
+ component={InstanceListPage}
+ onChangePassword={(id: string): void => {
+ route(`/instance/${id}/token`);
+ }}
+ onCreate={() => {
+ route(AdminPaths.new_instance);
+ }}
+ onUpdate={(id: string): void => {
+ route(`/instance/${id}/update`);
+ }}
+ />
+ )}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.new_instance}
+ component={InstanceCreatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(AdminPaths.list_instances);
+ }}
+ />
+ )}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.update_instance}
+ component={AdminInstanceUpdatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(AdminPaths.list_instances);
+ }}
+ />
+ )}
+ {state.isAdmin && (
+ <Route
+ path={AdminPaths.update_instance_auth}
+ component={AdminInstanceUpdateTokenPage}
+ onCancel={() => route(AdminPaths.list_instances)}
+ onChange={() => {
+ route(AdminPaths.list_instances);
+ }}
+ />
+ )}
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.settings}
+ component={InstanceUpdatePage}
+ onBack={() => {
+ route(`/`);
}}
+ onConfirm={() => {}}
+ />
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.password}
+ component={PasswordPage}
+ onChange={() => {
+ route(`/`);
+ }}
+ onCancel={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ {/**
+ * Money pots pages
+ */}
+ <Route
+ path={InstancePaths.pots_list}
+ component={ListMoneyPots}
onCreate={() => {
- route(AdminPaths.new_instance);
+ route(InstancePaths.pots_new);
}}
- onUpdate={(id: string): void => {
- route(`/instance/${id}/update`);
+ onSelect={(id: string) => {
+ route(InstancePaths.pots_update.replace(":cid", id));
}}
/>
- )}
- {state.isAdmin && (
<Route
- path={AdminPaths.new_instance}
- component={InstanceCreatePage}
- onBack={() => route(AdminPaths.list_instances)}
+ path={InstancePaths.pots_update}
+ component={UpdateMoneyPots}
onConfirm={() => {
- route(AdminPaths.list_instances);
+ route(InstancePaths.pots_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.pots_list);
}}
/>
- )}
- {state.isAdmin && (
<Route
- path={AdminPaths.update_instance}
- component={AdminInstanceUpdatePage}
- onBack={() => route(AdminPaths.list_instances)}
+ path={InstancePaths.pots_new}
+ component={CreateMoneyPots}
onConfirm={() => {
- route(AdminPaths.list_instances);
+ route(InstancePaths.pots_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.pots_list);
}}
/>
- )}
- {state.isAdmin && (
+ {/**
+ * Product group pages
+ */}
<Route
- path={AdminPaths.update_instance_auth}
- component={AdminInstanceUpdateTokenPage}
- onCancel={() => route(AdminPaths.list_instances)}
- onChange={() => {
- route(AdminPaths.list_instances);
- }}
- />
- )}
- {/**
- * Update instance page
- */}
- <Route
- path={InstancePaths.settings}
- component={InstanceUpdatePage}
- onBack={() => {
- route(`/`);
- }}
- onConfirm={() => {}}
- />
- {/**
- * Update instance page
- */}
- <Route
- path={InstancePaths.password}
- component={PasswordPage}
- onChange={() => {
- route(`/`);
- }}
- onCancel={() => {
- route(InstancePaths.order_list);
- }}
- />
- {/**
- * Money pots pages
- */}
- <Route
- path={InstancePaths.pots_list}
- component={ListMoneyPots}
- onCreate={() => {
- route(InstancePaths.pots_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.pots_update.replace(":cid", id));
- }}
- />
- <Route
- path={InstancePaths.pots_update}
- component={UpdateMoneyPots}
- onConfirm={() => {
- route(InstancePaths.pots_list);
- }}
- onBack={() => {
- route(InstancePaths.pots_list);
- }}
- />
- <Route
- path={InstancePaths.pots_new}
- component={CreateMoneyPots}
- onConfirm={() => {
- route(InstancePaths.pots_list);
- }}
- onBack={() => {
- route(InstancePaths.pots_list);
- }}
- />
- {/**
- * Product group pages
- */}
- <Route
- path={InstancePaths.groups_list}
- component={ListProductGroups}
- onCreate={() => {
- route(InstancePaths.groups_new);
- }}
- />
- <Route
- path={InstancePaths.groups_new}
- component={CreateProductGroup}
- onConfirm={() => {
- route(InstancePaths.groups_list);
- }}
- onBack={() => {
- route(InstancePaths.groups_list);
- }}
- />
- {/**
- * Scheduled report pages
- */}
- <Route path={InstancePaths.exchanges_list} component={ListExchanges} />
- {/**
- * Scheduled report pages
- */}
- <Route
- path={InstancePaths.reports_list}
- component={ListScheduledReport}
- onCreate={() => {
- route(InstancePaths.reports_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.reports_update.replace(":cid", id));
- }}
- />
- <Route
- path={InstancePaths.reports_update}
- component={UpdateScheduledReport}
- onConfirm={() => {
- route(InstancePaths.reports_list);
- }}
- onBack={() => {
- route(InstancePaths.reports_list);
- }}
- />
- <Route
- path={InstancePaths.reports_new}
- component={CreateScheduledReport}
- onConfirm={() => {
- route(InstancePaths.reports_list);
- }}
- onBack={() => {
- route(InstancePaths.reports_list);
- }}
- />
- {/**
- * Category pages
- */}
- <Route
- path={InstancePaths.category_list}
- component={ListCategories}
- onCreate={() => {
- route(InstancePaths.category_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.category_update.replace(":cid", id));
- }}
- />
- <Route
- path={InstancePaths.category_update}
- component={UpdateCategory}
- onConfirm={() => {
- route(InstancePaths.category_list);
- }}
- onBack={() => {
- route(InstancePaths.category_list);
- }}
- />
- <Route
- path={InstancePaths.category_new}
- component={CreateCategory}
- onConfirm={() => {
- route(InstancePaths.category_list);
- }}
- onBack={() => {
- route(InstancePaths.category_list);
- }}
- />
- {/**
- * Inventory pages
- */}
- <Route
- path={InstancePaths.inventory_list}
- component={ProductListPage}
- onCreate={() => {
- route(InstancePaths.inventory_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.inventory_update.replace(":pid", id));
- }}
- />
- <Route
- path={InstancePaths.inventory_update}
- component={ProductUpdatePage}
- onConfirm={() => {
- route(InstancePaths.inventory_list);
- }}
- onBack={() => {
- route(InstancePaths.inventory_list);
- }}
- />
- <Route
- path={InstancePaths.inventory_new}
- component={ProductCreatePage}
- onConfirm={() => {
- route(InstancePaths.inventory_list);
- }}
- onBack={() => {
- route(InstancePaths.inventory_list);
- }}
- />
- {/**
- * Bank pages
- */}
- <Route
- path={InstancePaths.bank_list}
- component={BankAccountListPage}
- onCreate={() => {
- route(InstancePaths.bank_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.bank_update.replace(":bid", id));
- }}
- />
- <Route
- path={InstancePaths.bank_update}
- component={BankAccountUpdatePage}
- onConfirm={() => {
- route(InstancePaths.bank_list);
- }}
- onBack={() => {
- route(InstancePaths.bank_list);
- }}
- />
- <Route
- path={InstancePaths.bank_new}
- component={BankAccountCreatePage}
- onConfirm={(id: string) => {
- route(InstancePaths.bank_update.replace(":bid", id));
- }}
- onBack={() => {
- route(InstancePaths.bank_list);
- }}
- />
- {/**
- * Sessions pages
- */}
- <Route
- path={InstancePaths.access_token_list}
- component={AccessTokenListPage}
- onCreate={() => {
- route(InstancePaths.access_token_new);
- }}
- />
- <Route
- path={InstancePaths.access_token_new}
- component={AccessTokenCreatePage}
- onConfirm={() => {
- route(InstancePaths.access_token_list);
- }}
- onBack={() => {
- route(InstancePaths.access_token_list);
- }}
- />
- <Route
- path={InstancePaths.access_token_new_pos}
- component={PosTokenCreatePage}
- onConfirm={() => {
- route(InstancePaths.access_token_list);
- }}
- onBack={() => {
- route(InstancePaths.access_token_list);
- }}
- />
- {/**
- * Order pages
- */}
- <Route
- path={InstancePaths.order_list}
- component={OrderListPage}
- section={OrderListSection.NEW}
- onCreate={() => {
- route(InstancePaths.order_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.order_details.replace(":oid", id));
- }}
- onChangeSection={(s?: OrderListSection) => {
- if (!s) {
+ path={InstancePaths.groups_list}
+ component={ListProductGroups}
+ onCreate={() => {
+ route(InstancePaths.groups_new);
+ }}
+ />
+ <Route
+ path={InstancePaths.groups_new}
+ component={CreateProductGroup}
+ onConfirm={() => {
+ route(InstancePaths.groups_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.groups_list);
+ }}
+ />
+ {/**
+ * Scheduled report pages
+ */}
+ <Route
+ path={InstancePaths.exchanges_list}
+ component={ListExchanges}
+ />
+ {/**
+ * Scheduled report pages
+ */}
+ <Route
+ path={InstancePaths.reports_list}
+ component={ListScheduledReport}
+ onCreate={() => {
+ route(InstancePaths.reports_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.reports_update.replace(":cid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.reports_update}
+ component={UpdateScheduledReport}
+ onConfirm={() => {
+ route(InstancePaths.reports_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.reports_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.reports_new}
+ component={CreateScheduledReport}
+ onConfirm={() => {
+ route(InstancePaths.reports_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.reports_list);
+ }}
+ />
+ {/**
+ * Category pages
+ */}
+ <Route
+ path={InstancePaths.category_list}
+ component={ListCategories}
+ onCreate={() => {
+ route(InstancePaths.category_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.category_update.replace(":cid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.category_update}
+ component={UpdateCategory}
+ onConfirm={() => {
+ route(InstancePaths.category_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.category_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.category_new}
+ component={CreateCategory}
+ onConfirm={() => {
+ route(InstancePaths.category_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.category_list);
+ }}
+ />
+ {/**
+ * Inventory pages
+ */}
+ <Route
+ path={InstancePaths.inventory_list}
+ component={ProductListPage}
+ onCreate={() => {
+ route(InstancePaths.inventory_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.inventory_update.replace(":pid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.inventory_update}
+ component={ProductUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.inventory_new}
+ component={ProductCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ />
+ {/**
+ * Bank pages
+ */}
+ <Route
+ path={InstancePaths.bank_list}
+ component={BankAccountListPage}
+ onCreate={() => {
+ route(InstancePaths.bank_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.bank_update.replace(":bid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.bank_update}
+ component={BankAccountUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.bank_new}
+ component={BankAccountCreatePage}
+ onConfirm={(id: string) => {
+ route(InstancePaths.bank_update.replace(":bid", id));
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ {/**
+ * Sessions pages
+ */}
+ <Route
+ path={InstancePaths.access_token_list}
+ component={AccessTokenListPage}
+ onCreate={() => {
+ route(InstancePaths.access_token_new);
+ }}
+ />
+ <Route
+ path={InstancePaths.access_token_new}
+ component={AccessTokenCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.access_token_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.access_token_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.access_token_new_pos}
+ component={PosTokenCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.access_token_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.access_token_list);
+ }}
+ />
+ {/**
+ * Order pages
+ */}
+ <Route
+ path={InstancePaths.order_list}
+ component={OrderListPage}
+ section={OrderListSection.NEW}
+ onCreate={() => {
+ route(InstancePaths.order_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onChangeSection={(s?: OrderListSection) => {
+ if (!s) {
+ route(InstancePaths.order_list);
+ } else {
+ route(InstancePaths.order_list_section.replace(":section", s));
+ }
+ }}
+ />
+ {/**
+ * Order pages
+ */}
+ <Route
+ path={InstancePaths.order_list_section}
+ component={OrderListPage}
+ onCreate={() => {
+ route(InstancePaths.order_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onChangeSection={(s?: OrderListSection) => {
+ if (!s) {
+ route(InstancePaths.order_list);
+ } else {
+ route(InstancePaths.order_list_section.replace(":section", s));
+ }
+ }}
+ />
+ <Route
+ path={InstancePaths.order_details}
+ component={OrderDetailsPage}
+ onBack={() => {
route(InstancePaths.order_list);
- } else {
- route(InstancePaths.order_list_section.replace(":section", s));
- }
- }}
- />
- {/**
- * Order pages
- */}
- <Route
- path={InstancePaths.order_list_section}
- component={OrderListPage}
- onCreate={() => {
- route(InstancePaths.order_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.order_details.replace(":oid", id));
- }}
- onChangeSection={(s?: OrderListSection) => {
- if (!s) {
+ }}
+ onSelectWireTransfer={(id) => {
+ route(
+ InstancePaths.transfers_details.replace(":tid", String(id)),
+ );
+ }}
+ />
+ <Route
+ path={InstancePaths.order_new}
+ component={OrderCreatePage}
+ onConfirm={(orderId: string) => {
+ route(InstancePaths.order_details.replace(":oid", orderId));
+ }}
+ onBack={() => {
route(InstancePaths.order_list);
- } else {
- route(InstancePaths.order_list_section.replace(":section", s));
- }
- }}
- />
- <Route
- path={InstancePaths.order_details}
- component={OrderDetailsPage}
- onBack={() => {
- route(InstancePaths.order_list);
- }}
- onSelectWireTransfer={(id) => {
- route(InstancePaths.transfers_details.replace(":tid", String(id)));
- }}
- />
- <Route
- path={InstancePaths.order_new}
- component={OrderCreatePage}
- onConfirm={(orderId: string) => {
- route(InstancePaths.order_details.replace(":oid", orderId));
- }}
- onBack={() => {
- route(InstancePaths.order_list);
- }}
- />
- {/**
- * Transfer pages
- */}
- <Route
- path={InstancePaths.transfers_list}
- component={TransferListPage}
- onTransferDetails={(id) => {
- route(InstancePaths.transfers_details.replace(":tid", String(id)));
- }}
- />
- <Route
- path={InstancePaths.transfers_details}
- component={TransferDetailPage}
- onOrderDetails={(orderId) => {
- route(InstancePaths.order_details.replace(":oid", orderId));
- }}
- onBack={() => {
- route(InstancePaths.transfers_list);
- }}
- />
- {/* *
- * Token family pages
- */}
- <Route
- path={InstancePaths.token_family_list}
- component={TokenFamilyListPage}
- onCreate={() => {
- route(InstancePaths.token_family_new);
- }}
- onSelect={(slug: string) => {
- route(InstancePaths.token_family_update.replace(":slug", slug));
- }}
- />
- <Route
- path={InstancePaths.token_family_update}
- component={TokenFamilyUpdatePage}
- onConfirm={() => {
- route(InstancePaths.token_family_list);
- }}
- onBack={() => {
- route(InstancePaths.token_family_list);
- }}
- />
- <Route
- path={InstancePaths.token_family_new}
- component={TokenFamilyCreatePage}
- onConfirm={() => {
- route(InstancePaths.token_family_list);
- }}
- onBack={() => {
- route(InstancePaths.token_family_list);
- }}
- />
- {/**
- * Webhooks pages
- */}
- <Route
- path={InstancePaths.webhooks_list}
- component={WebhookListPage}
- onCreate={() => {
- route(InstancePaths.webhooks_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.webhooks_update.replace(":tid", id));
- }}
- />
- <Route
- path={InstancePaths.webhooks_update}
- component={WebhookUpdatePage}
- onConfirm={() => {
- route(InstancePaths.webhooks_list);
- }}
- onBack={() => {
- route(InstancePaths.webhooks_list);
- }}
- />
- <Route
- path={InstancePaths.webhooks_new}
- component={WebhookCreatePage}
- onConfirm={() => {
- route(InstancePaths.webhooks_list);
- }}
- onBack={() => {
- route(InstancePaths.webhooks_list);
- }}
- />
- {/**
- * Validator pages
- */}
- <Route
- path={InstancePaths.otp_devices_list}
- component={ValidatorListPage}
- onCreate={() => {
- route(InstancePaths.otp_devices_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.otp_devices_update.replace(":vid", id));
- }}
- />
- <Route
- path={InstancePaths.otp_devices_update}
- component={ValidatorUpdatePage}
- onConfirm={() => {
- route(InstancePaths.otp_devices_list);
- }}
- onBack={() => {
- route(InstancePaths.otp_devices_list);
- }}
- />
- <Route
- path={InstancePaths.otp_devices_new}
- component={ValidatorCreatePage}
- onConfirm={() => {
- route(InstancePaths.otp_devices_list);
- }}
- onBack={() => {
- route(InstancePaths.otp_devices_list);
- }}
- />
- {/**
- * Templates pages
- */}
- <Route
- path={InstancePaths.templates_list}
- component={TemplateListPage}
- onCreate={() => {
- route(InstancePaths.templates_new);
- }}
- onNewOrder={(id: string) => {
- route(InstancePaths.templates_use.replace(":tid", id));
- }}
- onQR={(id: string) => {
- route(InstancePaths.templates_qr.replace(":tid", id));
- }}
- onSelect={(id: string) => {
- route(InstancePaths.templates_update.replace(":tid", id));
- }}
- />
- <Route
- path={InstancePaths.templates_update}
- component={TemplateUpdatePage}
- onConfirm={() => {
- route(InstancePaths.templates_list);
- }}
- onBack={() => {
- route(InstancePaths.templates_list);
- }}
- />
- <Route
- path={InstancePaths.templates_new}
- component={TemplateCreatePage}
- onConfirm={() => {
- route(InstancePaths.templates_list);
- }}
- onBack={() => {
- route(InstancePaths.templates_list);
- }}
- />
- <Route
- path={InstancePaths.templates_use}
- component={TemplateUsePage}
- onOrderCreated={(id: string) => {
- route(InstancePaths.order_details.replace(":oid", id));
- }}
- onBack={() => {
- route(InstancePaths.templates_list);
- }}
- />
- <Route
- path={InstancePaths.templates_qr}
- component={TemplateQrPage}
- onBack={() => {
- route(InstancePaths.templates_list);
- }}
- onNewOrder={(id:string)=>{
- route(InstancePaths.order_details.replace(":oid", id));
- }}
- />
-
- <Route
- path={InstancePaths.kyc}
- component={ListKYCPage}
- // onSelect={(id: string) => {
- // route(InstancePaths.bank_update.replace(":bid", id));
- // }}
- />
- <Route path={InstancePaths.interface} component={Settings} />
- {/**
- * Statistics pages
- */}
- <Route path={InstancePaths.statistics} component={Statistics} />
- {/**
- * Example pages
- */}
- <Route path="/loading" component={Loading} />
- <Route default component={Redirect} to={InstancePaths.order_list} />
- </Router>
+ }}
+ />
+ {/**
+ * Transfer pages
+ */}
+ <Route
+ path={InstancePaths.transfers_list}
+ component={TransferListPage}
+ onTransferDetails={(id) => {
+ route(
+ InstancePaths.transfers_details.replace(":tid", String(id)),
+ );
+ }}
+ />
+ <Route
+ path={InstancePaths.transfers_details}
+ component={TransferDetailPage}
+ onOrderDetails={(orderId) => {
+ route(InstancePaths.order_details.replace(":oid", orderId));
+ }}
+ onBack={() => {
+ route(InstancePaths.transfers_list);
+ }}
+ />
+ {/* *
+ * Token family pages
+ */}
+ <Route
+ path={InstancePaths.token_family_list}
+ component={TokenFamilyListPage}
+ onCreate={() => {
+ route(InstancePaths.token_family_new);
+ }}
+ onSelect={(slug: string) => {
+ route(InstancePaths.token_family_update.replace(":slug", slug));
+ }}
+ />
+ <Route
+ path={InstancePaths.token_family_update}
+ component={TokenFamilyUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.token_family_new}
+ component={TokenFamilyCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ />
+ {/**
+ * Webhooks pages
+ */}
+ <Route
+ path={InstancePaths.webhooks_list}
+ component={WebhookListPage}
+ onCreate={() => {
+ route(InstancePaths.webhooks_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.webhooks_update.replace(":tid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.webhooks_update}
+ component={WebhookUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.webhooks_new}
+ component={WebhookCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ />
+ {/**
+ * Validator pages
+ */}
+ <Route
+ path={InstancePaths.otp_devices_list}
+ component={ValidatorListPage}
+ onCreate={() => {
+ route(InstancePaths.otp_devices_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.otp_devices_update.replace(":vid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.otp_devices_update}
+ component={ValidatorUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.otp_devices_new}
+ component={ValidatorCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ />
+ {/**
+ * Templates pages
+ */}
+ <Route
+ path={InstancePaths.templates_list}
+ component={TemplateListPage}
+ onCreate={() => {
+ route(InstancePaths.templates_new);
+ }}
+ onNewOrder={(id: string) => {
+ route(InstancePaths.templates_use.replace(":tid", id));
+ }}
+ onQR={(id: string) => {
+ route(InstancePaths.templates_qr.replace(":tid", id));
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.templates_update.replace(":tid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_update}
+ component={TemplateUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_new}
+ component={TemplateCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_use}
+ component={TemplateUsePage}
+ onOrderCreated={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_qr}
+ component={TemplateQrPage}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onNewOrder={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ />
+
+ <Route
+ path={InstancePaths.kyc}
+ component={ListKYCPage}
+ // onSelect={(id: string) => {
+ // route(InstancePaths.bank_update.replace(":bid", id));
+ // }}
+ />
+ <Route path={InstancePaths.interface} component={Settings} />
+ {/**
+ * Statistics pages
+ */}
+ <Route path={InstancePaths.statistics} component={Statistics} />
+ {/**
+ * Example pages
+ */}
+ <Route path="/loading" component={Loading} />
+ <Route default component={Redirect} to={InstancePaths.order_list} />
+ </Router>
+ </CurrenciesProvider>
</Fragment>
);
}
diff --git a/packages/taler-merchant-webui/src/hooks/instance.ts b/packages/taler-merchant-webui/src/hooks/instance.ts
@@ -16,17 +16,12 @@
import {
AccessToken,
- isOperationFail,
- MerchantAccountKycRedirectsResponse,
- Result,
- succeedOrThrow,
TalerHttpError,
- TalerMerchantManagementResultByMethod,
+ TalerMerchantManagementResultByMethod
} from "@gnu-taler/taler-util";
import {
- delayMs,
LONG_POLL_DELAY,
- useLongPolling,
+ useLongPolling
} from "@gnu-taler/web-util/browser";
import { useSessionContext } from "../context/session.js";
@@ -74,24 +69,35 @@ export function useInstanceKYCDetailsLongPolling() {
const { state, lib } = useSessionContext();
const token = state.token;
- type Res = Awaited<ReturnType<typeof lib.instance.getCurrentInstanceKycStatus>>;
+ type Res =
+ | undefined
+ | Awaited<ReturnType<typeof lib.instance.getCurrentInstanceKycStatus>>;
const result = useLongPolling(
- async (ct, latestData): Promise<Res> => {
- const etag = latestData?.type === "ok" ? latestData.body.etag : undefined;
- const r = await lib.instance.getCurrentInstanceKycStatus(token!, {
- longpoll: etag
+ async (cancellationToken, latestData): Promise<Res> => {
+ if (!token) return undefined;
+
+ const longpoll =
+ latestData?.type === "ok"
? {
- type: "state-change",
- etag: etag,
+ type: "state-change" as const,
+ etag: latestData.body.etag!,
timeout: LONG_POLL_DELAY,
}
- : undefined,
- cancellationToken: ct,
+ : undefined;
+
+ return await lib.instance.getCurrentInstanceKycStatus(token, {
+ longpoll,
+ cancellationToken,
});
- return r;
},
- { minTime: LONG_POLL_DELAY },
+ {
+ minTime: LONG_POLL_DELAY,
+ deps: [token],
+ shouldStop(res) {
+ return token === undefined;
+ },
+ },
);
return result;
}
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
@@ -908,11 +908,8 @@ export class TalerCoreBankHttpClient {
) {
const url = new URL(`withdrawals/${wid}`, this.baseUrl);
addLongPollingParam(url, params);
- if (params) {
- url.searchParams.set(
- "old_state",
- !params.old_state ? "pending" : params.old_state,
- );
+ if (params?.old_state) {
+ url.searchParams.set("old_state", params.old_state);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
diff --git a/packages/web-util/src/hooks/useAsync.ts b/packages/web-util/src/hooks/useAsync.ts
@@ -58,7 +58,7 @@ export function useAsync<Res>(
}
export const LONG_POLL_DELAY = 15000;
-const emptyArray: unknown[] = []
+const emptyArray: unknown[] = [];
/**
* First start with `initial` value, if initial is undefined then finish.
* Otherwise:
@@ -96,7 +96,7 @@ export function useLongPolling<Res>(
/**
* Check before any request if loop should stop retrying. Default always false.
*/
- shouldStop?: (res: Res) => boolean;
+ shouldStop?: (res: undefined | Res) => boolean;
/**
* Dependency array to re-evaluate the state. Default empty.
*/
@@ -111,7 +111,7 @@ export function useLongPolling<Res>(
} = {},
): Res | undefined {
const minTime = opts?.minTime ?? LONG_POLL_DELAY;
- const dependencies = (opts?.deps ?? emptyArray)
+ const dependencies = opts?.deps ?? emptyArray;
const [result, setResult] = useState<{
counter: number;
prevResult: Res | undefined;
@@ -122,15 +122,40 @@ export function useLongPolling<Res>(
function retry(prevResult: Res) {
setResult((prev) => ({ counter: prev.counter + 1, prevResult }));
}
- useEffect(
- () => {
- setResult((prev) => ({
- counter: prev.counter,
- prevResult: opts.initial,
- }));
- },
- [opts.initial, ...dependencies],
- );
+ /**
+ * New initial value, just keep result updated
+ * but don't automatically trigger new request
+ */
+ useEffect(() => {
+ setResult((prev) => ({
+ counter: prev.counter,
+ prevResult: opts.initial,
+ }));
+ }, [opts.initial]);
+
+ /**
+ * the resultset is not needed anymore
+ */
+ useEffect(() => {
+ return () => {
+ console.log("unload");
+ ct.current.unloaded = true;
+ ct.current.startMs = 0;
+ };
+ }, emptyArray);
+
+ /**
+ * request dependency changed
+ * different resultset expected
+ * cancel current request and retry ASAP
+ */
+ useEffect(() => {
+ return () => {
+ console.log("dep unload");
+ ct.current.ct?.cancel("long-polling-stop");
+ ct.current.startMs = 0;
+ };
+ }, dependencies);
const ct = useRef<{
ct: CancellationToken.Source | undefined;
@@ -142,11 +167,7 @@ export function useLongPolling<Res>(
const tk = CancellationToken.create();
ct.current.ct = tk;
- const doWeRetry =
- !opts.shouldStop || !result.prevResult
- ? true
- : !opts.shouldStop(result.prevResult);
- if (!doWeRetry) return;
+ if (opts.shouldStop && opts.shouldStop(result.prevResult)) return;
const diff = new Date().getTime() - ct.current.startMs;
const instant = Promise.resolve();
@@ -168,30 +189,7 @@ export function useLongPolling<Res>(
})
.catch((error) => console.log("ERROR", error));
});
- return () => {};
- }, [result.counter]);
-
- /**
- * the resultset is not needed anymore
- */
- useEffect(() => {
- return () => {
- ct.current.unloaded = true;
- ct.current.startMs = 0;
- };
- }, emptyArray);
-
- /**
- * request dependency changed
- * different resultset expected
- */
- useEffect(() => {
- return () => {
- ct.current.ct?.cancel();
- ct.current.unloaded = true;
- ct.current.startMs = 0;
- };
- }, dependencies);
+ }, [result.counter, ...dependencies]);
return result.prevResult;
}