gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 12/19: no-fix: moved out AccountPage


From: gnunet
Subject: [taler-wallet-core] 12/19: no-fix: moved out AccountPage
Date: Wed, 07 Dec 2022 20:08:40 +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 c6f228bf142637eb72456aebabd0483d83402373
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Dec 7 12:38:50 2022 -0300

    no-fix: moved out AccountPage
---
 packages/demobank-ui/src/components/app.tsx        |   17 +
 packages/demobank-ui/src/pages/Routing.tsx         |    2 +-
 .../demobank-ui/src/pages/home/AccountPage.tsx     |  289 ++++
 packages/demobank-ui/src/pages/home/LoginForm.tsx  |  149 ++
 .../demobank-ui/src/pages/home/PaymentOptions.tsx  |   54 +
 .../src/pages/home/PaytoWireTransferForm.tsx       |  442 ++++++
 .../home/TalerWithdrawalConfirmationQuestion.tsx   |  300 ++++
 .../src/pages/home/TalerWithdrawalQRCode.tsx       |   97 ++
 .../src/pages/home/WalletWithdrawForm.tsx          |  176 +++
 packages/demobank-ui/src/pages/home/index.tsx      | 1502 --------------------
 packages/demobank-ui/src/utils.ts                  |   15 +
 11 files changed, 1540 insertions(+), 1503 deletions(-)

diff --git a/packages/demobank-ui/src/components/app.tsx 
b/packages/demobank-ui/src/components/app.tsx
index 35681a58c..f3bc3f571 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -3,6 +3,23 @@ import { PageStateProvider } from "../context/pageState.js";
 import { TranslationProvider } from "../context/translation.js";
 import { Routing } from "../pages/Routing.js";
 
