gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/02: anon withdrawal confirmation, and fix error w


From: gnunet
Subject: [taler-wallet-core] 01/02: anon withdrawal confirmation, and fix error with infinity loop
Date: Fri, 07 Apr 2023 22:31:10 +0200

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

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

commit a3aa7d95d09c83794067c47df4a455c0e3f21806
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Apr 7 17:30:01 2023 -0300

    anon withdrawal confirmation, and fix error with infinity loop
---
 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 ++++----
 19 files changed, 596 insertions(+), 565 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();
+      }}
     />
   );
 }

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