gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: fix #7729


From: gnunet
Subject: [taler-wallet-core] 02/02: fix #7729
Date: Tue, 28 Feb 2023 23:03:56 +0100

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

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

commit 9922192b0dba2e479b5af3e29c1d44b98e4d29d7
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Feb 28 19:03:43 2023 -0300

    fix #7729
---
 .../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 ++++-
 16 files changed, 567 insertions(+), 703 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;
+}

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