commit 9e01cb32c4585acb31c1a03ca2d09dfd02660da4
parent 38954ca8a3736edaada01b67b456ff72728c167d
Author: Sebastian <sebasjm@taler-systems.com>
Date: Fri, 10 Jul 2026 12:48:18 -0300
fix #11623
Diffstat:
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;
}
/**