gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated (4cbe754ac -> 9c16a2588)


From: gnunet
Subject: [taler-wallet-core] branch master updated (4cbe754ac -> 9c16a2588)
Date: Mon, 11 Mar 2024 18:58:04 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a change to branch master
in repository wallet-core.

    from 4cbe754ac fix ci issue
     new 37f46f4d6 obs and cancel request, plus lint
     new 6e02a3852 obs and cancel request, plus lint
     new 9c16a2588 adding id and time to obs event, plus pretty

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/bank-ui/src/Routing.tsx                   |   69 +-
 packages/bank-ui/src/app.tsx                       |    2 +-
 packages/bank-ui/src/components/Cashouts/views.tsx |   37 +-
 packages/bank-ui/src/components/Time.tsx           |   51 +-
 .../bank-ui/src/components/Transactions/index.ts   |   24 +-
 .../bank-ui/src/components/Transactions/state.ts   |   62 +-
 .../bank-ui/src/components/Transactions/views.tsx  |   50 +-
 packages/bank-ui/src/context/config.ts             |  108 +-
 packages/bank-ui/src/hooks/account.ts              |   30 +-
 packages/bank-ui/src/hooks/bank-state.ts           |    9 +-
 packages/bank-ui/src/hooks/form.ts                 |  125 +-
 packages/bank-ui/src/hooks/preferences.ts          |    3 +-
 packages/bank-ui/src/hooks/regional.ts             |   58 +-
 packages/bank-ui/src/pages/AccountPage/index.ts    |   12 +-
 packages/bank-ui/src/pages/AccountPage/views.tsx   |   18 +-
 packages/bank-ui/src/pages/BankFrame.tsx           |  166 ++-
 packages/bank-ui/src/pages/LoginForm.tsx           |   60 +-
 packages/bank-ui/src/pages/OperationState/index.ts |   16 +-
 packages/bank-ui/src/pages/OperationState/state.ts |    6 +-
 .../bank-ui/src/pages/OperationState/views.tsx     |   17 +-
 packages/bank-ui/src/pages/PaymentOptions.tsx      |   31 +-
 .../bank-ui/src/pages/PaytoWireTransferForm.tsx    |  304 ++--
 packages/bank-ui/src/pages/ProfileNavigation.tsx   |    8 +-
 packages/bank-ui/src/pages/PublicHistoriesPage.tsx |   11 +-
 packages/bank-ui/src/pages/QrCodeSection.tsx       |   22 +-
 packages/bank-ui/src/pages/RegistrationPage.tsx    |   39 +-
 packages/bank-ui/src/pages/ShowNotifications.tsx   |   55 +
 packages/bank-ui/src/pages/SolveChallengePage.tsx  |   43 +-
 packages/bank-ui/src/pages/WalletWithdrawForm.tsx  |   62 +-
 packages/bank-ui/src/pages/WireTransfer.tsx        |   10 +-
 .../src/pages/WithdrawalConfirmationQuestion.tsx   |   34 +-
 .../bank-ui/src/pages/WithdrawalOperationPage.tsx  |    2 +-
 .../src/pages/account/CashoutListForAccount.tsx    |    5 +-
 .../src/pages/account/ShowAccountDetails.tsx       |   10 +-
 .../src/pages/account/UpdateAccountPassword.tsx    |   17 +-
 packages/bank-ui/src/pages/admin/AccountForm.tsx   |  162 ++-
 packages/bank-ui/src/pages/admin/AccountList.tsx   |   13 +-
 packages/bank-ui/src/pages/admin/AdminHome.tsx     |   41 +-
 .../bank-ui/src/pages/admin/CreateNewAccount.tsx   |   11 +
 packages/bank-ui/src/pages/admin/DownloadStats.tsx |   11 +-
 packages/bank-ui/src/pages/admin/RemoveAccount.tsx |    4 +
 .../src/pages/regional/ConversionConfig.tsx        | 1464 +++++++++++---------
 .../bank-ui/src/pages/regional/CreateCashout.tsx   |  104 +-
 .../src/pages/regional/ShowCashoutDetails.tsx      |   34 +-
 packages/bank-ui/src/route.ts                      |   43 +-
 packages/bank-ui/src/stories.test.ts               |    7 +-
 packages/bank-ui/src/utils.ts                      |   82 +-
 packages/taler-util/src/errors.ts                  |    7 +-
 .../taler-util/src/http-client/bank-conversion.ts  |   10 +-
 packages/taler-util/src/http-client/bank-core.ts   |   16 +-
 .../taler-util/src/http-client/bank-integration.ts |    5 +-
 packages/taler-util/src/http-client/types.ts       |   10 +-
 packages/taler-util/src/http-client/utils.ts       |    4 +-
 packages/taler-util/src/notifications.ts           |   12 +-
 packages/taler-util/src/observability.ts           |   46 +-
 packages/taler-util/src/operation.ts               |    5 +-
 packages/web-util/src/components/Button.tsx        |   36 +-
 packages/web-util/src/components/CopyButton.tsx    |    5 +-
 packages/web-util/src/components/ErrorLoading.tsx  |   28 +
 packages/web-util/src/components/Header.tsx        |   39 +-
 .../web-util/src/components/NotificationBanner.tsx |    4 +-
 packages/web-util/src/components/ToastBanner.tsx   |   59 +-
 packages/web-util/src/hooks/useNotifications.ts    |   68 +-
 packages/web-util/src/utils/http-impl.sw.ts        |    5 +-
 64 files changed, 2416 insertions(+), 1495 deletions(-)
 create mode 100644 packages/bank-ui/src/pages/ShowNotifications.tsx

diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
index 75f070e4b..fbf5aa9ec 100644
--- a/packages/bank-ui/src/Routing.tsx
+++ b/packages/bank-ui/src/Routing.tsx
@@ -22,6 +22,7 @@ import {
 import { Fragment, VNode, h } from "preact";
 
 import {
+  AbsoluteTime,
   AccessToken,
   HttpStatusCode,
   TranslatedString,
@@ -30,13 +31,13 @@ import {
 import { useEffect } from "preact/hooks";
 import { useBankCoreApiContext } from "./context/config.js";
 import { useNavigationContext } from "./context/navigation.js";
-import { useSettingsContext } from "./context/settings.js";
 import { useSessionState } from "./hooks/session.js";
 import { AccountPage } from "./pages/AccountPage/index.js";
 import { BankFrame } from "./pages/BankFrame.js";
 import { LoginForm } from "./pages/LoginForm.js";
 import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js";
 import { RegistrationPage } from "./pages/RegistrationPage.js";
+import { ShowNotifications } from "./pages/ShowNotifications.js";
 import { SolveChallengePage } from "./pages/SolveChallengePage.js";
 import { WireTransfer } from "./pages/WireTransfer.js";
 import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js";
@@ -58,7 +59,10 @@ export function Routing(): VNode {
   if (session.state.status === "loggedIn") {
     const { isUserAdministrator, username } = session.state;
     return (
-      <BankFrame account={username} 
routeAccountDetails={privatePages.myAccountDetails}>
+      <BankFrame
+        account={username}
+        routeAccountDetails={privatePages.myAccountDetails}
+      >
         <PrivateRouting username={username} isAdmin={isUserAdministrator} />
       </BankFrame>
     );
@@ -90,7 +94,6 @@ function PublicRounting({
 }: {
   onLoggedUser: (username: string, token: AccessToken) => void;
 }): VNode {
-  const settings = useSettingsContext();
   const { i18n } = useTranslationContext();
   const location = useCurrentLocation(publicPages);
   const { navigateTo } = useNavigationContext();
@@ -109,12 +112,11 @@ function PublicRounting({
 
   async function doAutomaticLogin(username: string, password: string) {
     await handleError(async () => {
-      const resp = await authenticator(username)
-        .createAccessToken(password, {
-          scope: "readwrite",
-          duration: { d_us: "forever" },
-          refreshable: true,
-        });
+      const resp = await authenticator(username).createAccessToken(password, {
+        scope: "readwrite",
+        duration: { d_us: "forever" },
+        refreshable: true,
+      });
       if (resp.type === "ok") {
         onLoggedUser(username, resp.body.access_token);
       } else {
@@ -125,6 +127,7 @@ function PublicRounting({
               title: i18n.str`Wrong credentials for "${username}"`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
@@ -132,6 +135,7 @@ function PublicRounting({
               title: i18n.str`Account not found`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           default:
             assertUnreachable(resp);
@@ -198,14 +202,12 @@ export const privatePages = {
     () => "#/account/charge-wallet",
   ),
   homeWireTransfer: urlPattern<{
-    account?: string,
-    subject?: string,
-    amount?: string,
-  }>(
-    /\/account\/wire-transfer/,
-    () => "#/account/wire-transfer",
-  ),
+    account?: string;
+    subject?: string;
+    amount?: string;
+  }>(/\/account\/wire-transfer/, () => "#/account/wire-transfer"),
   home: urlPattern(/\/account/, () => "#/account"),
+  notifications: urlPattern(/\/notifications/, () => "#/notifications"),
   solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
   cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"),
   cashoutDetails: urlPattern<{ cid: string }>(
@@ -213,9 +215,9 @@ export const privatePages = {
     ({ cid }) => `#/cashout/${cid}`,
   ),
   wireTranserCreate: urlPattern<{
-    account?: string,
-    subject?: string,
-    amount?: string,
+    account?: string;
+    subject?: string;
+    amount?: string;
   }>(
     /\/wire-transfer\/(?<account>[a-zA-Z0-9]+)/,
     ({ account }) => `#/wire-transfer/${account}`,
@@ -278,7 +280,6 @@ function PrivateRouting({
 
   switch (location.name) {
     case "operationDetails": {
-
       return (
         <WithdrawalOperationPage
           operationId={location.values.wopid}
@@ -293,7 +294,6 @@ function PrivateRouting({
       );
     }
     case "startOperation": {
-
       return (
         <WithdrawalOperationPage
           operationId={location.values.wopid}
@@ -563,17 +563,19 @@ function PrivateRouting({
       );
     }
     case "conversionConfig": {
-      return <ConversionConfig
-        routeMyAccountCashout={privatePages.myAccountCashouts}
-        routeMyAccountDelete={privatePages.myAccountDelete}
-        routeMyAccountDetails={privatePages.myAccountDetails}
-        routeMyAccountPassword={privatePages.myAccountPassword}
-        routeConversionConfig={privatePages.conversionConfig}
-        routeCancel={privatePages.home}
-        onUpdateSuccess={() => {
-          navigateTo(privatePages.home.url({}))
-        }}
-      />;
+      return (
+        <ConversionConfig
+          routeMyAccountCashout={privatePages.myAccountCashouts}
+          routeMyAccountDelete={privatePages.myAccountDelete}
+          routeMyAccountDetails={privatePages.myAccountDetails}
+          routeMyAccountPassword={privatePages.myAccountPassword}
+          routeConversionConfig={privatePages.conversionConfig}
+          routeCancel={privatePages.home}
+          onUpdateSuccess={() => {
+            navigateTo(privatePages.home.url({}));
+          }}
+        />
+      );
     }
     case "homeWireTransfer": {
       return (
@@ -598,6 +600,9 @@ function PrivateRouting({
         />
       );
     }
+    case "notifications": {
+      return <ShowNotifications />;
+    }
     default:
       assertUnreachable(location);
   }
diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx
index 3a7fafccf..893942059 100644
--- a/packages/bank-ui/src/app.tsx
+++ b/packages/bank-ui/src/app.tsx
@@ -88,7 +88,7 @@ export function App() {
       </TranslationProvider>
     </SettingsProvider>
   );
-};
+}
 
 // @ts-expect-error creating a new property for window object
 window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx 
b/packages/bank-ui/src/components/Cashouts/views.tsx
index 7f16d5840..22b8d8c1b 100644
--- a/packages/bank-ui/src/components/Cashouts/views.tsx
+++ b/packages/bank-ui/src/components/Cashouts/views.tsx
@@ -17,7 +17,6 @@
 import {
   AbsoluteTime,
   Amounts,
-  Duration,
   HttpStatusCode,
   TalerError,
   assertUnreachable,
@@ -32,19 +31,19 @@ import { Fragment, VNode, h } from "preact";
 import { useConversionInfo } from "../../hooks/regional.js";
 import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
 import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
-import { State } from "./index.js";
 import { Time } from "../Time.js";
+import { State } from "./index.js";
 
 export function FailedView({ error }: State.Failed) {
   const { i18n } = useTranslationContext();
   switch (error.case) {
     case HttpStatusCode.NotImplemented: {
       return (
-        <Attention
-          type="danger"
-          title={i18n.str`Cashout are disabled`}
-        >
-          <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+        <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+          <i18n.Translate>
+            Cashout should be enable by configuration and the conversion rate
+            should be initialized with fee, ratio and rounding mode.
+          </i18n.Translate>
         </Attention>
       );
     }
@@ -69,11 +68,11 @@ export function ReadyView({
     switch (resp.case) {
       case HttpStatusCode.NotImplemented: {
         return (
-          <Attention
-            type="danger"
-            title={i18n.str`Cashout are disabled`}
-          >
-            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+            <i18n.Translate>
+              Cashout should be enable by configuration and the conversion rate
+              should be initialized with fee, ratio and rounding mode.
+            </i18n.Translate>
           </Attention>
         );
       }
@@ -89,8 +88,8 @@ export function ReadyView({
         cur.creation_time.t_s === "never"
           ? ""
           : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", {
-            locale: dateLocale,
-          });
+              locale: dateLocale,
+            });
       if (!prev[d]) {
         prev[d] = [];
       }
@@ -156,9 +155,12 @@ export function ReadyView({
                       >
                         <td class="relative py-2 pl-2 pr-2 text-sm ">
                           <div class="font-medium text-gray-900">
-                            <Time format="HH:mm:ss"
-                              
timestamp={AbsoluteTime.fromProtocolTimestamp(item.creation_time)}
-                            // relative={Duration.fromSpec({ days: 1 })} 
+                            <Time
+                              format="HH:mm:ss"
+                              timestamp={AbsoluteTime.fromProtocolTimestamp(
+                                item.creation_time,
+                              )}
+                              // relative={Duration.fromSpec({ days: 1 })}
                             />
                           </div>
                           {
@@ -200,7 +202,6 @@ export function ReadyView({
                         </td>
 
                         <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500 break-all min-w-md">
-
                           {item.subject}
                         </td>
                       </a>
diff --git a/packages/bank-ui/src/components/Time.tsx 
b/packages/bank-ui/src/components/Time.tsx
index 39ce33f60..5c8afe212 100644
--- a/packages/bank-ui/src/components/Time.tsx
+++ b/packages/bank-ui/src/components/Time.tsx
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
 
  GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
@@ -16,16 +16,21 @@
 
 import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
 import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { formatISO, format, formatDuration, intervalToDuration } from 
"date-fns";
+import {
+  formatISO,
+  format,
+  formatDuration,
+  intervalToDuration,
+} from "date-fns";
 import { Fragment, h, VNode } from "preact";
 
 /**
- * 
+ *
  * @param timestamp time to be formatted
  * @param relative duration threshold, if the difference is lower
  * the timestamp will be formatted as relative time from "now"
- * 
- * @returns 
+ *
+ * @returns
  */
 export function Time({
   timestamp,
@@ -33,34 +38,38 @@ export function Time({
   format: formatString,
 }: {
   timestamp: AbsoluteTime | undefined;
-  relative?: Duration,
+  relative?: Duration;
   format: string;
 }): VNode {
-  const { i18n, dateLocale } = useTranslationContext()
-  if (!timestamp) return <Fragment />
+  const { i18n, dateLocale } = useTranslationContext();
+  if (!timestamp) return <Fragment />;
 
   if (timestamp.t_ms === "never") {
-    return <time >{i18n.str`never`}</time>
+    return <time>{i18n.str`never`}</time>;
   }
 
   const now = AbsoluteTime.now();
-  const diff = AbsoluteTime.difference(now, timestamp)
+  const diff = AbsoluteTime.difference(now, timestamp);
   if (relative && now.t_ms !== "never" && Duration.cmp(diff, relative) === -1) 
{
     const d = intervalToDuration({
       start: now.t_ms,
-      end: timestamp.t_ms
-    })
-    d.seconds = 0
-    const duration = formatDuration(d, { locale: dateLocale })
-    const isFuture = AbsoluteTime.cmp(now, timestamp) < 0
+      end: timestamp.t_ms,
+    });
+    d.seconds = 0;
+    const duration = formatDuration(d, { locale: dateLocale });
+    const isFuture = AbsoluteTime.cmp(now, timestamp) < 0;
     if (isFuture) {
-      return <time dateTime={formatISO(timestamp.t_ms)}>
-        <i18n.Translate>in {duration}</i18n.Translate>
-      </time>
+      return (
+        <time dateTime={formatISO(timestamp.t_ms)}>
+          <i18n.Translate>in {duration}</i18n.Translate>
+        </time>
+      );
     } else {
-      return <time dateTime={formatISO(timestamp.t_ms)}>
-        <i18n.Translate>{duration} ago</i18n.Translate>
-      </time>
+      return (
+        <time dateTime={formatISO(timestamp.t_ms)}>
+          <i18n.Translate>{duration} ago</i18n.Translate>
+        </time>
+      );
     }
   }
   return (
diff --git a/packages/bank-ui/src/components/Transactions/index.ts 
b/packages/bank-ui/src/components/Transactions/index.ts
index c8bb1e108..4cad6f306 100644
--- a/packages/bank-ui/src/components/Transactions/index.ts
+++ b/packages/bank-ui/src/components/Transactions/index.ts
@@ -23,11 +23,13 @@ import { RouteDefinition } from "../../route.js";
 
 export interface Props {
   account: string;
-  routeCreateWireTransfer: RouteDefinition<{
-    account?: string,
-    subject?: string,
-    amount?: string,
-  }> | undefined;
+  routeCreateWireTransfer:
+    | RouteDefinition<{
+        account?: string;
+        subject?: string;
+        amount?: string;
+      }>
+    | undefined;
 }
 
 export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -49,11 +51,13 @@ export namespace State {
   export interface Ready extends BaseInfo {
     status: "ready";
     error: undefined;
-    routeCreateWireTransfer: RouteDefinition<{
-      account?: string,
-      subject?: string,
-      amount?: string,
-    }> | undefined;
+    routeCreateWireTransfer:
+      | RouteDefinition<{
+          account?: string;
+          subject?: string;
+          amount?: string;
+        }>
+      | undefined;
     transactions: Transaction[];
     onGoStart?: () => void;
     onGoNext?: () => void;
diff --git a/packages/bank-ui/src/components/Transactions/state.ts 
b/packages/bank-ui/src/components/Transactions/state.ts
index 3e9103b59..e792ddfa0 100644
--- a/packages/bank-ui/src/components/Transactions/state.ts
+++ b/packages/bank-ui/src/components/Transactions/state.ts
@@ -23,7 +23,10 @@ import {
 import { useTransactions } from "../../hooks/account.js";
 import { Props, State, Transaction } from "./index.js";
 
-export function useComponentState({ account, routeCreateWireTransfer }: 
Props): State {
+export function useComponentState({
+  account,
+  routeCreateWireTransfer,
+}: Props): State {
   const txResult = useTransactions(account);
   if (!txResult) {
     return {
@@ -38,36 +41,35 @@ export function useComponentState({ account, 
routeCreateWireTransfer }: Props):
     };
   }
 
-  const transactions =
-      txResult.result
-        .map((tx) => {
-          const negative = tx.direction === "debit";
-          const cp = parsePaytoUri(
-            negative ? tx.creditor_payto_uri : tx.debtor_payto_uri,
-          );
-          const counterpart =
-            (cp === undefined || !cp.isKnown
-              ? undefined
-              : cp.targetType === "iban"
-                ? cp.iban
-                : cp.targetType === "x-taler-bank"
-                  ? cp.account
-                  : cp.targetType === "bitcoin"
-                    ? `${cp.targetPath.substring(0, 6)}...`
-                    : undefined) ?? "unknown";
+  const transactions = txResult.result
+    .map((tx) => {
+      const negative = tx.direction === "debit";
+      const cp = parsePaytoUri(
+        negative ? tx.creditor_payto_uri : tx.debtor_payto_uri,
+      );
+      const counterpart =
+        (cp === undefined || !cp.isKnown
+          ? undefined
+          : cp.targetType === "iban"
+            ? cp.iban
+            : cp.targetType === "x-taler-bank"
+              ? cp.account
+              : cp.targetType === "bitcoin"
+                ? `${cp.targetPath.substring(0, 6)}...`
+                : undefined) ?? "unknown";
 
-          const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
-          const amount = Amounts.parse(tx.amount);
-          const subject = tx.subject;
-          return {
-            negative,
-            counterpart,
-            when,
-            amount,
-            subject,
-          };
-        })
-        .filter((x): x is Transaction => x !== undefined);
+      const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+      const amount = Amounts.parse(tx.amount);
+      const subject = tx.subject;
+      return {
+        negative,
+        counterpart,
+        when,
+        amount,
+        subject,
+      };
+    })
+    .filter((x): x is Transaction => x !== undefined);
 
   return {
     status: "ready",
diff --git a/packages/bank-ui/src/components/Transactions/views.tsx 
b/packages/bank-ui/src/components/Transactions/views.tsx
index 7da9fc5a9..417b34c71 100644
--- a/packages/bank-ui/src/components/Transactions/views.tsx
+++ b/packages/bank-ui/src/components/Transactions/views.tsx
@@ -19,9 +19,8 @@ import { format } from "date-fns";
 import { Fragment, VNode, h } from "preact";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
-import { State } from "./index.js";
-import { Duration } from "@gnu-taler/taler-util";
 import { Time } from "../Time.js";
+import { State } from "./index.js";
 
 export function ReadyView({
   transactions,
@@ -30,24 +29,26 @@ export function ReadyView({
   onGoStart,
 }: State.Ready): VNode {
   const { i18n, dateLocale } = useTranslationContext();
-  const { config } = useBankCoreApiContext()
+  const { config } = useBankCoreApiContext();
 
   if (!transactions.length) {
-    return <div class="px-4 mt-4">
-      <div class="sm:flex sm:items-center">
-        <div class="sm:flex-auto">
-          <h1 class="text-base font-semibold leading-6 text-gray-900">
-            <i18n.Translate>Transactions history</i18n.Translate>
-          </h1>
+    return (
+      <div class="px-4 mt-4">
+        <div class="sm:flex sm:items-center">
+          <div class="sm:flex-auto">
+            <h1 class="text-base font-semibold leading-6 text-gray-900">
+              <i18n.Translate>Transactions history</i18n.Translate>
+            </h1>
+          </div>
         </div>
-      </div>
 
-      <Attention type="low" title={i18n.str`No transactions yet.`}>
-        <i18n.Translate>
-          You can start sending a wire transfer or withdrawing to your wallet.
-        </i18n.Translate>
-      </Attention>
-    </div>;
+        <Attention type="low" title={i18n.str`No transactions yet.`}>
+          <i18n.Translate>
+            You can start sending a wire transfer or withdrawing to your 
wallet.
+          </i18n.Translate>
+        </Attention>
+      </div>
+    );
   }
 
   const txByDate = transactions.reduce(
@@ -116,9 +117,10 @@ export function ReadyView({
                       >
                         <td class="relative py-2 pl-2 pr-2 text-sm ">
                           <div class="font-medium text-gray-900">
-                            <Time format="HH:mm:ss"
+                            <Time
+                              format="HH:mm:ss"
                               timestamp={item.when}
-                            // relative={Duration.fromSpec({ days: 1 })} 
+                              // relative={Duration.fromSpec({ days: 1 })}
                             />
                           </div>
                           <dl class="font-normal sm:hidden">
@@ -153,7 +155,9 @@ export function ReadyView({
                             </dt>
                             <dd class="mt-1 truncate text-gray-500 sm:hidden">
                               {item.negative ? i18n.str`to` : 
i18n.str`from`}{" "}
-                              {!routeCreateWireTransfer ? item.counterpart :
+                              {!routeCreateWireTransfer ? (
+                                item.counterpart
+                              ) : (
                                 <a
                                   name={`transfer to ${item.counterpart}`}
                                   href={routeCreateWireTransfer.url({
@@ -163,7 +167,7 @@ export function ReadyView({
                                 >
                                   {item.counterpart}
                                 </a>
-                              }
+                              )}
                             </dd>
                             <dd class="mt-1 text-gray-500 sm:hidden">
                               <pre class="break-words w-56 
whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
@@ -190,7 +194,9 @@ export function ReadyView({
                           )}
                         </td>
                         <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500">
-                          {!routeCreateWireTransfer ? item.counterpart :
+                          {!routeCreateWireTransfer ? (
+                            item.counterpart
+                          ) : (
                             <a
                               name={`wire transfer to ${item.counterpart}`}
                               href={routeCreateWireTransfer.url({
@@ -200,7 +206,7 @@ export function ReadyView({
                             >
                               {item.counterpart}
                             </a>
-                          }
+                          )}
                         </td>
                         <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500 break-all min-w-md">
                           {item.subject}
diff --git a/packages/bank-ui/src/context/config.ts 
b/packages/bank-ui/src/context/config.ts
index cb0d599aa..f8be80a6c 100644
--- a/packages/bank-ui/src/context/config.ts
+++ b/packages/bank-ui/src/context/config.ts
@@ -26,11 +26,12 @@ import {
   TalerError,
   assertUnreachable,
   CacheEvictor,
+  ObservabilityEvent,
 } from "@gnu-taler/taler-util";
 import {
   BrowserFetchHttpLib,
   ErrorLoading,
-  useTranslationContext
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import {
   ComponentChildren,
@@ -63,6 +64,8 @@ export type Type = {
   conversion: TalerBankConversionHttpClient;
   authenticator: (user: string) => TalerAuthenticationHttpClient;
   hints: VersionHint[];
+  onBackendActivity: (fn: Listener) => Unsuscriber;
+  cancelRequest: (eventId: string) => void;
 };
 
 // FIXME: below
@@ -78,6 +81,25 @@ export enum VersionHint {
   CASHOUT_BEFORE_2FA,
 }
 
+const observers = new Array<(e: ObservabilityEvent) => void>();
+type Listener = (e: ObservabilityEvent) => void;
+type Unsuscriber = () => void;
+
+const activity = Object.freeze({
+  notify: (data: ObservabilityEvent) =>
+    observers.forEach((observer) => observer(data)),
+  subscribe: (func: Listener): Unsuscriber => {
+    observers.push(func);
+    return () => {
+      observers.forEach((observer, index) => {
+        if (observer === func) {
+          observers.splice(index, 1);
+        }
+      });
+    };
+  },
+});
+
 export type ConfigResult =
   | undefined
   | { type: "ok"; config: TalerCorebankApi.Config; hints: VersionHint[] }
@@ -96,7 +118,8 @@ export const BankCoreApiProvider = ({
   const [checked, setChecked] = useState<ConfigResult>();
   const { i18n } = useTranslationContext();
 
-  const { bankClient, conversionClient, authClient } = buildApiClient(new 
URL(baseUrl))
+  const { bankClient, conversionClient, authClient, cancelRequest } =
+    buildApiClient(new URL(baseUrl));
 
   useEffect(() => {
     bankClient
@@ -150,8 +173,10 @@ export const BankCoreApiProvider = ({
     url: new URL(bankClient.baseUrl),
     config: checked.config,
     bank: bankClient,
+    onBackendActivity: activity.subscribe,
     conversion: conversionClient,
     authenticator: authClient,
+    cancelRequest,
     hints: checked.hints,
   };
   return h(Context.Provider, {
@@ -162,8 +187,8 @@ export const BankCoreApiProvider = ({
 
 /**
  * build http client with cache breaker due to SWR
- * @param url 
- * @returns 
+ * @param url
+ * @returns
  */
 function buildApiClient(url: URL) {
   const httpFetch = new BrowserFetchHttpLib({
@@ -172,15 +197,32 @@ function buildApiClient(url: URL) {
   });
   const httpLib = new ObservableHttpClientLibrary(httpFetch, {
     observe(ev) {
-      console.log(ev)
-    }
-  })
+      activity.notify(ev);
+    },
+  });
 
-  const bankClient = new TalerCoreBankHttpClient(url.href, httpLib, 
evictBankSwrCache);
-  const conversionClient = new 
TalerBankConversionHttpClient(bankClient.getConversionInfoAPI().href, httpLib, 
evictConversionSwrCache);
-  const authClient = (user: string) => new 
TalerAuthenticationHttpClient(bankClient.getAuthenticationAPI(user).href, user, 
httpLib);
+  function cancelRequest(id: string) {
+    httpLib.cancelRequest(id);
+  }
 
-  return { bankClient, conversionClient, authClient }
+  const bankClient = new TalerCoreBankHttpClient(
+    url.href,
+    httpLib,
+    evictBankSwrCache,
+  );
+  const conversionClient = new TalerBankConversionHttpClient(
+    bankClient.getConversionInfoAPI().href,
+    httpLib,
+    evictConversionSwrCache,
+  );
+  const authClient = (user: string) =>
+    new TalerAuthenticationHttpClient(
+      bankClient.getAuthenticationAPI(user).href,
+      user,
+      httpLib,
+    );
+
+  return { bankClient, conversionClient, authClient, cancelRequest };
 }
 
 export const BankCoreApiProviderTesting = ({
@@ -206,7 +248,6 @@ export const BankCoreApiProviderTesting = ({
   });
 };
 
-
 const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
   async notifySuccess(op) {
     switch (op) {
@@ -215,7 +256,7 @@ const evictBankSwrCache: 
CacheEvictor<TalerCoreBankCacheEviction> = {
           revalidatePublicAccounts(),
           revalidateBusinessAccounts(),
         ]);
-        return
+        return;
       }
       case TalerCoreBankCacheEviction.CREATE_ACCOUNT: {
         // admin balance change on new account
@@ -224,27 +265,25 @@ const evictBankSwrCache: 
CacheEvictor<TalerCoreBankCacheEviction> = {
           revalidateTransactions(),
           revalidatePublicAccounts(),
           revalidateBusinessAccounts(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.UPDATE_ACCOUNT: {
-        await Promise.all([
-          revalidateAccountDetails(),
-        ])
+        await Promise.all([revalidateAccountDetails()]);
         return;
       }
       case TalerCoreBankCacheEviction.CREATE_TRANSACTION: {
         await Promise.all([
           revalidateAccountDetails(),
           revalidateTransactions(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.CONFIRM_WITHDRAWAL: {
         await Promise.all([
           revalidateAccountDetails(),
           revalidateTransactions(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.CREATE_CASHOUT: {
@@ -252,7 +291,7 @@ const evictBankSwrCache: 
CacheEvictor<TalerCoreBankCacheEviction> = {
           revalidateAccountDetails(),
           revalidateCashouts(),
           revalidateTransactions(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.UPDATE_PASSWORD:
@@ -260,20 +299,21 @@ const evictBankSwrCache: 
CacheEvictor<TalerCoreBankCacheEviction> = {
       case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL:
         return;
       default:
-        assertUnreachable(op)
+        assertUnreachable(op);
     }
-  }
-}
+  },
+};
 
-const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> 
= {
-  async notifySuccess(op) {
-    switch (op) {
-      case TalerBankConversionCacheEviction.UPDATE_RATE: {
-        await revalidateConversionInfo();
-        return
+const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> =
+  {
+    async notifySuccess(op) {
+      switch (op) {
+        case TalerBankConversionCacheEviction.UPDATE_RATE: {
+          await revalidateConversionInfo();
+          return;
+        }
+        default:
+          assertUnreachable(op);
       }
-      default:
-        assertUnreachable(op)
-    }
-  }
-}
\ No newline at end of file
+    },
+  };
diff --git a/packages/bank-ui/src/hooks/account.ts 
b/packages/bank-ui/src/hooks/account.ts
index aa0745253..5fe12573c 100644
--- a/packages/bank-ui/src/hooks/account.ts
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -62,7 +62,11 @@ export function useAccountDetails(account: string) {
 }
 
 export function revalidateWithdrawalDetails() {
-  return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"getWithdrawalById", undefined, { revalidate: true });
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById",
+    undefined,
+    { revalidate: true },
+  );
 }
 
 export function useWithdrawalDetails(wid: string) {
@@ -110,7 +114,9 @@ export function useWithdrawalDetails(wid: string) {
 
 export function revalidateTransactionDetails() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === 
"getTransactionById", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === 
"getTransactionById",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useTransactionDetails(account: string, tid: number) {
@@ -149,7 +155,9 @@ export function useTransactionDetails(account: string, tid: 
number) {
 
 export async function revalidatePublicAccounts() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === 
"getPublicAccounts", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts",
+    undefined,
+    { revalidate: true },
   );
 }
 export function usePublicAccounts(
@@ -193,9 +201,10 @@ export function usePublicAccounts(
     data && data.type === "ok" && data.body.public_accounts.length <= 
PAGE_SIZE;
   const isFirstPage = !offset;
 
-  const result = data && data.type == "ok" ? 
structuredClone(data.body.public_accounts) : []
-  if (result.length == PAGE_SIZE+1) {
-    result.pop()
+  const result =
+    data && data.type == "ok" ? structuredClone(data.body.public_accounts) : 
[];
+  if (result.length == PAGE_SIZE + 1) {
+    result.pop();
   }
   const pagination = {
     result,
@@ -243,7 +252,7 @@ export function useTransactions(account: string, initial?: 
number) {
     return await api.getTransactions(
       { username, token },
       {
-        limit: PAGE_SIZE +1 ,
+        limit: PAGE_SIZE + 1,
         offset: txid ? String(txid) : undefined,
         order: "dec",
       },
@@ -267,9 +276,10 @@ export function useTransactions(account: string, initial?: 
number) {
     data && data.type === "ok" && data.body.transactions.length <= PAGE_SIZE;
   const isFirstPage = !offset;
 
-  const result = data && data.type == "ok" ? 
structuredClone(data.body.transactions) : []
-  if (result.length == PAGE_SIZE+1) {
-    result.pop()
+  const result =
+    data && data.type == "ok" ? structuredClone(data.body.transactions) : [];
+  if (result.length == PAGE_SIZE + 1) {
+    result.pop();
   }
   const pagination = {
     result,
diff --git a/packages/bank-ui/src/hooks/bank-state.ts 
b/packages/bank-ui/src/hooks/bank-state.ts
index 83bb009cf..1d8c4f9e6 100644
--- a/packages/bank-ui/src/hooks/bank-state.ts
+++ b/packages/bank-ui/src/hooks/bank-state.ts
@@ -118,7 +118,7 @@ const codecForChallengeConfirmWithdrawal =
       .property("request", codecForString())
       .build("ConfirmWithdrawalChallenge");
 
-const codecForAppLocation = codecForString as () => Codec<AppLocation>
+const codecForAppLocation = codecForString as () => Codec<AppLocation>;
 
 const codecForChallengeCashout = (): Codec<CashoutChallenge> =>
   buildCodecForObject<CashoutChallenge>()
@@ -141,8 +141,6 @@ const codecForChallenge = (): Codec<ChallengeInProgess> =>
     .alternative("update-password", codecForChallengeUpdatePassword())
     .build("ChallengeInProgess");
 
-
-    
 interface BankState {
   currentWithdrawalOperationId: string | undefined;
   currentChallenge: ChallengeInProgess | undefined;
@@ -163,10 +161,10 @@ const BANK_STATE_KEY = buildStorageKey("bank-app-state", 
codecForBankState());
 
 /**
  * Client state saved in local storage.
- * 
+ *
  * This information is saved in the client because
  * the backend server session API is not enough.
- * 
+ *
  * @returns tuple of [state, update(), reset()]
  */
 export function useBankState(): [
@@ -185,4 +183,3 @@ export function useBankState(): [
   }
   return [value, updateField, reset];
 }
-
diff --git a/packages/bank-ui/src/hooks/form.ts 
b/packages/bank-ui/src/hooks/form.ts
index 26354b108..afa4912eb 100644
--- a/packages/bank-ui/src/hooks/form.ts
+++ b/packages/bank-ui/src/hooks/form.ts
@@ -14,87 +14,102 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AmountJson, TalerBankConversionApi, TranslatedString } from 
"@gnu-taler/taler-util";
+import { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
 import { useState } from "preact/hooks";
 
 export type UIField = {
   value: string | undefined;
   onUpdate: (s: string) => void;
   error: TranslatedString | undefined;
-}
+};
 
 type FormHandler<T> = {
-  [k in keyof T]?:
-  T[k] extends string ? UIField :
-  T[k] extends AmountJson ? UIField :
-  FormHandler<T[k]>;
-}
+  [k in keyof T]?: T[k] extends string
+    ? UIField
+    : T[k] extends AmountJson
+      ? UIField
+      : FormHandler<T[k]>;
+};
 
 export type FormValues<T> = {
-  [k in keyof T]:
-  T[k] extends string ? (string | undefined) :
-  T[k] extends AmountJson ? (string | undefined) :
-  FormValues<T[k]>;
-}
+  [k in keyof T]: T[k] extends string
+    ? string | undefined
+    : T[k] extends AmountJson
+      ? string | undefined
+      : FormValues<T[k]>;
+};
 
 export type RecursivePartial<T> = {
-  [k in keyof T]?:
-  T[k] extends string ? (string) :
-  T[k] extends AmountJson ? (AmountJson) :
-  RecursivePartial<T[k]>;
-}
+  [k in keyof T]?: T[k] extends string
+    ? string
+    : T[k] extends AmountJson
+      ? AmountJson
+      : RecursivePartial<T[k]>;
+};
 
 export type FormErrors<T> = {
-  [k in keyof T]?:
-  T[k] extends string ? (TranslatedString) :
-  T[k] extends AmountJson ? (TranslatedString) :
-  FormErrors<T[k]>;
-}
-
-export type FormStatus<T> = {
-  status: "ok",
-  result: T,
-  errors: undefined,
-} | {
-  status: "fail",
-  result: RecursivePartial<T>,
-  errors: FormErrors<T>,
-}
-
-
-function constructFormHandler<T>(form: FormValues<T>, updateForm: (d: 
FormValues<T>) => void, errors: FormErrors<T> | undefined): FormHandler<T> {
-  const keys = (Object.keys(form) as Array<keyof T>)
+  [k in keyof T]?: T[k] extends string
+    ? TranslatedString
+    : T[k] extends AmountJson
+      ? TranslatedString
+      : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+  | {
+      status: "ok";
+      result: T;
+      errors: undefined;
+    }
+  | {
+      status: "fail";
+      result: RecursivePartial<T>;
+      errors: FormErrors<T>;
+    };
+
+function constructFormHandler<T>(
+  form: FormValues<T>,
+  updateForm: (d: FormValues<T>) => void,
+  errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+  const keys = Object.keys(form) as Array<keyof T>;
 
   const handler = keys.reduce((prev, fieldName) => {
-    const currentValue: any = form[fieldName];
-    const currentError: any = errors ? errors[fieldName] : undefined;
-    function updater(newValue: any) {
-      updateForm({ ...form, [fieldName]: newValue })
+    const currentValue: unknown = form[fieldName];
+    const currentError: unknown = errors ? errors[fieldName] : undefined;
+    function updater(newValue: unknown) {
+      updateForm({ ...form, [fieldName]: newValue });
     }
     if (typeof currentValue === "object") {
-      const group = constructFormHandler(currentValue, updater, currentError)
-      // @ts-expect-error asdasd
-      prev[fieldName] = group
+      // @ts-expect-error FIXME better typing
+      const group = constructFormHandler(currentValue, updater, currentError);
+      // @ts-expect-error FIXME better typing
+      prev[fieldName] = group;
       return prev;
     }
     const field: UIField = {
+      // @ts-expect-error FIXME better typing
       error: currentError,
+      // @ts-expect-error FIXME better typing
       value: currentValue,
-      onUpdate: updater
-    }
-    // @ts-expect-error asdasd
-    prev[fieldName] = field
-    return prev
-  }, {} as FormHandler<T>)
+      onUpdate: updater,
+    };
+    // @ts-expect-error FIXME better typing
+    prev[fieldName] = field;
+    return prev;
+  }, {} as FormHandler<T>);
 
   return handler;
 }
 
-export function useFormState<T>(defaultValue: FormValues<T>, check: (f: 
FormValues<T>) => FormStatus<T>): [FormHandler<T>, FormStatus<T>] {
-  const [form, updateForm] = useState<FormValues<T>>(defaultValue)
+export function useFormState<T>(
+  defaultValue: FormValues<T>,
+  check: (f: FormValues<T>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+  const [form, updateForm] = useState<FormValues<T>>(defaultValue);
 
-  const status = check(form)
-  const handler = constructFormHandler(form, updateForm, status.errors)
+  const status = check(form);
+  const handler = constructFormHandler(form, updateForm, status.errors);
 
-  return [handler, status]
-}
\ No newline at end of file
+  return [handler, status];
+}
diff --git a/packages/bank-ui/src/hooks/preferences.ts 
b/packages/bank-ui/src/hooks/preferences.ts
index 454dc8d80..bb3dcb153 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -61,7 +61,7 @@ const BANK_PREFERENCES_KEY = buildStorageKey(
 );
 /**
  * User preferences.
- * 
+ *
  * @returns tuple of [state, update()]
  */
 export function usePreferences(): [
@@ -109,4 +109,3 @@ export function getLabelForPreferences(
       return i18n.str`Show debug info`;
   }
 }
-
diff --git a/packages/bank-ui/src/hooks/regional.ts 
b/packages/bank-ui/src/hooks/regional.ts
index bf948d293..51f3edad4 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -31,18 +31,20 @@ import {
   TalerHttpError,
   opFixedSuccess,
 } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
 import _useSWR, { SWRHook, mutate } from "swr";
 import { useBankCoreApiContext } from "../context/config.js";
-import { useState } from "preact/hooks";
 
 // FIX default import https://github.com/microsoft/TypeScript/issues/49189
 const useSWR = _useSWR as unknown as SWRHook;
 
-export type TransferCalculation = {
-  debit: AmountJson;
-  credit: AmountJson;
-  beforeFee: AmountJson;
-} | "amount-is-too-small";
+export type TransferCalculation =
+  | {
+      debit: AmountJson;
+      credit: AmountJson;
+      beforeFee: AmountJson;
+    }
+  | "amount-is-too-small";
 type EstimatorFunction = (
   amount: AmountJson,
   fee: AmountJson,
@@ -95,7 +97,7 @@ export function useCashinEstimator(): ConversionEstimators {
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call 
this function
@@ -120,7 +122,7 @@ export function useCashinEstimator(): ConversionEstimators {
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call 
this function
@@ -142,7 +144,7 @@ export function useCashinEstimator(): ConversionEstimators {
 }
 
 export function useCashoutEstimator(): ConversionEstimators {
-  const { bank, conversion } = useBankCoreApiContext();
+  const { conversion } = useBankCoreApiContext();
   return {
     estimateByCredit: async (fiatAmount, fee) => {
       const resp = await conversion.getCashoutRate({
@@ -151,7 +153,7 @@ export function useCashoutEstimator(): ConversionEstimators 
{
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call 
this function
@@ -176,7 +178,7 @@ export function useCashoutEstimator(): ConversionEstimators 
{
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call 
this function
@@ -201,11 +203,15 @@ export function useCashoutEstimator(): 
ConversionEstimators {
  * @deprecated use useCashoutEstimator
  */
 export function useEstimator(): ConversionEstimators {
-  return useCashoutEstimator()
+  return useCashoutEstimator();
 }
 
 export async function revalidateBusinessAccounts() {
-  return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"getAccounts", undefined, { revalidate: true });
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "getAccounts",
+    undefined,
+    { revalidate: true },
+  );
 }
 export function useBusinessAccounts() {
   const { state: credentials } = useSessionState();
@@ -247,9 +253,10 @@ export function useBusinessAccounts() {
     data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE;
   const isFirstPage = !offset;
 
-  const result = data && data.type == "ok" ? 
structuredClone(data.body.accounts) : []
+  const result =
+    data && data.type == "ok" ? structuredClone(data.body.accounts) : [];
   if (result.length == PAGE_SIZE + 1) {
-    result.pop()
+    result.pop();
   }
   const pagination = {
     result,
@@ -276,7 +283,9 @@ function notUndefined(c: CashoutWithId | undefined): c is 
CashoutWithId {
 export function revalidateOnePendingCashouts() {
   return mutate(
     (key) =>
-      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", 
undefined, { revalidate: true }
+      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useOnePendingCashouts(account: string) {
@@ -290,7 +299,8 @@ export function useOnePendingCashouts(account: string) {
     if (list.type !== "ok") {
       return list;
     }
-    const pendingCashout = list.body.cashouts.length > 0 ? 
list.body.cashouts[0] : undefined;
+    const pendingCashout =
+      list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
     if (!pendingCashout) return opFixedSuccess(undefined);
     const cashoutInfo = await api.getCashoutById(
       { username, token },
@@ -334,7 +344,9 @@ export function useOnePendingCashouts(account: string) {
 }
 
 export function revalidateCashouts() {
-  return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"useCashouts");
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "useCashouts",
+  );
 }
 export function useCashouts(account: string) {
   const { state: credentials } = useSessionState();
@@ -357,7 +369,7 @@ export function useCashouts(account: string) {
       }),
     );
     const cashouts = all.filter(notUndefined);
-    return { type: "ok" as const, body: { cashouts }};
+    return { type: "ok" as const, body: { cashouts } };
   }
   const { data, error } = useSWR<
     | OperationOk<{ cashouts: CashoutWithId[] }>
@@ -386,7 +398,9 @@ export function useCashouts(account: string) {
 
 export function revalidateCashoutDetails() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", 
undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useCashoutDetails(cashoutId: number | undefined) {
@@ -435,7 +449,9 @@ export type LastMonitor = {
 };
 export function revalidateLastMonitorInfo() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === 
"useLastMonitorInfo", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === 
"useLastMonitorInfo",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useLastMonitorInfo(
diff --git a/packages/bank-ui/src/pages/AccountPage/index.ts 
b/packages/bank-ui/src/pages/AccountPage/index.ts
index 7776fbaa3..757346c5c 100644
--- a/packages/bank-ui/src/pages/AccountPage/index.ts
+++ b/packages/bank-ui/src/pages/AccountPage/index.ts
@@ -88,14 +88,14 @@ export namespace State {
     routeChargeWallet: RouteDefinition;
     routePublicAccounts: RouteDefinition;
     routeWireTransfer: RouteDefinition<{
-      account?: string,
-      subject?: string,
-      amount?: string,
+      account?: string;
+      subject?: string;
+      amount?: string;
     }>;
     routeCreateWireTransfer: RouteDefinition<{
-      account?: string,
-      subject?: string,
-      amount?: string,
+      account?: string;
+      subject?: string;
+      amount?: string;
     }>;
     routeOperationDetails: RouteDefinition<{ wopid: string }>;
     routeSolveSecondFactor: RouteDefinition;
diff --git a/packages/bank-ui/src/pages/AccountPage/views.tsx 
b/packages/bank-ui/src/pages/AccountPage/views.tsx
index 7ad00cf1d..3a182ed1b 100644
--- a/packages/bank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/bank-ui/src/pages/AccountPage/views.tsx
@@ -32,7 +32,9 @@ export function InvalidIbanView({ error }: State.InvalidIban) 
{
 
 const IS_PUBLIC_ACCOUNT_ENABLED = false;
 
-function ShowDemoInfo({ routePublicAccounts }: {
+function ShowDemoInfo({
+  routePublicAccounts,
+}: {
   routePublicAccounts: RouteDefinition;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -50,7 +52,10 @@ function ShowDemoInfo({ routePublicAccounts }: {
           This part of the demo shows how a bank that supports Taler directly
           would work. In addition to using your own bank account, you can also
           see the transaction history of some{" "}
-          <a name="public account" href={routePublicAccounts.url({})}>Public 
Accounts</a>.
+          <a name="public account" href={routePublicAccounts.url({})}>
+            Public Accounts
+          </a>
+          .
         </i18n.Translate>
       ) : (
         <i18n.Translate>
@@ -62,7 +67,9 @@ function ShowDemoInfo({ routePublicAccounts }: {
   );
 }
 
-function ShowPedingOperation({ routeSolveSecondFactor }: {
+function ShowPedingOperation({
+  routeSolveSecondFactor,
+}: {
   routeSolveSecondFactor: RouteDefinition;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -140,7 +147,10 @@ export function ReadyView({
         onOperationCreated={onOperationCreated}
         onAuthorizationRequired={onAuthorizationRequired}
       />
-      <Transactions account={account} 
routeCreateWireTransfer={routeCreateWireTransfer} />
+      <Transactions
+        account={account}
+        routeCreateWireTransfer={routeCreateWireTransfer}
+      />
     </Fragment>
   );
 }
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx 
b/packages/bank-ui/src/pages/BankFrame.tsx
index 427e9a156..39f042455 100644
--- a/packages/bank-ui/src/pages/BankFrame.tsx
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -14,7 +14,14 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  Amounts,
+  ObservabilityEventType,
+  TalerError,
+  TranslatedString,
+  assertUnreachable,
+} from "@gnu-taler/taler-util";
 import {
   Footer,
   Header,
@@ -22,22 +29,23 @@ import {
   ToastBanner,
   notifyError,
   notifyException,
-  useTranslationContext
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useSettingsContext } from "../context/settings.js";
 import { useAccountDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
 import { useBankState } from "../hooks/bank-state.js";
 import {
   getAllBooleanPreferences,
   getLabelForPreferences,
   usePreferences,
 } from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
 import { RouteDefinition } from "../route.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { privatePages } from "../Routing.js";
 
 const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : 
undefined;
 const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -85,13 +93,18 @@ export function BankFrame({
           title="Bank"
           iconLinkURL={settings.iconLinkURL ?? "#"}
           profileURL={routeAccountDetails?.url({})}
+          notificationURL={
+            preferences.showDebugInfo
+              ? privatePages.notifications.url({})
+              : undefined
+          }
           onLogout={
             session.state.status !== "loggedIn"
               ? undefined
               : () => {
-                session.logOut();
-                resetBankState();
-              }
+                  session.logOut();
+                  resetBankState();
+                }
           }
           sites={
             !settings.topNavSites ? [] : Object.entries(settings.topNavSites)
@@ -102,11 +115,11 @@ export function BankFrame({
             <div class="text-xs font-semibold leading-6 text-gray-400">
               <i18n.Translate>Preferences</i18n.Translate>
             </div>
-            <ul role="list" class="space-y-1">
+            <ul role="list" class="space-y-4">
               {getAllBooleanPreferences().map((set) => {
                 const isOn: boolean = !!preferences[set];
                 return (
-                  <li key={set} class="mt-2 pl-2">
+                  <li key={set} class="pl-2">
                     <div class="flex items-center justify-between">
                       <span class="flex flex-grow flex-col">
                         <span
@@ -144,19 +157,23 @@ export function BankFrame({
         </Header>
       </div>
 
-      <div class="fixed z-20 w-full">
+      <div class="fixed z-20 top-14 w-full">
         <div class="mx-auto w-4/5">
           <ToastBanner />
+          {/* <Attention type="success" title={"hola" as TranslatedString} 
onClose={() => { }} /> */}
         </div>
       </div>
 
       <main class="-mt-32 flex-1">
         {account && routeAccountDetails && (
-          <header class="py-5 bg-indigo-600   ">
+          <header class="py-6 bg-indigo-600">
             <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
               <h1 class=" flex flex-wrap items-center justify-between 
sm:flex-nowrap">
                 <span class="text-2xl font-bold tracking-tight text-white">
-                  <WelcomeAccount account={account} 
routeAccountDetails={routeAccountDetails} />
+                  <WelcomeAccount
+                    account={account}
+                    routeAccountDetails={routeAccountDetails}
+                  />
                 </span>
                 <span class="text-2xl font-bold tracking-tight text-white">
                   <AccountBalance account={account} />
@@ -166,13 +183,15 @@ export function BankFrame({
           </header>
         )}
 
-        <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
+        <div class="mx-auto max-w-7xl px-4 pb-4 sm:px-6 lg:px-8">
           <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
             {children}
           </div>
         </div>
       </main>
 
+      <AppActivity />
+
       <Footer
         testingUrlKey="corebank-api-base-url"
         GIT_HASH={GIT_HASH}
@@ -182,8 +201,117 @@ export function BankFrame({
   );
 }
 
-function WelcomeAccount({ account, routeAccountDetails }: {
-  account: string,
+function Wait({ class: clazz }: { class?: string }): VNode {
+  return (
+    <Fragment>
+      <style>{`
+      .animated-loader {
+        display: inline-block;
+        --b: 5px; 
+        border-radius: 50%;
+        aspect-ratio: 1;
+        padding: 1px;
+        background: conic-gradient(#0000 10%,#4f46e5) content-box;
+        -webkit-mask:
+          repeating-conic-gradient(#0000 0deg,#000 1deg 20deg,#0000 21deg 
36deg),
+          radial-gradient(farthest-side,#0000 calc(100% - var(--b) - 1px),#000 
calc(100% - var(--b)));
+        -webkit-mask-composite: destination-in;
+                mask-composite: intersect;
+        animation:spinning-loader 1s infinite steps(10);
+      }
+      @keyframes spinning-loader {to{transform: rotate(1turn)}}    
+    `}</style>
+      <div class={`animated-loader ${clazz}`} />
+    </Fragment>
+  );
+}
+
+function AppActivity(): VNode {
+  const [lastEvent, setLastEvent] = useState<{
+    url: string;
+    id: string;
+    when: AbsoluteTime;
+  }>();
+  const [status, setStatus] = useState<"ok" | "fail">();
+  const d = useBankCoreApiContext();
+  const onBackendActivity = !d ? undefined : d.onBackendActivity;
+  const cancelRequest = !d ? undefined : d.cancelRequest;
+  const [pref] = usePreferences();
+  useEffect(() => {
+    // console.log("ASDASDS", onBackendActivity)
+    if (!pref.showDebugInfo) return;
+    if (!onBackendActivity) return;
+    return onBackendActivity((ev) => {
+      switch (ev.type) {
+        case ObservabilityEventType.HttpFetchStart: {
+          setLastEvent(ev);
+          setStatus(undefined);
+          return;
+        }
+        case ObservabilityEventType.HttpFetchFinishError: {
+          setStatus("fail");
+          return;
+        }
+        case ObservabilityEventType.HttpFetchFinishSuccess: {
+          setStatus("ok");
+          return;
+        }
+        /**
+         * all of this are ignored
+         */
+        case ObservabilityEventType.DbQueryStart:
+        case ObservabilityEventType.DbQueryFinishSuccess:
+        case ObservabilityEventType.DbQueryFinishError:
+        case ObservabilityEventType.RequestStart:
+        case ObservabilityEventType.RequestFinishSuccess:
+        case ObservabilityEventType.RequestFinishError:
+        case ObservabilityEventType.TaskStart:
+        case ObservabilityEventType.TaskStop:
+        case ObservabilityEventType.TaskReset:
+        case ObservabilityEventType.ShepherdTaskResult:
+        case ObservabilityEventType.DeclareTaskDependency:
+        case ObservabilityEventType.CryptoStart:
+        case ObservabilityEventType.CryptoFinishSuccess:
+        case ObservabilityEventType.CryptoFinishError:
+          return;
+        default: {
+          assertUnreachable(ev);
+        }
+      }
+    });
+  });
+  if (!pref.showDebugInfo || !lastEvent) return <Fragment />;
+  return (
+    <div
+      data-status={status}
+      class="fixed z-20 bottom-0 w-full ease-in-out delay-1000 
transition-transform data-[status=ok]:scale-y-0"
+    >
+      <div
+        data-status={status}
+        class="mx-auto w-4/5 center flex p-1 bg-gray-300 m-1 
data-[status=fail]:bg-red-200 data-[status=ok]:bg-green-200 "
+      >
+        {!status ? <Wait class="w-6 h-6" /> : <div class="w-6 h-6" />}
+
+        <p class="ml-2 my-auto text-sm text-gray-500">{lastEvent.url}</p>
+        {!status ? (
+          <button
+            onClick={() => {
+              if (cancelRequest) cancelRequest(lastEvent.id);
+            }}
+          >
+            cancel
+          </button>
+        ) : undefined}
+      </div>
+    </div>
+  );
+}
+
+function WelcomeAccount({
+  account,
+  routeAccountDetails,
+}: {
+  account: string;
   routeAccountDetails: RouteDefinition;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -196,7 +324,8 @@ function WelcomeAccount({ account, routeAccountDetails }: {
   }
   if (result.type === "fail") {
     return (
-      <a name="account details"
+      <a
+        name="account details"
         href={routeAccountDetails.url({})}
         class="underline underline-offset-2"
       >
@@ -205,7 +334,8 @@ function WelcomeAccount({ account, routeAccountDetails }: {
     );
   }
   return (
-    <a name="account details"
+    <a
+      name="account details"
       href={routeAccountDetails.url({})}
       class="underline underline-offset-2"
     >
diff --git a/packages/bank-ui/src/pages/LoginForm.tsx 
b/packages/bank-ui/src/pages/LoginForm.tsx
index bd20e79c8..a097417c3 100644
--- a/packages/bank-ui/src/pages/LoginForm.tsx
+++ b/packages/bank-ui/src/pages/LoginForm.tsx
@@ -14,15 +14,13 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import {
-  HttpStatusCode
-} from "@gnu-taler/taler-util";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
 import {
   Button,
   LocalNotificationBanner,
   ShowInputErrorLabel,
   useLocalNotificationHandler,
-  useTranslationContext
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import { VNode, h } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
@@ -62,38 +60,42 @@ export function LoginForm({
     ref.current?.focus();
   }, []);
 
-  const errors =
-    undefinedIfEmpty({
-      username: !username
-        ? i18n.str`Missing username`
-        : // : !USERNAME_REGEX.test(username)
+  const errors = undefinedIfEmpty({
+    username: !username
+      ? i18n.str`Missing username`
+      : // : !USERNAME_REGEX.test(username)
         //   ? i18n.str`Use letters and numbers only, and start with a 
lowercase letter`
         undefined,
-      password: !password ? i18n.str`Missing password` : undefined,
-    });
+    password: !password ? i18n.str`Missing password` : undefined,
+  });
 
   async function doLogout() {
     session.logOut();
   }
 
-  const loginHandler = !username || !password ? undefined : withErrorHandler(
-    async () => authenticator(username)
-      .createAccessToken(password, {
-        // scope: "readwrite" as "write", // FIX: different than merchant
-        scope: "readwrite",
-        duration: { d_us: "forever" },
-        refreshable: true,
-      }),
-    (result) => {
-      session.logIn({ username, token: result.body.access_token })
-    },
-    (fail) => {
-      switch (fail.case) {
-        case HttpStatusCode.Unauthorized: return i18n.str`Wrong credentials 
for "${username}"`;
-        case HttpStatusCode.NotFound: return i18n.str`Account not found`;
-      }
-    }
-  )
+  const loginHandler =
+    !username || !password
+      ? undefined
+      : withErrorHandler(
+          async () =>
+            authenticator(username).createAccessToken(password, {
+              // scope: "readwrite" as "write", // FIX: different than merchant
+              scope: "readwrite",
+              duration: { d_us: "forever" },
+              refreshable: true,
+            }),
+          (result) => {
+            session.logIn({ username, token: result.body.access_token });
+          },
+          (fail) => {
+            switch (fail.case) {
+              case HttpStatusCode.Unauthorized:
+                return i18n.str`Wrong credentials for "${username}"`;
+              case HttpStatusCode.NotFound:
+                return i18n.str`Account not found`;
+            }
+          },
+        );
 
   return (
     <div class="flex min-h-full flex-col justify-center ">
diff --git a/packages/bank-ui/src/pages/OperationState/index.ts 
b/packages/bank-ui/src/pages/OperationState/index.ts
index e4d9d45e3..8ab5659b1 100644
--- a/packages/bank-ui/src/pages/OperationState/index.ts
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -106,15 +106,15 @@ export namespace State {
     account: string;
     routeHere: RouteDefinition<{ wopid: string }>;
     onAbort:
-    | undefined
-    | (() => Promise<
-      TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
-    >);
+      | undefined
+      | (() => Promise<
+          TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+        >);
     onConfirm:
-    | undefined
-    | (() => Promise<
-      TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
-    >);
+      | undefined
+      | (() => Promise<
+          TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+        >);
     error: undefined;
     id: string;
   }
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts 
b/packages/bank-ui/src/pages/OperationState/state.ts
index 9c5626cce..80af1a91d 100644
--- a/packages/bank-ui/src/pages/OperationState/state.ts
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -191,9 +191,9 @@ export function useComponentState({
         routeClose,
         onAbort: !creds
           ? async () => {
-            onAbort();
-            return undefined;
-          }
+              onAbort();
+              return undefined;
+            }
           : doAbort,
       };
     }
diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx 
b/packages/bank-ui/src/pages/OperationState/views.tsx
index 6eee6daa9..330fe1072 100644
--- a/packages/bank-ui/src/pages/OperationState/views.tsx
+++ b/packages/bank-ui/src/pages/OperationState/views.tsx
@@ -73,6 +73,7 @@ export function NeedConfirmationView({
             title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.BadRequest:
           return notify({
@@ -80,6 +81,7 @@ export function NeedConfirmationView({
             title: i18n.str`The operation id is invalid.`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.NotFound:
           return notify({
@@ -87,6 +89,7 @@ export function NeedConfirmationView({
             title: i18n.str`The operation was not found.`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         default:
           assertUnreachable(resp);
@@ -111,6 +114,7 @@ export function NeedConfirmationView({
             title: i18n.str`The withdrawal has been aborted previously and 
can't be confirmed`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
           return notify({
@@ -118,6 +122,7 @@ export function NeedConfirmationView({
             title: i18n.str`The withdrawal operation can't be confirmed before 
a wallet accepted the transaction.`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.BadRequest:
           return notify({
@@ -125,6 +130,7 @@ export function NeedConfirmationView({
             title: i18n.str`The operation id is invalid.`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.NotFound:
           return notify({
@@ -132,6 +138,7 @@ export function NeedConfirmationView({
             title: i18n.str`The operation was not found.`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case TalerErrorCode.BANK_UNALLOWED_DEBIT:
           return notify({
@@ -139,6 +146,7 @@ export function NeedConfirmationView({
             title: i18n.str`Your balance is not enough.`,
             description: resp.detail.hint as TranslatedString,
             debug: resp.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.Accepted: {
           updateBankState("currentChallenge", {
@@ -147,7 +155,6 @@ export function NeedConfirmationView({
             sent: AbsoluteTime.never(),
             location: routeHere.url({ wopid: id }),
             request: id,
-
           });
           return onAuthorizationRequired();
         }
@@ -331,10 +338,7 @@ export function ConfirmedView({ routeClose }: 
State.Confirmed) {
   );
 }
 
-export function ReadyView({
-  uri,
-  onAbort: doAbort,
-}: State.Ready): VNode {
+export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
   const { i18n } = useTranslationContext();
   const walletInegrationApi = useTalerWalletIntegrationAPI();
   const [notification, notify, errorHandler] = useLocalNotification();
@@ -355,6 +359,7 @@ export function ReadyView({
             title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`,
             description: hasError.detail.hint as TranslatedString,
             debug: hasError.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.BadRequest:
           return notify({
@@ -362,6 +367,7 @@ export function ReadyView({
             title: i18n.str`The operation id is invalid.`,
             description: hasError.detail.hint as TranslatedString,
             debug: hasError.detail,
+            when: AbsoluteTime.now(),
           });
         case HttpStatusCode.NotFound:
           return notify({
@@ -369,6 +375,7 @@ export function ReadyView({
             title: i18n.str`The operation was not found.`,
             description: hasError.detail.hint as TranslatedString,
             debug: hasError.detail,
+            when: AbsoluteTime.now(),
           });
         default:
           assertUnreachable(hasError);
diff --git a/packages/bank-ui/src/pages/PaymentOptions.tsx 
b/packages/bank-ui/src/pages/PaymentOptions.tsx
index 07dd18931..a034392d2 100644
--- a/packages/bank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/bank-ui/src/pages/PaymentOptions.tsx
@@ -15,15 +15,15 @@
  */
 
 import { AmountJson, TalerError } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
+import { useEffect } from "preact/hooks";
+import { useWithdrawalDetails } from "../hooks/account.js";
 import { useBankState } from "../hooks/bank-state.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "../route.js";
 import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
 import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { useWithdrawalDetails } from "../hooks/account.js";
-import { useEffect } from "preact/hooks";
-import { useSessionState } from "../hooks/session.js";
 
 function ShowOperationPendingTag({
   woid,
@@ -35,14 +35,15 @@ function ShowOperationPendingTag({
   const { i18n } = useTranslationContext();
   const { state: credentials } = useSessionState();
   const result = useWithdrawalDetails(woid);
-  const loading = !result
+  const loading = !result;
   const error =
     !loading && (result instanceof TalerError || result.type === "fail");
   const pending =
-    !loading && !error &&
-    (result.body.status === "pending" || result.body.status === "selected")
-    && credentials.status === "loggedIn"
-    && credentials.username === result.body.username;
+    !loading &&
+    !error &&
+    (result.body.status === "pending" || result.body.status === "selected") &&
+    credentials.status === "loggedIn" &&
+    credentials.username === result.body.username;
   useEffect(() => {
     if (!loading && !pending && onOperationAlreadyCompleted) {
       onOperationAlreadyCompleted();
@@ -96,9 +97,9 @@ export function PaymentOptions({
   routeCashout: RouteDefinition;
   routeChargeWallet: RouteDefinition;
   routeWireTransfer: RouteDefinition<{
-    account?: string,
-    subject?: string,
-    amount?: string,
+    account?: string;
+    subject?: string;
+    amount?: string;
   }>;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -126,9 +127,7 @@ export function PaymentOptions({
                 <span class="flex">
                   <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
                   <span class="grow self-center text-lg text-gray-900 
align-middle text-center">
-                    <i18n.Translate>
-                      to a Taler wallet
-                    </i18n.Translate>
+                    <i18n.Translate>to a Taler wallet</i18n.Translate>
                   </span>
                   <svg
                     class="self-center flex-none h-5 w-5 text-indigo-600"
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 8d9df1151..d10f62cce 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -29,7 +29,7 @@ import {
   assertUnreachable,
   buildPayto,
   parsePaytoUri,
-  stringifyPaytoUri
+  stringifyPaytoUri,
 } from "@gnu-taler/taler-util";
 import {
   InternationalizationAPI,
@@ -43,9 +43,9 @@ import { ComponentChildren, Fragment, Ref, VNode, h } from 
"preact";
 import { useState } from "preact/hooks";
 import { mutate } from "swr";
 import { useBankCoreApiContext } from "../context/config.js";
-import { useSessionState } from "../hooks/session.js";
 import { useBankState } from "../hooks/bank-state.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "../route.js";
 import { undefinedIfEmpty, validateIBAN, validateTalerBank } from 
"../utils.js";
 
 interface Props {
@@ -59,9 +59,9 @@ interface Props {
   routeCancel?: RouteDefinition;
   routeCashout?: RouteDefinition;
   routeHere: RouteDefinition<{
-    account?: string,
-    subject?: string,
-    amount?: string,
+    account?: string;
+    subject?: string;
+    amount?: string;
   }>;
   limit: AmountJson;
   balance: AmountJson;
@@ -79,7 +79,6 @@ export function PaytoWireTransferForm({
   routeHere,
   onAuthorizationRequired,
   limit,
-  balance,
 }: Props): VNode {
   const [isRawPayto, setIsRawPayto] = useState(false);
   const { state: credentials } = useSessionState();
@@ -101,14 +100,19 @@ export function PaytoWireTransferForm({
   const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
   const [notification, notify, handleError] = useLocalNotification();
 
-  const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as 
const : "iban" as const;
+  const paytoType =
+    config.wire_type === "X_TALER_BANK"
+      ? ("x-taler-bank" as const)
+      : ("iban" as const);
 
   const errorsWire = undefinedIfEmpty({
     account: !account
       ? i18n.str`Required`
-      : paytoType === "iban" ? validateIBAN(account, i18n) :
-        paytoType === "x-taler-bank" ? validateTalerBank(account, i18n) :
-          undefined,
+      : paytoType === "iban"
+        ? validateIBAN(account, i18n)
+        : paytoType === "x-taler-bank"
+          ? validateTalerBank(account, i18n)
+          : undefined,
     subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n),
     amount: !trimmedAmountStr
       ? i18n.str`Required`
@@ -119,11 +123,11 @@ export function PaytoWireTransferForm({
 
   const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
 
-
   const errorsPayto = undefinedIfEmpty({
     rawPaytoInput: !rawPaytoInput
       ? i18n.str`Required`
-      : !parsed ? i18n.str`Does not follow the pattern`
+      : !parsed
+        ? i18n.str`Does not follow the pattern`
         : validateRawPayto(parsed, limit, url.host, i18n, paytoType),
   });
 
@@ -140,11 +144,15 @@ export function PaytoWireTransferForm({
       delete p.params.amount;
       // if this payto is valid then it already have message
       payto_uri = stringifyPaytoUri(p);
-      acName = !p.isKnown ? undefined :
-        p.targetType === "iban" ? p.iban :
-          p.targetType === "bitcoin" ? p.targetPath :
-            p.targetType === "x-taler-bank" ? p.account :
-              assertUnreachable(p);
+      acName = !p.isKnown
+        ? undefined
+        : p.targetType === "iban"
+          ? p.iban
+          : p.targetType === "bitcoin"
+            ? p.targetPath
+            : p.targetType === "x-taler-bank"
+              ? p.account
+              : assertUnreachable(p);
     } else {
       if (!account || !subject) return;
       let payto;
@@ -159,7 +167,8 @@ export function PaytoWireTransferForm({
           payto = buildPayto("iban", account, undefined);
           break;
         }
-        default: assertUnreachable(paytoType)
+        default:
+          assertUnreachable(paytoType);
       }
 
       payto.params.message = encodeURIComponent(subject);
@@ -184,6 +193,7 @@ export function PaytoWireTransferForm({
               title: i18n.str`The request was invalid or the payto://-URI used 
unacceptable features.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Unauthorized:
             return notify({
@@ -191,13 +201,25 @@ export function PaytoWireTransferForm({
               title: i18n.str`Not enough permission to complete the 
operation.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
+            });
+          case TalerErrorCode.BANK_ADMIN_CREDITOR:
+            return notify({
+              type: "error",
+              title: i18n.str`Bank administrator can't be the transfer 
creditor.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
             return notify({
               type: "error",
-              title: i18n.str`The destination account "${acName ?? puri}" was 
not found.`,
+              title: i18n.str`The destination account "${
+                acName ?? puri
+              }" was not found.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_SAME_ACCOUNT:
             return notify({
@@ -205,6 +227,7 @@ export function PaytoWireTransferForm({
               title: i18n.str`The origin and the destination of the transfer 
can't be the same.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_UNALLOWED_DEBIT:
             return notify({
@@ -212,6 +235,7 @@ export function PaytoWireTransferForm({
               title: i18n.str`Your balance is not enough.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
@@ -219,12 +243,17 @@ export function PaytoWireTransferForm({
               title: i18n.str`The origin account "${puri}" was not found.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Accepted: {
             updateBankState("currentChallenge", {
               operation: "create-transaction",
               id: String(resp.body.challenge_id),
-              location: routeHere.url({ account: account ?? "", amount, 
subject }),
+              location: routeHere.url({
+                account: account ?? "",
+                amount,
+                subject,
+              }),
               sent: AbsoluteTime.never(),
               request,
             });
@@ -281,10 +310,12 @@ export function PaytoWireTransferForm({
                       break;
                     }
                     default: {
-                      assertUnreachable(parsed)
+                      assertUnreachable(parsed);
                     }
                   }
-                  const amountStr = !parsed.params ? undefined : 
parsed.params["amount"];
+                  const amountStr = !parsed.params
+                    ? undefined
+                    : parsed.params["amount"];
                   if (amountStr) {
                     const amount = Amounts.parse(amountStr);
                     if (amount) {
@@ -350,7 +381,8 @@ export function PaytoWireTransferForm({
                         }
                         break;
                       }
-                      default: assertUnreachable(paytoType)
+                      default:
+                        assertUnreachable(paytoType);
                     }
                     rawPaytoInputSetter(stringifyPaytoUri(payto));
                   }
@@ -374,9 +406,7 @@ export function PaytoWireTransferForm({
             >
               <i18n.Translate>Cashout</i18n.Translate>
             </a>
-          ) : (
-            undefined
-          )}
+          ) : undefined}
         </div>
       </div>
 
@@ -394,34 +424,39 @@ export function PaytoWireTransferForm({
               {(() => {
                 switch (paytoType) {
                   case "x-taler-bank": {
-                    return <TextField
-                      id="x-taler-bank"
-                      required
-                      label={i18n.str`Recipient`}
-                      help={i18n.str`Id of the recipient's account`}
-                      error={errorsWire?.account}
-                      onChange={setAccount}
-                      value={account}
-                      placeholder={i18n.str`username`}
-                      focus={focus}
-                      disabled={sendingToFixedAccount}
-                    />
+                    return (
+                      <TextField
+                        id="x-taler-bank"
+                        required
+                        label={i18n.str`Recipient`}
+                        help={i18n.str`Id of the recipient's account`}
+                        error={errorsWire?.account}
+                        onChange={setAccount}
+                        value={account}
+                        placeholder={i18n.str`username`}
+                        focus={focus}
+                        disabled={sendingToFixedAccount}
+                      />
+                    );
                   }
                   case "iban": {
-                    return <TextField
-                      id="iban"
-                      required
-                      label={i18n.str`Recipient`}
-                      help={i18n.str`IBAN of the recipient's account`}
-                      placeholder={"CC0123456789" as TranslatedString}
-                      error={errorsWire?.account}
-                      onChange={(v) => setAccount(v.toUpperCase())}
-                      value={account}
-                      focus={focus}
-                      disabled={sendingToFixedAccount}
-                    />
+                    return (
+                      <TextField
+                        id="iban"
+                        required
+                        label={i18n.str`Recipient`}
+                        help={i18n.str`IBAN of the recipient's account`}
+                        placeholder={"CC0123456789" as TranslatedString}
+                        error={errorsWire?.account}
+                        onChange={(v) => setAccount(v.toUpperCase())}
+                        value={account}
+                        focus={focus}
+                        disabled={sendingToFixedAccount}
+                      />
+                    );
                   }
-                  default: assertUnreachable(paytoType)
+                  default:
+                    assertUnreachable(paytoType);
                 }
               })()}
 
@@ -506,11 +541,12 @@ export function PaytoWireTransferForm({
                     value={rawPaytoInput ?? ""}
                     required
                     title={i18n.str`Uniform resource identifier of the target 
account`}
-
                     placeholder={((): TranslatedString => {
                       switch (paytoType) {
-                        case "x-taler-bank": return 
i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`
-                        case "iban": return 
i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`
+                        case "x-taler-bank":
+                          return 
i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`;
+                        case "iban":
+                          return 
i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`;
                       }
                     })()}
                     onInput={(e): void => {
@@ -618,13 +654,13 @@ export function InputAmount(
             if (
               sep_pos !== -1 &&
               l - sep_pos - 1 >
-              config.currency_specification.num_fractional_input_digits
+                config.currency_specification.num_fractional_input_digits
             ) {
               e.currentTarget.value = e.currentTarget.value.substring(
                 0,
                 sep_pos +
-                config.currency_specification.num_fractional_input_digits +
-                1,
+                  config.currency_specification.num_fractional_input_digits +
+                  1,
               );
             }
             onChange(e.currentTarget.value);
@@ -668,81 +704,94 @@ export function RenderAmount({
   );
 }
 
-
-function validateRawPayto(parsed: PaytoUri, limit: AmountJson, host: string, 
i18n: InternationalizationAPI, type: "iban" | "x-taler-bank"): TranslatedString 
| undefined {
+function validateRawPayto(
+  parsed: PaytoUri,
+  limit: AmountJson,
+  host: string,
+  i18n: InternationalizationAPI,
+  type: "iban" | "x-taler-bank",
+): TranslatedString | undefined {
   if (!parsed.isKnown) {
-    return i18n.str`The target type is unknown, use "${type}"`
+    return i18n.str`The target type is unknown, use "${type}"`;
   }
   let result: TranslatedString | undefined;
   switch (type) {
     case "x-taler-bank": {
       if (parsed.targetType !== "x-taler-bank") {
-        return i18n.str`Only "x-taler-bank" target are supported`
+        return i18n.str`Only "x-taler-bank" target are supported`;
       }
 
       if (parsed.host !== host) {
-        return i18n.str`Only this host is allowed. Use "${host}"`
+        return i18n.str`Only this host is allowed. Use "${host}"`;
       }
 
       if (!parsed.account) {
-        return i18n.str`Missing account name`
+        return i18n.str`Missing account name`;
       }
-      const result = validateTalerBank(parsed.account, i18n)
-      if (result) return result
+      const result = validateTalerBank(parsed.account, i18n);
+      if (result) return result;
       break;
     }
     case "iban": {
       if (parsed.targetType !== "iban") {
-        return i18n.str`Only "IBAN" target are supported`
+        return i18n.str`Only "IBAN" target are supported`;
       }
-      const result = validateIBAN(parsed.iban, i18n)
-      if (result) return result
+      const result = validateIBAN(parsed.iban, i18n);
+      if (result) return result;
       break;
     }
-    default: assertUnreachable(type)
+    default:
+      assertUnreachable(type);
   }
   if (!parsed.params.amount) {
-    return i18n.str`Missing "amount" parameter to specify the amount to be 
transferred`
+    return i18n.str`Missing "amount" parameter to specify the amount to be 
transferred`;
   }
-  const amount = Amounts.parse(parsed.params.amount)
+  const amount = Amounts.parse(parsed.params.amount);
   if (!amount) {
-    return i18n.str`The "amount" parameter is not valid`
+    return i18n.str`The "amount" parameter is not valid`;
   }
-  result = validateAmount(amount, limit, i18n)
+  result = validateAmount(amount, limit, i18n);
   if (result) return result;
 
   if (!parsed.params.message) {
-    return i18n.str`Missing the "message" parameter to specify a reference 
text for the transfer`
+    return i18n.str`Missing the "message" parameter to specify a reference 
text for the transfer`;
   }
-  const subject = parsed.params.message
-  result = validateSubject(subject, i18n)
+  const subject = parsed.params.message;
+  result = validateSubject(subject, i18n);
   if (result) return result;
 
-  return undefined
+  return undefined;
 }
 
-function validateAmount(amount: AmountJson, limit: AmountJson, i18n: 
InternationalizationAPI): TranslatedString | undefined {
+function validateAmount(
+  amount: AmountJson,
+  limit: AmountJson,
+  i18n: InternationalizationAPI,
+): TranslatedString | undefined {
   if (amount.currency !== limit.currency) {
-    return i18n.str`The only currency allowed is "${limit.currency}"`
+    return i18n.str`The only currency allowed is "${limit.currency}"`;
   }
   if (Amounts.isZero(amount)) {
-    return i18n.str`Can't transfer zero amount`
+    return i18n.str`Can't transfer zero amount`;
   }
   if (Amounts.cmp(limit, amount) === -1) {
-    return i18n.str`Balance is not enough`
+    return i18n.str`Balance is not enough`;
   }
-  return undefined
+  return undefined;
 }
 
-function validateSubject(text: string, i18n: InternationalizationAPI): 
TranslatedString | undefined {
+function validateSubject(
+  text: string,
+  i18n: InternationalizationAPI,
+): TranslatedString | undefined {
   if (text.length < 2) {
-    return i18n.str`Use a longer subject`
+    return i18n.str`Use a longer subject`;
   }
-  return undefined
+  return undefined;
 }
 
 interface PaytoFieldProps {
-  id: string,
+  id: string;
   label: TranslatedString;
   required?: boolean;
   help?: TranslatedString;
@@ -755,13 +804,17 @@ interface PaytoFieldProps {
   disabled?: boolean;
 }
 
-function Wrapper({ withIcon, children }: { withIcon: boolean, children: 
ComponentChildren }): VNode {
+function Wrapper({
+  withIcon,
+  children,
+}: {
+  withIcon: boolean;
+  children: ComponentChildren;
+}): VNode {
   if (withIcon) {
-    return <div class="flex justify-between">
-      {children}
-    </div>
+    return <div class="flex justify-between">{children}</div>;
   }
-  return <Fragment>{children}</Fragment>
+  return <Fragment>{children}</Fragment>;
 }
 
 export function TextField({
@@ -777,43 +830,34 @@ export function TextField({
   value,
   error,
 }: PaytoFieldProps): VNode {
-  return <div class="sm:col-span-5">
-    <label
-      for={id}
-      class="block text-sm font-medium leading-6 text-gray-900"
-    >{label}
-      {required &&
-        <b style={{ color: "red" }}> *</b>
-      }
-    </label>
-    <div class="mt-2">
-      <Wrapper withIcon={rightIcons !== undefined}>
-        <input
-          ref={focus ? doAutoFocus : undefined}
-          type="text"
-          class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 
text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
-          name={id}
-          id={id}
-          disabled={disabled}
-          value={value ?? ""}
-          placeholder={placeholder}
-          autocomplete="off"
-          required
-          onInput={(e): void => {
-            onChange(e.currentTarget.value);
-          }}
-        />
-        {rightIcons}
-      </Wrapper>
-      <ShowInputErrorLabel
-        message={error}
-        isDirty={value !== undefined}
-      />
+  return (
+    <div class="sm:col-span-5">
+      <label for={id} class="block text-sm font-medium leading-6 
text-gray-900">
+        {label}
+        {required && <b style={{ color: "red" }}> *</b>}
+      </label>
+      <div class="mt-2">
+        <Wrapper withIcon={rightIcons !== undefined}>
+          <input
+            ref={focus ? doAutoFocus : undefined}
+            type="text"
+            class="block w-full disabled:bg-gray-200 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
+            name={id}
+            id={id}
+            disabled={disabled}
+            value={value ?? ""}
+            placeholder={placeholder}
+            autocomplete="off"
+            required
+            onInput={(e): void => {
+              onChange(e.currentTarget.value);
+            }}
+          />
+          {rightIcons}
+        </Wrapper>
+        <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+      </div>
+      {help && <p class="mt-2 text-sm text-gray-500">{help}</p>}
     </div>
-    {help &&
-      <p class="mt-2 text-sm text-gray-500">
-        {help}
-      </p>
-    }
-  </div>
+  );
 }
diff --git a/packages/bank-ui/src/pages/ProfileNavigation.tsx 
b/packages/bank-ui/src/pages/ProfileNavigation.tsx
index 10497f015..1775d9329 100644
--- a/packages/bank-ui/src/pages/ProfileNavigation.tsx
+++ b/packages/bank-ui/src/pages/ProfileNavigation.tsx
@@ -27,9 +27,9 @@ export function ProfileNavigation({
   routeMyAccountDelete,
   routeMyAccountDetails,
   routeMyAccountPassword,
-  routeConversionConfig
+  routeConversionConfig,
 }: {
-  current: "details" | "delete" | "credentials" | "cashouts" | "conversion",
+  current: "details" | "delete" | "credentials" | "cashouts" | "conversion";
   routeMyAccountDetails: RouteDefinition;
   routeMyAccountDelete: RouteDefinition;
   routeMyAccountPassword: RouteDefinition;
@@ -40,9 +40,7 @@ export function ProfileNavigation({
   const { config } = useBankCoreApiContext();
   const { state: credentials } = useSessionState();
   const isAdminUser =
-    credentials.status !== "loggedIn"
-      ? false
-      : credentials.isUserAdministrator;
+    credentials.status !== "loggedIn" ? false : 
credentials.isUserAdministrator;
   const nonAdminUser = !isAdminUser;
 
   const { navigateTo } = useNavigationContext();
diff --git a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx 
b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
index 84d703cbe..554da0c3f 100644
--- a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
@@ -31,8 +31,8 @@ export function PublicHistoriesPage(): VNode {
   const result = usePublicAccounts(undefined);
   const firstAccount =
     result &&
-      !(result instanceof TalerError) &&
-      result.data.public_accounts.length > 0
+    !(result instanceof TalerError) &&
+    result.data.public_accounts.length > 0
       ? result.data.public_accounts[0].username
       : undefined;
 
@@ -71,7 +71,12 @@ export function PublicHistoriesPage(): VNode {
         </a>
       </li>,
     );
-    txs[account.username] = <Transactions account={account.username} 
routeCreateWireTransfer={undefined} />;
+    txs[account.username] = (
+      <Transactions
+        account={account.username}
+        routeCreateWireTransfer={undefined}
+      />
+    );
   }
 
   return (
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx 
b/packages/bank-ui/src/pages/QrCodeSection.tsx
index da11e631d..f442857a8 100644
--- a/packages/bank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -17,13 +17,13 @@
 import {
   HttpStatusCode,
   stringifyWithdrawUri,
-  WithdrawUriResult
+  WithdrawUriResult,
 } from "@gnu-taler/taler-util";
 import {
   Button,
   LocalNotificationBanner,
   useLocalNotificationHandler,
-  useTranslationContext
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import { Fragment, h, VNode } from "preact";
 import { useEffect } from "preact/hooks";
@@ -56,20 +56,20 @@ export function QrCodeSection({
   const onAbortHandler = handleError(
     async () => {
       if (!creds) return undefined;
-      return api.abortWithdrawalById(
-        creds,
-        withdrawUri.withdrawalOperationId,
-      )
+      return api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId);
     },
     onAborted,
     (fail) => {
       switch (fail.case) {
-        case HttpStatusCode.BadRequest: return i18n.str`The operation id is 
invalid.`;
-        case HttpStatusCode.NotFound: return i18n.str`The operation was not 
found.`;
-        case HttpStatusCode.Conflict: return i18n.str`The reserve operation 
has been confirmed previously and can't be aborted`;
+        case HttpStatusCode.BadRequest:
+          return i18n.str`The operation id is invalid.`;
+        case HttpStatusCode.NotFound:
+          return i18n.str`The operation was not found.`;
+        case HttpStatusCode.Conflict:
+          return i18n.str`The reserve operation has been confirmed previously 
and can't be aborted`;
       }
-    }
-  )
+    },
+  );
 
   return (
     <Fragment>
diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx 
b/packages/bank-ui/src/pages/RegistrationPage.tsx
index e9f7e602f..2ade465c2 100644
--- a/packages/bank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/bank-ui/src/pages/RegistrationPage.tsx
@@ -16,10 +16,7 @@
 import {
   AccessToken,
   HttpStatusCode,
-  OperationFail,
   TalerErrorCode,
-  TranslatedString,
-  assertUnreachable,
 } from "@gnu-taler/taler-util";
 import {
   LocalNotificationBanner,
@@ -77,7 +74,7 @@ function RegistrationForm({
   // const [phone, setPhone] = useState<string | undefined>();
   // const [email, setEmail] = useState<string | undefined>();
   const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
-  const [notification, _, handleError] = useLocalNotification();
+  const [notification, , handleError] = useLocalNotification();
   const settings = useSettingsContext();
 
   const { bank: api } = useBankCoreApiContext();
@@ -125,19 +122,29 @@ function RegistrationForm({
         onComplete();
       } else {
         onError(resp, (_case) => {
-          switch(_case) {
-            case HttpStatusCode.BadRequest: return i18n.str`Server replied 
with invalid phone or email.`;
-            case HttpStatusCode.Unauthorized: return i18n.str`No enough 
permission to create that account.`;
-            case TalerErrorCode.BANK_UNALLOWED_DEBIT: return  
i18n.str`Registration is disabled because the bank ran out of bonus credit.`;
-            case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return  
i18n.str`That username can't be used because is reserved.`;
-            case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: return 
i18n.str`That username is already taken.`;
-            case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: return 
i18n.str`That account id is already taken.`;
-            case TalerErrorCode.BANK_MISSING_TAN_INFO: return i18n.str`No 
information for the selected authentication channel.`;
-            case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: return 
i18n.str`Authentication channel is not supported.`;
-            case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return 
i18n.str`Only admin is allow to set debt limit.`;
-            case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return 
i18n.str`Only admin can create accounts with second factor authentication.`;
+          switch (_case) {
+            case HttpStatusCode.BadRequest:
+              return i18n.str`Server replied with invalid phone or email.`;
+            case HttpStatusCode.Unauthorized:
+              return i18n.str`No enough permission to create that account.`;
+            case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+              return i18n.str`Registration is disabled because the bank ran 
out of bonus credit.`;
+            case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+              return i18n.str`That username can't be used because is 
reserved.`;
+            case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+              return i18n.str`That username is already taken.`;
+            case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+              return i18n.str`That account id is already taken.`;
+            case TalerErrorCode.BANK_MISSING_TAN_INFO:
+              return i18n.str`No information for the selected authentication 
channel.`;
+            case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+              return i18n.str`Authentication channel is not supported.`;
+            case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+              return i18n.str`Only admin is allow to set debt limit.`;
+            case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+              return i18n.str`Only admin can create accounts with second 
factor authentication.`;
           }
-        })
+        });
       }
     });
   }
diff --git a/packages/bank-ui/src/pages/ShowNotifications.tsx 
b/packages/bank-ui/src/pages/ShowNotifications.tsx
new file mode 100644
index 000000000..fe041fb19
--- /dev/null
+++ b/packages/bank-ui/src/pages/ShowNotifications.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ 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 { useNotifications } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { Time } from "../components/Time.js";
+
+export function ShowNotifications(): VNode {
+  const ns = useNotifications();
+  if (!ns.length) {
+    return <div>no notifications</div>;
+  }
+  return (
+    <div>
+      <p>Notifications</p>
+      <table>
+        <thead></thead>
+        <tbody>
+          {ns.map((n, idx) => {
+            return (
+              <tr key={idx}>
+                <td>
+                  <Time
+                    timestamp={n.message.when}
+                    format="dd/MM/yyyy HH:mm:ss"
+                  />
+                </td>
+                <td>{n.message.title}</td>
+                <td>
+                  {n.message.type === "error"
+                    ? n.message.description
+                    : undefined}
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+      {/* <ToastBanner all /> */}
+    </div>
+  );
+}
diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx 
b/packages/bank-ui/src/pages/SolveChallengePage.tsx
index b2e053b3c..528cc12df 100644
--- a/packages/bank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -34,21 +34,20 @@ import {
   useLocalNotification,
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
 import { Fragment, VNode, h } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { Time } from "../components/Time.js";
 import { useBankCoreApiContext } from "../context/config.js";
+import { useNavigationContext } from "../context/navigation.js";
 import { useWithdrawalDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
 import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
 import { useConversionInfo } from "../hooks/regional.js";
+import { useSessionState } from "../hooks/session.js";
 import { RouteDefinition } from "../route.js";
 import { undefinedIfEmpty } from "../utils.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
 import { OperationNotFound } from "./WithdrawalQRCode.js";
-import { useNavigationContext } from "../context/navigation.js";
-import { Time } from "../components/Time.js";
 
 export function SolveChallengePage({
   onChallengeCompleted,
@@ -107,6 +106,7 @@ export function SolveChallengePage({
               title: i18n.str`Cashout not found. It may be also mean that it 
was already aborted.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Unauthorized:
             return notify({
@@ -114,6 +114,7 @@ export function SolveChallengePage({
               title: i18n.str`Cashout not found. It may be also mean that it 
was already aborted.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
             return notify({
@@ -121,6 +122,7 @@ export function SolveChallengePage({
               title: i18n.str`Cashout not found. It may be also mean that it 
was already aborted.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           default:
             assertUnreachable(resp);
@@ -145,6 +147,7 @@ export function SolveChallengePage({
                 title: i18n.str`Challenge not found.`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             case HttpStatusCode.Unauthorized:
               return notify({
@@ -152,6 +155,7 @@ export function SolveChallengePage({
                 title: i18n.str`This user is not authorized to complete this 
challenge.`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             case HttpStatusCode.TooManyRequests:
               return notify({
@@ -159,6 +163,7 @@ export function SolveChallengePage({
                 title: i18n.str`Too many attempts, try another code.`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
               return notify({
@@ -166,6 +171,7 @@ export function SolveChallengePage({
                 title: i18n.str`The confirmation code is wrong, try again.`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
               return notify({
@@ -173,6 +179,7 @@ export function SolveChallengePage({
                 title: i18n.str`The operation expired.`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             default:
               assertUnreachable(resp);
@@ -206,6 +213,7 @@ export function SolveChallengePage({
               title: i18n.str`The operation failed.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           }
           // another challenge required, save the request and the ID
@@ -220,6 +228,7 @@ export function SolveChallengePage({
           return notify({
             type: "info",
             title: i18n.str`The operation needs another confirmation to 
complete.`,
+            when: AbsoluteTime.now(),
           });
         }
         updateBankState("currentChallenge", undefined);
@@ -267,7 +276,7 @@ export function SolveChallengePage({
             onStart={startChallenge}
             onCancel={() => {
               updateBankState("currentChallenge", undefined);
-              navigateTo(ch.location)
+              navigateTo(ch.location);
             }}
           />
           {ch.info && (
@@ -341,15 +350,15 @@ function ChallengeDetails({
   onStart: () => void;
   onCancel: () => void;
 }): VNode {
-  const { i18n, dateLocale } = useTranslationContext();
+  const { i18n } = useTranslationContext();
   const { config } = useBankCoreApiContext();
 
-  const firstTime = AbsoluteTime.isNever(challenge.sent)
+  const firstTime = AbsoluteTime.isNever(challenge.sent);
   useEffect(() => {
     if (firstTime) {
-      onStart()
+      onStart();
     }
-  }, [])
+  }, []);
   return (
     <div class="px-4 mt-4 ">
       <div class="w-full">
@@ -535,9 +544,11 @@ function ChallengeDetails({
                   <i18n.Translate>Sent at</i18n.Translate>
                 </dt>
                 <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 
sm:mt-0">
-                  <Time format="dd/MM/yyyy HH:mm:ss"
+                  <Time
+                    format="dd/MM/yyyy HH:mm:ss"
                     timestamp={challenge.sent}
-                    relative={Duration.fromSpec({ days: 1 })} />
+                    relative={Duration.fromSpec({ days: 1 })}
+                  />
                 </dd>
               </div>
             )}
@@ -668,11 +679,11 @@ function ShowCashoutDetails({
     switch (info.case) {
       case HttpStatusCode.NotImplemented: {
         return (
-          <Attention
-            type="danger"
-            title={i18n.str`Cashout are disabled`}
-          >
-            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+            <i18n.Translate>
+              Cashout should be enable by configuration and the conversion rate
+              should be initialized with fee, ratio and rounding mode.
+            </i18n.Translate>
           </Attention>
         );
       }
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
index 8c831199a..f16488b25 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -15,12 +15,13 @@
  */
 
 import {
+  AbsoluteTime,
   AmountJson,
   Amounts,
   HttpStatusCode,
   TranslatedString,
   assertUnreachable,
-  parseWithdrawUri
+  parseWithdrawUri,
 } from "@gnu-taler/taler-util";
 import {
   Attention,
@@ -39,7 +40,11 @@ import { usePreferences } from "../hooks/preferences.js";
 import { RouteDefinition } from "../route.js";
 import { undefinedIfEmpty } from "../utils.js";
 import { OperationState } from "./OperationState/index.js";
-import { InputAmount, RenderAmount, doAutoFocus } from 
"./PaytoWireTransferForm.js";
+import {
+  InputAmount,
+  RenderAmount,
+  doAutoFocus,
+} from "./PaytoWireTransferForm.js";
 
 const RefAmount = forwardRef(InputAmount);
 
@@ -54,7 +59,7 @@ function OldWithdrawalForm({
   limit: AmountJson;
   balance: AmountJson;
   focus?: boolean;
-  routeOperationDetails: RouteDefinition<{ wopid: string }>,
+  routeOperationDetails: RouteDefinition<{ wopid: string }>;
   onOperationCreated: (wopid: string) => void;
   routeCancel: RouteDefinition;
 }): VNode {
@@ -87,23 +92,25 @@ function OldWithdrawalForm({
       wopid: bankState.currentWithdrawalOperationId,
     });
     return (
-      <Attention type="warning" title={i18n.str`There is an operation 
already`} onClose={() => {
-        updateBankState("currentWithdrawalOperationId", undefined);
-      }}>
+      <Attention
+        type="warning"
+        title={i18n.str`There is an operation already`}
+        onClose={() => {
+          updateBankState("currentWithdrawalOperationId", undefined);
+        }}
+      >
         <span ref={focus ? doAutoFocus : undefined} />
-        <i18n.Translate>
-          Complete the operation in
-        </i18n.Translate>{" "}
+        <i18n.Translate>Complete the operation in</i18n.Translate>{" "}
         <a
           class="font-semibold text-yellow-700 hover:text-yellow-600"
           name="complete operation"
           href={url}
-        // onClick={(e) => {
-        //   e.preventDefault()
-        //   walletInegrationApi.publishTalerAction(uri, () => {
-        //     navigateTo(url)
-        //   })
-        // }}
+          // onClick={(e) => {
+          //   e.preventDefault()
+          //   walletInegrationApi.publishTalerAction(uri, () => {
+          //     navigateTo(url)
+          //   })
+          // }}
         >
           <i18n.Translate>this page</i18n.Translate>
         </a>
@@ -156,6 +163,7 @@ function OldWithdrawalForm({
               title: i18n.str`The operation was rejected due to insufficient 
funds`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
             break;
           }
@@ -165,6 +173,7 @@ function OldWithdrawalForm({
               title: i18n.str`The operation was rejected due to insufficient 
funds`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
             break;
           }
@@ -174,6 +183,7 @@ function OldWithdrawalForm({
               title: i18n.str`Account not found`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
             break;
           }
@@ -213,16 +223,24 @@ function OldWithdrawalForm({
         </div>
         <p class="mt-2 text-sm text-gray-500">
           <i18n.Translate>
-            Current balance is <RenderAmount value={balance} 
spec={config.currency_specification} />
+            Current balance is{" "}
+            <RenderAmount
+              value={balance}
+              spec={config.currency_specification}
+            />
           </i18n.Translate>
         </p>
-        {Amounts.cmp(limit, balance) > 0 ?
+        {Amounts.cmp(limit, balance) > 0 ? (
           <p class="mt-2 text-sm text-gray-500">
             <i18n.Translate>
-              Your account allows you to withdraw <RenderAmount value={limit} 
spec={config.currency_specification} />
+              Your account allows you to withdraw{" "}
+              <RenderAmount
+                value={limit}
+                spec={config.currency_specification}
+              />
             </i18n.Translate>
-          </p> : undefined
-        }
+          </p>
+        ) : undefined}
         <div class="mt-4">
           <div class="sm:inline">
             <button
@@ -312,7 +330,7 @@ export function WalletWithdrawForm({
   limit: AmountJson;
   balance: AmountJson;
   focus?: boolean;
-  routeOperationDetails: RouteDefinition<{ wopid: string }>,
+  routeOperationDetails: RouteDefinition<{ wopid: string }>;
   onAuthorizationRequired: () => void;
   onOperationCreated: (wopid: string) => void;
   onOperationAborted: () => void;
@@ -374,7 +392,7 @@ export function WalletWithdrawForm({
             routeClose={routeCancel}
             routeHere={routeOperationDetails}
             onAbort={onOperationAborted}
-          // route={routeCancel}
+            // route={routeCancel}
           />
         )}
       </div>
diff --git a/packages/bank-ui/src/pages/WireTransfer.tsx 
b/packages/bank-ui/src/pages/WireTransfer.tsx
index a3f7d6bc0..a459677f1 100644
--- a/packages/bank-ui/src/pages/WireTransfer.tsx
+++ b/packages/bank-ui/src/pages/WireTransfer.tsx
@@ -43,13 +43,13 @@ export function WireTransfer({
 }: {
   onSuccess?: () => void;
   routeHere: RouteDefinition<{
-    account?: string,
-    subject?: string,
-    amount?: string,
+    account?: string;
+    subject?: string;
+    amount?: string;
   }>;
   toAccount?: string;
-  withSubject?: string,
-  withAmount?: string,
+  withSubject?: string;
+  withAmount?: string;
   routeCancel?: RouteDefinition;
   onAuthorizationRequired: () => void;
 }): VNode {
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 5925719c3..965650eb0 100644
--- a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -96,6 +96,7 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`The withdrawal has been aborted previously and 
can't be confirmed`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
             return notify({
@@ -103,6 +104,7 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`The withdrawal operation can't be confirmed 
before a wallet accepted the transaction.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.BadRequest:
             return notify({
@@ -110,6 +112,7 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`The operation id is invalid.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
@@ -117,6 +120,7 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`The operation was not found.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_UNALLOWED_DEBIT:
             return notify({
@@ -124,12 +128,15 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`Your balance is not enough for the operation.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Accepted: {
             updateBankState("currentChallenge", {
               operation: "confirm-withdrawal",
               id: String(resp.body.challenge_id),
-              location: routeHere.url({ wopid: 
withdrawUri.withdrawalOperationId }),
+              location: routeHere.url({
+                wopid: withdrawUri.withdrawalOperationId,
+              }),
               sent: AbsoluteTime.never(),
               request: withdrawUri.withdrawalOperationId,
             });
@@ -157,6 +164,9 @@ export function WithdrawalConfirmationQuestion({
             return notify({
               type: "error",
               title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.BadRequest:
             return notify({
@@ -164,6 +174,7 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`The operation id is invalid.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
@@ -171,6 +182,7 @@ export function WithdrawalConfirmationQuestion({
               title: i18n.str`The operation was not found.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           default: {
             assertUnreachable(resp);
@@ -218,7 +230,9 @@ export function WithdrawalConfirmationQuestion({
                                   <Fragment>
                                     <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
                                       <dt class="text-sm font-medium leading-6 
text-gray-900">
-                                        <i18n.Translate>Payment provider's 
account number</i18n.Translate>
+                                        <i18n.Translate>
+                                          Payment provider's account number
+                                        </i18n.Translate>
                                       </dt>
                                       <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">
                                         {p.iban}
@@ -227,7 +241,9 @@ export function WithdrawalConfirmationQuestion({
                                     {name && (
                                       <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
                                         <dt class="text-sm font-medium 
leading-6 text-gray-900">
-                                          <i18n.Translate>Payment provider's 
name</i18n.Translate>
+                                          <i18n.Translate>
+                                            Payment provider's name
+                                          </i18n.Translate>
                                         </dt>
                                         <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">
                                           {name}
@@ -244,7 +260,9 @@ export function WithdrawalConfirmationQuestion({
                                   <Fragment>
                                     <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
                                       <dt class="text-sm font-medium leading-6 
text-gray-900">
-                                        <i18n.Translate>Payment provider's 
account id</i18n.Translate>
+                                        <i18n.Translate>
+                                          Payment provider's account id
+                                        </i18n.Translate>
                                       </dt>
                                       <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">
                                         {p.account}
@@ -253,7 +271,9 @@ export function WithdrawalConfirmationQuestion({
                                     {name && (
                                       <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
                                         <dt class="text-sm font-medium 
leading-6 text-gray-900">
-                                          <i18n.Translate>Payment provider's 
name</i18n.Translate>
+                                          <i18n.Translate>
+                                            Payment provider's name
+                                          </i18n.Translate>
                                         </dt>
                                         <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">
                                           {name}
@@ -267,7 +287,9 @@ export function WithdrawalConfirmationQuestion({
                                 return (
                                   <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
                                     <dt class="text-sm font-medium leading-6 
text-gray-900">
-                                      <i18n.Translate>Payment provider's 
account</i18n.Translate>
+                                      <i18n.Translate>
+                                        Payment provider's account
+                                      </i18n.Translate>
                                     </dt>
                                     <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">
                                       {details.account.targetPath}
diff --git a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx 
b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
index b91fecd9d..fb280cf9c 100644
--- a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -31,7 +31,7 @@ export function WithdrawalOperationPage({
 }: {
   onAuthorizationRequired: () => void;
   operationId: string;
-  purpose: "after-creation" | "after-confirmation",
+  purpose: "after-creation" | "after-confirmation";
   onOperationAborted: () => void;
   routeClose: RouteDefinition;
   routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
diff --git a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx 
b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
index 2216b96fc..bd9352b21 100644
--- a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -31,7 +31,7 @@ interface Props {
   routeMyAccountPassword: RouteDefinition;
   routeMyAccountCashout: RouteDefinition;
   routeCreateCashout: RouteDefinition;
-  routeConversionConfig:RouteDefinition;
+  routeConversionConfig: RouteDefinition;
 }
 
 export function CashoutListForAccount({
@@ -58,7 +58,8 @@ export function CashoutListForAccount({
   return (
     <Fragment>
       {accountIsTheCurrentUser ? (
-        <ProfileNavigation current="cashouts"
+        <ProfileNavigation
+          current="cashouts"
           routeMyAccountCashout={routeMyAccountCashout}
           routeMyAccountDelete={routeMyAccountDelete}
           routeMyAccountDetails={routeMyAccountDetails}
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx 
b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
index 62c8df7f8..39b2303c0 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -118,6 +118,7 @@ export function ShowAccountDetails({
               title: i18n.str`The rights to change the account are not 
sufficient`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
@@ -125,6 +126,7 @@ export function ShowAccountDetails({
               title: i18n.str`The username was not found`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
             return notify({
@@ -132,6 +134,7 @@ export function ShowAccountDetails({
               title: i18n.str`You can't change the legal name, please contact 
the your account administrator.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
             return notify({
@@ -139,6 +142,7 @@ export function ShowAccountDetails({
               title: i18n.str`You can't change the debt limit, please contact 
the your account administrator.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
             return notify({
@@ -146,6 +150,7 @@ export function ShowAccountDetails({
               title: i18n.str`You can't change the cashout address, please 
contact the your account administrator.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_MISSING_TAN_INFO:
             return notify({
@@ -153,6 +158,7 @@ export function ShowAccountDetails({
               title: i18n.str`No information for the selected authentication 
channel.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Accepted: {
             updateBankState("currentChallenge", {
@@ -170,6 +176,7 @@ export function ShowAccountDetails({
               title: i18n.str`Authentication channel is not supported.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           }
           default:
@@ -183,7 +190,8 @@ export function ShowAccountDetails({
     <Fragment>
       <LocalNotificationBanner notification={notification} showDebug={true} />
       {accountIsTheCurrentUser ? (
-        <ProfileNavigation current="details"
+        <ProfileNavigation
+          current="details"
           routeMyAccountCashout={routeMyAccountCashout}
           routeMyAccountDelete={routeMyAccountDelete}
           routeConversionConfig={routeConversionConfig}
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx 
b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
index c33aeb09e..8c0581312 100644
--- a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -17,6 +17,7 @@ import {
   AbsoluteTime,
   HttpStatusCode,
   TalerErrorCode,
+  TranslatedString,
   assertUnreachable,
 } from "@gnu-taler/taler-util";
 import {
@@ -112,21 +113,33 @@ export function UpdateAccountPassword({
             return notify({
               type: "error",
               title: i18n.str`Not authorized to change the password, maybe the 
session is invalid.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
               type: "error",
               title: i18n.str`Account not found`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
             return notify({
               type: "error",
               title: i18n.str`You need to provide the old password. If you 
don't have it contact your account administrator.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
             return notify({
               type: "error",
               title: i18n.str`Your current password doesn't match, can't 
change to a new password.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Accepted: {
             updateBankState("currentChallenge", {
@@ -149,7 +162,8 @@ export function UpdateAccountPassword({
     <Fragment>
       <LocalNotificationBanner notification={notification} />
       {accountIsTheCurrentUser ? (
-        <ProfileNavigation current="credentials"
+        <ProfileNavigation
+          current="credentials"
           routeMyAccountCashout={routeMyAccountCashout}
           routeMyAccountDelete={routeMyAccountDelete}
           routeMyAccountDetails={routeMyAccountDetails}
@@ -273,7 +287,6 @@ export function UpdateAccountPassword({
                   <i18n.Translate>Repeat the same password</i18n.Translate>
                 </p>
               </div>
-
             </div>
           </div>
           <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx 
b/packages/bank-ui/src/pages/admin/AccountForm.tsx
index bce7afe11..10b6afdf9 100644
--- a/packages/bank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -18,14 +18,12 @@ import {
   Amounts,
   PaytoString,
   TalerCorebankApi,
-  TranslatedString,
   assertUnreachable,
   buildPayto,
   parsePaytoUri,
   stringifyPaytoUri,
 } from "@gnu-taler/taler-util";
 import {
-  Attention,
   CopyButton,
   ShowInputErrorLabel,
   useTranslationContext,
@@ -41,7 +39,11 @@ import {
   validateIBAN,
   validateTalerBank,
 } from "../../utils.js";
-import { InputAmount, TextField, doAutoFocus } from 
"../PaytoWireTransferForm.js";
+import {
+  InputAmount,
+  TextField,
+  doAutoFocus,
+} from "../PaytoWireTransferForm.js";
 import { getRandomPassword } from "../rnd.js";
 
 const EMAIL_REGEX =
@@ -99,7 +101,10 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
     ErrorMessageMappingFor<typeof defaultValue> | undefined
   >(undefined);
 
-  const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as 
const : "iban" as const;
+  const paytoType =
+    config.wire_type === "X_TALER_BANK"
+      ? ("x-taler-bank" as const)
+      : ("iban" as const);
   const cashoutPaytoType: typeof paytoType = "iban" as const;
 
   const defaultValue: AccountFormData = {
@@ -110,8 +115,10 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
     isPublic: template?.is_public,
     name: template?.name ?? "",
     cashout_payto_uri:
-      getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as 
PaytoString),
-    payto_uri: getAccountId(paytoType, template?.payto_uri) ?? ("" as 
PaytoString),
+      getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ??
+      ("" as PaytoString),
+    payto_uri:
+      getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString),
     email: template?.contact_data?.email ?? "",
     phone: template?.contact_data?.phone ?? "",
     username: username ?? "",
@@ -130,9 +137,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
 
   const isCashoutEnabled = config.allow_conversion;
   const editableCashout =
-    (purpose === "create" ||
-      (purpose === "update" &&
-        (config.allow_edit_cashout_payto_uri || userIsAdmin)));
+    purpose === "create" ||
+    (purpose === "update" &&
+      (config.allow_edit_cashout_payto_uri || userIsAdmin));
   const editableThreshold =
     userIsAdmin && (purpose === "create" || purpose === "update");
   const editableAccount = purpose === "create" && userIsAdmin;
@@ -141,7 +148,6 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   const hasEmail = !!defaultValue.email || !!form.email;
 
   function updateForm(newForm: typeof defaultValue): void {
-
     const trimmedAmountStr = newForm.debit_threshold?.trim();
     const parsedAmount = Amounts.parse(
       `${config.currency}:${trimmedAmountStr}`,
@@ -154,19 +160,25 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
         ? undefined
         : !editableCashout
           ? undefined
-          : !newForm.cashout_payto_uri ? undefined
-            : cashoutPaytoType === "iban" ? 
validateIBAN(newForm.cashout_payto_uri, i18n) :
-              cashoutPaytoType === "x-taler-bank" ? 
validateTalerBank(newForm.cashout_payto_uri, i18n) :
-                undefined,
+          : !newForm.cashout_payto_uri
+            ? undefined
+            : cashoutPaytoType === "iban"
+              ? validateIBAN(newForm.cashout_payto_uri, i18n)
+              : cashoutPaytoType === "x-taler-bank"
+                ? validateTalerBank(newForm.cashout_payto_uri, i18n)
+                : undefined,
 
       payto_uri: !newForm.payto_uri
         ? undefined
         : !editableAccount
           ? undefined
-          : !newForm.payto_uri ? undefined
-            : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) :
-              paytoType === "x-taler-bank" ? 
validateTalerBank(newForm.payto_uri, i18n) :
-                undefined,
+          : !newForm.payto_uri
+            ? undefined
+            : paytoType === "iban"
+              ? validateIBAN(newForm.payto_uri, i18n)
+              : paytoType === "x-taler-bank"
+                ? validateTalerBank(newForm.payto_uri, i18n)
+                : undefined,
 
       email: !newForm.email
         ? undefined
@@ -207,30 +219,38 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
       onChange(undefined);
     } else {
       let cashout;
-      if (newForm.cashout_payto_uri) switch (cashoutPaytoType) {
-        case "x-taler-bank": {
-          cashout = buildPayto("x-taler-bank", url.host, 
newForm.cashout_payto_uri);
-          break;
-        }
-        case "iban": {
-          cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined);
-          break;
+      if (newForm.cashout_payto_uri)
+        switch (cashoutPaytoType) {
+          case "x-taler-bank": {
+            cashout = buildPayto(
+              "x-taler-bank",
+              url.host,
+              newForm.cashout_payto_uri,
+            );
+            break;
+          }
+          case "iban": {
+            cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined);
+            break;
+          }
+          default:
+            assertUnreachable(cashoutPaytoType);
         }
-        default: assertUnreachable(cashoutPaytoType)
-      }
       const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
       let internal;
-      if (newForm.payto_uri) switch (paytoType) {
-        case "x-taler-bank": {
-          internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri);
-          break;
-        }
-        case "iban": {
-          internal = buildPayto("iban", newForm.payto_uri, undefined);
-          break;
+      if (newForm.payto_uri)
+        switch (paytoType) {
+          case "x-taler-bank": {
+            internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri);
+            break;
+          }
+          case "iban": {
+            internal = buildPayto("iban", newForm.payto_uri, undefined);
+            break;
+          }
+          default:
+            assertUnreachable(paytoType);
         }
-        default: assertUnreachable(paytoType)
-      }
       const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
 
       const threshold = !parsedAmount
@@ -247,7 +267,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             username: newForm.username!,
             contact_data: undefinedIfEmpty({
               email: !newForm.email ? undefined : newForm.email,
-              phone: !newForm.phone ? undefined :newForm.phone,
+              phone: !newForm.phone ? undefined : newForm.phone,
             }),
             debit_threshold: threshold ?? config.default_debit_threshold,
             cashout_payto_uri: cashoutURI,
@@ -270,7 +290,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             cashout_payto_uri: cashoutURI,
             contact_data: undefinedIfEmpty({
               email: !newForm.email ? undefined : newForm.email,
-              phone: !newForm.phone ? undefined :newForm.phone,
+              phone: !newForm.phone ? undefined : newForm.phone,
             }),
             debit_threshold: threshold,
             is_public: newForm.isPublic,
@@ -370,7 +390,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             </p>
           </div>
 
-          {purpose === "create" ? undefined :
+          {purpose === "create" ? undefined : (
             <TextField
               id="internal-account"
               label={i18n.str`Internal account`}
@@ -379,20 +399,23 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                   ? i18n.str`If empty a random account id will be assigned`
                   : i18n.str`Share this id to receive bank transfers`
               }
-
               error={errors?.payto_uri}
               onChange={(e) => {
                 form.payto_uri = e as PaytoString;
                 updateForm(structuredClone(form));
               }}
-              rightIcons={<CopyButton
-                class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-                getContent={() => form.payto_uri ?? defaultValue.payto_uri ?? 
""}
-              />}
+              rightIcons={
+                <CopyButton
+                  class="p-2 rounded-full  text-black shadow-sm  
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+                  getContent={() =>
+                    form.payto_uri ?? defaultValue.payto_uri ?? ""
+                  }
+                />
+              }
               value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
               disabled={!editableAccount}
             />
-          }
+          )}
 
           <div class="sm:col-span-5">
             <label
@@ -422,7 +445,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               />
             </div>
             <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>To be used when second factor authentication is 
enabled</i18n.Translate>
+              <i18n.Translate>
+                To be used when second factor authentication is enabled
+              </i18n.Translate>
             </p>
           </div>
 
@@ -454,7 +479,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               />
             </div>
             <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>To be used when second factor authentication is 
enabled</i18n.Translate>
+              <i18n.Translate>
+                To be used when second factor authentication is enabled
+              </i18n.Translate>
             </p>
           </div>
 
@@ -468,14 +495,17 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                 form.cashout_payto_uri = e as PaytoString;
                 updateForm(structuredClone(form));
               }}
-              value={(form.cashout_payto_uri ?? 
defaultValue.cashout_payto_uri) as PaytoString}
+              value={
+                (form.cashout_payto_uri ??
+                  defaultValue.cashout_payto_uri) as PaytoString
+              }
               disabled={!editableCashout}
             />
           )}
 
           {/* channel, not shown if old cashout api */}
           {OLD_CASHOUT_API ||
-            config.supported_tan_channels.length === 0 ? undefined : (
+          config.supported_tan_channels.length === 0 ? undefined : (
             <div class="sm:col-span-5">
               <label
                 class="block text-sm font-medium leading-6 text-gray-900"
@@ -486,7 +516,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               <div class="mt-2 max-w-xl text-sm text-gray-500">
                 <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
                   {config.supported_tan_channels.indexOf(TanChannel.EMAIL) ===
-                    -1 ? undefined : (
+                  -1 ? undefined : (
                     <label
                       onClick={(e) => {
                         if (!hasEmail) return;
@@ -544,7 +574,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                   )}
 
                   {config.supported_tan_channels.indexOf(TanChannel.SMS) ===
-                    -1 ? undefined : (
+                  -1 ? undefined : (
                     <label
                       onClick={(e) => {
                         if (!hasPhone) return;
@@ -619,9 +649,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                 !editableThreshold
                   ? undefined
                   : (e) => {
-                    form.debit_threshold = e as AmountString;
-                    updateForm(structuredClone(form));
-                  }
+                      form.debit_threshold = e as AmountString;
+                      updateForm(structuredClone(form));
+                    }
               }
             />
             <ShowInputErrorLabel
@@ -633,7 +663,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               isDirty={form.debit_threshold !== undefined}
             />
             <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>How much the balance can go below 
zero.</i18n.Translate>
+              <i18n.Translate>
+                How much the balance can go below zero.
+              </i18n.Translate>
             </p>
           </div>
 
@@ -673,7 +705,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
               </button>
             </div>
             <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>Public accounts have their balance publicly 
accessible</i18n.Translate>
+              <i18n.Translate>
+                Public accounts have their balance publicly accessible
+              </i18n.Translate>
             </p>
           </div>
 
@@ -685,7 +719,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                     class="text-sm text-black font-medium leading-6 "
                     id="availability-label"
                   >
-                    <i18n.Translate>Is this account a payment 
provider?</i18n.Translate>
+                    <i18n.Translate>
+                      Is this account a payment provider?
+                    </i18n.Translate>
                   </span>
                 </span>
                 <button
@@ -726,13 +762,17 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   );
 }
 
-function getAccountId(type: "iban" | "x-taler-bank", s: PaytoString | 
undefined): string | undefined {
+function getAccountId(
+  type: "iban" | "x-taler-bank",
+  s: PaytoString | undefined,
+): string | undefined {
   if (s === undefined) return undefined;
   const p = parsePaytoUri(s);
   if (p === undefined) return undefined;
   if (!p.isKnown) return "<unknown>";
   if (type === "iban" && p.targetType === "iban") return p.iban;
-  if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return 
p.account;
+  if (type === "x-taler-bank" && p.targetType === "x-taler-bank")
+    return p.account;
   return "<unsupported>";
 }
 
diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx 
b/packages/bank-ui/src/pages/admin/AccountList.tsx
index 8a692aaed..3ab491960 100644
--- a/packages/bank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountList.tsx
@@ -24,8 +24,8 @@ import { Fragment, VNode, h } from "preact";
 import { ErrorLoadingWithDebug } from 
"../../components/ErrorLoadingWithDebug.js";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { useBusinessAccounts } from "../../hooks/regional.js";
-import { RenderAmount } from "../PaytoWireTransferForm.js";
 import { RouteDefinition } from "../../route.js";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
 
 interface Props {
   routeCreate: RouteDefinition;
@@ -33,14 +33,12 @@ interface Props {
   routeShowAccount: RouteDefinition<{ account: string }>;
   routeRemoveAccount: RouteDefinition<{ account: string }>;
   routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
-  routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
 }
 
 export function AccountList({
   routeCreate,
   routeRemoveAccount,
   routeShowAccount,
-  routeShowCashoutsAccount,
   routeUpdatePasswordAccount,
 }: Props): VNode {
   const result = useBusinessAccounts();
@@ -62,8 +60,8 @@ export function AccountList({
     }
   }
 
-  const onGoStart = result.isFirstPage ? undefined : result.loadFirst
-  const onGoNext = result.isLastPage ? undefined : result.loadNext
+  const onGoStart = result.isFirstPage ? undefined : result.loadFirst;
+  const onGoNext = result.isLastPage ? undefined : result.loadNext;
 
   const accounts = result.result;
   return (
@@ -90,9 +88,7 @@ export function AccountList({
           <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
             <div class="inline-block min-w-full py-2 align-middle sm:px-6 
lg:px-8">
               {!accounts.length ? (
-                <div>
-                  {/* FIXME: ADD empty list */}
-                </div>
+                <div>{/* FIXME: ADD empty list */}</div>
               ) : (
                 <table class="min-w-full divide-y divide-gray-300">
                   <thead>
@@ -230,7 +226,6 @@ export function AccountList({
                 </button>
               </div>
             </nav>
-
           </div>
         </div>
       </div>
diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx 
b/packages/bank-ui/src/pages/admin/AdminHome.tsx
index 752d86aa6..b8b28f8a0 100644
--- a/packages/bank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -53,9 +53,9 @@ interface Props {
   routeCreate: RouteDefinition;
   routeDownloadStats: RouteDefinition;
   routeCreateWireTransfer: RouteDefinition<{
-    account?: string,
-    subject?: string,
-    amount?: string,
+    account?: string;
+    subject?: string;
+    amount?: string;
   }>;
 
   routeShowAccount: RouteDefinition<{ account: string }>;
@@ -68,7 +68,6 @@ export function AdminHome({
   routeCreate,
   routeRemoveAccount,
   routeShowAccount,
-  routeShowCashoutsAccount,
   routeUpdatePasswordAccount,
   routeDownloadStats,
   routeCreateWireTransfer,
@@ -77,7 +76,10 @@ export function AdminHome({
   return (
     <Fragment>
       <Metrics routeDownloadStats={routeDownloadStats} />
-      <WireTransfer routeHere={routeCreateWireTransfer} 
onAuthorizationRequired={onAuthorizationRequired} />
+      <WireTransfer
+        routeHere={routeCreateWireTransfer}
+        onAuthorizationRequired={onAuthorizationRequired}
+      />
 
       <Transactions
         account="admin"
@@ -87,7 +89,6 @@ export function AdminHome({
         routeCreate={routeCreate}
         routeRemoveAccount={routeRemoveAccount}
         routeShowAccount={routeShowAccount}
-        routeShowCashoutsAccount={routeShowCashoutsAccount}
         routeUpdatePasswordAccount={routeUpdatePasswordAccount}
       />
     </Fragment>
@@ -355,13 +356,16 @@ function Metrics({
       </div>
       <dl class="mt-5 grid grid-cols-1 md:grid-cols-2  divide-y 
divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x 
md:divide-y-0">
         {resp.current.body.type !== "with-conversions" ||
-          resp.previous.body.type !== "with-conversions" ? undefined : (
+        resp.previous.body.type !== "with-conversions" ? undefined : (
           <Fragment>
             <div class="px-4 py-5 sm:p-6">
               <dt class="text-base font-normal text-gray-900">
                 <i18n.Translate>Cashin</i18n.Translate>
                 <div class="text-xs text-gray-500">
-                  <i18n.Translate>Transferred from an external account to an 
account in this bank.</i18n.Translate>
+                  <i18n.Translate>
+                    Transferred from an external account to an account in this
+                    bank.
+                  </i18n.Translate>
                 </div>
               </dt>
               <MetricValue
@@ -375,8 +379,11 @@ function Metrics({
                 <i18n.Translate>Cashout</i18n.Translate>
               </dt>
               <div class="text-xs text-gray-500">
-                  <i18n.Translate>Transferred from an account in this bank to 
an external account.</i18n.Translate>
-                </div>
+                <i18n.Translate>
+                  Transferred from an account in this bank to an external
+                  account.
+                </i18n.Translate>
+              </div>
               <MetricValue
                 current={resp.current.body.cashoutFiatVolume}
                 previous={resp.previous.body.cashoutFiatVolume}
@@ -389,7 +396,9 @@ function Metrics({
           <dt class="text-base font-normal text-gray-900">
             <i18n.Translate>Payin</i18n.Translate>
             <div class="text-xs text-gray-500">
-              <i18n.Translate>Transferred from an account to a Taler 
exchange.</i18n.Translate>
+              <i18n.Translate>
+                Transferred from an account to a Taler exchange.
+              </i18n.Translate>
             </div>
           </dt>
           <MetricValue
@@ -402,7 +411,9 @@ function Metrics({
           <dt class="text-base font-normal text-gray-900">
             <i18n.Translate>Payout</i18n.Translate>
             <div class="text-xs text-gray-500">
-              <i18n.Translate>Transferred from a Taler exchange to another 
account.</i18n.Translate>
+              <i18n.Translate>
+                Transferred from a Taler exchange to another account.
+              </i18n.Translate>
             </div>
           </dt>
           <MetricValue
@@ -444,9 +455,9 @@ function MetricValue({
 
   const rate =
     !currAmount ||
-      Number.isNaN(currAmount) ||
-      !prevAmount ||
-      Number.isNaN(prevAmount)
+    Number.isNaN(currAmount) ||
+    !prevAmount ||
+    Number.isNaN(prevAmount)
       ? 0
       : cmp === -1
         ? 1 - Math.round(currAmount) / Math.round(prevAmount)
diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx 
b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
index 38119735e..f5755e2cd 100644
--- a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -14,6 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 import {
+  AbsoluteTime,
   HttpStatusCode,
   TalerCorebankApi,
   TalerErrorCode,
@@ -69,6 +70,7 @@ export function CreateNewAccount({
               title: i18n.str`Server replied that phone or email is invalid`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Unauthorized:
             return notify({
@@ -76,6 +78,7 @@ export function CreateNewAccount({
               title: i18n.str`The rights to perform the operation are not 
sufficient`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
             return notify({
@@ -83,6 +86,7 @@ export function CreateNewAccount({
               title: i18n.str`Account username is already taken`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
             return notify({
@@ -90,6 +94,7 @@ export function CreateNewAccount({
               title: i18n.str`Account id is already taken`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_UNALLOWED_DEBIT:
             return notify({
@@ -97,6 +102,7 @@ export function CreateNewAccount({
               title: i18n.str`Bank ran out of bonus credit.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
             return notify({
@@ -104,6 +110,7 @@ export function CreateNewAccount({
               title: i18n.str`Account username can't be used because is 
reserved`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
             return notify({
@@ -111,6 +118,7 @@ export function CreateNewAccount({
               title: i18n.str`Only admin is allow to set debt limit.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_MISSING_TAN_INFO:
             return notify({
@@ -118,6 +126,7 @@ export function CreateNewAccount({
               title: i18n.str`No information for the selected authentication 
channel.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
             return notify({
@@ -125,6 +134,7 @@ export function CreateNewAccount({
               title: i18n.str`Authentication channel is not supported.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
             return notify({
@@ -132,6 +142,7 @@ export function CreateNewAccount({
               title: i18n.str`Only admin can create accounts with second 
factor authentication.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           default:
             assertUnreachable(resp);
diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx 
b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
index fba366676..40035db51 100644
--- a/packages/bank-ui/src/pages/admin/DownloadStats.tsx
+++ b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
@@ -31,7 +31,7 @@ import { VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { useSessionState } from "../../hooks/session.js";
-import { EmptyObject, RouteDefinition } from "../../route.js";
+import { RouteDefinition } from "../../route.js";
 import { getTimeframesForDate } from "./AdminHome.js";
 
 interface Props {
@@ -341,7 +341,8 @@ export function DownloadStats({ routeCancel }: Props): 
VNode {
           </div>
 
           <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
-            <a name="cancel"
+            <a
+              name="cancel"
               href={routeCancel.url({})}
               class="text-sm font-semibold leading-6 text-gray-900"
             >
@@ -459,9 +460,9 @@ async function fetchAllStatus(
       // await delay()
       const previous = options.compareWithPrevious
         ? await api.getMonitor(token, {
-          timeframe: frame.timeframe,
-          which: frame.moment.previous,
-        })
+            timeframe: frame.timeframe,
+            which: frame.moment.previous,
+          })
         : undefined;
 
       if (previous && previous.type === "fail" && options.endOnFirstFail) {
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx 
b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
index 61def9a95..74172d058 100644
--- a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
@@ -127,6 +127,7 @@ export function RemoveAccount({
               title: i18n.str`No enough permission to delete the account.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotFound:
             return notify({
@@ -134,6 +135,7 @@ export function RemoveAccount({
               title: i18n.str`The username was not found.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
             return notify({
@@ -141,6 +143,7 @@ export function RemoveAccount({
               title: i18n.str`Can't delete a reserved username.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
             return notify({
@@ -148,6 +151,7 @@ export function RemoveAccount({
               title: i18n.str`Can't delete an account with balance different 
than zero.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.Accepted: {
             updateBankState("currentChallenge", {
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx 
b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
index 8845ec9a0..818a131e0 100644
--- a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
+++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -15,13 +15,14 @@
  */
 
 import {
+  AbsoluteTime,
   AmountJson,
   Amounts,
   HttpStatusCode,
   TalerBankConversionApi,
   TalerError,
   TranslatedString,
-  assertUnreachable
+  assertUnreachable,
 } from "@gnu-taler/taler-util";
 import {
   Attention,
@@ -30,18 +31,30 @@ import {
   ShowInputErrorLabel,
   useLocalNotification,
   useTranslationContext,
-  utils
+  utils,
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { useSessionState } from "../../hooks/session.js";
-import { TransferCalculation, useCashinEstimator, useCashoutEstimator, 
useConversionInfo } from "../../hooks/regional.js";
+import {
+  TransferCalculation,
+  useCashinEstimator,
+  useCashoutEstimator,
+  useConversionInfo,
+} from "../../hooks/regional.js";
 import { RouteDefinition } from "../../route.js";
 import { undefinedIfEmpty } from "../../utils.js";
 import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js";
 import { ProfileNavigation } from "../ProfileNavigation.js";
-import { FormErrors, FormStatus, FormValues, RecursivePartial, UIField, 
useFormState } from "../../hooks/form.js";
+import {
+  FormErrors,
+  FormStatus,
+  FormValues,
+  RecursivePartial,
+  UIField,
+  useFormState,
+} from "../../hooks/form.js";
 
 interface Props {
   routeMyAccountDetails: RouteDefinition;
@@ -53,11 +66,12 @@ interface Props {
   onUpdateSuccess: () => void;
 }
 
-type FormType = { amount: AmountJson, conv: 
TalerBankConversionApi.ConversionRate }
-
+type FormType = {
+  amount: AmountJson;
+  conv: TalerBankConversionApi.ConversionRate;
+};
 
 function useComponentState({
-  onUpdateSuccess,
   routeCancel,
   routeConversionConfig,
   routeMyAccountCashout,
@@ -67,9 +81,11 @@ function useComponentState({
 }: Props): utils.RecursiveState<VNode> {
   const { i18n } = useTranslationContext();
 
-  const result = useConversionInfo()
-  const info = result && !(result instanceof TalerError) && result.type === 
"ok" ?
-    result.body : undefined;
+  const result = useConversionInfo();
+  const info =
+    result && !(result instanceof TalerError) && result.type === "ok"
+      ? result.body
+      : undefined;
 
   const { state: credentials } = useSessionState();
   const creds =
@@ -78,17 +94,17 @@ function useComponentState({
       : credentials;
 
   if (!info) {
-    return <i18n.Translate>loading...</i18n.Translate>
+    return <i18n.Translate>loading...</i18n.Translate>;
   }
 
   if (!creds) {
-    return <i18n.Translate>only admin can setup conversion</i18n.Translate>
+    return <i18n.Translate>only admin can setup conversion</i18n.Translate>;
   }
 
-  return () => {
+  return function afterComponentLoads() {
     const { i18n } = useTranslationContext();
 
-    const { bank, conversion, config } = useBankCoreApiContext();
+    const { conversion } = useBankCoreApiContext();
 
     const [notification, notify, handleError] = useLocalNotification();
 
@@ -96,66 +112,91 @@ function useComponentState({
       amount: "100",
       conv: {
         cashin_min_amount: 
info.conversion_rate.cashin_min_amount.split(":")[1],
-        cashin_tiny_amount: 
info.conversion_rate.cashin_tiny_amount.split(":")[1],
+        cashin_tiny_amount:
+          info.conversion_rate.cashin_tiny_amount.split(":")[1],
         cashin_fee: info.conversion_rate.cashin_fee.split(":")[1],
         cashin_ratio: info.conversion_rate.cashin_ratio,
         cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode,
-        cashout_min_amount: 
info.conversion_rate.cashout_min_amount.split(":")[1],
-        cashout_tiny_amount: 
info.conversion_rate.cashout_tiny_amount.split(":")[1],
+        cashout_min_amount:
+          info.conversion_rate.cashout_min_amount.split(":")[1],
+        cashout_tiny_amount:
+          info.conversion_rate.cashout_tiny_amount.split(":")[1],
         cashout_fee: info.conversion_rate.cashout_fee.split(":")[1],
         cashout_ratio: info.conversion_rate.cashout_ratio,
         cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode,
-      }
-    }
+      },
+    };
 
     const [form, status] = useFormState<FormType>(
       initalState,
-      createFormValidator(i18n, info.regional_currency, info.fiat_currency)
-    )
+      createFormValidator(i18n, info.regional_currency, info.fiat_currency),
+    );
 
-    const {
-      estimateByDebit: calculateCashoutFromDebit,
-    } = useCashoutEstimator();
+    const { estimateByDebit: calculateCashoutFromDebit } =
+      useCashoutEstimator();
 
-    const {
-      estimateByDebit: calculateCashinFromDebit,
-    } = useCashinEstimator();
+    const { estimateByDebit: calculateCashinFromDebit } = useCashinEstimator();
 
-    const [calculationResult, setCalc] = useState<{ cashin: 
TransferCalculation, cashout: TransferCalculation }>()
+    const [calculationResult, setCalc] = useState<{
+      cashin: TransferCalculation;
+      cashout: TransferCalculation;
+    }>();
 
     useEffect(() => {
       async function doAsync() {
         await handleError(async () => {
           if (!info) return;
           if (!form.amount?.value || form.amount.error) return;
-          const in_amount = 
Amounts.parseOrThrow(`${info.fiat_currency}:${form.amount.value}`)
-          const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee)
+          const in_amount = Amounts.parseOrThrow(
+            `${info.fiat_currency}:${form.amount.value}`,
+          );
+          const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
           const cashin = await calculateCashinFromDebit(in_amount, in_fee);
 
           if (cashin === "amount-is-too-small") {
-            setCalc(undefined)
+            setCalc(undefined);
             return;
           }
           // const out_amount = 
Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`)
-          const out_fee = 
Amounts.parseOrThrow(info.conversion_rate.cashout_fee)
-          const cashout = await calculateCashoutFromDebit(cashin.credit, 
out_fee);
+          const out_fee = Amounts.parseOrThrow(
+            info.conversion_rate.cashout_fee,
+          );
+          const cashout = await calculateCashoutFromDebit(
+            cashin.credit,
+            out_fee,
+          );
 
           setCalc({ cashin, cashout });
         });
       }
       doAsync();
-    }, [form.amount?.value, form.conv?.cashin_fee?.value, 
form.conv?.cashout_fee?.value]);
-
-    const [section, setSection] = useState<"detail" | "cashout" | 
"cashin">("detail")
-    const cashinCalc = calculationResult?.cashin === "amount-is-too-small" ? 
undefined : calculationResult?.cashin
-    const cashoutCalc = calculationResult?.cashout === "amount-is-too-small" ? 
undefined : calculationResult?.cashout
+    }, [
+      form.amount?.value,
+      form.conv?.cashin_fee?.value,
+      form.conv?.cashout_fee?.value,
+    ]);
+
+    const [section, setSection] = useState<"detail" | "cashout" | "cashin">(
+      "detail",
+    );
+    const cashinCalc =
+      calculationResult?.cashin === "amount-is-too-small"
+        ? undefined
+        : calculationResult?.cashin;
+    const cashoutCalc =
+      calculationResult?.cashout === "amount-is-too-small"
+        ? undefined
+        : calculationResult?.cashout;
     async function doUpdate() {
-      if (!creds) return
+      if (!creds) return;
       await handleError(async () => {
         if (status.status === "fail") return;
-        const resp = await conversion.updateConversionRate(creds.token, 
status.result.conv)
+        const resp = await conversion.updateConversionRate(
+          creds.token,
+          status.result.conv,
+        );
         if (resp.type === "ok") {
-          setSection("detail")
+          setSection("detail");
         } else {
           switch (resp.case) {
             case HttpStatusCode.Unauthorized: {
@@ -164,6 +205,7 @@ function useComponentState({
                 title: i18n.str`Wrong credentials`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             }
             case HttpStatusCode.NotImplemented: {
@@ -172,6 +214,7 @@ function useComponentState({
                 title: i18n.str`Conversion is disabled`,
                 description: resp.detail.hint as TranslatedString,
                 debug: resp.detail,
+                when: AbsoluteTime.now(),
               });
             }
             default:
@@ -181,16 +224,16 @@ function useComponentState({
       });
     }
 
-    const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio)
-    const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio)
+    const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio);
+    const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio);
 
     const both_high = in_ratio > 1 && out_ratio > 1;
     const both_low = in_ratio < 1 && out_ratio < 1;
 
-
     return (
       <div>
-        <ProfileNavigation current="conversion"
+        <ProfileNavigation
+          current="conversion"
           routeMyAccountCashout={routeMyAccountCashout}
           routeMyAccountDelete={routeMyAccountDelete}
           routeMyAccountDetails={routeMyAccountDetails}
@@ -200,7 +243,6 @@ function useComponentState({
 
         <LocalNotificationBanner notification={notification} />
         <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
-
           <div class="px-4 sm:px-0">
             <h2 class="text-base font-semibold leading-7 text-gray-900">
               <i18n.Translate>Conversion</i18n.Translate>
@@ -218,7 +260,7 @@ function useComponentState({
                   aria-labelledby="project-type-0-label"
                   aria-describedby="project-type-0-description-0 
project-type-0-description-1"
                   onChange={() => {
-                    setSection("detail")
+                    setSection("detail");
                   }}
                 />
                 <span class="flex flex-1">
@@ -242,7 +284,7 @@ function useComponentState({
                   aria-labelledby="project-type-1-label"
                   aria-describedby="project-type-1-description-0 
project-type-1-description-1"
                   onChange={() => {
-                    setSection("cashout")
+                    setSection("cashout");
                   }}
                 />
                 <span class="flex flex-1">
@@ -265,7 +307,7 @@ function useComponentState({
                   aria-labelledby="project-type-1-label"
                   aria-describedby="project-type-1-description-0 
project-type-1-description-1"
                   onChange={() => {
-                    setSection("cashin")
+                    setSection("cashin");
                   }}
                 />
                 <span class="flex flex-1">
@@ -277,7 +319,6 @@ function useComponentState({
                 </span>
               </label>
             </div>
-
           </div>
 
           <form
@@ -288,8 +329,9 @@ function useComponentState({
               e.preventDefault();
             }}
           >
-            {section == "cashin" &&
-              <ConversionForm id="cashin"
+            {section == "cashin" && (
+              <ConversionForm
+                id="cashin"
                 inputCurrency={info.fiat_currency}
                 outputCurrency={info.regional_currency}
                 fee={form?.conv?.cashin_fee}
@@ -297,682 +339,830 @@ function useComponentState({
                 ratio={form?.conv?.cashin_ratio}
                 rounding={form?.conv?.cashin_rounding_mode}
                 tiny={form?.conv?.cashin_tiny_amount}
-              />}
-
-            {section == "cashout" && <Fragment>
-              <ConversionForm id="cashout"
-                inputCurrency={info.regional_currency}
-                outputCurrency={info.fiat_currency}
-                fee={form?.conv?.cashout_fee}
-                minimum={form?.conv?.cashout_min_amount}
-                ratio={form?.conv?.cashout_ratio}
-                rounding={form?.conv?.cashout_rounding_mode}
-                tiny={form?.conv?.cashout_tiny_amount}
               />
-            </Fragment>}
-
-            {section == "detail" && <Fragment>
-              <div class="px-6 pt-6">
-                <div class="justify-between items-center flex ">
-                  <dt class="text-sm text-gray-600">
-                    <i18n.Translate>Cashin ratio</i18n.Translate>
-                  </dt>
-                  <dd class="text-sm text-gray-900">
-                    {info.conversion_rate.cashin_ratio}
-                  </dd>
-                </div>
-              </div>
+            )}
+
+            {section == "cashout" && (
+              <Fragment>
+                <ConversionForm
+                  id="cashout"
+                  inputCurrency={info.regional_currency}
+                  outputCurrency={info.fiat_currency}
+                  fee={form?.conv?.cashout_fee}
+                  minimum={form?.conv?.cashout_min_amount}
+                  ratio={form?.conv?.cashout_ratio}
+                  rounding={form?.conv?.cashout_rounding_mode}
+                  tiny={form?.conv?.cashout_tiny_amount}
+                />
+              </Fragment>
+            )}
 
-              <div class="px-6 pt-6">
-                <div class="justify-between items-center flex ">
-                  <dt class="text-sm text-gray-600">
-                    <i18n.Translate>Cashout ratio</i18n.Translate>
-                  </dt>
-                  <dd class="text-sm text-gray-900">
-                    {info.conversion_rate.cashout_ratio}
-                  </dd>
+            {section == "detail" && (
+              <Fragment>
+                <div class="px-6 pt-6">
+                  <div class="justify-between items-center flex ">
+                    <dt class="text-sm text-gray-600">
+                      <i18n.Translate>Cashin ratio</i18n.Translate>
+                    </dt>
+                    <dd class="text-sm text-gray-900">
+                      {info.conversion_rate.cashin_ratio}
+                    </dd>
+                  </div>
                 </div>
-              </div>
 
-              {both_low || both_high ? <div class="p-4">
-                <Attention title={i18n.str`Bad ratios`} type="warning">
-                  <i18n.Translate>
-                    One of the ratios should be higher or equal than 1 an the 
other should be lower or equal than 1.
-                  </i18n.Translate>
-                </Attention>
-              </div> : undefined}
-
-              <div class="px-6 pt-6">
-                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
-                  <div class="sm:col-span-5">
-                    <label
-                      for="amount"
-                      class="block text-sm font-medium leading-6 text-gray-900"
-                    >{i18n.str`Initial amount`}</label>
-                    <InputAmount
-                      name="amount"
-                      left
-                      currency={info.fiat_currency}
-                      value={form.amount?.value ?? ""}
-                      onChange={form.amount?.onUpdate}
-                    />
-                    <ShowInputErrorLabel
-                      message={form.amount?.error}
-                      isDirty={form.amount?.value !== undefined}
-                    />
-                    <p class="mt-2 text-sm text-gray-500">
-                      <i18n.Translate>Use it to test how the conversion will 
affect the amount.</i18n.Translate>
-                    </p>
+                <div class="px-6 pt-6">
+                  <div class="justify-between items-center flex ">
+                    <dt class="text-sm text-gray-600">
+                      <i18n.Translate>Cashout ratio</i18n.Translate>
+                    </dt>
+                    <dd class="text-sm text-gray-900">
+                      {info.conversion_rate.cashout_ratio}
+                    </dd>
                   </div>
                 </div>
-              </div>
 
-              {!cashoutCalc || !cashinCalc ? undefined : (
+                {both_low || both_high ? (
+                  <div class="p-4">
+                    <Attention title={i18n.str`Bad ratios`} type="warning">
+                      <i18n.Translate>
+                        One of the ratios should be higher or equal than 1 an
+                        the other should be lower or equal than 1.
+                      </i18n.Translate>
+                    </Attention>
+                  </div>
+                ) : undefined}
+
                 <div class="px-6 pt-6">
-                  <div class="sm:col-span-5">
-                    <dl class="mt-4 space-y-4">
-                      <div class="justify-between items-center flex ">
-                        <dt class="text-sm text-gray-600">
-                          <i18n.Translate>Sending to this bank</i18n.Translate>
-                        </dt>
-                        <dd class="text-sm text-gray-900">
-                          <RenderAmount
-                            value={cashinCalc.debit}
-                            negative
-                            withColor
-                            spec={info.regional_currency_specification}
-                          />
-                        </dd>
-                      </div>
+                  <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                    <div class="sm:col-span-5">
+                      <label
+                        for="amount"
+                        class="block text-sm font-medium leading-6 
text-gray-900"
+                      >{i18n.str`Initial amount`}</label>
+                      <InputAmount
+                        name="amount"
+                        left
+                        currency={info.fiat_currency}
+                        value={form.amount?.value ?? ""}
+                        onChange={form.amount?.onUpdate}
+                      />
+                      <ShowInputErrorLabel
+                        message={form.amount?.error}
+                        isDirty={form.amount?.value !== undefined}
+                      />
+                      <p class="mt-2 text-sm text-gray-500">
+                        <i18n.Translate>
+                          Use it to test how the conversion will affect the
+                          amount.
+                        </i18n.Translate>
+                      </p>
+                    </div>
+                  </div>
+                </div>
 
-                      {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
-                        <div class="flex items-center justify-between afu ">
-                          <dt class="flex items-center text-sm text-gray-600">
-                            <span>
-                              <i18n.Translate>Converted</i18n.Translate>
-                            </span>
+                {!cashoutCalc || !cashinCalc ? undefined : (
+                  <div class="px-6 pt-6">
+                    <div class="sm:col-span-5">
+                      <dl class="mt-4 space-y-4">
+                        <div class="justify-between items-center flex ">
+                          <dt class="text-sm text-gray-600">
+                            <i18n.Translate>
+                              Sending to this bank
+                            </i18n.Translate>
                           </dt>
                           <dd class="text-sm text-gray-900">
                             <RenderAmount
-                              value={cashinCalc.beforeFee}
-                              spec={info.fiat_currency_specification}
+                              value={cashinCalc.debit}
+                              negative
+                              withColor
+                              spec={info.regional_currency_specification}
                             />
                           </dd>
                         </div>
-                      )}
-                      <div class="flex justify-between items-center border-t-2 
afu pt-4">
-                        <dt class="text-lg text-gray-900 font-medium">
-                          <i18n.Translate>Cashin after fee</i18n.Translate>
-                        </dt>
-                        <dd class="text-lg text-gray-900 font-medium">
-                          <RenderAmount
-                            value={cashinCalc.credit}
-                            withColor
-                            spec={info.fiat_currency_specification}
-                          />
-                        </dd>
-                      </div>
-                    </dl>
-                  </div>
-
-                  <div class="sm:col-span-5">
-                    <dl class="mt-4 space-y-4">
-                      <div class="justify-between items-center flex ">
-                        <dt class="text-sm text-gray-600">
-                          <i18n.Translate>Sending from this 
bank</i18n.Translate>
-                        </dt>
-                        <dd class="text-sm text-gray-900">
-                          <RenderAmount
-                            value={cashoutCalc.debit}
-                            negative
-                            withColor
-                            spec={info.fiat_currency_specification}
-                          />
-                        </dd>
-                      </div>
 
-                      {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
-                        <div class="flex items-center justify-between afu">
-                          <dt class="flex items-center text-sm text-gray-600">
-                            <span>
-                              <i18n.Translate>Converted</i18n.Translate>
-                            </span>
+                        {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
+                          <div class="flex items-center justify-between afu ">
+                            <dt class="flex items-center text-sm 
text-gray-600">
+                              <span>
+                                <i18n.Translate>Converted</i18n.Translate>
+                              </span>
+                            </dt>
+                            <dd class="text-sm text-gray-900">
+                              <RenderAmount
+                                value={cashinCalc.beforeFee}
+                                spec={info.fiat_currency_specification}
+                              />
+                            </dd>
+                          </div>
+                        )}
+                        <div class="flex justify-between items-center 
border-t-2 afu pt-4">
+                          <dt class="text-lg text-gray-900 font-medium">
+                            <i18n.Translate>Cashin after fee</i18n.Translate>
+                          </dt>
+                          <dd class="text-lg text-gray-900 font-medium">
+                            <RenderAmount
+                              value={cashinCalc.credit}
+                              withColor
+                              spec={info.fiat_currency_specification}
+                            />
+                          </dd>
+                        </div>
+                      </dl>
+                    </div>
+
+                    <div class="sm:col-span-5">
+                      <dl class="mt-4 space-y-4">
+                        <div class="justify-between items-center flex ">
+                          <dt class="text-sm text-gray-600">
+                            <i18n.Translate>
+                              Sending from this bank
+                            </i18n.Translate>
                           </dt>
                           <dd class="text-sm text-gray-900">
                             <RenderAmount
-                              value={cashoutCalc.beforeFee}
+                              value={cashoutCalc.debit}
+                              negative
+                              withColor
+                              spec={info.fiat_currency_specification}
+                            />
+                          </dd>
+                        </div>
+
+                        {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
+                          <div class="flex items-center justify-between afu">
+                            <dt class="flex items-center text-sm 
text-gray-600">
+                              <span>
+                                <i18n.Translate>Converted</i18n.Translate>
+                              </span>
+                            </dt>
+                            <dd class="text-sm text-gray-900">
+                              <RenderAmount
+                                value={cashoutCalc.beforeFee}
+                                spec={info.regional_currency_specification}
+                              />
+                            </dd>
+                          </div>
+                        )}
+                        <div class="flex justify-between items-center 
border-t-2 afu pt-4">
+                          <dt class="text-lg text-gray-900 font-medium">
+                            <i18n.Translate>Cashout after fee</i18n.Translate>
+                          </dt>
+                          <dd class="text-lg text-gray-900 font-medium">
+                            <RenderAmount
+                              value={cashoutCalc.credit}
+                              withColor
                               spec={info.regional_currency_specification}
                             />
                           </dd>
                         </div>
-                      )}
-                      <div class="flex justify-between items-center border-t-2 
afu pt-4">
-                        <dt class="text-lg text-gray-900 font-medium">
-                          <i18n.Translate>Cashout after fee</i18n.Translate>
-                        </dt>
-                        <dd class="text-lg text-gray-900 font-medium">
-                          <RenderAmount
-                            value={cashoutCalc.credit}
-                            withColor
-                            spec={info.regional_currency_specification}
-                          />
-                        </dd>
+                      </dl>
+                    </div>
+
+                    {cashoutCalc &&
+                    status.status === "ok" &&
+                    Amounts.cmp(status.result.amount, cashoutCalc.credit) <
+                      0 ? (
+                      <div class="p-4">
+                        <Attention
+                          title={i18n.str`Bad configuration`}
+                          type="warning"
+                        >
+                          <i18n.Translate>
+                            This configuration allows users to cash out more of
+                            what has been cashed in.
+                          </i18n.Translate>
+                        </Attention>
                       </div>
-                    </dl>
+                    ) : undefined}
                   </div>
-
-                  {cashoutCalc && status.status === "ok" && 
Amounts.cmp(status.result.amount, cashoutCalc.credit) < 0 ? <div class="p-4">
-                    <Attention title={i18n.str`Bad configuration`} 
type="warning">
-                      <i18n.Translate>
-                        This configuration allows users to cash out more of 
what has been cashed in.
-                      </i18n.Translate>
-                    </Attention>
-                  </div> : undefined}
-                </div>
-              )}
-            </Fragment>}
-
+                )}
+              </Fragment>
+            )}
 
             <div class="flex items-center justify-between mt-4 gap-x-6 
border-t border-gray-900/10 px-4 py-4">
-              <a name="cancel"
+              <a
+                name="cancel"
                 href={routeCancel.url({})}
                 class="text-sm font-semibold leading-6 text-gray-900"
               >
                 <i18n.Translate>Cancel</i18n.Translate>
               </a>
-              {section == "cashin" || section == "cashout" ? <Fragment>
-                <button
-                  type="submit"
-                  name="update conversion"
-                  class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer 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"
-                  onClick={async () => {
-                    doUpdate()
-                  }}
-                >
-                  <i18n.Translate>Update</i18n.Translate>
-                </button>
-              </Fragment> : <div />}
+              {section == "cashin" || section == "cashout" ? (
+                <Fragment>
+                  <button
+                    type="submit"
+                    name="update conversion"
+                    class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer 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"
+                    onClick={async () => {
+                      doUpdate();
+                    }}
+                  >
+                    <i18n.Translate>Update</i18n.Translate>
+                  </button>
+                </Fragment>
+              ) : (
+                <div />
+              )}
             </div>
-
-
           </form>
         </div>
       </div>
     );
-
-  }
+  };
 }
 
 export const ConversionConfig = utils.recursive(useComponentState);
 
 /**
- * 
- * @param i18n 
- * @param regional 
- * @param fiat 
+ *
+ * @param i18n
+ * @param regional
+ * @param fiat
  * @returns form validator
  */
-function createFormValidator(i18n: InternationalizationAPI, regional: string, 
fiat: string) {
+function createFormValidator(
+  i18n: InternationalizationAPI,
+  regional: string,
+  fiat: string,
+) {
   return function check(state: FormValues<FormType>): FormStatus<FormType> {
+    const cashin_min_amount = Amounts.parse(
+      `${fiat}:${state.conv.cashin_min_amount}`,
+    );
+    const cashin_tiny_amount = Amounts.parse(
+      `${regional}:${state.conv.cashin_tiny_amount}`,
+    );
+    const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`);
 
-    const cashin_min_amount = 
Amounts.parse(`${fiat}:${state.conv.cashin_min_amount}`)
-    const cashin_tiny_amount = 
Amounts.parse(`${regional}:${state.conv.cashin_tiny_amount}`)
-    const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`)
-
-    const cashout_min_amount = 
Amounts.parse(`${regional}:${state.conv.cashout_min_amount}`)
-    const cashout_tiny_amount = 
Amounts.parse(`${fiat}:${state.conv.cashout_tiny_amount}`)
-    const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`)
+    const cashout_min_amount = Amounts.parse(
+      `${regional}:${state.conv.cashout_min_amount}`,
+    );
+    const cashout_tiny_amount = Amounts.parse(
+      `${fiat}:${state.conv.cashout_tiny_amount}`,
+    );
+    const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`);
 
-    const am = Amounts.parse(`${fiat}:${state.amount}`)
+    const am = Amounts.parse(`${fiat}:${state.amount}`);
 
-    const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "")
-    const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "")
+    const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "");
+    const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "");
 
     const errors = undefinedIfEmpty<FormErrors<FormType>>({
       conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({
-        cashin_min_amount: !state.conv.cashin_min_amount ? i18n.str`required` :
-          !cashin_min_amount ? i18n.str`invalid` :
-            undefined,
-        cashin_tiny_amount: !state.conv.cashin_tiny_amount ? 
i18n.str`required` :
-          !cashin_tiny_amount ? i18n.str`invalid` :
-            undefined,
-        cashin_fee: !state.conv.cashin_fee ? i18n.str`required` :
-          !cashin_fee ? i18n.str`invalid` :
-            undefined,
-
-        cashout_min_amount: !state.conv.cashout_min_amount ? 
i18n.str`required` :
-          !cashout_min_amount ? i18n.str`invalid` :
-            undefined,
-        cashout_tiny_amount: !state.conv.cashin_tiny_amount ? 
i18n.str`required` :
-          !cashout_tiny_amount ? i18n.str`invalid` :
-            undefined,
-        cashout_fee: !state.conv.cashin_fee ? i18n.str`required` :
-          !cashout_fee ? i18n.str`invalid` :
-            undefined,
-
-        cashin_rounding_mode: !state.conv.cashin_rounding_mode ? 
i18n.str`required` : undefined,
-        cashout_rounding_mode: !state.conv.cashout_rounding_mode ? 
i18n.str`required` : undefined,
-
-        cashin_ratio: !state.conv.cashin_ratio ? i18n.str`required` : 
Number.isNaN(cashin_ratio) ? i18n.str`invalid` : undefined,
-        cashout_ratio: !state.conv.cashout_ratio ? i18n.str`required` : 
Number.isNaN(cashout_ratio) ? i18n.str`invalid` : undefined,
+        cashin_min_amount: !state.conv.cashin_min_amount
+          ? i18n.str`required`
+          : !cashin_min_amount
+            ? i18n.str`invalid`
+            : undefined,
+        cashin_tiny_amount: !state.conv.cashin_tiny_amount
+          ? i18n.str`required`
+          : !cashin_tiny_amount
+            ? i18n.str`invalid`
+            : undefined,
+        cashin_fee: !state.conv.cashin_fee
+          ? i18n.str`required`
+          : !cashin_fee
+            ? i18n.str`invalid`
+            : undefined,
+
+        cashout_min_amount: !state.conv.cashout_min_amount
+          ? i18n.str`required`
+          : !cashout_min_amount
+            ? i18n.str`invalid`
+            : undefined,
+        cashout_tiny_amount: !state.conv.cashin_tiny_amount
+          ? i18n.str`required`
+          : !cashout_tiny_amount
+            ? i18n.str`invalid`
+            : undefined,
+        cashout_fee: !state.conv.cashin_fee
+          ? i18n.str`required`
+          : !cashout_fee
+            ? i18n.str`invalid`
+            : undefined,
+
+        cashin_rounding_mode: !state.conv.cashin_rounding_mode
+          ? i18n.str`required`
+          : undefined,
+        cashout_rounding_mode: !state.conv.cashout_rounding_mode
+          ? i18n.str`required`
+          : undefined,
+
+        cashin_ratio: !state.conv.cashin_ratio
+          ? i18n.str`required`
+          : Number.isNaN(cashin_ratio)
+            ? i18n.str`invalid`
+            : undefined,
+        cashout_ratio: !state.conv.cashout_ratio
+          ? i18n.str`required`
+          : Number.isNaN(cashout_ratio)
+            ? i18n.str`invalid`
+            : undefined,
       }),
 
-      amount: !state.amount ? i18n.str`required` :
-        !am ? i18n.str`invalid` :
-          undefined,
-    })
+      amount: !state.amount
+        ? i18n.str`required`
+        : !am
+          ? i18n.str`invalid`
+          : undefined,
+    });
 
     const result: RecursivePartial<FormType> = {
       amount: am,
       conv: {
-        cashin_fee: !errors?.conv?.cashin_fee ? Amounts.stringify(cashin_fee!) 
: undefined,
-        cashin_min_amount: !errors?.conv?.cashin_min_amount ? 
Amounts.stringify(cashin_min_amount!) : undefined,
-        cashin_ratio: !errors?.conv?.cashin_ratio ? String(cashin_ratio!) : 
undefined,
-        cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode ? 
(state.conv.cashin_rounding_mode!) : undefined,
-        cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount ? 
Amounts.stringify(cashin_tiny_amount!) : undefined,
-        cashout_fee: !errors?.conv?.cashout_fee ? 
Amounts.stringify(cashout_fee!) : undefined,
-        cashout_min_amount: !errors?.conv?.cashout_min_amount ? 
Amounts.stringify(cashout_min_amount!) : undefined,
-        cashout_ratio: !errors?.conv?.cashout_ratio ? String(cashout_ratio!) : 
undefined,
-        cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode ? 
(state.conv.cashout_rounding_mode!) : undefined,
-        cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount ? 
Amounts.stringify(cashout_tiny_amount!) : undefined,
-      }
-
-    }
-    return errors === undefined ?
-      { status: "ok", result: result as FormType, errors } :
-      { status: "fail", result, errors }
-  }
+        cashin_fee: !errors?.conv?.cashin_fee
+          ? Amounts.stringify(cashin_fee!)
+          : undefined,
+        cashin_min_amount: !errors?.conv?.cashin_min_amount
+          ? Amounts.stringify(cashin_min_amount!)
+          : undefined,
+        cashin_ratio: !errors?.conv?.cashin_ratio
+          ? String(cashin_ratio!)
+          : undefined,
+        cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode
+          ? state.conv.cashin_rounding_mode!
+          : undefined,
+        cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount
+          ? Amounts.stringify(cashin_tiny_amount!)
+          : undefined,
+        cashout_fee: !errors?.conv?.cashout_fee
+          ? Amounts.stringify(cashout_fee!)
+          : undefined,
+        cashout_min_amount: !errors?.conv?.cashout_min_amount
+          ? Amounts.stringify(cashout_min_amount!)
+          : undefined,
+        cashout_ratio: !errors?.conv?.cashout_ratio
+          ? String(cashout_ratio!)
+          : undefined,
+        cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode
+          ? state.conv.cashout_rounding_mode!
+          : undefined,
+        cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount
+          ? Amounts.stringify(cashout_tiny_amount!)
+          : undefined,
+      },
+    };
+    return errors === undefined
+      ? { status: "ok", result: result as FormType, errors }
+      : { status: "fail", result, errors };
+  };
 }
 
-
-function ConversionForm({ id, inputCurrency, outputCurrency, fee, minimum, 
ratio, rounding, tiny }: {
-  inputCurrency: string,
-  outputCurrency: string,
-  minimum: UIField | undefined,
-  tiny: UIField | undefined,
-  fee: UIField | undefined,
-  rounding: UIField | undefined,
-  ratio: UIField | undefined,
-  id: string,
+function ConversionForm({
+  id,
+  inputCurrency,
+  outputCurrency,
+  fee,
+  minimum,
+  ratio,
+  rounding,
+  tiny,
+}: {
+  inputCurrency: string;
+  outputCurrency: string;
+  minimum: UIField | undefined;
+  tiny: UIField | undefined;
+  fee: UIField | undefined;
+  rounding: UIField | undefined;
+  ratio: UIField | undefined;
+  id: string;
 }): VNode {
   const { i18n } = useTranslationContext();
-  return <Fragment>
-    <div class="px-6 pt-6">
-      <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
-        <div class="sm:col-span-5">
-          <label
-            for="cashin_min_amount"
-            class="block text-sm font-medium leading-6 text-gray-900"
-          >{i18n.str`Minimum amount`}</label>
-          <InputAmount
-            name="cashin_min_amount"
-            left
-            currency={inputCurrency}
-            value={minimum?.value ?? ""}
-            onChange={minimum?.onUpdate}
+  return (
+    <Fragment>
+      <div class="px-6 pt-6">
+        <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+          <div class="sm:col-span-5">
+            <label
+              for={`${id}_min_amount`}
+              class="block text-sm font-medium leading-6 text-gray-900"
+            >{i18n.str`Minimum amount`}</label>
+            <InputAmount
+              name={`${id}_min_amount`}
+              left
+              currency={inputCurrency}
+              value={minimum?.value ?? ""}
+              onChange={minimum?.onUpdate}
+            />
+            <ShowInputErrorLabel
+              message={minimum?.error}
+              isDirty={minimum?.value !== undefined}
+            />
+            <p class="mt-2 text-sm text-gray-500">
+              <i18n.Translate>
+                Only cashout operation above this threshold will be allowed
+              </i18n.Translate>
+            </p>
+          </div>
+        </div>
+      </div>
+
+      <div class="px-6 pt-6">
+        <label
+          class="block text-sm font-medium leading-6 text-gray-900"
+          for={`${id}_ratio`}
+        >
+          {i18n.str`Ratio`}
+        </label>
+        <div class="mt-2">
+          <input
+            type="number"
+            class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm 
ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
+            name="current"
+            id={`${id}_ratio`}
+            data-error={!!ratio?.error && ratio?.value !== undefined}
+            value={ratio?.value ?? ""}
+            onChange={(e) => {
+              ratio?.onUpdate(e.currentTarget.value);
+            }}
+            autocomplete="off"
           />
           <ShowInputErrorLabel
-            message={minimum?.error}
-            isDirty={minimum?.value !== undefined}
+            message={ratio?.error}
+            isDirty={ratio?.value !== undefined}
           />
-          <p class="mt-2 text-sm text-gray-500">
-            <i18n.Translate>Only cashout operation above this threshold will 
be allowed</i18n.Translate>
-          </p>
         </div>
+        <p class="mt-2 text-sm text-gray-500">
+          <i18n.Translate>Conversion ratio between currencies</i18n.Translate>
+        </p>
       </div>
-    </div>
-
-    <div class="px-6 pt-6">
-      <label
-        class="block text-sm font-medium leading-6 text-gray-900"
-        for="password"
-      >
-        {i18n.str`Ratio`}
-      </label>
-      <div class="mt-2">
-        <input
-          type="number"
-          class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm 
ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
-          name="current"
-          id="cashin_ratio"
-          data-error={!!ratio?.error && ratio?.value !== undefined}
-          value={ratio?.value ?? ""}
-          onChange={(e) => {
-            ratio?.onUpdate(e.currentTarget.value);
-          }}
-          autocomplete="off"
-        />
-        <ShowInputErrorLabel
-          message={ratio?.error}
-          isDirty={ratio?.value !== undefined}
-        />
+
+      <div class="px-6 pt-4">
+        <Attention title={i18n.str`Example conversion`}>
+          <i18n.Translate>
+            1 {inputCurrency} will be converted into {ratio?.value}{" "}
+            {outputCurrency}
+          </i18n.Translate>
+        </Attention>
       </div>
-      <p class="mt-2 text-sm text-gray-500">
-        <i18n.Translate>
-          Conversion ratio between currencies
-        </i18n.Translate>
-      </p>
-    </div>
-
-    <div class="px-6 pt-4">
-      <Attention title={i18n.str`Example conversion`}>
-        <i18n.Translate>1 {inputCurrency} will be converted into 
{ratio?.value} {outputCurrency}</i18n.Translate>
-      </Attention>
-    </div>
-
-    <div class="px-6 pt-6">
-      <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
-        <div class="sm:col-span-5">
-          <label
-            for="cashin_tiny_amount"
-            class="block text-sm font-medium leading-6 text-gray-900"
-          >{i18n.str`Rounding value`}</label>
-          <InputAmount
-            name="cashin_tiny_amount"
-            left
-            currency={outputCurrency}
-            value={tiny?.value ?? ""}
-            onChange={tiny?.onUpdate}
-          />
-          <ShowInputErrorLabel
-            message={tiny?.error}
-            isDirty={tiny?.value !== undefined}
-          />
-          <p class="mt-2 text-sm text-gray-500">
-            <i18n.Translate>Smallest difference between two amounts after the 
ratio is applied.</i18n.Translate>
-          </p>
+
+      <div class="px-6 pt-6">
+        <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+          <div class="sm:col-span-5">
+            <label
+              for={`${id}_tiny_amount`}
+              class="block text-sm font-medium leading-6 text-gray-900"
+            >{i18n.str`Rounding value`}</label>
+            <InputAmount
+              name={`${id}_tiny_amount`}
+              left
+              currency={outputCurrency}
+              value={tiny?.value ?? ""}
+              onChange={tiny?.onUpdate}
+            />
+            <ShowInputErrorLabel
+              message={tiny?.error}
+              isDirty={tiny?.value !== undefined}
+            />
+            <p class="mt-2 text-sm text-gray-500">
+              <i18n.Translate>
+                Smallest difference between two amounts after the ratio is
+                applied.
+              </i18n.Translate>
+            </p>
+          </div>
         </div>
       </div>
-    </div>
-
-    <div class="px-6 pt-6">
-      <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
-        <div class="sm:col-span-5">
-          <label
-            class="block text-sm font-medium leading-6 text-gray-900"
-            for="channel"
-          >
-            {i18n.str`Rounding mode`}
-          </label>
-          <div class="mt-2 max-w-xl text-sm text-gray-500">
-            <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
-              <label
-                onClick={(e) => {
-                  e.preventDefault();
-                  rounding?.onUpdate("zero")
-                }}
-                data-selected={rounding?.value === "zero"}
-                class="relative flex data-[disabled=false]:cursor-pointer 
rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600"
-              >
-                <input
-                  type="radio"
-                  name="channel"
-                  value="Newsletter"
-                  class="sr-only"
-                />
-                <span class="flex flex-1">
-                  <span class="flex flex-col">
-                    <span
-                      id="project-type-0-label"
-                      class="block text-sm font-medium text-gray-900 "
-                    >
-                      <i18n.Translate>Zero</i18n.Translate>
+
+      <div class="px-6 pt-6">
+        <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+          <div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for={`${id}_channel`}
+            >
+              {i18n.str`Rounding mode`}
+            </label>
+            <div class="mt-2 max-w-xl text-sm text-gray-500">
+              <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+                <label
+                  onClick={(e) => {
+                    e.preventDefault();
+                    rounding?.onUpdate("zero");
+                  }}
+                  data-selected={rounding?.value === "zero"}
+                  class="relative flex data-[disabled=false]:cursor-pointer 
rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600"
+                >
+                  <input
+                    type="radio"
+                    name="channel"
+                    value="Newsletter"
+                    class="sr-only"
+                  />
+                  <span class="flex flex-1">
+                    <span class="flex flex-col">
+                      <span class="block text-sm font-medium text-gray-900 ">
+                        <i18n.Translate>Zero</i18n.Translate>
+                      </span>
+                      <i18n.Translate>
+                        Amount will be round below to the largest possible 
value
+                        smaller than the input.
+                      </i18n.Translate>
                     </span>
-                    <i18n.Translate>Amount will be round below to the largest 
possible value smaller than the input.</i18n.Translate>
                   </span>
-                </span>
+                  <svg
+                    data-selected={rounding?.value === "zero"}
+                    class="h-5 w-5 text-indigo-600 
data-[selected=false]:hidden"
+                    viewBox="0 0 20 20"
+                    fill="currentColor"
+                    aria-hidden="true"
+                  >
+                    <path
+                      fill-rule="evenodd"
+                      d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 
0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 
001.137-.089l4-5.5z"
+                      clip-rule="evenodd"
+                    />
+                  </svg>
+                </label>
+
+                <label
+                  onClick={(e) => {
+                    e.preventDefault();
+                    rounding?.onUpdate("up");
+                  }}
+                  data-selected={rounding?.value === "up"}
+                  class="relative flex data-[disabled=false]:cursor-pointer 
rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600"
+                >
+                  <input
+                    type="radio"
+                    name="channel"
+                    value="Existing Customers"
+                    class="sr-only"
+                  />
+                  <span class="flex flex-1">
+                    <span class="flex flex-col">
+                      <span class="block text-sm font-medium text-gray-900 ">
+                        <i18n.Translate>Up</i18n.Translate>
+                      </span>
+                      <i18n.Translate>
+                        Amount will be round up to the smallest possible value
+                        larger than the input.
+                      </i18n.Translate>
+                    </span>
+                  </span>
+                  <svg
+                    data-selected={rounding?.value === "up"}
+                    class="h-5 w-5 text-indigo-600 
data-[selected=false]:hidden"
+                    viewBox="0 0 20 20"
+                    fill="currentColor"
+                    aria-hidden="true"
+                  >
+                    <path
+                      fill-rule="evenodd"
+                      d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 
0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 
001.137-.089l4-5.5z"
+                      clip-rule="evenodd"
+                    />
+                  </svg>
+                </label>
+                <label
+                  onClick={(e) => {
+                    e.preventDefault();
+                    rounding?.onUpdate("nearest");
+                  }}
+                  data-selected={rounding?.value === "nearest"}
+                  class="relative flex data-[disabled=false]:cursor-pointer 
rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600"
+                >
+                  <input
+                    type="radio"
+                    name="channel"
+                    value="Existing Customers"
+                    class="sr-only"
+                  />
+                  <span class="flex flex-1">
+                    <span class="flex flex-col">
+                      <span class="block text-sm font-medium text-gray-900 ">
+                        <i18n.Translate>Nearest</i18n.Translate>
+                      </span>
+                      <i18n.Translate>
+                        Amount will be round to the closest possible value.
+                      </i18n.Translate>
+                    </span>
+                  </span>
+                  <svg
+                    data-selected={rounding?.value === "nearest"}
+                    class="h-5 w-5 text-indigo-600 
data-[selected=false]:hidden"
+                    viewBox="0 0 20 20"
+                    fill="currentColor"
+                    aria-hidden="true"
+                  >
+                    <path
+                      fill-rule="evenodd"
+                      d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 
0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 
001.137-.089l4-5.5z"
+                      clip-rule="evenodd"
+                    />
+                  </svg>
+                </label>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="px-6 pt-4">
+        <Attention title={i18n.str`Examples`}>
+          <section class="grid grid-cols-1 gap-y-3  text-gray-600">
+            <details class="group  text-sm">
+              <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
+                <i18n.Translate>
+                  Rounding an amount of 1.24 with rounding value 0.1
+                </i18n.Translate>
                 <svg
-                  data-selected={rounding?.value === "zero"}
-                  class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
-                  viewBox="0 0 20 20"
-                  fill="currentColor"
+                  class="h-6 w-6 rotate-0 transform  group-open:rotate-180"
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="2"
+                  stroke="currentColor"
                   aria-hidden="true"
                 >
                   <path
-                    fill-rule="evenodd"
-                    d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 
00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 
001.137-.089l4-5.5z"
-                    clip-rule="evenodd"
-                  />
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M19 9l-7 7-7-7"
+                  ></path>
                 </svg>
-              </label>
-
-              <label
-                onClick={(e) => {
-                  e.preventDefault();
-                  rounding?.onUpdate("up")
-                }}
-                data-selected={rounding?.value === "up"}
-                class="relative flex data-[disabled=false]:cursor-pointer 
rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600"
-              >
-                <input
-                  type="radio"
-                  name="channel"
-                  value="Existing Customers"
-                  class="sr-only"
-                />
-                <span class="flex flex-1">
-                  <span class="flex flex-col">
-                    <span
-                      id="project-type-0-label"
-                      class="block text-sm font-medium text-gray-900 "
-                    >
-                      <i18n.Translate>Up</i18n.Translate>
-                    </span>
-                    <i18n.Translate>Amount will be round up to the smallest 
possible value larger than the input.</i18n.Translate>
-                  </span>
-                </span>
+              </summary>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  Given the rounding value of 0.1 the possible values closest 
to
+                  1.24 are: 1.1, 1.2, 1.3, 1.4.
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "zero" mode the value will be rounded to 1.2
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "nearest" mode the value will be rounded to 1.2
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 mt-4">
+                <i18n.Translate>
+                  With the "up" mode the value will be rounded to 1.3
+                </i18n.Translate>
+              </p>
+            </details>
+            <details class="group ">
+              <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
+                <i18n.Translate>
+                  Rounding an amount of 1.26 with rounding value 0.1
+                </i18n.Translate>
                 <svg
-                  data-selected={rounding?.value === "up"}
-                  class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
-                  viewBox="0 0 20 20"
-                  fill="currentColor"
+                  class="h-6 w-6 rotate-0 transform  group-open:rotate-180"
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="2"
+                  stroke="currentColor"
                   aria-hidden="true"
                 >
                   <path
-                    fill-rule="evenodd"
-                    d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 
00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 
001.137-.089l4-5.5z"
-                    clip-rule="evenodd"
-                  />
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M19 9l-7 7-7-7"
+                  ></path>
                 </svg>
-              </label>
-              <label
-                onClick={(e) => {
-                  e.preventDefault();
-                  rounding?.onUpdate("nearest")
-                }}
-                data-selected={rounding?.value === "nearest"}
-                class="relative flex data-[disabled=false]:cursor-pointer 
rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600"
-              >
-                <input
-                  type="radio"
-                  name="channel"
-                  value="Existing Customers"
-                  class="sr-only"
-                />
-                <span class="flex flex-1">
-                  <span class="flex flex-col">
-                    <span
-                      id="project-type-0-label"
-                      class="block text-sm font-medium text-gray-900 "
-                    >
-                      <i18n.Translate>Nearest</i18n.Translate>
-                    </span>
-                    <i18n.Translate>Amount will be round to the closest 
possible value.</i18n.Translate>
-                  </span>
-                </span>
+              </summary>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  Given the rounding value of 0.1 the possible values closest 
to
+                  1.24 are: 1.1, 1.2, 1.3, 1.4.
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "zero" mode the value will be rounded to 1.2
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "nearest" mode the value will be rounded to 1.3
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "up" mode the value will be rounded to 1.3
+                </i18n.Translate>
+              </p>
+            </details>
+            <details class="group ">
+              <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
+                <i18n.Translate>
+                  Rounding an amount of 1.24 with rounding value 0.3
+                </i18n.Translate>
                 <svg
-                  data-selected={rounding?.value === "nearest"}
-                  class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
-                  viewBox="0 0 20 20"
-                  fill="currentColor"
+                  class="h-6 w-6 rotate-0 transform  group-open:rotate-180"
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="2"
+                  stroke="currentColor"
                   aria-hidden="true"
                 >
                   <path
-                    fill-rule="evenodd"
-                    d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 
00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 
001.137-.089l4-5.5z"
-                    clip-rule="evenodd"
-                  />
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M19 9l-7 7-7-7"
+                  ></path>
                 </svg>
-              </label>
-            </div>
-          </div>
-        </div>
+              </summary>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  Given the rounding value of 0.3 the possible values closest 
to
+                  1.24 are: 0.9, 1.2, 1.5, 1.8.
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "zero" mode the value will be rounded to 1.2
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "nearest" mode the value will be rounded to 1.2
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "up" mode the value will be rounded to 1.5
+                </i18n.Translate>
+              </p>
+            </details>
+            <details class="group ">
+              <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
+                <i18n.Translate>
+                  Rounding an amount of 1.26 with rounding value 0.3
+                </i18n.Translate>
+                <svg
+                  class="h-6 w-6 rotate-0 transform  group-open:rotate-180"
+                  xmlns="http://www.w3.org/2000/svg";
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke-width="2"
+                  stroke="currentColor"
+                  aria-hidden="true"
+                >
+                  <path
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                    d="M19 9l-7 7-7-7"
+                  ></path>
+                </svg>
+              </summary>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  Given the rounding value of 0.3 the possible values closest 
to
+                  1.24 are: 0.9, 1.2, 1.5, 1.8.
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "zero" mode the value will be rounded to 1.2
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "nearest" mode the value will be rounded to 1.3
+                </i18n.Translate>
+              </p>
+              <p class="text-gray-900 my-4">
+                <i18n.Translate>
+                  With the "up" mode the value will be rounded to 1.3
+                </i18n.Translate>
+              </p>
+            </details>
+          </section>
+        </Attention>
       </div>
-    </div>
 
-    <div class="px-6 pt-4">
-      <Attention title={i18n.str`Examples`}>
-        <section class="grid grid-cols-1 gap-y-3  text-gray-600">
-          <details class="group  text-sm">
-            <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
+      <div class="px-6 pt-6">
+        <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+          <div class="sm:col-span-5">
+            <label
+              for={`${id}_fee`}
+              class="block text-sm font-medium leading-6 text-gray-900"
+            >{i18n.str`Fee`}</label>
+            <InputAmount
+              name={`${id}_fee`}
+              left
+              currency={outputCurrency}
+              value={fee?.value ?? ""}
+              onChange={fee?.onUpdate}
+            />
+            <ShowInputErrorLabel
+              message={fee?.error}
+              isDirty={fee?.value !== undefined}
+            />
+            <p class="mt-2 text-sm text-gray-500">
               <i18n.Translate>
-                Rounding an amount of 1.24 with rounding value 0.1
-              </i18n.Translate>
-              <svg class="h-6 w-6 rotate-0 transform  group-open:rotate-180" 
xmlns="http://www.w3.org/2000/svg"; fill="none" viewBox="0 0 24 24" 
stroke-width="2" stroke="currentColor" aria-hidden="true">
-                <path stroke-linecap="round" stroke-linejoin="round" d="M19 
9l-7 7-7-7"></path>
-              </svg>
-            </summary>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                Given the rounding value of 0.1 the possible values closest to 
1.24 are: 1.1, 1.2, 1.3, 1.4.
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "zero" mode the value will be rounded to 1.2
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "nearest" mode the value will be rounded to 1.2
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 mt-4">
-              <i18n.Translate>
-                With the "up" mode the value will be rounded to 1.3
-              </i18n.Translate>
-            </p>
-          </details>
-          <details class="group ">
-            <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
-              <i18n.Translate>
-                Rounding an amount of 1.26 with rounding value 0.1
-              </i18n.Translate>
-              <svg class="h-6 w-6 rotate-0 transform  group-open:rotate-180" 
xmlns="http://www.w3.org/2000/svg"; fill="none" viewBox="0 0 24 24" 
stroke-width="2" stroke="currentColor" aria-hidden="true">
-                <path stroke-linecap="round" stroke-linejoin="round" d="M19 
9l-7 7-7-7"></path>
-              </svg>
-            </summary>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                Given the rounding value of 0.1 the possible values closest to 
1.24 are: 1.1, 1.2, 1.3, 1.4.
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "zero" mode the value will be rounded to 1.2
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "nearest" mode the value will be rounded to 1.3
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "up" mode the value will be rounded to 1.3
-              </i18n.Translate>
-            </p>
-          </details>
-          <details class="group ">
-            <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
-              <i18n.Translate>
-                Rounding an amount of 1.24 with rounding value 0.3
-              </i18n.Translate>
-              <svg class="h-6 w-6 rotate-0 transform  group-open:rotate-180" 
xmlns="http://www.w3.org/2000/svg"; fill="none" viewBox="0 0 24 24" 
stroke-width="2" stroke="currentColor" aria-hidden="true">
-                <path stroke-linecap="round" stroke-linejoin="round" d="M19 
9l-7 7-7-7"></path>
-              </svg>
-            </summary>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                Given the rounding value of 0.3 the possible values closest to 
1.24 are: 0.9, 1.2, 1.5, 1.8.
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "zero" mode the value will be rounded to 1.2
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "nearest" mode the value will be rounded to 1.2
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "up" mode the value will be rounded to 1.5
-              </i18n.Translate>
-            </p>
-          </details>
-          <details class="group ">
-            <summary class="flex cursor-pointer flex-row items-center 
justify-between  ">
-              <i18n.Translate>
-                Rounding an amount of 1.26 with rounding value 0.3
-              </i18n.Translate>
-              <svg class="h-6 w-6 rotate-0 transform  group-open:rotate-180" 
xmlns="http://www.w3.org/2000/svg"; fill="none" viewBox="0 0 24 24" 
stroke-width="2" stroke="currentColor" aria-hidden="true">
-                <path stroke-linecap="round" stroke-linejoin="round" d="M19 
9l-7 7-7-7"></path>
-              </svg>
-            </summary>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                Given the rounding value of 0.3 the possible values closest to 
1.24 are: 0.9, 1.2, 1.5, 1.8.
+                Amount to be deducted before amount is credited.
               </i18n.Translate>
             </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "zero" mode the value will be rounded to 1.2
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "nearest" mode the value will be rounded to 1.3
-              </i18n.Translate>
-            </p>
-            <p class="text-gray-900 my-4">
-              <i18n.Translate>
-                With the "up" mode the value will be rounded to 1.3
-              </i18n.Translate>
-            </p>
-          </details>
-        </section>
-      </Attention>
-    </div>
-
-
-
-    <div class="px-6 pt-6">
-      <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
-        <div class="sm:col-span-5">
-          <label
-            for="cashin_fee"
-            class="block text-sm font-medium leading-6 text-gray-900"
-          >{i18n.str`Fee`}</label>
-          <InputAmount
-            name="cashin_fee"
-            left
-            currency={outputCurrency}
-            value={fee?.value ?? ""}
-            onChange={fee?.onUpdate}
-          />
-          <ShowInputErrorLabel
-            message={fee?.error}
-            isDirty={fee?.value !== undefined}
-          />
-          <p class="mt-2 text-sm text-gray-500">
-            <i18n.Translate>Amount to be deducted before amount is 
credited.</i18n.Translate>
-          </p>
+          </div>
         </div>
       </div>
-    </div>
-
-  </Fragment>
+    </Fragment>
+  );
 }
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx 
b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
index 2f15d16b4..a76179b4d 100644
--- a/packages/bank-ui/src/pages/regional/CreateCashout.tsx
+++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -39,9 +39,13 @@ import { useEffect, useState } from "preact/hooks";
 import { ErrorLoadingWithDebug } from 
"../../components/ErrorLoadingWithDebug.js";
 import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
 import { useAccountDetails } from "../../hooks/account.js";
-import { useSessionState } from "../../hooks/session.js";
 import { useBankState } from "../../hooks/bank-state.js";
-import { TransferCalculation, useCashoutEstimator, useConversionInfo, 
useEstimator } from "../../hooks/regional.js";
+import {
+  TransferCalculation,
+  useCashoutEstimator,
+  useConversionInfo,
+} from "../../hooks/regional.js";
+import { useSessionState } from "../../hooks/session.js";
 import { RouteDefinition } from "../../route.js";
 import { TanChannel, undefinedIfEmpty } from "../../utils.js";
 import { LoginForm } from "../LoginForm.js";
@@ -141,11 +145,11 @@ export function CreateCashout({
     switch (info.case) {
       case HttpStatusCode.NotImplemented: {
         return (
-          <Attention
-            type="danger"
-            title={i18n.str`Cashout are disabled`}
-          >
-            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+            <i18n.Translate>
+              Cashout should be enable by configuration and the conversion rate
+              should be initialized with fee, ratio and rounding mode.
+            </i18n.Translate>
           </Attention>
         );
       }
@@ -185,7 +189,8 @@ export function CreateCashout({
     credit: fiatZero,
     beforeFee: fiatZero,
   };
-  const [calculationResult, setCalculation] = 
useState<TransferCalculation>(zeroCalc);
+  const [calculationResult, setCalculation] =
+    useState<TransferCalculation>(zeroCalc);
   const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
   const sellRate = conversionInfo.cashout_ratio;
   /**
@@ -193,30 +198,33 @@ export function CreateCashout({
    * depending on the isDebit flag
    */
   const inputAmount = Amounts.parseOrThrow(
-    `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" 
: form.amount
+    `${form.isDebit ? regional_currency : fiat_currency}:${
+      !form.amount ? "0" : form.amount
     }`,
   );
 
   useEffect(() => {
     async function doAsync() {
       await handleError(async () => {
-        const higerThanMin = form.isDebit ?
-          Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1 : 
true;
-        const notZero = Amounts.isNonZero(inputAmount)
+        const higerThanMin = form.isDebit
+          ? Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1
+          : true;
+        const notZero = Amounts.isNonZero(inputAmount);
         if (notZero && higerThanMin) {
           const resp = await (form.isDebit
             ? calculateFromDebit(inputAmount, sellFee)
             : calculateFromCredit(inputAmount, sellFee));
           setCalculation(resp);
         } else {
-          setCalculation(zeroCalc)
+          setCalculation(zeroCalc);
         }
       });
     }
     doAsync();
   }, [form.amount, form.isDebit]);
 
-  const calc = calculationResult === "amount-is-too-small" ? zeroCalc : 
calculationResult
+  const calc =
+    calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult;
 
   const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
 
@@ -231,8 +239,14 @@ export function CreateCashout({
         ? i18n.str`Invalid`
         : Amounts.cmp(limit, calc.debit) === -1
           ? i18n.str`Balance is not enough`
-          : form.isDebit && Amounts.cmp(inputAmount, 
conversionInfo.cashout_min_amount) < 1
-            ? i18n.str`Needs to be higher than 
${Amounts.stringifyValueWithSpec(Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
 regional_currency_specification).normal}`
+          : form.isDebit &&
+              Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1
+            ? i18n.str`Needs to be higher than ${
+                Amounts.stringifyValueWithSpec(
+                  Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
+                  regional_currency_specification,
+                ).normal
+              }`
             : calculationResult === "amount-is-too-small"
               ? i18n.str`Amount needs to be higher`
               : Amounts.isZero(calc.credit)
@@ -280,6 +294,7 @@ export function CreateCashout({
               title: i18n.str`Account not found`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
             return notify({
@@ -287,6 +302,7 @@ export function CreateCashout({
               title: i18n.str`Duplicated request detected, check if the 
operation succeeded or try again.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_BAD_CONVERSION:
             return notify({
@@ -294,6 +310,7 @@ export function CreateCashout({
               title: i18n.str`The conversion rate was incorrectly applied`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_UNALLOWED_DEBIT:
             return notify({
@@ -301,6 +318,7 @@ export function CreateCashout({
               title: i18n.str`The account does not have sufficient funds`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case HttpStatusCode.NotImplemented:
             return notify({
@@ -308,6 +326,7 @@ export function CreateCashout({
               title: i18n.str`Cashout are disabled`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
             return notify({
@@ -315,6 +334,7 @@ export function CreateCashout({
               title: i18n.str`Missing cashout URI in the profile`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
           case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
             return notify({
@@ -322,6 +342,7 @@ export function CreateCashout({
               title: i18n.str`Sending the confirmation message failed, retry 
later or contact the administrator.`,
               description: resp.detail.hint as TranslatedString,
               debug: resp.detail,
+              when: AbsoluteTime.now(),
             });
         }
         assertUnreachable(resp);
@@ -406,7 +427,10 @@ export function CreateCashout({
                   <dd class="text-sm text-gray-900">{cashoutLegalName}</dd>
                 </div>
                 <p class="mt-2 text-sm text-gray-500">
-                  <i18n.Translate>If this name doesn't match the account 
holder's name your transaction may fail.</i18n.Translate>
+                  <i18n.Translate>
+                    If this name doesn't match the account holder's name your
+                    transaction may fail.
+                  </i18n.Translate>
                 </p>
               </Fragment>
             ) : (
@@ -482,7 +506,7 @@ export function CreateCashout({
                       updateForm(structuredClone(form));
                     }}
                   >
-                    {form.isDebit ?
+                    {form.isDebit ? (
                       <svg
                         class="self-center flex-none h-5 w-5 text-indigo-600"
                         viewBox="0 0 20 20"
@@ -495,12 +519,17 @@ export function CreateCashout({
                           clip-rule="evenodd"
                         />
                       </svg>
-
-                      :
-                      <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" 
stroke="currentColor" class="w-5 h-5">
+                    ) : (
+                      <svg
+                        fill="none"
+                        viewBox="0 0 24 24"
+                        stroke-width="1.5"
+                        stroke="currentColor"
+                        class="w-5 h-5"
+                      >
                         <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 
/>
                       </svg>
-                    }
+                    )}
 
                     <i18n.Translate>Send {regional_currency}</i18n.Translate>
                   </button>
@@ -514,7 +543,7 @@ export function CreateCashout({
                       updateForm(structuredClone(form));
                     }}
                   >
-                    {!form.isDebit ?
+                    {!form.isDebit ? (
                       <svg
                         class="self-center flex-none h-5 w-5 text-indigo-600"
                         viewBox="0 0 20 20"
@@ -527,12 +556,17 @@ export function CreateCashout({
                           clip-rule="evenodd"
                         />
                       </svg>
-
-                      :
-                      <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" 
stroke="currentColor" class="w-5 h-5">
+                    ) : (
+                      <svg
+                        fill="none"
+                        viewBox="0 0 24 24"
+                        stroke-width="1.5"
+                        stroke="currentColor"
+                        class="w-5 h-5"
+                      >
                         <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 
/>
                       </svg>
-                    }
+                    )}
 
                     <i18n.Translate>Receive {fiat_currency}</i18n.Translate>
                   </button>
@@ -579,9 +613,9 @@ export function CreateCashout({
                       cashoutDisabled
                         ? undefined
                         : (value) => {
-                          form.amount = value;
-                          updateForm(structuredClone(form));
-                        }
+                            form.amount = value;
+                            updateForm(structuredClone(form));
+                          }
                     }
                   />
                   <ShowInputErrorLabel
@@ -622,7 +656,7 @@ export function CreateCashout({
                       </dd>
                     </div>
                     {Amounts.isZero(sellFee) ||
-                      Amounts.isZero(calc.beforeFee) ? undefined : (
+                    Amounts.isZero(calc.beforeFee) ? undefined : (
                       <div class="flex items-center justify-between border-t-2 
afu pt-4">
                         <dt class="flex items-center text-sm text-gray-600">
                           <span>
@@ -655,7 +689,7 @@ export function CreateCashout({
 
               {/* channel, not shown if new cashout api */}
               {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels
-                .length === 0 ? (
+                  .length === 0 ? (
                 <div class="sm:col-span-5">
                   <Attention
                     type="warning"
@@ -727,7 +761,7 @@ export function CreateCashout({
                       )}
 
                       {config.supported_tan_channels.indexOf(TanChannel.SMS) 
===
-                        -1 ? undefined : (
+                      -1 ? undefined : (
                         <label
                           onClick={() => {
                             if (!resultAccount.body.contact_data?.phone) 
return;
@@ -803,7 +837,7 @@ export function CreateCashout({
             </button>
           </div>
         </form>
-      </div >
-    </div >
+      </div>
+    </div>
   );
 }
diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx 
b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
index 415f88868..3f635db7e 100644
--- a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
+++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
@@ -16,7 +16,6 @@
 import {
   AbsoluteTime,
   Amounts,
-  Duration,
   HttpStatusCode,
   TalerError,
   assertUnreachable,
@@ -26,20 +25,19 @@ import {
   Loading,
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
 import { VNode, h } from "preact";
 import { ErrorLoadingWithDebug } from 
"../../components/ErrorLoadingWithDebug.js";
+import { Time } from "../../components/Time.js";
 import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js";
 import { RouteDefinition } from "../../route.js";
 import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { Time } from "../../components/Time.js";
 
 interface Props {
   id: string;
   routeClose: RouteDefinition;
 }
 export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
-  const { i18n, dateLocale } = useTranslationContext();
+  const { i18n } = useTranslationContext();
   const cid = Number.parseInt(id, 10);
 
   const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
@@ -70,11 +68,11 @@ export function ShowCashoutDetails({ id, routeClose }: 
Props): VNode {
         );
       case HttpStatusCode.NotImplemented:
         return (
-          <Attention
-            type="warning"
-            title={i18n.str`Cashout are disabled`}
-          >
-            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          <Attention type="warning" title={i18n.str`Cashout are disabled`}>
+            <i18n.Translate>
+              Cashout should be enable by configuration and the conversion rate
+              should be initialized with fee, ratio and rounding mode.
+            </i18n.Translate>
           </Attention>
         );
       default:
@@ -92,10 +90,11 @@ export function ShowCashoutDetails({ id, routeClose }: 
Props): VNode {
     switch (info.case) {
       case HttpStatusCode.NotImplemented: {
         return (
-          <Attention type="danger"
-            title={i18n.str`Cashout are disabled`}
-          >
-            <i18n.Translate>Cashout should be enable by configuration and the 
conversion rate should be initialized with fee, ratio and rounding 
mode.</i18n.Translate>
+          <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+            <i18n.Translate>
+              Cashout should be enable by configuration and the conversion rate
+              should be initialized with fee, ratio and rounding mode.
+            </i18n.Translate>
           </Attention>
         );
       }
@@ -134,9 +133,12 @@ export function ShowCashoutDetails({ id, routeClose }: 
Props): VNode {
                         <i18n.Translate>Created</i18n.Translate>
                       </dt>
                       <dd class="text-sm ">
-                        <Time format="dd/MM/yyyy HH:mm:ss"
-                          
timestamp={AbsoluteTime.fromProtocolTimestamp(result.body.creation_time)}
-                        // relative={Duration.fromSpec({ days: 1 })} 
+                        <Time
+                          format="dd/MM/yyyy HH:mm:ss"
+                          timestamp={AbsoluteTime.fromProtocolTimestamp(
+                            result.body.creation_time,
+                          )}
+                          // relative={Duration.fromSpec({ days: 1 })}
                         />
                       </dd>
                     </div>
diff --git a/packages/bank-ui/src/route.ts b/packages/bank-ui/src/route.ts
index 1f85ce54e..11f13d140 100644
--- a/packages/bank-ui/src/route.ts
+++ b/packages/bank-ui/src/route.ts
@@ -18,7 +18,7 @@ import { useNavigationContext } from 
"./context/navigation.js";
 declare const __location: unique symbol;
 /**
  * special string that defined a location in the application
- * 
+ *
  * this help to prevent wrong path
  */
 export type AppLocation = string & {
@@ -29,7 +29,7 @@ export type EmptyObject = Record<string, never>;
 export function urlPattern<
   T extends Record<string, string | undefined> = EmptyObject,
 >(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
-  const url = reverse as ((p: T) => AppLocation)
+  const url = reverse as (p: T) => AppLocation;
   return {
     pattern: new RegExp(pattern),
     url,
@@ -38,14 +38,16 @@ export function urlPattern<
 
 /**
  * defines a location in the app
- * 
+ *
  * pattern: how a string will trigger this location
  * url(): how a state serialize to a location
  */
 
 export type ObjectOf<T> = Record<string, T> | EmptyObject;
 
-export type RouteDefinition<T extends ObjectOf<string | undefined> = 
EmptyObject> = {
+export type RouteDefinition<
+  T extends ObjectOf<string | undefined> = EmptyObject,
+> = {
   pattern: RegExp;
   url: (p: T) => AppLocation;
 };
@@ -54,7 +56,9 @@ const nullRountDef = {
   pattern: new RegExp(/.*/),
   url: () => "" as AppLocation,
 };
-export function buildNullRoutDefinition<T extends ObjectOf<string>>(): 
RouteDefinition<T> {
+export function buildNullRoutDefinition<
+  T extends ObjectOf<string>,
+>(): RouteDefinition<T> {
   return nullRountDef;
 }
 
@@ -76,7 +80,7 @@ function findMatch<T extends ObjectOf<RouteDefinition>>(
     const name = pageList[idx];
     const found = pagesMap[name].pattern.exec(path);
     if (found !== null) {
-      const values = {} as Record<string, any>
+      const values = {} as Record<string, unknown>;
 
       Object.entries(params).forEach(([key, value]) => {
         values[key] = value;
@@ -97,7 +101,7 @@ function findMatch<T extends ObjectOf<RouteDefinition>>(
 
 /**
  * get the type of the params of a location
- * 
+ *
  */
 type RouteParamsType<
   RouteType,
@@ -105,24 +109,29 @@ type RouteParamsType<
 > = RouteType[Key] extends RouteDefinition<infer ParamType> ? ParamType : 
 > never;
 
 /**
- * Helps to create a map of a type with the key 
+ * Helps to create a map of a type with the key
  */
 type MapKeyValue<Type> = {
-  [Key in keyof Type]: Key extends string ? {
-    parent: Type,
-    name: Key,
-    values: RouteParamsType<Type, Key>;
-  } : never;
-}
+  [Key in keyof Type]: Key extends string
+    ? {
+        parent: Type;
+        name: Key;
+        values: RouteParamsType<Type, Key>;
+      }
+    : never;
+};
 
 /**
  * create a enumeration of value of a mapped type
  */
-type EnumerationOf<T> = T[keyof T]
+type EnumerationOf<T> = T[keyof T];
 
-type Location<T> = EnumerationOf<MapKeyValue<T>>
+type Location<T> = EnumerationOf<MapKeyValue<T>>;
 
-export function useCurrentLocation<T extends 
ObjectOf<RouteDefinition<any>>>(pagesMap: T): Location<T> | undefined {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
+  pagesMap: T,
+): Location<T> | undefined {
   const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
   const { path, params } = useNavigationContext();
 
diff --git a/packages/bank-ui/src/stories.test.ts 
b/packages/bank-ui/src/stories.test.ts
index 207945865..8ed00a1e6 100644
--- a/packages/bank-ui/src/stories.test.ts
+++ b/packages/bank-ui/src/stories.test.ts
@@ -19,7 +19,6 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 import {
-  AccessToken,
   AmountString,
   TalerCorebankApi,
   setupI18n,
@@ -51,11 +50,7 @@ describe("All the examples:", () => {
   });
 });
 
-function DefaultTestingContext({
-  children,
-}: {
-  children: ComponentChildren;
-}): VNode {
+function DefaultTestingContext(_props: { children: ComponentChildren }): VNode 
{
   const cfg: TalerCorebankApi.Config = {
     name: "libeufin-bank",
     allow_deletions: true,
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts
index 8b0febe42..305f13803 100644
--- a/packages/bank-ui/src/utils.ts
+++ b/packages/bank-ui/src/utils.ts
@@ -15,6 +15,7 @@
  */
 
 import {
+  AbsoluteTime,
   AmountString,
   PaytoString,
   TalerError,
@@ -73,36 +74,36 @@ export type PartialButDefined<T> = {
  */
 export type WithIntermediate<Type> = {
   [prop in keyof Type]: Type[prop] extends PaytoString
-  ? Type[prop] | undefined
-  : Type[prop] extends AmountString
-  ? Type[prop] | undefined
-  : Type[prop] extends TranslatedString
-  ? Type[prop] | undefined
-  : Type[prop] extends object
-  ? WithIntermediate<Type[prop]>
-  : Type[prop] | undefined;
+    ? Type[prop] | undefined
+    : Type[prop] extends AmountString
+      ? Type[prop] | undefined
+      : Type[prop] extends TranslatedString
+        ? Type[prop] | undefined
+        : Type[prop] extends object
+          ? WithIntermediate<Type[prop]>
+          : Type[prop] | undefined;
 };
 export type RecursivePartial<Type> = {
   [P in keyof Type]?: Type[P] extends (infer U)[]
-  ? RecursivePartial<U>[]
-  : Type[P] extends object
-  ? RecursivePartial<Type[P]>
-  : Type[P];
+    ? RecursivePartial<U>[]
+    : Type[P] extends object
+      ? RecursivePartial<Type[P]>
+      : Type[P];
 };
 export type ErrorMessageMappingFor<Type> = {
   [prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString 
// enumerate known object
-  ? TranslatedString
-  : Exclude<Type[prop], undefined> extends AmountString
-  ? TranslatedString
-  : Exclude<Type[prop], undefined> extends TranslatedString
-  ? TranslatedString
-  : // arrays: every element
-  Exclude<Type[prop], undefined> extends (infer U)[]
-  ? ErrorMessageMappingFor<U>[]
-  : // map: every field
-  Exclude<Type[prop], undefined> extends object
-  ? ErrorMessageMappingFor<Type[prop]>
-  : TranslatedString;
+    ? TranslatedString
+    : Exclude<Type[prop], undefined> extends AmountString
+      ? TranslatedString
+      : Exclude<Type[prop], undefined> extends TranslatedString
+        ? TranslatedString
+        : // arrays: every element
+          Exclude<Type[prop], undefined> extends (infer U)[]
+          ? ErrorMessageMappingFor<U>[]
+          : // map: every field
+            Exclude<Type[prop], undefined> extends object
+            ? ErrorMessageMappingFor<Type[prop]>
+            : TranslatedString;
 };
 
 export enum TanChannel {
@@ -155,6 +156,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Request timeout`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -164,6 +166,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Request throttled`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -173,6 +176,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Malformed response`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -182,6 +186,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Network error`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -191,6 +196,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Unexpected request error`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -200,6 +206,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Unexpected error`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -373,11 +380,10 @@ export function validateIBAN(
   i18n: InternationalizationAPI,
 ): TranslatedString | undefined {
   if (!IBAN_REGEX.test(account)) {
-    return i18n.str`IBAN only have uppercased letters and numbers`
+    return i18n.str`IBAN only have uppercased letters and numbers`;
   }
   // Check total length
-  if (account.length < 4)
-    return i18n.str`IBAN numbers have more that 4 digits`;
+  if (account.length < 4) return i18n.str`IBAN numbers have more that 4 
digits`;
   if (account.length > 34)
     return i18n.str`IBAN numbers have less that 34 digits`;
 
@@ -423,25 +429,7 @@ export function validateTalerBank(
   i18n: InternationalizationAPI,
 ): TranslatedString | undefined {
   if (!USERNAME_REGEX.test(account)) {
-    return i18n.str`Account only have letters and numbers`
+    return i18n.str`Account only have letters and numbers`;
   }
-  return undefined
-}
-
-export function validateRawIBAN(
-  payto: string,
-  i18n: InternationalizationAPI,
-): TranslatedString | undefined {
-  return undefined
-}
-
-
-
-export function validateRawTalerBank(
-  payto: string,
-  currentHost: string,
-  i18n: InternationalizationAPI,
-): TranslatedString | undefined {
-  return undefined
+  return undefined;
 }
-
diff --git a/packages/taler-util/src/errors.ts 
b/packages/taler-util/src/errors.ts
index c5110bda8..11f01a3fe 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -93,6 +93,11 @@ export interface DetailsMap {
     requestMethod: string;
     timeoutMs: number;
   };
+  [TalerErrorCode.GENERIC_TIMEOUT]: {
+    requestUrl: string;
+    requestMethod: string;
+    timeoutMs: number;
+  };
   [TalerErrorCode.WALLET_NETWORK_ERROR]: {
     requestUrl: string;
     requestMethod: string;
@@ -112,7 +117,7 @@ export interface DetailsMap {
     operation: string;
     error: string;
     detail: TalerErrorDetail | undefined;
-  }
+  };
   [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
   [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
     numErrors: number;
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts 
b/packages/taler-util/src/http-client/bank-conversion.ts
index ea247ccfc..3db9df101 100644
--- a/packages/taler-util/src/http-client/bank-conversion.ts
+++ b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -39,7 +39,11 @@ import {
   codecForCashoutConversionResponse,
   codecForConversionBankConfig,
 } from "./types.js";
-import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from 
"./utils.js";
+import {
+  CacheEvictor,
+  makeBearerTokenAuthHeader,
+  nullEvictor,
+} from "./utils.js";
 
 export type TalerBankConversionResultByMethod<
   prop extends keyof TalerBankConversionHttpClient,
@@ -203,7 +207,9 @@ export class TalerBankConversionHttpClient {
     });
     switch (resp.status) {
       case HttpStatusCode.NoContent: {
-        
this.cacheEvictor.notifySuccess(TalerBankConversionCacheEviction.UPDATE_RATE);
+        this.cacheEvictor.notifySuccess(
+          TalerBankConversionCacheEviction.UPDATE_RATE,
+        );
         return opEmptySuccess(resp);
       }
       case HttpStatusCode.Unauthorized:
diff --git a/packages/taler-util/src/http-client/bank-core.ts 
b/packages/taler-util/src/http-client/bank-core.ts
index b9fd6da80..c02bf1ec9 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -153,7 +153,9 @@ export class TalerCoreBankHttpClient {
     });
     switch (resp.status) {
       case HttpStatusCode.Ok: {
-        await 
this.cacheEvictor.notifySuccess(TalerCoreBankCacheEviction.CREATE_ACCOUNT)
+        await this.cacheEvictor.notifySuccess(
+          TalerCoreBankCacheEviction.CREATE_ACCOUNT,
+        );
         return opSuccessFromHttp(resp, codecForRegisterAccountResponse());
       }
       case HttpStatusCode.BadRequest:
@@ -440,7 +442,10 @@ export class TalerCoreBankHttpClient {
     });
     switch (resp.status) {
       case HttpStatusCode.Ok:
-        return opSuccessFromHttp(resp, 
codecForBankAccountTransactionsResponse());
+        return opSuccessFromHttp(
+          resp,
+          codecForBankAccountTransactionsResponse(),
+        );
       case HttpStatusCode.NoContent:
         return opFixedSuccess({ transactions: [] });
       case HttpStatusCode.Unauthorized:
@@ -516,6 +521,8 @@ export class TalerCoreBankHttpClient {
         const body = await resp.json();
         const details = codecForTalerErrorDetail().decode(body);
         switch (details.code) {
+          case TalerErrorCode.BANK_ADMIN_CREDITOR:
+            return opKnownTalerFailure(details.code, resp);
           case TalerErrorCode.BANK_SAME_ACCOUNT:
             return opKnownTalerFailure(details.code, resp);
           case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
@@ -553,7 +560,10 @@ export class TalerCoreBankHttpClient {
     });
     switch (resp.status) {
       case HttpStatusCode.Ok:
-        return opSuccessFromHttp(resp, 
codecForBankAccountCreateWithdrawalResponse());
+        return opSuccessFromHttp(
+          resp,
+          codecForBankAccountCreateWithdrawalResponse(),
+        );
       case HttpStatusCode.NotFound:
         return opKnownHttpFailure(resp.status, resp);
       case HttpStatusCode.Conflict:
diff --git a/packages/taler-util/src/http-client/bank-integration.ts 
b/packages/taler-util/src/http-client/bank-integration.ts
index a224c1f09..f63fa4445 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -129,7 +129,10 @@ export class TalerBankIntegrationHttpClient {
     });
     switch (resp.status) {
       case HttpStatusCode.Ok:
-        return opSuccessFromHttp(resp, 
codecForBankWithdrawalOperationPostResponse());
+        return opSuccessFromHttp(
+          resp,
+          codecForBankWithdrawalOperationPostResponse(),
+        );
       case HttpStatusCode.NotFound:
         return opKnownHttpFailure(resp.status, resp);
       case HttpStatusCode.Conflict: {
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index 22272d12f..132ca867d 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -17,7 +17,11 @@ import {
 import { PaytoString, codecForPaytoString } from "../payto.js";
 import { AmountString } from "../taler-types.js";
 import { TalerActionString, codecForTalerActionString } from "../taleruri.js";
-import { TalerProtocolDuration, TalerProtocolTimestamp, codecForTimestamp } 
from "../time.js";
+import {
+  TalerProtocolDuration,
+  TalerProtocolTimestamp,
+  codecForTimestamp,
+} from "../time.js";
 
 export type UserAndPassword = {
   username: string;
@@ -163,9 +167,9 @@ type ImageDataUrl = string;
 
 type WadId = string;
 
-type Timestamp = TalerProtocolTimestamp
+type Timestamp = TalerProtocolTimestamp;
 
-type RelativeTime = TalerProtocolDuration
+type RelativeTime = TalerProtocolDuration;
 
 export interface LoginToken {
   token: AccessToken;
diff --git a/packages/taler-util/src/http-client/utils.ts 
b/packages/taler-util/src/http-client/utils.ts
index 2b8920b66..f925a5610 100644
--- a/packages/taler-util/src/http-client/utils.ts
+++ b/packages/taler-util/src/http-client/utils.ts
@@ -71,5 +71,5 @@ export interface CacheEvictor<T> {
 }
 
 export const nullEvictor: CacheEvictor<unknown> = {
-  notifySuccess: () => Promise.resolve()
-}
+  notifySuccess: () => Promise.resolve(),
+};
diff --git a/packages/taler-util/src/notifications.ts 
b/packages/taler-util/src/notifications.ts
index ab28cc2ee..f439b4a6f 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -22,6 +22,8 @@
 /**
  * Imports.
  */
+import { CancellationToken } from "./CancellationToken.js";
+import { AbsoluteTime } from "./time.js";
 import { TransactionState } from "./transactions-types.js";
 import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
 
@@ -132,15 +134,21 @@ export enum ObservabilityEventType {
 
 export type ObservabilityEvent =
   | {
+      id: string;
+      when: AbsoluteTime;
       type: ObservabilityEventType.HttpFetchStart;
       url: string;
     }
   | {
+      id: string;
+      when: AbsoluteTime;
       type: ObservabilityEventType.HttpFetchFinishSuccess;
       url: string;
       status: number;
     }
   | {
+      id: string;
+      when: AbsoluteTime;
       type: ObservabilityEventType.HttpFetchFinishError;
       url: string;
       error: TalerErrorDetail;
@@ -207,9 +215,9 @@ export interface BackupOperationErrorNotification {
   error: TalerErrorDetail;
 }
 /**
- * This notification is required to signal UI that 
+ * This notification is required to signal UI that
  * the withdrawal operation changed the state.
- * 
+ *
  * https://bugs.gnunet.org/view.php?id=8099
  */
 export interface WithdrawalOperationTransitionNotification {
diff --git a/packages/taler-util/src/observability.ts 
b/packages/taler-util/src/observability.ts
index 198dcbe6e..f40967c52 100644
--- a/packages/taler-util/src/observability.ts
+++ b/packages/taler-util/src/observability.ts
@@ -14,9 +14,17 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { ObservabilityEvent } from "./index.js";
-import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from 
"./http-common.js";
-import { ObservabilityEventType } from "./notifications.js"
+import {
+  AbsoluteTime,
+  CancellationToken,
+  ObservabilityEvent,
+} from "./index.js";
+import {
+  HttpRequestLibrary,
+  HttpRequestOptions,
+  HttpResponse,
+} from "./http-common.js";
+import { ObservabilityEventType } from "./notifications.js";
 import { getErrorDetailFromException } from "./errors.js";
 
 /**
@@ -27,22 +35,48 @@ export interface ObservabilityContext {
   observe(evt: ObservabilityEvent): void;
 }
 
+let seqId = 1000;
+
 export class ObservableHttpClientLibrary implements HttpRequestLibrary {
+  private readonly currentRequest = new Map<string, 
CancellationToken.Source>();
   constructor(
     private impl: HttpRequestLibrary,
     private oc: ObservabilityContext,
-  ) { }
+  ) {}
+
+  public cancelRequest(id: string) {
+    const cancelator = this.currentRequest.get(id);
+    if (!cancelator) return;
+    cancelator.cancel();
+  }
+
   async fetch(
     url: string,
     opt?: HttpRequestOptions | undefined,
   ): Promise<HttpResponse> {
+    const id = `req-${seqId}`;
+    seqId = seqId + 1;
+
+    const cancelator = CancellationToken.create();
+    if (opt?.cancellationToken) {
+      opt.cancellationToken.onCancelled(cancelator.cancel);
+    }
+    this.currentRequest.set(id, cancelator);
+
     this.oc.observe({
+      id,
+      when: AbsoluteTime.now(),
       type: ObservabilityEventType.HttpFetchStart,
       url: url,
     });
+
+    const optsWithCancel = opt ?? {};
+    optsWithCancel.cancellationToken = cancelator.token;
     try {
-      const res = await this.impl.fetch(url, opt);
+      const res = await this.impl.fetch(url, optsWithCancel);
       this.oc.observe({
+        id,
+        when: AbsoluteTime.now(),
         type: ObservabilityEventType.HttpFetchFinishSuccess,
         url,
         status: res.status,
@@ -50,6 +84,8 @@ export class ObservableHttpClientLibrary implements 
HttpRequestLibrary {
       return res;
     } catch (e) {
       this.oc.observe({
+        id,
+        when: AbsoluteTime.now(),
         type: ObservabilityEventType.HttpFetchFinishError,
         url,
         error: getErrorDetailFromException(e),
diff --git a/packages/taler-util/src/operation.ts 
b/packages/taler-util/src/operation.ts
index ecf4a020a..0debd09fd 100644
--- a/packages/taler-util/src/operation.ts
+++ b/packages/taler-util/src/operation.ts
@@ -174,10 +174,7 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
       {
         operation: opName,
         error: String(opRes.case),
-        detail:
-          "detail" in opRes
-            ? opRes.detail
-            : undefined,
+        detail: "detail" in opRes ? opRes.detail : undefined,
       },
       `Operation ${opName} failed: ${String(opRes.case)}`,
     );
diff --git a/packages/web-util/src/components/Button.tsx 
b/packages/web-util/src/components/Button.tsx
index 26b778eec..ea0ea2f38 100644
--- a/packages/web-util/src/components/Button.tsx
+++ b/packages/web-util/src/components/Button.tsx
@@ -1,8 +1,24 @@
-import { OperationFail, OperationOk, OperationResult, TalerError, 
TranslatedString } from "@gnu-taler/taler-util";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ 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 { AbsoluteTime, OperationFail, OperationOk, OperationResult, 
TalerError, TranslatedString } from "@gnu-taler/taler-util";
 // import { NotificationMessage, notifyInfo } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { HTMLAttributes, useEffect, useState, useTransition } from 
"preact/compat";
-import { NotificationMessage, buildRequestErrorMessage, notifyInfo, 
useTranslationContext } from "../index.browser.js";
+import { NotificationMessage, buildUnifiedRequestErrorMessage, notifyInfo, 
useTranslationContext } from "../index.browser.js";
 // import { useBankCoreApiContext } from "../context/config.js";
 
 // function errorMap<T extends OperationFail<unknown>>(resp: T, map: (d: 
T["case"]) => TranslatedString): void {
@@ -10,7 +26,7 @@ import { NotificationMessage, buildRequestErrorMessage, 
notifyInfo, useTranslati
 export interface ButtonHandler<T extends OperationResult<A, B>, A, B> {
   onClick: () => Promise<T | undefined>,
   onNotification: (n: NotificationMessage) => void;
-  onOperationSuccess: ((result:T extends OperationOk<any> ? T :never) => void) 
| ((result:T extends OperationOk<any> ? T :never) => TranslatedString | 
undefined),
+  onOperationSuccess: ((result: T extends OperationOk<any> ? T : never) => 
void) | ((result: T extends OperationOk<any> ? T : never) => TranslatedString | 
undefined),
   onOperationFail: (d: T extends OperationFail<any> ? T : never) => 
TranslatedString;
   onOperationComplete?: () => void;
 }
@@ -33,10 +49,10 @@ export function Button<T extends OperationResult<A, B>, A, 
B>({
   handler,
   children,
   disabled,
-  onClick:clickEvent,
+  onClick: clickEvent,
   ...rest
 }: Props<T, A, B>): VNode {
-  const {i18n} = useTranslationContext();
+  const { i18n } = useTranslationContext();
   const [running, setRunning] = useState(false)
   return <button {...rest} disabled={disabled || running} onClick={(e) => {
     e.preventDefault();
@@ -62,6 +78,7 @@ export function Button<T extends OperationResult<A, B>, A, 
B>({
             type: "error",
             description: error.detail.hint as TranslatedString,
             debug: error.detail,
+            when: AbsoluteTime.now(),
           })
         }
       }
@@ -71,17 +88,18 @@ export function Button<T extends OperationResult<A, B>, A, 
B>({
       setRunning(false)
     }).catch(error => {
       console.error(error)
-      
+
       if (error instanceof TalerError) {
-        handler.onNotification(buildRequestErrorMessage(i18n, error))
+        handler.onNotification(buildUnifiedRequestErrorMessage(i18n, error))
       } else {
         const description = (error instanceof Error ?
           error.message : String(error)) as TranslatedString
-  
+
         handler.onNotification({
           title: i18n.str`Operation failed`,
           type: "error",
           description,
+          when: AbsoluteTime.now(),
         })
       }
 
@@ -95,7 +113,7 @@ export function Button<T extends OperationResult<A, B>, A, 
B>({
   </button>
 }
 
-function Wait():VNode {
+function Wait(): VNode {
   return <Fragment>
     <style>{`
       #l1 {          width: 120px;
diff --git a/packages/web-util/src/components/CopyButton.tsx 
b/packages/web-util/src/components/CopyButton.tsx
index e76447291..fd7f8b3b4 100644
--- a/packages/web-util/src/components/CopyButton.tsx
+++ b/packages/web-util/src/components/CopyButton.tsx
@@ -38,7 +38,10 @@ export function CopyButton({ class: clazz, getContent }: { 
class: string, getCon
 
   if (!copied) {
     return (
-      <button class={clazz} onClick={copyText} >
+      <button class={clazz} onClick={e => {
+        e.preventDefault()
+        copyText()
+      }} >
         <CopyIcon />
       </button>
     );
diff --git a/packages/web-util/src/components/ErrorLoading.tsx 
b/packages/web-util/src/components/ErrorLoading.tsx
index 02f2a3282..7089266b9 100644
--- a/packages/web-util/src/components/ErrorLoading.tsx
+++ b/packages/web-util/src/components/ErrorLoading.tsx
@@ -26,6 +26,34 @@ export function ErrorLoading({ error, showDetail }: { error: 
TalerError, showDet
     //////////////////
     // Every error that can be produce in a Http Request
     //////////////////
+    case TalerErrorCode.GENERIC_TIMEOUT: {
+      if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
+        const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+        return <Attention type="danger" title={i18n.str`The request reached a 
timeout, check your connection.`}>
+          {error.message}
+          {showDetail &&
+            <pre class="whitespace-break-spaces ">
+              {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, 
undefined, 2)}
+            </pre>
+          }
+        </Attention>
+      }
+      assertUnreachable(1 as never)
+    }
+    case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+      if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
+        const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+        return <Attention type="danger" title={i18n.str`The request was 
cancelled.`}>
+          {error.message}
+          {showDetail &&
+            <pre class="whitespace-break-spaces ">
+              {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, 
undefined, 2)}
+            </pre>
+          }
+        </Attention>
+      }
+      assertUnreachable(1 as never)
+    }
     case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
       if 
(error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
         const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
diff --git a/packages/web-util/src/components/Header.tsx 
b/packages/web-util/src/components/Header.tsx
index fc7716320..29f4a4949 100644
--- a/packages/web-util/src/components/Header.tsx
+++ b/packages/web-util/src/components/Header.tsx
@@ -1,12 +1,24 @@
 import { useState } from "preact/hooks";
-import { LangSelector, useTranslationContext } from "../index.browser.js";
+import { LangSelector, useNotifications, useTranslationContext } from 
"../index.browser.js";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
 import logo from "../assets/logo-2021.svg";
 
-export function Header({ title, profileURL, iconLinkURL, sites, onLogout, 
children }:
-  { title: string, iconLinkURL: string, profileURL?: string, children?: 
ComponentChildren, onLogout: (() => void) | undefined, sites: 
Array<Array<string>>, supportedLangs: string[] }): VNode {
+interface Props {
+  title: string;
+  iconLinkURL: string;
+  profileURL?: string;
+  notificationURL?: string;
+  children?: ComponentChildren;
+  onLogout: (() => void) | undefined;
+  sites: Array<Array<string>>;
+  supportedLangs: string[]
+}
+
+export function Header({ title, profileURL, notificationURL, iconLinkURL, 
sites, onLogout, children }: Props): VNode {
   const { i18n } = useTranslationContext();
   const [open, setOpen] = useState(false)
+  const ns = useNotifications();
+
   return <Fragment>
     <header class="bg-indigo-600 w-full mx-auto px-2 border-b 
border-opacity-25 border-indigo-400">
       <div class="flex flex-row h-16 items-center ">
@@ -35,6 +47,22 @@ export function Header({ title, profileURL, iconLinkURL, 
sites, onLogout, childr
           </div>
         </div>
         <div class="flex justify-end">
+          {!notificationURL ? undefined :
+            <a href={notificationURL} name="notifications" class="relative 
inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 
text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white 
focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 
focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false">
+              <span class="absolute -inset-0.5"></span>
+              <span class="sr-only"><i18n.Translate>Show 
notifications</i18n.Translate></span>
+              {ns.length > 0 ?
+                <svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 24 24" 
fill="currentColor" class="w-10 h-10">
+                  <path d="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 
0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 
2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 
9.72 0 0 0 19.266 2.5Z" />
+                  <path fill-rule="evenodd" d="M12 2.25A6.75 6.75 0 0 0 5.25 
9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 
4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 
.298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 
18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 
0Z" clip-rule="evenodd" />
+                </svg>
+                :
+                <svg xmlns="http://www.w3.org/2000/svg"; fill="none" viewBox="0 
0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10">
+                  <path stroke-linecap="round" stroke-linejoin="round" 
d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 
6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 
1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
+                </svg>
+              }
+            </a>
+          }
           {!profileURL ? undefined :
             <a href={profileURL} name="profile" class="relative inline-flex 
items-center justify-center rounded-md bg-indigo-600 p-1 mr-2 text-indigo-200 
hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none 
focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" 
aria-controls="mobile-menu" aria-expanded="false">
               <span class="absolute -inset-0.5"></span>
@@ -58,7 +86,8 @@ export function Header({ title, profileURL, iconLinkURL, 
sites, onLogout, childr
       </div>
     </header>
 
-    {open &&
+    {
+      open &&
       <div class="relative z-10" name="sidebar overlay" 
aria-labelledby="slide-over-title" role="dialog" aria-modal="true"
         onClick={() => {
           setOpen(false)
@@ -150,5 +179,5 @@ export function Header({ title, profileURL, iconLinkURL, 
sites, onLogout, childr
         </div>
       </div>
     }
-  </Fragment>
+  </Fragment >
 }
diff --git a/packages/web-util/src/components/NotificationBanner.tsx 
b/packages/web-util/src/components/NotificationBanner.tsx
index 62733ab3c..31d5a5d01 100644
--- a/packages/web-util/src/components/NotificationBanner.tsx
+++ b/packages/web-util/src/components/NotificationBanner.tsx
@@ -9,7 +9,7 @@ export function LocalNotificationBanner({ notification, 
showDebug }: { notificat
       return <div class="relative">
         <div class="fixed top-0 left-0 right-0 z-20 w-full p-4">
           <Attention type="danger" title={notification.message.title} 
onClose={() => {
-            notification.remove()
+            notification.acknowledge()
           }}>
             {notification.message.description &&
               <div class="mt-2 text-sm text-red-700">
@@ -26,7 +26,7 @@ export function LocalNotificationBanner({ notification, 
showDebug }: { notificat
       return <div class="relative">
         <div class="fixed top-0 left-0 right-0 z-20 w-full p-4">
           <Attention type="success" title={notification.message.title} 
onClose={() => {
-            notification.remove();
+            notification.acknowledge();
           }} /></div></div>
   }
 }
diff --git a/packages/web-util/src/components/ToastBanner.tsx 
b/packages/web-util/src/components/ToastBanner.tsx
index 2424b17ff..ece26285f 100644
--- a/packages/web-util/src/components/ToastBanner.tsx
+++ b/packages/web-util/src/components/ToastBanner.tsx
@@ -1,5 +1,20 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ 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 { Fragment, VNode, h } from "preact"
-import { Attention, GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT, 
useNotifications } from "../index.browser.js"
+import { Attention, GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT, 
Notification, useNotifications } from "../index.browser.js"
 
 /**
  * Toasts should be considered when displaying these types of information to 
the user:
@@ -21,24 +36,26 @@ import { Attention, GLOBAL_NOTIFICATION_TIMEOUT as 
GLOBAL_TOAST_TIMEOUT, useNoti
 export function ToastBanner(): VNode {
   const notifs = useNotifications()
   if (notifs.length === 0) return <Fragment />
-  return <Fragment> {
-    notifs.map(n => {
-      switch (n.message.type) {
-        case "error":
-          return <Attention type="danger" title={n.message.title} onClose={() 
=> {
-            n.remove()
-          }} timeout={GLOBAL_TOAST_TIMEOUT}>
-            {n.message.description &&
-              <div class="mt-2 text-sm text-red-700">
-                {n.message.description}
-              </div>
-            }
-          </Attention>
-        case "info":
-          return <Attention type="success" title={n.message.title} onClose={() 
=> {
-            n.remove();
-          }} timeout={GLOBAL_TOAST_TIMEOUT} />
-      }
-    })}
-  </Fragment>
+  const show = notifs.filter(e => !e.message.ack && !e.message.timeout)
+  if (show.length === 0) return <Fragment />
+  return <AttentionByType msg={show[0]} />
+}
+
+function AttentionByType({ msg }: { msg: Notification }) {
+  switch (msg.message.type) {
+    case "error":
+      return <Attention type="danger" title={msg.message.title} onClose={() => 
{
+        msg.acknowledge()
+      }} timeout={GLOBAL_TOAST_TIMEOUT}>
+        {msg.message.description &&
+          <div class="mt-2 text-sm text-red-700">
+            {msg.message.description}
+          </div>
+        }
+      </Attention>
+    case "info":
+      return <Attention type="success" title={msg.message.title} onClose={() 
=> {
+        msg.acknowledge();
+      }} timeout={GLOBAL_TOAST_TIMEOUT} />
+  }
 }
diff --git a/packages/web-util/src/hooks/useNotifications.ts 
b/packages/web-util/src/hooks/useNotifications.ts
index 000abbc94..99f4f2699 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -1,4 +1,5 @@
 import {
+  AbsoluteTime,
   Duration,
   OperationFail,
   OperationOk,
@@ -20,12 +21,18 @@ export type NotificationMessage = ErrorNotification | 
InfoNotification;
 export interface ErrorNotification {
   type: "error";
   title: TranslatedString;
+  ack?: boolean;
+  timeout?: boolean;
   description?: TranslatedString;
   debug?: any;
+  when: AbsoluteTime;
 }
 export interface InfoNotification {
   type: "info";
   title: TranslatedString;
+  ack?: boolean;
+  timeout?: boolean;
+  when: AbsoluteTime;
 }
 
 const storage = memoryMap<Map<string, NotificationMessage>>();
@@ -35,11 +42,11 @@ export const GLOBAL_NOTIFICATION_TIMEOUT = 
Duration.fromSpec({
   seconds: 5,
 });
 
-function removeFromStorage(n: NotificationMessage) {
+function updateInStorage(n: NotificationMessage) {
   const h = hash(n);
   const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
   const newState = new Map(mem);
-  newState.delete(h);
+  newState.set(h, n);
   storage.set(NOTIFICATION_KEY, newState);
 }
 
@@ -50,7 +57,8 @@ export function notify(notif: NotificationMessage): void {
 
   if (GLOBAL_NOTIFICATION_TIMEOUT.d_ms !== "forever") {
     setTimeout(() => {
-      removeFromStorage(notif);
+      notif.timeout = true;
+      updateInStorage(notif);
     }, GLOBAL_NOTIFICATION_TIMEOUT.d_ms);
   }
 
@@ -66,6 +74,7 @@ export function notifyError(
     title,
     description,
     debug,
+    when: AbsoluteTime.now(),
   });
 }
 export function notifyException(title: TranslatedString, ex: Error) {
@@ -74,34 +83,40 @@ export function notifyException(title: TranslatedString, 
ex: Error) {
     title,
     description: ex.message as TranslatedString,
     debug: ex.stack,
+    when: AbsoluteTime.now(),
   });
 }
 export function notifyInfo(title: TranslatedString) {
   notify({
     type: "info" as const,
     title,
+    when: AbsoluteTime.now(),
   });
 }
 
 export type Notification = {
   message: NotificationMessage;
-  remove: () => void;
+  acknowledge: () => void;
 };
 
 export function useNotifications(): Notification[] {
-  const [value, setter] = useState<Map<string, NotificationMessage>>(new 
Map());
+  const [, setLastUpdate] = useState<number>();
+  const value = storage.get(NOTIFICATION_KEY) ?? new Map();
+
   useEffect(() => {
     return storage.onUpdate(NOTIFICATION_KEY, () => {
-      const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
-      setter(structuredClone(mem));
+      setLastUpdate(Date.now())
+      // const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
+      // setter(structuredClone(mem));
     });
   });
 
   return Array.from(value.values()).map((message, idx) => {
     return {
       message,
-      remove: () => {
-        removeFromStorage(message);
+      acknowledge: () => {
+        message.ack = true;
+        updateInStorage(message);
       },
     };
   });
@@ -141,6 +156,7 @@ function errorMap<T extends OperationFail<unknown>>(
     title: map(resp.case),
     description: resp.detail.hint as TranslatedString,
     debug: resp.detail,
+    when: AbsoluteTime.now(),
   });
 }
 
@@ -165,7 +181,7 @@ export function useLocalNotification(): [
     ? undefined
     : {
       message: value,
-      remove: () => {
+      acknowledge: () => {
         setter(undefined);
       },
     };
@@ -175,7 +191,7 @@ export function useLocalNotification(): [
       return await cb(errorMap);
     } catch (error: unknown) {
       if (error instanceof TalerError) {
-        notify(buildRequestErrorMessage(i18n, error));
+        notify(buildUnifiedRequestErrorMessage(i18n, error));
       } else {
         notifyError(
           i18n.str`Operation failed, please report`,
@@ -212,7 +228,7 @@ export function useLocalNotificationHandler(): [
     ? undefined
     : {
       message: value,
-      remove: () => {
+      acknowledge: () => {
         setter(undefined);
       },
     };
@@ -241,18 +257,39 @@ export function useLocalNotificationHandler(): [
   return [notif, makeHandler, setter];
 }
 
-export function buildRequestErrorMessage(
+export function buildUnifiedRequestErrorMessage(
   i18n: InternationalizationAPI,
   cause: TalerError,
 ): ErrorNotification {
   let result: ErrorNotification;
   switch (cause.errorDetail.code) {
+    case TalerErrorCode.GENERIC_TIMEOUT: {
+      result = {
+        type: "error",
+        title: i18n.str`Request timeout`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
+      };
+      break;
+    }
+    case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+      result = {
+        type: "error",
+        title: i18n.str`Request cancelled`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
+      };
+      break;
+    }
     case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
       result = {
         type: "error",
         title: i18n.str`Request timeout`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -262,6 +299,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Request throttled`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -271,6 +309,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Malformed response`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -280,6 +319,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Network error`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -289,6 +329,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Unexpected request error`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
@@ -298,6 +339,7 @@ export function buildRequestErrorMessage(
         title: i18n.str`Unexpected error`,
         description: cause.message as TranslatedString,
         debug: JSON.stringify(cause.errorDetail, undefined, 2),
+        when: AbsoluteTime.now(),
       };
       break;
     }
diff --git a/packages/web-util/src/utils/http-impl.sw.ts 
b/packages/web-util/src/utils/http-impl.sw.ts
index 316b75dfd..4d7f3a8a1 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -22,6 +22,7 @@ import {
   TalerErrorCode,
   TalerError,
   Duration,
+  CancellationToken,
 } from "@gnu-taler/taler-util";
 
 import {
@@ -137,13 +138,13 @@ export class BrowserFetchHttpLib implements 
HttpRequestLibrary {
     } catch (e) {
       if (controller.signal) {
         throw TalerError.fromDetail(
-          TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT,
+          controller.signal.reason,
           {
             requestUrl,
             requestMethod,
             timeoutMs: requestTimeout.d_ms === "forever" ? 0 : 
requestTimeout.d_ms
           },
-          `request to ${requestUrl} timed out`,
+          `HTTP request failed.`,
         );
       }
       throw e;

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]