commit 01ae524ead2264774d877b135a581681651a74f5
parent d5d3fe085550aec1f8f62bb4e3c636be85370ae2
Author: Sebastian <sebasjm@taler-systems.com>
Date: Mon, 1 Jun 2026 11:13:42 -0300
several fixes and improvements
better error reporting (user can copy the content)
normalize safeHandler and MFA
simplify button
only one notificaton handler
prevent printing sensitive info
Diffstat:
15 files changed, 445 insertions(+), 410 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
@@ -23,6 +23,7 @@ import {
BrowserHashNavigationProvider,
ExchangeApiProvider,
Loading,
+ NotificationProvider,
TranslationProvider,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
@@ -56,48 +57,50 @@ export function App(): VNode {
return (
<UiSettingsProvider value={settings}>
<TranslationProvider source={strings}>
- <ExchangeApiProvider
- baseUrl={new URL("/", baseUrl)}
- frameOnError={ExchangeAmlFrame}
- evictors={{
- exchange: evictExchangeSwrCache,
- }}
- preventCompression={preventCompression}
- >
- <SWRConfig
- value={{
- provider: WITH_LOCAL_STORAGE_CACHE
- ? localStorageProvider
- : undefined,
- // normally, do not revalidate
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- revalidateIfStale: false,
- revalidateOnMount: undefined,
- focusThrottleInterval: undefined,
+ <NotificationProvider>
+ <ExchangeApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={ExchangeAmlFrame}
+ evictors={{
+ exchange: evictExchangeSwrCache,
+ }}
+ preventCompression={preventCompression}
+ >
+ <SWRConfig
+ 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,
- }}
- >
- <BrowserHashNavigationProvider>
- <UiFormsProvider>
- <Routing />
- </UiFormsProvider>
- </BrowserHashNavigationProvider>
- </SWRConfig>
- </ExchangeApiProvider>
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <BrowserHashNavigationProvider>
+ <UiFormsProvider>
+ <Routing />
+ </UiFormsProvider>
+ </BrowserHashNavigationProvider>
+ </SWRConfig>
+ </ExchangeApiProvider>
+ </NotificationProvider>
</TranslationProvider>
</UiSettingsProvider>
);
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -13,17 +13,15 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TranslatedString } from "@gnu-taler/taler-util";
import {
Footer,
Header,
ToastBanner,
- notifyError,
- notifyException,
+ useCommonPreferences,
+ useRenderErrorReport,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
import { useUiSettingsContext } from "./context/ui-settings.js";
import { OfficerState } from "./hooks/officer.js";
import {
@@ -37,25 +35,6 @@ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
const TALER_SCREEN_ID = 101;
-function useErrorReport() {
- const { i18n } = useTranslationContext();
- const [error] = useErrorBoundary();
-
- useEffect(() => {
- if (error) {
- if (error instanceof Error) {
- notifyException(i18n.str`Internal error, please report.`, error);
- } else {
- notifyError(
- i18n.str`Internal error, please report.`,
- String(error) as TranslatedString,
- );
- }
- console.log(error);
- }
- }, [error]);
-}
-
export function ExchangeAmlFrame({
children,
officer,
@@ -67,7 +46,11 @@ export function ExchangeAmlFrame({
}): VNode {
const { i18n } = useTranslationContext();
- useErrorReport();
+ const [{ showDebugInfo }, update] = useCommonPreferences();
+ useRenderErrorReport({
+ hash: __GIT_HASH__,
+ version: __VERSION__,
+ });
const [preferences, updatePreferences] = usePreferences();
const settings = useUiSettingsContext();
@@ -127,6 +110,37 @@ export function ExchangeAmlFrame({
</li>
);
})}
+ <li class="mt-2 pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Show debug information</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`debug switch`}
+ data-enabled={showDebugInfo}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ update("showDebugInfo", !showDebugInfo);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={showDebugInfo}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
</ul>
</li>
</Header>
@@ -134,7 +148,7 @@ export function ExchangeAmlFrame({
<div class="fixed z-20 w-full">
<div class="mx-auto w-4/5">
- <ToastBanner debug={preferences.showDebugInfo} />
+ <ToastBanner />
</div>
</div>
diff --git a/packages/aml-backoffice-ui/src/components/CreateSession.tsx b/packages/aml-backoffice-ui/src/components/CreateSession.tsx
@@ -14,18 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- ButtonBetter,
+ Button,
FormDesign,
FormUI,
InternationalizationAPI,
- LocalNotificationBanner,
useForm,
- useLocalNotificationBetter,
+ useNotificationContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { OfficerNotFound } from "../hooks/officer.js";
import { usePreferences } from "../hooks/preferences.js";
+import { asPassword } from "@gnu-taler/taler-util";
type FormType = {
password: string;
@@ -84,7 +84,7 @@ export function CreateSession({
}): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreferences();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
const design = createAccountForm(i18n, settings.allowInsecurePassword);
@@ -93,16 +93,13 @@ export function CreateSession({
repeat: undefined,
});
- const create = safeFunctionHandler(
- i18n.str`create session`,
- officer.create,
- status.status === "fail" ? undefined : [status.result.password],
+ const create = actionHandler(
+ (ct, pw) => officer.create(pw),
+ status.status === "fail" ? undefined : [asPassword(status.result.password)],
);
return (
<div class="flex min-h-full flex-col ">
- <LocalNotificationBanner notification={notification} />
-
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Create session</i18n.Translate>
@@ -110,15 +107,15 @@ export function CreateSession({
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
- <FormUI design={design} model={handler} />
+ <FormUI design={design} model={handler} focus />
<div class="mt-8">
- <ButtonBetter
+ <Button
submit
class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={create}
>
<i18n.Translate>Create</i18n.Translate>
- </ButtonBetter>
+ </Button>
</div>
</div>
</div>
diff --git a/packages/aml-backoffice-ui/src/components/MeasureList.tsx b/packages/aml-backoffice-ui/src/components/MeasureList.tsx
@@ -21,6 +21,7 @@ import {
import {
Attention,
ErrorLoading,
+ FailLoading,
Loading,
RouteDefinition,
useTranslationContext,
@@ -43,50 +44,46 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) {
return <Loading />;
}
if (measures instanceof TalerError) {
- return <ErrorLoading error={measures} />;
+ return <ErrorLoading title={i18n.str`Failed to load server measures.`} error={measures} />;
}
if (measures.type === "fail") {
- switch (measures.case) {
- case HttpStatusCode.Forbidden:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This session signature is invalid, contact administrator or
- create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.NotFound:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not known, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.Conflict:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not enabled, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- default:
- assertUnreachable(measures);
- }
+ return (
+ <Fragment>
+ <Profile />
+ <FailLoading
+ operation={measures}
+ title={i18n.str`Failed to load the measures`}
+ translate={(d) => {
+ switch (d.case) {
+ case HttpStatusCode.Forbidden:
+ return (
+ <i18n.Translate>
+ This session signature is invalid, contact administrator or
+ create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.NotFound:
+ return (
+ <i18n.Translate>
+ The designated AML session is not known, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.Conflict:
+ return (
+ <i18n.Translate>
+ The designated AML session is not enabled, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ default:
+ assertUnreachable(d.case);
+ }
+ }}
+ />
+ </Fragment>
+ );
}
const ms = computeMeasureInformation(measures.body);
diff --git a/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/components/MeasuresTable.tsx
@@ -13,10 +13,6 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- AmlProgramRequirement,
- KycCheckInformation,
-} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import {
diff --git a/packages/aml-backoffice-ui/src/components/UnlockSession.tsx b/packages/aml-backoffice-ui/src/components/UnlockSession.tsx
@@ -14,18 +14,21 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- ButtonBetter,
+ asPassword,
+ assertUnreachable,
+ HttpStatusCode,
+} from "@gnu-taler/taler-util";
+import {
+ Button,
FormDesign,
InputLine,
InternationalizationAPI,
- LocalNotificationBanner,
useForm,
- useLocalNotificationBetter,
+ useNotificationContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { h, VNode } from "preact";
import { OfficerLocked } from "../hooks/officer.js";
-import { assertUnreachable, HttpStatusCode } from "@gnu-taler/taler-util";
type FormType = {
password: string;
@@ -48,7 +51,7 @@ const unlockAccountForm = (i18n: InternationalizationAPI): FormDesign => ({
export function UnlockSession({ officer }: { officer: OfficerLocked }): VNode {
const { i18n } = useTranslationContext();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
const design = unlockAccountForm(i18n);
@@ -56,30 +59,22 @@ export function UnlockSession({ officer }: { officer: OfficerLocked }): VNode {
password: undefined,
});
- const unlock = safeFunctionHandler(
- i18n.str`unlock session`,
- officer.tryUnlock,
- status.status === "fail" ? undefined : [status.result.password],
+ const unlock = actionHandler(
+ (ct, pw) => officer.tryUnlock(pw),
+ status.status === "fail" ? undefined : [asPassword(status.result.password)],
);
- unlock.onFail = (fail) => {
+ unlock.onFail = showError(i18n.str`Failed to unlock the session.`, (fail) => {
switch (fail.case) {
case HttpStatusCode.Forbidden:
- return i18n.str`Wrong password.`;
-
+ return i18n.str`Authorization denied for this session. Contact the administrator.`;
default:
assertUnreachable(fail.case);
}
- };
- const forget = safeFunctionHandler(
- i18n.str`forget session`,
- async () => officer.forget(),
- [],
- );
+ });
+ const forget = actionHandler(async () => officer.forget(), []);
return (
<div class="flex min-h-full flex-col ">
- <LocalNotificationBanner notification={notification} />
-
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Session locked</i18n.Translate>
@@ -105,21 +100,21 @@ export function UnlockSession({ officer }: { officer: OfficerLocked }): VNode {
</div>
<div class="mt-8">
- <ButtonBetter
+ <Button
submit
onClick={unlock}
class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<i18n.Translate>Unlock</i18n.Translate>
- </ButtonBetter>
+ </Button>
</div>
</form>
- <ButtonBetter
+ <Button
onClick={forget}
class="disabled:opacity-50 disabled:cursor-default m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
>
<i18n.Translate>Forget session</i18n.Translate>
- </ButtonBetter>
+ </Button>
</div>
</div>
);
diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -24,6 +24,7 @@ import {
OfficerSession,
OperationFail,
OperationOk,
+ Password,
buildCodecForObject,
codecForAbsoluteTime,
codecForString,
@@ -87,13 +88,13 @@ export type OfficerState = OfficerNotReady | OfficerReady;
export type OfficerNotReady = OfficerNotFound | OfficerLocked;
export interface OfficerNotFound {
state: "not-found";
- create: (password: string) => Promise<OperationOk<OfficerId>>;
+ create: (password: Password) => Promise<OperationOk<OfficerId>>;
}
export interface OfficerLocked {
state: "locked";
forget: () => OperationOk<void>;
tryUnlock: (
- password: string,
+ password: Password,
) => Promise<OperationOk<void> | OperationFail<HttpStatusCode.Forbidden>>;
}
export interface OfficerReady {
@@ -123,7 +124,7 @@ export function useOfficer(): OfficerState {
return {
id: sessionStorage.value.id as OfficerId,
- signingKey: decodeCrock(sessionStorage.value.strKey) as EddsaPrivP,
+ __signingKey: decodeCrock(sessionStorage.value.strKey) as EddsaPrivP,
unlocked: sessionStorage.value.unlocked,
};
}, [sessionStorage.value?.id, sessionStorage.value?.strKey]);
@@ -138,11 +139,11 @@ export function useOfficer(): OfficerState {
if (currentSession === undefined) {
return {
state: "not-found",
- create: async (pwd: string) => {
+ create: async (pwd: Password) => {
const resp = await api.getSeed();
const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array();
- const { id, safe, signingKey } = await createNewOfficerAccount(
+ const { id, safe, __signingKey } = await createNewOfficerAccount(
pwd,
extraEntropy,
);
@@ -153,7 +154,7 @@ export function useOfficer(): OfficerState {
});
// accountStorage.update({ id, signingKey });
- const strKey = encodeCrock(signingKey);
+ const strKey = encodeCrock(__signingKey);
sessionStorage.update({ id, strKey, unlocked: AbsoluteTime.now() });
// FIXME: This is really not the right type to use here.
@@ -176,13 +177,13 @@ export function useOfficer(): OfficerState {
officerStorage.reset();
return opFixedSuccess(dummyHttpResponse, undefined);
},
- tryUnlock: async (pwd: string) => {
+ tryUnlock: async (pwd: Password) => {
try {
const ac = await unlockOfficerAccount(currentSession, pwd);
// accountStorage.update(ac);
sessionStorage.update({
id: ac.id,
- strKey: encodeCrock(ac.signingKey),
+ strKey: encodeCrock(ac.__signingKey),
unlocked: AbsoluteTime.now(),
});
return opFixedSuccess(dummyHttpResponse, undefined);
diff --git a/packages/aml-backoffice-ui/src/hooks/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -28,7 +28,6 @@ import {
} from "@gnu-taler/web-util/browser";
interface Preferences {
- showDebugInfo: boolean;
allowInsecurePassword: boolean;
keepSessionAfterReload: boolean;
testingDialect: boolean;
@@ -41,7 +40,6 @@ export const codecForPreferences = (): Codec<Preferences> =>
"allowInsecurePassword",
codecOptionalDefault(codecForBoolean(), false),
)
- .property("showDebugInfo", codecOptionalDefault(codecForBoolean(), false))
.property("testingDialect", codecOptionalDefault(codecForBoolean(), false))
.property(
"keepSessionAfterReload",
@@ -55,7 +53,6 @@ export const codecForPreferences = (): Codec<Preferences> =>
const defaultPreferences: Preferences = {
allowInsecurePassword: false,
- showDebugInfo: false,
testingDialect: false,
keepSessionAfterReload: false,
preventCompression: false,
@@ -88,7 +85,6 @@ export function usePreferences(): [
export function getAllBooleanPreferences(): Array<keyof Preferences> {
return [
- "showDebugInfo",
"allowInsecurePassword",
"keepSessionAfterReload",
"testingDialect",
@@ -103,8 +99,6 @@ export function getLabelForPreferences(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString {
switch (k) {
- case "showDebugInfo":
- return i18n.str`Show debug info`;
case "testingDialect":
return i18n.str`Use testing dialect`;
case "allowInsecurePassword":
diff --git a/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx b/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx
@@ -24,14 +24,13 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
- ButtonBetter,
+ Button,
CopyButton,
ErrorLoading,
Loading,
- LocalNotificationBanner,
RouteDefinition,
useExchangeApiContext,
- useLocalNotificationBetter,
+ useNotificationContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
@@ -95,7 +94,7 @@ export function AccountDetails({
const session = officer.state === "ready" ? officer.session : undefined;
const { lib } = useExchangeApiContext();
const [exported, setExported] = useState<{ content: string; file: string }>();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
const measures = useServerMeasures();
@@ -103,7 +102,12 @@ export function AccountDetails({
return <Loading />;
}
if (details instanceof TalerError) {
- return <ErrorLoading error={details} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load account information.`}
+ error={details}
+ />
+ );
}
if (details.type === "fail") {
switch (details.case) {
@@ -116,7 +120,12 @@ export function AccountDetails({
}
}
if (history instanceof TalerError) {
- return <ErrorLoading error={history} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load decision history.`}
+ error={history}
+ />
+ );
}
if (history.type === "fail") {
switch (history.case) {
@@ -129,7 +138,12 @@ export function AccountDetails({
}
}
if (legistimizations instanceof TalerError) {
- return <ErrorLoading error={legistimizations} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load current legitimizations.`}
+ error={legistimizations}
+ />
+ );
}
const collectionEvents = details.body.details;
@@ -197,11 +211,11 @@ export function AccountDetails({
const time = format(new Date(), "yyyyMMdd_HHmmss");
- const downloadPdf = safeFunctionHandler(
- i18n.str`download pdf`,
- lib.exchange.getAmlAttributesForAccountAsPdf.bind(lib.exchange),
- session ? [session, account] : undefined,
+ const downloadPdf = actionHandler(
+ (ct, s, ac) => lib.exchange.getAmlAttributesForAccountAsPdf(s, ac),
+ session ? ([session, account] as const) : undefined,
);
+
downloadPdf.onSuccess = (result) => {
setExported({
content: new Uint8Array(result).reduce(
@@ -211,26 +225,28 @@ export function AccountDetails({
file: `account_${time}_${account}.pdf`,
});
};
- downloadPdf.onFail = (fail) => {
- switch (fail.case) {
- case HttpStatusCode.NoContent:
- return i18n.str`The account has no KYC info.`;
- case HttpStatusCode.Forbidden:
- return i18n.str`Invalid session.`;
- case HttpStatusCode.NotFound:
- return i18n.str`Session not found. Contact the administrator.`;
- case HttpStatusCode.Conflict:
- return i18n.str`The session is disabled. Contact the administrator.`;
- case HttpStatusCode.NotImplemented:
- return i18n.str`The server doesn't support PDF download. Contact the administrator.`;
- default:
- assertUnreachable(fail.case);
- }
- };
+ downloadPdf.onFail = showError(
+ i18n.str`Failed to download report.`,
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.NoContent:
+ return i18n.str`The account has no KYC info.`;
+ case HttpStatusCode.Forbidden:
+ return i18n.str`Authorization denied for this session. Contact the administrator.`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Session not found. Contact the administrator.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`The session is disabled. Contact the administrator.`;
+ case HttpStatusCode.NotImplemented:
+ return i18n.str`The server doesn't support PDF download. Contact the administrator.`;
+ default:
+ assertUnreachable(fail.case);
+ }
+ },
+ );
return (
<div class="min-w-60">
- <LocalNotificationBanner notification={notification} />
<header class="flex flex-col justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8 gap-2">
<h1 class="text-base font-semibold leading-7 text-black">
<i18n.Translate>Case history for selected account</i18n.Translate>
@@ -256,9 +272,9 @@ export function AccountDetails({
<div class="flex space-x-2 mb-4">
<i18n.Translate>Export as PDF</i18n.Translate>
- <ButtonBetter onClick={downloadPdf}>
+ <Button onClick={downloadPdf}>
<img class="size-6 w-6" src={pdfIcon} />
- </ButtonBetter>
+ </Button>
</div>
{!exported ? (
<div />
diff --git a/packages/aml-backoffice-ui/src/pages/AccountList.tsx b/packages/aml-backoffice-ui/src/pages/AccountList.tsx
@@ -16,24 +16,23 @@
import {
AbsoluteTime,
CustomerAccountSummary,
- Duration,
HttpStatusCode,
+ OfficerSession,
Paytos,
TalerError,
assertUnreachable,
- opFixedSuccess,
} from "@gnu-taler/taler-util";
import {
Attention,
- ButtonBetter,
+ Button,
ErrorLoading,
+ FailLoading,
InputToggle,
Loading,
- LocalNotificationBanner,
Pagination,
RouteDefinition,
useExchangeApiContext,
- useLocalNotificationBetter,
+ useNotificationContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
@@ -58,7 +57,8 @@ export function AccountList({
const [opened, setOpened] = useState<boolean>();
const [highRisk, setHighRisk] = useState<boolean>();
const list = useAmlAccounts({ investigated, open: opened, highRisk });
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
+
const officer = useOfficer();
const session = officer.state === "ready" ? officer.session : undefined;
const { lib } = useExchangeApiContext();
@@ -67,50 +67,48 @@ export function AccountList({
return <Loading />;
}
if (list instanceof TalerError) {
- return <ErrorLoading error={list} />;
+ return (
+ <ErrorLoading title={i18n.str`Failed to load accounts`} error={list} />
+ );
}
if (list.type === "fail") {
- switch (list.case) {
- case HttpStatusCode.Forbidden:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This session signature is invalid, contact administrator or
- create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.NotFound:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not known, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.Conflict:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not enabled, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- default:
- assertUnreachable(list);
- }
+ return (
+ <Fragment>
+ <Profile />
+ <FailLoading
+ operation={list}
+ title={i18n.str`Failed to load the accounts`}
+ translate={(d) => {
+ switch (d.case) {
+ case HttpStatusCode.Forbidden:
+ return (
+ <i18n.Translate>
+ This session signature is invalid, contact administrator or
+ create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.NotFound:
+ return (
+ <i18n.Translate>
+ The designated AML session is not known, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.Conflict:
+ return (
+ <i18n.Translate>
+ The designated AML session is not enabled, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ default:
+ assertUnreachable(d.case);
+ }
+ }}
+ />
+ </Fragment>
+ );
}
const records = list.body;
@@ -168,56 +166,74 @@ export function AccountList({
: `not-investigated`;
const time = format(new Date(), "yyyyMMdd_HHmmss");
- const downloadCsv = safeFunctionHandler(
- i18n.str`download csv`,
- lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange),
- session ? [session, "text/csv"] : undefined,
+
+ type Mime = "text/csv" | "application/vnd.ms-excel";
+ const download = actionHandler((ct, s: OfficerSession, f: Mime) =>
+ lib.exchange.getAmlAccountsAsOtherFormat(s, f),
);
- downloadCsv.onFail = (fail) => {
+ download.onFail = showError(i18n.str`Failed to download`, (fail) => {
switch (fail.case) {
case HttpStatusCode.NoContent:
- return i18n.str`Ther are no accounts in the resultset.`;
+ return i18n.str`There are no accounts in the resultset.`;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str`The format requested is not acceptable from the service provider.`;
case HttpStatusCode.Forbidden:
- return i18n.str`Invalid session.`;
+ return i18n.str`Authorization denied for this session.`;
case HttpStatusCode.NotFound:
return i18n.str`Session not found. Contact the administrator.`;
case HttpStatusCode.Conflict:
return i18n.str`The session is disabled. Contact the administrator`;
}
- };
- const downloadXls = safeFunctionHandler(
- i18n.str`download xls`,
- lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange),
- session ? [session, "application/vnd.ms-excel"] : undefined,
- );
- downloadXls.onFail = (fail) => {
- switch (fail.case) {
- case HttpStatusCode.NoContent:
- return i18n.str`Ther are no accounts in the resultset.`;
- case HttpStatusCode.Forbidden:
- return i18n.str`Invalid session.`;
- case HttpStatusCode.NotFound:
- return i18n.str`Session not found. Contact the administrator.`;
- case HttpStatusCode.Conflict:
- return i18n.str`The session is disabled. Contact the administrator`;
+ });
+ download.onSuccess = (result, s, f) => {
+ switch (f) {
+ case "text/csv":
+ return setExported({
+ content: utfDecoder.decode(result),
+ file: `accounts_${time}_${fileDescription}.csv`,
+ });
+ case "application/vnd.ms-excel":
+ return setExported({
+ content: utfDecoder.decode(result),
+ file: `accounts_${time}_${fileDescription}.xls`,
+ });
+ default:
+ assertUnreachable(f);
}
};
- downloadCsv.onSuccess = (result) => {
- setExported({
- content: utfDecoder.decode(result),
- file: `accounts_${time}_${fileDescription}.csv`,
- });
- };
- downloadXls.onSuccess = (result) => {
- setExported({
- content: utfDecoder.decode(result),
- file: `accounts_${time}_${fileDescription}.xls`,
- });
- };
+ // const downloadXls = actionHandler(
+ // (ct, s, f) => lib.exchange.getAmlAccountsAsOtherFormat(s, f),
+ // session ? ([session, "application/vnd.ms-excel"] as const) : undefined,
+ // );
+
+ // downloadXls.onFail = showError(i18n.str`Failed to download XLS.`, (fail) => {
+ // switch (fail.case) {
+ // case HttpStatusCode.NoContent:
+ // return i18n.str`There are no accounts in the resultset.`;
+ // case HttpStatusCode.Forbidden:
+ // return i18n.str`Invalid session.`;
+ // case HttpStatusCode.NotFound:
+ // return i18n.str`Session not found. Contact the administrator.`;
+ // case HttpStatusCode.Conflict:
+ // return i18n.str`The session is disabled. Contact the administrator`;
+ // }
+ // });
+
+ // downloadCsv.onSuccess = (result) => {
+ // setExported({
+ // content: utfDecoder.decode(result),
+ // file: `accounts_${time}_${fileDescription}.csv`,
+ // });
+ // };
+ // downloadXls.onSuccess = (result) => {
+ // setExported({
+ // content: utfDecoder.decode(result),
+ // file: `accounts_${time}_${fileDescription}.xls`,
+ // });
+ // };
return (
<div>
- <LocalNotificationBanner notification={notification} />
<div class="sm:flex sm:items-center">
<div class="px-2 sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900">
@@ -226,15 +242,28 @@ export function AccountList({
<p class="mt-2 text-sm text-gray-700 w-80">
<i18n.Translate>{description}</i18n.Translate>
</p>
- <div class="flex space-x-2 mt-4">
- <i18n.Translate>Export as file</i18n.Translate>
- <ButtonBetter onClick={downloadCsv}>
- <img class="size-6 w-6" src={csvIcon} />
- </ButtonBetter>
- <ButtonBetter onClick={downloadXls}>
- <img class="size-6 w-6" src={xlsIcon} />
- </ButtonBetter>
- </div>
+ {!records.length ? undefined : (
+ <div class="flex space-x-2 mt-4">
+ <i18n.Translate>Export as file</i18n.Translate>
+ <Button
+ onClick={
+ !session ? undefined : download.withArgs(session, "text/csv")
+ }
+ >
+ <img class="size-6 w-6" src={csvIcon} />
+ </Button>
+ <Button
+ onClick={
+ !session
+ ? undefined
+ : download.withArgs(session, "application/vnd.ms-excel")
+ }
+ >
+ <img class="size-6 w-6" src={xlsIcon} />
+ </Button>
+ </div>
+ )}
+
{!exported ? (
<div class="h-5 mb-5" />
) : (
diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx
@@ -83,7 +83,7 @@ function EventMetrics(): VNode {
return <Loading />;
}
if (resp instanceof TalerError) {
- return <ErrorLoading error={resp} />;
+ return <ErrorLoading title={i18n.str`Failed to load TOPS statistics.`} error={resp} />;
}
return (
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -37,6 +37,7 @@ import {
Attention,
encodeCrockForURI,
ErrorLoading,
+ FailLoading,
FormDesign,
FormUI,
InternationalizationAPI,
@@ -148,49 +149,47 @@ function ShowResult({
return <Loading />;
}
if (history instanceof TalerError) {
- return <ErrorLoading error={history} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load account decisions.`}
+ error={history}
+ />
+ );
}
if (history.type === "fail") {
- switch (history.case) {
- case HttpStatusCode.Forbidden:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This session signature is invalid, contact administrator or
- create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.NotFound:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not known, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.Conflict:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not enabled, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- default:
- assertUnreachable(history);
- }
+ return (
+ <FailLoading
+ operation={history}
+ title={i18n.str`Failed to load the account history.`}
+ translate={(d) => {
+ switch (d.case) {
+ case HttpStatusCode.Forbidden:
+ return (
+ <i18n.Translate>
+ This session signature is invalid, contact administrator or
+ create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.NotFound:
+ return (
+ <i18n.Translate>
+ The designated AML session is not known, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.Conflict:
+ return (
+ <i18n.Translate>
+ The designated AML session is not enabled, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ default:
+ assertUnreachable(d.case);
+ }
+ }}
+ />
+ );
}
if (history.body.length) {
diff --git a/packages/aml-backoffice-ui/src/pages/ShowCollectedInfo.tsx b/packages/aml-backoffice-ui/src/pages/ShowCollectedInfo.tsx
@@ -23,6 +23,7 @@ import {
import {
Attention,
ErrorLoading,
+ FailLoading,
FormMetadata,
FormUI,
Loading,
@@ -56,49 +57,45 @@ export function ShowCollectedInfo({
return <Loading />;
}
if (details instanceof TalerError) {
- return <ErrorLoading error={details} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load account information.`}
+ error={details}
+ />
+ );
}
if (details.type === "fail") {
- switch (details.case) {
- case HttpStatusCode.Forbidden:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
+ return <FailLoading
+ operation={details}
+ title={i18n.str`Failed to load the account information.`}
+ translate={(d) => {
+ switch (d.case) {
+ case HttpStatusCode.Forbidden:
+ return (
<i18n.Translate>
This session signature is invalid, contact administrator or
create a new one.
</i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.NotFound:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
+ );
+ case HttpStatusCode.NotFound:
+ return (
<i18n.Translate>
The designated AML session is not known, contact administrator
or create a new one.
</i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.Conflict:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
+ );
+ case HttpStatusCode.Conflict:
+ return (
<i18n.Translate>
The designated AML session is not enabled, contact administrator
or create a new one.
</i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- default:
- assertUnreachable(details);
- }
+ );
+ default:
+ assertUnreachable(d.case);
+ }
+ }}
+ />;
}
const { details: history } = details.body;
diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx
@@ -26,6 +26,7 @@ import {
import {
Attention,
ErrorLoading,
+ FailLoading,
FormDesign,
FormUI,
Loading,
@@ -108,49 +109,47 @@ export function Transfers({
return <Loading />;
}
if (resp instanceof Error) {
- return <ErrorLoading error={resp} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load transfer list.`}
+ error={resp}
+ />
+ );
}
if (resp.type === "fail") {
- switch (resp.case) {
- case HttpStatusCode.Forbidden:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- This session signature is invalid, contact administrator or
- create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.NotFound:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not known, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- case HttpStatusCode.Conflict:
- return (
- <Fragment>
- <Attention type="danger" title={i18n.str`Operation denied`}>
- <i18n.Translate>
- The designated AML session is not enabled, contact administrator
- or create a new one.
- </i18n.Translate>
- </Attention>
- <Profile />
- </Fragment>
- );
- default:
- assertUnreachable(resp);
- }
+ return (
+ <FailLoading
+ operation={resp}
+ title={i18n.str`Failed to load the transfer list.`}
+ translate={(d) => {
+ switch (d.case) {
+ case HttpStatusCode.Forbidden:
+ return (
+ <i18n.Translate>
+ This session signature is invalid, contact administrator or
+ create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.NotFound:
+ return (
+ <i18n.Translate>
+ The designated AML session is not known, contact administrator
+ or create a new one.
+ </i18n.Translate>
+ );
+ case HttpStatusCode.Conflict:
+ return (
+ <i18n.Translate>
+ The designated AML session is not enabled, contact
+ administrator or create a new one.
+ </i18n.Translate>
+ );
+ default:
+ assertUnreachable(d.case);
+ }
+ }}
+ />
+ );
}
const transactions = resp.body;
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx
@@ -26,11 +26,10 @@ import {
import { dummyHttpResponse } from "@gnu-taler/taler-util/http";
import {
Attention,
- ButtonBetter,
- LocalNotificationBanner,
+ Button,
useExchangeApiContext,
- useLocalNotificationBetter,
- useTranslationContext,
+ useNotificationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -75,7 +74,7 @@ export function Summary({
const [decision, , cleanUpDecision] = useCurrentDecisionRequest();
const measures = useServerMeasures();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
const session = officer.session;
const allMeasures = computeMeasureInformation(
@@ -167,9 +166,9 @@ export function Summary({
const [submitConfirmation, setSubmitConfirmation] = useState<boolean>(false);
const requiresConfirmation = MROS_REPORT_COMPLETED;
- const submit = safeFunctionHandler(
- i18n.str`make aml decision`,
- async (req) => {
+
+ const submit = actionHandler(
+ async (ct, req) => {
if (requiresConfirmation && !submitConfirmation) {
setSubmitConfirmation(true);
// FIXME: This is not the right type to use here.
@@ -181,7 +180,7 @@ export function Summary({
);
submit.onSuccess = clearUp;
- submit.onFail = (fail) => {
+ submit.onFail = showError(i18n.str`Failed to make the decision.`, (fail) => {
switch (fail.case) {
case HttpStatusCode.Forbidden:
return i18n.str`Invalid credentials.`;
@@ -192,7 +191,7 @@ export function Summary({
default:
assertUnreachable(fail.case);
}
- };
+ });
if (submitConfirmation) {
return (
@@ -215,13 +214,13 @@ export function Summary({
>
<i18n.Translate>I want to check first!</i18n.Translate>
</button>
- <ButtonBetter
+ <Button
submit
onClick={submit}
class="mt-4 disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<i18n.Translate>Confirm decision</i18n.Translate>
- </ButtonBetter>
+ </Button>
</div>
</div>
</div>
@@ -231,7 +230,6 @@ export function Summary({
return (
<Fragment>
- <LocalNotificationBanner notification={notification} />
{INVALID_RULES ? (
<Fragment>
{!decision.deadline && (
@@ -355,13 +353,13 @@ export function Summary({
>
<i18n.Translate>Clear</i18n.Translate>
</button>
- <ButtonBetter
+ <Button
submit
onClick={submit}
class="mt-4 disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<i18n.Translate>Send decision</i18n.Translate>
- </ButtonBetter>
+ </Button>
</div>
</Fragment>
);