gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (04eec324b -> 9922192b0)


From: gnunet
Subject: [taler-wallet-core] branch master updated (04eec324b -> 9922192b0)
Date: Tue, 28 Feb 2023 23:03:54 +0100

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

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

    from 04eec324b fix #7731
     new 740849dd8 better error handling on unknown error
     new 9922192b0 fix #7729

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:
 .../demobank-ui/src/components/Cashouts/test.ts    |  12 +-
 packages/demobank-ui/src/hooks/backend.ts          |   5 +-
 packages/demobank-ui/src/hooks/circuit.ts          |  10 +-
 packages/demobank-ui/src/pages/AdminPage.tsx       | 149 ++++++----
 packages/demobank-ui/src/pages/BankFrame.tsx       | 100 +++++--
 packages/demobank-ui/src/pages/BusinessAccount.tsx | 311 +++++++++------------
 packages/demobank-ui/src/pages/HomePage.tsx        |  29 +-
 packages/demobank-ui/src/pages/PaymentOptions.tsx  |   4 +-
 .../src/pages/PaytoWireTransferForm.tsx            |  64 +++--
 packages/demobank-ui/src/pages/QrCodeSection.tsx   |   8 +-
 .../demobank-ui/src/pages/RegistrationPage.tsx     |  65 ++---
 .../demobank-ui/src/pages/WalletWithdrawForm.tsx   | 111 ++------
 .../src/pages/WithdrawalConfirmationQuestion.tsx   | 306 ++++----------------
 .../demobank-ui/src/pages/WithdrawalQRCode.tsx     |  28 +-
 packages/demobank-ui/src/scss/demo.scss            |   4 +-
 packages/demobank-ui/src/utils.ts                  |  64 ++++-
 packages/web-util/src/utils/request.ts             | 100 +++++--
 17 files changed, 642 insertions(+), 728 deletions(-)

