From 9922192b0dba2e479b5af3e29c1d44b98e4d29d7 Mon Sep 17 00:00:00 2001
From: Sebastian <sebasjm@gmail.com>
Date: Tue, 28 Feb 2023 19:03:43 -0300
Subject: fix #7729

---
 packages/demobank-ui/src/pages/BusinessAccount.tsx | 311 +++++++++------------
 1 file changed, 130 insertions(+), 181 deletions(-)

(limited to 'packages/demobank-ui/src/pages/BusinessAccount.tsx')

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),
                     });
                   }
                 }
-- 
cgit v1.2.3

