gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (43ae414a5 -> 958747bd0)


From: gnunet
Subject: [taler-wallet-core] branch master updated (43ae414a5 -> 958747bd0)
Date: Fri, 07 Apr 2023 22:31:09 +0200

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

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

    from 43ae414a5 -re-add missing fields, fix types
     new a3aa7d95d anon withdrawal confirmation, and fix error with infinity 
loop
     new 958747bd0 validate iban

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


Summary of changes:
 packages/demobank-ui/src/components/app.tsx        |  24 ++-
 packages/demobank-ui/src/context/pageState.ts      |  32 ++--
 packages/demobank-ui/src/hooks/access.ts           |  64 +++----
 packages/demobank-ui/src/hooks/backend.ts          |  26 ++-
 packages/demobank-ui/src/pages/AccountPage.tsx     |  20 ++-
 packages/demobank-ui/src/pages/AdminPage.tsx       | 179 +++++++++----------
 packages/demobank-ui/src/pages/BankFrame.tsx       |  80 ++++++---
 packages/demobank-ui/src/pages/BusinessAccount.tsx |  44 ++---
 packages/demobank-ui/src/pages/HomePage.tsx        | 197 ++++++++++-----------
 packages/demobank-ui/src/pages/LoginForm.tsx       |  82 ++++++---
 packages/demobank-ui/src/pages/PaymentOptions.tsx  |  30 ++--
 .../src/pages/PaytoWireTransferForm.tsx            |  20 +--
 .../demobank-ui/src/pages/PublicHistoriesPage.tsx  |  16 +-
 packages/demobank-ui/src/pages/QrCodeSection.tsx   |   8 +-
 .../demobank-ui/src/pages/RegistrationPage.tsx     |  22 +--
 packages/demobank-ui/src/pages/Routing.tsx         | 177 ++++++------------
 .../demobank-ui/src/pages/WalletWithdrawForm.tsx   |  29 +--
 .../src/pages/WithdrawalConfirmationQuestion.tsx   |  36 ++--
 .../demobank-ui/src/pages/WithdrawalQRCode.tsx     |  75 ++++----
 .../src/wallet/ManageAccount/state.ts              |  19 +-
 .../src/wallet/ManageAccount/stories.tsx           |  15 +-
 .../src/wallet/ManageAccount/views.tsx             |  99 ++++++-----
 .../src/wallet/Transaction.stories.tsx             |   4 -
 23 files changed, 665 insertions(+), 633 deletions(-)

diff --git a/packages/demobank-ui/src/components/app.tsx 
b/packages/demobank-ui/src/components/app.tsx
index e024be41b..ef535bb9f 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -48,19 +48,17 @@ const WITH_LOCAL_STORAGE_CACHE = false;
 const App: FunctionalComponent = () => {
   return (
     <TranslationProvider source={strings}>
-      <PageStateProvider>
-        <BackendStateProvider>
-          <SWRConfig
-            value={{
-              provider: WITH_LOCAL_STORAGE_CACHE
-                ? localStorageProvider
-                : undefined,
-            }}
-          >
-            <Routing />
-          </SWRConfig>
-        </BackendStateProvider>
-      </PageStateProvider>
+      <BackendStateProvider>
+        <SWRConfig
+          value={{
+            provider: WITH_LOCAL_STORAGE_CACHE
+              ? localStorageProvider
+              : undefined,
+          }}
+        >
+          <Routing />
+        </SWRConfig>
+      </BackendStateProvider>
     </TranslationProvider>
   );
 };