diff --git a/packages/demobank-ui/src/components/Cashouts/test.ts 
b/packages/demobank-ui/src/components/Cashouts/test.ts
index 014819f44..6d61b0af4 100644
--- a/packages/demobank-ui/src/components/Cashouts/test.ts
+++ b/packages/demobank-ui/src/components/Cashouts/test.ts
@@ -32,7 +32,9 @@ describe("Transaction states", () => {
 
     const props: Props = {
       account: "123",
-      onSelected: () => { null },
+      onSelected: () => {
+        null;
+      },
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
@@ -117,7 +119,9 @@ describe("Transaction states", () => {
 
     const props: Props = {
       account: "123",
-      onSelected: () => { null },
+      onSelected: () => {
+        null;
+      },
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
@@ -151,7 +155,9 @@ describe("Transaction states", () => {
 
     const props: Props = {
       account: "123",
-      onSelected: () => { null },
+      onSelected: () => {
+        null;
+      },
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
diff --git a/packages/demobank-ui/src/hooks/backend.ts 
b/packages/demobank-ui/src/hooks/backend.ts
index 8d526c0e4..3f2981edf 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -279,7 +279,10 @@ export function useAuthenticatedBackend(): useBackendType {
     sandboxCashoutFetcher,
   };
 }
-
+/**
+ *
+ * @deprecated
+ */
 export function useBackendConfig(): HttpResponse<
   SandboxBackend.Config,
   SandboxBackend.SandboxError
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index 6cf543a3c..c2563adb4 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -82,11 +82,11 @@ export function useAdminAccountAPI(): AdminAccountAPI {
       contentType: "json",
     });
     if (account === state.username) {
-      await mutateAll(/.*/)
+      await mutateAll(/.*/);
       logIn({
         username: account,
-        password: data.new_password
-      })
+        password: data.new_password,
+      });
     }
     return res;
   };
@@ -284,7 +284,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
     HttpResponseOk<SandboxBackend.Circuit.Config>,
     RequestError<SandboxBackend.SandboxError>
   >([`circuit-api/config`], fetcher, {
-    refreshInterval: 0,
+    refreshInterval: 1000,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
     revalidateOnReconnect: false,
@@ -298,7 +298,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
   if (data) {
     // data.data.ratios_and_fees.sell_out_fee = 2
     if (!data.data.ratios_and_fees.fiat_currency) {
-      data.data.ratios_and_fees.fiat_currency = "FIAT"
+      data.data.ratios_and_fees.fiat_currency = "FIAT";
     }
   }
   if (data) return data;
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx 
b/packages/demobank-ui/src/pages/AdminPage.tsx
index 0a1dc26ec..2a5701a95 100644
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -16,6 +16,7 @@
 
 import {
   Amounts,
+  HttpStatusCode,
   parsePaytoUri,
   TranslatedString,
 } from "@gnu-taler/taler-util";
@@ -35,11 +36,13 @@ import {
   useAdminAccountAPI,
 } from "../hooks/circuit.js";
 import {
+  buildRequestErrorMessage,
   PartialButDefined,
+  RecursivePartial,
   undefinedIfEmpty,
   WithIntermediate,
 } from "../utils.js";
-import { ErrorBanner } from "./BankFrame.js";
+import { ErrorBannerFloat } from "./BankFrame.js";
 import { ShowCashoutDetails } from "./BusinessAccount.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
@@ -373,7 +376,7 @@ export function UpdateAccountPassword({
         </h1>
       </div>
       {error && (
-        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+        <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
       )}
 
       <form class="pure-form">
@@ -435,7 +438,17 @@ export function UpdateAccountPassword({
                   });
                   onUpdateSuccess();
                 } catch (error) {
-                  handleError(error, saveError, i18n);
+                  if (error instanceof RequestError) {
+                    saveError(buildRequestErrorMessage(i18n, error.cause));
+                  } else {
+                    saveError({
+                      title: i18n.str`Operation failed, please report`,
+                      description:
+                        error instanceof Error
+                          ? error.message
+                          : JSON.stringify(error),
+                    });
+                  }
                 }
               }}
             />
@@ -467,13 +480,16 @@ function CreateNewAccount({
         </h1>
       </div>
       {error && (
-        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+        <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
       )}
 
       <AccountForm
         template={undefined}
         purpose="create"
-        onChange={(a) => setSubmitAccount(a)}
+        onChange={(a) => {
+          console.log(a);
+          setSubmitAccount(a);
+        }}
       />
 
       <p>
@@ -514,7 +530,28 @@ function CreateNewAccount({
                   await createAccount(account);
                   onCreateSuccess(account.password);
                 } catch (error) {
-                  handleError(error, saveError, i18n);
+                  if (error instanceof RequestError) {
+                    saveError(
+                      buildRequestErrorMessage(i18n, error.cause, {
+                        onClientError: (status) =>
+                          status === HttpStatusCode.Forbidden
+                            ? i18n.str`The rights to perform the operation are 
not sufficient`
+                            : status === HttpStatusCode.BadRequest
+                            ? i18n.str`Input data was invalid`
+                            : status === HttpStatusCode.Conflict
+                            ? i18n.str`At least one registration detail was 
not available`
+                            : undefined,
+                      }),
+                    );
+                  } else {
+                    saveError({
+                      title: i18n.str`Operation failed, please report`,
+                      description:
+                        error instanceof Error
+                          ? error.message
+                          : JSON.stringify(error),
+                    });
+                  }
                 }
               }}
             />
@@ -564,7 +601,7 @@ export function ShowAccountDetails({
         </h1>
       </div>
       {error && (
-        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+        <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
       )}
       <AccountForm
         template={result.data}
@@ -622,7 +659,26 @@ export function ShowAccountDetails({
                       });
                       onUpdateSuccess();
                     } catch (error) {
-                      handleError(error, saveError, i18n);
+                      if (error instanceof RequestError) {
+                        saveError(
+                          buildRequestErrorMessage(i18n, error.cause, {
+                            onClientError: (status) =>
+                              status === HttpStatusCode.Forbidden
+                                ? i18n.str`The rights to change the account 
are not sufficient`
+                                : status === HttpStatusCode.NotFound
+                                ? i18n.str`The username was not found`
+                                : undefined,
+                          }),
+                        );
+                      } else {
+                        saveError({
+                          title: i18n.str`Operation failed, please report`,
+                          description:
+                            error instanceof Error
+                              ? error.message
+                              : JSON.stringify(error),
+                        });
+                      }
                     }
                   }
                 }}
@@ -673,7 +729,7 @@ function RemoveAccount({
         </h1>
       </div>
       {!isBalanceEmpty && (
-        <ErrorBanner
+        <ErrorBannerFloat
           error={{
             title: i18n.str`Can't delete the account`,
             description: i18n.str`Balance is not empty`,
@@ -681,7 +737,7 @@ function RemoveAccount({
         />
       )}
       {error && (
-        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+        <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
       )}
 
       <p>
@@ -710,7 +766,28 @@ function RemoveAccount({
                   const r = await deleteAccount(account);
                   onUpdateSuccess();
                 } catch (error) {
-                  handleError(error, saveError, i18n);
+                  if (error instanceof RequestError) {
+                    saveError(
+                      buildRequestErrorMessage(i18n, error.cause, {
+                        onClientError: (status) =>
+                          status === HttpStatusCode.Forbidden
+                            ? i18n.str`The administrator specified a 
institutional username`
+                            : status === HttpStatusCode.NotFound
+                            ? i18n.str`The username was not found`
+                            : status === HttpStatusCode.PreconditionFailed
+                            ? i18n.str`Balance was not zero`
+                            : undefined,
+                      }),
+                    );
+                  } else {
+                    saveError({
+                      title: i18n.str`Operation failed, please report`,
+                      description:
+                        error instanceof Error
+                          ? error.message
+                          : JSON.stringify(error),
+                    });
+                  }
                 }
               }}
             />
@@ -720,7 +797,6 @@ function RemoveAccount({
     </div>
   );
 }
-
 /**
  * Create valid account object to update or create
  * Take template as initial values for the form
@@ -740,7 +816,9 @@ function AccountForm({
 }): VNode {
   const initial = initializeFromTemplate(template);
   const [form, setForm] = useState(initial);
-  const [errors, setErrors] = useState<typeof initial | undefined>(undefined);
+  const [errors, setErrors] = useState<
+    RecursivePartial<typeof initial> | undefined
+  >(undefined);
   const { i18n } = useTranslationContext();
 
   function updateForm(newForm: typeof initial): void {
@@ -748,7 +826,7 @@ function AccountForm({
       ? undefined
       : parsePaytoUri(newForm.cashout_address);
 
-    const validationResult = undefinedIfEmpty<typeof initial>({
+    const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
       cashout_address: !newForm.cashout_address
         ? i18n.str`required`
         : !parsed
@@ -758,20 +836,20 @@ function AccountForm({
         : !IBAN_REGEX.test(parsed.iban)
         ? i18n.str`IBAN should have just uppercased letters and numbers`
         : undefined,
-      contact_data: {
-        email: !newForm.contact_data.email
+      contact_data: undefinedIfEmpty({
+        email: !newForm.contact_data?.email
           ? undefined
           : !EMAIL_REGEX.test(newForm.contact_data.email)
           ? i18n.str`it should be an email`
           : undefined,
-        phone: !newForm.contact_data.phone
+        phone: !newForm.contact_data?.phone
           ? undefined
           : !newForm.contact_data.phone.startsWith("+")
           ? i18n.str`should start with +`
           : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
           ? i18n.str`phone number can't have other than numbers`
           : undefined,
-      },
+      }),
       iban: !newForm.iban
         ? i18n.str`required`
         : !IBAN_REGEX.test(newForm.iban)
@@ -780,10 +858,9 @@ function AccountForm({
       name: !newForm.name ? i18n.str`required` : undefined,
       username: !newForm.username ? i18n.str`required` : undefined,
     });
-
-    setErrors(validationResult);
+    setErrors(errors);
     setForm(newForm);
-    onChange(validationResult === undefined ? undefined : (newForm as any));
+    onChange(errors === undefined ? (newForm as any) : undefined);
   }
 
   return (
@@ -846,7 +923,7 @@ function AccountForm({
           }}
         />
         <ShowInputErrorLabel
-          message={errors?.contact_data.email}
+          message={errors?.contact_data?.email}
           isDirty={form.contact_data.email !== undefined}
         />
       </fieldset>
@@ -861,7 +938,7 @@ function AccountForm({
           }}
         />
         <ShowInputErrorLabel
-          message={errors?.contact_data.phone}
+          message={errors?.contact_data?.phone}
           isDirty={form.contact_data?.phone !== undefined}
         />
       </fieldset>
@@ -883,29 +960,3 @@ function AccountForm({
     </form>
   );
 }
-
-function handleError(
-  error: unknown,
-  saveError: (e: ErrorMessage) => void,
-  i18n: ReturnType<typeof useTranslationContext>["i18n"],
-): void {
-  if (error instanceof RequestError) {
-    const payload = error.info.error as SandboxBackend.SandboxError;
-    saveError({
-      title: error.info.serverError
-        ? i18n.str`Server had an error`
-        : i18n.str`Server didn't accept the request`,
-      description: payload.error.description,
-    });
-  } else if (error instanceof Error) {
-    saveError({
-      title: i18n.str`Could not update account`,
-      description: error.message,
-    });
-  } else {
-    saveError({
-      title: i18n.str`Error, please report`,
-      debug: JSON.stringify(error),
-    });
-  }
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx 
b/packages/demobank-ui/src/pages/BankFrame.tsx
index cf52cb0f3..e75a5c1d0 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -126,14 +126,6 @@ export function BankFrame({
         </nav>
       </div>
       <section id="main" class="content">
-        {pageState.error && (
-          <ErrorBanner
-            error={pageState.error}
-            onClear={() => {
-              pageStateSetter((prev) => ({ ...prev, error: undefined }));
-            }}
-          />
-        )}
         <StatusBanner />
         {backend.state.status === "loggedIn" ? (
           <div class="top-right">
@@ -191,20 +183,48 @@ function maybeDemoContent(content: VNode): VNode {
   return <Fragment />;
 }
 
-export function ErrorBanner({
+export function ErrorBannerFloat({
   error,
   onClear,
 }: {
   error: ErrorMessage;
   onClear?: () => void;
-}): VNode | null {
+}): VNode {
+  return (
+    <div
+      style={{
+        position: "fixed",
+        top: 0,
+        zIndex: 200,
+        width: "90%",
+      }}
+    >
+      <ErrorBanner error={error} onClear={onClear} />
+    </div>
+  );
+}
+
+function ErrorBanner({
+  error,
+  onClear,
+}: {
+  error: ErrorMessage;
+  onClear?: () => void;
+}): VNode {
   return (
-    <div class="informational informational-fail" style={{ marginTop: 8 }}>
+    <div
+      class="informational informational-fail"
+      style={{
+        marginTop: 8,
+        paddingLeft: 16,
+        paddingRight: 16,
+      }}
+    >
       <div style={{ display: "flex", justifyContent: "space-between" }}>
         <p>
           <b>{error.title}</b>
         </p>
-        <div>
+        <div style={{ marginTop: "auto", marginBottom: "auto" }}>
           {onClear && (
             <input
               type="button"
@@ -225,26 +245,46 @@ export function ErrorBanner({
 
 function StatusBanner(): VNode | null {
   const { pageState, pageStateSetter } = usePageContext();
-  if (!pageState.info) return null;
 
-  const rval = (
-    <div class="informational informational-ok" style={{ marginTop: 8 }}>
-      <div style={{ display: "flex", justifyContent: "space-between" }}>
-        <p>
-          <b>{pageState.info}</b>
-        </p>
-        <div>
-          <input
-            type="button"
-            class="pure-button"
-            value="Clear"
-            onClick={async () => {
-              pageStateSetter((prev) => ({ ...prev, info: undefined }));
-            }}
-          />
+  return (
+    <div
+      style={{
+        position: "fixed",
+        top: 0,
+        zIndex: 200,
+        width: "90%",
+      }}
+    >
+      {!pageState.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>
+            </p>
+            <div>
+              <input
+                type="button"
+                class="pure-button"
+                value="Clear"
+                onClick={async () => {
+                  pageStateSetter((prev) => ({ ...prev, info: undefined }));
+                }}
+              />
+            </div>
+          </div>
         </div>
-      </div>
+      )}
+      {!pageState.error ? undefined : (
+        <ErrorBanner
+          error={pageState.error}
+          onClear={() => {
+            pageStateSetter((prev) => ({ ...prev, error: undefined }));
+          }}
+        />
+      )}
     </div>
   );
-  return rval;
 }
diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx 
b/packages/demobank-ui/src/pages/BusinessAccount.tsx
index 6278fe08b..9bd799746 100644
--- a/packages/demobank-ui/src/pages/BusinessAccount.tsx
+++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx
@@ -20,13 +20,13 @@ import {
   TranslatedString,
 } from "@gnu-taler/taler-util";
 import {
-  ErrorType,
+  HttpResponse,
   HttpResponsePaginated,
   RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { 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";
@@ -36,9 +36,13 @@ import {
   useCircuitAccountAPI,
   useRatiosAndFeeConfig,
 } from "../hooks/circuit.js";
-import { TanChannel, undefinedIfEmpty } from "../utils.js";
+import {
+  buildRequestErrorMessage,
+  TanChannel,
+  undefinedIfEmpty,
+} from "../utils.js";
 import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
-import { ErrorBanner } from "./BankFrame.js";
+import { ErrorBannerFloat } from "./BankFrame.js";
 import { LoginForm } from "./LoginForm.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
@@ -177,6 +181,46 @@ type ErrorFrom<T> = {
   [P in keyof T]+?: string;
 };
 
+// check #7719
+function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
+  SandboxBackend.Circuit.Config & { hasChanged?: boolean },
+  SandboxBackend.SandboxError
+> {
+  const result = useRatiosAndFeeConfig();
+  const [oldResult, setOldResult] = useState<
+    SandboxBackend.Circuit.Config | undefined
+  >(undefined);
+  const dataFromBackend = result.ok ? result.data : undefined;
+  useEffect(() => {
+    // save only the first result of /config to the backend
+    if (!dataFromBackend || oldResult !== undefined) return;
+    setOldResult(dataFromBackend);
+  }, [dataFromBackend]);
+
+  if (!result.ok) return result;
+
+  const data = !oldResult ? result.data : oldResult;
+  const hasChanged =
+    oldResult &&
+    (result.data.name !== oldResult.name ||
+      result.data.version !== oldResult.version ||
+      result.data.ratios_and_fees.buy_at_ratio !==
+        oldResult.ratios_and_fees.buy_at_ratio ||
+      result.data.ratios_and_fees.buy_in_fee !==
+        oldResult.ratios_and_fees.buy_in_fee ||
+      result.data.ratios_and_fees.sell_at_ratio !==
+        oldResult.ratios_and_fees.sell_at_ratio ||
+      result.data.ratios_and_fees.sell_out_fee !==
+        oldResult.ratios_and_fees.sell_out_fee ||
+      result.data.ratios_and_fees.fiat_currency !==
+        oldResult.ratios_and_fees.fiat_currency);
+
+  return {
+    ...result,
+    data: { ...data, hasChanged },
+  };
+}
+
 function CreateCashout({
   account,
   onComplete,
@@ -207,15 +251,6 @@ function CreateCashout({
 
   if (!sellRate || sellRate < 0) return <div>error rate</div>;
 
-  function truncate(a: AmountJson): AmountJson {
-    const str = Amounts.stringify(a);
-    const idx = str.indexOf(".");
-    if (idx === -1) return a;
-    const truncated = str.substring(0, idx + 3);
-    console.log(str, truncated);
-    return Amounts.parseOrThrow(truncated);
-  }
-
   const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
   const amount_debit = !amount
     ? zero
@@ -256,7 +291,7 @@ function CreateCashout({
   return (
     <div>
       {error && (
-        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+        <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
       )}
       <h1>New cashout</h1>
       <form class="pure-form">
@@ -555,74 +590,31 @@ function CreateCashout({
                 onComplete(res.data.uuid);
               } catch (error) {
                 if (error instanceof RequestError) {
-                  const e = error as RequestError<SandboxBackend.SandboxError>;
-                  switch (e.cause.type) {
-                    case ErrorType.TIMEOUT: {
-                      saveError({
-                        title: i18n.str`Request timeout, try again later.`,
-                      });
-                      break;
-                    }
-                    case ErrorType.CLIENT: {
-                      const errorData = e.cause.error;
-
-                      if (
-                        e.cause.status === HttpStatusCode.PreconditionFailed
-                      ) {
-                        saveError({
-                          title: i18n.str`The account does not have sufficient 
funds`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.info),
-                        });
-                      } else if (e.cause.status === HttpStatusCode.Conflict) {
-                        saveError({
-                          title: i18n.str`No contact information for this 
channel`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.info),
-                        });
-                      } else {
-                        saveError({
-                          title: i18n.str`New cashout gave response error`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.info),
-                        });
-                      }
-                      break;
-                    }
-                    case ErrorType.SERVER: {
-                      const errorData = e.cause.error;
-                      if (
-                        e.cause.status === HttpStatusCode.ServiceUnavailable
-                      ) {
-                        saveError({
-                          title: i18n.str`The bank does not support the TAN 
channel for this operation`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.info),
-                        });
-                      } else {
-                        saveError({
-                          title: i18n.str`Creating cashout returned with a 
server error`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.cause),
-                        });
-                      }
-                      break;
-                    }
-                    case ErrorType.UNEXPECTED: {
-                      saveError({
-                        title: i18n.str`Unexpected error trying to create 
cashout.`,
-                        debug: JSON.stringify(error.cause),
-                      });
-                      break;
-                    }
-                    default: {
-                      assertUnreachable(e.cause);
-                    }
-                  }
-                } else if (error instanceof Error) {
+                  saveError(
+                    buildRequestErrorMessage(i18n, error.cause, {
+                      onClientError: (status) =>
+                        status === HttpStatusCode.BadRequest
+                          ? i18n.str`The exchange rate was incorrectly applied`
+                          : status === HttpStatusCode.Forbidden
+                          ? i18n.str`A institutional user tried the operation`
+                          : status === HttpStatusCode.Conflict
+                          ? i18n.str`Need a contact data where to send the TAN`
+                          : status === HttpStatusCode.PreconditionFailed
+                          ? i18n.str`The account does not have sufficient 
funds`
+                          : undefined,
+                      onServerError: (status) =>
+                        status === HttpStatusCode.ServiceUnavailable
+                          ? i18n.str`The bank does not support the TAN channel 
for this operation`
+                          : undefined,
+                    }),
+                  );
+                } else {
                   saveError({
-                    title: i18n.str`Cashout failed, please report`,
-                    description: error.message,
+                    title: i18n.str`Operation failed, please report`,
+                    description:
+                      error instanceof Error
+                        ? error.message
+                        : JSON.stringify(error),
                   });
                 }
               }
@@ -636,6 +628,25 @@ function CreateCashout({
   );
 }
 
+const MAX_AMOUNT_DIGIT = 2;
+/**
+ * Truncate the amount of digits to display
+ * in the form based on the fee calculations
+ *
+ * Backend must have the same truncation
+ * @param a
+ * @returns
+ */
+function truncate(a: AmountJson): AmountJson {
+  const str = Amounts.stringify(a);
+  const idx = str.indexOf(".");
+  if (idx === -1) {
+    return a;
+  }
+  const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
+  return Amounts.parseOrThrow(truncated);
+}
+
 interface ShowCashoutProps {
   id: string;
   onCancel: () => void;
@@ -662,7 +673,7 @@ export function ShowCashoutDetails({
     <div>
       <h1>Cashout details {id}</h1>
       {error && (
-        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+        <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
       )}
       <form class="pure-form">
         <fieldset>
@@ -744,68 +755,27 @@ export function ShowCashoutDetails({
               onClick={async (e) => {
                 e.preventDefault();
                 try {
-                  const rest = await abortCashout(id);
+                  await abortCashout(id);
                   onCancel();
                 } catch (error) {
                   if (error instanceof RequestError) {
-                    const e =
-                      error as RequestError<SandboxBackend.SandboxError>;
-                    switch (e.cause.type) {
-                      case ErrorType.TIMEOUT: {
-                        saveError({
-                          title: i18n.str`Request timeout, try again later.`,
-                        });
-                        break;
-                      }
-                      case ErrorType.CLIENT: {
-                        const errorData = e.cause.error;
-                        if (
-                          e.cause.status === HttpStatusCode.PreconditionFailed
-                        ) {
-                          saveError({
-                            title: i18n.str`Cashout was already aborted`,
-                            description: errorData.error.description,
-                            debug: JSON.stringify(error.info),
-                          });
-                        } else {
-                          saveError({
-                            title: i18n.str`Aborting cashout gave response 
error`,
-                            description: errorData.error.description,
-                            debug: JSON.stringify(error.info),
-                          });
-                        }
-
-                        saveError({
-                          title: i18n.str`Aborting cashout gave response 
error`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.cause),
-                        });
-                        break;
-                      }
-                      case ErrorType.SERVER: {
-                        const errorData = e.cause.error;
-                        saveError({
-                          title: i18n.str`Aborting cashout returned with a 
server error`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.cause),
-                        });
-                        break;
-                      }
-                      case ErrorType.UNEXPECTED: {
-                        saveError({
-                          title: i18n.str`Unexpected error trying to abort 
cashout.`,
-                          debug: JSON.stringify(error.cause),
-                        });
-                        break;
-                      }
-                      default: {
-                        assertUnreachable(e.cause);
-                      }
-                    }
-                  } else if (error instanceof Error) {
+                    saveError(
+                      buildRequestErrorMessage(i18n, error.cause, {
+                        onClientError: (status) =>
+                          status === HttpStatusCode.NotFound
+                            ? i18n.str`Cashout not found. It may be also mean 
that it was already aborted.`
+                            : status === HttpStatusCode.PreconditionFailed
+                            ? i18n.str`Cashout was already confimed`
+                            : undefined,
+                      }),
+                    );
+                  } else {
                     saveError({
-                      title: i18n.str`Aborting failed, please report`,
-                      description: error.message,
+                      title: i18n.str`Operation failed, please report`,
+                      description:
+                        error instanceof Error
+                          ? error.message
+                          : JSON.stringify(error),
                     });
                   }
                 }
@@ -827,48 +797,27 @@ export function ShowCashoutDetails({
                   });
                 } catch (error) {
                   if (error instanceof RequestError) {
-                    const e =
-                      error as RequestError<SandboxBackend.SandboxError>;
-                    switch (e.cause.type) {
-                      case ErrorType.TIMEOUT: {
-                        saveError({
-                          title: i18n.str`Request timeout, try again later.`,
-                        });
-                        break;
-                      }
-                      case ErrorType.CLIENT: {
-                        const errorData = e.cause.error;
-                        saveError({
-                          title: i18n.str`Confirmation of cashout gave 
response error`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.cause),
-                        });
-                        break;
-                      }
-                      case ErrorType.SERVER: {
-                        const errorData = e.cause.error;
-                        saveError({
-                          title: i18n.str`Confirmation of cashout gave 
response error`,
-                          description: errorData.error.description,
-                          debug: JSON.stringify(error.cause),
-                        });
-                        break;
-                      }
-                      case ErrorType.UNEXPECTED: {
-                        saveError({
-                          title: i18n.str`Unexpected error trying to cashout.`,
-                          debug: JSON.stringify(error.cause),
-                        });
-                        break;
-                      }
-                      default: {
-                        assertUnreachable(e.cause);
-                      }
-                    }
-                  } else if (error instanceof Error) {
+                    saveError(
+                      buildRequestErrorMessage(i18n, error.cause, {
+                        onClientError: (status) =>
+                          status === HttpStatusCode.NotFound
+                            ? i18n.str`Cashout not found. It may be also mean 
that it was already aborted.`
+                            : status === HttpStatusCode.PreconditionFailed
+                            ? i18n.str`Cashout was already confimed`
+                            : status === HttpStatusCode.Conflict
+                            ? i18n.str`Confirmation failed. Maybe the user 
changed their cash-out address between the creation and the confirmation`
+                            : status === HttpStatusCode.Forbidden
+                            ? i18n.str`Invalid code`
+                            : undefined,
+                      }),
+                    );
+                  } else {
                     saveError({
-                      title: i18n.str`Confirmation failed, please report`,
-                      description: error.message,
+                      title: i18n.str`Operation failed, please report`,
+                      description:
+                        error instanceof Error
+                          ? error.message
+                          : JSON.stringify(error),
                     });
                   }
                 }
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx 
b/packages/demobank-ui/src/pages/HomePage.tsx
index a360bd64c..7ef4284bf 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -14,11 +14,10 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
+import { Logger } from "@gnu-taler/taler-util";
 import {
   ErrorType,
   HttpResponsePaginated,
-  RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
@@ -79,7 +78,27 @@ export function HomePage({ onRegister }: { onRegister: () => 
void }): VNode {
         account={backend.state.username}
         withdrawalId={withdrawalId}
         talerWithdrawUri={talerWithdrawUri}
-        onAbort={clearCurrentWithdrawal}
+        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,
@@ -147,7 +166,7 @@ function handleNotOkResult(
           break;
         }
         case ErrorType.CLIENT: {
-          const errorData = result.error;
+          const errorData = result.payload;
           onErrorHandler({
             title: i18n.str`Could not load due to a client error`,
             description: errorData.error.description,
@@ -168,7 +187,7 @@ function handleNotOkResult(
           onErrorHandler({
             title: i18n.str`Unexpected error.`,
             description: `Diagnostic from ${result.info?.url} is 
"${result.message}"`,
-            debug: JSON.stringify(result.error),
+            debug: JSON.stringify(result.exception),
           });
           break;
         }
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index dd04ed6e2..610efafc0 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -14,12 +14,12 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { PageStateType, usePageContext } from "../context/pageState.js";
 import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
 import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
 
 /**
  * Let the user choose a payment option,
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 07b011a00..9698d5b98 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -17,22 +17,20 @@
 import {
   Amounts,
   buildPayto,
+  HttpStatusCode,
   Logger,
   parsePaytoUri,
   stringifyPaytoUri,
 } from "@gnu-taler/taler-util";
 import {
-  InternationalizationAPI,
   RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType } from "../context/pageState.js";
 import { useAccessAPI } from "../hooks/access.js";
-import { BackendState } from "../hooks/backend.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("PaytoWireTransferForm");
@@ -184,11 +182,35 @@ export function PaytoWireTransferForm({
                 ibanPayto.params.message = encodeURIComponent(subject);
                 const paytoUri = stringifyPaytoUri(ibanPayto);
 
-                await createTransaction({
-                  paytoUri,
-                  amount: `${currency}:${amount}`,
-                });
-                onSuccess();
+                try {
+                  await createTransaction({
+                    paytoUri,
+                    amount: `${currency}:${amount}`,
+                  });
+                  onSuccess();
+                  setAmount(undefined);
+                  setIban(undefined);
+                  setSubject(undefined);
+                } catch (error) {
+                  if (error instanceof RequestError) {
+                    onError(
+                      buildRequestErrorMessage(i18n, error.cause, {
+                        onClientError: (status) =>
+                          status === HttpStatusCode.BadRequest
+                            ? i18n.str`The request was invalid or the 
payto://-URI used unacceptable features.`
+                            : undefined,
+                      }),
+                    );
+                  } else {
+                    onError({
+                      title: i18n.str`Operation failed, please report`,
+                      description:
+                        error instanceof Error
+                          ? error.message
+                          : JSON.stringify(error),
+                    });
+                  }
+                }
               }}
             />
             <input
@@ -298,13 +320,21 @@ export function PaytoWireTransferForm({
                 rawPaytoInputSetter(undefined);
               } catch (error) {
                 if (error instanceof RequestError) {
-                  const errorData: SandboxBackend.SandboxError =
-                    error.info.error;
-
+                  onError(
+                    buildRequestErrorMessage(i18n, error.cause, {
+                      onClientError: (status) =>
+                        status === HttpStatusCode.BadRequest
+                          ? i18n.str`The request was invalid or the 
payto://-URI used unacceptable features.`
+                          : undefined,
+                    }),
+                  );
+                } else {
                   onError({
-                    title: i18n.str`Transfer creation gave response error`,
-                    description: errorData.error.description,
-                    debug: JSON.stringify(errorData),
+                    title: i18n.str`Operation failed, please report`,
+                    description:
+                      error instanceof Error
+                        ? error.message
+                        : JSON.stringify(error),
                   });
                 }
               }
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx 
b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index 708e28657..8f85fff91 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -14,17 +14,17 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useEffect } from "preact/hooks";
 import { QR } from "../components/QR.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
 
 export function QrCodeSection({
   talerWithdrawUri,
-  onAbort,
+  onAborted,
 }: {
   talerWithdrawUri: string;
-  onAbort: () => void;
+  onAborted: () => void;
 }): VNode {
   const { i18n } = useTranslationContext();
   useEffect(() => {
@@ -64,7 +64,7 @@ export function QrCodeSection({
           <br />
           <a
             class="pure-button btn-cancel"
-            onClick={onAbort}
+            onClick={onAborted}
           >{i18n.str`Abort`}</a>
         </div>
       </article>
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx 
b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index c6bc3c327..f22475e10 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -15,7 +15,6 @@
  */
 import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
 import {
-  ErrorType,
   RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
@@ -25,7 +24,7 @@ import { useBackendContext } from "../context/backend.js";
 import { PageStateType } from "../context/pageState.js";
 import { useTestingAPI } from "../hooks/access.js";
 import { bankUiSettings } from "../settings.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("RegistrationPage");
@@ -177,52 +176,22 @@ function RegistrationForm({
                     onComplete();
                   } catch (error) {
                     if (error instanceof RequestError) {
-                      const e =
-                        error as RequestError<SandboxBackend.SandboxError>;
-                      switch (e.cause.type) {
-                        case ErrorType.TIMEOUT: {
-                          onError({
-                            title: i18n.str`Request timeout, try again later.`,
-                          });
-                          break;
-                        }
-                        case ErrorType.CLIENT: {
-                          const errorData = e.cause.error;
-                          if (e.cause.status === HttpStatusCode.Conflict) {
-                            onError({
-                              title: i18n.str`That username is already taken`,
-                              description: errorData.error.description,
-                              debug: JSON.stringify(error.cause),
-                            });
-                          } else {
-                            onError({
-                              title: i18n.str`New registration gave response 
error`,
-                              description: errorData.error.description,
-                              debug: JSON.stringify(error.cause),
-                            });
-                          }
-                          break;
-                        }
-                        case ErrorType.SERVER: {
-                          const errorData = e.cause.error;
-                          onError({
-                            title: i18n.str`New registration gave response 
error`,
-                            description: errorData?.error?.description,
-                            debug: JSON.stringify(error.cause),
-                          });
-                          break;
-                        }
-                        case ErrorType.UNEXPECTED: {
-                          onError({
-                            title: i18n.str`Unexpected error doing the 
registration.`,
-                            debug: JSON.stringify(error.cause),
-                          });
-                          break;
-                        }
-                        default: {
-                          assertUnreachable(e.cause);
-                        }
-                      }
+                      onError(
+                        buildRequestErrorMessage(i18n, error.cause, {
+                          onClientError: (status) =>
+                            status === HttpStatusCode.Conflict
+                              ? i18n.str`That username is already taken`
+                              : undefined,
+                        }),
+                      );
+                    } else {
+                      onError({
+                        title: i18n.str`Operation failed, please report`,
+                        description:
+                          error instanceof Error
+                            ? error.message
+                            : JSON.stringify(error),
+                      });
                     }
                   }
                 }}
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 02b389c6c..c1ad2f0cf 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -14,16 +14,16 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, Logger } from "@gnu-taler/taler-util";
+import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
 import {
   RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { h, VNode } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { PageStateType } from "../context/pageState.js";
 import { useAccessAPI } from "../hooks/access.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("WalletWithdrawForm");
@@ -127,16 +127,21 @@ export function WalletWithdrawForm({
                 onSuccess(result.data);
               } catch (error) {
                 if (error instanceof RequestError) {
+                  onError(
+                    buildRequestErrorMessage(i18n, error.cause, {
+                      onClientError: (status) =>
+                        status === HttpStatusCode.Forbidden
+                          ? i18n.str`The operation was rejected due to 
insufficient funds`
+                          : undefined,
+                    }),
+                  );
+                } else {
                   onError({
-                    title: i18n.str`Could not create withdrawal operation`,
-                    description: (error as any).error.description,
-                    debug: JSON.stringify(error),
-                  });
-                }
-                if (error instanceof Error) {
-                  onError({
-                    title: i18n.str`Something when wrong trying to start the 
withdrawal`,
-                    description: error.message,
+                    title: i18n.str`Operation failed, please report`,
+                    description:
+                      error instanceof Error
+                        ? error.message
+                        : JSON.stringify(error),
                   });
                 }
               }
@@ -147,85 +152,3 @@ export function WalletWithdrawForm({
     </form>
   );
 }
-
-// /**
-//  * This function creates a withdrawal operation via the Access API.
-//  *
-//  * After having successfully created the withdrawal operation, the
-//  * user should receive a QR code of the "taler://withdraw/" type and
-//  * supposed to scan it with their phone.
-//  *
-//  * TODO: (1) after the scan, the page should refresh itself and inform
-//  * the user about the operation's outcome.  (2) use POST helper.  */
-// async function createWithdrawalCall(
-//   amount: string,
-//   backendState: BackendState,
-//   pageStateSetter: StateUpdater<PageStateType>,
-//   i18n: InternationalizationAPI,
-// ): Promise<void> {
-//   if (backendState?.status === "loggedOut") {
-//     logger.error("Page has a problem: no credentials found in the state.");
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`No credentials given.`,
-//       },
-//     }));
-//     return;
-//   }
-
-//   let res: Response;
-//   try {
-//     const { username, password } = backendState;
-//     const headers = prepareHeaders(username, password);
-
-//     // Let bank generate withdraw URI:
-//     const url = new URL(
-//       `access-api/accounts/${backendState.username}/withdrawals`,
-//       backendState.url,
-//     );
-//     res = await fetch(url.href, {
-//       method: "POST",
-//       headers,
-//       body: JSON.stringify({ amount }),
-//     });
-//   } catch (error) {
-//     logger.trace("Could not POST withdrawal request to the bank", error);
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`Could not create withdrawal operation`,
-//         description: (error as any).error.description,
-//         debug: JSON.stringify(error),
-//       },
-//     }));
-//     return;
-//   }
-//   if (!res.ok) {
-//     const response = await res.json();
-//     logger.error(
-//       `Withdrawal creation gave response error: ${response} 
(${res.status})`,
-//     );
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`Withdrawal creation gave response error`,
-//         description: response.error.description,
-//         debug: JSON.stringify(response),
-//       },
-//     }));
-//     return;
-//   }
-
-//   logger.trace("Withdrawal operation created!");
-//   const resp = await res.json();
-//   pageStateSetter((prevState: PageStateType) => ({
-//     ...prevState,
-//     withdrawalInProgress: true,
-//     talerWithdrawUri: resp.taler_withdraw_uri,
-//     withdrawalId: resp.withdrawal_id,
-//   }));
-// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 4e5c621e2..d7ed215be 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -14,32 +14,36 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Logger } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { HttpStatusCode, Logger } 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 { useBackendContext } from "../context/backend.js";
-import { usePageContext } from "../context/pageState.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
 import { useAccessAPI } from "../hooks/access.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("WithdrawalConfirmationQuestion");
 
 interface Props {
-  account: string;
   withdrawalId: string;
+  onError: (e: PageStateType["error"]) => void;
+  onConfirmed: () => void;
+  onAborted: () => void;
 }
 /**
  * Additional authentication required to complete the operation.
  * Not providing a back button, only abort.
  */
 export function WithdrawalConfirmationQuestion({
-  account,
+  onError,
+  onConfirmed,
+  onAborted,
   withdrawalId,
 }: Props): VNode {
-  const { pageState, pageStateSetter } = usePageContext();
-  const backend = useBackendContext();
   const { i18n } = useTranslationContext();
 
   const captchaNumbers = useMemo(() => {
@@ -111,35 +115,29 @@ export function WithdrawalConfirmationQuestion({
                     e.preventDefault();
                     try {
                       await confirmWithdrawal(withdrawalId);
-                      pageStateSetter((prevState) => {
-                        const { talerWithdrawUri, ...rest } = prevState;
-                        return {
-                          ...rest,
-                          info: i18n.str`Withdrawal confirmed!`,
-                        };
-                      });
+                      onConfirmed();
                     } catch (error) {
-                      pageStateSetter((prevState) => ({
-                        ...prevState,
-                        error: {
-                          title: i18n.str`Could not confirm the withdrawal`,
-                          description: (error as any).error.description,
-                          debug: JSON.stringify(error),
-                        },
-                      }));
+                      if (error instanceof RequestError) {
+                        onError(
+                          buildRequestErrorMessage(i18n, error.cause, {
+                            onClientError: (status) =>
+                              status === HttpStatusCode.Conflict
+                                ? i18n.str`The withdrawal has been aborted 
previously and can't be confirmed`
+                                : status === HttpStatusCode.UnprocessableEntity
+                                ? i18n.str`The withdraw operation cannot be 
confirmed because no exchange and reserve public key selection happened before`
+                                : undefined,
+                          }),
+                        );
+                      } else {
+                        onError({
+                          title: i18n.str`Operation failed, please report`,
+                          description:
+                            error instanceof Error
+                              ? error.message
+                              : JSON.stringify(error),
+                        });
+                      }
                     }
-                    // if (
-                    //   captchaAnswer ==
-                    //   (captchaNumbers.a + captchaNumbers.b).toString()
-                    // ) {
-                    //   await confirmWithdrawalCall(
-                    //     backend.state,
-                    //     pageState.withdrawalId,
-                    //     pageStateSetter,
-                    //     i18n,
-                    //   );
-                    //   return;
-                    // }
                   }}
                 >
                   {i18n.str`Confirm`}
@@ -151,29 +149,27 @@ export function WithdrawalConfirmationQuestion({
                     e.preventDefault();
                     try {
                       await abortWithdrawal(withdrawalId);
-                      pageStateSetter((prevState) => {
-                        const { talerWithdrawUri, ...rest } = prevState;
-                        return {
-                          ...rest,
-                          info: i18n.str`Withdrawal confirmed!`,
-                        };
-                      });
+                      onAborted();
                     } catch (error) {
-                      pageStateSetter((prevState) => ({
-                        ...prevState,
-                        error: {
-                          title: i18n.str`Could not confirm the withdrawal`,
-                          description: (error as any).error.description,
-                          debug: JSON.stringify(error),
-                        },
-                      }));
+                      if (error instanceof RequestError) {
+                        onError(
+                          buildRequestErrorMessage(i18n, error.cause, {
+                            onClientError: (status) =>
+                              status === HttpStatusCode.Conflict
+                                ? i18n.str`The reserve operation has been 
confirmed previously and can't be aborted`
+                                : undefined,
+                          }),
+                        );
+                      } else {
+                        onError({
+                          title: i18n.str`Operation failed, please report`,
+                          description:
+                            error instanceof Error
+                              ? error.message
+                              : JSON.stringify(error),
+                        });
+                      }
                     }
-                    // await abortWithdrawalCall(
-                    //   backend.state,
-                    //   pageState.withdrawalId,
-                    //   pageStateSetter,
-                    //   i18n,
-                    // );
                   }}
                 >
                   {i18n.str`Cancel`}
@@ -195,199 +191,3 @@ export function WithdrawalConfirmationQuestion({
     </Fragment>
   );
 }
-
-/**
- * This function confirms a withdrawal operation AFTER
- * the wallet has given the exchange's payment details
- * to the bank (via the Integration API).  Such details
- * can be given by scanning a QR code or by passing the
- * raw taler://withdraw-URI to the CLI wallet.
- *
- * This function will set the confirmation status in the
- * 'page state' and let the related components refresh.
- */
-// async function confirmWithdrawalCall(
-//   backendState: BackendState,
-//   withdrawalId: string | undefined,
-//   pageStateSetter: StateUpdater<PageStateType>,
-//   i18n: InternationalizationAPI,
-// ): Promise<void> {
-//   if (backendState.status === "loggedOut") {
-//     logger.error("No credentials found.");
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`No credentials found.`,
-//       },
-//     }));
-//     return;
-//   }
-//   if (typeof withdrawalId === "undefined") {
-//     logger.error("No withdrawal ID found.");
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`No withdrawal ID found.`,
-//       },
-//     }));
-//     return;
-//   }
-//   let res: Response;
-//   try {
-//     const { username, password } = backendState;
-//     const headers = prepareHeaders(username, password);
-//     /**
-//      * NOTE: tests show that when a same object is being
-//      * POSTed, caching might prevent same requests from being
-//      * made.  Hence, trying to POST twice the same amount might
-//      * get silently ignored.
-//      *
-//      * headers.append("cache-control", "no-store");
-//      * headers.append("cache-control", "no-cache");
-//      * headers.append("pragma", "no-cache");
-//      * */
-
-//     // Backend URL must have been stored _with_ a final slash.
-//     const url = new URL(
-//       
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
-//       backendState.url,
-//     );
-//     res = await fetch(url.href, {
-//       method: "POST",
-//       headers,
-//     });
-//   } catch (error) {
-//     logger.error("Could not POST withdrawal confirmation to the bank", 
error);
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`Could not confirm the withdrawal`,
-//         description: (error as any).error.description,
-//         debug: JSON.stringify(error),
-//       },
-//     }));
-//     return;
-//   }
-//   if (!res || !res.ok) {
-//     const response = await res.json();
-//     // assume not ok if res is null
-//     logger.error(
-//       `Withdrawal confirmation gave response error (${res.status})`,
-//       res.statusText,
-//     );
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`Withdrawal confirmation gave response error`,
-//         debug: JSON.stringify(response),
-//       },
-//     }));
-//     return;
-//   }
-//   logger.trace("Withdrawal operation confirmed!");
-//   pageStateSetter((prevState) => {
-//     const { talerWithdrawUri, ...rest } = prevState;
-//     return {
-//       ...rest,
-
-//       info: i18n.str`Withdrawal confirmed!`,
-//     };
-//   });
-// }
-
-// /**
-//  * Abort a withdrawal operation via the Access API's /abort.
-//  */
-// async function abortWithdrawalCall(
-//   backendState: BackendState,
-//   withdrawalId: string | undefined,
-//   pageStateSetter: StateUpdater<PageStateType>,
-//   i18n: InternationalizationAPI,
-// ): Promise<void> {
-//   if (backendState.status === "loggedOut") {
-//     logger.error("No credentials found.");
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`No credentials found.`,
-//       },
-//     }));
-//     return;
-//   }
-//   if (typeof withdrawalId === "undefined") {
-//     logger.error("No withdrawal ID found.");
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`No withdrawal ID found.`,
-//       },
-//     }));
-//     return;
-//   }
-//   let res: Response;
-//   try {
-//     const { username, password } = backendState;
-//     const headers = prepareHeaders(username, password);
-//     /**
-//      * NOTE: tests show that when a same object is being
-//      * POSTed, caching might prevent same requests from being
-//      * made.  Hence, trying to POST twice the same amount might
-//      * get silently ignored.  Needs more observation!
-//      *
-//      * headers.append("cache-control", "no-store");
-//      * headers.append("cache-control", "no-cache");
-//      * headers.append("pragma", "no-cache");
-//      * */
-
-//     // Backend URL must have been stored _with_ a final slash.
-//     const url = new URL(
-//       
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
-//       backendState.url,
-//     );
-//     res = await fetch(url.href, { method: "POST", headers });
-//   } catch (error) {
-//     logger.error("Could not abort the withdrawal", error);
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`Could not abort the withdrawal.`,
-//         description: (error as any).error.description,
-//         debug: JSON.stringify(error),
-//       },
-//     }));
-//     return;
-//   }
-//   if (!res.ok) {
-//     const response = await res.json();
-//     logger.error(
-//       `Withdrawal abort gave response error (${res.status})`,
-//       res.statusText,
-//     );
-//     pageStateSetter((prevState) => ({
-//       ...prevState,
-
-//       error: {
-//         title: i18n.str`Withdrawal abortion failed.`,
-//         description: response.error.description,
-//         debug: JSON.stringify(response),
-//       },
-//     }));
-//     return;
-//   }
-//   logger.trace("Withdrawal operation aborted!");
-//   pageStateSetter((prevState) => {
-//     const { ...rest } = prevState;
-//     return {
-//       ...rest,
-
-//       info: i18n.str`Withdrawal aborted!`,
-//     };
-//   });
-// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx 
b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 5169fc00f..1a4157d06 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -21,7 +21,7 @@ import {
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { Loading } from "../components/Loading.js";
-import { usePageContext } from "../context/pageState.js";
+import { PageStateType } from "../context/pageState.js";
 import { useWithdrawalDetails } from "../hooks/access.js";
 import { QrCodeSection } from "./QrCodeSection.js";
 import { WithdrawalConfirmationQuestion } from 
"./WithdrawalConfirmationQuestion.js";
@@ -32,7 +32,9 @@ interface Props {
   account: string;
   withdrawalId: string;
   talerWithdrawUri: string;
-  onAbort: () => void;
+  onError: (e: PageStateType["error"]) => void;
+  onAborted: () => void;
+  onConfirmed: () => void;
   onLoadNotOk: <T>(
     error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
   ) => VNode;
@@ -46,10 +48,12 @@ export function WithdrawalQRCode({
   account,
   withdrawalId,
   talerWithdrawUri,
-  onAbort,
+  onConfirmed,
+  onAborted,
+  onError,
   onLoadNotOk,
 }: Props): VNode {
-  logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`);
+  const { i18n } = useTranslationContext();
 
   const result = useWithdrawalDetails(account, withdrawalId);
   if (!result.ok) {
@@ -61,18 +65,24 @@ export function WithdrawalQRCode({
   if (data.aborted) {
     // signal that this withdrawal is aborted
     // will redirect to account info
-    onAbort();
+    onAborted();
     return <Loading />;
   }
 
   const parsedUri = parseWithdrawUri(talerWithdrawUri);
   if (!parsedUri) {
-    throw Error("can't parse withdrawal URI");
+    onError({
+      title: i18n.str`The Withdrawal URI is not valid: "${talerWithdrawUri}"`,
+    });
+    return <Loading />;
   }
 
   if (!data.selection_done) {
     return (
-      <QrCodeSection talerWithdrawUri={talerWithdrawUri} onAbort={onAbort} />
+      <QrCodeSection
+        talerWithdrawUri={talerWithdrawUri}
+        onAborted={onAborted}
+      />
     );
   }
 
@@ -80,8 +90,10 @@ export function WithdrawalQRCode({
   // user to authorize the operation (here CAPTCHA).
   return (
     <WithdrawalConfirmationQuestion
-      account={account}
       withdrawalId={parsedUri.withdrawalOperationId}
+      onError={onError}
+      onConfirmed={onConfirmed}
+      onAborted={onAborted}
     />
   );
 }
diff --git a/packages/demobank-ui/src/scss/demo.scss 
b/packages/demobank-ui/src/scss/demo.scss
index 3b7acaa1f..cd676f8d9 100644
--- a/packages/demobank-ui/src/scss/demo.scss
+++ b/packages/demobank-ui/src/scss/demo.scss
@@ -66,14 +66,14 @@ body {
   width: 100vw;
   backdrop-filter: blur(10px);
   opacity: 1;
-  z-index: 10000;
+  z-index: 100;
 }
 
 nav {
   left: 1vw;
   position: relative;
   background: #0042b2;
-  z-index: 10000;
+  z-index: 100;
 }
 
 nav a,
diff --git a/packages/demobank-ui/src/utils.ts 
b/packages/demobank-ui/src/utils.ts
index 49b9ac276..81dd450a4 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -14,7 +14,13 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  ErrorType,
+  HttpError,
+  useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { ErrorMessage } from "./context/pageState.js";
 
 /**
  * Validate (the number part of) an amount.  If needed,
@@ -58,6 +64,13 @@ export type WithIntermediate<Type extends object> = {
     ? WithIntermediate<Type[prop]>
     : Type[prop] | undefined;
 };
+export type RecursivePartial<T> = {
+  [P in keyof T]?: T[P] extends (infer U)[]
+    ? RecursivePartial<U>[]
+    : T[P] extends object
+    ? RecursivePartial<T[P]>
+    : T[P];
+};
 
 export enum TanChannel {
   SMS = "sms",
@@ -99,3 +112,52 @@ export enum CashoutStatus {
 
 export const PAGE_SIZE = 20;
 export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
+
+export function buildRequestErrorMessage(
+  i18n: ReturnType<typeof useTranslationContext>["i18n"],
+  cause: HttpError<SandboxBackend.SandboxError>,
+  specialCases: {
+    onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
+    onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
+  } = {},
+): ErrorMessage {
+  let result: ErrorMessage;
+  switch (cause.type) {
+    case ErrorType.TIMEOUT: {
+      result = {
+        title: i18n.str`Request timeout`,
+      };
+      break;
+    }
+    case ErrorType.CLIENT: {
+      const title =
+        specialCases.onClientError && specialCases.onClientError(cause.status);
+      result = {
+        title: title ? title : i18n.str`The server didn't accept the request`,
+        description: cause.payload.error.description,
+        debug: JSON.stringify(cause),
+      };
+      break;
+    }
+    case ErrorType.SERVER: {
+      const title =
+        specialCases.onServerError && specialCases.onServerError(cause.status);
+      result = {
+        title: title
+          ? title
+          : i18n.str`The server had problems processing the request`,
+        description: cause.payload.error.description,
+        debug: JSON.stringify(cause),
+      };
+      break;
+    }
+    case ErrorType.UNEXPECTED: {
+      result = {
+        title: i18n.str`Unexpected error`,
+        debug: JSON.stringify(cause),
+      };
+      break;
+    }
+  }
+  return result;
+}
diff --git a/packages/web-util/src/utils/request.ts 
b/packages/web-util/src/utils/request.ts
index 3d91024dc..c7bca2de9 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -18,15 +18,18 @@ import { HttpStatusCode } from "@gnu-taler/taler-util";
 import { base64encode } from "./base64.js";
 
 export enum ErrorType {
-  CLIENT, SERVER, TIMEOUT, UNEXPECTED
+  CLIENT,
+  SERVER,
+  TIMEOUT,
+  UNEXPECTED,
 }
 
 /**
- * 
+ *
  * @param baseUrl URL where the service is located
- * @param endpoint endpoint of the service to be called 
+ * @param endpoint endpoint of the service to be called
  * @param options auth, method and params
- * @returns 
+ * @returns
  */
 export async function defaultRequestHandler<T>(
   baseUrl: string,
@@ -35,11 +38,14 @@ export async function defaultRequestHandler<T>(
 ): Promise<HttpResponseOk<T>> {
   const requestHeaders: Record<string, string> = {};
   if (options.token) {
-    requestHeaders.Authorization = `Bearer ${options.token}`
+    requestHeaders.Authorization = `Bearer ${options.token}`;
   } else if (options.basicAuth) {
-    requestHeaders.Authorization = `Basic 
${base64encode(`${options.basicAuth.username}:${options.basicAuth.password}`)}`
+    requestHeaders.Authorization = `Basic ${base64encode(
+      `${options.basicAuth.username}:${options.basicAuth.password}`,
+    )}`;
   }
-  requestHeaders["Content-Type"] = options.contentType === "json" ? 
"application/json" : "text/plain"
+  requestHeaders["Content-Type"] =
+    options.contentType === "json" ? "application/json" : "text/plain";
 
   const requestMethod = options?.method ?? "GET";
   const requestBody = options?.data;
@@ -178,15 +184,23 @@ export type HttpError<ErrorDetail> =
   | HttpResponseServerError<ErrorDetail>
   | HttpResponseUnexpectedError;
 
-
 export interface HttpResponseServerError<ErrorDetail> {
   ok?: false;
   loading?: false;
+  /**
+   * @deprecated use status
+   */
   clientError?: false;
+  /**
+   * @deprecated use status
+   */
   serverError: true;
-  type: ErrorType.SERVER,
-
+  type: ErrorType.SERVER;
+  /**
+   * @deprecated use payload
+   */
   error: ErrorDetail;
+  payload: ErrorDetail;
   status: HttpStatusCode;
   message: string;
   info?: RequestInfo;
@@ -194,12 +208,18 @@ export interface HttpResponseServerError<ErrorDetail> {
 interface HttpRequestTimeoutError {
   ok?: false;
   loading?: false;
+  /**
+   * @deprecated use type
+   */
   clientError: true;
+  /**
+   * @deprecated use type
+   */
   serverError?: false;
-  type: ErrorType.TIMEOUT,
+  type: ErrorType.TIMEOUT;
 
   info?: RequestInfo;
-  error: undefined,
+  error: undefined;
 
   isUnauthorized: false;
   isNotfound: false;
@@ -208,28 +228,54 @@ interface HttpRequestTimeoutError {
 interface HttpResponseClientError<ErrorDetail> {
   ok?: false;
   loading?: false;
+  /**
+   * @deprecated use type
+   */
   clientError: true;
+  /**
+   * @deprecated use type
+   */
   serverError?: false;
-  type: ErrorType.CLIENT,
+  type: ErrorType.CLIENT;
 
   info?: RequestInfo;
+  /**
+   * @deprecated use status
+   */
   isUnauthorized: boolean;
+  /**
+   * @deprecated use status
+   */
   isNotfound: boolean;
   status: HttpStatusCode;
+  /**
+   * @deprecated use payload
+   */
   error: ErrorDetail;
+  payload: ErrorDetail;
   message: string;
 }
 
 interface HttpResponseUnexpectedError {
   ok?: false;
   loading?: false;
+  /**
+   * @deprecated use type
+   */
   clientError?: false;
+  /**
+   * @deprecated use type
+   */
   serverError?: false;
-  type: ErrorType.UNEXPECTED,
+  type: ErrorType.UNEXPECTED;
 
   info?: RequestInfo;
   status?: HttpStatusCode;
+  /**
+   * @deprecated use exception
+   */
   error: unknown;
+  exception: unknown;
   message: string;
 }
 
@@ -240,9 +286,9 @@ export class RequestError<ErrorDetail> extends Error {
   info: HttpError<ErrorDetail>;
   cause: HttpError<ErrorDetail>;
   constructor(d: HttpError<ErrorDetail>) {
-    super(d.message)
-    this.info = d
-    this.cause = d
+    super(d.message);
+    this.info = d;
+    this.cause = d;
   }
 }
 
@@ -252,13 +298,13 @@ export interface RequestOptions {
   method?: Methods;
   token?: string;
   basicAuth?: {
-    username: string,
-    password: string,
-  }
+    username: string;
+    password: string;
+  };
   data?: any;
   params?: unknown;
-  timeout?: number,
-  contentType?: "text" | "json"
+  timeout?: number;
+  contentType?: "text" | "json";
 }
 
 async function buildRequestOk<T>(
@@ -312,7 +358,8 @@ async function buildRequestFailed<ErrorDetail>(
         status,
         info,
         message: data?.hint,
-        error: data,
+        error: data, // remove this
+        payload: data,
       };
       return error;
     }
@@ -323,7 +370,8 @@ async function buildRequestFailed<ErrorDetail>(
         status,
         info,
         message: `${data?.hint} (code ${data?.code})`,
-        error: data,
+        error: data, //remove this
+        payload: data,
       };
       return error;
     }
@@ -331,7 +379,8 @@ async function buildRequestFailed<ErrorDetail>(
       info,
       type: ErrorType.UNEXPECTED,
       status,
-      error: {},
+      error: {}, // remove this
+      exception: undefined,
       message: "NOT DEFINED",
     };
   } catch (ex) {
@@ -340,6 +389,7 @@ async function buildRequestFailed<ErrorDetail>(
       status,
       type: ErrorType.UNEXPECTED,
       error: ex,
+      exception: ex,
       message: "NOT DEFINED",
     };
 

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