gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (f7982ed99 -> a8c5a9696)


From: gnunet
Subject: [taler-wallet-core] branch master updated (f7982ed99 -> a8c5a9696)
Date: Wed, 08 Feb 2023 21:41:33 +0100

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

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

    from f7982ed99 address #7523
     new be01d1479 move request api to web-util
     new 603efbd07 use request api from web-util
     new 9b0d887a1 more doc
     new a8c5a9696 impl accout management and refactor

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


Summary of changes:
 packages/demobank-ui/package.json                  |   4 +-
 .../components/{Transactions => Cashouts}/index.ts |  14 +-
 .../src/components/Cashouts}/state.ts              |  24 +-
 .../{Transactions => Cashouts}/stories.tsx         |   0
 .../components/{Transactions => Cashouts}/test.ts  |   9 +-
 .../{Transactions => Cashouts}/views.tsx           |  26 +-
 packages/demobank-ui/src/components/Loading.tsx    |  24 +-
 .../src/components/Transactions/index.ts           |  10 +-
 .../src/components/Transactions/state.ts           | 105 ++-
 .../src/components/Transactions/test.ts            |   9 +-
 packages/demobank-ui/src/components/app.tsx        |  23 +-
 packages/demobank-ui/src/context/backend.ts        |   4 +-
 packages/demobank-ui/src/context/pageState.ts      |  21 +-
 packages/demobank-ui/src/declaration.d.ts          | 362 ++++++++++-
 packages/demobank-ui/src/hooks/access.ts           | 330 ++++++++++
 packages/demobank-ui/src/hooks/async.ts            |   1 -
 packages/demobank-ui/src/hooks/backend.ts          | 195 +++++-
 packages/demobank-ui/src/hooks/circuit.ts          | 317 +++++++++
 packages/demobank-ui/src/pages/AccountPage.tsx     | 283 +++------
 packages/demobank-ui/src/pages/AdminPage.tsx       | 707 +++++++++++++++++++++
 packages/demobank-ui/src/pages/BankFrame.tsx       |  42 +-
 packages/demobank-ui/src/pages/HomePage.tsx        | 149 +++++
 packages/demobank-ui/src/pages/LoginForm.tsx       | 188 +++---
 packages/demobank-ui/src/pages/PaymentOptions.tsx  |  33 +-
 .../src/pages/PaytoWireTransferForm.tsx            | 317 +++------
 .../demobank-ui/src/pages/PublicHistoriesPage.tsx  |  93 +--
 packages/demobank-ui/src/pages/QrCodeSection.tsx   |   9 +-
 .../demobank-ui/src/pages/RegistrationPage.tsx     | 176 ++---
 packages/demobank-ui/src/pages/Routing.tsx         |  84 ++-
 .../demobank-ui/src/pages/WalletWithdrawForm.tsx   | 259 ++++----
 .../src/pages/WithdrawalConfirmationQuestion.tsx   | 466 ++++++++------
 .../demobank-ui/src/pages/WithdrawalQRCode.tsx     | 111 ++--
 packages/demobank-ui/src/scss/bank.scss            |   7 +
 packages/demobank-ui/src/utils.ts                  |  48 +-
 .../merchant-backoffice-ui/src/InstanceRoutes.tsx  |  15 +-
 .../src/components/form/InputImage.tsx             |   2 +-
 .../merchant-backoffice-ui/src/declaration.d.ts    |  15 +-
 .../merchant-backoffice-ui/src/hooks/backend.ts    | 108 ++--
 .../merchant-backoffice-ui/src/hooks/instance.ts   |  34 +-
 packages/merchant-backoffice-ui/src/hooks/order.ts |  34 +-
 .../merchant-backoffice-ui/src/hooks/product.ts    |  22 +-
 .../merchant-backoffice-ui/src/hooks/reserves.ts   |  32 +-
 .../merchant-backoffice-ui/src/hooks/templates.ts  |  37 +-
 .../merchant-backoffice-ui/src/hooks/testing.tsx   |   9 +-
 .../merchant-backoffice-ui/src/hooks/transfer.ts   |  27 +-
 .../merchant-backoffice-ui/src/hooks/webhooks.ts   |  32 +-
 .../src/paths/admin/list/index.tsx                 |   8 +-
 .../src/paths/instance/details/index.tsx           |   9 +-
 .../src/paths/instance/kyc/list/index.tsx          |   5 +-
 .../src/paths/instance/orders/create/index.tsx     |   5 +-
 .../src/paths/instance/orders/details/index.tsx    |   9 +-
 .../src/paths/instance/orders/list/index.tsx       |  10 +-
 .../src/paths/instance/products/list/index.tsx     |   8 +-
 .../src/paths/instance/products/update/index.tsx   |   8 +-
 .../paths/instance/reserves/create/CreatePage.tsx  |   3 +-
 .../src/paths/instance/reserves/details/index.tsx  |   5 +-
 .../src/paths/instance/reserves/list/index.tsx     |   8 +-
 .../src/paths/instance/templates/list/index.tsx    |   8 +-
 .../src/paths/instance/templates/update/index.tsx  |   8 +-
 .../paths/instance/templates/use/Use.stories.tsx   |   1 -
 .../src/paths/instance/templates/use/index.tsx     |   8 +-
 .../src/paths/instance/transfers/list/index.tsx    |   4 +-
 .../src/paths/instance/update/index.tsx            |  16 +-
 .../src/paths/instance/webhooks/list/index.tsx     |   8 +-
 .../src/paths/instance/webhooks/update/index.tsx   |   8 +-
 .../src/components/AmountField.tsx                 |  40 +-
 packages/web-util/package.json                     |   2 +-
 .../src/context/api.ts                             |   0
 packages/web-util/src/context/index.ts             |   4 +-
 packages/web-util/src/index.browser.ts             |   1 +
 packages/web-util/src/utils/base64.ts              | 243 +++++++
 .../src/utils/request.ts                           | 139 ++--
 pnpm-lock.yaml                                     |  22 +-
 73 files changed, 3874 insertions(+), 1533 deletions(-)
 copy packages/demobank-ui/src/components/{Transactions => Cashouts}/index.ts 
(88%)
 copy packages/{taler-wallet-webextension/src/wallet/EmptyComponentExample => 
demobank-ui/src/components/Cashouts}/state.ts (60%)
 copy packages/demobank-ui/src/components/{Transactions => 
Cashouts}/stories.tsx (100%)
 copy packages/demobank-ui/src/components/{Transactions => Cashouts}/test.ts 
(97%)
 copy packages/demobank-ui/src/components/{Transactions => Cashouts}/views.tsx 
(70%)
 create mode 100644 packages/demobank-ui/src/hooks/access.ts
 create mode 100644 packages/demobank-ui/src/hooks/circuit.ts
 create mode 100644 packages/demobank-ui/src/pages/AdminPage.tsx
 create mode 100644 packages/demobank-ui/src/pages/HomePage.tsx
 rename packages/{merchant-backoffice-ui => web-util}/src/context/api.ts (100%)
 create mode 100644 packages/web-util/src/utils/base64.ts
 rename packages/{merchant-backoffice-ui => web-util}/src/utils/request.ts (66%)

diff --git a/packages/demobank-ui/package.json 
b/packages/demobank-ui/package.json
index cdf457ed4..ff402cf3e 100644
--- a/packages/demobank-ui/package.json
+++ b/packages/demobank-ui/package.json
@@ -25,7 +25,7 @@
     "preact": "10.11.3",
     "preact-router": "3.2.1",
     "qrcode-generator": "^1.4.4",
-    "swr": "1.3.0"
+    "swr": "2.0.3"
   },
   "eslintConfig": {
     "plugins": [
@@ -66,4 +66,4 @@
   "pogen": {
     "domain": "bank"
   }
-}
+}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/components/Transactions/index.ts 
b/packages/demobank-ui/src/components/Cashouts/index.ts
similarity index 88%
copy from packages/demobank-ui/src/components/Transactions/index.ts
copy to packages/demobank-ui/src/components/Cashouts/index.ts
index 0c9084946..db39ba7e4 100644
--- a/packages/demobank-ui/src/components/Transactions/index.ts
+++ b/packages/demobank-ui/src/components/Cashouts/index.ts
@@ -14,18 +14,16 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { HttpError, utils } from "@gnu-taler/web-util/lib/index.browser";
 import { Loading } from "../Loading.js";
-import { HookError, utils } from "@gnu-taler/web-util/lib/index.browser";
 // import { compose, StateViewMap } from "../../utils/index.js";
 // import { wxApi } from "../../wxApi.js";
+import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util";
 import { useComponentState } from "./state.js";
 import { LoadingUriView, ReadyView } from "./views.js";
