taler-typescript-core

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

commit 9e01cb32c4585acb31c1a03ca2d09dfd02660da4
parent 38954ca8a3736edaada01b67b456ff72728c167d
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Fri, 10 Jul 2026 12:48:18 -0300

fix #11623

Diffstat:
Mpackages/taler-merchant-webui/src/Routing.tsx | 24++++++++----------------
Mpackages/taler-merchant-webui/src/components/menu/SideBar.tsx | 50+++++++++++++++++++++++++-------------------------
Mpackages/taler-merchant-webui/src/context/currency.ts | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mpackages/taler-merchant-webui/src/hooks/instance.ts | 80++-----------------------------------------------------------------------------
Mpackages/taler-merchant-webui/src/paths/instance/accounts/list/index.tsx | 12+++++-------
Mpackages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx | 5++---
Mpackages/web-util/src/hooks/useAsync.ts | 34+++++++++++++++-------------------
7 files changed, 166 insertions(+), 179 deletions(-)

diff --git a/packages/taler-merchant-webui/src/Routing.tsx b/packages/taler-merchant-webui/src/Routing.tsx @@ -37,9 +37,9 @@ 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 { useSessionContext } from "./context/session.js"; import { useInstanceBankAccounts } from "./hooks/bank.js"; -import { useInstanceKYCDetails } from "./hooks/instance.js"; import { usePreference } from "./hooks/preference.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; @@ -179,7 +179,7 @@ export enum AdminPaths { update_instance_auth = "/instance/:id/token", } -export interface Props {} +export interface Props { } export const privatePages = { home: urlPattern(/\/home/, () => "#/home"), @@ -355,7 +355,7 @@ export function Routing(_p: Props): VNode { onBack={() => { route(`/`); }} - onConfirm={() => {}} + onConfirm={() => { }} /> {/** * Update instance page @@ -829,9 +829,9 @@ export function Routing(_p: Props): VNode { <Route path={InstancePaths.kyc} component={ListKYCPage} - // onSelect={(id: string) => { - // route(InstancePaths.bank_update.replace(":bid", id)); - // }} + // onSelect={(id: string) => { + // route(InstancePaths.bank_update.replace(":bid", id)); + // }} /> <Route path={InstancePaths.interface} component={Settings} /> {/** @@ -914,22 +914,14 @@ function BankAccountBanner(): VNode { } function KycBanner(): VNode { - const kycStatus = useInstanceKYCDetails(); + const { kycStatus } = useCurrenciesContext() const { i18n } = useTranslationContext(); - // const today = format(new Date(), dateFormatForSettings(settings)); const [prefs, updatePref] = usePreference(); const now = AbsoluteTime.now(); - const needsToBeShown = - kycStatus !== undefined && - !(kycStatus instanceof TalerError) && - kycStatus.type === "ok" && - kycStatus.body.kyc_data.findIndex( - (d) => d.payto_kycauths !== undefined || d.access_token !== undefined, - ) !== -1; const hidden = AbsoluteTime.cmp(now, prefs.hideKycUntil) < 1; - if (hidden || !needsToBeShown) return <Fragment />; + if (hidden || !kycStatus.verificationRequired) return <Fragment />; const oneDay = { d_ms: 1000 * 60 * 60 * 24 }; const tomorrow = AbsoluteTime.addDuration(now, oneDay); diff --git a/packages/taler-merchant-webui/src/components/menu/SideBar.tsx b/packages/taler-merchant-webui/src/components/menu/SideBar.tsx @@ -28,7 +28,6 @@ import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useCurrenciesContext } from "../../context/currency.js"; import { useSessionContext } from "../../context/session.js"; import { useSettingsContext } from "../../context/settings.js"; -import { useInstanceKYCSimplifiedWorstStatusLongPolling } from "../../hooks/instance.js"; import { UIElement, usePreference } from "../../hooks/preference.js"; import { LangSelector } from "./LangSelector.js"; @@ -43,19 +42,20 @@ interface Props { export function Sidebar({ mobile }: Props): VNode { const { i18n } = useTranslationContext(); const { state, logOut, config } = useSessionContext(); - const worstKycStatus = useInstanceKYCSimplifiedWorstStatusLongPolling(); const [{ persona }] = usePreference(); - const hideKycMenuItem = - worstKycStatus === MerchantAccountKycStatusSimplified.OK && - persona !== "expert"; const isLoggedIn = state.status === "loggedIn"; const hasToken = isLoggedIn && state.token !== undefined; const { isTestingEnvironment } = useSettingsContext(); - const { list: cList, switchCurrency, currency } = useCurrenciesContext(); + const { list: cList, switchCurrency, currency, kycStatus } = useCurrenciesContext(); + + const hideKycMenuItem = + kycStatus.worst === MerchantAccountKycStatusSimplified.OK && + persona !== "expert"; + const hasMultiCurrency = cList.length > 1; return ( @@ -246,14 +246,14 @@ export function Sidebar({ mobile }: Props): VNode { htmlElement="li" point={UIElement.sidebar_kycStatus} class={ - worstKycStatus === - MerchantAccountKycStatusSimplified.WARNING + kycStatus.worst === + MerchantAccountKycStatusSimplified.WARNING ? "is-warning" - : worstKycStatus === - MerchantAccountKycStatusSimplified.ERROR + : kycStatus.worst === + MerchantAccountKycStatusSimplified.ERROR ? "is-error" - : worstKycStatus === - MerchantAccountKycStatusSimplified.ACTION_REQUIRED + : kycStatus.worst === + MerchantAccountKycStatusSimplified.ACTION_REQUIRED ? "is-warning" : undefined } @@ -262,24 +262,24 @@ export function Sidebar({ mobile }: Props): VNode { href={"#/kyc"} class="has-icon" style={ - worstKycStatus === - MerchantAccountKycStatusSimplified.WARNING + kycStatus.worst === + MerchantAccountKycStatusSimplified.WARNING ? { - backgroundColor: "darkorange", + backgroundColor: "darkorange", + color: "black", + } + : kycStatus.worst === + MerchantAccountKycStatusSimplified.ERROR + ? { + backgroundColor: "#e93c3c", color: "black", } - : worstKycStatus === - MerchantAccountKycStatusSimplified.ERROR - ? { - backgroundColor: "#e93c3c", + : kycStatus.worst === + MerchantAccountKycStatusSimplified.ACTION_REQUIRED + ? { + backgroundColor: "darkorange", color: "black", } - : worstKycStatus === - MerchantAccountKycStatusSimplified.ACTION_REQUIRED - ? { - backgroundColor: "darkorange", - color: "black", - } : undefined } > diff --git a/packages/taler-merchant-webui/src/context/currency.ts b/packages/taler-merchant-webui/src/context/currency.ts @@ -16,17 +16,18 @@ import { CurrencySpecification, + getMerchantAccountKycStatusSimplified, MerchantAccountKycStatus, + MerchantAccountKycStatusSimplified, TalerError, } from "@gnu-taler/taler-util"; import { ComponentChildren, createContext, h, VNode } from "preact"; -import { useContext } from "preact/hooks"; -import { useSessionContext } from "./session.js"; -import { useState } from "preact/hooks"; +import { useContext, useState } from "preact/hooks"; import { - useAvailableCurrencies, useInstanceKYCDetails, + useInstanceKYCDetailsLongPolling } from "../hooks/instance.js"; +import { useSessionContext } from "./session.js"; /** * @@ -35,19 +36,34 @@ import { type CurrencyWithId = undefined | (CurrencySpecification & { id: string }); export type Type = { list: string[]; + kycStatus: KycStatusMap; currency: CurrencyWithId; + kycResult: ReturnType<typeof useInstanceKYCDetails>; switchCurrency: (id: string) => void; }; const initial: Type = { list: [], + kycResult: undefined, + kycStatus: { + byAccount: {}, + verificationRequired: false, + }, currency: undefined, - switchCurrency(id) {}, + switchCurrency(id) { }, }; const Context = createContext<Type>(initial); export const useCurrenciesContext = (): Type => useContext(Context); + +export type KycStatusMap = { + best?: MerchantAccountKycStatusSimplified, + worst?: MerchantAccountKycStatusSimplified, + byAccount: { [payto: string]: MerchantAccountKycStatusSimplified }, + verificationRequired: boolean; +} + export const CurrenciesProvider = ({ children, }: { @@ -58,41 +74,60 @@ export const CurrenciesProvider = ({ undefined | (CurrencySpecification & { id: string }) >(); - const kyc = useInstanceKYCDetails(); - - const status = - kyc instanceof TalerError || !kyc || kyc.type === "fail" - ? undefined - : kyc.body; - - const readyToBeUsed = !status - ? [] - : Object.keys( - status.kyc_data.reduce( - (prev, st) => { - if ( - st.exchange_currency && - st.status === MerchantAccountKycStatus.READY - ) { - prev[st.exchange_currency] = true; - } - return prev; - }, - {} as { [s: string]: boolean }, - ), - ); + const kycResult = useInstanceKYCDetailsLongPolling(); + + + const kycData = (kycResult instanceof TalerError || !kycResult || kycResult.type === "fail" + ? undefined + : kycResult.body)?.kyc_data ?? [] + + const readyToBeUsed = Object.keys( + kycData.reduce( + (prev, st) => { + if ( + st.exchange_currency && + st.status === MerchantAccountKycStatus.READY + ) { + prev[st.exchange_currency] = true; + } + return prev; + }, + {} as { [s: string]: boolean }, + ), + ); const firstCurrency = !readyToBeUsed.length ? undefined : readyToBeUsed[0]; const fromDefault = !firstCurrency ? undefined : { - ...config.currencies[firstCurrency], - id: firstCurrency, + ...config.currencies[firstCurrency], + id: firstCurrency, + }; + + const kycStatus = kycData.reduce( + (prev, cur) => { + const st = getMerchantAccountKycStatusSimplified(cur.status); + prev.byAccount[cur.payto_uri] = st + if (prev.best === undefined || st < prev.best) { + prev.best = st + }; + if (prev.worst === undefined || st > prev.worst) { + prev.worst = st }; + prev.verificationRequired = prev.verificationRequired || cur.payto_kycauths !== undefined || cur.access_token !== undefined + return prev; + }, + { + byAccount: {}, + verificationRequired: false, + } as KycStatusMap, + ); return h(Context.Provider, { value: { list: readyToBeUsed, + kycResult, + kycStatus, currency: currency ?? fromDefault, switchCurrency(id: string) { const def = config.currencies[id]; @@ -102,7 +137,50 @@ export const CurrenciesProvider = ({ id: id, }); }, - }, + } as Type, children, }); }; + + +// export function useInstanceKYCSimplifiedWorstStatusLongPolling( +// account?: Paytos.URI, +// ) { +// const kycStatus = useInstanceKYCDetailsLongPolling(); + +// const allKycData = +// kycStatus !== undefined && +// !(kycStatus instanceof TalerError) && +// kycStatus.type === "ok" +// ? kycStatus.body.kyc_data +// : []; + +// const uri_str = +// account === undefined ? undefined : Paytos.toFullString(account); + +// const filtered = +// uri_str === undefined +// ? allKycData +// : allKycData.filter((data) => data.payto_uri === uri_str); + +// const r = filtered.reduce((prev, cur) => { +// const st = getMerchantAccountKycStatusSimplified(cur.status); +// if (st > prev) return st; +// return prev; +// }, MerchantAccountKycStatusSimplified.OK); + +// return r; +// } + +// export function useInstanceKYCSimplifiedBestStatusLongPolling() { +// const kycStatus = useInstanceKYCDetailsLongPolling(); + +// const allKycData = +// kycStatus !== undefined && +// !(kycStatus instanceof TalerError) && +// kycStatus.type === "ok" +// ? kycStatus.body.kyc_data +// : []; + +// } + diff --git a/packages/taler-merchant-webui/src/hooks/instance.ts b/packages/taler-merchant-webui/src/hooks/instance.ts @@ -16,13 +16,8 @@ import { AccessToken, - getMerchantAccountKycStatusSimplified, - MerchantAccountKycStatus, - MerchantAccountKycStatusSimplified, - Paytos, - TalerError, TalerHttpError, - TalerMerchantManagementResultByMethod, + TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; import { LONG_POLL_DELAY, useLongPolling } from "@gnu-taler/web-util/browser"; import { useSessionContext } from "../context/session.js"; @@ -83,11 +78,7 @@ export function useInstanceKYCDetailsLongPolling() { const result = useLongPolling( data, (result) => { - return ( - result !== undefined && - result.type === "ok" && - result.body.etag !== undefined - ); + return true; }, async (ct, latestData) => { if ( @@ -117,73 +108,6 @@ export function useInstanceKYCDetailsLongPolling() { return result; } -export function useInstanceKYCSimplifiedWorstStatusLongPolling( - account?: Paytos.URI, -) { - const kycStatus = useInstanceKYCDetailsLongPolling(); - - const allKycData = - kycStatus !== undefined && - !(kycStatus instanceof TalerError) && - kycStatus.type === "ok" - ? kycStatus.body.kyc_data - : []; - - const uri_str = - account === undefined ? undefined : Paytos.toFullString(account); - - const filtered = - uri_str === undefined - ? allKycData - : allKycData.filter((data) => data.payto_uri === uri_str); - - const r = filtered.reduce((prev, cur) => { - const st = getMerchantAccountKycStatusSimplified(cur.status); - if (st > prev) return st; - return prev; - }, MerchantAccountKycStatusSimplified.OK); - - return r; -} - -export function useInstanceKYCSimplifiedBestStatusLongPolling() { - const kycStatus = useInstanceKYCDetailsLongPolling(); - - const allKycData = - kycStatus !== undefined && - !(kycStatus instanceof TalerError) && - kycStatus.type === "ok" - ? kycStatus.body.kyc_data - : []; - - return allKycData.reduce( - (prev, cur) => { - const st = getMerchantAccountKycStatusSimplified(cur.status); - if (prev === undefined) return st; - if (st < prev) return st; - return prev; - }, - undefined as MerchantAccountKycStatusSimplified | undefined, - ); -} - -export function useAvailableCurrencies(): string[] { - const kyc = useInstanceKYCDetails(); - - const status = - kyc instanceof TalerError || !kyc || kyc.type === "fail" - ? undefined - : kyc.body; - - const cs = !status - ? [] - : status.kyc_data.map((st) => - st.status === MerchantAccountKycStatus.READY - ? st.exchange_currency - : undefined, - ); - return cs.filter((c): c is string => !!c); -} export function useInstanceKYCDetails() { const { state, lib } = useSessionContext(); diff --git a/packages/taler-merchant-webui/src/paths/instance/accounts/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/accounts/list/index.tsx @@ -33,15 +33,11 @@ import { import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; +import { useCurrenciesContext } from "../../../../context/currency.js"; import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; -import { - useInstanceKYCSimplifiedBestStatusLongPolling, - useInstanceKYCSimplifiedWorstStatusLongPolling, -} from "../../../../hooks/instance.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CardTable } from "./Table.js"; -import { useEffect } from "preact/hooks"; const TALER_SCREEN_ID = 34; @@ -81,7 +77,8 @@ export function UnableToUseBankAccountWarning({ account: Paytos.URI; }): VNode { const { i18n } = useTranslationContext(); - const status = useInstanceKYCSimplifiedWorstStatusLongPolling(account); + const {kycStatus} = useCurrenciesContext() + const status = kycStatus.byAccount[Paytos.toFullString(account)] switch (status) { case MerchantAccountKycStatusSimplified.ACTION_REQUIRED: case MerchantAccountKycStatusSimplified.WARNING: @@ -114,7 +111,8 @@ export function UnableToUseBankAccountWarning({ export function LimitedKycActionWarning(): VNode { const { i18n } = useTranslationContext(); - const status = useInstanceKYCSimplifiedBestStatusLongPolling(); + const {kycStatus} = useCurrenciesContext() + const status = kycStatus.best switch (status) { case MerchantAccountKycStatusSimplified.WARNING: diff --git a/packages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx @@ -38,8 +38,8 @@ import { ConfirmModal, ValidBankAccount, } from "../../../../components/modal/index.js"; -import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; import { ListPage } from "./ListPage.js"; +import { useCurrenciesContext } from "../../../../context/currency.js"; const TALER_SCREEN_ID = 95; @@ -49,8 +49,7 @@ interface Props { } export default function ListKYC(_p: Props): VNode { - const result = useInstanceKYCDetails(); - + const { kycResult: result } = useCurrenciesContext() const [showingInstructions, setShowingInstructions] = useState< TalerMerchantApi.MerchantAccountKycRedirect | undefined >(undefined); diff --git a/packages/web-util/src/hooks/useAsync.ts b/packages/web-util/src/hooks/useAsync.ts @@ -60,10 +60,6 @@ export function useAsync<Res>( export const LONG_POLL_DELAY = 15000; -// FIXME: the problem with this compared with useSWR is that -// if the hook is called from more than one place then -// you will have multiple request. This needs to be merged, maybe based on a -// key /** * First start with `initial` value, if initial is undefined then finish. * Otherwise: @@ -92,10 +88,12 @@ export function useLongPolling<Res>( ) { const minTime = opts?.minTime ?? 1000; - const [result, setResult] = useState(initial); - + const [result, setResult] = useState<{ counter: number, prevResult: Res }>({ counter: 0, prevResult: initial }); + function retry(prevResult: Res) { + setResult(prev => ({ counter: prev.counter + 1, prevResult })) + } useEffect(() => { - setResult(initial); + retry(initial); }, [initial, ...deps]); const ct = useRef<{ @@ -105,41 +103,39 @@ export function useLongPolling<Res>( }>({ ct: undefined, unloaded: false, startMs: 0 }); useEffect(() => { - // if (result === undefined || result instanceof TalerError) return; - const tk = CancellationToken.create(); ct.current.ct = tk; - const doWeRetry = shouldRetryFn(result); + const doWeRetry = shouldRetryFn(result.prevResult); if (!doWeRetry) return; const diff = new Date().getTime() - ct.current.startMs; if (ct.current.startMs === 0 || diff > minTime) { ct.current.startMs = new Date().getTime(); - retryFn(tk.token, result, deps) + retryFn(tk.token, result.prevResult, deps) .then((r) => { if (!tk.token.isCancelled) { - setResult(r); + retry(r); } }) - .catch((error) => console.log("")); + .catch((error) => console.log("ERROR", error)); } else { // calling too fast, wait whats left to reach minTime delayMs(minTime - diff).then(() => { if (ct.current.unloaded) return; ct.current.startMs = new Date().getTime(); - retryFn(tk.token, result, deps) + retryFn(tk.token, result.prevResult, deps) .then((r) => { if (!tk.token.isCancelled) { - setResult(r); + retry(r); } }) - .catch((error) => console.log("")); + .catch((error) => console.log("ERROR", error)); }); } - return () => {}; - }, [result]); + return () => { }; + }, [result.counter]); /** * the resultset is not needed anymore @@ -163,7 +159,7 @@ export function useLongPolling<Res>( }; }, deps); - return result; + return result.prevResult; } /**