+/**
+ * FIXME:
+ *
+ * - INPUT elements have their 'required' attribute ignored.
+ *
+ * - the page needs a "home" button that either redirects to
+ *   the profile page (when the user is logged in), or to
+ *   the very initial home page.
+ *
+ * - histories 'pages' are grouped in UL elements that cause
+ *   the rendering to visually separate each UL.  History elements
+ *   should instead line up without any separation caused by
+ *   a implementation detail.
+ *
+ * - Many strings need to be i18n-wrapped.
+ */
+
 const App: FunctionalComponent = () => {
   return (
     <TranslationProvider>
diff --git a/packages/demobank-ui/src/pages/Routing.tsx 
b/packages/demobank-ui/src/pages/Routing.tsx
index 1ef042297..7f079a7de 100644
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ b/packages/demobank-ui/src/pages/Routing.tsx
@@ -18,7 +18,7 @@ import { createHashHistory } from "history";
 import { h, VNode } from "preact";
 import Router, { route, Route } from "preact-router";
 import { useEffect } from "preact/hooks";
-import { AccountPage } from "./home/index.js";
+import { AccountPage } from "./home/AccountPage.js";
 import { PublicHistoriesPage } from "./home/PublicHistoriesPage.js";
 import { RegistrationPage } from "./home/RegistrationPage.js";
 
diff --git a/packages/demobank-ui/src/pages/home/AccountPage.tsx 
b/packages/demobank-ui/src/pages/home/AccountPage.tsx
new file mode 100644
index 000000000..2bc05c332
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/AccountPage.tsx
@@ -0,0 +1,289 @@
+/*
+ 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 { Amounts, HttpStatusCode } from "@gnu-taler/taler-util";
+import { hooks } from "@gnu-taler/web-util/lib/index.browser";
+import { h, Fragment, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import useSWR, { SWRConfig, useSWRConfig } from "swr";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { useBackendState } from "../../hooks/backend.js";
+import { bankUiSettings } from "../../settings.js";
+import { getIbanFromPayto } from "../../utils.js";
+import { BankFrame } from "./BankFrame.js";
+import { LoginForm } from "./LoginForm.js";
+import { PaymentOptions } from "./PaymentOptions.js";
+import { TalerWithdrawalQRCode } from "./TalerWithdrawalQRCode.js";
+import { Transactions } from "./Transactions.js";
+
+export function AccountPage(): VNode {
+  const [backendState, backendStateSetter] = useBackendState();
+  const { i18n } = useTranslationContext();
+  const { pageState, pageStateSetter } = usePageContext();
+
+  if (!pageState.isLoggedIn) {
+    return (
+      <BankFrame>
+        <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
+        <LoginForm />
+      </BankFrame>
+    );
+  }
+
+  if (typeof backendState === "undefined") {
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      isLoggedIn: false,
+      error: {
+        title: i18n.str`Page has a problem: logged in but backend state is 
lost.`,
+      },
+    }));
+    return <p>Error: waiting for details...</p>;
+  }
+  console.log("Showing the profile page..");
+  return (
+    <SWRWithCredentials
+      username={backendState.username}
+      password={backendState.password}
+      backendUrl={backendState.url}
+    >
+      <Account
+        accountLabel={backendState.username}
+        backendState={backendState}
+      />
+    </SWRWithCredentials>
+  );
+}
+
+/**
+ * Factor out login credentials.
+ */
+function SWRWithCredentials(props: any): VNode {
+  const { username, password, backendUrl } = props;
+  const headers = new Headers();
+  headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`);
+  console.log("Likely backend base URL", backendUrl);
+  return (
+    <SWRConfig
+      value={{
+        fetcher: (url: string) => {
+          return fetch(backendUrl + url || "", { headers }).then((r) => {
+            if (!r.ok) throw { status: r.status, json: r.json() };
+
+            return r.json();
+          });
+        },
+      }}
+    >
+      {props.children}
+    </SWRConfig>
+  );
+}
+
+/**
+ * Show only the account's balance.  NOTE: the backend state
+ * is mostly needed to provide the user's credentials to POST
+ * to the bank.
+ */
+function Account(Props: any): VNode {
+  const { cache } = useSWRConfig();
+  const { accountLabel, backendState } = Props;
+  // 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 { pageState, pageStateSetter: setPageState } = usePageContext();
+  const {
+    withdrawalInProgress,
+    withdrawalId,
+    isLoggedIn,
+    talerWithdrawUri,
+    timestamp,
+  } = pageState;
+  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") {
+    console.log("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: {
+        setPageState((prevState: PageStateType) => ({
+          ...prevState,
+
+          isLoggedIn: false,
+          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: {
+        setPageState((prevState: PageStateType) => ({
+          ...prevState,
+
+          isLoggedIn: false,
+          error: {
+            title: i18n.str`Wrong credentials given.`,
+          },
+        }));
+        return <p>Wrong credentials...</p>;
+      }
+      default: {
+        setPageState((prevState: PageStateType) => ({
+          ...prevState,
+
+          isLoggedIn: false,
+          error: {
+            title: i18n.str`Account information could not be retrieved.`,
+            debug: JSON.stringify(error),
+          },
+        }));
+        return <p>Unknown problem...</p>;
+      }
+    }
+  }
+  const balance = !data ? undefined : 
Amounts.parseOrThrow(data.balance.amount);
+  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.
+   */
+  console.log(`maybe new withdrawal ${talerWithdrawUri}`);
+  if (talerWithdrawUri) {
+    console.log("Bank created a new Taler withdrawal");
+    return (
+      <BankFrame>
+        <TalerWithdrawalQRCode
+          accountLabel={accountLabel}
+          backendState={backendState}
+          withdrawalId={withdrawalId}
+          talerWithdrawUri={talerWithdrawUri}
+        />
+      </BankFrame>
+    );
+  }
+  const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
+
+  return (
+    <BankFrame>
+      <div>
+        <h1 class="nav welcome-text">
+          <i18n.Translate>
+            Welcome,
+            {accountNumber
+              ? `${accountLabel} (${accountNumber})`
+              : accountLabel}
+            !
+          </i18n.Translate>
+        </h1>
+      </div>
+      <section id="assets">
+        <div class="asset-summary">
+          <h2>{i18n.str`Bank account balance`}</h2>
+          {!balance ? (
+            <div class="large-amount" style={{ color: "gray" }}>
+              Waiting server response...
+            </div>
+          ) : (
+            <div class="large-amount amount">
+              {balanceIsDebit ? <b>-</b> : null}
+              <span class="value">{`${balanceValue}`}</span>&nbsp;
+              <span class="currency">{`${balance.currency}`}</span>
+            </div>
+          )}
+        </div>
+      </section>
+      <section id="payments">
+        <div class="payments">
+          <h2>{i18n.str`Payments`}</h2>
+          <PaymentOptions currency={balance?.currency} />
+        </div>
+      </section>
+      <section id="main">
+        <article>
+          <h2>{i18n.str`Latest transactions:`}</h2>
+          <Transactions
+            balanceValue={balanceValue}
+            pageNumber="0"
+            accountLabel={accountLabel}
+          />
+        </article>
+      </section>
+    </BankFrame>
+  );
+}
+
+function useTransactionPageNumber(): [number, StateUpdater<number>] {
+  const ret = hooks.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];
+}
diff --git a/packages/demobank-ui/src/pages/home/LoginForm.tsx 
b/packages/demobank-ui/src/pages/home/LoginForm.tsx
new file mode 100644
index 000000000..f60c9f600
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/LoginForm.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 { h, VNode } from "preact";
+import { route } from "preact-router";
+import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType, useBackendState } from "../../hooks/backend.js";
+import { bankUiSettings } from "../../settings.js";
+import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+/**
+ * Collect and submit login data.
+ */
+export function LoginForm(): VNode {
+  const [backendState, backendStateSetter] = useBackendState();
+  const { pageState, pageStateSetter } = usePageContext();
+  const [username, setUsername] = useState<string | undefined>();
+  const [password, setPassword] = useState<string | undefined>();
+  const { i18n } = useTranslationContext();
+  const ref = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    ref.current?.focus();
+  }, []);
+
+  const errors = undefinedIfEmpty({
+    username: !username ? i18n.str`Missing username` : undefined,
+    password: !password ? i18n.str`Missing password` : undefined,
+  });
+
+  return (
+    <div class="login-div">
+      <form action="javascript:void(0);" class="login-form" noValidate>
+        <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={() => {
+              if (!username || !password) return;
+              loginCall(
+                { username, password },
+                backendStateSetter,
+                pageStateSetter,
+              );
+              setUsername(undefined);
+              setPassword(undefined);
+            }}
+          >
+            {i18n.str`Login`}
+          </button>
+
+          {bankUiSettings.allowRegistrations ? (
+            <button
+              class="pure-button pure-button-secondary btn-cancel"
+              onClick={() => {
+                route("/register");
+              }}
+            >
+              {i18n.str`Register`}
+            </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.
+   */
+  backendStateSetter: StateUpdater<BackendStateType | undefined>,
+  pageStateSetter: StateUpdater<PageStateType>,
+): Promise<void> {
+  /**
+   * Optimistically setting the state as 'logged in', and
+   * let the Account component request the balance to check
+   * whether the credentials are valid.  */
+  pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
+  let baseUrl = getBankBackendBaseUrl();
+  if (!baseUrl.endsWith("/")) baseUrl += "/";
+
+  backendStateSetter((prevState) => ({
+    ...prevState,
+    url: baseUrl,
+    username: req.username,
+    password: req.password,
+  }));
+}
diff --git a/packages/demobank-ui/src/pages/home/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/home/PaymentOptions.tsx
new file mode 100644
index 000000000..69c8d383e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/PaymentOptions.tsx
@@ -0,0 +1,54 @@
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslationContext } from "../../context/translation.js";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
+
+/**
+ * Let the user choose a payment option,
+ * then specify the details trigger the action.
+ */
+export function PaymentOptions({ currency }: { currency?: string }): VNode {
+  const { i18n } = useTranslationContext();
+
+  const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
+    "charge-wallet",
+  );
+
+  return (
+    <article>
+      <div class="payments">
+        <div class="tab">
+          <button
+            class={tab === "charge-wallet" ? "tablinks active" : "tablinks"}
+            onClick={(): void => {
+              setTab("charge-wallet");
+            }}
+          >
+            {i18n.str`Obtain digital cash`}
+          </button>
+          <button
+            class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
+            onClick={(): void => {
+              setTab("wire-transfer");
+            }}
+          >
+            {i18n.str`Transfer to bank account`}
+          </button>
+        </div>
+        {tab === "charge-wallet" && (
+          <div id="charge-wallet" class="tabcontent active">
+            <h3>{i18n.str`Obtain digital cash`}</h3>
+            <WalletWithdrawForm focus currency={currency} />
+          </div>
+        )}
+        {tab === "wire-transfer" && (
+          <div id="wire-transfer" class="tabcontent active">
+            <h3>{i18n.str`Transfer to bank account`}</h3>
+            <PaytoWireTransferForm focus currency={currency} />
+          </div>
+        )}
+      </div>
+    </article>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx
new file mode 100644
index 000000000..45e7cf5ca
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx
@@ -0,0 +1,442 @@
+/*
+ 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 { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
+import { hooks } from "@gnu-taler/web-util/lib/index.browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType, useBackendState } from "../../hooks/backend.js";
+import { prepareHeaders, undefinedIfEmpty } from "../../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+export function PaytoWireTransferForm({
+  focus,
+  currency,
+}: {
+  focus?: boolean;
+  currency?: string;
+}): VNode {
+  const [backendState, backendStateSetter] = useBackendState();
+  const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for 
go-back button?
+
+  const [submitData, submitDataSetter] = useWireTransferRequestType();
+
+  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]);
+
+  let parsedAmount = undefined;
+
+  const errorsWire = {
+    iban: !submitData?.iban
+      ? i18n.str`Missing IBAN`
+      : !/^[A-Z0-9]*$/.test(submitData.iban)
+      ? i18n.str`IBAN should have just uppercased letters and numbers`
+      : undefined,
+    subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
+    amount: !submitData?.amount
+      ? i18n.str`Missing amount`
+      : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
+      ? i18n.str`Amount is not valid`
+      : Amounts.isZero(parsedAmount)
+      ? i18n.str`Should be greater than 0`
+      : undefined,
+  };
+
+  if (!pageState.isRawPayto)
+    return (
+      <div>
+        <form class="pure-form" name="wire-transfer-form">
+          <p>
+            <label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
+            <input
+              ref={ref}
+              type="text"
+              id="iban"
+              name="iban"
+              value={submitData?.iban ?? ""}
+              placeholder="CC0123456789"
+              required
+              pattern={ibanRegex}
+              onInput={(e): void => {
+                submitDataSetter((submitData: any) => ({
+                  ...submitData,
+                  iban: e.currentTarget.value,
+                }));
+              }}
+            />
+            <br />
+            <ShowInputErrorLabel
+              message={errorsWire?.iban}
+              isDirty={submitData?.iban !== undefined}
+            />
+            <br />
+            <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
+            <input
+              type="text"
+              name="subject"
+              id="subject"
+              placeholder="subject"
+              value={submitData?.subject ?? ""}
+              required
+              onInput={(e): void => {
+                submitDataSetter((submitData: any) => ({
+                  ...submitData,
+                  subject: e.currentTarget.value,
+                }));
+              }}
+            />
+            <br />
+            <ShowInputErrorLabel
+              message={errorsWire?.subject}
+              isDirty={submitData?.subject !== undefined}
+            />
+            <br />
+            <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
+            <input
+              type="text"
+              readonly
+              class="currency-indicator"
+              size={currency?.length}
+              maxLength={currency?.length}
+              tabIndex={-1}
+              value={currency}
+            />
+            &nbsp;
+            <input
+              type="number"
+              name="amount"
+              id="amount"
+              placeholder="amount"
+              required
+              value={submitData?.amount ?? ""}
+              onInput={(e): void => {
+                submitDataSetter((submitData: any) => ({
+                  ...submitData,
+                  amount: e.currentTarget.value,
+                }));
+              }}
+            />
+            <ShowInputErrorLabel
+              message={errorsWire?.amount}
+              isDirty={submitData?.amount !== undefined}
+            />
+          </p>
+
+          <p style={{ display: "flex", justifyContent: "space-between" }}>
+            <input
+              type="submit"
+              class="pure-button pure-button-primary"
+              disabled={!!errorsWire}
+              value="Send"
+              onClick={async () => {
+                if (
+                  typeof submitData === "undefined" ||
+                  typeof submitData.iban === "undefined" ||
+                  submitData.iban === "" ||
+                  typeof submitData.subject === "undefined" ||
+                  submitData.subject === "" ||
+                  typeof submitData.amount === "undefined" ||
+                  submitData.amount === ""
+                ) {
+                  console.log("Not all the fields were given.");
+                  pageStateSetter((prevState: PageStateType) => ({
+                    ...prevState,
+
+                    error: {
+                      title: i18n.str`Field(s) missing.`,
+                    },
+                  }));
+                  return;
+                }
+                transactionData = {
+                  paytoUri: `payto://iban/${
+                    submitData.iban
+                  }?message=${encodeURIComponent(submitData.subject)}`,
+                  amount: `${currency}:${submitData.amount}`,
+                };
+                return await createTransactionCall(
+                  transactionData,
+                  backendState,
+                  pageStateSetter,
+                  () =>
+                    submitDataSetter((p) => ({
+                      amount: undefined,
+                      iban: undefined,
+                      subject: undefined,
+                    })),
+                );
+              }}
+            />
+            <input
+              type="button"
+              class="pure-button"
+              value="Clear"
+              onClick={async () => {
+                submitDataSetter((p) => ({
+                  amount: undefined,
+                  iban: undefined,
+                  subject: undefined,
+                }));
+              }}
+            />
+          </p>
+        </form>
+        <p>
+          <a
+            href="/account"
+            onClick={() => {
+              console.log("switch to raw payto form");
+              pageStateSetter((prevState: any) => ({
+                ...prevState,
+                isRawPayto: true,
+              }));
+            }}
+          >
+            {i18n.str`Want to try the raw payto://-format?`}
+          </a>
+        </p>
+      </div>
+    );
+
+  const errorsPayto = undefinedIfEmpty({
+    rawPaytoInput: !rawPaytoInput
+      ? i18n.str`Missing payto address`
+      : !parsePaytoUri(rawPaytoInput)
+      ? i18n.str`Payto does not follow the pattern`
+      : undefined,
+  });
+
+  return (
+    <div>
+      <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
+      <div class="pure-form" name="payto-form">
+        <p>
+          <label for="address">{i18n.str`payto URI:`}</label>&nbsp;
+          <input
+            name="address"
+            type="text"
+            size={50}
+            ref={ref}
+            id="address"
+            value={rawPaytoInput ?? ""}
+            required
+            placeholder={i18n.str`payto address`}
+            // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 
]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
+            onInput={(e): void => {
+              rawPaytoInputSetter(e.currentTarget.value);
+            }}
+          />
+          <ShowInputErrorLabel
+            message={errorsPayto?.rawPaytoInput}
+            isDirty={rawPaytoInput !== undefined}
+          />
+          <br />
+          <div class="hint">
+            Hint:
+            <code>
+              payto://iban/[receiver-iban]?message=[subject]&amount=[{currency}
+              :X.Y]
+            </code>
+          </div>
+        </p>
+        <p>
+          <input
+            class="pure-button pure-button-primary"
+            type="submit"
+            disabled={!!errorsPayto}
+            value={i18n.str`Send`}
+            onClick={async () => {
+              // empty string evaluates to false.
+              if (!rawPaytoInput) {
+                console.log("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,
+                backendState,
+                pageStateSetter,
+                () => rawPaytoInputSetter(undefined),
+              );
+            }}
+          />
+        </p>
+        <p>
+          <a
+            href="/account"
+            onClick={() => {
+              console.log("switch to wire-transfer-form");
+              pageStateSetter((prevState: any) => ({
+                ...prevState,
+                isRawPayto: false,
+              }));
+            }}
+          >
+            {i18n.str`Use wire-transfer form?`}
+          </a>
+        </p>
+      </div>
+    </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 = hooks.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: BackendStateType | undefined,
+  pageStateSetter: StateUpdater<PageStateType>,
+  /**
+   * Optional since the raw payto form doesn't have
+   * a stateful management of the input data yet.
+   */
+  cleanUpForm: () => void,
+): Promise<void> {
+  let res: any;
+  try {
+    res = await postToBackend(
+      `access-api/accounts/${getUsername(backendState)}/transactions`,
+      backendState,
+      JSON.stringify(req),
+    );
+  } catch (error) {
+    console.log("Could not POST transaction request to the bank", error);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `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();
+    console.log(
+      `Transfer creation gave response error: ${response} (${res.status})`,
+    );
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `Transfer creation gave response error`,
+        description: response.error.description,
+        debug: JSON.stringify(response),
+      },
+    }));
+    return;
+  }
+  // status is 200 OK here, tell the user.
+  console.log("Wire transfer created!");
+  pageStateSetter((prevState) => ({
+    ...prevState,
+
+    info: "Wire transfer created!",
+  }));
+
+  // Only at this point the input data can
+  // be discarded.
+  cleanUpForm();
+}
+
+/**
+ * Get username from the backend state, and throw
+ * exception if not found.
+ */
+function getUsername(backendState: BackendStateType | undefined): string {
+  if (typeof backendState === "undefined")
+    throw Error("Username can't be found in a undefined backend state.");
+
+  if (!backendState.username) {
+    throw Error("No username, must login first.");
+  }
+  return backendState.username;
+}
+
+/**
+ * Helps extracting the credentials from the state
+ * and wraps the actual call to 'fetch'.  Should be
+ * enclosed in a try-catch block by the caller.
+ */
+async function postToBackend(
+  uri: string,
+  backendState: BackendStateType | undefined,
+  body: string,
+): Promise<any> {
+  if (typeof backendState === "undefined")
+    throw Error("Credentials can't be found in a undefined backend state.");
+
+  const { username, password } = backendState;
+  const headers = prepareHeaders(username, password);
+  // Backend URL must have been stored _with_ a final slash.
+  const url = new URL(uri, backendState.url);
+  return await fetch(url.href, {
+    method: "POST",
+    headers,
+    body,
+  });
+}
diff --git 
a/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx
new file mode 100644
index 000000000..e3d8957b8
--- /dev/null
+++ 
b/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx
@@ -0,0 +1,300 @@
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType } from "../../hooks/backend.js";
+import { prepareHeaders } from "../../utils.js";
+
+/**
+ * Additional authentication required to complete the operation.
+ * Not providing a back button, only abort.
+ */
+export function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
+  const { pageState, pageStateSetter } = usePageContext();
+  const { backendState } = Props;
+  const { i18n } = useTranslationContext();
+  const captchaNumbers = {
+    a: Math.floor(Math.random() * 10),
+    b: Math.floor(Math.random() * 10),
+  };
+  let captchaAnswer = "";
+
+  return (
+    <Fragment>
+      <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
+      <article>
+        <div class="challenge-div">
+          <form class="challenge-form" noValidate>
+            <div class="pure-form" id="captcha" name="capcha-form">
+              <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
+              <p>
+                <label for="answer">
+                  {i18n.str`What is`}&nbsp;
+                  <em>
+                    {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
+                  </em>
+                  ?&nbsp;
+                </label>
+                &nbsp;
+                <input
+                  name="answer"
+                  id="answer"
+                  type="text"
+                  autoFocus
+                  required
+                  onInput={(e): void => {
+                    captchaAnswer = e.currentTarget.value;
+                  }}
+                />
+              </p>
+              <p>
+                <button
+                  class="pure-button pure-button-primary btn-confirm"
+                  onClick={(e) => {
+                    e.preventDefault();
+                    if (
+                      captchaAnswer ==
+                      (captchaNumbers.a + captchaNumbers.b).toString()
+                    ) {
+                      confirmWithdrawalCall(
+                        backendState,
+                        pageState.withdrawalId,
+                        pageStateSetter,
+                      );
+                      return;
+                    }
+                    pageStateSetter((prevState: PageStateType) => ({
+                      ...prevState,
+
+                      error: {
+                        title: i18n.str`Answer is wrong.`,
+                      },
+                    }));
+                  }}
+                >
+                  {i18n.str`Confirm`}
+                </button>
+                &nbsp;
+                <button
+                  class="pure-button pure-button-secondary btn-cancel"
+                  onClick={async () =>
+                    await abortWithdrawalCall(
+                      backendState,
+                      pageState.withdrawalId,
+                      pageStateSetter,
+                    )
+                  }
+                >
+                  {i18n.str`Cancel`}
+                </button>
+              </p>
+            </div>
+          </form>
+          <div class="hint">
+            <p>
+              <i18n.Translate>
+                A this point, a <b>real</b> bank would ask for an additional
+                authentication proof (PIN/TAN, one time password, ..), instead
+                of a simple calculation.
+              </i18n.Translate>
+            </p>
+          </div>
+        </div>
+      </article>
+    </Fragment>
+  );
+}
+
+/**
+ * This function confirms a withdrawal operation AFTER
+ * the wallet has given the exchange's payment details
+ * to the bank (via the Integration API).  Such details
+ * can be given by scanning a QR code or by passing the
+ * raw taler://withdraw-URI to the CLI wallet.
+ *
+ * This function will set the confirmation status in the
+ * 'page state' and let the related components refresh.
+ */
+async function confirmWithdrawalCall(
+  backendState: BackendStateType | undefined,
+  withdrawalId: string | undefined,
+  pageStateSetter: StateUpdater<PageStateType>,
+): Promise<void> {
+  if (typeof backendState === "undefined") {
+    console.log("No credentials found.");
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: "No credentials found.",
+      },
+    }));
+    return;
+  }
+  if (typeof withdrawalId === "undefined") {
+    console.log("No withdrawal ID found.");
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: "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) {
+    console.log("Could not POST withdrawal confirmation to the bank", error);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `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
+    console.log(
+      `Withdrawal confirmation gave response error (${res.status})`,
+      res.statusText,
+    );
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `Withdrawal confirmation gave response error`,
+        debug: JSON.stringify(response),
+      },
+    }));
+    return;
+  }
+  console.log("Withdrawal operation confirmed!");
+  pageStateSetter((prevState) => {
+    const { talerWithdrawUri, ...rest } = prevState;
+    return {
+      ...rest,
+
+      info: "Withdrawal confirmed!",
+    };
+  });
+}
+
+/**
+ * Abort a withdrawal operation via the Access API's /abort.
+ */
+async function abortWithdrawalCall(
+  backendState: BackendStateType | undefined,
+  withdrawalId: string | undefined,
+  pageStateSetter: StateUpdater<PageStateType>,
+): Promise<void> {
+  if (typeof backendState === "undefined") {
+    console.log("No credentials found.");
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `No credentials found.`,
+      },
+    }));
+    return;
+  }
+  if (typeof withdrawalId === "undefined") {
+    console.log("No withdrawal ID found.");
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `No withdrawal ID found.`,
+      },
+    }));
+    return;
+  }
+  let res: any;
+  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) {
+    console.log("Could not abort the withdrawal", error);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `Could not abort the withdrawal.`,
+        description: (error as any).error.description,
+        debug: JSON.stringify(error),
+      },
+    }));
+    return;
+  }
+  if (!res.ok) {
+    const response = await res.json();
+    console.log(
+      `Withdrawal abort gave response error (${res.status})`,
+      res.statusText,
+    );
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `Withdrawal abortion failed.`,
+        description: response.error.description,
+        debug: JSON.stringify(response),
+      },
+    }));
+    return;
+  }
+  console.log("Withdrawal operation aborted!");
+  pageStateSetter((prevState) => {
+    const { ...rest } = prevState;
+    return {
+      ...rest,
+
+      info: "Withdrawal aborted!",
+    };
+  });
+}
diff --git a/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx 
b/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx
new file mode 100644
index 000000000..da4ccc45e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx
@@ -0,0 +1,97 @@
+import { Fragment, h, VNode } from "preact";
+import useSWR from "swr";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { QrCodeSection } from "./QrCodeSection.js";
+import { TalerWithdrawalConfirmationQuestion } from 
"./TalerWithdrawalConfirmationQuestion.js";
+
+/**
+ * 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 TalerWithdrawalQRCode(Props: any): VNode {
+  // turns true when the wallet POSTed the reserve details:
+  const { pageState, pageStateSetter } = usePageContext();
+  const { withdrawalId, talerWithdrawUri, backendState } = Props;
+  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>
+  );
+
+  console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
+  // waiting for the wallet:
+
+  const { data, error } = useSWR(
+    `integration-api/withdrawal-operation/${withdrawalId}`,
+    { refreshInterval: 1000 },
+  );
+
+  if (typeof error !== "undefined") {
+    console.log(
+      `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>
+    );
+  }
+
+  // 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:
+   */
+  console.log("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.selection_done) {
+    return (
+      <QrCodeSection
+        talerWithdrawUri={talerWithdrawUri}
+        abortButton={abortButton}
+      />
+    );
+  }
+  /**
+   * Wallet POSTed the withdrawal details!  Ask the
+   * user to authorize the operation (here CAPTCHA).
+   */
+  return <TalerWithdrawalConfirmationQuestion backendState={backendState} />;
+}
diff --git a/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx
new file mode 100644
index 000000000..842f14a5f
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx
@@ -0,0 +1,176 @@
+/*
+ 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 { StateUpdater, useEffect, useRef } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType, useBackendState } from "../../hooks/backend.js";
+import { prepareHeaders, validateAmount } from "../../utils.js";
+
+export function WalletWithdrawForm({
+  focus,
+  currency,
+}: {
+  currency?: string;
+  focus?: boolean;
+}): VNode {
+  const [backendState, backendStateSetter] = useBackendState();
+  const { pageState, pageStateSetter } = usePageContext();
+  const { i18n } = useTranslationContext();
+  let submitAmount = "5.00";
+
+  const ref = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    if (focus) ref.current?.focus();
+  }, [focus]);
+  return (
+    <form id="reserve-form" class="pure-form" name="tform">
+      <p>
+        <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
+        &nbsp;
+        <input
+          type="text"
+          readonly
+          class="currency-indicator"
+          size={currency?.length ?? 5}
+          maxLength={currency?.length}
+          tabIndex={-1}
+          value={currency}
+        />
+        &nbsp;
+        <input
+          type="number"
+          ref={ref}
+          id="withdraw-amount"
+          name="withdraw-amount"
+          value={submitAmount}
+          onChange={(e): void => {
+            // FIXME: validate using 'parseAmount()',
+            // deactivate submit button as long as
+            // amount is not valid
+            submitAmount = e.currentTarget.value;
+          }}
+        />
+      </p>
+      <p>
+        <div>
+          <input
+            id="select-exchange"
+            class="pure-button pure-button-primary"
+            type="submit"
+            value={i18n.str`Withdraw`}
+            onClick={() => {
+              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}`,
+                backendState,
+                pageStateSetter,
+              );
+            }}
+          />
+        </div>
+      </p>
+    </form>
+  );
+}
+
+/**
+ * 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: BackendStateType | undefined,
+  pageStateSetter: StateUpdater<PageStateType>,
+): Promise<void> {
+  if (typeof backendState === "undefined") {
+    console.log("Page has a problem: no credentials found in the state.");
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: "No credentials given.",
+      },
+    }));
+    return;
+  }
+
+  let res: any;
+  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) {
+    console.log("Could not POST withdrawal request to the bank", error);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `Could not create withdrawal operation`,
+        description: (error as any).error.description,
+        debug: JSON.stringify(error),
+      },
+    }));
+    return;
+  }
+  if (!res.ok) {
+    const response = await res.json();
+    console.log(
+      `Withdrawal creation gave response error: ${response} (${res.status})`,
+    );
+    pageStateSetter((prevState) => ({
+      ...prevState,
+
+      error: {
+        title: `Withdrawal creation gave response error`,
+        description: response.error.description,
+        debug: JSON.stringify(response),
+      },
+    }));
+    return;
+  }
+
+  console.log("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/home/index.tsx 
b/packages/demobank-ui/src/pages/home/index.tsx
deleted file mode 100644
index ca5cae571..000000000
--- a/packages/demobank-ui/src/pages/home/index.tsx
+++ /dev/null
@@ -1,1502 +0,0 @@
-/*
- 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/>
- */
-
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { h, Fragment, VNode } from "preact";
-import useSWR, { SWRConfig, useSWRConfig } from "swr";
-
-import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-import { route } from "preact-router";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendStateType, useBackendState } from "../../hooks/backend.js";
-import { bankUiSettings } from "../../settings.js";
-import { QrCodeSection } from "./QrCodeSection.js";
-import {
-  getBankBackendBaseUrl,
-  getIbanFromPayto,
-  undefinedIfEmpty,
-  validateAmount,
-} from "../../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { Transactions } from "./Transactions.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-
-/**
- * FIXME:
- *
- * - INPUT elements have their 'required' attribute ignored.
- *
- * - the page needs a "home" button that either redirects to
- *   the profile page (when the user is logged in), or to
- *   the very initial home page.
- *
- * - histories 'pages' are grouped in UL elements that cause
- *   the rendering to visually separate each UL.  History elements
- *   should instead line up without any separation caused by
- *   a implementation detail.
- *
- * - Many strings need to be i18n-wrapped.
- */
-
-/************
- * Helpers. *
- ***********/
-
-/**
- * Get username from the backend state, and throw
- * exception if not found.
- */
-function getUsername(backendState: BackendStateType | undefined): string {
-  if (typeof backendState === "undefined")
-    throw Error("Username can't be found in a undefined backend state.");
-
-  if (!backendState.username) {
-    throw Error("No username, must login first.");
-  }
-  return backendState.username;
-}
-
-/**
- * Helps extracting the credentials from the state
- * and wraps the actual call to 'fetch'.  Should be
- * enclosed in a try-catch block by the caller.
- */
-async function postToBackend(
-  uri: string,
-  backendState: BackendStateType | undefined,
-  body: string,
-): Promise<any> {
-  if (typeof backendState === "undefined")
-    throw Error("Credentials can't be found in a undefined backend state.");
-
-  const { username, password } = backendState;
-  const headers = prepareHeaders(username, password);
-  // Backend URL must have been stored _with_ a final slash.
-  const url = new URL(uri, backendState.url);
-  return await fetch(url.href, {
-    method: "POST",
-    headers,
-    body,
-  });
-}
-
-function useTransactionPageNumber(): [number, StateUpdater<number>] {
-  const ret = hooks.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];
-}
-
-/**
- * Craft headers with Authorization and Content-Type.
- */
-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;
-}
-
-/*******************
- * State managers. *
- ******************/
-
-/**
- * Stores the raw Payto value entered by the user in the state.
- */
-type RawPaytoInputType = string;
-type RawPaytoInputTypeOpt = RawPaytoInputType | undefined;
-function useRawPaytoInputType(
-  state?: RawPaytoInputType,
-): [RawPaytoInputTypeOpt, StateUpdater<RawPaytoInputTypeOpt>] {
-  const ret = hooks.useLocalStorage("raw-payto-input-state", state);
-  const retObj: RawPaytoInputTypeOpt = ret[0];
-  const retSetter: StateUpdater<RawPaytoInputTypeOpt> = function (val) {
-    const newVal = val instanceof Function ? val(retObj) : val;
-    ret[1](newVal);
-  };
-  return [retObj, retSetter];
-}
-
-/**
- * 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 = hooks.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];
-}
-
-/**
- * Request preparators.
- *
- * These functions aim at sanitizing the input received
- * from users - for example via a HTML form - and create
- * a HTTP request object out of that.
- */
-
-/******************
- * HTTP wrappers. *
- *****************/
-
-/**
- * A 'wrapper' is typically a function that prepares one
- * particular API call and updates the state accordingly.  */
-
-/**
- * Abort a withdrawal operation via the Access API's /abort.
- */
-async function abortWithdrawalCall(
-  backendState: BackendStateType | undefined,
-  withdrawalId: string | undefined,
-  pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
-  if (typeof backendState === "undefined") {
-    console.log("No credentials found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `No credentials found.`,
-      },
-    }));
-    return;
-  }
-  if (typeof withdrawalId === "undefined") {
-    console.log("No withdrawal ID found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `No withdrawal ID found.`,
-      },
-    }));
-    return;
-  }
-  let res: any;
-  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) {
-    console.log("Could not abort the withdrawal", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `Could not abort the withdrawal.`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  if (!res.ok) {
-    const response = await res.json();
-    console.log(
-      `Withdrawal abort gave response error (${res.status})`,
-      res.statusText,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `Withdrawal abortion failed.`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  console.log("Withdrawal operation aborted!");
-  pageStateSetter((prevState) => {
-    const { ...rest } = prevState;
-    return {
-      ...rest,
-
-      info: "Withdrawal aborted!",
-    };
-  });
-}
-
-/**
- * This function confirms a withdrawal operation AFTER
- * the wallet has given the exchange's payment details
- * to the bank (via the Integration API).  Such details
- * can be given by scanning a QR code or by passing the
- * raw taler://withdraw-URI to the CLI wallet.
- *
- * This function will set the confirmation status in the
- * 'page state' and let the related components refresh.
- */
-async function confirmWithdrawalCall(
-  backendState: BackendStateType | undefined,
-  withdrawalId: string | undefined,
-  pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
-  if (typeof backendState === "undefined") {
-    console.log("No credentials found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: "No credentials found.",
-      },
-    }));
-    return;
-  }
-  if (typeof withdrawalId === "undefined") {
-    console.log("No withdrawal ID found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: "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) {
-    console.log("Could not POST withdrawal confirmation to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `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
-    console.log(
-      `Withdrawal confirmation gave response error (${res.status})`,
-      res.statusText,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `Withdrawal confirmation gave response error`,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  console.log("Withdrawal operation confirmed!");
-  pageStateSetter((prevState) => {
-    const { talerWithdrawUri, ...rest } = prevState;
-    return {
-      ...rest,
-
-      info: "Withdrawal confirmed!",
-    };
-  });
-}
-
-/**
- * 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: BackendStateType | undefined,
-  pageStateSetter: StateUpdater<PageStateType>,
-  /**
-   * Optional since the raw payto form doesn't have
-   * a stateful management of the input data yet.
-   */
-  cleanUpForm: () => void,
-): Promise<void> {
-  let res: any;
-  try {
-    res = await postToBackend(
-      `access-api/accounts/${getUsername(backendState)}/transactions`,
-      backendState,
-      JSON.stringify(req),
-    );
-  } catch (error) {
-    console.log("Could not POST transaction request to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `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();
-    console.log(
-      `Transfer creation gave response error: ${response} (${res.status})`,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `Transfer creation gave response error`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  // status is 200 OK here, tell the user.
-  console.log("Wire transfer created!");
-  pageStateSetter((prevState) => ({
-    ...prevState,
-
-    info: "Wire transfer created!",
-  }));
-
-  // Only at this point the input data can
-  // be discarded.
-  cleanUpForm();
-}
-
-/**
- * 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: BackendStateType | undefined,
-  pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
-  if (typeof backendState === "undefined") {
-    console.log("Page has a problem: no credentials found in the state.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: "No credentials given.",
-      },
-    }));
-    return;
-  }
-
-  let res: any;
-  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) {
-    console.log("Could not POST withdrawal request to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `Could not create withdrawal operation`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  if (!res.ok) {
-    const response = await res.json();
-    console.log(
-      `Withdrawal creation gave response error: ${response} (${res.status})`,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: `Withdrawal creation gave response error`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-
-  console.log("Withdrawal operation created!");
-  const resp = await res.json();
-  pageStateSetter((prevState: PageStateType) => ({
-    ...prevState,
-    withdrawalInProgress: true,
-    talerWithdrawUri: resp.taler_withdraw_uri,
-    withdrawalId: resp.withdrawal_id,
-  }));
-}
-
-async function loginCall(
-  req: { username: string; password: string },
-  /**
-   * FIXME: figure out if the two following
-   * functions can be retrieved from the state.
-   */
-  backendStateSetter: StateUpdater<BackendStateType | undefined>,
-  pageStateSetter: StateUpdater<PageStateType>,
-): Promise<void> {
-  /**
-   * Optimistically setting the state as 'logged in', and
-   * let the Account component request the balance to check
-   * whether the credentials are valid.  */
-  pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
-  let baseUrl = getBankBackendBaseUrl();
-  if (!baseUrl.endsWith("/")) baseUrl += "/";
-
-  backendStateSetter((prevState) => ({
-    ...prevState,
-    url: baseUrl,
-    username: req.username,
-    password: req.password,
-  }));
-}
-
-/**************************
- * Functional components. *
- *************************/
-
-function PaytoWireTransfer({
-  focus,
-  currency,
-}: {
-  focus?: boolean;
-  currency?: string;
-}): VNode {
-  const [backendState, backendStateSetter] = useBackendState();
-  const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for 
go-back button?
-
-  const [submitData, submitDataSetter] = useWireTransferRequestType();
-
-  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]);
-
-  let parsedAmount = undefined;
-
-  const errorsWire = {
-    iban: !submitData?.iban
-      ? i18n.str`Missing IBAN`
-      : !/^[A-Z0-9]*$/.test(submitData.iban)
-      ? i18n.str`IBAN should have just uppercased letters and numbers`
-      : undefined,
-    subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
-    amount: !submitData?.amount
-      ? i18n.str`Missing amount`
-      : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
-      ? i18n.str`Amount is not valid`
-      : Amounts.isZero(parsedAmount)
-      ? i18n.str`Should be greater than 0`
-      : undefined,
-  };
-
-  if (!pageState.isRawPayto)
-    return (
-      <div>
-        <form class="pure-form" name="wire-transfer-form">
-          <p>
-            <label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
-            <input
-              ref={ref}
-              type="text"
-              id="iban"
-              name="iban"
-              value={submitData?.iban ?? ""}
-              placeholder="CC0123456789"
-              required
-              pattern={ibanRegex}
-              onInput={(e): void => {
-                submitDataSetter((submitData: any) => ({
-                  ...submitData,
-                  iban: e.currentTarget.value,
-                }));
-              }}
-            />
-            <br />
-            <ShowInputErrorLabel
-              message={errorsWire?.iban}
-              isDirty={submitData?.iban !== undefined}
-            />
-            <br />
-            <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
-            <input
-              type="text"
-              name="subject"
-              id="subject"
-              placeholder="subject"
-              value={submitData?.subject ?? ""}
-              required
-              onInput={(e): void => {
-                submitDataSetter((submitData: any) => ({
-                  ...submitData,
-                  subject: e.currentTarget.value,
-                }));
-              }}
-            />
-            <br />
-            <ShowInputErrorLabel
-              message={errorsWire?.subject}
-              isDirty={submitData?.subject !== undefined}
-            />
-            <br />
-            <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
-            <input
-              type="text"
-              readonly
-              class="currency-indicator"
-              size={currency?.length}
-              maxLength={currency?.length}
-              tabIndex={-1}
-              value={currency}
-            />
-            &nbsp;
-            <input
-              type="number"
-              name="amount"
-              id="amount"
-              placeholder="amount"
-              required
-              value={submitData?.amount ?? ""}
-              onInput={(e): void => {
-                submitDataSetter((submitData: any) => ({
-                  ...submitData,
-                  amount: e.currentTarget.value,
-                }));
-              }}
-            />
-            <ShowInputErrorLabel
-              message={errorsWire?.amount}
-              isDirty={submitData?.amount !== undefined}
-            />
-          </p>
-
-          <p style={{ display: "flex", justifyContent: "space-between" }}>
-            <input
-              type="submit"
-              class="pure-button pure-button-primary"
-              disabled={!!errorsWire}
-              value="Send"
-              onClick={async () => {
-                if (
-                  typeof submitData === "undefined" ||
-                  typeof submitData.iban === "undefined" ||
-                  submitData.iban === "" ||
-                  typeof submitData.subject === "undefined" ||
-                  submitData.subject === "" ||
-                  typeof submitData.amount === "undefined" ||
-                  submitData.amount === ""
-                ) {
-                  console.log("Not all the fields were given.");
-                  pageStateSetter((prevState: PageStateType) => ({
-                    ...prevState,
-
-                    error: {
-                      title: i18n.str`Field(s) missing.`,
-                    },
-                  }));
-                  return;
-                }
-                transactionData = {
-                  paytoUri: `payto://iban/${
-                    submitData.iban
-                  }?message=${encodeURIComponent(submitData.subject)}`,
-                  amount: `${currency}:${submitData.amount}`,
-                };
-                return await createTransactionCall(
-                  transactionData,
-                  backendState,
-                  pageStateSetter,
-                  () =>
-                    submitDataSetter((p) => ({
-                      amount: undefined,
-                      iban: undefined,
-                      subject: undefined,
-                    })),
-                );
-              }}
-            />
-            <input
-              type="button"
-              class="pure-button"
-              value="Clear"
-              onClick={async () => {
-                submitDataSetter((p) => ({
-                  amount: undefined,
-                  iban: undefined,
-                  subject: undefined,
-                }));
-              }}
-            />
-          </p>
-        </form>
-        <p>
-          <a
-            href="/account"
-            onClick={() => {
-              console.log("switch to raw payto form");
-              pageStateSetter((prevState: any) => ({
-                ...prevState,
-                isRawPayto: true,
-              }));
-            }}
-          >
-            {i18n.str`Want to try the raw payto://-format?`}
-          </a>
-        </p>
-      </div>
-    );
-
-  const errorsPayto = undefinedIfEmpty({
-    rawPaytoInput: !rawPaytoInput
-      ? i18n.str`Missing payto address`
-      : !parsePaytoUri(rawPaytoInput)
-      ? i18n.str`Payto does not follow the pattern`
-      : undefined,
-  });
-
-  return (
-    <div>
-      <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
-      <div class="pure-form" name="payto-form">
-        <p>
-          <label for="address">{i18n.str`payto URI:`}</label>&nbsp;
-          <input
-            name="address"
-            type="text"
-            size={50}
-            ref={ref}
-            id="address"
-            value={rawPaytoInput ?? ""}
-            required
-            placeholder={i18n.str`payto address`}
-            // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 
]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
-            onInput={(e): void => {
-              rawPaytoInputSetter(e.currentTarget.value);
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errorsPayto?.rawPaytoInput}
-            isDirty={rawPaytoInput !== undefined}
-          />
-          <br />
-          <div class="hint">
-            Hint:
-            <code>
-              payto://iban/[receiver-iban]?message=[subject]&amount=[{currency}
-              :X.Y]
-            </code>
-          </div>
-        </p>
-        <p>
-          <input
-            class="pure-button pure-button-primary"
-            type="submit"
-            disabled={!!errorsPayto}
-            value={i18n.str`Send`}
-            onClick={async () => {
-              // empty string evaluates to false.
-              if (!rawPaytoInput) {
-                console.log("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,
-                backendState,
-                pageStateSetter,
-                () => rawPaytoInputSetter(undefined),
-              );
-            }}
-          />
-        </p>
-        <p>
-          <a
-            href="/account"
-            onClick={() => {
-              console.log("switch to wire-transfer-form");
-              pageStateSetter((prevState: any) => ({
-                ...prevState,
-                isRawPayto: false,
-              }));
-            }}
-          >
-            {i18n.str`Use wire-transfer form?`}
-          </a>
-        </p>
-      </div>
-    </div>
-  );
-}
-
-/**
- * Additional authentication required to complete the operation.
- * Not providing a back button, only abort.
- */
-function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
-  const { pageState, pageStateSetter } = usePageContext();
-  const { backendState } = Props;
-  const { i18n } = useTranslationContext();
-  const captchaNumbers = {
-    a: Math.floor(Math.random() * 10),
-    b: Math.floor(Math.random() * 10),
-  };
-  let captchaAnswer = "";
-
-  return (
-    <Fragment>
-      <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
-      <article>
-        <div class="challenge-div">
-          <form class="challenge-form" noValidate>
-            <div class="pure-form" id="captcha" name="capcha-form">
-              <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
-              <p>
-                <label for="answer">
-                  {i18n.str`What is`}&nbsp;
-                  <em>
-                    {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
-                  </em>
-                  ?&nbsp;
-                </label>
-                &nbsp;
-                <input
-                  name="answer"
-                  id="answer"
-                  type="text"
-                  autoFocus
-                  required
-                  onInput={(e): void => {
-                    captchaAnswer = e.currentTarget.value;
-                  }}
-                />
-              </p>
-              <p>
-                <button
-                  class="pure-button pure-button-primary btn-confirm"
-                  onClick={(e) => {
-                    e.preventDefault();
-                    if (
-                      captchaAnswer ==
-                      (captchaNumbers.a + captchaNumbers.b).toString()
-                    ) {
-                      confirmWithdrawalCall(
-                        backendState,
-                        pageState.withdrawalId,
-                        pageStateSetter,
-                      );
-                      return;
-                    }
-                    pageStateSetter((prevState: PageStateType) => ({
-                      ...prevState,
-
-                      error: {
-                        title: i18n.str`Answer is wrong.`,
-                      },
-                    }));
-                  }}
-                >
-                  {i18n.str`Confirm`}
-                </button>
-                &nbsp;
-                <button
-                  class="pure-button pure-button-secondary btn-cancel"
-                  onClick={async () =>
-                    await abortWithdrawalCall(
-                      backendState,
-                      pageState.withdrawalId,
-                      pageStateSetter,
-                    )
-                  }
-                >
-                  {i18n.str`Cancel`}
-                </button>
-              </p>
-            </div>
-          </form>
-          <div class="hint">
-            <p>
-              <i18n.Translate>
-                A this point, a <b>real</b> bank would ask for an additional
-                authentication proof (PIN/TAN, one time password, ..), instead
-                of a simple calculation.
-              </i18n.Translate>
-            </p>
-          </div>
-        </div>
-      </article>
-    </Fragment>
-  );
-}
-
-/**
- * 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.
- */
-function TalerWithdrawalQRCode(Props: any): VNode {
-  // turns true when the wallet POSTed the reserve details:
-  const { pageState, pageStateSetter } = usePageContext();
-  const { withdrawalId, talerWithdrawUri, backendState } = Props;
-  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>
-  );
-
-  console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
-  // waiting for the wallet:
-
-  const { data, error } = useSWR(
-    `integration-api/withdrawal-operation/${withdrawalId}`,
-    { refreshInterval: 1000 },
-  );
-
-  if (typeof error !== "undefined") {
-    console.log(
-      `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>
-    );
-  }
-
-  // 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:
-   */
-  console.log("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.selection_done) {
-    return (
-      <QrCodeSection
-        talerWithdrawUri={talerWithdrawUri}
-        abortButton={abortButton}
-      />
-    );
-  }
-  /**
-   * Wallet POSTed the withdrawal details!  Ask the
-   * user to authorize the operation (here CAPTCHA).
-   */
-  return <TalerWithdrawalConfirmationQuestion backendState={backendState} />;
-}
-
-function WalletWithdraw({
-  focus,
-  currency,
-}: {
-  currency?: string;
-  focus?: boolean;
-}): VNode {
-  const [backendState, backendStateSetter] = useBackendState();
-  const { pageState, pageStateSetter } = usePageContext();
-  const { i18n } = useTranslationContext();
-  let submitAmount = "5.00";
-
-  const ref = useRef<HTMLInputElement>(null);
-  useEffect(() => {
-    if (focus) ref.current?.focus();
-  }, [focus]);
-  return (
-    <form id="reserve-form" class="pure-form" name="tform">
-      <p>
-        <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
-        &nbsp;
-        <input
-          type="text"
-          readonly
-          class="currency-indicator"
-          size={currency?.length ?? 5}
-          maxLength={currency?.length}
-          tabIndex={-1}
-          value={currency}
-        />
-        &nbsp;
-        <input
-          type="number"
-          ref={ref}
-          id="withdraw-amount"
-          name="withdraw-amount"
-          value={submitAmount}
-          onChange={(e): void => {
-            // FIXME: validate using 'parseAmount()',
-            // deactivate submit button as long as
-            // amount is not valid
-            submitAmount = e.currentTarget.value;
-          }}
-        />
-      </p>
-      <p>
-        <div>
-          <input
-            id="select-exchange"
-            class="pure-button pure-button-primary"
-            type="submit"
-            value={i18n.str`Withdraw`}
-            onClick={() => {
-              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}`,
-                backendState,
-                pageStateSetter,
-              );
-            }}
-          />
-        </div>
-      </p>
-    </form>
-  );
-}
-
-/**
- * Let the user choose a payment option,
- * then specify the details trigger the action.
- */
-function PaymentOptions({ currency }: { currency?: string }): VNode {
-  const { i18n } = useTranslationContext();
-
-  const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
-    "charge-wallet",
-  );
-
-  return (
-    <article>
-      <div class="payments">
-        <div class="tab">
-          <button
-            class={tab === "charge-wallet" ? "tablinks active" : "tablinks"}
-            onClick={(): void => {
-              setTab("charge-wallet");
-            }}
-          >
-            {i18n.str`Obtain digital cash`}
-          </button>
-          <button
-            class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
-            onClick={(): void => {
-              setTab("wire-transfer");
-            }}
-          >
-            {i18n.str`Transfer to bank account`}
-          </button>
-        </div>
-        {tab === "charge-wallet" && (
-          <div id="charge-wallet" class="tabcontent active">
-            <h3>{i18n.str`Obtain digital cash`}</h3>
-            <WalletWithdraw focus currency={currency} />
-          </div>
-        )}
-        {tab === "wire-transfer" && (
-          <div id="wire-transfer" class="tabcontent active">
-            <h3>{i18n.str`Transfer to bank account`}</h3>
-            <PaytoWireTransfer focus currency={currency} />
-          </div>
-        )}
-      </div>
-    </article>
-  );
-}
-
-/**
- * Collect and submit login data.
- */
-function LoginForm(): VNode {
-  const [backendState, backendStateSetter] = useBackendState();
-  const { pageState, pageStateSetter } = usePageContext();
-  const [username, setUsername] = useState<string | undefined>();
-  const [password, setPassword] = useState<string | undefined>();
-  const { i18n } = useTranslationContext();
-  const ref = useRef<HTMLInputElement>(null);
-  useEffect(() => {
-    ref.current?.focus();
-  }, []);
-
-  const errors = undefinedIfEmpty({
-    username: !username ? i18n.str`Missing username` : undefined,
-    password: !password ? i18n.str`Missing password` : undefined,
-  });
-
-  return (
-    <div class="login-div">
-      <form action="javascript:void(0);" class="login-form" noValidate>
-        <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={() => {
-              if (!username || !password) return;
-              loginCall(
-                { username, password },
-                backendStateSetter,
-                pageStateSetter,
-              );
-              setUsername(undefined);
-              setPassword(undefined);
-            }}
-          >
-            {i18n.str`Login`}
-          </button>
-
-          {bankUiSettings.allowRegistrations ? (
-            <button
-              class="pure-button pure-button-secondary btn-cancel"
-              onClick={() => {
-                route("/register");
-              }}
-            >
-              {i18n.str`Register`}
-            </button>
-          ) : (
-            <div />
-          )}
-        </div>
-      </form>
-    </div>
-  );
-}
-
-/**
- * Show only the account's balance.  NOTE: the backend state
- * is mostly needed to provide the user's credentials to POST
- * to the bank.
- */
-function Account(Props: any): VNode {
-  const { cache } = useSWRConfig();
-  const { accountLabel, backendState } = Props;
-  // 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 { pageState, pageStateSetter: setPageState } = usePageContext();
-  const {
-    withdrawalInProgress,
-    withdrawalId,
-    isLoggedIn,
-    talerWithdrawUri,
-    timestamp,
-  } = pageState;
-  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") {
-    console.log("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: {
-        setPageState((prevState: PageStateType) => ({
-          ...prevState,
-
-          isLoggedIn: false,
-          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: {
-        setPageState((prevState: PageStateType) => ({
-          ...prevState,
-
-          isLoggedIn: false,
-          error: {
-            title: i18n.str`Wrong credentials given.`,
-          },
-        }));
-        return <p>Wrong credentials...</p>;
-      }
-      default: {
-        setPageState((prevState: PageStateType) => ({
-          ...prevState,
-
-          isLoggedIn: false,
-          error: {
-            title: i18n.str`Account information could not be retrieved.`,
-            debug: JSON.stringify(error),
-          },
-        }));
-        return <p>Unknown problem...</p>;
-      }
-    }
-  }
-  const balance = !data ? undefined : 
Amounts.parseOrThrow(data.balance.amount);
-  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.
-   */
-  console.log(`maybe new withdrawal ${talerWithdrawUri}`);
-  if (talerWithdrawUri) {
-    console.log("Bank created a new Taler withdrawal");
-    return (
-      <BankFrame>
-        <TalerWithdrawalQRCode
-          accountLabel={accountLabel}
-          backendState={backendState}
-          withdrawalId={withdrawalId}
-          talerWithdrawUri={talerWithdrawUri}
-        />
-      </BankFrame>
-    );
-  }
-  const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
-
-  return (
-    <BankFrame>
-      <div>
-        <h1 class="nav welcome-text">
-          <i18n.Translate>
-            Welcome,
-            {accountNumber
-              ? `${accountLabel} (${accountNumber})`
-              : accountLabel}
-            !
-          </i18n.Translate>
-        </h1>
-      </div>
-      <section id="assets">
-        <div class="asset-summary">
-          <h2>{i18n.str`Bank account balance`}</h2>
-          {!balance ? (
-            <div class="large-amount" style={{ color: "gray" }}>
-              Waiting server response...
-            </div>
-          ) : (
-            <div class="large-amount amount">
-              {balanceIsDebit ? <b>-</b> : null}
-              <span class="value">{`${balanceValue}`}</span>&nbsp;
-              <span class="currency">{`${balance.currency}`}</span>
-            </div>
-          )}
-        </div>
-      </section>
-      <section id="payments">
-        <div class="payments">
-          <h2>{i18n.str`Payments`}</h2>
-          <PaymentOptions currency={balance?.currency} />
-        </div>
-      </section>
-      <section id="main">
-        <article>
-          <h2>{i18n.str`Latest transactions:`}</h2>
-          <Transactions
-            balanceValue={balanceValue}
-            pageNumber="0"
-            accountLabel={accountLabel}
-          />
-        </article>
-      </section>
-    </BankFrame>
-  );
-}
-
-/**
- * Factor out login credentials.
- */
-function SWRWithCredentials(props: any): VNode {
-  const { username, password, backendUrl } = props;
-  const headers = new Headers();
-  headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`);
-  console.log("Likely backend base URL", backendUrl);
-  return (
-    <SWRConfig
-      value={{
-        fetcher: (url: string) => {
-          return fetch(backendUrl + url || "", { headers }).then((r) => {
-            if (!r.ok) throw { status: r.status, json: r.json() };
-
-            return r.json();
-          });
-        },
-      }}
-    >
-      {props.children}
-    </SWRConfig>
-  );
-}
-
-export function AccountPage(): VNode {
-  const [backendState, backendStateSetter] = useBackendState();
-  const { i18n } = useTranslationContext();
-  const { pageState, pageStateSetter } = usePageContext();
-
-  if (!pageState.isLoggedIn) {
-    return (
-      <BankFrame>
-        <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
-        <LoginForm />
-      </BankFrame>
-    );
-  }
-
-  if (typeof backendState === "undefined") {
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      isLoggedIn: false,
-      error: {
-        title: i18n.str`Page has a problem: logged in but backend state is 
lost.`,
-      },
-    }));
-    return <p>Error: waiting for details...</p>;
-  }
-  console.log("Showing the profile page..");
-  return (
-    <SWRWithCredentials
-      username={backendState.username}
-      password={backendState.password}
-      backendUrl={backendState.url}
-    >
-      <Account
-        accountLabel={backendState.username}
-        backendState={backendState}
-      />
-    </SWRWithCredentials>
-  );
-}
diff --git a/packages/demobank-ui/src/utils.ts 
b/packages/demobank-ui/src/utils.ts
index b8e0a2acb..23cade0e8 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -52,3 +52,18 @@ export function undefinedIfEmpty<T extends object>(obj: T): 
T | undefined {
     ? obj
     : undefined;
 }
+
+/**
+ * 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;
+}

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