-import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util";
 
 export interface Props {
-  pageNumber: number;
-  accountLabel: string;
-  balanceValue?: string;
+  account: string;
 }
 
 export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -38,7 +36,7 @@ export namespace State {
 
   export interface LoadingUriError {
     status: "loading-error";
-    error: HookError;
+    error: HttpError<SandboxBackend.SandboxError>;
   }
 
   export interface BaseInfo {
@@ -47,7 +45,7 @@ export namespace State {
   export interface Ready extends BaseInfo {
     status: "ready";
     error: undefined;
-    transactions: Transaction[];
+    cashouts: SandboxBackend.Circuit.CashoutStatusResponse[];
   }
 }
 
@@ -65,7 +63,7 @@ const viewMapping: utils.StateViewMap<State> = {
   ready: ReadyView,
 };
 
-export const Transactions = utils.compose(
+export const Cashouts = utils.compose(
   (p: Props) => useComponentState(p),
   viewMapping,
 );
diff --git 
a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts 
b/packages/demobank-ui/src/components/Cashouts/state.ts
similarity index 60%
copy from 
packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
copy to packages/demobank-ui/src/components/Cashouts/state.ts
index 31a351579..7e420940f 100644
--- 
a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
+++ b/packages/demobank-ui/src/components/Cashouts/state.ts
@@ -14,11 +14,31 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Props, State } from "./index.js";
+import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
+import { useCashouts } from "../../hooks/circuit.js";
+import { Props, State, Transaction } from "./index.js";
+
+export function useComponentState({
+  account,
+}: Props): State {
+  const result = useCashouts()
+  if (result.loading) {
+    return {
+      status: "loading",
+      error: undefined
+    }
+  }
+  if (!result.ok) {
+    return {
+      status: "loading-error",
+      error: result
+    }
+  }
+
 
-export function useComponentState({ p }: Props): State {
   return {
     status: "ready",
     error: undefined,
+    cashout: result.data,
   };
 }
diff --git a/packages/demobank-ui/src/components/Transactions/stories.tsx 
b/packages/demobank-ui/src/components/Cashouts/stories.tsx
similarity index 100%
copy from packages/demobank-ui/src/components/Transactions/stories.tsx
copy to packages/demobank-ui/src/components/Cashouts/stories.tsx
diff --git a/packages/demobank-ui/src/components/Transactions/test.ts 
b/packages/demobank-ui/src/components/Cashouts/test.ts
similarity index 97%
copy from packages/demobank-ui/src/components/Transactions/test.ts
copy to packages/demobank-ui/src/components/Cashouts/test.ts
index 21a0eefbb..3f2d5fb68 100644
--- a/packages/demobank-ui/src/components/Transactions/test.ts
+++ b/packages/demobank-ui/src/components/Cashouts/test.ts
@@ -31,8 +31,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment();
 
     const props: Props = {
-      accountLabel: "myAccount",
-      pageNumber: 0,
+      account: "myAccount",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
@@ -116,8 +115,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment();
 
     const props: Props = {
-      accountLabel: "myAccount",
-      pageNumber: 0,
+      account: "myAccount",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
@@ -150,8 +148,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment(false);
 
     const props: Props = {
-      accountLabel: "myAccount",
-      pageNumber: 0,
+      account: "myAccount",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx 
b/packages/demobank-ui/src/components/Cashouts/views.tsx
similarity index 70%
copy from packages/demobank-ui/src/components/Transactions/views.tsx
copy to packages/demobank-ui/src/components/Cashouts/views.tsx
index dad352420..30803d4d1 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -30,38 +30,30 @@ export function LoadingUriView({ error }: 
State.LoadingUriError): VNode {
   );
 }
 
-export function ReadyView({ transactions }: State.Ready): VNode {
+export function ReadyView({ cashouts }: State.Ready): VNode {
   const { i18n } = useTranslationContext();
   return (
     <div class="results">
       <table class="pure-table pure-table-striped">
         <thead>
           <tr>
-            <th>{i18n.str`Date`}</th>
-            <th>{i18n.str`Amount`}</th>
+            <th>{i18n.str`Created`}</th>
+            <th>{i18n.str`Confirmed`}</th>
             <th>{i18n.str`Counterpart`}</th>
             <th>{i18n.str`Subject`}</th>
           </tr>
         </thead>
         <tbody>
-          {transactions.map((item, idx) => {
+          {cashouts.map((item, idx) => {
             return (
               <tr key={idx}>
+                <td>{format(item.creation_time, "dd/MM/yyyy HH:mm:ss")}</td>
                 <td>
-                  {item.when.t_ms === "never"
-                    ? ""
-                    : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}
-                </td>
-                <td>
-                  {item.negative ? "-" : ""}
-                  {item.amount ? (
-                    `${Amounts.stringifyValue(item.amount)} ${
-                      item.amount.currency
-                    }`
-                  ) : (
-                    <span style={{ color: "grey" }}>&lt;invalid 
value&gt;</span>
-                  )}
+                  {item.confirmation_time
+                    ? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss")
+                    : "-"}
                 </td>
+                <td>{Amounts.stringifyValue(item.amount_credit)}</td>
                 <td>{item.counterpart}</td>
                 <td>{item.subject}</td>
               </tr>
diff --git a/packages/demobank-ui/src/components/Loading.tsx 
b/packages/demobank-ui/src/components/Loading.tsx
index 8fd01858b..7cbdad681 100644
--- a/packages/demobank-ui/src/components/Loading.tsx
+++ b/packages/demobank-ui/src/components/Loading.tsx
@@ -17,5 +17,27 @@
 import { h, VNode } from "preact";
 
 export function Loading(): VNode {
-  return <div>loading...</div>;
+  return (
+    <div
+      class="columns is-centered is-vcentered"
+      style={{
+        height: "calc(100% - 3rem)",
+        position: "absolute",
+        width: "100%",
+      }}
+    >
+      <Spinner />
+    </div>
+  );
+}
+
+export function Spinner(): VNode {
+  return (
+    <div class="lds-ring">
+      <div />
+      <div />
+      <div />
+      <div />
+    </div>
+  );
 }
diff --git a/packages/demobank-ui/src/components/Transactions/index.ts 
b/packages/demobank-ui/src/components/Transactions/index.ts
index 0c9084946..e43b9401c 100644
--- a/packages/demobank-ui/src/components/Transactions/index.ts
+++ b/packages/demobank-ui/src/components/Transactions/index.ts
@@ -14,18 +14,16 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { HttpError, utils } from "@gnu-taler/web-util/lib/index.browser";
 import { Loading } from "../Loading.js";
-import { HookError, utils } from "@gnu-taler/web-util/lib/index.browser";
 // import { compose, StateViewMap } from "../../utils/index.js";
 // import { wxApi } from "../../wxApi.js";
+import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util";
 import { useComponentState } from "./state.js";
 import { LoadingUriView, ReadyView } from "./views.js";
-import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util";
 
 export interface Props {
-  pageNumber: number;
-  accountLabel: string;
-  balanceValue?: string;
+  account: string;
 }
 
 export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -38,7 +36,7 @@ export namespace State {
 
   export interface LoadingUriError {
     status: "loading-error";
-    error: HookError;
+    error: HttpError<SandboxBackend.SandboxError>;
   }
 
   export interface BaseInfo {
diff --git a/packages/demobank-ui/src/components/Transactions/state.ts 
b/packages/demobank-ui/src/components/Transactions/state.ts
index a5087ef32..9e1bce39b 100644
--- a/packages/demobank-ui/src/components/Transactions/state.ts
+++ b/packages/demobank-ui/src/components/Transactions/state.ts
@@ -15,66 +15,65 @@
  */
 
 import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
-import { parse } from "date-fns";
-import { useEffect } from "preact/hooks";
-import useSWR from "swr";
-import { Props, State } from "./index.js";
+import { useTransactions } from "../../hooks/access.js";
+import { Props, State, Transaction } from "./index.js";
 
 export function useComponentState({
-  accountLabel,
-  pageNumber,
-  balanceValue,
+  account,
 }: Props): State {
-  const { data, error, mutate } = useSWR(
-    `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
-  );
-
-  useEffect(() => {
-    if (balanceValue) {
-      mutate();
-    }
-  }, [balanceValue ?? ""]);
-
-  if (error) {
-    switch (error.status) {
-      case 404:
-        return {
-          status: "loading-error",
-          error: {
-            hasError: true,
-            operational: false,
-            message: `Transactions page ${pageNumber} was not found.`,
-          },
-        };
-      case 401:
-        return {
-          status: "loading-error",
-          error: {
-            hasError: true,
-            operational: false,
-            message: "Wrong credentials given.",
-          },
-        };
-      default:
-        return {
-          status: "loading-error",
-          error: {
-            hasError: true,
-            operational: false,
-            message: `Transaction page ${pageNumber} could not be retrieved.`,
-          } as any,
-        };
+  const result = useTransactions(account)
+  if (result.loading) {
+    return {
+      status: "loading",
+      error: undefined
     }
   }
-
-  if (!data) {
+  if (!result.ok) {
     return {
-      status: "loading",
-      error: undefined,
-    };
+      status: "loading-error",
+      error: result
+    }
   }
+  // if (error) {
+  //   switch (error.status) {
+  //     case 404:
+  //       return {
+  //         status: "loading-error",
+  //         error: {
+  //           hasError: true,
+  //           operational: false,
+  //           message: `Transactions page ${pageNumber} was not found.`,
+  //         },
+  //       };
+  //     case 401:
+  //       return {
+  //         status: "loading-error",
+  //         error: {
+  //           hasError: true,
+  //           operational: false,
+  //           message: "Wrong credentials given.",
+  //         },
+  //       };
+  //     default:
+  //       return {
+  //         status: "loading-error",
+  //         error: {
+  //           hasError: true,
+  //           operational: false,
+  //           message: `Transaction page ${pageNumber} could not be 
retrieved.`,
+  //         } as any,
+  //       };
+  //   }
+  // }
+
+  // if (!data) {
+  //   return {
+  //     status: "loading",
+  //     error: undefined,
+  //   };
+  // }
 
-  const transactions = data.transactions.map((item: unknown) => {
+  const transactions = result.data.transactions.map((item: unknown) => {
     if (
       !item ||
       typeof item !== "object" ||
@@ -120,7 +119,7 @@ export function useComponentState({
       amount,
       subject,
     };
-  });
+  }).filter((x): x is Transaction => x !== undefined);
 
   return {
     status: "ready",
diff --git a/packages/demobank-ui/src/components/Transactions/test.ts 
b/packages/demobank-ui/src/components/Transactions/test.ts
index 21a0eefbb..3f2d5fb68 100644
--- a/packages/demobank-ui/src/components/Transactions/test.ts
+++ b/packages/demobank-ui/src/components/Transactions/test.ts
@@ -31,8 +31,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment();
 
     const props: Props = {
-      accountLabel: "myAccount",
-      pageNumber: 0,
+      account: "myAccount",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
@@ -116,8 +115,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment();
 
     const props: Props = {
-      accountLabel: "myAccount",
-      pageNumber: 0,
+      account: "myAccount",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
@@ -150,8 +148,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment(false);
 
     const props: Props = {
-      accountLabel: "myAccount",
-      pageNumber: 0,
+      account: "myAccount",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
diff --git a/packages/demobank-ui/src/components/app.tsx 
b/packages/demobank-ui/src/components/app.tsx
index 8679b05dd..e024be41b 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -24,6 +24,9 @@ import { PageStateProvider } from "../context/pageState.js";
 import { Routing } from "../pages/Routing.js";
 import { strings } from "../i18n/strings.js";
 import { TranslationProvider } from "@gnu-taler/web-util/lib/index.browser";
+import { SWRConfig } from "swr";
+
+const WITH_LOCAL_STORAGE_CACHE = false;
 
 /**
  * FIXME:
@@ -47,7 +50,15 @@ const App: FunctionalComponent = () => {
     <TranslationProvider source={strings}>
       <PageStateProvider>
         <BackendStateProvider>
-          <Routing />
+          <SWRConfig
+            value={{
+              provider: WITH_LOCAL_STORAGE_CACHE
+                ? localStorageProvider
+                : undefined,
+            }}
+          >
+            <Routing />
+          </SWRConfig>
         </BackendStateProvider>
       </PageStateProvider>
     </TranslationProvider>
@@ -58,4 +69,14 @@ const App: FunctionalComponent = () => {
   return globalLogLevel;
 };
 
+function localStorageProvider(): Map<unknown, unknown> {
+  const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+  window.addEventListener("beforeunload", () => {
+    const appCache = JSON.stringify(Array.from(map.entries()));
+    localStorage.setItem("app-cache", appCache);
+  });
+  return map;
+}
+
 export default App;
diff --git a/packages/demobank-ui/src/context/backend.ts 
b/packages/demobank-ui/src/context/backend.ts
index 58907e565..b462d20e3 100644
--- a/packages/demobank-ui/src/context/backend.ts
+++ b/packages/demobank-ui/src/context/backend.ts
@@ -31,10 +31,10 @@ export type Type = BackendStateHandler;
 
 const initial: Type = {
   state: defaultState,
-  clear() {
+  logOut() {
     null;
   },
-  save(info) {
+  logIn(info) {
     null;
   },
 };
diff --git a/packages/demobank-ui/src/context/pageState.ts 
b/packages/demobank-ui/src/context/pageState.ts
index fd7a6c90c..d5428b9b7 100644
--- a/packages/demobank-ui/src/context/pageState.ts
+++ b/packages/demobank-ui/src/context/pageState.ts
@@ -14,6 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { TranslatedString } from "@gnu-taler/taler-util";
 import { useNotNullLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
 import { ComponentChildren, createContext, h, VNode } from "preact";
 import { StateUpdater, useContext } from "preact/hooks";
@@ -29,7 +30,6 @@ export type Type = {
 };
 const initial: Type = {
   pageState: {
-    isRawPayto: false,
     withdrawalInProgress: false,
   },
   pageStateSetter: () => {
@@ -58,7 +58,6 @@ export const PageStateProvider = ({
  */
 function usePageState(
   state: PageStateType = {
-    isRawPayto: false,
     withdrawalInProgress: false,
   },
 ): [PageStateType, StateUpdater<PageStateType>] {
@@ -92,24 +91,24 @@ function usePageState(
   return [retObj, removeLatestInfo];
 }
 
+export type ErrorMessage = {
+  description?: string;
+  title: TranslatedString;
+  debug?: string;
+}
 /**
  * Track page state.
  */
 export interface PageStateType {
-  isRawPayto: boolean;
-  withdrawalInProgress: boolean;
-  error?: {
-    description?: string;
-    title: string;
-    debug?: string;
-  };
+  error?: ErrorMessage;
+  info?: TranslatedString;
 
-  info?: string;
+  withdrawalInProgress: boolean;
   talerWithdrawUri?: string;
   /**
    * Not strictly a presentational value, could
    * be moved in a future "withdrawal state" object.
    */
   withdrawalId?: string;
-  timestamp?: number;
+
 }
diff --git a/packages/demobank-ui/src/declaration.d.ts 
b/packages/demobank-ui/src/declaration.d.ts
index 29538e44a..cf3eb5774 100644
--- a/packages/demobank-ui/src/declaration.d.ts
+++ b/packages/demobank-ui/src/declaration.d.ts
@@ -30,10 +30,6 @@ declare module "*.png" {
   const content: any;
   export default content;
 }
-declare module "jed" {
-  const x: any;
-  export = x;
-}
 
 /**********************************************
  * Type definitions for states and API calls. *
@@ -73,3 +69,361 @@ interface WireTransferRequestType {
   subject?: string;
   amount?: string;
 }
+
+
+type HashCode = string;
+type EddsaPublicKey = string;
+type EddsaSignature = string;
+type WireTransferIdentifierRawP = string;
+type RelativeTime = Duration;
+type ImageDataUrl = string;
+
+interface WithId {
+  id: string;
+}
+
+interface Timestamp {
+  // Milliseconds since epoch, or the special
+  // value "forever" to represent an event that will
+  // never happen.
+  t_s: number | "never";
+}
+interface Duration {
+  d_us: number | "forever";
+}
+
+interface WithId {
+  id: string;
+}
+
+type Amount = string;
+type UUID = string;
+type Integer = number;
+
+namespace SandboxBackend {
+
+  export interface Config {
+    // Name of this API, always "circuit".
+    name: string;
+    // API version in the form $n:$n:$n
+    version: string;
+    // Contains ratios and fees related to buying
+    // and selling the circuit currency.
+    ratios_and_fees: RatiosAndFees;
+  }
+  interface RatiosAndFees {
+    // Exchange rate to buy the circuit currency from fiat.
+    buy_at_ratio: number;
+    // Exchange rate to sell the circuit currency for fiat.
+    sell_at_ratio: number;
+    // Fee to subtract after applying the buy ratio.
+    buy_in_fee: number;
+    // Fee to subtract after applying the sell ratio.
+    sell_out_fee: number;
+  }
+
+  export interface SandboxError {
+    error: SandboxErrorDetail;
+  }
+  interface SandboxErrorDetail {
+
+    // String enum classifying the error.
+    type: ErrorType;
+
+    // Human-readable error description.
+    description: string;
+  }
+  enum ErrorType {
+    /**
+     * This error can be related to a business operation,
+     * a non-existent object requested by the client, or
+     * even when the bank itself fails.
+     */
+    SandboxError = "sandbox-error",
+
+    /**
+     * It is the error type thrown by helper functions
+     * from the Util library.  Those are used by both
+     * Sandbox and Nexus, therefore the actual meaning
+     * must be carried by the error 'message' field.
+     */
+    UtilError = "util-error"
+  }
+
+  namespace Access {
+
+    interface PublicAccountsResponse {
+      publicAccounts: PublicAccount[]
+    }
+    interface PublicAccount {
+      iban: string;
+      balance: string;
+      // The account name _and_ the username of the
+      // Sandbox customer that owns such a bank account.
+      accountLabel: string;
+    }
+
+    interface BankAccountBalanceResponse {
+      // Available balance on the account.
+      balance: {
+        amount: Amount;
+        credit_debit_indicator: "credit" | "debit";
+      };
+      // payto://-URI of the account. (New)
+      paytoUri: string;
+    }
+    interface BankAccountCreateWithdrawalRequest {
+      // Amount to withdraw.
+      amount: Amount;
+    }
+    interface BankAccountCreateWithdrawalResponse {
+      // ID of the withdrawal, can be used to view/modify the withdrawal 
operation.
+      withdrawal_id: string;
+
+      // URI that can be passed to the wallet to initiate the withdrawal.
+      taler_withdraw_uri: string;
+    }
+    interface BankAccountGetWithdrawalResponse {
+      // Amount that will be withdrawn with this withdrawal operation.
+      amount: Amount;
+
+      // Was the withdrawal aborted?
+      aborted: boolean;
+
+      // Has the withdrawal been confirmed by the bank?
+      // The wire transfer for a withdrawal is only executed once
+      // both confirmation_done is true and selection_done is true.
+      confirmation_done: boolean;
+
+      // Did the wallet select reserve details?
+      selection_done: boolean;
+
+      // Reserve public key selected by the exchange,
+      // only non-null if selection_done is true.
+      selected_reserve_pub: string | null;
+
+      // Exchange account selected by the wallet, or by the bank
+      // (with the default exchange) in case the wallet did not provide one
+      // through the Integration API.
+      selected_exchange_account: string | null;
+    }
+
+    interface BankAccountTransactionsResponse {
+      transactions: BankAccountTransactionInfo[];
+    }
+
+    interface BankAccountTransactionInfo {
+
+      creditorIban: string;
+      creditorBic: string; // Optional
+      creditorName: string;
+
+      debtorIban: string;
+      debtorBic: string;
+      debtorName: string;
+
+      amount: number;
+      currency: string;
+      subject: string;
+
+      // Transaction unique ID.  Matches
+      // $transaction_id from the URI.
+      uid: string;
+      direction: "DBIT" | "CRDT";
+      date: string; // milliseconds since the Unix epoch
+    }
+    interface CreateBankAccountTransactionCreate {
+
+      // Address in the Payto format of the wire transfer receiver.
+      // It needs at least the 'message' query string parameter.
+      paytoUri: string;
+
+      // Transaction amount (in the $currency:x.y format), optional.
+      // However, when not given, its value must occupy the 'amount'
+      // query string parameter of the 'payto' field.  In case it
+      // is given in both places, the paytoUri's takes the precedence.
+      amount?: string;
+    }
+
+    interface BankRegistrationRequest {
+      username: string;
+
+      password: string;
+    }
+
+  }
+
+  namespace Circuit {
+    interface CircuitAccountRequest {
+      // Username
+      username: string;
+
+      // Password.
+      password: string;
+
+      // Addresses where to send the TAN.  If
+      // this field is missing, then the cashout
+      // won't succeed.
+      contact_data: CircuitContactData;
+
+      // Legal subject owning the account.
+      name: string;
+
+      // 'payto' address pointing the bank account
+      // where to send payments, in case the user
+      // wants to convert the local currency back
+      // to fiat.
+      cashout_address: string;
+
+      // IBAN of this bank account, which is therefore
+      // internal to the circuit.  Randomly generated,
+      // when it is not given.
+      internal_iban?: string;
+    }
+    interface CircuitContactData {
+
+      // E-Mail address
+      email?: string;
+
+      // Phone number.
+      phone?: string;
+    }
+    interface CircuitAccountReconfiguration {
+
+      // Addresses where to send the TAN.
+      contact_data: CircuitContactData;
+
+      // 'payto' address pointing the bank account
+      // where to send payments, in case the user
+      // wants to convert the local currency back
+      // to fiat.
+      cashout_address: string;
+    }
+    interface AccountPasswordChange {
+
+      // New password.
+      new_password: string;
+    }
+
+    interface CircuitAccounts {
+      customers: CircuitAccountMinimalData[];
+    }
+    interface CircuitAccountMinimalData {
+      // Username
+      username: string;
+
+      // Legal subject owning the account.
+      name: string;
+
+    }
+
+    interface CircuitAccountData {
+      // Username
+      username: string;
+
+      // IBAN hosted at Libeufin Sandbox
+      iban: string;
+
+      contact_data: CircuitContactData;
+
+      // Legal subject owning the account.
+      name: string;
+
+      // 'payto' address pointing the bank account
+      // where to send cashouts.
+      cashout_address: string;
+    }
+    enum TanChannel {
+      SMS = "sms",
+      EMAIL = "email",
+      FILE = "file"
+    }
+    interface CashoutRequest {
+
+      // Optional subject to associate to the
+      // cashout operation.  This data will appear
+      // as the incoming wire transfer subject in
+      // the user's external bank account.
+      subject?: string;
+
+      // That is the plain amount that the user specified
+      // to cashout.  Its $currency is the circuit currency.
+      amount_debit: Amount;
+
+      // That is the amount that will effectively be
+      // transferred by the bank to the user's bank
+      // account, that is external to the circuit.
+      // It is expressed in the fiat currency and
+      // is calculated after the cashout fee and the
+      // exchange rate.  See the /cashout-rates call.
+      amount_credit: Amount;
+
+      // Which channel the TAN should be sent to.  If
+      // this field is missing, it defaults to SMS.
+      // The default choice prefers to change the communication
+      // channel respect to the one used to issue this request.
+      tan_channel?: TanChannel;
+    }
+    interface CashoutPending {
+      // UUID identifying the operation being created
+      // and now waiting for the TAN confirmation.
+      uuid: string;
+    }
+    interface CashoutConfirm {
+
+      // the TAN that confirms $cashoutId.
+      tan: string;
+    }
+    interface Config {
+      // Name of this API, always "circuit".
+      name: string;
+      // API version in the form $n:$n:$n
+      version: string;
+      // Contains ratios and fees related to buying
+      // and selling the circuit currency.
+      ratios_and_fees: RatiosAndFees;
+    }
+    interface RatiosAndFees {
+      // Exchange rate to buy the circuit currency from fiat.
+      buy_at_ratio: float;
+      // Exchange rate to sell the circuit currency for fiat.
+      sell_at_ratio: float;
+      // Fee to subtract after applying the buy ratio.
+      buy_in_fee: float;
+      // Fee to subtract after applying the sell ratio.
+      sell_out_fee: float;
+    }
+    interface Cashouts {
+      // Every string represents a cash-out operation UUID.
+      cashouts: string[];
+    }
+    interface CashoutStatusResponse {
+
+      status: CashoutStatus;
+      // Amount debited to the circuit bank account.
+      amount_debit: Amount;
+      // Amount credited to the external bank account.
+      amount_credit: Amount;
+      // Transaction subject.
+      subject: string;
+      // Circuit bank account that created the cash-out.
+      account: string;
+      // Time when the cash-out was created.
+      creation_time: number; // milliseconds since the Unix epoch
+      // Time when the cash-out was confirmed via its TAN.
+      // Missing or null, when the operation wasn't confirmed yet.
+      confirmation_time?: number | null; // milliseconds since the Unix epoch
+    }
+    enum CashoutStatus {
+
+      // The payment was initiated after a valid
+      // TAN was received by the bank.
+      CONFIRMED = "confirmed",
+
+      // The cashout was created and now waits
+      // for the TAN by the author.
+      PENDING = "pending",
+    }
+  }
+
+}
diff --git a/packages/demobank-ui/src/hooks/access.ts 
b/packages/demobank-ui/src/hooks/access.ts
new file mode 100644
index 000000000..4d4574dac
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -0,0 +1,330 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 useSWR from "swr";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
+import { useEffect, useState } from "preact/hooks";
+import {
+  HttpError,
+  HttpResponse,
+  HttpResponseOk,
+  HttpResponsePaginated,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { useAuthenticatedBackend, useMatchMutate, usePublicBackend } from 
"./backend.js";
+import { useBackendContext } from "../context/backend.js";
+
+export function useAccessAPI(): AccessAPI {
+  const mutateAll = useMatchMutate();
+  const { request } = useAuthenticatedBackend();
+  const { state } = useBackendContext()
+  if (state.status === "loggedOut") {
+    throw Error("access-api can't be used when the user is not logged In")
+  }
+  const account = state.username
+
+  const createWithdrawal = async (
+    data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+  ): 
Promise<HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>>
 => {
+    const res = await 
request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(`access-api/accounts/${account}/withdrawals`,
 {
+      method: "POST",
+      data,
+      contentType: "json"
+    });
+    return res;
+  };
+  const abortWithdrawal = async (
+    id: string,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await 
request<void>(`access-api/accounts/${account}/withdrawals/${id}`, {
+      method: "POST",
+      contentType: "json"
+    });
+    await mutateAll(/.*accounts\/.*\/withdrawals\/.*/);
+    return res;
+  };
+  const confirmWithdrawal = async (
+    id: string,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await 
request<void>(`access-api/accounts/${account}/withdrawals/${id}`, {
+      method: "POST",
+      contentType: "json"
+    });
+    await mutateAll(/.*accounts\/.*\/withdrawals\/.*/);
+    return res;
+  };
+  const createTransaction = async (
+    data: SandboxBackend.Access.CreateBankAccountTransactionCreate
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await 
request<void>(`access-api/accounts/${account}/transactions`, {
+      method: "POST",
+      data,
+      contentType: "json"
+    });
+    await mutateAll(/.*accounts\/.*\/transactions.*/);
+    return res;
+  };
+  const deleteAccount = async (
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`access-api/accounts/${account}`, {
+      method: "DELETE",
+      contentType: "json"
+    });
+    await mutateAll(/.*accounts\/.*/);
+    return res;
+  };
+
+  return { abortWithdrawal, confirmWithdrawal, createWithdrawal, 
createTransaction, deleteAccount };
+}
+
+export function useTestingAPI(): TestingAPI {
+  const mutateAll = useMatchMutate();
+  const { request: noAuthRequest } = usePublicBackend();
+  const register = async (
+    data: SandboxBackend.Access.BankRegistrationRequest
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await noAuthRequest<void>(`access-api/testing/register`, {
+      method: "POST",
+      data,
+      contentType: "json"
+    });
+    await mutateAll(/.*accounts\/.*/);
+    return res;
+  };
+
+  return { register };
+}
+
+
+export interface TestingAPI {
+  register: (
+    data: SandboxBackend.Access.BankRegistrationRequest
+  ) => Promise<HttpResponseOk<void>>;
+}
+
+export interface AccessAPI {
+  createWithdrawal: (
+    data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+  ) => 
Promise<HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>>;
+  abortWithdrawal: (
+    wid: string,
+  ) => Promise<HttpResponseOk<void>>;
+  confirmWithdrawal: (
+    wid: string
+  ) => Promise<HttpResponseOk<void>>;
+  createTransaction: (
+    data: SandboxBackend.Access.CreateBankAccountTransactionCreate
+  ) => Promise<HttpResponseOk<void>>;
+  deleteAccount: () => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceTemplateFilter {
+  //FIXME: add filter to the template list
+  position?: string;
+}
+
+
+export function useAccountDetails(account: string): 
HttpResponse<SandboxBackend.Access.BankAccountBalanceResponse, 
SandboxBackend.SandboxError> {
+  const { fetcher } = useAuthenticatedBackend();
+
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>,
+    HttpError<SandboxBackend.SandboxError>
+  >([`access-api/accounts/${account}`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  if (data) return data;
+  if (error) return error;
+  return { loading: true };
+}
+
+// FIXME: should poll
+export function useWithdrawalDetails(account: string, wid: string): 
HttpResponse<SandboxBackend.Access.BankAccountGetWithdrawalResponse, 
SandboxBackend.SandboxError> {
+  const { fetcher } = useAuthenticatedBackend();
+
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
+    HttpError<SandboxBackend.SandboxError>
+  >([`access-api/accounts/${account}/withdrawals/${wid}`], fetcher, {
+    refreshInterval: 1000,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+
+  });
+
+  // if (isValidating) return { loading: true, data: data?.data };
+  if (data) return data;
+  if (error) return error;
+  return { loading: true };
+}
+
+export function useTransactionDetails(account: string, tid: string): 
HttpResponse<SandboxBackend.Access.BankAccountTransactionInfo, 
SandboxBackend.SandboxError> {
+  const { fetcher } = useAuthenticatedBackend();
+
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>,
+    HttpError<SandboxBackend.SandboxError>
+  >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  // if (isValidating) return { loading: true, data: data?.data };
+  if (data) return data;
+  if (error) return error;
+  return { loading: true };
+}
+
+interface PaginationFilter {
+  page: number,
+}
+
+export function usePublicAccounts(
+  args?: PaginationFilter,
+): HttpResponsePaginated<SandboxBackend.Access.PublicAccountsResponse, 
SandboxBackend.SandboxError> {
+  const { paginatedFetcher } = usePublicBackend();
+
+  const [page, setPage] = useState(1);
+
+  const {
+    data: afterData,
+    error: afterError,
+    isValidating: loadingAfter,
+  } = useSWR<
+    HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>,
+    HttpError<SandboxBackend.SandboxError>
+  >([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
+
+  const [lastAfter, setLastAfter] = useState<
+    HttpResponse<SandboxBackend.Access.PublicAccountsResponse, 
SandboxBackend.SandboxError>
+  >({ loading: true });
+
+  useEffect(() => {
+    if (afterData) setLastAfter(afterData);
+  }, [afterData]);
+
+  if (afterError) return afterError;
+
+  // if the query returns less that we ask, then we have reach the end or 
beginning
+  const isReachingEnd =
+    afterData && afterData.data.publicAccounts.length < PAGE_SIZE;
+  const isReachingStart = false;
+
+  const pagination = {
+    isReachingEnd,
+    isReachingStart,
+    loadMore: () => {
+      if (!afterData || isReachingEnd) return;
+      if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) {
+        setPage(page + 1);
+      }
+    },
+    loadMorePrev: () => {
+      null
+    },
+  };
+
+  const publicAccounts = !afterData ? [] : (afterData || 
lastAfter).data.publicAccounts;
+  if (loadingAfter)
+    return { loading: true, data: { publicAccounts } };
+  if (afterData) {
+    return { ok: true, data: { publicAccounts }, ...pagination };
+  }
+  return { loading: true };
+}
+
+
+/**
+ * FIXME: mutate result when balance change (transaction )
+ * @param account 
+ * @param args 
+ * @returns 
+ */
+export function useTransactions(
+  account: string,
+  args?: PaginationFilter,
+): 
HttpResponsePaginated<SandboxBackend.Access.BankAccountTransactionsResponse, 
SandboxBackend.SandboxError> {
+  const { paginatedFetcher } = useAuthenticatedBackend();
+
+  const [page, setPage] = useState(1);
+
+  const {
+    data: afterData,
+    error: afterError,
+    isValidating: loadingAfter,
+  } = useSWR<
+    HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>,
+    HttpError<SandboxBackend.SandboxError>
+  >([`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], 
paginatedFetcher);
+
+  const [lastAfter, setLastAfter] = useState<
+    HttpResponse<SandboxBackend.Access.BankAccountTransactionsResponse, 
SandboxBackend.SandboxError>
+  >({ loading: true });
+
+  useEffect(() => {
+    if (afterData) setLastAfter(afterData);
+  }, [afterData]);
+
+  if (afterError) return afterError;
+
+  // if the query returns less that we ask, then we have reach the end or 
beginning
+  const isReachingEnd =
+    afterData && afterData.data.transactions.length < PAGE_SIZE;
+  const isReachingStart = false;
+
+  const pagination = {
+    isReachingEnd,
+    isReachingStart,
+    loadMore: () => {
+      if (!afterData || isReachingEnd) return;
+      if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+        setPage(page + 1);
+      }
+    },
+    loadMorePrev: () => {
+      null
+    },
+  };
+
+  const transactions = !afterData ? [] : (afterData || 
lastAfter).data.transactions;
+  if (loadingAfter)
+    return { loading: true, data: { transactions } };
+  if (afterData) {
+    return { ok: true, data: { transactions }, ...pagination };
+  }
+  return { loading: true };
+}
diff --git a/packages/demobank-ui/src/hooks/async.ts 
b/packages/demobank-ui/src/hooks/async.ts
index 6492b7729..b968cfb84 100644
--- a/packages/demobank-ui/src/hooks/async.ts
+++ b/packages/demobank-ui/src/hooks/async.ts
@@ -62,7 +62,6 @@ export function useAsync<T>(
   };
 
   function cancel() {
-    // cancelPendingRequest()
     setLoading(false);
     setSlow(false);
   }
diff --git a/packages/demobank-ui/src/hooks/backend.ts 
b/packages/demobank-ui/src/hooks/backend.ts
index 13a158f4f..f4f5ecfd0 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -14,7 +14,17 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
 import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpResponse,
+  HttpResponseOk,
+  RequestOptions,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { useApiContext } from "@gnu-taler/web-util/lib/index.browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useSWRConfig } from "swr";
+import { useBackendContext } from "../context/backend.js";
 
 /**
  * Has the information to reach and
@@ -22,25 +32,38 @@ import { useLocalStorage } from 
"@gnu-taler/web-util/lib/index.browser";
  */
 export type BackendState = LoggedIn | LoggedOut;
 
-export interface BackendInfo {
-  url: string;
+export interface BackendCredentials {
   username: string;
   password: string;
 }
 
-interface LoggedIn extends BackendInfo {
+interface LoggedIn extends BackendCredentials {
+  url: string;
   status: "loggedIn";
+  isUserAdministrator: boolean;
 }
 interface LoggedOut {
+  url: string;
   status: "loggedOut";
 }
 
-export const defaultState: BackendState = { status: "loggedOut" };
+const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/";;
+
+export function getInitialBackendBaseURL(): string {
+  const overrideUrl = localStorage.getItem("bank-base-url");
+
+  return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath);
+}
+
+export const defaultState: BackendState = {
+  status: "loggedOut",
+  url: getInitialBackendBaseURL()
+};
 
 export interface BackendStateHandler {
   state: BackendState;
-  clear(): void;
-  save(info: BackendInfo): void;
+  logOut(): void;
+  logIn(info: BackendCredentials): void;
 }
 /**
  * Return getters and setters for
@@ -52,7 +75,7 @@ export function useBackendState(): BackendStateHandler {
     "backend-state",
     JSON.stringify(defaultState),
   );
-  // const parsed = value !== undefined ? JSON.parse(value) : value;
+
   let parsed;
   try {
     parsed = JSON.parse(value!);
@@ -63,12 +86,162 @@ export function useBackendState(): BackendStateHandler {
 
   return {
     state,
-    clear() {
-      update(JSON.stringify(defaultState));
+    logOut() {
+      update(JSON.stringify({ ...defaultState, url: state.url }));
     },
-    save(info) {
-      const nextState: BackendState = { status: "loggedIn", ...info };
+    logIn(info) {
+      //admin is defined by the username
+      const nextState: BackendState = { status: "loggedIn", url: state.url, 
...info, isUserAdministrator: info.username === "admin" };
       update(JSON.stringify(nextState));
     },
   };
 }
+
+interface useBackendType {
+  request: <T>(
+    path: string,
+    options?: RequestOptions,
+  ) => Promise<HttpResponseOk<T>>;
+  fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+  multiFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>[]>;
+  paginatedFetcher: <T>(args: [string, number, number]) => 
Promise<HttpResponseOk<T>>;
+  sandboxAccountsFetcher: <T>(args: [string, number, number, string]) => 
Promise<HttpResponseOk<T>>;
+}
+
+
+export function usePublicBackend(): useBackendType {
+  const { state } = useBackendContext();
+  const { request: requestHandler } = useApiContext();
+
+  const baseUrl = state.url
+
+  const request = useCallback(
+    function requestImpl<T>(
+      path: string,
+      options: RequestOptions = {},
+    ): Promise<HttpResponseOk<T>> {
+
+      return requestHandler<T>(baseUrl, path, options);
+    },
+    [baseUrl],
+  );
+
+  const fetcher = useCallback(
+    function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint);
+    },
+    [baseUrl],
+  );
+  const paginatedFetcher = useCallback(
+    function fetcherImpl<T>([endpoint, page, size]: [string, number, number]): 
Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, 
size } });
+    },
+    [baseUrl],
+  );
+  const multiFetcher = useCallback(
+    function multiFetcherImpl<T>(
+      endpoints: string[],
+    ): Promise<HttpResponseOk<T>[]> {
+      return Promise.all(
+        endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)),
+      );
+    },
+    [baseUrl],
+  );
+  const sandboxAccountsFetcher = useCallback(
+    function fetcherImpl<T>([endpoint, page, size, account]: [string, number, 
number, string]): Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, 
size } });
+    },
+    [baseUrl],
+  );
+  return { request, fetcher, paginatedFetcher, multiFetcher, 
sandboxAccountsFetcher };
+}
+
+export function useAuthenticatedBackend(): useBackendType {
+  const { state } = useBackendContext();
+  const { request: requestHandler } = useApiContext();
+
+  const creds = state.status === "loggedIn" ? state : undefined
+  const baseUrl = state.url
+
+  const request = useCallback(
+    function requestImpl<T>(
+      path: string,
+      options: RequestOptions = {},
+    ): Promise<HttpResponseOk<T>> {
+
+      return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options 
});
+    },
+    [baseUrl, creds],
+  );
+
+  const fetcher = useCallback(
+    function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds });
+    },
+    [baseUrl, creds],
+  );
+  const paginatedFetcher = useCallback(
+    function fetcherImpl<T>([endpoint, page = 0, size]: [string, number, 
number]): Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: 
{ page, size } });
+    },
+    [baseUrl, creds],
+  );
+  const multiFetcher = useCallback(
+    function multiFetcherImpl<T>(
+      endpoints: string[],
+    ): Promise<HttpResponseOk<T>[]> {
+      return Promise.all(
+        endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint, { 
basicAuth: creds })),
+      );
+    },
+    [baseUrl, creds],
+  );
+  const sandboxAccountsFetcher = useCallback(
+    function fetcherImpl<T>([endpoint, page, size, account]: [string, number, 
number, string]): Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: 
{ page: page || 1, size } });
+    },
+    [baseUrl],
+  );
+  return { request, fetcher, paginatedFetcher, multiFetcher, 
sandboxAccountsFetcher };
+}
+
+export function useBackendConfig(): HttpResponse<SandboxBackend.Config, 
SandboxBackend.SandboxError> {
+  const { request } = usePublicBackend();
+
+  type Type = SandboxBackend.Config;
+
+  const [result, setResult] = useState<HttpResponse<Type, 
SandboxBackend.SandboxError>>({ loading: true });
+
+  useEffect(() => {
+    request<Type>(`/config`)
+      .then((data) => setResult(data))
+      .catch((error) => setResult(error));
+  }, [request]);
+
+  return result;
+}
+
+export function useMatchMutate(): (
+  re: RegExp,
+  value?: unknown,
+) => Promise<any> {
+  const { cache, mutate } = useSWRConfig();
+
+  if (!(cache instanceof Map)) {
+    throw new Error(
+      "matchMutate requires the cache provider to be a Map instance",
+    );
+  }
+
+  return function matchRegexMutate(re: RegExp, value?: unknown) {
+    const allKeys = Array.from(cache.keys());
+    const keys = allKeys.filter((key) => re.test(key));
+    const mutations = keys.map((key) => {
+      mutate(key, value, true);
+    });
+    return Promise.all(mutations);
+  };
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
new file mode 100644
index 000000000..6e9ada601
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -0,0 +1,317 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 {
+  HttpError,
+  HttpResponse,
+  HttpResponseOk,
+  HttpResponsePaginated,
+  RequestError
+} from "@gnu-taler/web-util/lib/index.browser";
+import { useEffect, useMemo, useState } from "preact/hooks";
+import useSWR from "swr";
+import { useBackendContext } from "../context/backend.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
+import { useAuthenticatedBackend } from "./backend.js";
+
+export function useAdminAccountAPI(): AdminAccountAPI {
+  const { request } = useAuthenticatedBackend();
+  const { state } = useBackendContext()
+  if (state.status === "loggedOut") {
+    throw Error("access-api can't be used when the user is not logged In")
+  }
+
+  const createAccount = async (
+    data: SandboxBackend.Circuit.CircuitAccountRequest,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/accounts`, {
+      method: "POST",
+      data,
+      contentType: "json"
+    });
+    return res;
+  };
+
+  const updateAccount = async (
+    account: string,
+    data: SandboxBackend.Circuit.CircuitAccountReconfiguration,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/accounts/${account}`, {
+      method: "PATCH",
+      data,
+      contentType: "json"
+    });
+    return res;
+  };
+  const deleteAccount = async (
+    account: string,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/accounts/${account}`, {
+      method: "DELETE",
+      contentType: "json"
+    });
+    return res;
+  };
+  const changePassword = async (
+    account: string,
+    data: SandboxBackend.Circuit.AccountPasswordChange,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/accounts/${account}/auth`, {
+      method: "PATCH",
+      data,
+      contentType: "json"
+    });
+    return res;
+  };
+
+  return { createAccount, deleteAccount, updateAccount, changePassword };
+}
+
+export function useCircuitAccountAPI(): CircuitAccountAPI {
+  const { request } = useAuthenticatedBackend();
+  const { state } = useBackendContext()
+  if (state.status === "loggedOut") {
+    throw Error("access-api can't be used when the user is not logged In")
+  }
+  const account = state.username;
+
+  const updateAccount = async (
+    data: SandboxBackend.Circuit.CircuitAccountReconfiguration,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/accounts/${account}`, {
+      method: "PATCH",
+      data,
+      contentType: "json"
+    });
+    return res;
+  };
+  const changePassword = async (
+    data: SandboxBackend.Circuit.AccountPasswordChange,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/accounts/${account}/auth`, {
+      method: "PATCH",
+      data,
+      contentType: "json"
+    });
+    return res;
+  };
+
+  return { updateAccount, changePassword };
+}
+
+export interface AdminAccountAPI {
+  createAccount: (
+    data: SandboxBackend.Circuit.CircuitAccountRequest,
+  ) => Promise<HttpResponseOk<void>>;
+  deleteAccount: (account: string) => Promise<HttpResponseOk<void>>;
+
+  updateAccount: (
+    account: string,
+    data: SandboxBackend.Circuit.CircuitAccountReconfiguration
+  ) => Promise<HttpResponseOk<void>>;
+  changePassword: (
+    account: string,
+    data: SandboxBackend.Circuit.AccountPasswordChange
+  ) => Promise<HttpResponseOk<void>>;
+}
+
+export interface CircuitAccountAPI {
+  updateAccount: (
+    data: SandboxBackend.Circuit.CircuitAccountReconfiguration
+  ) => Promise<HttpResponseOk<void>>;
+  changePassword: (
+    data: SandboxBackend.Circuit.AccountPasswordChange
+  ) => Promise<HttpResponseOk<void>>;
+}
+
+
+export interface InstanceTemplateFilter {
+  //FIXME: add filter to the template list
+  position?: string;
+}
+
+
+export function useMyAccountDetails(): 
HttpResponse<SandboxBackend.Circuit.CircuitAccountData, 
SandboxBackend.SandboxError> {
+  const { fetcher } = useAuthenticatedBackend();
+  const { state } = useBackendContext()
+  if (state.status === "loggedOut") {
+    throw Error("can't access my-account-details when logged out")
+  }
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>,
+    HttpError<SandboxBackend.SandboxError>
+  >([`accounts/${state.username}`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  if (data) return data;
+  if (error) return error;
+  return { loading: true };
+}
+
+export function useAccountDetails(account: string): 
HttpResponse<SandboxBackend.Circuit.CircuitAccountData, 
SandboxBackend.SandboxError> {
+  const { fetcher } = useAuthenticatedBackend();
+
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>,
+    RequestError<SandboxBackend.SandboxError>
+  >([`circuit-api/accounts/${account}`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  // if (isValidating) return { loading: true, data: data?.data };
+  if (data) return data;
+  if (error) return error.info;
+  return { loading: true };
+}
+
+interface PaginationFilter {
+  account?: string,
+  page?: number,
+}
+
+export function useAccounts(
+  args?: PaginationFilter,
+): HttpResponsePaginated<SandboxBackend.Circuit.CircuitAccounts, 
SandboxBackend.SandboxError> {
+  const { sandboxAccountsFetcher } = useAuthenticatedBackend();
+  const [page, setPage] = useState(0);
+
+  const {
+    data: afterData,
+    error: afterError,
+    // isValidating: loadingAfter,
+  } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>,
+    RequestError<SandboxBackend.SandboxError>
+  >([`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], 
sandboxAccountsFetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  // const [lastAfter, setLastAfter] = useState<
+  //   HttpResponse<SandboxBackend.Circuit.CircuitAccounts, 
SandboxBackend.SandboxError>
+  // >({ loading: true });
+
+  // useEffect(() => {
+  //   if (afterData) setLastAfter(afterData);
+  // }, [afterData]);
+
+  // if the query returns less that we ask, then we have reach the end or 
beginning
+  const isReachingEnd =
+    afterData && afterData.data?.customers?.length < PAGE_SIZE;
+  const isReachingStart = false;
+
+  const pagination = {
+    isReachingEnd,
+    isReachingStart,
+    loadMore: () => {
+      if (!afterData || isReachingEnd) return;
+      if (afterData.data?.customers?.length < MAX_RESULT_SIZE) {
+        setPage(page + 1);
+      }
+    },
+    loadMorePrev: () => {
+      null
+    },
+  };
+
+  const result = useMemo(() => {
+    const customers = !afterData ? [] : (afterData)?.data?.customers ?? [];
+    return { ok: true as const, data: { customers }, ...pagination }
+  }, [afterData?.data])
+
+  if (afterError) return afterError.info;
+  if (afterData) {
+    return result
+  }
+
+  // if (loadingAfter)
+  //   return { loading: true, data: { customers } };
+  // if (afterData) {
+  //   return { ok: true, data: { customers }, ...pagination };
+  // }
+  return { loading: true };
+}
+
+export function useCashouts(): HttpResponse<
+  (SandboxBackend.Circuit.CashoutStatusResponse & WithId)[],
+  SandboxBackend.SandboxError
+> {
+  const { fetcher, multiFetcher } = useAuthenticatedBackend();
+
+  const { data: list, error: listError } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.Cashouts>,
+    RequestError<SandboxBackend.SandboxError>
+  >([`circuit-api/cashouts`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+  });
+
+  const paths = (list?.data.cashouts || []).map(
+    (cashoutId) => `circuit-api/cashouts/${cashoutId}`,
+  );
+  const { data: cashouts, error: productError } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.CashoutStatusResponse>[],
+    RequestError<SandboxBackend.SandboxError>
+  >([paths], multiFetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+  });
+
+  if (listError) return listError.info;
+  if (productError) return productError.info;
+
+  if (cashouts) {
+    const dataWithId = cashouts.map((d) => {
+      //take the id from the queried url
+      return {
+        ...d.data,
+        id: d.info?.url.replace(/.*\/cashouts\//, "") || "",
+      };
+    });
+    return { ok: true, data: dataWithId };
+  }
+  return { loading: true };
+}
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx 
b/packages/demobank-ui/src/pages/AccountPage.tsx
index 8d29bd933..769e85804 100644
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage.tsx
@@ -14,206 +14,52 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { useEffect } from "preact/hooks";
-import useSWR, { SWRConfig, useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { BackendInfo } from "../hooks/backend.js";
-import { bankUiSettings } from "../settings.js";
-import { getIbanFromPayto, prepareHeaders } from "../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaymentOptions } from "./PaymentOptions.js";
+import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
+import {
+  HttpResponsePaginated,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Cashouts } from "../components/Cashouts/index.js";
 import { Transactions } from "../components/Transactions/index.js";
-import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
-
-export function AccountPage(): VNode {
-  const backend = useBackendContext();
-  const { i18n } = useTranslationContext();
-
-  if (backend.state.status === "loggedOut") {
-    return (
-      <BankFrame>
-        <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
-        <LoginForm />
-      </BankFrame>
-    );
-  }
-
-  return (
-    <SWRWithCredentials info={backend.state}>
-      <Account accountLabel={backend.state.username} />
-    </SWRWithCredentials>
-  );
-}
-
-/**
- * Factor out login credentials.
- */
-function SWRWithCredentials({
-  children,
-  info,
-}: {
-  children: ComponentChildren;
-  info: BackendInfo;
-}): VNode {
-  const { username, password, url: backendUrl } = info;
-  const headers = prepareHeaders(username, password);
-  return (
-    <SWRConfig
-      value={{
-        fetcher: (url: string) => {
-          return fetch(new URL(url, backendUrl).href, { headers }).then((r) => 
{
-            if (!r.ok) throw { status: r.status, json: r.json() };
+import { useAccountDetails } from "../hooks/access.js";
+import { PaymentOptions } from "./PaymentOptions.js";
 
-            return r.json();
-          });
-        },
-      }}
-    >
-      {children as any}
-    </SWRConfig>
-  );
+interface Props {
+  account: string;
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
 }
-
-const logger = new Logger("AccountPage");
-
 /**
- * Show only the account's balance.  NOTE: the backend state
- * is mostly needed to provide the user's credentials to POST
- * to the bank.
+ * Query account information and show QR code if there is pending withdrawal
  */
-function Account({ accountLabel }: { accountLabel: string }): VNode {
-  const { cache } = useSWRConfig();
-
-  // Getting the bank account balance:
-  const endpoint = `access-api/accounts/${accountLabel}`;
-  const { data, error, mutate } = useSWR(endpoint, {
-    // refreshInterval: 0,
-    // revalidateIfStale: false,
-    // revalidateOnMount: false,
-    // revalidateOnFocus: false,
-    // revalidateOnReconnect: false,
-  });
-  const backend = useBackendContext();
-  const { pageState, pageStateSetter: setPageState } = usePageContext();
-  const { withdrawalId, talerWithdrawUri, timestamp } = pageState;
+export function AccountPage({ account, onLoadNotOk }: Props): VNode {
+  const result = useAccountDetails(account);
   const { i18n } = useTranslationContext();
-  useEffect(() => {
-    mutate();
-  }, [timestamp]);
 
-  /**
-   * This part shows a list of transactions: with 5 elements by
-   * default and offers a "load more" button.
-   */
-  // const [txPageNumber, setTxPageNumber] = useTransactionPageNumber();
-  // const txsPages = [];
-  // for (let i = 0; i <= txPageNumber; i++) {
-  //   txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} 
/>);
-  // }
-
-  if (typeof error !== "undefined") {
-    logger.error("account error", error, endpoint);
-    /**
-     * FIXME: to minimize the code, try only one invocation
-     * of pageStateSetter, after having decided the error
-     * message in the case-branch.
-     */
-    switch (error.status) {
-      case 404: {
-        backend.clear();
-        setPageState((prevState: PageStateType) => ({
-          ...prevState,
-
-          error: {
-            title: i18n.str`Username or account label '${accountLabel}' not 
found.  Won't login.`,
-          },
-        }));
-
-        /**
-         * 404 should never stick to the cache, because they
-         * taint successful future registrations.  How?  After
-         * registering, the user gets navigated to this page,
-         * therefore a previous 404 on this SWR key (the requested
-         * resource) would still appear as valid and cause this
-         * page not to be shown! A typical case is an attempted
-         * login of a unregistered user X, and then a registration
-         * attempt of the same user X: in this case, the failed
-         * login would cache a 404 error to X's profile, resulting
-         * in the legitimate request after the registration to still
-         * be flagged as 404.  Clearing the cache should prevent
-         * this.  */
-        (cache as any).clear();
-        return <p>Profile not found...</p>;
-      }
-      case HttpStatusCode.Unauthorized:
-      case HttpStatusCode.Forbidden: {
-        backend.clear();
-        setPageState((prevState: PageStateType) => ({
-          ...prevState,
-          error: {
-            title: i18n.str`Wrong credentials given.`,
-          },
-        }));
-        return <p>Wrong credentials...</p>;
-      }
-      default: {
-        backend.clear();
-        setPageState((prevState: PageStateType) => ({
-          ...prevState,
-          error: {
-            title: i18n.str`Account information could not be retrieved.`,
-            debug: JSON.stringify(error),
-          },
-        }));
-        return <p>Unknown problem...</p>;
-      }
-    }
+  if (!result.ok) {
+    return onLoadNotOk(result);
   }
-  const balance = !data ? undefined : Amounts.parse(data.balance.amount);
-  const errorParsingBalance = data && !balance;
-  const accountNumber = !data ? undefined : getIbanFromPayto(data.paytoUri);
-  const balanceIsDebit = data && data.balance.credit_debit_indicator == 
"debit";
 
-  /**
-   * This block shows the withdrawal QR code.
-   *
-   * A withdrawal operation replaces everything in the page and
-   * (ToDo:) starts polling the backend until either the wallet
-   * selected a exchange and reserve public key, or a error / abort
-   * happened.
-   *
-   * After reaching one of the above states, the user should be
-   * brought to this ("Account") page where they get informed about
-   * the outcome.
-   */
-  if (talerWithdrawUri && withdrawalId) {
-    logger.trace("Bank created a new Taler withdrawal");
+  const { data } = result;
+  const balance = Amounts.parse(data.balance.amount);
+  const errorParsingBalance = !balance;
+  const payto = parsePaytoUri(data.paytoUri);
+  if (!payto || !payto.isKnown || payto.targetType !== "iban") {
     return (
-      <BankFrame>
-        <WithdrawalQRCode
-          withdrawalId={withdrawalId}
-          talerWithdrawUri={talerWithdrawUri}
-        />
-      </BankFrame>
+      <div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
     );
   }
-  const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
+  const accountNumber = payto.iban;
+  const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
 
   return (
-    <BankFrame>
+    <Fragment>
       <div>
         <h1 class="nav welcome-text">
           <i18n.Translate>
             Welcome,
-            {accountNumber
-              ? `${accountLabel} (${accountNumber})`
-              : accountLabel}
-            !
+            {accountNumber ? `${account} (${accountNumber})` : account}!
           </i18n.Translate>
         </h1>
       </div>
@@ -239,7 +85,10 @@ function Account({ accountLabel }: { accountLabel: string 
}): VNode {
               ) : (
                 <div class="large-amount amount">
                   {balanceIsDebit ? <b>-</b> : null}
-                  <span class="value">{`${balanceValue}`}</span>&nbsp;
+                  <span class="value">{`${Amounts.stringifyValue(
+                    balance,
+                  )}`}</span>
+                  &nbsp;
                   <span class="currency">{`${balance.currency}`}</span>
                 </div>
               )}
@@ -248,34 +97,56 @@ function Account({ accountLabel }: { accountLabel: string 
}): VNode {
           <section id="payments">
             <div class="payments">
               <h2>{i18n.str`Payments`}</h2>
-              <PaymentOptions currency={balance?.currency} />
+              <PaymentOptions currency={balance.currency} />
             </div>
           </section>
         </Fragment>
       )}
-      <section id="main">
-        <article>
-          <h2>{i18n.str`Latest transactions:`}</h2>
-          <Transactions
-            balanceValue={balanceValue}
-            pageNumber={0}
-            accountLabel={accountLabel}
-          />
-        </article>
+
+      <section style={{ marginTop: "2em" }}>
+        <Moves account={account} />
       </section>
-    </BankFrame>
+    </Fragment>
   );
 }
 
-// function useTransactionPageNumber(): [number, StateUpdater<number>] {
-//   const ret = useNotNullLocalStorage("transaction-page", "0");
-//   const retObj = JSON.parse(ret[0]);
-//   const retSetter: StateUpdater<number> = function (val) {
-//     const newVal =
-//       val instanceof Function
-//         ? JSON.stringify(val(retObj))
-//         : JSON.stringify(val);
-//     ret[1](newVal);
-//   };
-//   return [retObj, retSetter];
-// }
+function Moves({ account }: { account: string }): VNode {
+  const [tab, setTab] = useState<"transactions" | "cashouts">("transactions");
+  const { i18n } = useTranslationContext();
+  return (
+    <article>
+      <div class="payments">
+        <div class="tab">
+          <button
+            class={tab === "transactions" ? "tablinks active" : "tablinks"}
+            onClick={(): void => {
+              setTab("transactions");
+            }}
+          >
+            {i18n.str`Transactions`}
+          </button>
+          <button
+            class={tab === "cashouts" ? "tablinks active" : "tablinks"}
+            onClick={(): void => {
+              setTab("cashouts");
+            }}
+          >
+            {i18n.str`Cashouts`}
+          </button>
+        </div>
+        {tab === "transactions" && (
+          <div class="active">
+            <h3>{i18n.str`Latest transactions`}</h3>
+            <Transactions account={account} />
+          </div>
+        )}
+        {tab === "cashouts" && (
+          <div class="active">
+            <h3>{i18n.str`Latest cashouts`}</h3>
+            <Cashouts account={account} />
+          </div>
+        )}
+      </div>
+    </article>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx 
b/packages/demobank-ui/src/pages/AdminPage.tsx
new file mode 100644
index 000000000..9efd37f12
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -0,0 +1,707 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { parsePaytoUri, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  HttpResponsePaginated,
+  RequestError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorMessage, usePageContext } from "../context/pageState.js";
+import {
+  useAccountDetails,
+  useAccounts,
+  useAdminAccountAPI,
+} from "../hooks/circuit.js";
+import {
+  PartialButDefined,
+  undefinedIfEmpty,
+  WithIntermediate,
+} from "../utils.js";
+import { ErrorBanner } from "./BankFrame.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+const charset =
+  "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const upperIdx = charset.indexOf("A");
+
+function randomPassword(): string {
+  const random = Array.from({ length: 16 }).map(() => {
+    return charset.charCodeAt(Math.random() * charset.length);
+  });
+  // first char can't be upper
+  const charIdx = charset.indexOf(String.fromCharCode(random[0]));
+  random[0] =
+    charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
+  return String.fromCharCode(...random);
+}
+
+interface Props {
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+}
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+export function AdminPage({ onLoadNotOk }: Props): VNode {
+  const [account, setAccount] = useState<string | undefined>();
+  const [showDetails, setShowDetails] = useState<string | undefined>();
+  const [updatePassword, setUpdatePassword] = useState<string | undefined>();
+  const [createAccount, setCreateAccount] = useState(false);
+  const { pageStateSetter } = usePageContext();
+
+  function showInfoMessage(info: TranslatedString): void {
+    pageStateSetter((prev) => ({
+      ...prev,
+      info,
+    }));
+  }
+
+  const result = useAccounts({ account });
+  const { i18n } = useTranslationContext();
+
+  if (result.loading) return <div />;
+  if (!result.ok) {
+    return onLoadNotOk(result);
+  }
+
+  const { customers } = result.data;
+
+  if (showDetails) {
+    return (
+      <ShowAccountDetails
+        account={showDetails}
+        onLoadNotOk={onLoadNotOk}
+        onUpdateSuccess={() => {
+          showInfoMessage(i18n.str`Account updated`);
+          setShowDetails(undefined);
+        }}
+        onClear={() => {
+          setShowDetails(undefined);
+        }}
+      />
+    );
+  }
+  if (updatePassword) {
+    return (
+      <UpdateAccountPassword
+        account={updatePassword}
+        onLoadNotOk={onLoadNotOk}
+        onUpdateSuccess={() => {
+          showInfoMessage(i18n.str`Password changed`);
+          setUpdatePassword(undefined);
+        }}
+        onClear={() => {
+          setUpdatePassword(undefined);
+        }}
+      />
+    );
+  }
+  if (createAccount) {
+    return (
+      <CreateNewAccount
+        onClose={() => setCreateAccount(false)}
+        onCreateSuccess={(password) => {
+          showInfoMessage(
+            i18n.str`Account created with password "${password}"`,
+          );
+          setCreateAccount(false);
+        }}
+      />
+    );
+  }
+  return (
+    <Fragment>
+      <div>
+        <h1 class="nav welcome-text">
+          <i18n.Translate>Admin panel</i18n.Translate>
+        </h1>
+      </div>
+
+      <p>
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div></div>
+          <div>
+            <input
+              class="pure-button pure-button-primary content"
+              type="submit"
+              value={i18n.str`Create account`}
+              onClick={async (e) => {
+                e.preventDefault();
+
+                setCreateAccount(true);
+              }}
+            />
+          </div>
+        </div>
+      </p>
+
+      <section id="main">
+        <article>
+          <h2>{i18n.str`Accounts:`}</h2>
+          <div class="results">
+            <table class="pure-table pure-table-striped">
+              <thead>
+                <tr>
+                  <th>{i18n.str`Username`}</th>
+                  <th>{i18n.str`Name`}</th>
+                  <th></th>
+                </tr>
+              </thead>
+              <tbody>
+                {customers.map((item, idx) => {
+                  return (
+                    <tr key={idx}>
+                      <td>
+                        <a
+                          href="#"
+                          onClick={(e) => {
+                            e.preventDefault();
+                            setShowDetails(item.username);
+                          }}
+                        >
+                          {item.username}
+                        </a>
+                      </td>
+                      <td>{item.name}</td>
+                      <td>
+                        <a
+                          href="#"
+                          onClick={(e) => {
+                            e.preventDefault();
+                            setUpdatePassword(item.username);
+                          }}
+                        >
+                          change password
+                        </a>
+                      </td>
+                    </tr>
+                  );
+                })}
+              </tbody>
+            </table>
+          </div>
+        </article>
+      </section>
+    </Fragment>
+  );
+}
+
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+const EMAIL_REGEX =
+  
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
+
+function initializeFromTemplate(
+  account: SandboxBackend.Circuit.CircuitAccountData | undefined,
+): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
+  const emptyAccount = {
+    cashout_address: undefined,
+    iban: undefined,
+    name: undefined,
+    username: undefined,
+    contact_data: undefined,
+  };
+  const emptyContact = {
+    email: undefined,
+    phone: undefined,
+  };
+
+  const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
+    structuredClone(account) ?? emptyAccount;
+  if (typeof initial.contact_data === "undefined") {
+    initial.contact_data = emptyContact;
+  }
+  initial.contact_data.email;
+  return initial as any;
+}
+
+function UpdateAccountPassword({
+  account,
+  onClear,
+  onUpdateSuccess,
+  onLoadNotOk,
+}: {
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+  onClear: () => void;
+  onUpdateSuccess: () => void;
+  account: string;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useAccountDetails(account);
+  const { changePassword } = useAdminAccountAPI();
+  const [password, setPassword] = useState<string | undefined>();
+  const [repeat, setRepeat] = useState<string | undefined>();
+  const [error, saveError] = useState<ErrorMessage | undefined>();
+
+  if (result.clientError) {
+    if (result.isNotfound) return <div>account not found</div>;
+  }
+  if (!result.ok) {
+    return onLoadNotOk(result);
+  }
+
+  const errors = undefinedIfEmpty({
+    password: !password ? i18n.str`required` : undefined,
+    repeat: !repeat
+      ? i18n.str`required`
+      : password !== repeat
+      ? i18n.str`password doesn't match`
+      : undefined,
+  });
+
+  return (
+    <div>
+      <div>
+        <h1 class="nav welcome-text">
+          <i18n.Translate>Admin panel</i18n.Translate>
+        </h1>
+      </div>
+      {error && (
+        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+      )}
+
+      <form class="pure-form">
+        <fieldset>
+          <label for="username">{i18n.str`Username`}</label>
+          <input name="username" type="text" readOnly value={account} />
+        </fieldset>
+        <fieldset>
+          <label>{i18n.str`Password`}</label>
+          <input
+            type="password"
+            value={password ?? ""}
+            onChange={(e) => {
+              setPassword(e.currentTarget.value);
+            }}
+          />
+          <ShowInputErrorLabel
+            message={errors?.password}
+            isDirty={password !== undefined}
+          />
+        </fieldset>
+        <fieldset>
+          <label>{i18n.str`Repeast password`}</label>
+          <input
+            type="password"
+            value={repeat ?? ""}
+            onChange={(e) => {
+              setRepeat(e.currentTarget.value);
+            }}
+          />
+          <ShowInputErrorLabel
+            message={errors?.repeat}
+            isDirty={repeat !== undefined}
+          />
+        </fieldset>
+      </form>
+      <p>
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div>
+            <input
+              class="pure-button"
+              type="submit"
+              value={i18n.str`Close`}
+              onClick={async (e) => {
+                e.preventDefault();
+                onClear();
+              }}
+            />
+          </div>
+          <div>
+            <input
+              id="select-exchange"
+              class="pure-button pure-button-primary content"
+              disabled={!!errors}
+              type="submit"
+              value={i18n.str`Confirm`}
+              onClick={async (e) => {
+                e.preventDefault();
+                if (!!errors || !password) return;
+                try {
+                  const r = await changePassword(account, {
+                    new_password: password,
+                  });
+                  onUpdateSuccess();
+                } catch (error) {
+                  handleError(error, saveError, i18n);
+                }
+              }}
+            />
+          </div>
+        </div>
+      </p>
+    </div>
+  );
+}
+
+function CreateNewAccount({
+  onClose,
+  onCreateSuccess,
+}: {
+  onClose: () => void;
+  onCreateSuccess: (password: string) => void;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const { createAccount } = useAdminAccountAPI();
+  const [submitAccount, setSubmitAccount] = useState<
+    SandboxBackend.Circuit.CircuitAccountData | undefined
+  >();
+  const [error, saveError] = useState<ErrorMessage | undefined>();
+  return (
+    <div>
+      <div>
+        <h1 class="nav welcome-text">
+          <i18n.Translate>Admin panel</i18n.Translate>
+        </h1>
+      </div>
+      {error && (
+        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+      )}
+
+      <AccountForm
+        template={undefined}
+        purpose="create"
+        onChange={(a) => setSubmitAccount(a)}
+      />
+
+      <p>
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div>
+            <input
+              class="pure-button"
+              type="submit"
+              value={i18n.str`Close`}
+              onClick={async (e) => {
+                e.preventDefault();
+                onClose();
+              }}
+            />
+          </div>
+          <div>
+            <input
+              id="select-exchange"
+              class="pure-button pure-button-primary content"
+              disabled={!submitAccount}
+              type="submit"
+              value={i18n.str`Confirm`}
+              onClick={async (e) => {
+                e.preventDefault();
+
+                if (!submitAccount) return;
+                try {
+                  const account: SandboxBackend.Circuit.CircuitAccountRequest =
+                    {
+                      cashout_address: submitAccount.cashout_address,
+                      contact_data: submitAccount.contact_data,
+                      internal_iban: submitAccount.iban,
+                      name: submitAccount.name,
+                      username: submitAccount.username,
+                      password: randomPassword(),
+                    };
+
+                  await createAccount(account);
+                  onCreateSuccess(account.password);
+                } catch (error) {
+                  handleError(error, saveError, i18n);
+                }
+              }}
+            />
+          </div>
+        </div>
+      </p>
+    </div>
+  );
+}
+
+function ShowAccountDetails({
+  account,
+  onClear,
+  onUpdateSuccess,
+  onLoadNotOk,
+}: {
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+  onClear: () => void;
+  onUpdateSuccess: () => void;
+  account: string;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useAccountDetails(account);
+  const { updateAccount } = useAdminAccountAPI();
+  const [update, setUpdate] = useState(false);
+  const [submitAccount, setSubmitAccount] = useState<
+    SandboxBackend.Circuit.CircuitAccountData | undefined
+  >();
+  const [error, saveError] = useState<ErrorMessage | undefined>();
+
+  if (result.clientError) {
+    if (result.isNotfound) return <div>account not found</div>;
+  }
+  if (!result.ok) {
+    return onLoadNotOk(result);
+  }
+
+  return (
+    <div>
+      <div>
+        <h1 class="nav welcome-text">
+          <i18n.Translate>Admin panel</i18n.Translate>
+        </h1>
+      </div>
+      {error && (
+        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+      )}
+      <AccountForm
+        template={result.data}
+        purpose={update ? "update" : "show"}
+        onChange={(a) => setSubmitAccount(a)}
+      />
+
+      <p>
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div>
+            <input
+              class="pure-button"
+              type="submit"
+              value={i18n.str`Close`}
+              onClick={async (e) => {
+                e.preventDefault();
+                onClear();
+              }}
+            />
+          </div>
+          <div>
+            <input
+              id="select-exchange"
+              class="pure-button pure-button-primary content"
+              disabled={update && !submitAccount}
+              type="submit"
+              value={update ? i18n.str`Confirm` : i18n.str`Update`}
+              onClick={async (e) => {
+                e.preventDefault();
+
+                if (!update) {
+                  setUpdate(true);
+                } else {
+                  if (!submitAccount) return;
+                  try {
+                    await updateAccount(account, {
+                      cashout_address: submitAccount.cashout_address,
+                      contact_data: submitAccount.contact_data,
+                    });
+                    onUpdateSuccess();
+                  } catch (error) {
+                    handleError(error, saveError, i18n);
+                  }
+                }
+              }}
+            />
+          </div>
+        </div>
+      </p>
+    </div>
+  );
+}
+
+/**
+ * Create valid account object to update or create
+ * Take template as initial values for the form
+ * Purpose indicate if all field al read only (show), part of them (update)
+ * or none (create)
+ * @param param0
+ * @returns
+ */
+function AccountForm({
+  template,
+  purpose,
+  onChange,
+}: {
+  template: SandboxBackend.Circuit.CircuitAccountData | undefined;
+  onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
+  purpose: "create" | "update" | "show";
+}): VNode {
+  const initial = initializeFromTemplate(template);
+  const [form, setForm] = useState(initial);
+  const [errors, setErrors] = useState<typeof initial | undefined>(undefined);
+  const { i18n } = useTranslationContext();
+
+  function updateForm(newForm: typeof initial): void {
+    const parsed = !newForm.cashout_address
+      ? undefined
+      : parsePaytoUri(newForm.cashout_address);
+
+    const validationResult = undefinedIfEmpty<typeof initial>({
+      cashout_address: !newForm.cashout_address
+        ? i18n.str`required`
+        : !parsed
+        ? i18n.str`does not follow the pattern`
+        : !parsed.isKnown || parsed.targetType !== "iban"
+        ? i18n.str`only "IBAN" target are supported`
+        : !IBAN_REGEX.test(parsed.iban)
+        ? i18n.str`IBAN should have just uppercased letters and numbers`
+        : undefined,
+      contact_data: {
+        email: !newForm.contact_data.email
+          ? undefined
+          : !EMAIL_REGEX.test(newForm.contact_data.email)
+          ? i18n.str`it should be an email`
+          : undefined,
+        phone: !newForm.contact_data.phone
+          ? undefined
+          : !newForm.contact_data.phone.startsWith("+")
+          ? i18n.str`should start with +`
+          : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
+          ? i18n.str`phone number can't have other than numbers`
+          : undefined,
+      },
+      iban: !newForm.iban
+        ? i18n.str`required`
+        : !IBAN_REGEX.test(newForm.iban)
+        ? i18n.str`IBAN should have just uppercased letters and numbers`
+        : undefined,
+      name: !newForm.name ? i18n.str`required` : undefined,
+      username: !newForm.username ? i18n.str`required` : undefined,
+    });
+
+    setErrors(validationResult);
+    setForm(newForm);
+    onChange(validationResult === undefined ? undefined : (newForm as any));
+  }
+
+  return (
+    <form class="pure-form">
+      <fieldset>
+        <label for="username">{i18n.str`Username`}</label>
+        <input
+          name="username"
+          type="text"
+          disabled={purpose !== "create"}
+          value={form.username}
+          onChange={(e) => {
+            form.username = e.currentTarget.value;
+            updateForm(structuredClone(form));
+          }}
+        />
+        <ShowInputErrorLabel
+          message={errors?.username}
+          isDirty={form.username !== undefined}
+        />
+      </fieldset>
+      <fieldset>
+        <label>{i18n.str`Name`}</label>
+        <input
+          disabled={purpose !== "create"}
+          value={form.name ?? ""}
+          onChange={(e) => {
+            form.name = e.currentTarget.value;
+            updateForm(structuredClone(form));
+          }}
+        />
+        <ShowInputErrorLabel
+          message={errors?.name}
+          isDirty={form.name !== undefined}
+        />
+      </fieldset>
+      <fieldset>
+        <label>{i18n.str`IBAN`}</label>
+        <input
+          disabled={purpose !== "create"}
+          value={form.iban ?? ""}
+          onChange={(e) => {
+            form.iban = e.currentTarget.value;
+            updateForm(structuredClone(form));
+          }}
+        />
+        <ShowInputErrorLabel
+          message={errors?.iban}
+          isDirty={form.iban !== undefined}
+        />
+      </fieldset>
+      <fieldset>
+        <label>{i18n.str`Email`}</label>
+        <input
+          disabled={purpose === "show"}
+          value={form.contact_data.email ?? ""}
+          onChange={(e) => {
+            form.contact_data.email = e.currentTarget.value;
+            updateForm(structuredClone(form));
+          }}
+        />
+        <ShowInputErrorLabel
+          message={errors?.contact_data.email}
+          isDirty={form.contact_data.email !== undefined}
+        />
+      </fieldset>
+      <fieldset>
+        <label>{i18n.str`Phone`}</label>
+        <input
+          disabled={purpose === "show"}
+          value={form.contact_data.phone ?? ""}
+          onChange={(e) => {
+            form.contact_data.phone = e.currentTarget.value;
+            updateForm(structuredClone(form));
+          }}
+        />
+        <ShowInputErrorLabel
+          message={errors?.contact_data.phone}
+          isDirty={form.contact_data?.phone !== undefined}
+        />
+      </fieldset>
+      <fieldset>
+        <label>{i18n.str`Cashout address`}</label>
+        <input
+          disabled={purpose === "show"}
+          value={form.cashout_address ?? ""}
+          onChange={(e) => {
+            form.cashout_address = e.currentTarget.value;
+            updateForm(structuredClone(form));
+          }}
+        />
+        <ShowInputErrorLabel
+          message={errors?.cashout_address}
+          isDirty={form.cashout_address !== undefined}
+        />
+      </fieldset>
+    </form>
+  );
+}
+
+function handleError(
+  error: unknown,
+  saveError: (e: ErrorMessage) => void,
+  i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): void {
+  if (error instanceof RequestError) {
+    const payload = error.info.error as SandboxBackend.SandboxError;
+    saveError({
+      title: error.info.serverError
+        ? i18n.str`Server had an error`
+        : i18n.str`Server didn't accept the request`,
+      description: payload.error.description,
+    });
+  } else if (error instanceof Error) {
+    saveError({
+      title: i18n.str`Could not update account`,
+      description: error.message,
+    });
+  } else {
+    saveError({
+      title: i18n.str`Error, please report`,
+      debug: JSON.stringify(error),
+    });
+  }
+}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx 
b/packages/demobank-ui/src/pages/BankFrame.tsx
index e36629e2a..ed36daa21 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -19,7 +19,11 @@ import { ComponentChildren, Fragment, h, VNode } from 
"preact";
 import talerLogo from "../assets/logo-white.svg";
 import { LangSelectorLikePy as LangSelector } from 
"../components/LangSelector.js";
 import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import {
+  ErrorMessage,
+  PageStateType,
+  usePageContext,
+} from "../context/pageState.js";
 import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { bankUiSettings } from "../settings.js";
 
@@ -42,7 +46,7 @@ export function BankFrame({
         onClick={() => {
           pageStateSetter((prevState: PageStateType) => {
             const { talerWithdrawUri, withdrawalId, ...rest } = prevState;
-            backend.clear();
+            backend.logOut();
             return {
               ...rest,
               withdrawalInProgress: false,
@@ -107,7 +111,14 @@ export function BankFrame({
         </nav>
       </div>
       <section id="main" class="content">
-        <ErrorBanner />
+        {pageState.error && (
+          <ErrorBanner
+            error={pageState.error}
+            onClear={() => {
+              pageStateSetter((prev) => ({ ...prev, error: undefined }));
+            }}
+          />
+        )}
         <StatusBanner />
         {backend.state.status === "loggedIn" ? logOut : null}
         {children}
@@ -136,33 +147,34 @@ function maybeDemoContent(content: VNode): VNode {
   return <Fragment />;
 }
 
-function ErrorBanner(): VNode | null {
-  const { pageState, pageStateSetter } = usePageContext();
-
-  if (!pageState.error) return null;
-
-  const rval = (
+export function ErrorBanner({
+  error,
+  onClear,
+}: {
+  error: ErrorMessage;
+  onClear: () => void;
+}): VNode | null {
+  return (
     <div class="informational informational-fail" style={{ marginTop: 8 }}>
       <div style={{ display: "flex", justifyContent: "space-between" }}>
         <p>
-          <b>{pageState.error.title}</b>
+          <b>{error.title}</b>
         </p>
         <div>
           <input
             type="button"
             class="pure-button"
             value="Clear"
-            onClick={async () => {
-              pageStateSetter((prev) => ({ ...prev, error: undefined }));
+            onClick={(e) => {
+              e.preventDefault();
+              onClear();
             }}
           />
         </div>
       </div>
-      <p>{pageState.error.description}</p>
+      <p>{error.description}</p>
     </div>
   );
-  delete pageState.error;
-  return rval;
 }
 
 function StatusBanner(): VNode | null {
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx 
b/packages/demobank-ui/src/pages/HomePage.tsx
new file mode 100644
index 000000000..e60732d42
--- /dev/null
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -0,0 +1,149 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { Logger } from "@gnu-taler/taler-util";
+import {
+  HttpResponsePaginated,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { Loading } from "../components/Loading.js";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { AccountPage } from "./AccountPage.js";
+import { AdminPage } from "./AdminPage.js";
+import { LoginForm } from "./LoginForm.js";
+import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
+
+const logger = new Logger("AccountPage");
+
+/**
+ * show content based on state:
+ * - LoginForm if the user is not logged in
+ * - qr code if withdrawal in progress
+ * - else account information
+ * Use the handler to catch error cases
+ *
+ * @param param0
+ * @returns
+ */
+export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
+  const backend = useBackendContext();
+  const { pageState, pageStateSetter } = usePageContext();
+  const { i18n } = useTranslationContext();
+
+  function saveError(error: PageStateType["error"]): void {
+    pageStateSetter((prev) => ({ ...prev, error }));
+  }
+
+  function saveErrorAndLogout(error: PageStateType["error"]): void {
+    saveError(error);
+    backend.logOut();
+  }
+
+  function clearCurrentWithdrawal(): void {
+    pageStateSetter((prevState: PageStateType) => {
+      return {
+        ...prevState,
+        withdrawalId: undefined,
+        talerWithdrawUri: undefined,
+        withdrawalInProgress: false,
+      };
+    });
+  }
+
+  if (backend.state.status === "loggedOut") {
+    return <LoginForm onRegister={onRegister} />;
+  }
+
+  const { withdrawalId, talerWithdrawUri } = pageState;
+
+  if (talerWithdrawUri && withdrawalId) {
+    return (
+      <WithdrawalQRCode
+        account={backend.state.username}
+        withdrawalId={withdrawalId}
+        talerWithdrawUri={talerWithdrawUri}
+        onAbort={clearCurrentWithdrawal}
+        onLoadNotOk={handleNotOkResult(
+          backend.state.username,
+          saveError,
+          i18n,
+          onRegister,
+        )}
+      />
+    );
+  }
+
+  if (backend.state.isUserAdministrator) {
+    return (
+      <AdminPage
+        onLoadNotOk={handleNotOkResult(
+          backend.state.username,
+          saveErrorAndLogout,
+          i18n,
+          onRegister,
+        )}
+      />
+    );
+  }
+
+  return (
+    <AccountPage
+      account={backend.state.username}
+      onLoadNotOk={handleNotOkResult(
+        backend.state.username,
+        saveErrorAndLogout,
+        i18n,
+        onRegister,
+      )}
+    />
+  );
+}
+
+function handleNotOkResult(
+  account: string,
+  onErrorHandler: (state: PageStateType["error"]) => void,
+  i18n: ReturnType<typeof useTranslationContext>["i18n"],
+  onRegister: () => void,
+): <T, E>(result: HttpResponsePaginated<T, E>) => VNode {
+  return function handleNotOkResult2<T, E>(
+    result: HttpResponsePaginated<T, E>,
+  ): VNode {
+    if (result.clientError && result.isUnauthorized) {
+      onErrorHandler({
+        title: i18n.str`Wrong credentials for "${account}"`,
+      });
+      return <LoginForm onRegister={onRegister} />;
+    }
+    if (result.clientError && result.isNotfound) {
+      onErrorHandler({
+        title: i18n.str`Username or account label "${account}" not found`,
+      });
+      return <LoginForm onRegister={onRegister} />;
+    }
+    if (result.loading) return <Loading />;
+    if (!result.ok) {
+      onErrorHandler({
+        title: i18n.str`The backend reported a problem: HTTP status 
#${result.status}`,
+        description: `Diagnostic from ${result.info?.url.href} is 
"${result.message}"`,
+        debug: JSON.stringify(result.error),
+      });
+      return <LoginForm onRegister={onRegister} />;
+    }
+    return <div />;
+  };
+}
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx 
b/packages/demobank-ui/src/pages/LoginForm.tsx
index a5d8695dc..3d4279f99 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,21 +14,19 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { h, VNode } from "preact";
-import { route } from "preact-router";
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
 import { useBackendContext } from "../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
-import { BackendStateHandler } from "../hooks/backend.js";
 import { bankUiSettings } from "../settings.js";
-import { getBankBackendBaseUrl, undefinedIfEmpty } from "../utils.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { PASSWORD_REGEX, USERNAME_REGEX } from "./RegistrationPage.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { USERNAME_REGEX, PASSWORD_REGEX } from "./RegistrationPage.js";
 
 /**
  * Collect and submit login data.
  */
-export function LoginForm(): VNode {
+export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
   const backend = useBackendContext();
   const [username, setUsername] = useState<string | undefined>();
   const [password, setPassword] = useState<string | undefined>();
@@ -52,107 +50,93 @@ export function LoginForm(): VNode {
   });
 
   return (
-    <div class="login-div">
-      <form
-        class="login-form"
-        noValidate
-        onSubmit={(e) => {
-          e.preventDefault();
-        }}
-        autoCapitalize="none"
-        autoCorrect="off"
-      >
-        <div class="pure-form">
-          <h2>{i18n.str`Please login!`}</h2>
-          <p class="unameFieldLabel loginFieldLabel formFieldLabel">
-            <label for="username">{i18n.str`Username:`}</label>
-          </p>
-          <input
-            ref={ref}
-            autoFocus
-            type="text"
-            name="username"
-            id="username"
-            value={username ?? ""}
-            placeholder="Username"
-            required
-            onInput={(e): void => {
-              setUsername(e.currentTarget.value);
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errors?.username}
-            isDirty={username !== undefined}
-          />
-          <p class="passFieldLabel loginFieldLabel formFieldLabel">
-            <label for="password">{i18n.str`Password:`}</label>
-          </p>
-          <input
-            type="password"
-            name="password"
-            id="password"
-            value={password ?? ""}
-            placeholder="Password"
-            required
-            onInput={(e): void => {
-              setPassword(e.currentTarget.value);
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errors?.password}
-            isDirty={password !== undefined}
-          />
-          <br />
-          <button
-            type="submit"
-            class="pure-button pure-button-primary"
-            disabled={!!errors}
-            onClick={(e) => {
-              e.preventDefault();
-              if (!username || !password) return;
-              loginCall({ username, password }, backend);
-              setUsername(undefined);
-              setPassword(undefined);
-            }}
-          >
-            {i18n.str`Login`}
-          </button>
+    <Fragment>
+      <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
 
-          {bankUiSettings.allowRegistrations ? (
+      <div class="login-div">
+        <form
+          class="login-form"
+          noValidate
+          onSubmit={(e) => {
+            e.preventDefault();
+          }}
+          autoCapitalize="none"
+          autoCorrect="off"
+        >
+          <div class="pure-form">
+            <h2>{i18n.str`Please login!`}</h2>
+            <p class="unameFieldLabel loginFieldLabel formFieldLabel">
+              <label for="username">{i18n.str`Username:`}</label>
+            </p>
+            <input
+              ref={ref}
+              autoFocus
+              type="text"
+              name="username"
+              id="username"
+              value={username ?? ""}
+              placeholder="Username"
+              autocomplete="username"
+              required
+              onInput={(e): void => {
+                setUsername(e.currentTarget.value);
+              }}
+            />
+            <ShowInputErrorLabel
+              message={errors?.username}
+              isDirty={username !== undefined}
+            />
+            <p class="passFieldLabel loginFieldLabel formFieldLabel">
+              <label for="password">{i18n.str`Password:`}</label>
+            </p>
+            <input
+              type="password"
+              name="password"
+              id="password"
+              autocomplete="current-password"
+              value={password ?? ""}
+              placeholder="Password"
+              required
+              onInput={(e): void => {
+                setPassword(e.currentTarget.value);
+              }}
+            />
+            <ShowInputErrorLabel
+              message={errors?.password}
+              isDirty={password !== undefined}
+            />
+            <br />
             <button
-              class="pure-button pure-button-secondary btn-cancel"
+              type="submit"
+              class="pure-button pure-button-primary"
+              disabled={!!errors}
               onClick={(e) => {
                 e.preventDefault();
-                route("/register");
+                if (!username || !password) return;
+                backend.logIn({ username, password });
+                setUsername(undefined);
+                setPassword(undefined);
               }}
             >
-              {i18n.str`Register`}
+              {i18n.str`Login`}
             </button>
-          ) : (
-            <div />
-          )}
-        </div>
-      </form>
-    </div>
-  );
-}
-
-async function loginCall(
-  req: { username: string; password: string },
-  /**
-   * FIXME: figure out if the two following
-   * functions can be retrieved from the state.
-   */
-  backend: BackendStateHandler,
-): Promise<void> {
-  /**
-   * Optimistically setting the state as 'logged in', and
-   * let the Account component request the balance to check
-   * whether the credentials are valid.  */
 
-  backend.save({
-    url: getBankBackendBaseUrl(),
-    username: req.username,
-    password: req.password,
-  });
+            {bankUiSettings.allowRegistrations ? (
+              <button
+                class="pure-button pure-button-secondary btn-cancel"
+                onClick={(e) => {
+                  e.preventDefault();
+                  onRegister();
+                }}
+              >
+                {i18n.str`Register`}
+              </button>
+            ) : (
+              <div />
+            )}
+          </div>
+        </form>
+      </div>
+    </Fragment>
+  );
 }
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index ae876d556..dd04ed6e2 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -19,17 +19,22 @@ import { useState } from "preact/hooks";
 import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
 import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
 
 /**
  * Let the user choose a payment option,
  * then specify the details trigger the action.
  */
-export function PaymentOptions({ currency }: { currency?: string }): VNode {
+export function PaymentOptions({ currency }: { currency: string }): VNode {
   const { i18n } = useTranslationContext();
+  const { pageStateSetter } = usePageContext();
 
   const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
     "charge-wallet",
   );
+  function saveError(error: PageStateType["error"]): void {
+    pageStateSetter((prev) => ({ ...prev, error }));
+  }
 
   return (
     <article>
@@ -55,13 +60,35 @@ export function PaymentOptions({ currency }: { currency?: 
string }): VNode {
         {tab === "charge-wallet" && (
           <div id="charge-wallet" class="tabcontent active">
             <h3>{i18n.str`Obtain digital cash`}</h3>
-            <WalletWithdrawForm focus currency={currency} />
+            <WalletWithdrawForm
+              focus
+              currency={currency}
+              onSuccess={(data) => {
+                pageStateSetter((prevState: PageStateType) => ({
+                  ...prevState,
+                  withdrawalInProgress: true,
+                  talerWithdrawUri: data.taler_withdraw_uri,
+                  withdrawalId: data.withdrawal_id,
+                }));
+              }}
+              onError={saveError}
+            />
           </div>
         )}
         {tab === "wire-transfer" && (
           <div id="wire-transfer" class="tabcontent active">
             <h3>{i18n.str`Transfer to bank account`}</h3>
-            <PaytoWireTransferForm focus currency={currency} />
+            <PaytoWireTransferForm
+              focus
+              currency={currency}
+              onSuccess={() => {
+                pageStateSetter((prevState: PageStateType) => ({
+                  ...prevState,
+                  info: i18n.str`Wire transfer created!`,
+                }));
+              }}
+              onError={saveError}
+            />
           </div>
         )}
       </div>
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 46b006880..d859b1cc7 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -14,64 +14,81 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, Logger, parsePaytoUri } from "@gnu-taler/taler-util";
-import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
-import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import {
+  Amounts,
+  buildPayto,
+  Logger,
+  parsePaytoUri,
+  stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
 import {
   InternationalizationAPI,
+  RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { useAccessAPI } from "../hooks/access.js";
 import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders, undefinedIfEmpty } from "../utils.js";
+import { undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("PaytoWireTransferForm");
 
 export function PaytoWireTransferForm({
   focus,
+  onError,
+  onSuccess,
   currency,
 }: {
   focus?: boolean;
-  currency?: string;
+  onError: (e: PageStateType["error"]) => void;
+  onSuccess: () => void;
+  currency: string;
 }): VNode {
   const backend = useBackendContext();
-  const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for 
go-back button?
+  // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used 
for go-back button?
 
-  const [submitData, submitDataSetter] = useWireTransferRequestType();
+  const [isRawPayto, setIsRawPayto] = useState(false);
+  // const [submitData, submitDataSetter] = useWireTransferRequestType();
+  const [iban, setIban] = useState<string | undefined>(undefined);
+  const [subject, setSubject] = useState<string | undefined>(undefined);
+  const [amount, setAmount] = useState<string | undefined>(undefined);
 
   const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
     undefined,
   );
   const { i18n } = useTranslationContext();
   const ibanRegex = "^[A-Z][A-Z][0-9]+$";
-  let transactionData: TransactionRequestType;
   const ref = useRef<HTMLInputElement>(null);
   useEffect(() => {
     if (focus) ref.current?.focus();
-  }, [focus, pageState.isRawPayto]);
+  }, [focus, isRawPayto]);
 
   let parsedAmount = undefined;
+  const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
 
   const errorsWire = undefinedIfEmpty({
-    iban: !submitData?.iban
+    iban: !iban
       ? i18n.str`Missing IBAN`
-      : !/^[A-Z0-9]*$/.test(submitData.iban)
+      : !IBAN_REGEX.test(iban)
       ? i18n.str`IBAN should have just uppercased letters and numbers`
       : undefined,
-    subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
-    amount: !submitData?.amount
+    subject: !subject ? i18n.str`Missing subject` : undefined,
+    amount: !amount
       ? i18n.str`Missing amount`
-      : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
+      : !(parsedAmount = Amounts.parse(`${currency}:${amount}`))
       ? i18n.str`Amount is not valid`
       : Amounts.isZero(parsedAmount)
       ? i18n.str`Should be greater than 0`
       : undefined,
   });
 
-  if (!pageState.isRawPayto)
+  const { createTransaction } = useAccessAPI();
+
+  if (!isRawPayto)
     return (
       <div>
         <form
@@ -90,21 +107,18 @@ export function PaytoWireTransferForm({
               type="text"
               id="iban"
               name="iban"
-              value={submitData?.iban ?? ""}
+              value={iban ?? ""}
               placeholder="CC0123456789"
               required
               pattern={ibanRegex}
               onInput={(e): void => {
-                submitDataSetter((submitData) => ({
-                  ...submitData,
-                  iban: e.currentTarget.value,
-                }));
+                setIban(e.currentTarget.value);
               }}
             />
             <br />
             <ShowInputErrorLabel
               message={errorsWire?.iban}
-              isDirty={submitData?.iban !== undefined}
+              isDirty={iban !== undefined}
             />
             <br />
             <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
@@ -113,19 +127,16 @@ export function PaytoWireTransferForm({
               name="subject"
               id="subject"
               placeholder="subject"
-              value={submitData?.subject ?? ""}
+              value={subject ?? ""}
               required
               onInput={(e): void => {
-                submitDataSetter((submitData) => ({
-                  ...submitData,
-                  subject: e.currentTarget.value,
-                }));
+                setSubject(e.currentTarget.value);
               }}
             />
             <br />
             <ShowInputErrorLabel
               message={errorsWire?.subject}
-              isDirty={submitData?.subject !== undefined}
+              isDirty={subject !== undefined}
             />
             <br />
             <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
@@ -146,18 +157,15 @@ export function PaytoWireTransferForm({
                 id="amount"
                 placeholder="amount"
                 required
-                value={submitData?.amount ?? ""}
+                value={amount ?? ""}
                 onInput={(e): void => {
-                  submitDataSetter((submitData) => ({
-                    ...submitData,
-                    amount: e.currentTarget.value,
-                  }));
+                  setAmount(e.currentTarget.value);
                 }}
               />
             </div>
             <ShowInputErrorLabel
               message={errorsWire?.amount}
-              isDirty={submitData?.amount !== undefined}
+              isDirty={amount !== undefined}
             />
           </p>
 
@@ -169,43 +177,28 @@ export function PaytoWireTransferForm({
               value="Send"
               onClick={async (e) => {
                 e.preventDefault();
-                if (
-                  typeof submitData === "undefined" ||
-                  typeof submitData.iban === "undefined" ||
-                  submitData.iban === "" ||
-                  typeof submitData.subject === "undefined" ||
-                  submitData.subject === "" ||
-                  typeof submitData.amount === "undefined" ||
-                  submitData.amount === ""
-                ) {
-                  logger.error("Not all the fields were given.");
-                  pageStateSetter((prevState: PageStateType) => ({
-                    ...prevState,
-
-                    error: {
-                      title: i18n.str`Field(s) missing.`,
-                    },
-                  }));
+                if (!(iban && subject && amount)) {
                   return;
                 }
-                transactionData = {
-                  paytoUri: `payto://iban/${
-                    submitData.iban
-                  }?message=${encodeURIComponent(submitData.subject)}`,
-                  amount: `${currency}:${submitData.amount}`,
-                };
-                return await createTransactionCall(
-                  transactionData,
-                  backend.state,
-                  pageStateSetter,
-                  () =>
-                    submitDataSetter((p) => ({
-                      amount: undefined,
-                      iban: undefined,
-                      subject: undefined,
-                    })),
-                  i18n,
-                );
+                const ibanPayto = buildPayto("iban", iban, undefined);
+                ibanPayto.params.message = encodeURIComponent(subject);
+                const paytoUri = stringifyPaytoUri(ibanPayto);
+
+                await createTransaction({
+                  paytoUri,
+                  amount: `${currency}:${amount}`,
+                });
+                // return await createTransactionCall(
+                //   transactionData,
+                //   backend.state,
+                //   pageStateSetter,
+                //   () => {
+                //     setAmount(undefined);
+                //     setIban(undefined);
+                //     setSubject(undefined);
+                //   },
+                //   i18n,
+                // );
               }}
             />
             <input
@@ -214,11 +207,9 @@ export function PaytoWireTransferForm({
               value="Clear"
               onClick={async (e) => {
                 e.preventDefault();
-                submitDataSetter((p) => ({
-                  amount: undefined,
-                  iban: undefined,
-                  subject: undefined,
-                }));
+                setAmount(undefined);
+                setIban(undefined);
+                setSubject(undefined);
               }}
             />
           </p>
@@ -227,11 +218,7 @@ export function PaytoWireTransferForm({
           <a
             href="/account"
             onClick={() => {
-              logger.trace("switch to raw payto form");
-              pageStateSetter((prevState) => ({
-                ...prevState,
-                isRawPayto: true,
-              }));
+              setIsRawPayto(true);
             }}
           >
             {i18n.str`Want to try the raw payto://-format?`}
@@ -240,11 +227,23 @@ export function PaytoWireTransferForm({
       </div>
     );
 
+  const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
+
   const errorsPayto = undefinedIfEmpty({
     rawPaytoInput: !rawPaytoInput
-      ? i18n.str`Missing payto address`
-      : !parsePaytoUri(rawPaytoInput)
-      ? i18n.str`Payto does not follow the pattern`
+      ? i18n.str`required`
+      : !parsed
+      ? i18n.str`does not follow the pattern`
+      : !parsed.params.amount
+      ? i18n.str`use the "amount" parameter to specify the amount to be 
transferred`
+      : Amounts.parse(parsed.params.amount) === undefined
+      ? i18n.str`the amount is not valid`
+      : !parsed.params.message
+      ? i18n.str`use the "message" parameter to specify a reference text for 
the transfer`
+      : !parsed.isKnown || parsed.targetType !== "iban"
+      ? i18n.str`only "IBAN" target are supported`
+      : !IBAN_REGEX.test(parsed.iban)
+      ? i18n.str`IBAN should have just uppercased letters and numbers`
       : undefined,
   });
 
@@ -296,25 +295,29 @@ export function PaytoWireTransferForm({
             disabled={!!errorsPayto}
             value={i18n.str`Send`}
             onClick={async () => {
-              // empty string evaluates to false.
               if (!rawPaytoInput) {
                 logger.error("Didn't get any raw Payto string!");
                 return;
               }
-              transactionData = { paytoUri: rawPaytoInput };
-              if (
-                typeof transactionData.paytoUri === "undefined" ||
-                transactionData.paytoUri.length === 0
-              )
-                return;
 
-              return await createTransactionCall(
-                transactionData,
-                backend.state,
-                pageStateSetter,
-                () => rawPaytoInputSetter(undefined),
-                i18n,
-              );
+              try {
+                await createTransaction({
+                  paytoUri: rawPaytoInput,
+                });
+                onSuccess();
+                rawPaytoInputSetter(undefined);
+              } catch (error) {
+                if (error instanceof RequestError) {
+                  const errorData: SandboxBackend.SandboxError =
+                    error.info.error;
+
+                  onError({
+                    title: i18n.str`Transfer creation gave response error`,
+                    description: errorData.error.description,
+                    debug: JSON.stringify(errorData),
+                  });
+                }
+              }
             }}
           />
         </p>
@@ -322,11 +325,7 @@ export function PaytoWireTransferForm({
           <a
             href="/account"
             onClick={() => {
-              logger.trace("switch to wire-transfer-form");
-              pageStateSetter((prevState) => ({
-                ...prevState,
-                isRawPayto: false,
-              }));
+              setIsRawPayto(false);
             }}
           >
             {i18n.str`Use wire-transfer form?`}
@@ -336,115 +335,3 @@ export function PaytoWireTransferForm({
     </div>
   );
 }
-
-/**
- * Stores in the state a object representing a wire transfer,
- * in order to avoid losing the handle of the data entered by
- * the user in <input> fields.  FIXME: name not matching the
- * purpose, as this is not a HTTP request body but rather the
- * state of the <input>-elements.
- */
-type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
-function useWireTransferRequestType(
-  state?: WireTransferRequestType,
-): [WireTransferRequestTypeOpt, StateUpdater<WireTransferRequestTypeOpt>] {
-  const ret = useLocalStorage(
-    "wire-transfer-request-state",
-    JSON.stringify(state),
-  );
-  const retObj: WireTransferRequestTypeOpt = ret[0]
-    ? JSON.parse(ret[0])
-    : ret[0];
-  const retSetter: StateUpdater<WireTransferRequestTypeOpt> = function (val) {
-    const newVal =
-      val instanceof Function
-        ? JSON.stringify(val(retObj))
-        : JSON.stringify(val);
-    ret[1](newVal);
-  };
-  return [retObj, retSetter];
-}
-
-/**
- * This function creates a new transaction.  It reads a Payto
- * address entered by the user and POSTs it to the bank.  No
- * sanity-check of the input happens before the POST as this is
- * already conducted by the backend.
- */
-async function createTransactionCall(
-  req: TransactionRequestType,
-  backendState: BackendState,
-  pageStateSetter: StateUpdater<PageStateType>,
-  /**
-   * Optional since the raw payto form doesn't have
-   * a stateful management of the input data yet.
-   */
-  cleanUpForm: () => void,
-  i18n: InternationalizationAPI,
-): Promise<void> {
-  if (backendState.status === "loggedOut") {
-    logger.error("No credentials found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`No credentials found.`,
-      },
-    }));
-    return;
-  }
-  let res: Response;
-  try {
-    const { username, password } = backendState;
-    const headers = prepareHeaders(username, password);
-    const url = new URL(
-      `access-api/accounts/${backendState.username}/transactions`,
-      backendState.url,
-    );
-    res = await fetch(url.href, {
-      method: "POST",
-      headers,
-      body: JSON.stringify(req),
-    });
-  } catch (error) {
-    logger.error("Could not POST transaction request to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Could not create the wire transfer`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  // POST happened, status not sure yet.
-  if (!res.ok) {
-    const response = await res.json();
-    logger.error(
-      `Transfer creation gave response error: ${response} (${res.status})`,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Transfer creation gave response error`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  // status is 200 OK here, tell the user.
-  logger.trace("Wire transfer created!");
-  pageStateSetter((prevState) => ({
-    ...prevState,
-
-    info: i18n.str`Wire transfer created!`,
-  }));
-
-  // Only at this point the input data can
-  // be discarded.
-  cleanUpForm();
-}
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx 
b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 7bf5c41c7..54a77b42a 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -15,91 +15,42 @@
  */
 
 import { Logger } from "@gnu-taler/taler-util";
-import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { route } from "preact-router";
+import {
+  HttpResponsePaginated,
+  useLocalStorage,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
 import { StateUpdater } from "preact/hooks";
-import useSWR, { SWRConfig } from "swr";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
-import { getBankBackendBaseUrl } from "../utils.js";
-import { BankFrame } from "./BankFrame.js";
 import { Transactions } from "../components/Transactions/index.js";
+import { usePublicAccounts } from "../hooks/access.js";
 
 const logger = new Logger("PublicHistoriesPage");
 
-export function PublicHistoriesPage(): VNode {
-  return (
-    <SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
-      <BankFrame>
-        <PublicHistories />
-      </BankFrame>
-    </SWRWithoutCredentials>
-  );
-}
-
-function SWRWithoutCredentials({
-  baseUrl,
-  children,
-}: {
-  children: ComponentChildren;
-  baseUrl: string;
-}): VNode {
-  logger.trace("Base URL", baseUrl);
-  return (
-    <SWRConfig
-      value={{
-        fetcher: (url: string) =>
-          fetch(baseUrl + url || "").then((r) => {
-            if (!r.ok) throw { status: r.status, json: r.json() };
+// export function PublicHistoriesPage2(): VNode {
+//   return (
+//     <BankFrame>
+//       <PublicHistories />
+//     </BankFrame>
+//   );
+// }
 
-            return r.json();
-          }),
-      }}
-    >
-      {children as any}
-    </SWRConfig>
-  );
+interface Props {
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
 }
 
 /**
  * Show histories of public accounts.
  */
-function PublicHistories(): VNode {
-  const { pageState, pageStateSetter } = usePageContext();
+export function PublicHistoriesPage({ onLoadNotOk }: Props): VNode {
   const [showAccount, setShowAccount] = useShowPublicAccount();
-  const { data, error } = useSWR("access-api/public-accounts");
   const { i18n } = useTranslationContext();
 
-  if (typeof error !== "undefined") {
-    switch (error.status) {
-      case 404:
-        logger.error("public accounts: 404", error);
-        route("/account");
-        pageStateSetter((prevState: PageStateType) => ({
-          ...prevState,
+  const result = usePublicAccounts();
+  if (!result.ok) return onLoadNotOk(result);
 
-          error: {
-            title: i18n.str`List of public accounts was not found.`,
-            debug: JSON.stringify(error),
-          },
-        }));
-        break;
-      default:
-        logger.error("public accounts: non-404 error", error);
-        route("/account");
-        pageStateSetter((prevState: PageStateType) => ({
-          ...prevState,
+  const { data } = result;
 
-          error: {
-            title: i18n.str`List of public accounts could not be retrieved.`,
-            debug: JSON.stringify(error),
-          },
-        }));
-        break;
-    }
-  }
-  if (!data) return <p>Waiting public accounts list...</p>;
   const txs: Record<string, h.JSX.Element> = {};
   const accountsBar = [];
 
@@ -133,9 +84,7 @@ function PublicHistories(): VNode {
         </a>
       </li>,
     );
-    txs[account.accountLabel] = (
-      <Transactions accountLabel={account.accountLabel} pageNumber={0} />
-    );
+    txs[account.accountLabel] = <Transactions account={account.accountLabel} 
/>;
   }
 
   return (
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx 
b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index e02c6efb1..708e28657 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -21,10 +21,10 @@ import { useTranslationContext } from 
"@gnu-taler/web-util/lib/index.browser";
 
 export function QrCodeSection({
   talerWithdrawUri,
-  abortButton,
+  onAbort,
 }: {
   talerWithdrawUri: string;
-  abortButton: h.JSX.Element;
+  onAbort: () => void;
 }): VNode {
   const { i18n } = useTranslationContext();
   useEffect(() => {
@@ -62,7 +62,10 @@ export function QrCodeSection({
             </i18n.Translate>
           </p>
           <br />
-          {abortButton}
+          <a
+            class="pure-button btn-cancel"
+            onClick={onAbort}
+          >{i18n.str`Abort`}</a>
         </div>
       </article>
     </section>
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx 
b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index 29f1bf5ee..247ef8d80 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,38 +13,36 @@
  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 { Logger } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { route } from "preact-router";
-import { StateUpdater, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
 import {
-  InternationalizationAPI,
+  RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
-import { BackendStateHandler } from "../hooks/backend.js";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType } from "../context/pageState.js";
+import { useTestingAPI } from "../hooks/access.js";
 import { bankUiSettings } from "../settings.js";
-import { getBankBackendBaseUrl, undefinedIfEmpty } from "../utils.js";
-import { BankFrame } from "./BankFrame.js";
+import { undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("RegistrationPage");
 
-export function RegistrationPage(): VNode {
+export function RegistrationPage({
+  onError,
+  onComplete,
+}: {
+  onComplete: () => void;
+  onError: (e: PageStateType["error"]) => void;
+}): VNode {
   const { i18n } = useTranslationContext();
   if (!bankUiSettings.allowRegistrations) {
     return (
-      <BankFrame>
-        <p>{i18n.str`Currently, the bank is not accepting new 
registrations!`}</p>
-      </BankFrame>
+      <p>{i18n.str`Currently, the bank is not accepting new 
registrations!`}</p>
     );
   }
-  return (
-    <BankFrame>
-      <RegistrationForm />
-    </BankFrame>
-  );
+  return <RegistrationForm onComplete={onComplete} onError={onError} />;
 }
 
 export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
@@ -53,13 +51,19 @@ export const PASSWORD_REGEX = /^[a-z0-9][a-zA-Z0-9]*$/;
 /**
  * Collect and submit registration data.
  */
-function RegistrationForm(): VNode {
+function RegistrationForm({
+  onComplete,
+  onError,
+}: {
+  onComplete: () => void;
+  onError: (e: PageStateType["error"]) => void;
+}): VNode {
   const backend = useBackendContext();
-  const { pageState, pageStateSetter } = usePageContext();
   const [username, setUsername] = useState<string | undefined>();
   const [password, setPassword] = useState<string | undefined>();
   const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
 
+  const { register } = useTestingAPI();
   const { i18n } = useTranslationContext();
 
   const errors = undefinedIfEmpty({
@@ -104,6 +108,7 @@ function RegistrationForm(): VNode {
                 name="register-un"
                 type="text"
                 placeholder="Username"
+                autocomplete="username"
                 value={username ?? ""}
                 onInput={(e): void => {
                   setUsername(e.currentTarget.value);
@@ -121,6 +126,7 @@ function RegistrationForm(): VNode {
                 name="register-pw"
                 id="register-pw"
                 placeholder="Password"
+                autocomplete="new-password"
                 value={password ?? ""}
                 required
                 onInput={(e): void => {
@@ -139,6 +145,7 @@ function RegistrationForm(): VNode {
                 style={{ marginBottom: 8 }}
                 name="register-repeat"
                 id="register-repeat"
+                autocomplete="new-password"
                 placeholder="Same password"
                 value={repeatPassword ?? ""}
                 required
@@ -155,19 +162,42 @@ function RegistrationForm(): VNode {
                 class="pure-button pure-button-primary btn-register"
                 type="submit"
                 disabled={!!errors}
-                onClick={(e) => {
+                onClick={async (e) => {
                   e.preventDefault();
-                  if (!username || !password) return;
-                  registrationCall(
-                    { username, password },
-                    backend, // will store BE URL, if OK.
-                    pageStateSetter,
-                    i18n,
-                  );
 
-                  setUsername(undefined);
-                  setPassword(undefined);
-                  setRepeatPassword(undefined);
+                  if (!username || !password) return;
+                  try {
+                    const credentials = { username, password };
+                    await register(credentials);
+                    setUsername(undefined);
+                    setPassword(undefined);
+                    setRepeatPassword(undefined);
+                    backend.logIn(credentials);
+                    onComplete();
+                  } catch (error) {
+                    if (error instanceof RequestError) {
+                      const errorData: SandboxBackend.SandboxError =
+                        error.info.error;
+                      if (error.info.status === HttpStatusCode.Conflict) {
+                        onError({
+                          title: i18n.str`That username is already taken`,
+                          description: errorData.error.description,
+                          debug: JSON.stringify(error.info),
+                        });
+                      } else {
+                        onError({
+                          title: i18n.str`New registration gave response 
error`,
+                          description: errorData.error.description,
+                          debug: JSON.stringify(error.info),
+                        });
+                      }
+                    } else if (error instanceof Error) {
+                      onError({
+                        title: i18n.str`Registration failed, please report`,
+                        description: error.message,
+                      });
+                    }
+                  }
                 }}
               >
                 {i18n.str`Register`}
@@ -180,7 +210,7 @@ function RegistrationForm(): VNode {
                   setUsername(undefined);
                   setPassword(undefined);
                   setRepeatPassword(undefined);
-                  route("/account");
+                  onComplete();
                 }}
               >
                 {i18n.str`Cancel`}
@@ -192,83 +222,3 @@ function RegistrationForm(): VNode {
     </Fragment>
   );
 }
-
-/**
- * This function requests /register.
- *
- * This function is responsible to change two states:
- * the backend's (to store the login credentials) and
- * the page's (to indicate a successful login or a problem).
- */
-async function registrationCall(
-  req: { username: string; password: string },
-  /**
-   * FIXME: figure out if the two following
-   * functions can be retrieved somewhat from
-   * the state.
-   */
-  backend: BackendStateHandler,
-  pageStateSetter: StateUpdater<PageStateType>,
-  i18n: InternationalizationAPI,
-): Promise<void> {
-  const url = getBankBackendBaseUrl();
-
-  const headers = new Headers();
-  headers.append("Content-Type", "application/json");
-  const registerEndpoint = new URL("access-api/testing/register", url);
-  let res: Response;
-  try {
-    res = await fetch(registerEndpoint.href, {
-      method: "POST",
-      body: JSON.stringify({
-        username: req.username,
-        password: req.password,
-      }),
-      headers,
-    });
-  } catch (error) {
-    logger.error(
-      `Could not POST new registration to the bank (${registerEndpoint.href})`,
-      error,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Registration failed, please report`,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  if (!res.ok) {
-    const response = await res.json();
-    if (res.status === 409) {
-      pageStateSetter((prevState) => ({
-        ...prevState,
-
-        error: {
-          title: i18n.str`That username is already taken`,
-          debug: JSON.stringify(response),
-        },
-      }));
-    } else {
-      pageStateSetter((prevState) => ({
-        ...prevState,
-
-        error: {
-          title: i18n.str`New registration gave response error`,
-          debug: JSON.stringify(response),
-        },
-      }));
-    }
-  } else {
-    // registration was ok
-    backend.save({
-      url,
-      username: req.username,
-      password: req.password,
-    });
-    route("/account");
-  }
-}
diff --git a/packages/demobank-ui/src/pages/Routing.tsx 
b/packages/demobank-ui/src/pages/Routing.tsx
index 3c3aae0ce..a88af9b0b 100644
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ b/packages/demobank-ui/src/pages/Routing.tsx
@@ -14,21 +14,97 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import {
+  HttpResponsePaginated,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { createHashHistory } from "history";
 import { h, VNode } from "preact";
 import Router, { route, Route } from "preact-router";
 import { useEffect } from "preact/hooks";
-import { AccountPage } from "./AccountPage.js";
+import { Loading } from "../components/Loading.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { HomePage } from "./HomePage.js";
+import { BankFrame } from "./BankFrame.js";
 import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
 import { RegistrationPage } from "./RegistrationPage.js";
 
+function handleNotOkResult(
+  safe: string,
+  saveError: (state: PageStateType["error"]) => void,
+  i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): <T, E>(result: HttpResponsePaginated<T, E>) => VNode {
+  return function handleNotOkResult2<T, E>(
+    result: HttpResponsePaginated<T, E>,
+  ): VNode {
+    if (result.clientError && result.isUnauthorized) {
+      route(safe);
+      return <Loading />;
+    }
+    if (result.clientError && result.isNotfound) {
+      route(safe);
+      return (
+        <div>Page not found, you are going to be redirected to {safe}</div>
+      );
+    }
+    if (result.loading) return <Loading />;
+    if (!result.ok) {
+      saveError({
+        title: i18n.str`The backend reported a problem: HTTP status 
#${result.status}`,
+        description: i18n.str`Diagnostic from ${result.info?.url} is 
"${result.message}"`,
+        debug: JSON.stringify(result.error),
+      });
+      route(safe);
+    }
+    return <div />;
+  };
+}
+
 export function Routing(): VNode {
   const history = createHashHistory();
+  const { pageStateSetter } = usePageContext();
+
+  function saveError(error: PageStateType["error"]): void {
+    pageStateSetter((prev) => ({ ...prev, error }));
+  }
+  const { i18n } = useTranslationContext();
   return (
     <Router history={history}>
-      <Route path="/public-accounts" component={PublicHistoriesPage} />
-      <Route path="/register" component={RegistrationPage} />
-      <Route path="/account" component={AccountPage} />
+      <Route
+        path="/public-accounts"
+        component={() => (
+          <BankFrame>
+            <PublicHistoriesPage
+              onLoadNotOk={handleNotOkResult("/account", saveError, i18n)}
+            />
+          </BankFrame>
+        )}
+      />
+      <Route
+        path="/register"
+        component={() => (
+          <BankFrame>
+            <RegistrationPage
+              onError={saveError}
+              onComplete={() => {
+                route("/account");
+              }}
+            />
+          </BankFrame>
+        )}
+      />
+      <Route
+        path="/account"
+        component={() => (
+          <BankFrame>
+            <HomePage
+              onRegister={() => {
+                route("/register");
+              }}
+            />
+          </BankFrame>
+        )}
+      />
       <Route default component={Redirect} to="/account" />
     </Router>
   );
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index a1b616657..2b2df3baa 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -14,36 +14,54 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Logger } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { Amounts, Logger } from "@gnu-taler/taler-util";
 import {
-  InternationalizationAPI,
+  RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
-import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders, validateAmount } from "../utils.js";
+import { h, VNode } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { useAccessAPI } from "../hooks/access.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("WalletWithdrawForm");
 
 export function WalletWithdrawForm({
   focus,
   currency,
+  onError,
+  onSuccess,
 }: {
-  currency?: string;
+  currency: string;
   focus?: boolean;
+  onError: (e: PageStateType["error"]) => void;
+  onSuccess: (
+    data: SandboxBackend.Access.BankAccountCreateWithdrawalResponse,
+  ) => void;
 }): VNode {
-  const backend = useBackendContext();
-  const { pageState, pageStateSetter } = usePageContext();
+  // const backend = useBackendContext();
+  // const { pageState, pageStateSetter } = usePageContext();
   const { i18n } = useTranslationContext();
-  let submitAmount: string | undefined = "5.00";
+  const { createWithdrawal } = useAccessAPI();
 
+  const [amount, setAmount] = useState<string | undefined>("5.00");
   const ref = useRef<HTMLInputElement>(null);
   useEffect(() => {
     if (focus) ref.current?.focus();
   }, [focus]);
+
+  const amountFloat = amount ? parseFloat(amount) : undefined;
+  const errors = undefinedIfEmpty({
+    amount: !amountFloat
+      ? i18n.str`required`
+      : Number.isNaN(amountFloat)
+      ? i18n.str`should be a number`
+      : amountFloat < 0
+      ? i18n.str`should be positive`
+      : undefined,
+  });
   return (
     <form
       id="reserve-form"
@@ -63,8 +81,8 @@ export function WalletWithdrawForm({
             type="text"
             readonly
             class="currency-indicator"
-            size={currency?.length ?? 5}
-            maxLength={currency?.length}
+            size={currency.length}
+            maxLength={currency.length}
             tabIndex={-1}
             value={currency}
           />
@@ -74,14 +92,15 @@ export function WalletWithdrawForm({
             ref={ref}
             id="withdraw-amount"
             name="withdraw-amount"
-            value={submitAmount}
+            value={amount ?? ""}
             onChange={(e): void => {
-              // FIXME: validate using 'parseAmount()',
-              // deactivate submit button as long as
-              // amount is not valid
-              submitAmount = e.currentTarget.value;
+              setAmount(e.currentTarget.value);
             }}
           />
+          <ShowInputErrorLabel
+            message={errors?.amount}
+            isDirty={amount !== undefined}
+          />
         </div>
       </p>
       <p>
@@ -90,22 +109,34 @@ export function WalletWithdrawForm({
             id="select-exchange"
             class="pure-button pure-button-primary"
             type="submit"
+            disabled={!!errors}
             value={i18n.str`Withdraw`}
-            onClick={(e) => {
+            onClick={async (e) => {
               e.preventDefault();
-              submitAmount = validateAmount(submitAmount);
-              /**
-               * By invalid amounts, the validator prints error messages
-               * on the console, and the browser colourizes the amount input
-               * box to indicate a error.
-               */
-              if (!submitAmount && currency) return;
-              createWithdrawalCall(
-                `${currency}:${submitAmount}`,
-                backend.state,
-                pageStateSetter,
-                i18n,
-              );
+              if (!amountFloat) return;
+              try {
+                const result = await createWithdrawal({
+                  amount: Amounts.stringify(
+                    Amounts.fromFloat(amountFloat, currency),
+                  ),
+                });
+
+                onSuccess(result.data);
+              } catch (error) {
+                if (error instanceof RequestError) {
+                  onError({
+                    title: i18n.str`Could not create withdrawal operation`,
+                    description: (error as any).error.description,
+                    debug: JSON.stringify(error),
+                  });
+                }
+                if (error instanceof Error) {
+                  onError({
+                    title: i18n.str`Something when wrong trying to start the 
withdrawal`,
+                    description: error.message,
+                  });
+                }
+              }
             }}
           />
         </div>
@@ -114,84 +145,84 @@ export function WalletWithdrawForm({
   );
 }
 
-/**
- * This function creates a withdrawal operation via the Access API.
- *
- * After having successfully created the withdrawal operation, the
- * user should receive a QR code of the "taler://withdraw/" type and
- * supposed to scan it with their phone.
- *
- * TODO: (1) after the scan, the page should refresh itself and inform
- * the user about the operation's outcome.  (2) use POST helper.  */
-async function createWithdrawalCall(
-  amount: string,
-  backendState: BackendState,
-  pageStateSetter: StateUpdater<PageStateType>,
-  i18n: InternationalizationAPI,
-): Promise<void> {
-  if (backendState?.status === "loggedOut") {
-    logger.error("Page has a problem: no credentials found in the state.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`No credentials given.`,
-      },
-    }));
-    return;
-  }
-
-  let res: Response;
-  try {
-    const { username, password } = backendState;
-    const headers = prepareHeaders(username, password);
-
-    // Let bank generate withdraw URI:
-    const url = new URL(
-      `access-api/accounts/${backendState.username}/withdrawals`,
-      backendState.url,
-    );
-    res = await fetch(url.href, {
-      method: "POST",
-      headers,
-      body: JSON.stringify({ amount }),
-    });
-  } catch (error) {
-    logger.trace("Could not POST withdrawal request to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Could not create withdrawal operation`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  if (!res.ok) {
-    const response = await res.json();
-    logger.error(
-      `Withdrawal creation gave response error: ${response} (${res.status})`,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Withdrawal creation gave response error`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-
-  logger.trace("Withdrawal operation created!");
-  const resp = await res.json();
-  pageStateSetter((prevState: PageStateType) => ({
-    ...prevState,
-    withdrawalInProgress: true,
-    talerWithdrawUri: resp.taler_withdraw_uri,
-    withdrawalId: resp.withdrawal_id,
-  }));
-}
+// /**
+//  * This function creates a withdrawal operation via the Access API.
+//  *
+//  * After having successfully created the withdrawal operation, the
+//  * user should receive a QR code of the "taler://withdraw/" type and
+//  * supposed to scan it with their phone.
+//  *
+//  * TODO: (1) after the scan, the page should refresh itself and inform
+//  * the user about the operation's outcome.  (2) use POST helper.  */
+// async function createWithdrawalCall(
+//   amount: string,
+//   backendState: BackendState,
+//   pageStateSetter: StateUpdater<PageStateType>,
+//   i18n: InternationalizationAPI,
+// ): Promise<void> {
+//   if (backendState?.status === "loggedOut") {
+//     logger.error("Page has a problem: no credentials found in the state.");
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
+
+//       error: {
+//         title: i18n.str`No credentials given.`,
+//       },
+//     }));
+//     return;
+//   }
+
+//   let res: Response;
+//   try {
+//     const { username, password } = backendState;
+//     const headers = prepareHeaders(username, password);
+
+//     // Let bank generate withdraw URI:
+//     const url = new URL(
+//       `access-api/accounts/${backendState.username}/withdrawals`,
+//       backendState.url,
+//     );
+//     res = await fetch(url.href, {
+//       method: "POST",
+//       headers,
+//       body: JSON.stringify({ amount }),
+//     });
+//   } catch (error) {
+//     logger.trace("Could not POST withdrawal request to the bank", error);
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
+
+//       error: {
+//         title: i18n.str`Could not create withdrawal operation`,
+//         description: (error as any).error.description,
+//         debug: JSON.stringify(error),
+//       },
+//     }));
+//     return;
+//   }
+//   if (!res.ok) {
+//     const response = await res.json();
+//     logger.error(
+//       `Withdrawal creation gave response error: ${response} 
(${res.status})`,
+//     );
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
+
+//       error: {
+//         title: i18n.str`Withdrawal creation gave response error`,
+//         description: response.error.description,
+//         debug: JSON.stringify(response),
+//       },
+//     }));
+//     return;
+//   }
+
+//   logger.trace("Withdrawal operation created!");
+//   const resp = await res.json();
+//   pageStateSetter((prevState: PageStateType) => ({
+//     ...prevState,
+//     withdrawalInProgress: true,
+//     talerWithdrawUri: resp.taler_withdraw_uri,
+//     withdrawalId: resp.withdrawal_id,
+//   }));
+// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index b87b77c83..4e5c621e2 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,24 +15,29 @@
  */
 
 import { Logger } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
-import { StateUpdater, useMemo, useState } from "preact/hooks";
+import { useMemo, useState } from "preact/hooks";
 import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import {
-  InternationalizationAPI,
-  useTranslationContext,
-} from "@gnu-taler/web-util/lib/index.browser";
-import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders } from "../utils.js";
+import { usePageContext } from "../context/pageState.js";
+import { useAccessAPI } from "../hooks/access.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("WithdrawalConfirmationQuestion");
 
+interface Props {
+  account: string;
+  withdrawalId: string;
+}
 /**
  * Additional authentication required to complete the operation.
  * Not providing a back button, only abort.
  */
-export function WithdrawalConfirmationQuestion(): VNode {
+export function WithdrawalConfirmationQuestion({
+  account,
+  withdrawalId,
+}: Props): VNode {
   const { pageState, pageStateSetter } = usePageContext();
   const backend = useBackendContext();
   const { i18n } = useTranslationContext();
@@ -42,10 +47,20 @@ export function WithdrawalConfirmationQuestion(): VNode {
       a: Math.floor(Math.random() * 10),
       b: Math.floor(Math.random() * 10),
     };
-  }, [pageState.withdrawalId]);
+  }, []);
 
+  const { confirmWithdrawal, abortWithdrawal } = useAccessAPI();
   const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
-
+  const answer = parseInt(captchaAnswer ?? "", 10);
+  const errors = undefinedIfEmpty({
+    answer: !captchaAnswer
+      ? i18n.str`Answer the question before continue`
+      : Number.isNaN(answer)
+      ? i18n.str`The answer should be a number`
+      : answer !== captchaNumbers.a + captchaNumbers.b
+      ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + 
${captchaNumbers.b}" is wrong.`
+      : undefined,
+  });
   return (
     <Fragment>
       <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
@@ -82,33 +97,49 @@ export function WithdrawalConfirmationQuestion(): VNode {
                     setCaptchaAnswer(e.currentTarget.value);
                   }}
                 />
+                <ShowInputErrorLabel
+                  message={errors?.answer}
+                  isDirty={captchaAnswer !== undefined}
+                />
               </p>
               <p>
                 <button
                   type="submit"
                   class="pure-button pure-button-primary btn-confirm"
+                  disabled={!!errors}
                   onClick={async (e) => {
                     e.preventDefault();
-                    if (
-                      captchaAnswer ==
-                      (captchaNumbers.a + captchaNumbers.b).toString()
-                    ) {
-                      await confirmWithdrawalCall(
-                        backend.state,
-                        pageState.withdrawalId,
-                        pageStateSetter,
-                        i18n,
-                      );
-                      return;
+                    try {
+                      await confirmWithdrawal(withdrawalId);
+                      pageStateSetter((prevState) => {
+                        const { talerWithdrawUri, ...rest } = prevState;
+                        return {
+                          ...rest,
+                          info: i18n.str`Withdrawal confirmed!`,
+                        };
+                      });
+                    } catch (error) {
+                      pageStateSetter((prevState) => ({
+                        ...prevState,
+                        error: {
+                          title: i18n.str`Could not confirm the withdrawal`,
+                          description: (error as any).error.description,
+                          debug: JSON.stringify(error),
+                        },
+                      }));
                     }
-                    pageStateSetter((prevState: PageStateType) => ({
-                      ...prevState,
-
-                      error: {
-                        title: i18n.str`The answer "${captchaAnswer}" to 
"${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`,
-                      },
-                    }));
-                    setCaptchaAnswer(undefined);
+                    // if (
+                    //   captchaAnswer ==
+                    //   (captchaNumbers.a + captchaNumbers.b).toString()
+                    // ) {
+                    //   await confirmWithdrawalCall(
+                    //     backend.state,
+                    //     pageState.withdrawalId,
+                    //     pageStateSetter,
+                    //     i18n,
+                    //   );
+                    //   return;
+                    // }
                   }}
                 >
                   {i18n.str`Confirm`}
@@ -118,12 +149,31 @@ export function WithdrawalConfirmationQuestion(): VNode {
                   class="pure-button pure-button-secondary btn-cancel"
                   onClick={async (e) => {
                     e.preventDefault();
-                    await abortWithdrawalCall(
-                      backend.state,
-                      pageState.withdrawalId,
-                      pageStateSetter,
-                      i18n,
-                    );
+                    try {
+                      await abortWithdrawal(withdrawalId);
+                      pageStateSetter((prevState) => {
+                        const { talerWithdrawUri, ...rest } = prevState;
+                        return {
+                          ...rest,
+                          info: i18n.str`Withdrawal confirmed!`,
+                        };
+                      });
+                    } catch (error) {
+                      pageStateSetter((prevState) => ({
+                        ...prevState,
+                        error: {
+                          title: i18n.str`Could not confirm the withdrawal`,
+                          description: (error as any).error.description,
+                          debug: JSON.stringify(error),
+                        },
+                      }));
+                    }
+                    // await abortWithdrawalCall(
+                    //   backend.state,
+                    //   pageState.withdrawalId,
+                    //   pageStateSetter,
+                    //   i18n,
+                    // );
                   }}
                 >
                   {i18n.str`Cancel`}
@@ -156,188 +206,188 @@ export function WithdrawalConfirmationQuestion(): VNode 
{
  * This function will set the confirmation status in the
  * 'page state' and let the related components refresh.
  */
-async function confirmWithdrawalCall(
-  backendState: BackendState,
-  withdrawalId: string | undefined,
-  pageStateSetter: StateUpdater<PageStateType>,
-  i18n: InternationalizationAPI,
-): Promise<void> {
-  if (backendState.status === "loggedOut") {
-    logger.error("No credentials found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
+// async function confirmWithdrawalCall(
+//   backendState: BackendState,
+//   withdrawalId: string | undefined,
+//   pageStateSetter: StateUpdater<PageStateType>,
+//   i18n: InternationalizationAPI,
+// ): Promise<void> {
+//   if (backendState.status === "loggedOut") {
+//     logger.error("No credentials found.");
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`No credentials found.`,
-      },
-    }));
-    return;
-  }
-  if (typeof withdrawalId === "undefined") {
-    logger.error("No withdrawal ID found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
+//       error: {
+//         title: i18n.str`No credentials found.`,
+//       },
+//     }));
+//     return;
+//   }
+//   if (typeof withdrawalId === "undefined") {
+//     logger.error("No withdrawal ID found.");
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`No withdrawal ID found.`,
-      },
-    }));
-    return;
-  }
-  let res: Response;
-  try {
-    const { username, password } = backendState;
-    const headers = prepareHeaders(username, password);
-    /**
-     * NOTE: tests show that when a same object is being
-     * POSTed, caching might prevent same requests from being
-     * made.  Hence, trying to POST twice the same amount might
-     * get silently ignored.
-     *
-     * headers.append("cache-control", "no-store");
-     * headers.append("cache-control", "no-cache");
-     * headers.append("pragma", "no-cache");
-     * */
+//       error: {
+//         title: i18n.str`No withdrawal ID found.`,
+//       },
+//     }));
+//     return;
+//   }
+//   let res: Response;
+//   try {
+//     const { username, password } = backendState;
+//     const headers = prepareHeaders(username, password);
+//     /**
+//      * NOTE: tests show that when a same object is being
+//      * POSTed, caching might prevent same requests from being
+//      * made.  Hence, trying to POST twice the same amount might
+//      * get silently ignored.
+//      *
+//      * headers.append("cache-control", "no-store");
+//      * headers.append("cache-control", "no-cache");
+//      * headers.append("pragma", "no-cache");
+//      * */
 
-    // Backend URL must have been stored _with_ a final slash.
-    const url = new URL(
-      
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
-      backendState.url,
-    );
-    res = await fetch(url.href, {
-      method: "POST",
-      headers,
-    });
-  } catch (error) {
-    logger.error("Could not POST withdrawal confirmation to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
+//     // Backend URL must have been stored _with_ a final slash.
+//     const url = new URL(
+//       
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
+//       backendState.url,
+//     );
+//     res = await fetch(url.href, {
+//       method: "POST",
+//       headers,
+//     });
+//   } catch (error) {
+//     logger.error("Could not POST withdrawal confirmation to the bank", 
error);
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`Could not confirm the withdrawal`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  if (!res || !res.ok) {
-    const response = await res.json();
-    // assume not ok if res is null
-    logger.error(
-      `Withdrawal confirmation gave response error (${res.status})`,
-      res.statusText,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
+//       error: {
+//         title: i18n.str`Could not confirm the withdrawal`,
+//         description: (error as any).error.description,
+//         debug: JSON.stringify(error),
+//       },
+//     }));
+//     return;
+//   }
+//   if (!res || !res.ok) {
+//     const response = await res.json();
+//     // assume not ok if res is null
+//     logger.error(
+//       `Withdrawal confirmation gave response error (${res.status})`,
+//       res.statusText,
+//     );
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`Withdrawal confirmation gave response error`,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  logger.trace("Withdrawal operation confirmed!");
-  pageStateSetter((prevState) => {
-    const { talerWithdrawUri, ...rest } = prevState;
-    return {
-      ...rest,
+//       error: {
+//         title: i18n.str`Withdrawal confirmation gave response error`,
+//         debug: JSON.stringify(response),
+//       },
+//     }));
+//     return;
+//   }
+//   logger.trace("Withdrawal operation confirmed!");
+//   pageStateSetter((prevState) => {
+//     const { talerWithdrawUri, ...rest } = prevState;
+//     return {
+//       ...rest,
 
-      info: i18n.str`Withdrawal confirmed!`,
-    };
-  });
-}
+//       info: i18n.str`Withdrawal confirmed!`,
+//     };
+//   });
+// }
 
-/**
- * Abort a withdrawal operation via the Access API's /abort.
- */
-async function abortWithdrawalCall(
-  backendState: BackendState,
-  withdrawalId: string | undefined,
-  pageStateSetter: StateUpdater<PageStateType>,
-  i18n: InternationalizationAPI,
-): Promise<void> {
-  if (backendState.status === "loggedOut") {
-    logger.error("No credentials found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
+// /**
+//  * Abort a withdrawal operation via the Access API's /abort.
+//  */
+// async function abortWithdrawalCall(
+//   backendState: BackendState,
+//   withdrawalId: string | undefined,
+//   pageStateSetter: StateUpdater<PageStateType>,
+//   i18n: InternationalizationAPI,
+// ): Promise<void> {
+//   if (backendState.status === "loggedOut") {
+//     logger.error("No credentials found.");
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`No credentials found.`,
-      },
-    }));
-    return;
-  }
-  if (typeof withdrawalId === "undefined") {
-    logger.error("No withdrawal ID found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
+//       error: {
+//         title: i18n.str`No credentials found.`,
+//       },
+//     }));
+//     return;
+//   }
+//   if (typeof withdrawalId === "undefined") {
+//     logger.error("No withdrawal ID found.");
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`No withdrawal ID found.`,
-      },
-    }));
-    return;
-  }
-  let res: Response;
-  try {
-    const { username, password } = backendState;
-    const headers = prepareHeaders(username, password);
-    /**
-     * NOTE: tests show that when a same object is being
-     * POSTed, caching might prevent same requests from being
-     * made.  Hence, trying to POST twice the same amount might
-     * get silently ignored.  Needs more observation!
-     *
-     * headers.append("cache-control", "no-store");
-     * headers.append("cache-control", "no-cache");
-     * headers.append("pragma", "no-cache");
-     * */
+//       error: {
+//         title: i18n.str`No withdrawal ID found.`,
+//       },
+//     }));
+//     return;
+//   }
+//   let res: Response;
+//   try {
+//     const { username, password } = backendState;
+//     const headers = prepareHeaders(username, password);
+//     /**
+//      * NOTE: tests show that when a same object is being
+//      * POSTed, caching might prevent same requests from being
+//      * made.  Hence, trying to POST twice the same amount might
+//      * get silently ignored.  Needs more observation!
+//      *
+//      * headers.append("cache-control", "no-store");
+//      * headers.append("cache-control", "no-cache");
+//      * headers.append("pragma", "no-cache");
+//      * */
 
-    // Backend URL must have been stored _with_ a final slash.
-    const url = new URL(
-      
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
-      backendState.url,
-    );
-    res = await fetch(url.href, { method: "POST", headers });
-  } catch (error) {
-    logger.error("Could not abort the withdrawal", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
+//     // Backend URL must have been stored _with_ a final slash.
+//     const url = new URL(
+//       
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
+//       backendState.url,
+//     );
+//     res = await fetch(url.href, { method: "POST", headers });
+//   } catch (error) {
+//     logger.error("Could not abort the withdrawal", error);
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`Could not abort the withdrawal.`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  if (!res.ok) {
-    const response = await res.json();
-    logger.error(
-      `Withdrawal abort gave response error (${res.status})`,
-      res.statusText,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
+//       error: {
+//         title: i18n.str`Could not abort the withdrawal.`,
+//         description: (error as any).error.description,
+//         debug: JSON.stringify(error),
+//       },
+//     }));
+//     return;
+//   }
+//   if (!res.ok) {
+//     const response = await res.json();
+//     logger.error(
+//       `Withdrawal abort gave response error (${res.status})`,
+//       res.statusText,
+//     );
+//     pageStateSetter((prevState) => ({
+//       ...prevState,
 
-      error: {
-        title: i18n.str`Withdrawal abortion failed.`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  logger.trace("Withdrawal operation aborted!");
-  pageStateSetter((prevState) => {
-    const { ...rest } = prevState;
-    return {
-      ...rest,
+//       error: {
+//         title: i18n.str`Withdrawal abortion failed.`,
+//         description: response.error.description,
+//         debug: JSON.stringify(response),
+//       },
+//     }));
+//     return;
+//   }
+//   logger.trace("Withdrawal operation aborted!");
+//   pageStateSetter((prevState) => {
+//     const { ...rest } = prevState;
+//     return {
+//       ...rest,
 
-      info: i18n.str`Withdrawal aborted!`,
-    };
-  });
-}
+//       info: i18n.str`Withdrawal aborted!`,
+//     };
+//   });
+// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx 
b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 174c19288..fd91c0e1a 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -15,106 +15,67 @@
  */
 
 import { Logger } from "@gnu-taler/taler-util";
+import {
+  HttpResponsePaginated,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
-import useSWR from "swr";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { Loading } from "../components/Loading.js";
+import { usePageContext } from "../context/pageState.js";
+import { useWithdrawalDetails } from "../hooks/access.js";
 import { QrCodeSection } from "./QrCodeSection.js";
 import { WithdrawalConfirmationQuestion } from 
"./WithdrawalConfirmationQuestion.js";
 
 const logger = new Logger("WithdrawalQRCode");
+
+interface Props {
+  account: string;
+  withdrawalId: string;
+  talerWithdrawUri: string;
+  onAbort: () => void;
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+}
 /**
  * Offer the QR code (and a clickable taler://-link) to
  * permit the passing of exchange and reserve details to
  * the bank.  Poll the backend until such operation is done.
  */
 export function WithdrawalQRCode({
+  account,
   withdrawalId,
   talerWithdrawUri,
-}: {
-  withdrawalId: string;
-  talerWithdrawUri: string;
-}): VNode {
-  // turns true when the wallet POSTed the reserve details:
-  const { pageState, pageStateSetter } = usePageContext();
-  const { i18n } = useTranslationContext();
-  const abortButton = (
-    <a
-      class="pure-button btn-cancel"
-      onClick={() => {
-        pageStateSetter((prevState: PageStateType) => {
-          return {
-            ...prevState,
-            withdrawalId: undefined,
-            talerWithdrawUri: undefined,
-            withdrawalInProgress: false,
-          };
-        });
-      }}
-    >{i18n.str`Abort`}</a>
-  );
-
+  onAbort,
+  onLoadNotOk,
+}: Props): VNode {
   logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`);
-  // waiting for the wallet:
-
-  const { data, error } = useSWR(
-    `integration-api/withdrawal-operation/${withdrawalId}`,
-    { refreshInterval: 1000 },
-  );
 
-  if (typeof error !== "undefined") {
-    logger.error(
-      `withdrawal (${withdrawalId}) was never (correctly) created at the 
bank...`,
-      error,
-    );
-    pageStateSetter((prevState: PageStateType) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`withdrawal (${withdrawalId}) was never (correctly) 
created at the bank...`,
-      },
-    }));
-    return (
-      <Fragment>
-        <br />
-        <br />
-        {abortButton}
-      </Fragment>
-    );
+  const result = useWithdrawalDetails(account, withdrawalId);
+  if (!result.ok) {
+    return onLoadNotOk(result);
   }
+  const { data } = result;
 
-  // data didn't arrive yet and wallet didn't communicate:
-  if (typeof data === "undefined")
-    return <p>{i18n.str`Waiting the bank to create the operation...`}</p>;
-
-  /**
-   * Wallet didn't communicate withdrawal details yet:
-   */
   logger.trace("withdrawal status", data);
-  if (data.aborted)
-    pageStateSetter((prevState: PageStateType) => {
-      const { withdrawalId, talerWithdrawUri, ...rest } = prevState;
-      return {
-        ...rest,
-        withdrawalInProgress: false,
-
-        error: {
-          title: i18n.str`This withdrawal was aborted!`,
-        },
-      };
-    });
+  if (data.aborted) {
+    //signal that this withdrawal is aborted
+    //will redirect to account info
+    onAbort();
+    return <Loading />;
+  }
 
   if (!data.selection_done) {
     return (
-      <QrCodeSection
-        talerWithdrawUri={talerWithdrawUri}
-        abortButton={abortButton}
-      />
+      <QrCodeSection talerWithdrawUri={talerWithdrawUri} onAbort={onAbort} />
     );
   }
   /**
    * Wallet POSTed the withdrawal details!  Ask the
    * user to authorize the operation (here CAPTCHA).
    */
-  return <WithdrawalConfirmationQuestion />;
+  return (
+    <WithdrawalConfirmationQuestion
+      account={account}
+      withdrawalId={talerWithdrawUri}
+    />
+  );
 }
diff --git a/packages/demobank-ui/src/scss/bank.scss 
b/packages/demobank-ui/src/scss/bank.scss
index e8a4d664c..c55dfe966 100644
--- a/packages/demobank-ui/src/scss/bank.scss
+++ b/packages/demobank-ui/src/scss/bank.scss
@@ -268,3 +268,10 @@ html {
 h1.nav {
   text-align: center;
 }
+
+.pure-form > fieldset > label {
+  display: block;
+}
+.pure-form > fieldset > input[disabled] {
+  color: black !important;
+}
diff --git a/packages/demobank-ui/src/utils.ts 
b/packages/demobank-ui/src/utils.ts
index e1d35a2b5..0dc24e468 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -43,30 +43,42 @@ export function getIbanFromPayto(url: string): string {
   return iban;
 }
 
-const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/";;
-
-export function getBankBackendBaseUrl(): string {
-  const overrideUrl = localStorage.getItem("bank-base-url");
-  return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath);
-}
-
 export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
   return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
     ? obj
     : undefined;
 }
 
+export type PartialButDefined<T> = {
+  [P in keyof T]: T[P] | undefined;
+};
+
+export type WithIntermediate<Type extends object> = {
+  [prop in keyof Type]: Type[prop] extends object ? 
WithIntermediate<Type[prop]> : (Type[prop] | undefined);
+}
+
+// export function partialWithObjects<T extends object>(obj: T | undefined, () 
=> complete): WithIntermediate<T> {
+//   const root = obj === undefined ? {} : obj;
+//   return Object.entries(root).([key, value]) => {
+
+//   })
+//   return undefined as any
+// }
+
 /**
  * Craft headers with Authorization and Content-Type.
  */
-export function prepareHeaders(username?: string, password?: string): Headers {
-  const headers = new Headers();
-  if (username && password) {
-    headers.append(
-      "Authorization",
-      `Basic ${window.btoa(`${username}:${password}`)}`,
-    );
-  }
-  headers.append("Content-Type", "application/json");
-  return headers;
-}
+// export function prepareHeaders(username?: string, password?: string): 
Headers {
+//   const headers = new Headers();
+//   if (username && password) {
+//     headers.append(
+//       "Authorization",
+//       `Basic ${window.btoa(`${username}:${password}`)}`,
+//     );
+//   }
+//   headers.append("Content-Type", "application/json");
+//   return headers;
+// }
+
+export const PAGE_SIZE = 20;
+export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx 
b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index 56f223620..5929b031a 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -19,7 +19,10 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  useTranslationContext,
+  HttpError,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { format } from "date-fns";
 import { Fragment, FunctionComponent, h, VNode } from "preact";
 import { Route, route, Router } from "preact-router";
@@ -28,7 +31,6 @@ import { Loading } from "./components/exception/loading.js";
 import { Menu, NotificationCard } from "./components/menu/index.js";
 import { useBackendContext } from "./context/backend.js";
 import { InstanceContextProvider } from "./context/instance.js";
-import { HttpError } from "./utils/request.js";
 import {
   useBackendDefaultToken,
   useBackendInstanceToken,
@@ -63,6 +65,7 @@ import InstanceUpdatePage, {
 import LoginPage from "./paths/login/index.js";
 import NotFoundPage from "./paths/notfound/index.js";
 import { Notification } from "./utils/types.js";
+import { MerchantBackend } from "./declaration.js";
 
 export enum InstancePaths {
   // details = '/',
@@ -157,7 +160,9 @@ export function InstanceRoutes({
   );
 
   function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
-    return function ServerErrorRedirectToImpl(error: HttpError) {
+    return function ServerErrorRedirectToImpl(
+      error: HttpError<MerchantBackend.ErrorDetail>,
+    ) {
       setGlobalNotification({
         message: i18n.str`The backend reported a problem: HTTP status 
#${error.status}`,
         description: i18n.str`Diagnostic from ${error.info?.url} is 
"${error.message}"`,
@@ -551,7 +556,7 @@ function AdminInstanceUpdatePage({
 }: { id: string } & InstanceUpdatePageProps): VNode {
   const [token, changeToken] = useBackendInstanceToken(id);
   const { updateLoginStatus: changeBackend } = useBackendContext();
-  const updateLoginStatus = (url: string, token?: string) => {
+  const updateLoginStatus = (url: string, token?: string): void => {
     changeBackend(url);
     if (token) changeToken(token);
   };
@@ -566,7 +571,7 @@ function AdminInstanceUpdatePage({
       <InstanceAdminUpdatePage
         {...rest}
         instanceId={id}
-        onLoadError={(error: HttpError) => {
+        onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => {
           return (
             <Fragment>
               <NotificationCard
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx 
b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
index 43a7af1a3..4e4031c6a 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
@@ -86,7 +86,7 @@ export function InputImage<T>({
                 }
                 setSizeError(false);
                 return f[0].arrayBuffer().then((b) => {
-                  const b64 = btoa(
+                  const b64 = window.btoa(
                     new Uint8Array(b).reduce(
                       (data, byte) => data + String.fromCharCode(byte),
                       "",
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts 
b/packages/merchant-backoffice-ui/src/declaration.d.ts
index 32e6b44ea..e65835bfd 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1355,14 +1355,13 @@ export namespace MerchantBackend {
 
     interface UsingTemplateResponse {
       // After enter the request. The user will be pay with a taler URL.
-      order_id: string,
-      token: string,
+      order_id: string;
+      token: string;
     }
   }
 
   namespace Webhooks {
     interface WebhookAddDetails {
-
       // Webhook ID to use.
       webhook_id: string;
 
@@ -1380,10 +1379,8 @@ export namespace MerchantBackend {
 
       // Body template by the webhook
       body_template?: string;
-
     }
     interface WebhookPatchDetails {
-
       // The event of the webhook: why the webhook is used.
       event_type: string;
 
@@ -1398,25 +1395,19 @@ export namespace MerchantBackend {
 
       // Body template by the webhook
       body_template?: string;
-
     }
     interface WebhookSummaryResponse {
-
       // List of webhooks that are present in our backend.
       webhooks: WebhookEntry[];
-
     }
     interface WebhookEntry {
-
       // Webhook identifier, as found in the webhook.
       webhook_id: string;
 
       // The event of the webhook: why the webhook is used.
       event_type: string;
-
     }
     interface WebhookDetails {
-
       // The event of the webhook: why the webhook is used.
       event_type: string;
 
@@ -1431,9 +1422,7 @@ export namespace MerchantBackend {
 
       // Body template by the webhook
       body_template?: string;
-
     }
-
   }
 
   interface ContractTerms {
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts 
b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index 3f3db2fa1..952b33f7e 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -28,8 +28,8 @@ import {
   HttpResponse,
   HttpResponseOk,
   RequestOptions,
-} from "../utils/request.js";
-import { useApiContext } from "../context/api.js";
+} from "@gnu-taler/web-util/lib/index.browser";
+import { useApiContext } from "@gnu-taler/web-util/lib/index.browser";
 
 export function useMatchMutate(): (
   re: RegExp,
@@ -54,12 +54,17 @@ export function useMatchMutate(): (
   };
 }
 
-export function useBackendInstancesTestForAdmin(): 
HttpResponse<MerchantBackend.Instances.InstancesResponse> {
+export function useBackendInstancesTestForAdmin(): HttpResponse<
+  MerchantBackend.Instances.InstancesResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { request } = useBackendBaseRequest();
 
   type Type = MerchantBackend.Instances.InstancesResponse;
 
-  const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });
+  const [result, setResult] = useState<
+    HttpResponse<Type, MerchantBackend.ErrorDetail>
+  >({ loading: true });
 
   useEffect(() => {
     request<Type>(`/management/instances`)
@@ -70,12 +75,17 @@ export function useBackendInstancesTestForAdmin(): 
HttpResponse<MerchantBackend.
   return result;
 }
 
-export function useBackendConfig(): 
HttpResponse<MerchantBackend.VersionResponse> {
+export function useBackendConfig(): HttpResponse<
+  MerchantBackend.VersionResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { request } = useBackendBaseRequest();
 
   type Type = MerchantBackend.VersionResponse;
 
-  const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });
+  const [result, setResult] = useState<
+    HttpResponse<Type, MerchantBackend.ErrorDetail>
+  >({ loading: true });
 
   useEffect(() => {
     request<Type>(`/config`)
@@ -88,15 +98,15 @@ export function useBackendConfig(): 
HttpResponse<MerchantBackend.VersionResponse
 
 interface useBackendInstanceRequestType {
   request: <T>(
-    path: string,
+    endpoint: string,
     options?: RequestOptions,
   ) => Promise<HttpResponseOk<T>>;
-  fetcher: <T>(path: string) => Promise<HttpResponseOk<T>>;
-  reserveDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>;
-  tipsDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>;
+  fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+  reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+  tipsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
   multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>;
   orderFetcher: <T>(
-    path: string,
+    endpoint: string,
     paid?: YesOrNo,
     refunded?: YesOrNo,
     wired?: YesOrNo,
@@ -104,26 +114,26 @@ interface useBackendInstanceRequestType {
     delta?: number,
   ) => Promise<HttpResponseOk<T>>;
   transferFetcher: <T>(
-    path: string,
+    endpoint: string,
     payto_uri?: string,
     verified?: string,
     position?: string,
     delta?: number,
   ) => Promise<HttpResponseOk<T>>;
   templateFetcher: <T>(
-    path: string,
+    endpoint: string,
     position?: string,
     delta?: number,
   ) => Promise<HttpResponseOk<T>>;
   webhookFetcher: <T>(
-    path: string,
+    endpoint: string,
     position?: string,
     delta?: number,
   ) => Promise<HttpResponseOk<T>>;
 }
 interface useBackendBaseRequestType {
   request: <T>(
-    path: string,
+    endpoint: string,
     options?: RequestOptions,
   ) => Promise<HttpResponseOk<T>>;
 }
@@ -141,10 +151,10 @@ export function useBackendBaseRequest(): 
useBackendBaseRequestType {
 
   const request = useCallback(
     function requestImpl<T>(
-      path: string,
+      endpoint: string,
       options: RequestOptions = {},
     ): Promise<HttpResponseOk<T>> {
-      return requestHandler<T>(backend, path, { token, ...options });
+      return requestHandler<T>(backend, endpoint, { token, ...options });
     },
     [backend, token],
   );
@@ -153,45 +163,47 @@ export function useBackendBaseRequest(): 
useBackendBaseRequestType {
 }
 
 export function useBackendInstanceRequest(): useBackendInstanceRequestType {
-  const { url: baseUrl, token: baseToken } = useBackendContext();
+  const { url: rootBackendUrl, token: rootToken } = useBackendContext();
   const { token: instanceToken, id, admin } = useInstanceContext();
   const { request: requestHandler } = useApiContext();
 
-  const { backend, token } = !admin
-    ? { backend: baseUrl, token: baseToken }
-    : { backend: `${baseUrl}/instances/${id}`, token: instanceToken };
+  const { baseUrl, token } = !admin
+    ? { baseUrl: rootBackendUrl, token: rootToken }
+    : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
 
   const request = useCallback(
     function requestImpl<T>(
-      path: string,
+      endpoint: string,
       options: RequestOptions = {},
     ): Promise<HttpResponseOk<T>> {
-      return requestHandler<T>(backend, path, { token, ...options });
+      return requestHandler<T>(baseUrl, endpoint, { token, ...options });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const multiFetcher = useCallback(
     function multiFetcherImpl<T>(
-      paths: string[],
+      endpoints: string[],
     ): Promise<HttpResponseOk<T>[]> {
       return Promise.all(
-        paths.map((path) => requestHandler<T>(backend, path, { token })),
+        endpoints.map((endpoint) =>
+          requestHandler<T>(baseUrl, endpoint, { token }),
+        ),
       );
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const fetcher = useCallback(
-    function fetcherImpl<T>(path: string): Promise<HttpResponseOk<T>> {
-      return requestHandler<T>(backend, path, { token });
+    function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
+      return requestHandler<T>(baseUrl, endpoint, { token });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const orderFetcher = useCallback(
     function orderFetcherImpl<T>(
-      path: string,
+      endpoint: string,
       paid?: YesOrNo,
       refunded?: YesOrNo,
       wired?: YesOrNo,
@@ -208,42 +220,42 @@ export function useBackendInstanceRequest(): 
useBackendInstanceRequestType {
       if (refunded !== undefined) params.refunded = refunded;
       if (wired !== undefined) params.wired = wired;
       if (date_ms !== undefined) params.date_ms = date_ms;
-      return requestHandler<T>(backend, path, { params, token });
+      return requestHandler<T>(baseUrl, endpoint, { params, token });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const reserveDetailFetcher = useCallback(
     function reserveDetailFetcherImpl<T>(
-      path: string,
+      endpoint: string,
     ): Promise<HttpResponseOk<T>> {
-      return requestHandler<T>(backend, path, {
+      return requestHandler<T>(baseUrl, endpoint, {
         params: {
           tips: "yes",
         },
         token,
       });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const tipsDetailFetcher = useCallback(
     function tipsDetailFetcherImpl<T>(
-      path: string,
+      endpoint: string,
     ): Promise<HttpResponseOk<T>> {
-      return requestHandler<T>(backend, path, {
+      return requestHandler<T>(baseUrl, endpoint, {
         params: {
           pickups: "yes",
         },
         token,
       });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const transferFetcher = useCallback(
     function transferFetcherImpl<T>(
-      path: string,
+      endpoint: string,
       payto_uri?: string,
       verified?: string,
       position?: string,
@@ -257,14 +269,14 @@ export function useBackendInstanceRequest(): 
useBackendInstanceRequestType {
       }
       if (position !== undefined) params.offset = position;
 
-      return requestHandler<T>(backend, path, { params, token });
+      return requestHandler<T>(baseUrl, endpoint, { params, token });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const templateFetcher = useCallback(
     function templateFetcherImpl<T>(
-      path: string,
+      endpoint: string,
       position?: string,
       delta?: number,
     ): Promise<HttpResponseOk<T>> {
@@ -274,14 +286,14 @@ export function useBackendInstanceRequest(): 
useBackendInstanceRequestType {
       }
       if (position !== undefined) params.offset = position;
 
-      return requestHandler<T>(backend, path, { params, token });
+      return requestHandler<T>(baseUrl, endpoint, { params, token });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   const webhookFetcher = useCallback(
     function webhookFetcherImpl<T>(
-      path: string,
+      endpoint: string,
       position?: string,
       delta?: number,
     ): Promise<HttpResponseOk<T>> {
@@ -291,9 +303,9 @@ export function useBackendInstanceRequest(): 
useBackendInstanceRequestType {
       }
       if (position !== undefined) params.offset = position;
 
-      return requestHandler<T>(backend, path, { params, token });
+      return requestHandler<T>(baseUrl, endpoint, { params, token });
     },
-    [backend, token],
+    [baseUrl, token],
   );
 
   return {
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts 
b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index 3c05472d0..f118e1e6e 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -16,7 +16,11 @@
 import useSWR, { useSWRConfig } from "swr";
 import { useBackendContext } from "../context/backend.js";
 import { MerchantBackend } from "../declaration.js";
-import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";
+import {
+  HttpError,
+  HttpResponse,
+  HttpResponseOk,
+} from "@gnu-taler/web-util/lib/index.browser";
 import {
   useBackendBaseRequest,
   useBackendInstanceRequest,
@@ -176,12 +180,15 @@ export function useInstanceAPI(): InstanceAPI {
   return { updateInstance, deleteInstance, setNewToken, clearToken };
 }
 
-export function useInstanceDetails(): 
HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
+export function useInstanceDetails(): HttpResponse<
+  MerchantBackend.Instances.QueryInstancesResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { fetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/`], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
@@ -203,12 +210,15 @@ type KYCStatus =
   | { type: "ok" }
   | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects 
};
 
-export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {
+export function useInstanceKYCDetails(): HttpResponse<
+  KYCStatus,
+  MerchantBackend.ErrorDetail
+> {
   const { fetcher } = useBackendInstanceRequest();
 
   const { data, error } = useSWR<
     HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/kyc`], fetcher, {
     refreshInterval: 5000,
     refreshWhenHidden: false,
@@ -231,12 +241,15 @@ export function useInstanceKYCDetails(): 
HttpResponse<KYCStatus> {
 
 export function useManagedInstanceDetails(
   instanceId: string,
-): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
+): HttpResponse<
+  MerchantBackend.Instances.QueryInstancesResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { request } = useBackendBaseRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/management/instances/${instanceId}`], request, {
     refreshInterval: 0,
     refreshWhenHidden: false,
@@ -254,12 +267,15 @@ export function useManagedInstanceDetails(
   return { loading: true };
 }
 
-export function useBackendInstances(): 
HttpResponse<MerchantBackend.Instances.InstancesResponse> {
+export function useBackendInstances(): HttpResponse<
+  MerchantBackend.Instances.InstancesResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { request } = useBackendBaseRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >(["/management/instances"], request);
 
   if (isValidating) return { loading: true, data: data?.data };
diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts 
b/packages/merchant-backoffice-ui/src/hooks/order.ts
index 5be480160..c01f8dd83 100644
--- a/packages/merchant-backoffice-ui/src/hooks/order.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/order.ts
@@ -22,7 +22,7 @@ import {
   HttpResponse,
   HttpResponseOk,
   HttpResponsePaginated,
-} from "../utils/request.js";
+} from "@gnu-taler/web-util/lib/index.browser";
 import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
 
 export interface OrderAPI {
@@ -128,12 +128,15 @@ export function useOrderAPI(): OrderAPI {
 
 export function useOrderDetails(
   oderId: string,
-): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> {
+): HttpResponse<
+  MerchantBackend.Orders.MerchantOrderStatusResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { fetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/orders/${oderId}`], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
@@ -158,7 +161,10 @@ export interface InstanceOrderFilter {
 export function useInstanceOrders(
   args?: InstanceOrderFilter,
   updateFilter?: (d: Date) => void,
-): HttpResponsePaginated<MerchantBackend.Orders.OrderHistory> {
+): HttpResponsePaginated<
+  MerchantBackend.Orders.OrderHistory,
+  MerchantBackend.ErrorDetail
+> {
   const { orderFetcher } = useBackendInstanceRequest();
 
   const [pageBefore, setPageBefore] = useState(1);
@@ -177,7 +183,10 @@ export function useInstanceOrders(
     data: beforeData,
     error: beforeError,
     isValidating: loadingBefore,
-  } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
+  } = useSWR<
+    HttpResponseOk<MerchantBackend.Orders.OrderHistory>,
+    HttpError<MerchantBackend.ErrorDetail>
+  >(
     [
       `/private/orders`,
       args?.paid,
@@ -192,7 +201,10 @@ export function useInstanceOrders(
     data: afterData,
     error: afterError,
     isValidating: loadingAfter,
-  } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
+  } = useSWR<
+    HttpResponseOk<MerchantBackend.Orders.OrderHistory>,
+    HttpError<MerchantBackend.ErrorDetail>
+  >(
     [
       `/private/orders`,
       args?.paid,
@@ -206,10 +218,16 @@ export function useInstanceOrders(
 
   //this will save last result
   const [lastBefore, setLastBefore] = useState<
-    HttpResponse<MerchantBackend.Orders.OrderHistory>
+    HttpResponse<
+      MerchantBackend.Orders.OrderHistory,
+      MerchantBackend.ErrorDetail
+    >
   >({ loading: true });
   const [lastAfter, setLastAfter] = useState<
-    HttpResponse<MerchantBackend.Orders.OrderHistory>
+    HttpResponse<
+      MerchantBackend.Orders.OrderHistory,
+      MerchantBackend.ErrorDetail
+    >
   >({ loading: true });
   useEffect(() => {
     if (afterData) setLastAfter(afterData);
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts 
b/packages/merchant-backoffice-ui/src/hooks/product.ts
index af8ad74f3..5d95a2f8f 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.ts
@@ -15,7 +15,11 @@
  */
 import useSWR, { useSWRConfig } from "swr";
 import { MerchantBackend, WithId } from "../declaration.js";
-import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";
+import {
+  HttpError,
+  HttpResponse,
+  HttpResponseOk,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
 
 export interface ProductAPI {
@@ -85,13 +89,14 @@ export function useProductAPI(): ProductAPI {
 }
 
 export function useInstanceProducts(): HttpResponse<
-  (MerchantBackend.Products.ProductDetail & WithId)[]
+  (MerchantBackend.Products.ProductDetail & WithId)[],
+  MerchantBackend.ErrorDetail
 > {
   const { fetcher, multiFetcher } = useBackendInstanceRequest();
 
   const { data: list, error: listError } = useSWR<
     HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/products`], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
@@ -105,7 +110,7 @@ export function useInstanceProducts(): HttpResponse<
   );
   const { data: products, error: productError } = useSWR<
     HttpResponseOk<MerchantBackend.Products.ProductDetail>[],
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([paths], multiFetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
@@ -122,7 +127,7 @@ export function useInstanceProducts(): HttpResponse<
       //take the id from the queried url
       return {
         ...d.data,
-        id: d.info?.url.href.replace(/.*\/private\/products\//, "") || "",
+        id: d.info?.url.replace(/.*\/private\/products\//, "") || "",
       };
     });
     return { ok: true, data: dataWithId };
@@ -132,12 +137,15 @@ export function useInstanceProducts(): HttpResponse<
 
 export function useProductDetails(
   productId: string,
-): HttpResponse<MerchantBackend.Products.ProductDetail> {
+): HttpResponse<
+  MerchantBackend.Products.ProductDetail,
+  MerchantBackend.ErrorDetail
+> {
   const { fetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Products.ProductDetail>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/products/${productId}`], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts 
b/packages/merchant-backoffice-ui/src/hooks/reserves.ts
index dc127af13..0215f32c5 100644
--- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/reserves.ts
@@ -15,7 +15,11 @@
  */
 import useSWR, { useSWRConfig } from "swr";
 import { MerchantBackend } from "../declaration.js";
-import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";
+import {
+  HttpError,
+  HttpResponse,
+  HttpResponseOk,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
 
 export function useReservesAPI(): ReserveMutateAPI {
@@ -77,7 +81,9 @@ export function useReservesAPI(): ReserveMutateAPI {
     return res;
   };
 
-  const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => {
+  const deleteReserve = async (
+    pub: string,
+  ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => {
     const res = await request<void>(`/private/reserves/${pub}`, {
       method: "DELETE",
     });
@@ -102,15 +108,20 @@ export interface ReserveMutateAPI {
   authorizeTip: (
     data: MerchantBackend.Tips.TipCreateRequest,
   ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
-  deleteReserve: (id: string) => Promise<HttpResponse<void>>;
+  deleteReserve: (
+    id: string,
+  ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>;
 }
 
-export function useInstanceReserves(): 
HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
+export function useInstanceReserves(): HttpResponse<
+  MerchantBackend.Tips.TippingReserveStatus,
+  MerchantBackend.ErrorDetail
+> {
   const { fetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/reserves`], fetcher);
 
   if (isValidating) return { loading: true, data: data?.data };
@@ -121,12 +132,15 @@ export function useInstanceReserves(): 
HttpResponse<MerchantBackend.Tips.Tipping
 
 export function useReserveDetails(
   reserveId: string,
-): HttpResponse<MerchantBackend.Tips.ReserveDetail> {
+): HttpResponse<
+  MerchantBackend.Tips.ReserveDetail,
+  MerchantBackend.ErrorDetail
+> {
   const { reserveDetailFetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
@@ -143,12 +157,12 @@ export function useReserveDetails(
 
 export function useTipDetails(
   tipId: string,
-): HttpResponse<MerchantBackend.Tips.TipDetails> {
+): HttpResponse<MerchantBackend.Tips.TipDetails, MerchantBackend.ErrorDetail> {
   const { tipsDetailFetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Tips.TipDetails>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/tips/${tipId}`], tipsDetailFetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts 
b/packages/merchant-backoffice-ui/src/hooks/templates.ts
index 3a28b903d..124786887 100644
--- a/packages/merchant-backoffice-ui/src/hooks/templates.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts
@@ -23,7 +23,7 @@ import {
   HttpResponse,
   HttpResponseOk,
   HttpResponsePaginated,
-} from "../utils/request.js";
+} from "@gnu-taler/web-util/lib/index.browser";
 
 export function useTemplateAPI(): TemplateAPI {
   const mutateAll = useMatchMutate();
@@ -79,7 +79,12 @@ export function useTemplateAPI(): TemplateAPI {
     return res;
   };
 
-  return { createTemplate, updateTemplate, deleteTemplate, 
createOrderFromTemplate };
+  return {
+    createTemplate,
+    updateTemplate,
+    deleteTemplate,
+    createOrderFromTemplate,
+  };
 }
 
 export interface TemplateAPI {
@@ -105,7 +110,10 @@ export interface InstanceTemplateFilter {
 export function useInstanceTemplates(
   args?: InstanceTemplateFilter,
   updatePosition?: (id: string) => void,
-): HttpResponsePaginated<MerchantBackend.Template.TemplateSummaryResponse> {
+): HttpResponsePaginated<
+  MerchantBackend.Template.TemplateSummaryResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { templateFetcher } = useBackendInstanceRequest();
 
   // const [pageBefore, setPageBefore] = useState(1);
@@ -140,15 +148,18 @@ export function useInstanceTemplates(
     isValidating: loadingAfter,
   } = useSWR<
     HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/templates`, args?.position, -totalAfter], templateFetcher);
 
   //this will save last result
   // const [lastBefore, setLastBefore] = useState<
-  //   HttpResponse<MerchantBackend.Template.TemplateSummaryResponse>
+  //   HttpResponse<MerchantBackend.Template.TemplateSummaryResponse, 
MerchantBackend.ErrorDetail>
   // >({ loading: true });
   const [lastAfter, setLastAfter] = useState<
-    HttpResponse<MerchantBackend.Template.TemplateSummaryResponse>
+    HttpResponse<
+      MerchantBackend.Template.TemplateSummaryResponse,
+      MerchantBackend.ErrorDetail
+    >
   >({ loading: true });
   useEffect(() => {
     if (afterData) setLastAfter(afterData);
@@ -174,9 +185,10 @@ export function useInstanceTemplates(
       if (afterData.data.templates.length < MAX_RESULT_SIZE) {
         setPageAfter(pageAfter + 1);
       } else {
-        const from = 
`${afterData.data.templates[afterData.data.templates.length - 1]
-          .template_id
-          }`;
+        const from = `${
+          afterData.data.templates[afterData.data.templates.length - 1]
+            .template_id
+        }`;
         if (from && updatePosition) updatePosition(from);
       }
     },
@@ -211,12 +223,15 @@ export function useInstanceTemplates(
 
 export function useTemplateDetails(
   templateId: string,
-): HttpResponse<MerchantBackend.Template.TemplateDetails> {
+): HttpResponse<
+  MerchantBackend.Template.TemplateDetails,
+  MerchantBackend.ErrorDetail
+> {
   const { templateFetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Template.TemplateDetails>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/templates/${templateId}`], templateFetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx 
b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
index 8c5a5a36b..64e646bb5 100644
--- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx
+++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
@@ -22,10 +22,13 @@
 import { MockEnvironment } from "@gnu-taler/web-util/lib/tests/mock";
 import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
 import { SWRConfig } from "swr";
-import { ApiContextProvider } from "../context/api.js";
+import { ApiContextProvider } from "@gnu-taler/web-util/lib/index.browser";
 import { BackendContextProvider } from "../context/backend.js";
 import { InstanceContextProvider } from "../context/instance.js";
-import { HttpResponseOk, RequestOptions } from "../utils/request.js";
+import {
+  HttpResponseOk,
+  RequestOptions,
+} from "@gnu-taler/web-util/lib/index.browser";
 
 export class ApiMockEnvironment extends MockEnvironment {
   constructor(debug = false) {
@@ -78,7 +81,7 @@ export class ApiMockEnvironment extends MockEnvironment {
           info: {
             hasToken: !!options.token,
             status: !mocked ? 200 : mocked.status,
-            url: _url,
+            url: _url.href,
             payload: options.data,
           },
         };
diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts 
b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
index b86247476..6b30047e9 100644
--- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
@@ -22,7 +22,7 @@ import {
   HttpResponse,
   HttpResponseOk,
   HttpResponsePaginated,
-} from "../utils/request.js";
+} from "@gnu-taler/web-util/lib/index.browser";
 import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
 
 export function useTransferAPI(): TransferAPI {
@@ -67,7 +67,10 @@ export interface InstanceTransferFilter {
 export function useInstanceTransfers(
   args?: InstanceTransferFilter,
   updatePosition?: (id: string) => void,
-): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> {
+): HttpResponsePaginated<
+  MerchantBackend.Transfers.TransferList,
+  MerchantBackend.ErrorDetail
+> {
   const { transferFetcher } = useBackendInstanceRequest();
 
   const [pageBefore, setPageBefore] = useState(1);
@@ -86,7 +89,10 @@ export function useInstanceTransfers(
     data: beforeData,
     error: beforeError,
     isValidating: loadingBefore,
-  } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, 
HttpError>(
+  } = useSWR<
+    HttpResponseOk<MerchantBackend.Transfers.TransferList>,
+    HttpError<MerchantBackend.ErrorDetail>
+  >(
     [
       `/private/transfers`,
       args?.payto_uri,
@@ -100,7 +106,10 @@ export function useInstanceTransfers(
     data: afterData,
     error: afterError,
     isValidating: loadingAfter,
-  } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, 
HttpError>(
+  } = useSWR<
+    HttpResponseOk<MerchantBackend.Transfers.TransferList>,
+    HttpError<MerchantBackend.ErrorDetail>
+  >(
     [
       `/private/transfers`,
       args?.payto_uri,
@@ -113,10 +122,16 @@ export function useInstanceTransfers(
 
   //this will save last result
   const [lastBefore, setLastBefore] = useState<
-    HttpResponse<MerchantBackend.Transfers.TransferList>
+    HttpResponse<
+      MerchantBackend.Transfers.TransferList,
+      MerchantBackend.ErrorDetail
+    >
   >({ loading: true });
   const [lastAfter, setLastAfter] = useState<
-    HttpResponse<MerchantBackend.Transfers.TransferList>
+    HttpResponse<
+      MerchantBackend.Transfers.TransferList,
+      MerchantBackend.ErrorDetail
+    >
   >({ loading: true });
   useEffect(() => {
     if (afterData) setLastAfter(afterData);
diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts 
b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
index 9f196cefa..e1cd3daf2 100644
--- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
@@ -23,7 +23,7 @@ import {
   HttpResponse,
   HttpResponseOk,
   HttpResponsePaginated,
-} from "../utils/request.js";
+} from "@gnu-taler/web-util/lib/index.browser";
 
 export function useWebhookAPI(): WebhookAPI {
   const mutateAll = useMatchMutate();
@@ -84,7 +84,10 @@ export interface InstanceWebhookFilter {
 export function useInstanceWebhooks(
   args?: InstanceWebhookFilter,
   updatePosition?: (id: string) => void,
-): HttpResponsePaginated<MerchantBackend.Webhooks.WebhookSummaryResponse> {
+): HttpResponsePaginated<
+  MerchantBackend.Webhooks.WebhookSummaryResponse,
+  MerchantBackend.ErrorDetail
+> {
   const { webhookFetcher } = useBackendInstanceRequest();
 
   const [pageAfter, setPageAfter] = useState(1);
@@ -97,11 +100,14 @@ export function useInstanceWebhooks(
     isValidating: loadingAfter,
   } = useSWR<
     HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher);
 
   const [lastAfter, setLastAfter] = useState<
-    HttpResponse<MerchantBackend.Webhooks.WebhookSummaryResponse>
+    HttpResponse<
+      MerchantBackend.Webhooks.WebhookSummaryResponse,
+      MerchantBackend.ErrorDetail
+    >
   >({ loading: true });
   useEffect(() => {
     if (afterData) setLastAfter(afterData);
@@ -121,21 +127,20 @@ export function useInstanceWebhooks(
       if (afterData.data.webhooks.length < MAX_RESULT_SIZE) {
         setPageAfter(pageAfter + 1);
       } else {
-        const from = `${afterData.data.webhooks[afterData.data.webhooks.length 
- 1]
-          .webhook_id
-          }`;
+        const from = `${
+          afterData.data.webhooks[afterData.data.webhooks.length - 
1].webhook_id
+        }`;
         if (from && updatePosition) updatePosition(from);
       }
     },
     loadMorePrev: () => {
-      return
+      return;
     },
   };
 
   const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks;
 
-  if (loadingAfter)
-    return { loading: true, data: { webhooks } };
+  if (loadingAfter) return { loading: true, data: { webhooks } };
   if (afterData) {
     return { ok: true, data: { webhooks }, ...pagination };
   }
@@ -144,12 +149,15 @@ export function useInstanceWebhooks(
 
 export function useWebhookDetails(
   webhookId: string,
-): HttpResponse<MerchantBackend.Webhooks.WebhookDetails> {
+): HttpResponse<
+  MerchantBackend.Webhooks.WebhookDetails,
+  MerchantBackend.ErrorDetail
+> {
   const { webhookFetcher } = useBackendInstanceRequest();
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>,
-    HttpError
+    HttpError<MerchantBackend.ErrorDetail>
   >([`/private/webhooks/${webhookId}`], webhookFetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
index bac7a39eb..8efb5598d 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
@@ -19,14 +19,16 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../components/exception/loading.js";
 import { NotificationCard } from "../../../components/menu/index.js";
 import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
 import { MerchantBackend } from "../../../declaration.js";
-import { HttpError } from "../../../utils/request.js";
 import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
 import { Notification } from "../../../utils/types.js";
 import { View } from "./View.js";
@@ -37,7 +39,7 @@ interface Props {
   instances: MerchantBackend.Instances.Instance[];
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   setInstanceName: (s: string) => void;
 }
 
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
index 56d5c0755..8f7d9b136 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
@@ -13,18 +13,19 @@
  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 { HttpError } from "@gnu-taler/web-util/lib/index.browser.js";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../components/exception/loading.js";
 import { DeleteModal } from "../../../components/modal/index.js";
 import { useInstanceContext } from "../../../context/instance.js";
-import { HttpError } from "../../../utils/request.js";
+import { MerchantBackend } from "../../../declaration.js";
 import { useInstanceAPI, useInstanceDetails } from 
"../../../hooks/instance.js";
 import { DetailPage } from "./DetailPage.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onUpdate: () => void;
   onNotFound: () => VNode;
   onDelete: () => void;
@@ -63,7 +64,9 @@ export default function Detail({
             try {
               await deleteInstance();
               onDelete();
-            } catch (error) {}
+            } catch (error) {
+              //FIXME: show message error
+            }
             setDeleting(false);
           }}
         />
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
index 83af002b3..dba2aab21 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -19,15 +19,16 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { HttpError } from "@gnu-taler/web-util/lib/index.browser.js";
 import { h, VNode } from "preact";
 import { Loading } from "../../../../components/exception/loading.js";
-import { HttpError } from "../../../../utils/request.js";
+import { MerchantBackend } from "../../../../declaration.js";
 import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
 import { ListPage } from "./ListPage.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
 }
 
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
index 5c6293a81..a37df2176 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
@@ -19,18 +19,17 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { HttpError } from "@gnu-taler/web-util/lib/index.browser.js";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import { useInstanceDetails } from "../../../../hooks/instance.js";
 import { useOrderAPI } from "../../../../hooks/order.js";
 import { useInstanceProducts } from "../../../../hooks/product.js";
 import { Notification } from "../../../../utils/types.js";
 import { CreatePage } from "./CreatePage.js";
-import { OrderCreatedSuccessfully } from "./OrderCreatedSuccessfully.js";
 
 export type Entity = {
   request: MerchantBackend.Orders.PostOrderRequest;
@@ -41,7 +40,7 @@ interface Props {
   onConfirm: () => void;
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
 }
 export default function OrderCreate({
   onConfirm,
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
index 19aaddf50..986c46b95 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -13,12 +13,15 @@
  You should have received a copy of the GNU General Public License along with
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  useTranslationContext,
+  HttpError,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
-import { HttpError } from "../../../../utils/request.js";
+import { MerchantBackend } from "../../../../declaration.js";
 import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
 import { Notification } from "../../../../utils/types.js";
 import { DetailPage } from "./DetailPage.js";
@@ -29,7 +32,7 @@ export interface Props {
   onBack: () => void;
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
 }
 
 export default function Update({
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
index 3744ce8c5..bd0924808 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -19,13 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import {
   InstanceOrderFilter,
   useInstanceOrders,
@@ -38,7 +40,7 @@ import { RefundModal } from "./Table.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
   onSelect: (id: string) => void;
   onCreate: () => void;
@@ -177,7 +179,7 @@ export default function OrderList({
 interface RefundProps {
   id: string;
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
   onCancel: () => void;
   onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void;
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
index 25332acee..c32339563 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
@@ -19,13 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import {
   useInstanceProducts,
   useProductAPI,
@@ -38,7 +40,7 @@ interface Props {
   onNotFound: () => VNode;
   onCreate: () => void;
   onSelect: (id: string) => void;
-  onLoadError: (e: HttpError) => VNode;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
 }
 export default function ProductList({
   onUnauthorized,
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
index 5b19a7aa3..b35606f53 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -19,13 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import { useProductAPI, useProductDetails } from 
"../../../../hooks/product.js";
 import { Notification } from "../../../../utils/types.js";
 import { UpdatePage } from "./UpdatePage.js";
@@ -36,7 +38,7 @@ interface Props {
   onConfirm: () => void;
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   pid: string;
 }
 export default function UpdateProduct({
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
index eeb59611c..c0c36e651 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
@@ -30,8 +30,7 @@ import {
 import { Input } from "../../../../components/form/Input.js";
 import { InputCurrency } from "../../../../components/form/InputCurrency.js";
 import { InputSelector } from "../../../../components/form/InputSelector.js";
-import { ExchangeBackend, MerchantBackend } from "../../../../declaration.js";
-// import { request } from "../../../../utils/request.js";
+import { MerchantBackend } from "../../../../declaration.js";
 import {
   PAYTO_WIRE_METHOD_LOOKUP,
   URL_REGEX,
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx
index 57ee566d1..e7ec68fab 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx
@@ -19,9 +19,10 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { HttpError } from "@gnu-taler/web-util/lib/index.browser.js";
 import { Fragment, h, VNode } from "preact";
 import { Loading } from "../../../../components/exception/loading.js";
-import { HttpError } from "../../../../utils/request.js";
+import { MerchantBackend } from "../../../../declaration.js";
 import { useReserveDetails } from "../../../../hooks/reserves.js";
 import { DetailPage } from "./DetailPage.js";
 
@@ -29,7 +30,7 @@ interface Props {
   rid: string;
 
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
   onDelete: () => void;
   onBack: () => void;
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
index 597bde167..e6c6abc23 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
@@ -19,13 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import {
   useInstanceReserves,
   useReservesAPI,
@@ -36,7 +38,7 @@ import { CardTable } from "./Table.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onSelect: (id: string) => void;
   onNotFound: () => VNode;
   onCreate: () => void;
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
index 5fce3a819..0b7c191bd 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -19,13 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import {
   useInstanceTemplates,
   useTemplateAPI,
@@ -35,7 +37,7 @@ import { ListPage } from "./ListPage.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
   onCreate: () => void;
   onSelect: (id: string) => void;
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
index 684ffd429..73489869b 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -19,13 +19,15 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { NotificationCard } from "../../../../components/menu/index.js";
 import { MerchantBackend, WithId } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import {
   useTemplateAPI,
   useTemplateDetails,
@@ -40,7 +42,7 @@ interface Props {
   onConfirm: () => void;
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   tid: string;
 }
 export default function UpdateTemplate({
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
index cbcadc608..13576d94d 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
@@ -19,7 +19,6 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { h, VNode, FunctionalComponent } from "preact";
 import { UsePage as TestedComponent } from "./UsePage.js";
 
 export default {
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
index fa9a98c6d..d5fa6d39d 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -19,7 +19,10 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
@@ -29,7 +32,6 @@ import {
   useTemplateAPI,
   useTemplateDetails,
 } from "../../../../hooks/templates.js";
-import { HttpError } from "../../../../utils/request.js";
 import { Notification } from "../../../../utils/types.js";
 import { UsePage } from "./UsePage.js";
 
@@ -39,7 +41,7 @@ interface Props {
   onOrderCreated: (id: string) => void;
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   tid: string;
 }
 
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
index 59b56a613..9f2b59efd 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -19,18 +19,18 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { HttpError } from "@gnu-taler/web-util/lib/index.browser.js";
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
 import { MerchantBackend } from "../../../../declaration.js";
-import { HttpError } from "../../../../utils/request.js";
 import { useInstanceDetails } from "../../../../hooks/instance.js";
 import { useInstanceTransfers } from "../../../../hooks/transfer.js";
 import { ListPage } from "./ListPage.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
   onCreate: () => void;
 }
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
index 02beb36f2..912393c7c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -13,7 +13,11 @@
  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 { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  HttpResponse,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../components/exception/loading.js";
@@ -26,7 +30,6 @@ import {
   useManagedInstanceDetails,
   useManagementAPI,
 } from "../../../hooks/instance.js";
-import { HttpError, HttpResponse } from "../../../utils/request.js";
 import { Notification } from "../../../utils/types.js";
 import { UpdatePage } from "./UpdatePage.js";
 
@@ -36,8 +39,8 @@ export interface Props {
 
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
-  onUpdateError: (e: HttpError) => void;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+  onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void;
 }
 
 export default function Update(props: Props): VNode {
@@ -63,7 +66,10 @@ function CommonUpdate(
     onUpdateError,
     onUnauthorized,
   }: Props,
-  result: HttpResponse<MerchantBackend.Instances.QueryInstancesResponse>,
+  result: HttpResponse<
+    MerchantBackend.Instances.QueryInstancesResponse,
+    MerchantBackend.ErrorDetail
+  >,
   updateInstance: any,
   clearToken: any,
   setNewToken: any,
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
index c5846e4db..670fc7218 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -19,7 +19,10 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
@@ -29,13 +32,12 @@ import {
   useInstanceWebhooks,
   useWebhookAPI,
 } from "../../../../hooks/webhooks.js";
-import { HttpError } from "../../../../utils/request.js";
 import { Notification } from "../../../../utils/types.js";
 import { ListPage } from "./ListPage.js";
 
 interface Props {
   onUnauthorized: () => VNode;
-  onLoadError: (error: HttpError) => VNode;
+  onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   onNotFound: () => VNode;
   onCreate: () => void;
   onSelect: (id: string) => void;
diff --git 
a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
index 3597fb849..b9a8674b3 100644
--- 
a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
+++ 
b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -19,7 +19,10 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import {
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../../../../components/exception/loading.js";
@@ -29,7 +32,6 @@ import {
   useWebhookAPI,
   useWebhookDetails,
 } from "../../../../hooks/webhooks.js";
-import { HttpError } from "../../../../utils/request.js";
 import { Notification } from "../../../../utils/types.js";
 import { UpdatePage } from "./UpdatePage.js";
 
@@ -40,7 +42,7 @@ interface Props {
   onConfirm: () => void;
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
+  onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
   tid: string;
 }
 export default function UpdateWebhook({
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.tsx 
b/packages/taler-wallet-webextension/src/components/AmountField.tsx
index 4936e0604..786244433 100644
--- a/packages/taler-wallet-webextension/src/components/AmountField.tsx
+++ b/packages/taler-wallet-webextension/src/components/AmountField.tsx
@@ -31,6 +31,9 @@ import { TextField } from "../mui/TextField.js";
 const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"];
 const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"];
 
+/**
+ * Show normalized value based on the currency unit
+ */
 export function AmountField({
   label,
   handler,
@@ -77,7 +80,7 @@ export function AmountField({
 
   const previousValue = Amounts.stringifyValue(handler.value, decimalPlaces);
 
-  const normal = denormalize(handler.value, unit) ?? handler.value;
+  const normal = normalize(handler.value, unit) ?? handler.value;
 
   let textValue = Amounts.stringifyValue(normal, decimalPlaces);
   if (decimalPlaces === 0) {
@@ -95,25 +98,25 @@ export function AmountField({
     }
     try {
       //remove all but last dot
-      const parsed = value.replace(/(\.)(?=.*\1)/g, "");
-      const parts = parsed.split(".");
+      const withoutDots = value.replace(/(\.)(?=.*\1)/g, "");
+      const parts = withoutDots.split(".");
       setDecimalPlaces(parts.length === 1 ? undefined : parts[1].length);
 
       //FIXME: should normalize before parsing
       //parsing first add some restriction on the rage of the values
-      const real = parseValue(currency, parsed);
+      const parsed = parseValue(currency, withoutDots);
 
-      if (!real || real.value < 0) {
+      if (!parsed || parsed.value < 0) {
         return previousValue;
       }
 
-      const realNormalized = normalize(real, unit);
+      const realValue = denormalize(parsed, unit);
 
       // console.log(real, unit, normal);
-      if (realNormalized && handler.onInput) {
-        handler.onInput(realNormalized);
+      if (realValue && handler.onInput) {
+        handler.onInput(realValue);
       }
-      return parsed;
+      return withoutDots;
     } catch (e) {
       // do nothing
     }
@@ -191,7 +194,14 @@ function parseValue(currency: string, s: string): 
AmountJson | undefined {
   return { currency, fraction, value };
 }
 
-function normalize(amount: AmountJson, unit: number): AmountJson | undefined {
+/**
+ * Return the real value of a normalized unit
+ * If the value is 20 and the unit is kilo == 1000 the returned value will be 
amount * 1000
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function denormalize(amount: AmountJson, unit: number): AmountJson | undefined 
{
   if (unit === 1 || Amounts.isZero(amount)) return amount;
   const result =
     unit < 1
@@ -200,7 +210,15 @@ function normalize(amount: AmountJson, unit: number): 
AmountJson | undefined {
   return result;
 }
 
-function denormalize(amount: AmountJson, unit: number): AmountJson | undefined 
{
+/**
+ * Return the amount in the current unit.
+ * If the value is 20000 and the unit is kilo == 1000 and the returned value 
will be amount / unit
+ *
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function normalize(amount: AmountJson, unit: number): AmountJson | undefined {
   if (unit === 1 || Amounts.isZero(amount)) return amount;
   const result =
     unit < 1
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index ad44ed67f..1d3dcfca6 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -37,7 +37,7 @@
     "preact-render-to-string": "^5.2.6",
     "prettier": "^2.5.1",
     "rimraf": "^3.0.2",
-    "swr": "1.3.0",
+    "swr": "2.0.3",
     "tslib": "^2.4.0",
     "typescript": "^4.9.4",
     "ws": "7.4.5"
diff --git a/packages/merchant-backoffice-ui/src/context/api.ts 
b/packages/web-util/src/context/api.ts
similarity index 100%
rename from packages/merchant-backoffice-ui/src/context/api.ts
rename to packages/web-util/src/context/api.ts
diff --git a/packages/web-util/src/context/index.ts 
b/packages/web-util/src/context/index.ts
index 4bc1b22f2..9ed3ef645 100644
--- a/packages/web-util/src/context/index.ts
+++ b/packages/web-util/src/context/index.ts
@@ -1,5 +1,7 @@
+export { ApiContextProvider, useApiContext } from "./api.js";
 export {
   InternationalizationAPI,
   TranslationProvider,
-  useTranslationContext,
+  useTranslationContext
 } from "./translation.js";
+
diff --git a/packages/web-util/src/index.browser.ts 
b/packages/web-util/src/index.browser.ts
index d3aeae168..2ae3f2a0b 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,4 +1,5 @@
 export * from "./hooks/index.js";
+export * from "./utils/request.js";
 export * from "./context/index.js";
 export * from "./components/index.js";
 export * as tests from "./tests/index.js";
diff --git a/packages/web-util/src/utils/base64.ts 
b/packages/web-util/src/utils/base64.ts
new file mode 100644
index 000000000..0e075880f
--- /dev/null
+++ b/packages/web-util/src/utils/base64.ts
@@ -0,0 +1,243 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+
+export function base64encode(str: string): string {
+  return base64EncArr(strToUTF8Arr(str))
+}
+
+export function base64decode(str: string): string {
+  return UTF8ArrToStr(base64DecToArr(str))
+}
+
+// from https://developer.mozilla.org/en-US/docs/Glossary/Base64
+
+// Array of bytes to Base64 string decoding
+function b64ToUint6(nChr: number): number {
+  return nChr > 64 && nChr < 91
+    ? nChr - 65
+    : nChr > 96 && nChr < 123
+      ? nChr - 71
+      : nChr > 47 && nChr < 58
+        ? nChr + 4
+        : nChr === 43
+          ? 62
+          : nChr === 47
+            ? 63
+            : 0;
+}
+
+function base64DecToArr(sBase64: string, nBlocksSize?: number): Uint8Array {
+  const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); // Only necessary if 
the base64 includes whitespace such as line breaks.
+  const nInLen = sB64Enc.length;
+  const nOutLen = nBlocksSize
+    ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
+    : (nInLen * 3 + 1) >> 2;
+  const taBytes = new Uint8Array(nOutLen);
+
+  let nMod3;
+  let nMod4;
+  let nUint24 = 0;
+  let nOutIdx = 0;
+  for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+    nMod4 = nInIdx & 3;
+    nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
+    if (nMod4 === 3 || nInLen - nInIdx === 1) {
+      nMod3 = 0;
+      while (nMod3 < 3 && nOutIdx < nOutLen) {
+        taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
+        nMod3++;
+        nOutIdx++;
+      }
+      nUint24 = 0;
+    }
+  }
+
+  return taBytes;
+}
+
+/* Base64 string to array encoding */
+function uint6ToB64(nUint6: number): number {
+  return nUint6 < 26
+    ? nUint6 + 65
+    : nUint6 < 52
+      ? nUint6 + 71
+      : nUint6 < 62
+        ? nUint6 - 4
+        : nUint6 === 62
+          ? 43
+          : nUint6 === 63
+            ? 47
+            : 65;
+}
+
+function base64EncArr(aBytes: Uint8Array): string {
+  let nMod3 = 2;
+  let sB64Enc = "";
+
+  const nLen = aBytes.length;
+  let nUint24 = 0;
+  for (let nIdx = 0; nIdx < nLen; nIdx++) {
+    nMod3 = nIdx % 3;
+    // To break your base64 into several 80-character lines, add:
+    //   if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
+    //      sB64Enc += "\r\n";
+    //    }
+
+    nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
+    if (nMod3 === 2 || aBytes.length - nIdx === 1) {
+      sB64Enc += String.fromCodePoint(
+        uint6ToB64((nUint24 >>> 18) & 63),
+        uint6ToB64((nUint24 >>> 12) & 63),
+        uint6ToB64((nUint24 >>> 6) & 63),
+        uint6ToB64(nUint24 & 63)
+      );
+      nUint24 = 0;
+    }
+  }
+  return (
+    sB64Enc.substring(0, sB64Enc.length - 2 + nMod3) +
+    (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
+  );
+}
+
+/* UTF-8 array to JS string and vice versa */
+
+function UTF8ArrToStr(aBytes: Uint8Array): string {
+  let sView = "";
+  let nPart;
+  const nLen = aBytes.length;
+  for (let nIdx = 0; nIdx < nLen; nIdx++) {
+    nPart = aBytes[nIdx];
+    sView += String.fromCodePoint(
+      nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
+        ? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
+        (nPart - 252) * 1073741824 +
+        ((aBytes[++nIdx] - 128) << 24) +
+        ((aBytes[++nIdx] - 128) << 18) +
+        ((aBytes[++nIdx] - 128) << 12) +
+        ((aBytes[++nIdx] - 128) << 6) +
+        aBytes[++nIdx] -
+        128
+        : nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
+          ? ((nPart - 248) << 24) +
+          ((aBytes[++nIdx] - 128) << 18) +
+          ((aBytes[++nIdx] - 128) << 12) +
+          ((aBytes[++nIdx] - 128) << 6) +
+          aBytes[++nIdx] -
+          128
+          : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
+            ? ((nPart - 240) << 18) +
+            ((aBytes[++nIdx] - 128) << 12) +
+            ((aBytes[++nIdx] - 128) << 6) +
+            aBytes[++nIdx] -
+            128
+            : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
+              ? ((nPart - 224) << 12) +
+              ((aBytes[++nIdx] - 128) << 6) +
+              aBytes[++nIdx] -
+              128
+              : nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
+                ? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
+                : /* nPart < 127 ? */ /* one byte */
+                nPart
+    );
+  }
+  return sView;
+}
+
+function strToUTF8Arr(sDOMStr: string): Uint8Array {
+  let nChr;
+  const nStrLen = sDOMStr.length;
+  let nArrLen = 0;
+
+  /* mapping… */
+  for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
+    nChr = sDOMStr.codePointAt(nMapIdx);
+    if (nChr === undefined) {
+      throw Error(`No char at ${nMapIdx} on string with length: 
${sDOMStr.length}`)
+    }
+
+    if (nChr >= 0x10000) {
+      nMapIdx++;
+    }
+
+    nArrLen +=
+      nChr < 0x80
+        ? 1
+        : nChr < 0x800
+          ? 2
+          : nChr < 0x10000
+            ? 3
+            : nChr < 0x200000
+              ? 4
+              : nChr < 0x4000000
+                ? 5
+                : 6;
+  }
+
+  const aBytes = new Uint8Array(nArrLen);
+
+  /* transcription… */
+  let nIdx = 0;
+  let nChrIdx = 0;
+  while (nIdx < nArrLen) {
+    nChr = sDOMStr.codePointAt(nChrIdx);
+    if (nChr === undefined) {
+      throw Error(`No char at ${nChrIdx} on string with length: 
${sDOMStr.length}`)
+    }
+    if (nChr < 128) {
+      /* one byte */
+      aBytes[nIdx++] = nChr;
+    } else if (nChr < 0x800) {
+      /* two bytes */
+      aBytes[nIdx++] = 192 + (nChr >>> 6);
+      aBytes[nIdx++] = 128 + (nChr & 63);
+    } else if (nChr < 0x10000) {
+      /* three bytes */
+      aBytes[nIdx++] = 224 + (nChr >>> 12);
+      aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+      aBytes[nIdx++] = 128 + (nChr & 63);
+    } else if (nChr < 0x200000) {
+      /* four bytes */
+      aBytes[nIdx++] = 240 + (nChr >>> 18);
+      aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+      aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+      aBytes[nIdx++] = 128 + (nChr & 63);
+      nChrIdx++;
+    } else if (nChr < 0x4000000) {
+      /* five bytes */
+      aBytes[nIdx++] = 248 + (nChr >>> 24);
+      aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
+      aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+      aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+      aBytes[nIdx++] = 128 + (nChr & 63);
+      nChrIdx++;
+    } /* if (nChr <= 0x7fffffff) */ else {
+      /* six bytes */
+      aBytes[nIdx++] = 252 + (nChr >>> 30);
+      aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63);
+      aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
+      aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+      aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+      aBytes[nIdx++] = 128 + (nChr & 63);
+      nChrIdx++;
+    }
+    nChrIdx++;
+  }
+
+  return aBytes;
+}
diff --git a/packages/merchant-backoffice-ui/src/utils/request.ts 
b/packages/web-util/src/utils/request.ts
similarity index 66%
rename from packages/merchant-backoffice-ui/src/utils/request.ts
rename to packages/web-util/src/utils/request.ts
index 821eca4a7..24342bb80 100644
--- a/packages/merchant-backoffice-ui/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -13,24 +13,36 @@
  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 axios, { AxiosError, AxiosResponse } from "axios";
-import { MerchantBackend } from "../declaration.js";
 
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { base64encode } from "./base64.js";
+
+/**
+ * 
+ * @param baseUrl URL where the service is located
+ * @param endpoint endpoint of the service to be called 
+ * @param options auth, method and params
+ * @returns 
+ */
 export async function defaultRequestHandler<T>(
-  base: string,
-  path: string,
+  baseUrl: string,
+  endpoint: string,
   options: RequestOptions = {},
 ): Promise<HttpResponseOk<T>> {
-  const requestHeaders = options.token
-    ? { Authorization: `Bearer ${options.token}` }
-    : undefined;
+  const requestHeaders: Record<string, string> = {};
+  if (options.token) {
+    requestHeaders.Authorization = `Bearer ${options.token}`
+  } else if (options.basicAuth) {
+    requestHeaders.Authorization = `Basic 
${base64encode(`${options.basicAuth.username}:${options.basicAuth.password}`)}`
+  }
+  requestHeaders["Content-Type"] = options.contentType === "json" ? 
"application/json" : "text/plain"
 
   const requestMethod = options?.method ?? "GET";
   const requestBody = options?.data;
-  const requestTimeout = 2 * 1000;
+  const requestTimeout = options?.timeout ?? 2 * 1000;
   const requestParams = options.params ?? {};
 
-  const _url = new URL(`${base}${path}`);
+  const _url = new URL(`${baseUrl}${endpoint}`);
 
   Object.entries(requestParams).forEach(([key, value]) => {
     _url.searchParams.set(key, String(value));
@@ -56,17 +68,31 @@ export async function defaultRequestHandler<T>(
     controller.abort("HTTP_REQUEST_TIMEOUT");
   }, requestTimeout);
 
-  const response = await fetch(_url.href, {
-    headers: {
-      ...requestHeaders,
-      "Content-Type": "text/plain",
-    },
-    method: requestMethod,
-    credentials: "omit",
-    mode: "cors",
-    body: payload,
-    signal: controller.signal,
-  });
+  let response;
+  try {
+    response = await fetch(_url.href, {
+      headers: requestHeaders,
+      method: requestMethod,
+      credentials: "omit",
+      mode: "cors",
+      body: payload,
+      signal: controller.signal,
+    });
+  } catch (ex) {
+    const info: RequestInfo = {
+      payload,
+      url: _url.href,
+      hasToken: !!options.token,
+      status: 0,
+    };
+    const error: HttpResponseUnexpectedError = {
+      info,
+      status: 0,
+      error: ex,
+      message: "Request timeout",
+    };
+    throw new RequestError(error);
+  }
 
   if (timeoutId) {
     clearTimeout(timeoutId);
@@ -79,7 +105,7 @@ export async function defaultRequestHandler<T>(
   if (response.ok) {
     const result = await buildRequestOk<T>(
       response,
-      _url,
+      _url.href,
       payload,
       !!options.token,
     );
@@ -87,25 +113,26 @@ export async function defaultRequestHandler<T>(
   } else {
     const error = await buildRequestFailed(
       response,
-      _url,
+      _url.href,
       payload,
       !!options.token,
     );
-    throw error;
+    throw new RequestError(error);
   }
 }
 
-export type HttpResponse<T> =
+export type HttpResponse<T, ErrorDetail> =
   | HttpResponseOk<T>
   | HttpResponseLoading<T>
-  | HttpError;
-export type HttpResponsePaginated<T> =
+  | HttpError<ErrorDetail>;
+
+export type HttpResponsePaginated<T, ErrorDetail> =
   | HttpResponseOkPaginated<T>
   | HttpResponseLoading<T>
-  | HttpError;
+  | HttpError<ErrorDetail>;
 
 export interface RequestInfo {
-  url: URL;
+  url: string;
   hasToken: boolean;
   payload: any;
   status: number;
@@ -138,27 +165,23 @@ export interface WithPagination {
   isReachingStart?: boolean;
 }
 
-export type HttpError =
-  | HttpResponseClientError
-  | HttpResponseServerError
+export type HttpError<ErrorDetail> =
+  | HttpResponseClientError<ErrorDetail>
+  | HttpResponseServerError<ErrorDetail>
   | HttpResponseUnexpectedError;
-export interface SwrError {
-  info: unknown;
-  status: number;
-  message: string;
-}
-export interface HttpResponseServerError {
+
+export interface HttpResponseServerError<ErrorDetail> {
   ok?: false;
   loading?: false;
   clientError?: false;
   serverError: true;
 
-  error?: MerchantBackend.ErrorDetail;
-  status: number;
+  error?: ErrorDetail;
+  status: HttpStatusCode;
   message: string;
   info?: RequestInfo;
 }
-interface HttpResponseClientError {
+interface HttpResponseClientError<ErrorDetail> {
   ok?: false;
   loading?: false;
   clientError: true;
@@ -167,8 +190,8 @@ interface HttpResponseClientError {
   info?: RequestInfo;
   isUnauthorized: boolean;
   isNotfound: boolean;
-  status: number;
-  error?: MerchantBackend.ErrorDetail;
+  status: HttpStatusCode;
+  error?: ErrorDetail;
   message: string;
 }
 
@@ -179,23 +202,37 @@ interface HttpResponseUnexpectedError {
   serverError?: false;
 
   info?: RequestInfo;
-  status?: number;
+  status?: HttpStatusCode;
   error: unknown;
   message: string;
 }
 
+export class RequestError<ErrorDetail> extends Error {
+  info: HttpError<ErrorDetail>;
+  constructor(d: HttpError<ErrorDetail>) {
+    super(d.message)
+    this.info = d
+  }
+}
+
 type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
 
 export interface RequestOptions {
   method?: Methods;
   token?: string;
+  basicAuth?: {
+    username: string,
+    password: string,
+  }
   data?: any;
   params?: unknown;
+  timeout?: number,
+  contentType?: "text" | "json"
 }
 
 async function buildRequestOk<T>(
   response: Response,
-  url: URL,
+  url: string,
   payload: any,
   hasToken: boolean,
 ): Promise<HttpResponseOk<T>> {
@@ -213,14 +250,14 @@ async function buildRequestOk<T>(
   };
 }
 
-async function buildRequestFailed(
+async function buildRequestFailed<ErrorDetail>(
   response: Response,
-  url: URL,
+  url: string,
   payload: any,
   hasToken: boolean,
 ): Promise<
-  | HttpResponseClientError
-  | HttpResponseServerError
+  | HttpResponseClientError<ErrorDetail>
+  | HttpResponseServerError<ErrorDetail>
   | HttpResponseUnexpectedError
 > {
   const status = response?.status;
@@ -236,7 +273,7 @@ async function buildRequestFailed(
     const dataTxt = await response.text();
     const data = dataTxt ? JSON.parse(dataTxt) : undefined;
     if (status && status >= 400 && status < 500) {
-      const error: HttpResponseClientError = {
+      const error: HttpResponseClientError<ErrorDetail> = {
         clientError: true,
         isNotfound: status === 404,
         isUnauthorized: status === 401,
@@ -248,7 +285,7 @@ async function buildRequestFailed(
       return error;
     }
     if (status && status >= 500 && status < 600) {
-      const error: HttpResponseServerError = {
+      const error: HttpResponseServerError<ErrorDetail> = {
         serverError: true,
         status,
         info,
@@ -271,7 +308,7 @@ async function buildRequestFailed(
       message: "NOT DEFINED",
     };
 
-    throw error;
+    return error;
   }
 }
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eef15f25e..4058ca82a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -119,7 +119,7 @@ importers:
       preact-router: 3.2.1
       qrcode-generator: ^1.4.4
       sass: 1.56.1
-      swr: 1.3.0
+      swr: 2.0.3
       typescript: 4.9.4
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
@@ -130,7 +130,7 @@ importers:
       preact: 10.11.3
       preact-router: 3.2.1_preact@10.11.3
       qrcode-generator: 1.4.4
-      swr: 1.3.0
+      swr: 2.0.3
     devDependencies:
       '@creativebulma/bulma-tooltip': 1.2.0
       '@gnu-taler/pogen': link:../pogen
@@ -640,7 +640,7 @@ importers:
       preact-render-to-string: ^5.2.6
       prettier: ^2.5.1
       rimraf: ^3.0.2
-      swr: 1.3.0
+      swr: 2.0.3
       tslib: ^2.4.0
       typescript: ^4.9.4
       ws: 7.4.5
@@ -658,7 +658,7 @@ importers:
       preact-render-to-string: 5.2.6_preact@10.11.3
       prettier: 2.7.1
       rimraf: 3.0.2
-      swr: 1.3.0
+      swr: 2.0.3
       tslib: 2.4.1
       typescript: 4.9.4
       ws: 7.4.5
@@ -14646,6 +14646,15 @@ packages:
     resolution: {integrity: 
sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
     peerDependencies:
       react: ^16.11.0 || ^17.0.0 || ^18.0.0
+    dev: false
+
+  /swr/2.0.3:
+    resolution: {integrity: 
sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==}
+    engines: {pnpm: '7'}
+    peerDependencies:
+      react: ^16.11.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      use-sync-external-store: 1.2.0
 
   /symbol-tree/3.2.4:
     resolution: {integrity: 
sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -15301,6 +15310,11 @@ packages:
       querystring: 0.2.0
     dev: true
 
+  /use-sync-external-store/1.2.0:
+    resolution: {integrity: 
sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
   /use/3.1.1:
     resolution: {integrity: 
sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
     engines: {node: '>=0.10.0'}

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