taler-typescript-core

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

commit 247e16b9843a89e378cbb5e4c0ff7944671c6bb0
parent 406c8e1be44a2fd004372d668bf4f29717fce8da
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Sat, 18 Jul 2026 00:21:52 -0300

fix #11644

Diffstat:
Mpackages/libeufin-bank-webui/src/hooks/account.ts | 14++++++++------
Mpackages/taler-exchange-kyc-webui/src/hooks/kyc.ts | 29++++++++++++++++++++---------
Mpackages/taler-merchant-webui/src/Application.tsx | 52+++++++++++++++++++++++++++-------------------------
Mpackages/taler-merchant-webui/src/Routing.tsx | 1132++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/taler-merchant-webui/src/hooks/instance.ts | 42++++++++++++++++++++++++------------------
Mpackages/taler-util/src/http-client/bank-core.ts | 7++-----
Mpackages/web-util/src/hooks/useAsync.ts | 80+++++++++++++++++++++++++++++++++++++++----------------------------------------
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; }