gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 04/04: impl accout management and refactor


From: gnunet
Subject: [taler-wallet-core] 04/04: impl accout management and refactor
Date: Wed, 08 Feb 2023 21:41:37 +0100

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

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

commit a8c5a9696c1735a178158cbc9ac4f9bb4b6f013d
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Feb 8 17:41:19 2023 -0300

    impl accout management and refactor
---
 packages/demobank-ui/package.json                  |   4 +-
 .../components/{Transactions => Cashouts}/index.ts |  14 +-
 .../components/{Loading.tsx => Cashouts/state.ts}  |  29 +-
 .../{Loading.tsx => Cashouts/stories.tsx}          |  32 +-
 .../components/{Transactions => Cashouts}/test.ts  |   9 +-
 .../demobank-ui/src/components/Cashouts/views.tsx  |  66 ++
 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 +-
 34 files changed, 3260 insertions(+), 1267 deletions(-)

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/demobank-ui/src/components/Loading.tsx 
b/packages/demobank-ui/src/components/Cashouts/state.ts
similarity index 55%
copy from packages/demobank-ui/src/components/Loading.tsx
copy to packages/demobank-ui/src/components/Cashouts/state.ts
index 8fd01858b..7e420940f 100644
--- a/packages/demobank-ui/src/components/Loading.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/state.ts
@@ -14,8 +14,31 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { h, VNode } from "preact";
+import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
+import { useCashouts } from "../../hooks/circuit.js";
+import { Props, State, Transaction } from "./index.js";
 
-export function Loading(): VNode {
-  return <div>loading...</div>;
+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
+    }
+  }
+
+
+  return {
+    status: "ready",
+    error: undefined,
+    cashout: result.data,
+  };
 }
diff --git a/packages/demobank-ui/src/components/Loading.tsx 
b/packages/demobank-ui/src/components/Cashouts/stories.tsx
similarity index 56%
copy from packages/demobank-ui/src/components/Loading.tsx
copy to packages/demobank-ui/src/components/Cashouts/stories.tsx
index 8fd01858b..77fdde092 100644
--- a/packages/demobank-ui/src/components/Loading.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/stories.tsx
@@ -14,8 +14,32 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { h, VNode } from "preact";
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { tests } from "@gnu-taler/web-util/lib/index.browser";
+import { ReadyView } from "./views.js";
+
+export default {
+  title: "transaction list",
+};
 
-export function Loading(): VNode {
-  return <div>loading...</div>;
-}
+export const Ready = tests.createExample(ReadyView, {
+  transactions: [
+    {
+      amount: {
+        currency: "USD",
+        fraction: 0,
+        value: 1,
+      },
+      counterpart: "ASD",
+      negative: false,
+      subject: "Some",
+      when: {
+        t_ms: new Date().getTime(),
+      },
+    },
+  ],
+});
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/Cashouts/views.tsx 
b/packages/demobank-ui/src/components/Cashouts/views.tsx
new file mode 100644
index 000000000..30803d4d1
--- /dev/null
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -0,0 +1,66 @@
+/*
+ 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 { h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { State } from "./index.js";
+import { format } from "date-fns";
+import { Amounts } from "@gnu-taler/taler-util";
+
+export function LoadingUriView({ error }: State.LoadingUriError): VNode {
+  const { i18n } = useTranslationContext();
+
+  return (
+    <div>
+      <i18n.Translate>Could not load</i18n.Translate>
+    </div>
+  );
+}
+
+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`Created`}</th>
+            <th>{i18n.str`Confirmed`}</th>
+            <th>{i18n.str`Counterpart`}</th>
+            <th>{i18n.str`Subject`}</th>
+          </tr>
+        </thead>
+        <tbody>
+          {cashouts.map((item, idx) => {
+            return (
+              <tr key={idx}>
+                <td>{format(item.creation_time, "dd/MM/yyyy HH:mm:ss")}</td>
+                <td>
+                  {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>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+}
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;

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