diff --git a/packages/demobank-ui/src/context/pageState.ts 
b/packages/demobank-ui/src/context/pageState.ts
index 247297c7b..074fbcafc 100644
--- a/packages/demobank-ui/src/context/pageState.ts
+++ b/packages/demobank-ui/src/context/pageState.ts
@@ -29,9 +29,7 @@ export type Type = {
   pageStateSetter: StateUpdater<PageStateType>;
 };
 const initial: Type = {
-  pageState: {
-    withdrawalInProgress: false,
-  },
+  pageState: {},
   pageStateSetter: () => {
     null;
   },
@@ -57,9 +55,7 @@ export const PageStateProvider = ({
  * Wrapper providing defaults.
  */
 function usePageState(
-  state: PageStateType = {
-    withdrawalInProgress: false,
-  },
+  state: PageStateType = {},
 ): [PageStateType, StateUpdater<PageStateType>] {
   const ret = useNotNullLocalStorage("page-state", JSON.stringify(state));
   const retObj: PageStateType = JSON.parse(ret[0]);
@@ -100,14 +96,18 @@ export type ErrorMessage = {
  * Track page state.
  */
 export interface PageStateType {
-  error?: ErrorMessage;
-  info?: TranslatedString;
-
-  withdrawalInProgress: boolean;
-  talerWithdrawUri?: string;
-  /**
-   * Not strictly a presentational value, could
-   * be moved in a future "withdrawal state" object.
-   */
-  withdrawalId?: string;
+  currentWithdrawalOperationId?: string;
+}
+
+export interface ObservedStateType {
+  error: ErrorMessage | undefined;
+  info: TranslatedString | undefined;
+}
+export const errorListeners: Array<(error: ErrorMessage) => void> = [];
+export const infoListeners: Array<(info: TranslatedString) => void> = [];
+export function notifyError(error: ErrorMessage) {
+  errorListeners.forEach((cb) => cb(error));
+}
+export function notifyInfo(info: TranslatedString) {
+  infoListeners.forEach((cb) => cb(info));
 }
diff --git a/packages/demobank-ui/src/hooks/access.ts 
b/packages/demobank-ui/src/hooks/access.ts
index ee8566efe..546d59a84 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -59,30 +59,6 @@ export function useAccessAPI(): AccessAPI {
       );
     return res;
   };
-  const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => 
{
-    const res = await request<void>(
-      `access-api/accounts/${account}/withdrawals/${id}/abort`,
-      {
-        method: "POST",
-        contentType: "json",
-      },
-    );
-    await mutateAll(/.*accounts\/.*\/withdrawals\/.*/);
-    return res;
-  };
-  const confirmWithdrawal = async (
-    id: string,
-  ): Promise<HttpResponseOk<void>> => {
-    const res = await request<void>(
-      `access-api/accounts/${account}/withdrawals/${id}/confirm`,
-      {
-        method: "POST",
-        contentType: "json",
-      },
-    );
-    await mutateAll(/.*accounts\/.*\/withdrawals\/.*/);
-    return res;
-  };
   const createTransaction = async (
     data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
   ): Promise<HttpResponseOk<void>> => {
@@ -107,14 +83,41 @@ export function useAccessAPI(): AccessAPI {
   };
 
   return {
-    abortWithdrawal,
-    confirmWithdrawal,
     createWithdrawal,
     createTransaction,
     deleteAccount,
   };
 }
 
+export function useAccessAnonAPI(): AccessAnonAPI {
+  const mutateAll = useMatchMutate();
+  const { request } = useAuthenticatedBackend();
+
+  const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => 
{
+    const res = await request<void>(`access-api/withdrawals/${id}/abort`, {
+      method: "POST",
+      contentType: "json",
+    });
+    await mutateAll(/.*withdrawals\/.*/);
+    return res;
+  };
+  const confirmWithdrawal = async (
+    id: string,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`access-api/withdrawals/${id}/confirm`, {
+      method: "POST",
+      contentType: "json",
+    });
+    await mutateAll(/.*withdrawals\/.*/);
+    return res;
+  };
+
+  return {
+    abortWithdrawal,
+    confirmWithdrawal,
+  };
+}
+
 export function useTestingAPI(): TestingAPI {
   const mutateAll = useMatchMutate();
   const { request: noAuthRequest } = usePublicBackend();
@@ -145,13 +148,15 @@ export interface AccessAPI {
   ) => Promise<
     HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
   >;
-  abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
-  confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
   createTransaction: (
     data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
   ) => Promise<HttpResponseOk<void>>;
   deleteAccount: () => Promise<HttpResponseOk<void>>;
 }
+export interface AccessAnonAPI {
+  abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
+  confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
+}
 
 export interface InstanceTemplateFilter {
   //FIXME: add filter to the template list
@@ -210,7 +215,6 @@ export function useAccountDetails(
 
 // FIXME: should poll
 export function useWithdrawalDetails(
-  account: string,
   wid: string,
 ): HttpResponse<
   SandboxBackend.Access.BankAccountGetWithdrawalResponse,
@@ -221,7 +225,7 @@ export function useWithdrawalDetails(
   const { data, error } = useSWR<
     HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
     RequestError<SandboxBackend.SandboxError>
-  >([`access-api/accounts/${account}/withdrawals/${wid}`], fetcher, {
+  >([`access-api/withdrawals/${wid}`], fetcher, {
     refreshInterval: 1000,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
diff --git a/packages/demobank-ui/src/hooks/backend.ts 
b/packages/demobank-ui/src/hooks/backend.ts
index 3fe744874..e0b8d83ef 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -17,6 +17,7 @@
 import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
 import {
   ErrorType,
+  HttpError,
   RequestError,
   useLocalStorage,
 } from "@gnu-taler/web-util/lib/index.browser";
@@ -193,6 +194,22 @@ export function usePublicBackend(): useBackendType {
   };
 }
 
+type CheckResult = ValidResult | RequestInvalidResult | InvalidationResult;
+
+interface ValidResult {
+  valid: true;
+}
+interface RequestInvalidResult {
+  valid: false;
+  requestError: true;
+  cause: RequestError<any>["cause"];
+}
+interface InvalidationResult {
+  valid: false;
+  requestError: false;
+  error: unknown;
+}
+
 export function useCredentialsChecker() {
   const { request } = useApiContext();
   const baseUrl = getInitialBackendBaseURL();
@@ -201,10 +218,7 @@ export function useCredentialsChecker() {
   return async function testLogin(
     username: string,
     password: string,
-  ): Promise<{
-    valid: boolean;
-    cause?: ErrorType;
-  }> {
+  ): Promise<CheckResult> {
     try {
       await request(baseUrl, `access-api/accounts/${username}/`, {
         basicAuth: { username, password },
@@ -213,9 +227,9 @@ export function useCredentialsChecker() {
       return { valid: true };
     } catch (error) {
       if (error instanceof RequestError) {
-        return { valid: false, cause: error.cause.type };
+        return { valid: false, requestError: true, cause: error.cause };
       }
-      return { valid: false, cause: ErrorType.UNEXPECTED };
+      return { valid: false, requestError: false, error };
     }
   };
 }
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx 
b/packages/demobank-ui/src/pages/AccountPage.tsx
index c6ec7c88e..bab8cca16 100644
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage.tsx
@@ -14,15 +14,21 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
+import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
 import {
+  ErrorType,
   HttpResponsePaginated,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
+import { Loading } from "../components/Loading.js";
 import { Transactions } from "../components/Transactions/index.js";
+import { PageStateType, notifyError } from "../context/pageState.js";
 import { useAccountDetails } from "../hooks/access.js";
+import { LoginForm } from "./LoginForm.js";
 import { PaymentOptions } from "./PaymentOptions.js";
+import { StateUpdater } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
 
 interface Props {
   account: string;
@@ -35,9 +41,21 @@ interface Props {
  */
 export function AccountPage({ account, onLoadNotOk }: Props): VNode {
   const result = useAccountDetails(account);
+  const backend = useBackendContext();
   const { i18n } = useTranslationContext();
 
   if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
+      return onLoadNotOk(result);
+    }
+    //logout if there is any error, not if loading
+    backend.logOut();
+    if (result.status === HttpStatusCode.NotFound) {
+      notifyError({
+        title: i18n.str`Username or account label "${account}" not found`,
+      });
+      return <LoginForm />;
+    }
     return onLoadNotOk(result);
   }
 
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx 
b/packages/demobank-ui/src/pages/AdminPage.tsx
index 92464a43e..b867d0103 100644
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -14,13 +14,9 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
 import {
-  Amounts,
-  HttpStatusCode,
-  parsePaytoUri,
-  TranslatedString,
-} from "@gnu-taler/taler-util";
-import {
+  ErrorType,
   HttpResponsePaginated,
   RequestError,
   useTranslationContext,
@@ -29,11 +25,7 @@ import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Cashouts } from "../components/Cashouts/index.js";
 import { useBackendContext } from "../context/backend.js";
-import {
-  ErrorMessage,
-  PageStateType,
-  usePageContext,
-} from "../context/pageState.js";
+import { ErrorMessage, notifyInfo } from "../context/pageState.js";
 import { useAccountDetails } from "../hooks/access.js";
 import {
   useAdminAccountAPI,
@@ -50,6 +42,7 @@ import {
 } from "../utils.js";
 import { ErrorBannerFloat } from "./BankFrame.js";
 import { ShowCashoutDetails } from "./BusinessAccount.js";
+import { handleNotOkResult } from "./HomePage.js";
 import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
@@ -69,14 +62,12 @@ function randomPassword(): string {
 }
 
 interface Props {
-  onLoadNotOk: <T>(
-    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-  ) => VNode;
+  onRegister: () => void;
 }
 /**
  * Query account information and show QR code if there is pending withdrawal
  */
-export function AdminPage({ onLoadNotOk }: Props): VNode {
+export function AdminPage({ onRegister }: Props): VNode {
   const [account, setAccount] = useState<string | undefined>();
   const [showDetails, setShowDetails] = useState<string | undefined>();
   const [showCashouts, setShowCashouts] = useState<string | undefined>();
@@ -87,24 +78,13 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
   >();
 
   const [createAccount, setCreateAccount] = useState(false);
-  const { pageStateSetter } = usePageContext();
-
-  function showInfoMessage(info: TranslatedString): void {
-    pageStateSetter((prev) => ({
-      ...prev,
-      info,
-    }));
-  }
-  function saveError(error: PageStateType["error"]): void {
-    pageStateSetter((prev) => ({ ...prev, error }));
-  }
 
   const result = useBusinessAccounts({ account });
   const { i18n } = useTranslationContext();
 
   if (result.loading) return <div />;
   if (!result.ok) {
-    return onLoadNotOk(result);
+    return handleNotOkResult(i18n, onRegister)(result);
   }
 
   const { customers } = result.data;
@@ -113,7 +93,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
     return (
       <ShowCashoutDetails
         id={showCashoutDetails}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onCancel={() => {
           setShowCashoutDetails(undefined);
         }}
@@ -155,13 +135,13 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
     return (
       <ShowAccountDetails
         account={showDetails}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onChangePassword={() => {
           setUpdatePassword(showDetails);
           setShowDetails(undefined);
         }}
         onUpdateSuccess={() => {
-          showInfoMessage(i18n.str`Account updated`);
+          notifyInfo(i18n.str`Account updated`);
           setShowDetails(undefined);
         }}
         onClear={() => {
@@ -174,9 +154,9 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
     return (
       <RemoveAccount
         account={removeAccount}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onUpdateSuccess={() => {
-          showInfoMessage(i18n.str`Account removed`);
+          notifyInfo(i18n.str`Account removed`);
           setRemoveAccount(undefined);
         }}
         onClear={() => {
@@ -189,9 +169,9 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
     return (
       <UpdateAccountPassword
         account={updatePassword}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onUpdateSuccess={() => {
-          showInfoMessage(i18n.str`Password changed`);
+          notifyInfo(i18n.str`Password changed`);
           setUpdatePassword(undefined);
         }}
         onClear={() => {
@@ -205,7 +185,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
       <CreateNewAccount
         onClose={() => setCreateAccount(false)}
         onCreateSuccess={(password) => {
-          showInfoMessage(
+          notifyInfo(
             i18n.str`Account created with password "${password}". The user 
must change the password on the next login.`,
           );
           setCreateAccount(false);
@@ -214,59 +194,6 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
     );
   }
 
-  function AdminAccount(): VNode {
-    const r = useBackendContext();
-    const account = r.state.status === "loggedIn" ? r.state.username : "admin";
-    const result = useAccountDetails(account);
-
-    if (!result.ok) {
-      return onLoadNotOk(result);
-    }
-    const { data } = result;
-    const balance = Amounts.parseOrThrow(data.balance.amount);
-    const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
-    const balanceIsDebit =
-      result.data.balance.credit_debit_indicator == "debit";
-    const limit = balanceIsDebit
-      ? Amounts.sub(debitThreshold, balance).amount
-      : Amounts.add(balance, debitThreshold).amount;
-    if (!balance) return <Fragment />;
-    return (
-      <Fragment>
-        <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">{`${Amounts.stringifyValue(
-                  balance,
-                )}`}</span>
-                &nbsp;
-                <span class="currency">{`${balance.currency}`}</span>
-              </div>
-            )}
-          </div>
-        </section>
-        <PaytoWireTransferForm
-          focus
-          limit={limit}
-          onSuccess={() => {
-            pageStateSetter((prevState: PageStateType) => ({
-              ...prevState,
-              info: i18n.str`Wire transfer created!`,
-            }));
-          }}
-          onError={saveError}
-        />
-      </Fragment>
-    );
-  }
-
   return (
     <Fragment>
       <div>
@@ -293,7 +220,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
         </div>
       </p>
 
-      <AdminAccount />
+      <AdminAccount onRegister={onRegister} />
       <section
         id="main"
         style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
@@ -393,6 +320,53 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
   );
 }
 
+function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
+  const { i18n } = useTranslationContext();
+  const r = useBackendContext();
+  const account = r.state.status === "loggedIn" ? r.state.username : "admin";
+  const result = useAccountDetails(account);
+
+  if (!result.ok) {
+    return handleNotOkResult(i18n, onRegister)(result);
+  }
+  const { data } = result;
+  const balance = Amounts.parseOrThrow(data.balance.amount);
+  const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
+  const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
+  const limit = balanceIsDebit
+    ? Amounts.sub(debitThreshold, balance).amount
+    : Amounts.add(balance, debitThreshold).amount;
+  if (!balance) return <Fragment />;
+  return (
+    <Fragment>
+      <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">{`${Amounts.stringifyValue(balance)}`}</span>
+              &nbsp;
+              <span class="currency">{`${balance.currency}`}</span>
+            </div>
+          )}
+        </div>
+      </section>
+      <PaytoWireTransferForm
+        focus
+        limit={limit}
+        onSuccess={() => {
+          notifyInfo(i18n.str`Wire transfer created!`);
+        }}
+      />
+    </Fragment>
+  );
+}
+
 const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
 const EMAIL_REGEX =
   
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -442,10 +416,13 @@ export function UpdateAccountPassword({
   const [repeat, setRepeat] = useState<string | undefined>();
   const [error, saveError] = useState<ErrorMessage | undefined>();
 
-  if (result.clientError) {
-    if (result.isNotfound) return <div>account not found</div>;
-  }
   if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
+      return onLoadNotOk(result);
+    }
+    if (result.status === HttpStatusCode.NotFound) {
+      return <div>account not found</div>;
+    }
     return onLoadNotOk(result);
   }
 
@@ -679,10 +656,13 @@ export function ShowAccountDetails({
   >();
   const [error, saveError] = useState<ErrorMessage | undefined>();
 
-  if (result.clientError) {
-    if (result.isNotfound) return <div>account not found</div>;
-  }
   if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
+      return onLoadNotOk(result);
+    }
+    if (result.status === HttpStatusCode.NotFound) {
+      return <div>account not found</div>;
+    }
     return onLoadNotOk(result);
   }
 
@@ -804,10 +784,13 @@ function RemoveAccount({
   const { deleteAccount } = useAdminAccountAPI();
   const [error, saveError] = useState<ErrorMessage | undefined>();
 
-  if (result.clientError) {
-    if (result.isNotfound) return <div>account not found</div>;
-  }
   if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
+      return onLoadNotOk(result);
+    }
+    if (result.status === HttpStatusCode.NotFound) {
+      return <div>account not found</div>;
+    }
     return onLoadNotOk(result);
   }
 
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx 
b/packages/demobank-ui/src/pages/BankFrame.tsx
index e75a5c1d0..d1f7250b9 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -14,15 +14,19 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Logger } from "@gnu-taler/taler-util";
+import { Logger, TranslatedString } from "@gnu-taler/taler-util";
 import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
 import talerLogo from "../assets/logo-white.svg";
 import { LangSelectorLikePy as LangSelector } from 
"../components/LangSelector.js";
 import { useBackendContext } from "../context/backend.js";
 import {
   ErrorMessage,
+  PageStateProvider,
   PageStateType,
+  errorListeners,
+  infoListeners,
   usePageContext,
 } from "../context/pageState.js";
 import { useBusinessAccountDetails } from "../hooks/circuit.js";
@@ -56,7 +60,20 @@ function MaybeBusinessButton({
   );
 }
 
-export function BankFrame({
+export function BankFrame(props: {
+  children: ComponentChildren;
+  goToBusinessAccount?: () => void;
+}): VNode {
+  return (
+    <PageStateProvider>
+      <BankFrame2 goToBusinessAccount={props.goToBusinessAccount}>
+        {props.children}
+      </BankFrame2>
+    </PageStateProvider>
+  );
+}
+
+function BankFrame2({
   children,
   goToBusinessAccount,
 }: {
@@ -65,8 +82,8 @@ export function BankFrame({
 }): VNode {
   const { i18n } = useTranslationContext();
   const backend = useBackendContext();
-  const { pageState, pageStateSetter } = usePageContext();
-  logger.trace("state", pageState);
+
+  const { pageStateSetter } = usePageContext();
 
   const demo_sites = [];
   for (const i in bankUiSettings.demoSites)
@@ -140,17 +157,9 @@ export function BankFrame({
                 href="#"
                 class="pure-button logout-button"
                 onClick={() => {
-                  pageStateSetter((prevState: PageStateType) => {
-                    const { talerWithdrawUri, withdrawalId, ...rest } =
-                      prevState;
-                    backend.logOut();
-                    return {
-                      ...rest,
-                      withdrawalInProgress: false,
-                      error: undefined,
-                      info: undefined,
-                      isRawPayto: false,
-                    };
+                  backend.logOut();
+                  pageStateSetter({
+                    currentWithdrawalOperationId: undefined,
                   });
                 }}
               >{i18n.str`Logout`}</a>
@@ -244,8 +253,33 @@ function ErrorBanner({
 }
 
 function StatusBanner(): VNode | null {
-  const { pageState, pageStateSetter } = usePageContext();
-
+  const [info, setInfo] = useState<TranslatedString>();
+  const [error, setError] = useState<ErrorMessage>();
+  console.log("render", info, error);
+  function listenError(e: ErrorMessage) {
+    setError(e);
+  }
+  function listenInfo(m: TranslatedString) {
+    console.log("update info", m, info);
+    setInfo(m);
+  }
+  useEffect(() => {
+    console.log("sadasdsad", infoListeners.length);
+    errorListeners.push(listenError);
+    infoListeners.push(listenInfo);
+    console.log("sadasdsad", infoListeners.length);
+    return function unsuscribe() {
+      const idx = infoListeners.findIndex((d) => d === listenInfo);
+      if (idx !== -1) {
+        infoListeners.splice(idx, 1);
+      }
+      const idx2 = errorListeners.findIndex((d) => d === listenError);
+      if (idx2 !== -1) {
+        errorListeners.splice(idx2, 1);
+      }
+      console.log("unload", idx);
+    };
+  }, []);
   return (
     <div
       style={{
@@ -255,14 +289,14 @@ function StatusBanner(): VNode | null {
         width: "90%",
       }}
     >
-      {!pageState.info ? undefined : (
+      {!info ? undefined : (
         <div
           class="informational informational-ok"
           style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }}
         >
           <div style={{ display: "flex", justifyContent: "space-between" }}>
             <p>
-              <b>{pageState.info}</b>
+              <b>{info}</b>
             </p>
             <div>
               <input
@@ -270,18 +304,18 @@ function StatusBanner(): VNode | null {
                 class="pure-button"
                 value="Clear"
                 onClick={async () => {
-                  pageStateSetter((prev) => ({ ...prev, info: undefined }));
+                  setInfo(undefined);
                 }}
               />
             </div>
           </div>
         </div>
       )}
-      {!pageState.error ? undefined : (
+      {!error ? undefined : (
         <ErrorBanner
-          error={pageState.error}
+          error={error}
           onClear={() => {
-            pageStateSetter((prev) => ({ ...prev, error: undefined }));
+            setError(undefined);
           }}
         />
       )}
diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx 
b/packages/demobank-ui/src/pages/BusinessAccount.tsx
index 262376fa2..02e64ac39 100644
--- a/packages/demobank-ui/src/pages/BusinessAccount.tsx
+++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx
@@ -25,11 +25,17 @@ import {
   RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
-import { Fragment, h, VNode } from "preact";
-import { useEffect, useMemo, useState } from "preact/hooks";
+import { Fragment, VNode, h } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
 import { Cashouts } from "../components/Cashouts/index.js";
 import { useBackendContext } from "../context/backend.js";
-import { ErrorMessage, usePageContext } from "../context/pageState.js";
+import {
+  ErrorMessage,
+  ObservedStateType,
+  PageStateType,
+  notifyInfo,
+  usePageContext,
+} from "../context/pageState.js";
 import { useAccountDetails } from "../hooks/access.js";
 import {
   useCashoutDetails,
@@ -38,21 +44,20 @@ import {
   useRatiosAndFeeConfig,
 } from "../hooks/circuit.js";
 import {
-  buildRequestErrorMessage,
   TanChannel,
+  buildRequestErrorMessage,
   undefinedIfEmpty,
 } from "../utils.js";
 import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
 import { ErrorBannerFloat } from "./BankFrame.js";
 import { LoginForm } from "./LoginForm.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { handleNotOkResult } from "./HomePage.js";
 
 interface Props {
   onClose: () => void;
   onRegister: () => void;
-  onLoadNotOk: <T>(
-    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-  ) => VNode;
+  onLoadNotOk: () => void;
 }
 export function BusinessAccount({
   onClose,
@@ -60,19 +65,12 @@ export function BusinessAccount({
   onRegister,
 }: Props): VNode {
   const { i18n } = useTranslationContext();
-  const { pageStateSetter } = usePageContext();
   const backend = useBackendContext();
   const [updatePassword, setUpdatePassword] = useState(false);
   const [newCashout, setNewcashout] = useState(false);
   const [showCashoutDetails, setShowCashoutDetails] = useState<
     string | undefined
   >();
-  function showInfoMessage(info: TranslatedString): void {
-    pageStateSetter((prev) => ({
-      ...prev,
-      info,
-    }));
-  }
 
   if (backend.state.status === "loggedOut") {
     return <LoginForm onRegister={onRegister} />;
@@ -82,12 +80,12 @@ export function BusinessAccount({
     return (
       <CreateCashout
         account={backend.state.username}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onCancel={() => {
           setNewcashout(false);
         }}
         onComplete={(id) => {
-          showInfoMessage(
+          notifyInfo(
             i18n.str`Cashout created. You need to confirm the operation to 
complete the transaction.`,
           );
           setNewcashout(false);
@@ -100,7 +98,7 @@ export function BusinessAccount({
     return (
       <ShowCashoutDetails
         id={showCashoutDetails}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onCancel={() => {
           setShowCashoutDetails(undefined);
         }}
@@ -111,9 +109,9 @@ export function BusinessAccount({
     return (
       <UpdateAccountPassword
         account={backend.state.username}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onUpdateSuccess={() => {
-          showInfoMessage(i18n.str`Password changed`);
+          notifyInfo(i18n.str`Password changed`);
           setUpdatePassword(false);
         }}
         onClear={() => {
@@ -126,9 +124,9 @@ export function BusinessAccount({
     <div>
       <ShowAccountDetails
         account={backend.state.username}
-        onLoadNotOk={onLoadNotOk}
+        onLoadNotOk={handleNotOkResult(i18n, onRegister)}
         onUpdateSuccess={() => {
-          showInfoMessage(i18n.str`Account updated`);
+          notifyInfo(i18n.str`Account updated`);
         }}
         onChangePassword={() => {
           setUpdatePassword(true);
@@ -168,7 +166,9 @@ interface PropsCashout {
   onComplete: (id: string) => void;
   onCancel: () => void;
   onLoadNotOk: <T>(
-    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+    error:
+      | HttpResponsePaginated<T, SandboxBackend.SandboxError>
+      | HttpResponse<T, SandboxBackend.SandboxError>,
   ) => VNode;
 }
 
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx 
b/packages/demobank-ui/src/pages/HomePage.tsx
index 7ef4284bf..0a5a61396 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -14,16 +14,30 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Logger } from "@gnu-taler/taler-util";
+import {
+  HttpStatusCode,
+  Logger,
+  parseWithdrawUri,
+  stringifyWithdrawUri,
+} from "@gnu-taler/taler-util";
 import {
   ErrorType,
+  HttpResponse,
   HttpResponsePaginated,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
+import { StateUpdater } from "preact/hooks";
 import { Loading } from "../components/Loading.js";
 import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import {
+  ObservedStateType,
+  PageStateType,
+  notifyError,
+  notifyInfo,
+  usePageContext,
+} from "../context/pageState.js";
+import { getInitialBackendBaseURL } from "../hooks/backend.js";
 import { AccountPage } from "./AccountPage.js";
 import { AdminPage } from "./AdminPage.js";
 import { LoginForm } from "./LoginForm.js";
@@ -41,133 +55,109 @@ const logger = new Logger("AccountPage");
  * @param param0
  * @returns
  */
-export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
+export function HomePage({
+  onRegister,
+  onPendingOperationFound,
+}: {
+  onPendingOperationFound: (id: string) => void;
+  onRegister: () => void;
+}): VNode {
   const backend = useBackendContext();
   const { pageState, pageStateSetter } = usePageContext();
   const { i18n } = useTranslationContext();
 
-  function saveError(error: PageStateType["error"]): void {
-    pageStateSetter((prev) => ({ ...prev, error }));
-  }
-
-  function saveErrorAndLogout(error: PageStateType["error"]): void {
-    saveError(error);
-    backend.logOut();
-  }
-
-  function clearCurrentWithdrawal(): void {
-    pageStateSetter((prevState: PageStateType) => {
-      return {
-        ...prevState,
-        withdrawalId: undefined,
-        talerWithdrawUri: undefined,
-        withdrawalInProgress: false,
-      };
-    });
-  }
-
   if (backend.state.status === "loggedOut") {
     return <LoginForm onRegister={onRegister} />;
   }
 
-  const { withdrawalId, talerWithdrawUri } = pageState;
-
-  if (talerWithdrawUri && withdrawalId) {
-    return (
-      <WithdrawalQRCode
-        account={backend.state.username}
-        withdrawalId={withdrawalId}
-        talerWithdrawUri={talerWithdrawUri}
-        onConfirmed={() => {
-          pageStateSetter((prevState) => {
-            const { talerWithdrawUri, ...rest } = prevState;
-            // remove talerWithdrawUri and add info
-            return {
-              ...rest,
-              info: i18n.str`Withdrawal confirmed!`,
-            };
-          });
-        }}
-        onError={(error) => {
-          pageStateSetter((prevState) => {
-            const { talerWithdrawUri, ...rest } = prevState;
-            // remove talerWithdrawUri and add error
-            return {
-              ...rest,
-              error,
-            };
-          });
-        }}
-        onAborted={clearCurrentWithdrawal}
-        onLoadNotOk={handleNotOkResult(
-          backend.state.username,
-          saveError,
-          i18n,
-          onRegister,
-        )}
-      />
-    );
+  if (pageState.currentWithdrawalOperationId) {
+    onPendingOperationFound(pageState.currentWithdrawalOperationId);
+    return <Loading />;
   }
 
   if (backend.state.isUserAdministrator) {
-    return (
-      <AdminPage
-        onLoadNotOk={handleNotOkResult(
-          backend.state.username,
-          saveErrorAndLogout,
-          i18n,
-          onRegister,
-        )}
-      />
-    );
+    return <AdminPage onRegister={onRegister} />;
   }
 
   return (
     <AccountPage
       account={backend.state.username}
-      onLoadNotOk={handleNotOkResult(
-        backend.state.username,
-        saveErrorAndLogout,
-        i18n,
-        onRegister,
-      )}
+      onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+    />
+  );
+}
+
+export function WithdrawalOperationPage({
+  operationId,
+  onLoadNotOk,
+  onAbort,
+}: {
+  operationId: string;
+  onLoadNotOk: () => void;
+  onAbort: () => void;
+}): VNode {
+  const uri = stringifyWithdrawUri({
+    bankIntegrationApiBaseUrl: getInitialBackendBaseURL(),
+    withdrawalOperationId: operationId,
+  });
+  const parsedUri = parseWithdrawUri(uri);
+  const { i18n } = useTranslationContext();
+  const { pageStateSetter } = usePageContext();
+  function clearCurrentWithdrawal(): void {
+    pageStateSetter({});
+    onAbort();
+  }
+
+  if (!parsedUri) {
+    notifyError({
+      title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
+    });
+    return <Loading />;
+  }
+
+  return (
+    <WithdrawalQRCode
+      withdrawUri={parsedUri}
+      onConfirmed={() => {
+        notifyInfo(i18n.str`Withdrawal confirmed!`);
+      }}
+      onAborted={clearCurrentWithdrawal}
+      onLoadNotOk={onLoadNotOk}
     />
   );
 }
 
-function handleNotOkResult(
-  account: string,
-  onErrorHandler: (state: PageStateType["error"]) => void,
+export function handleNotOkResult(
   i18n: ReturnType<typeof useTranslationContext>["i18n"],
-  onRegister: () => void,
-): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode 
{
+  onRegister?: () => void,
+): <T>(
+  result:
+    | HttpResponsePaginated<T, SandboxBackend.SandboxError>
+    | HttpResponse<T, SandboxBackend.SandboxError>,
+) => VNode {
   return function handleNotOkResult2<T>(
-    result: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+    result:
+      | HttpResponsePaginated<T, SandboxBackend.SandboxError>
+      | HttpResponse<T, SandboxBackend.SandboxError>,
   ): VNode {
-    if (result.clientError && result.isUnauthorized) {
-      onErrorHandler({
-        title: i18n.str`Wrong credentials for "${account}"`,
-      });
-      return <LoginForm onRegister={onRegister} />;
-    }
-    if (result.clientError && result.isNotfound) {
-      onErrorHandler({
-        title: i18n.str`Username or account label "${account}" not found`,
-      });
-      return <LoginForm onRegister={onRegister} />;
-    }
     if (result.loading) return <Loading />;
     if (!result.ok) {
       switch (result.type) {
         case ErrorType.TIMEOUT: {
-          onErrorHandler({
+          notifyError({
             title: i18n.str`Request timeout, try again later.`,
           });
           break;
         }
         case ErrorType.CLIENT: {
+          if (result.status === HttpStatusCode.Unauthorized) {
+            notifyError({
+              title: i18n.str`Wrong credentials`,
+            });
+            return <LoginForm onRegister={onRegister} />;
+          }
           const errorData = result.payload;
-          onErrorHandler({
+          notifyError({
             title: i18n.str`Could not load due to a client error`,
             description: errorData.error.description,
             debug: JSON.stringify(result),
@@ -175,19 +165,18 @@ function handleNotOkResult(
           break;
         }
         case ErrorType.SERVER: {
-          const errorData = result.error;
-          onErrorHandler({
+          notifyError({
             title: i18n.str`Server returned with error`,
-            description: errorData.error.description,
-            debug: JSON.stringify(result),
+            description: result.payload.error.description,
+            debug: JSON.stringify(result.payload),
           });
           break;
         }
         case ErrorType.UNEXPECTED: {
-          onErrorHandler({
+          notifyError({
             title: i18n.str`Unexpected error.`,
             description: `Diagnostic from ${result.info?.url} is 
"${result.message}"`,
-            debug: JSON.stringify(result.exception),
+            debug: JSON.stringify(result),
           });
           break;
         }
@@ -196,7 +185,7 @@ function handleNotOkResult(
         }
       }
 
-      return <LoginForm onRegister={onRegister} />;
+      return <div>error</div>;
     }
     return <div />;
   };
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx 
b/packages/demobank-ui/src/pages/LoginForm.tsx
index 16d2373da..7116e724e 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,6 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { HttpStatusCode } from "@gnu-taler/taler-util";
 import {
   ErrorType,
   useTranslationContext,
@@ -32,7 +33,7 @@ import { ShowInputErrorLabel } from 
"./ShowInputErrorLabel.js";
 /**
  * Collect and submit login data.
  */
-export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
+export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
   const backend = useBackendContext();
   const [username, setUsername] = useState<string | undefined>();
   const [password, setPassword] = useState<string | undefined>();
@@ -119,35 +120,60 @@ export function LoginForm({ onRegister }: { onRegister: 
() => void }): VNode {
               onClick={async (e) => {
                 e.preventDefault();
                 if (!username || !password) return;
-                const { valid, cause } = await testLogin(username, password);
-                if (valid) {
+                const testResult = await testLogin(username, password);
+                if (testResult.valid) {
                   backend.logIn({ username, password });
                 } else {
-                  switch (cause) {
-                    case ErrorType.CLIENT: {
-                      saveError({
-                        title: i18n.str`Wrong credentials or username`,
-                      });
-                      break;
-                    }
-                    case ErrorType.SERVER: {
-                      saveError({
-                        title: i18n.str`Server had a problem, try again later 
or report.`,
-                      });
-                      break;
-                    }
-                    case ErrorType.TIMEOUT: {
-                      saveError({
-                        title: i18n.str`Could not reach the server, please 
report.`,
-                      });
-                      break;
-                    }
-                    default: {
-                      saveError({
-                        title: i18n.str`Unexpected error, please report.`,
-                      });
-                      break;
+                  if (testResult.requestError) {
+                    const { cause } = testResult;
+                    switch (cause.type) {
+                      case ErrorType.CLIENT: {
+                        if (cause.status === HttpStatusCode.Unauthorized) {
+                          saveError({
+                            title: i18n.str`Wrong credentials for 
"${username}"`,
+                          });
+                        }
+                        if (cause.status === HttpStatusCode.NotFound) {
+                          saveError({
+                            title: i18n.str`Account not found`,
+                          });
+                        } else {
+                          saveError({
+                            title: i18n.str`Could not load due to a client 
error`,
+                            description: cause.payload.error.description,
+                            debug: JSON.stringify(cause.payload),
+                          });
+                        }
+                        break;
+                      }
+                      case ErrorType.SERVER: {
+                        saveError({
+                          title: i18n.str`Server had a problem, try again 
later or report.`,
+                          description: cause.payload.error.description,
+                          debug: JSON.stringify(cause.payload),
+                        });
+                        break;
+                      }
+                      case ErrorType.TIMEOUT: {
+                        saveError({
+                          title: i18n.str`Request timeout, try again later.`,
+                        });
+                        break;
+                      }
+                      default: {
+                        saveError({
+                          title: i18n.str`Unexpected error, please report.`,
+                          description: `Diagnostic from ${cause.info?.url} is 
"${cause.message}"`,
+                          debug: JSON.stringify(cause),
+                        });
+                        break;
+                      }
                     }
+                  } else {
+                    saveError({
+                      title: i18n.str`Unexpected error, please report.`,
+                      debug: JSON.stringify(testResult.error),
+                    });
                   }
                   backend.logOut();
                 }
@@ -158,7 +184,7 @@ export function LoginForm({ onRegister }: { onRegister: () 
=> void }): VNode {
               {i18n.str`Login`}
             </button>
 
-            {bankUiSettings.allowRegistrations ? (
+            {bankUiSettings.allowRegistrations && onRegister ? (
               <button
                 class="pure-button pure-button-secondary btn-cancel"
                 onClick={(e) => {
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 291f2aa9e..e0ad64e64 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -17,8 +17,13 @@
 import { AmountJson } from "@gnu-taler/taler-util";
 import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { StateUpdater, useState } from "preact/hooks";
+import {
+  notifyError,
+  notifyInfo,
+  PageStateType,
+  usePageContext,
+} from "../context/pageState.js";
 import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
 import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
 
@@ -33,9 +38,6 @@ export function PaymentOptions({ limit }: { limit: AmountJson 
}): VNode {
   const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
     "charge-wallet",
   );
-  function saveError(error: PageStateType["error"]): void {
-    pageStateSetter((prev) => ({ ...prev, error }));
-  }
 
   return (
     <article>
@@ -64,15 +66,11 @@ export function PaymentOptions({ limit }: { limit: 
AmountJson }): VNode {
             <WalletWithdrawForm
               focus
               limit={limit}
-              onSuccess={(data) => {
-                pageStateSetter((prevState: PageStateType) => ({
-                  ...prevState,
-                  withdrawalInProgress: true,
-                  talerWithdrawUri: data.taler_withdraw_uri,
-                  withdrawalId: data.withdrawal_id,
-                }));
+              onSuccess={(currentWithdrawalOperationId) => {
+                pageStateSetter({
+                  currentWithdrawalOperationId,
+                });
               }}
-              onError={saveError}
             />
           </div>
         )}
@@ -83,12 +81,8 @@ export function PaymentOptions({ limit }: { limit: 
AmountJson }): VNode {
               focus
               limit={limit}
               onSuccess={() => {
-                pageStateSetter((prevState: PageStateType) => ({
-                  ...prevState,
-                  info: i18n.str`Wire transfer created!`,
-                }));
+                notifyInfo(i18n.str`Wire transfer created!`);
               }}
-              onError={saveError}
             />
           </div>
         )}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 027f8e25a..5f16fbf6b 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -29,7 +29,11 @@ import {
 } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
-import { PageStateType } from "../context/pageState.js";
+import {
+  notifyError,
+  ObservedStateType,
+  PageStateType,
+} from "../context/pageState.js";
 import { useAccessAPI } from "../hooks/access.js";
 import {
   buildRequestErrorMessage,
@@ -42,20 +46,14 @@ const logger = new Logger("PaytoWireTransferForm");
 
 export function PaytoWireTransferForm({
   focus,
-  onError,
   onSuccess,
   limit,
 }: {
   focus?: boolean;
-  onError: (e: PageStateType["error"]) => void;
   onSuccess: () => void;
   limit: AmountJson;
 }): VNode {
-  // const backend = useBackendContext();
-  // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used 
for go-back button?
-
   const [isRawPayto, setIsRawPayto] = useState(false);
-  // const [submitData, submitDataSetter] = useWireTransferRequestType();
   const [iban, setIban] = useState<string | undefined>(undefined);
   const [subject, setSubject] = useState<string | undefined>(undefined);
   const [amount, setAmount] = useState<string | undefined>(undefined);
@@ -201,7 +199,7 @@ export function PaytoWireTransferForm({
                   setSubject(undefined);
                 } catch (error) {
                   if (error instanceof RequestError) {
-                    onError(
+                    notifyError(
                       buildRequestErrorMessage(i18n, error.cause, {
                         onClientError: (status) =>
                           status === HttpStatusCode.BadRequest
@@ -210,7 +208,7 @@ export function PaytoWireTransferForm({
                       }),
                     );
                   } else {
-                    onError({
+                    notifyError({
                       title: i18n.str`Operation failed, please report`,
                       description:
                         error instanceof Error
@@ -330,7 +328,7 @@ export function PaytoWireTransferForm({
                 rawPaytoInputSetter(undefined);
               } catch (error) {
                 if (error instanceof RequestError) {
-                  onError(
+                  notifyError(
                     buildRequestErrorMessage(i18n, error.cause, {
                       onClientError: (status) =>
                         status === HttpStatusCode.BadRequest
@@ -339,7 +337,7 @@ export function PaytoWireTransferForm({
                     }),
                   );
                 } else {
-                  onError({
+                  notifyError({
                     title: i18n.str`Operation failed, please report`,
                     description:
                       error instanceof Error
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx 
b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 2b5f7e26c..290fd0a79 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -24,6 +24,13 @@ import { Fragment, h, VNode } from "preact";
 import { StateUpdater } from "preact/hooks";
 import { Transactions } from "../components/Transactions/index.js";
 import { usePublicAccounts } from "../hooks/access.js";
+import {
+  PageStateType,
+  notifyError,
+  usePageContext,
+} from "../context/pageState.js";
+import { handleNotOkResult } from "./HomePage.js";
+import { Loading } from "../components/Loading.js";
 
 const logger = new Logger("PublicHistoriesPage");
 
@@ -36,9 +43,7 @@ const logger = new Logger("PublicHistoriesPage");
 // }
 
 interface Props {
-  onLoadNotOk: <T>(
-    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-  ) => VNode;
+  onLoadNotOk: () => void;
 }
 
 /**
@@ -49,7 +54,10 @@ export function PublicHistoriesPage({ onLoadNotOk }: Props): 
VNode {
   const { i18n } = useTranslationContext();
 
   const result = usePublicAccounts();
-  if (!result.ok) return onLoadNotOk(result);
+  if (!result.ok) {
+    onLoadNotOk();
+    return handleNotOkResult(i18n)(result);
+  }
 
   const { data } = result;
 
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx 
b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index 8f85fff91..8613bfca7 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -14,16 +14,17 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { stringifyWithdrawUri, WithdrawUriResult } from 
"@gnu-taler/taler-util";
 import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useEffect } from "preact/hooks";
 import { QR } from "../components/QR.js";
 
 export function QrCodeSection({
-  talerWithdrawUri,
+  withdrawUri,
   onAborted,
 }: {
-  talerWithdrawUri: string;
+  withdrawUri: WithdrawUriResult;
   onAborted: () => void;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -33,8 +34,9 @@ export function QrCodeSection({
     //this hack manually triggers the tab update after the QR is in the DOM.
     // WebExtension will be using
     // 
https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
-    document.title = `${document.title} ${talerWithdrawUri}`;
+    document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`;
   }, []);
+  const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
 
   return (
     <section id="main" class="content">
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx 
b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index 8554b1def..5b9584dde 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -21,7 +21,11 @@ import {
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { useBackendContext } from "../context/backend.js";
-import { PageStateType } from "../context/pageState.js";
+import {
+  PageStateType,
+  notifyError,
+  usePageContext,
+} from "../context/pageState.js";
 import { useTestingAPI } from "../hooks/access.js";
 import { bankUiSettings } from "../settings.js";
 import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
@@ -30,11 +34,9 @@ import { ShowInputErrorLabel } from 
"./ShowInputErrorLabel.js";
 const logger = new Logger("RegistrationPage");
 
 export function RegistrationPage({
-  onError,
   onComplete,
 }: {
   onComplete: () => void;
-  onError: (e: PageStateType["error"]) => void;
 }): VNode {
   const { i18n } = useTranslationContext();
   if (!bankUiSettings.allowRegistrations) {
@@ -42,7 +44,7 @@ export function RegistrationPage({
       <p>{i18n.str`Currently, the bank is not accepting new 
registrations!`}</p>
     );
   }
-  return <RegistrationForm onComplete={onComplete} onError={onError} />;
+  return <RegistrationForm onComplete={onComplete} />;
 }
 
 export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
@@ -50,13 +52,7 @@ export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
 /**
  * Collect and submit registration data.
  */
-function RegistrationForm({
-  onComplete,
-  onError,
-}: {
-  onComplete: () => void;
-  onError: (e: PageStateType["error"]) => void;
-}): VNode {
+function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
   const backend = useBackendContext();
   const [username, setUsername] = useState<string | undefined>();
   const [password, setPassword] = useState<string | undefined>();
@@ -171,7 +167,7 @@ function RegistrationForm({
                     onComplete();
                   } catch (error) {
                     if (error instanceof RequestError) {
-                      onError(
+                      notifyError(
                         buildRequestErrorMessage(i18n, error.cause, {
                           onClientError: (status) =>
                             status === HttpStatusCode.Conflict
@@ -180,7 +176,7 @@ function RegistrationForm({
                         }),
                       );
                     } else {
-                      onError({
+                      notifyError({
                         title: i18n.str`Operation failed, please report`,
                         description:
                           error instanceof Error
diff --git a/packages/demobank-ui/src/pages/Routing.tsx 
b/packages/demobank-ui/src/pages/Routing.tsx
index 8234d8988..27aae69e9 100644
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ b/packages/demobank-ui/src/pages/Routing.tsx
@@ -14,140 +14,77 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import {
-  ErrorType,
-  HttpResponsePaginated,
-  useTranslationContext,
-} from "@gnu-taler/web-util/lib/index.browser";
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { createHashHistory } from "history";
-import { h, VNode } from "preact";
-import { Router, route, Route } from "preact-router";
-import { useEffect } from "preact/hooks";
-import { Loading } from "../components/Loading.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { HomePage } from "./HomePage.js";
+import { VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useEffect, useMemo, useState } from "preact/hooks";
 import { BankFrame } from "./BankFrame.js";
+import { BusinessAccount } from "./BusinessAccount.js";
+import { HomePage, WithdrawalOperationPage } from "./HomePage.js";
 import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
 import { RegistrationPage } from "./RegistrationPage.js";
-import { BusinessAccount } from "./BusinessAccount.js";
-
-function handleNotOkResult(
-  safe: string,
-  saveError: (state: PageStateType["error"]) => void,
-  i18n: ReturnType<typeof useTranslationContext>["i18n"],
-): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode 
{
-  return function handleNotOkResult2<T>(
-    result: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-  ): VNode {
-    if (result.clientError && result.isUnauthorized) {
-      route(safe);
-      return <Loading />;
-    }
-    if (result.clientError && result.isNotfound) {
-      route(safe);
-      return (
-        <div>Page not found, you are going to be redirected to {safe}</div>
-      );
-    }
-    if (result.loading) return <Loading />;
-    if (!result.ok) {
-      switch (result.type) {
-        case ErrorType.TIMEOUT: {
-          saveError({
-            title: i18n.str`Request timeout, try again later.`,
-          });
-          break;
-        }
-        case ErrorType.CLIENT: {
-          const errorData = result.error;
-          saveError({
-            title: i18n.str`Could not load due to a client error`,
-            description: errorData.error.description,
-            debug: JSON.stringify(result),
-          });
-          break;
-        }
-        case ErrorType.SERVER: {
-          const errorData = result.error;
-          saveError({
-            title: i18n.str`Server returned with error`,
-            description: errorData.error.description,
-            debug: JSON.stringify(result),
-          });
-          break;
-        }
-        case ErrorType.UNEXPECTED: {
-          saveError({
-            title: i18n.str`Unexpected error.`,
-            description: `Diagnostic from ${result.info?.url} is 
"${result.message}"`,
-            debug: JSON.stringify(result.error),
-          });
-          break;
-        }
-        default:
-          {
-            assertUnreachable(result);
-          }
-          route(safe);
-      }
-    }
-    return <div />;
-  };
-}
 
 export function Routing(): VNode {
   const history = createHashHistory();
-  const { pageStateSetter } = usePageContext();
 
-  function saveError(error: PageStateType["error"]): void {
-    pageStateSetter((prev) => ({ ...prev, error }));
-  }
-  const { i18n } = useTranslationContext();
   return (
-    <Router history={history}>
-      <Route
-        path="/public-accounts"
-        component={() => (
-          <BankFrame>
+    <BankFrame
+      goToBusinessAccount={() => {
+        route("/business");
+      }}
+    >
+      <Router history={history}>
+        <Route
+          path="/operation/:wopid"
+          component={({ wopid }: { wopid: string }) => (
+            <WithdrawalOperationPage
+              operationId={wopid}
+              onAbort={() => {
+                route("/account");
+              }}
+              onLoadNotOk={() => {
+                route("/account");
+              }}
+            />
+          )}
+        />
+        <Route
+          path="/public-accounts"
+          component={() => (
             <PublicHistoriesPage
-              onLoadNotOk={handleNotOkResult("/account", saveError, i18n)}
+              onLoadNotOk={() => {
+                route("/account");
+              }}
             />
-          </BankFrame>
-        )}
-      />
-      <Route
-        path="/register"
-        component={() => (
-          <BankFrame>
+          )}
+        />
+        <Route
+          path="/register"
+          component={() => (
             <RegistrationPage
-              onError={saveError}
               onComplete={() => {
                 route("/account");
               }}
             />
-          </BankFrame>
-        )}
-      />
-      <Route
-        path="/account"
-        component={() => (
-          <BankFrame
-            goToBusinessAccount={() => {
-              route("/business");
-            }}
-          >
+          )}
+        />
+        <Route
+          path="/account"
+          component={() => (
             <HomePage
+              onPendingOperationFound={(wopid) => {
+                route(`/operation/${wopid}`);
+              }}
               onRegister={() => {
                 route("/register");
               }}
             />
-          </BankFrame>
-        )}
-      />
-      <Route
-        path="/business"
-        component={() => (
-          <BankFrame>
+          )}
+        />
+        <Route
+          path="/business"
+          component={() => (
             <BusinessAccount
               onClose={() => {
                 route("/account");
@@ -155,13 +92,15 @@ export function Routing(): VNode {
               onRegister={() => {
                 route("/register");
               }}
-              onLoadNotOk={handleNotOkResult("/account", saveError, i18n)}
+              onLoadNotOk={() => {
+                route("/account");
+              }}
             />
-          </BankFrame>
-        )}
-      />
-      <Route default component={Redirect} to="/account" />
-    </Router>
+          )}
+        />
+        <Route default component={Redirect} to="/account" />
+      </Router>
+    </BankFrame>
   );
 }
 
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 8bbfe0713..7f3e207ac 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -19,6 +19,7 @@ import {
   Amounts,
   HttpStatusCode,
   Logger,
+  parseWithdrawUri,
 } from "@gnu-taler/taler-util";
 import {
   RequestError,
@@ -26,7 +27,11 @@ import {
 } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
-import { PageStateType } from "../context/pageState.js";
+import {
+  ObservedStateType,
+  PageStateType,
+  notifyError,
+} from "../context/pageState.js";
 import { useAccessAPI } from "../hooks/access.js";
 import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@@ -36,18 +41,12 @@ const logger = new Logger("WalletWithdrawForm");
 export function WalletWithdrawForm({
   focus,
   limit,
-  onError,
   onSuccess,
 }: {
   limit: AmountJson;
   focus?: boolean;
-  onError: (e: PageStateType["error"]) => void;
-  onSuccess: (
-    data: SandboxBackend.Access.BankAccountCreateWithdrawalResponse,
-  ) => void;
+  onSuccess: (operationId: string) => void;
 }): VNode {
-  // const backend = useBackendContext();
-  // const { pageState, pageStateSetter } = usePageContext();
   const { i18n } = useTranslationContext();
   const { createWithdrawal } = useAccessAPI();
 
@@ -129,10 +128,18 @@ export function WalletWithdrawForm({
                 const result = await createWithdrawal({
                   amount: Amounts.stringify(parsedAmount),
                 });
-                onSuccess(result.data);
+                const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+                if (!uri) {
+                  return notifyError({
+                    title: i18n.str`Server responded with an invalid  withdraw 
URI`,
+                    description: i18n.str`Withdraw URI: 
${result.data.taler_withdraw_uri}`,
+                  });
+                } else {
+                  onSuccess(uri.withdrawalOperationId);
+                }
               } catch (error) {
                 if (error instanceof RequestError) {
-                  onError(
+                  notifyError(
                     buildRequestErrorMessage(i18n, error.cause, {
                       onClientError: (status) =>
                         status === HttpStatusCode.Forbidden
@@ -141,7 +148,7 @@ export function WalletWithdrawForm({
                     }),
                   );
                 } else {
-                  onError({
+                  notifyError({
                     title: i18n.str`Operation failed, please report`,
                     description:
                       error instanceof Error
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index d7ed215be..10a37cd88 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -14,35 +14,41 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
+import {
+  HttpStatusCode,
+  Logger,
+  WithdrawUriResult,
+} from "@gnu-taler/taler-util";
 import {
   RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useMemo, useState } from "preact/hooks";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { useAccessAPI } from "../hooks/access.js";
+import {
+  ObservedStateType,
+  PageStateType,
+  notifyError,
+} from "../context/pageState.js";
+import { useAccessAnonAPI } from "../hooks/access.js";
 import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("WithdrawalConfirmationQuestion");
 
 interface Props {
-  withdrawalId: string;
-  onError: (e: PageStateType["error"]) => void;
   onConfirmed: () => void;
   onAborted: () => void;
+  withdrawUri: WithdrawUriResult;
 }
 /**
  * Additional authentication required to complete the operation.
  * Not providing a back button, only abort.
  */
 export function WithdrawalConfirmationQuestion({
-  onError,
   onConfirmed,
   onAborted,
-  withdrawalId,
+  withdrawUri,
 }: Props): VNode {
   const { i18n } = useTranslationContext();
 
@@ -53,7 +59,7 @@ export function WithdrawalConfirmationQuestion({
     };
   }, []);
 
-  const { confirmWithdrawal, abortWithdrawal } = useAccessAPI();
+  const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI();
   const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
   const answer = parseInt(captchaAnswer ?? "", 10);
   const errors = undefinedIfEmpty({
@@ -114,11 +120,13 @@ export function WithdrawalConfirmationQuestion({
                   onClick={async (e) => {
                     e.preventDefault();
                     try {
-                      await confirmWithdrawal(withdrawalId);
+                      await confirmWithdrawal(
+                        withdrawUri.withdrawalOperationId,
+                      );
                       onConfirmed();
                     } catch (error) {
                       if (error instanceof RequestError) {
-                        onError(
+                        notifyError(
                           buildRequestErrorMessage(i18n, error.cause, {
                             onClientError: (status) =>
                               status === HttpStatusCode.Conflict
@@ -129,7 +137,7 @@ export function WithdrawalConfirmationQuestion({
                           }),
                         );
                       } else {
-                        onError({
+                        notifyError({
                           title: i18n.str`Operation failed, please report`,
                           description:
                             error instanceof Error
@@ -148,11 +156,11 @@ export function WithdrawalConfirmationQuestion({
                   onClick={async (e) => {
                     e.preventDefault();
                     try {
-                      await abortWithdrawal(withdrawalId);
+                      await abortWithdrawal(withdrawUri.withdrawalOperationId);
                       onAborted();
                     } catch (error) {
                       if (error instanceof RequestError) {
-                        onError(
+                        notifyError(
                           buildRequestErrorMessage(i18n, error.cause, {
                             onClientError: (status) =>
                               status === HttpStatusCode.Conflict
@@ -161,7 +169,7 @@ export function WithdrawalConfirmationQuestion({
                           }),
                         );
                       } else {
-                        onError({
+                        notifyError({
                           title: i18n.str`Operation failed, please report`,
                           description:
                             error instanceof Error
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx 
b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 1a4157d06..9c5f83eca 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -14,30 +14,35 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Logger, parseWithdrawUri } from "@gnu-taler/taler-util";
 import {
+  HttpStatusCode,
+  Logger,
+  WithdrawUriResult,
+} from "@gnu-taler/taler-util";
+import {
+  ErrorType,
   HttpResponsePaginated,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { Loading } from "../components/Loading.js";
-import { PageStateType } from "../context/pageState.js";
+import {
+  ObservedStateType,
+  notifyError,
+  notifyInfo,
+} from "../context/pageState.js";
 import { useWithdrawalDetails } from "../hooks/access.js";
 import { QrCodeSection } from "./QrCodeSection.js";
 import { WithdrawalConfirmationQuestion } from 
"./WithdrawalConfirmationQuestion.js";
+import { handleNotOkResult } from "./HomePage.js";
 
 const logger = new Logger("WithdrawalQRCode");
 
 interface Props {
-  account: string;
-  withdrawalId: string;
-  talerWithdrawUri: string;
-  onError: (e: PageStateType["error"]) => void;
+  withdrawUri: WithdrawUriResult;
   onAborted: () => void;
   onConfirmed: () => void;
-  onLoadNotOk: <T>(
-    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-  ) => VNode;
+  onLoadNotOk: () => void;
 }
 /**
  * Offer the QR code (and a clickable taler://-link) to
@@ -45,43 +50,46 @@ interface Props {
  * the bank.  Poll the backend until such operation is done.
  */
 export function WithdrawalQRCode({
-  account,
-  withdrawalId,
-  talerWithdrawUri,
+  withdrawUri,
   onConfirmed,
   onAborted,
-  onError,
   onLoadNotOk,
 }: Props): VNode {
   const { i18n } = useTranslationContext();
-
-  const result = useWithdrawalDetails(account, withdrawalId);
+  const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
   if (!result.ok) {
-    return onLoadNotOk(result);
+    if (result.loading) {
+      return <Loading />;
+    }
+    if (
+      result.type === ErrorType.CLIENT &&
+      result.status === HttpStatusCode.NotFound
+    ) {
+      return <div>operation not found</div>;
+    }
+    console.log("result", result);
+    onLoadNotOk();
+    return handleNotOkResult(i18n)(result);
   }
   const { data } = result;
 
   logger.trace("withdrawal status", data);
-  if (data.aborted) {
+  if (data.aborted || data.confirmation_done) {
     // signal that this withdrawal is aborted
     // will redirect to account info
+    notifyInfo(i18n.str`Operation was completed from other session`);
     onAborted();
     return <Loading />;
   }
 
-  const parsedUri = parseWithdrawUri(talerWithdrawUri);
-  if (!parsedUri) {
-    onError({
-      title: i18n.str`The Withdrawal URI is not valid: "${talerWithdrawUri}"`,
-    });
-    return <Loading />;
-  }
-
   if (!data.selection_done) {
     return (
       <QrCodeSection
-        talerWithdrawUri={talerWithdrawUri}
-        onAborted={onAborted}
+        withdrawUri={withdrawUri}
+        onAborted={() => {
+          notifyInfo(i18n.str`Operation canceled`);
+          onAborted();
+        }}
       />
     );
   }
@@ -90,10 +98,15 @@ export function WithdrawalQRCode({
   // user to authorize the operation (here CAPTCHA).
   return (
     <WithdrawalConfirmationQuestion
-      withdrawalId={parsedUri.withdrawalOperationId}
-      onError={onError}
-      onConfirmed={onConfirmed}
-      onAborted={onAborted}
+      withdrawUri={withdrawUri}
+      onConfirmed={() => {
+        notifyInfo(i18n.str`Operation confirmed`);
+        onConfirmed();
+      }}
+      onAborted={() => {
+        notifyInfo(i18n.str`Operation canceled`);
+        onAborted();
+      }}
     />
   );
 }
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts 
b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
index d8bc7d980..d7c7d2b48 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
@@ -40,10 +40,18 @@ export function useComponentState({
     api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
   );
   const { devMode } = useDevContext();
+  const accountType: Record<string, string> = {
+    iban: "IBAN",
+    // "x-taler-bank": "Taler Bank",
+  };
+  if (devMode) {
+    accountType["bitcoin"] = "Bitcoin";
+    accountType["x-taler-bank"] = "Taler Bank";
+  }
 
   const [payto, setPayto] = useState("");
   const [alias, setAlias] = useState("");
-  const [type, setType] = useState("");
+  const [type, setType] = useState("iban");
 
   if (!hook) {
     return {
@@ -58,15 +66,6 @@ export function useComponentState({
     };
   }
 
-  const accountType: Record<string, string> = {
-    "": "Choose one account type",
-    iban: "IBAN",
-    // "x-taler-bank": "Taler Bank",
-  };
-  if (devMode) {
-    accountType["bitcoin"] = "Bitcoin";
-    accountType["x-taler-bank"] = "Taler Bank";
-  }
   const uri = parsePaytoUri(payto);
   const found =
     hook.response.accounts.findIndex(
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
index e20d4e0e8..81c1fcf5a 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
@@ -32,12 +32,11 @@ export const JustTwoBitcoinAccounts = 
tests.createExample(ReadyView, {
   currency: "ARS",
   accountType: {
     list: {
-      "": "Choose one account type",
       iban: "IBAN",
-      // bitcoin: "Bitcoin",
-      // "x-taler-bank": "Taler Bank",
+      bitcoin: "Bitcoin",
+      "x-taler-bank": "Taler Bank",
     },
-    value: "",
+    value: "bitcoin",
   },
   alias: {
     value: "",
@@ -86,12 +85,11 @@ export const WithAllTypeOfAccounts = 
tests.createExample(ReadyView, {
   currency: "ARS",
   accountType: {
     list: {
-      "": "Choose one account type",
       iban: "IBAN",
-      // bitcoin: "Bitcoin",
-      // "x-taler-bank": "Taler Bank",
+      bitcoin: "Bitcoin",
+      "x-taler-bank": "Taler Bank",
     },
-    value: "",
+    value: "x-taler-bank",
   },
   alias: {
     value: "",
@@ -167,7 +165,6 @@ export const AddingIbanAccount = 
tests.createExample(ReadyView, {
   currency: "ARS",
   accountType: {
     list: {
-      "": "Choose one account type",
       iban: "IBAN",
       // bitcoin: "Bitcoin",
       // "x-taler-bank": "Taler Bank",
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx 
b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
index 75e1feca4..fb32e5a59 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -21,18 +21,14 @@ import {
   PaytoUriIBAN,
   PaytoUriTalerBank,
   stringifyPaytoUri,
+  validateIban,
 } from "@gnu-taler/taler-util";
 import { styled } from "@linaria/react";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { ErrorMessage } from "../../components/ErrorMessage.js";
 import { SelectList } from "../../components/SelectList.js";
-import {
-  Input,
-  SubTitle,
-  SvgIcon,
-  WarningText,
-} from "../../components/styled/index.js";
+import { Input, SubTitle, SvgIcon } from "../../components/styled/index.js";
 import { useTranslationContext } from "../../context/translation.js";
 import { Button } from "../../mui/Button.js";
 import { TextFieldHandler } from "../../mui/handlers.js";
@@ -111,38 +107,47 @@ export function ReadyView({
             description={error}
           />
         )}
-        <p>
-          <Input>
-            <SelectList
-              label={i18n.str`Select account type`}
-              list={accountType.list}
-              name="accountType"
-              value={accountType.value}
-              onChange={accountType.onChange}
+        <div style={{ width: "100%", display: "flex" }}>
+          {Object.entries(accountType.list).map(([key, name], idx) => (
+            <div
+              style={{
+                marginLeft: 8,
+                padding: 8,
+                borderTopLeftRadius: 5,
+                borderTopRightRadius: 5,
+                backgroundColor:
+                  accountType.value === key ? "#0042b2" : "unset",
+                color: accountType.value === key ? "white" : "unset",
+              }}
+              onClick={(e) => {
+                if (accountType.onChange) {
+                  accountType.onChange(key);
+                }
+              }}
+            >
+              {name}
+            </div>
+          ))}
+        </div>
+        <div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
+          <p>
+            <CustomFieldByAccountType
+              type={accountType.value as AccountType}
+              field={uri}
             />
-          </Input>
+          </p>
+        </div>
+        <p>
+          <TextField
+            label="Alias"
+            variant="filled"
+            placeholder="Easy to remember description"
+            fullWidth
+            disabled={accountType.value === ""}
+            value={alias.value}
+            onChange={alias.onInput}
+          />
         </p>
-        {accountType.value === "" ? undefined : (
-          <Fragment>
-            <p>
-              <CustomFieldByAccountType
-                type={accountType.value as AccountType}
-                field={uri}
-              />
-            </p>
-            <p>
-              <TextField
-                label="Alias"
-                variant="filled"
-                placeholder="Easy to remember description"
-                fullWidth
-                disabled={accountType.value === ""}
-                value={alias.value}
-                onChange={alias.onInput}
-              />
-            </p>
-          </Fragment>
-        )}
       </section>
       <section>
         <Button
@@ -403,6 +408,9 @@ function BitcoinAddressAccount({ field }: { field: 
TextFieldHandler }): VNode {
   });
   return (
     <Fragment>
+      <h3>
+        <i18n.Translate>Bitcoin Account</i18n.Translate>
+      </h3>
       <TextField
         label="Bitcoin address"
         variant="standard"
@@ -442,6 +450,9 @@ function TalerBankAddressAccount({
   });
   return (
     <Fragment>
+      <h3>
+        <i18n.Translate>Taler Bank</i18n.Translate>
+      </h3>
       <TextField
         label="Bank host"
         variant="standard"
@@ -493,7 +504,7 @@ function IbanAddressAccount({ field }: { field: 
TextFieldHandler }): VNode {
       : undefined,
     iban: !iban
       ? i18n.str`Can't be empty`
-      : !ibanRegex.test(iban)
+      : validateIban(iban).type === "invalid"
       ? i18n.str`Invalid iban`
       : undefined,
     name: !name ? i18n.str`Can't be empty` : undefined,
@@ -512,7 +523,10 @@ function IbanAddressAccount({ field }: { field: 
TextFieldHandler }): VNode {
   }
   return (
     <Fragment>
-      <p>
+      <h3>
+        <i18n.Translate>International Bank Account Number</i18n.Translate>
+      </h3>
+      {/* <p>
         <TextField
           label="BIC"
           variant="filled"
@@ -526,7 +540,7 @@ function IbanAddressAccount({ field }: { field: 
TextFieldHandler }): VNode {
             sendUpdateIfNoErrors(v, iban || "", name || "");
           }}
         />
-      </p>
+      </p> */}
       <p>
         <TextField
           label="IBAN"
@@ -545,7 +559,7 @@ function IbanAddressAccount({ field }: { field: 
TextFieldHandler }): VNode {
       </p>
       <p>
         <TextField
-          label="Receiver name"
+          label="Account name"
           variant="filled"
           placeholder="Name of the target bank account owner"
           fullWidth
@@ -576,11 +590,6 @@ function CustomFieldByAccountType({
 
   return (
     <div>
-      <WarningText>
-        <i18n.Translate>
-          We can not validate the account so make sure the value is correct.
-        </i18n.Translate>
-      </WarningText>
       <AccountForm field={field} />
     </div>
   );
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index bf59573ec..5e0b0cbee 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -42,10 +42,6 @@ import {
   WithdrawalDetails,
   WithdrawalType,
 } from "@gnu-taler/taler-util";
-// import {
-//   createExample,
-//   createExampleWithCustomContext as createExampleInCustomContext,
-// } from "../test-utils.js";
 import { tests } from "@gnu-taler/web-util/lib/index.browser";
 import beer from "../../static-dev/beer.png";
 import { TransactionView as TestedComponent } from "./Transaction.js";

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