gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/03: obs and cancel request, plus lint


From: gnunet
Subject: [taler-wallet-core] 01/03: obs and cancel request, plus lint
Date: Mon, 11 Mar 2024 18:58:05 +0100

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

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

commit 37f46f4d6b821d163c3e4db5c374b1120212ac74
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Mar 11 14:56:25 2024 -0300

    obs and cancel request, plus lint
---
 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 +-
 47 files changed, 2133 insertions(+), 1419 deletions(-)

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;
 }
-

-- 
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]