From a8c5a9696c1735a178158cbc9ac4f9bb4b6f013d Mon Sep 17 00:00:00 2001
From: Sebastian <sebasjm@gmail.com>
Date: Wed, 8 Feb 2023 17:41:19 -0300
Subject: impl accout management and refactor

---
 .../src/pages/PaytoWireTransferForm.tsx            | 317 +++++++--------------
 1 file changed, 102 insertions(+), 215 deletions(-)

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

diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 46b006880..d859b1cc7 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -14,64 +14,81 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, Logger, parsePaytoUri } from "@gnu-taler/taler-util";
-import { useLocalStorage } 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 {
+  Amounts,
+  buildPayto,
+  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 { useAccessAPI } from "../hooks/access.js";
 import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders, undefinedIfEmpty } from "../utils.js";
+import { undefinedIfEmpty } from "../utils.js";
 import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 const logger = new Logger("PaytoWireTransferForm");
 
 export function PaytoWireTransferForm({
   focus,
+  onError,
+  onSuccess,
   currency,
 }: {
   focus?: boolean;
-  currency?: string;
+  onError: (e: PageStateType["error"]) => void;
+  onSuccess: () => void;
+  currency: string;
 }): VNode {
   const backend = useBackendContext();
-  const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
+  // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
 
-  const [submitData, submitDataSetter] = useWireTransferRequestType();
+  const [isRawPayto, setIsRawPayto] = useState(false);
+  // const [submitData, submitDataSetter] = useWireTransferRequestType();
+  const [iban, setIban] = useState<string | undefined>(undefined);
+  const [subject, setSubject] = useState<string | undefined>(undefined);
+  const [amount, setAmount] = useState<string | undefined>(undefined);
 
   const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
     undefined,
   );
   const { i18n } = useTranslationContext();
   const ibanRegex = "^[A-Z][A-Z][0-9]+$";
-  let transactionData: TransactionRequestType;
   const ref = useRef<HTMLInputElement>(null);
   useEffect(() => {
     if (focus) ref.current?.focus();
-  }, [focus, pageState.isRawPayto]);
+  }, [focus, isRawPayto]);
 
   let parsedAmount = undefined;
+  const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
 
   const errorsWire = undefinedIfEmpty({
-    iban: !submitData?.iban
+    iban: !iban
       ? i18n.str`Missing IBAN`
-      : !/^[A-Z0-9]*$/.test(submitData.iban)
+      : !IBAN_REGEX.test(iban)
       ? i18n.str`IBAN should have just uppercased letters and numbers`
       : undefined,
-    subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
-    amount: !submitData?.amount
+    subject: !subject ? i18n.str`Missing subject` : undefined,
+    amount: !amount
       ? i18n.str`Missing amount`
-      : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
+      : !(parsedAmount = Amounts.parse(`${currency}:${amount}`))
       ? i18n.str`Amount is not valid`
       : Amounts.isZero(parsedAmount)
       ? i18n.str`Should be greater than 0`
       : undefined,
   });
 
-  if (!pageState.isRawPayto)
+  const { createTransaction } = useAccessAPI();
+
+  if (!isRawPayto)
     return (
       <div>
         <form
@@ -90,21 +107,18 @@ export function PaytoWireTransferForm({
               type="text"
               id="iban"
               name="iban"
-              value={submitData?.iban ?? ""}
+              value={iban ?? ""}
               placeholder="CC0123456789"
               required
               pattern={ibanRegex}
               onInput={(e): void => {
-                submitDataSetter((submitData) => ({
-                  ...submitData,
-                  iban: e.currentTarget.value,
-                }));
+                setIban(e.currentTarget.value);
               }}
             />
             <br />
             <ShowInputErrorLabel
               message={errorsWire?.iban}
-              isDirty={submitData?.iban !== undefined}
+              isDirty={iban !== undefined}
             />
             <br />
             <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
@@ -113,19 +127,16 @@ export function PaytoWireTransferForm({
               name="subject"
               id="subject"
               placeholder="subject"
-              value={submitData?.subject ?? ""}
+              value={subject ?? ""}
               required
               onInput={(e): void => {
-                submitDataSetter((submitData) => ({
-                  ...submitData,
-                  subject: e.currentTarget.value,
-                }));
+                setSubject(e.currentTarget.value);
               }}
             />
             <br />
             <ShowInputErrorLabel
               message={errorsWire?.subject}
-              isDirty={submitData?.subject !== undefined}
+              isDirty={subject !== undefined}
             />
             <br />
             <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
@@ -146,18 +157,15 @@ export function PaytoWireTransferForm({
                 id="amount"
                 placeholder="amount"
                 required
-                value={submitData?.amount ?? ""}
+                value={amount ?? ""}
                 onInput={(e): void => {
-                  submitDataSetter((submitData) => ({
-                    ...submitData,
-                    amount: e.currentTarget.value,
-                  }));
+                  setAmount(e.currentTarget.value);
                 }}
               />
             </div>
             <ShowInputErrorLabel
               message={errorsWire?.amount}
-              isDirty={submitData?.amount !== undefined}
+              isDirty={amount !== undefined}
             />
           </p>
 
@@ -169,43 +177,28 @@ export function PaytoWireTransferForm({
               value="Send"
               onClick={async (e) => {
                 e.preventDefault();
-                if (
-                  typeof submitData === "undefined" ||
-                  typeof submitData.iban === "undefined" ||
-                  submitData.iban === "" ||
-                  typeof submitData.subject === "undefined" ||
-                  submitData.subject === "" ||
-                  typeof submitData.amount === "undefined" ||
-                  submitData.amount === ""
-                ) {
-                  logger.error("Not all the fields were given.");
-                  pageStateSetter((prevState: PageStateType) => ({
-                    ...prevState,
-
-                    error: {
-                      title: i18n.str`Field(s) missing.`,
-                    },
-                  }));
+                if (!(iban && subject && amount)) {
                   return;
                 }
-                transactionData = {
-                  paytoUri: `payto://iban/${
-                    submitData.iban
-                  }?message=${encodeURIComponent(submitData.subject)}`,
-                  amount: `${currency}:${submitData.amount}`,
-                };
-                return await createTransactionCall(
-                  transactionData,
-                  backend.state,
-                  pageStateSetter,
-                  () =>
-                    submitDataSetter((p) => ({
-                      amount: undefined,
-                      iban: undefined,
-                      subject: undefined,
-                    })),
-                  i18n,
-                );
+                const ibanPayto = buildPayto("iban", iban, undefined);
+                ibanPayto.params.message = encodeURIComponent(subject);
+                const paytoUri = stringifyPaytoUri(ibanPayto);
+
+                await createTransaction({
+                  paytoUri,
+                  amount: `${currency}:${amount}`,
+                });
+                // return await createTransactionCall(
+                //   transactionData,
+                //   backend.state,
+                //   pageStateSetter,
+                //   () => {
+                //     setAmount(undefined);
+                //     setIban(undefined);
+                //     setSubject(undefined);
+                //   },
+                //   i18n,
+                // );
               }}
             />
             <input
@@ -214,11 +207,9 @@ export function PaytoWireTransferForm({
               value="Clear"
               onClick={async (e) => {
                 e.preventDefault();
-                submitDataSetter((p) => ({
-                  amount: undefined,
-                  iban: undefined,
-                  subject: undefined,
-                }));
+                setAmount(undefined);
+                setIban(undefined);
+                setSubject(undefined);
               }}
             />
           </p>
@@ -227,11 +218,7 @@ export function PaytoWireTransferForm({
           <a
             href="/account"
             onClick={() => {
-              logger.trace("switch to raw payto form");
-              pageStateSetter((prevState) => ({
-                ...prevState,
-                isRawPayto: true,
-              }));
+              setIsRawPayto(true);
             }}
           >
             {i18n.str`Want to try the raw payto://-format?`}
@@ -240,11 +227,23 @@ export function PaytoWireTransferForm({
       </div>
     );
 
+  const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
+
   const errorsPayto = undefinedIfEmpty({
     rawPaytoInput: !rawPaytoInput
-      ? i18n.str`Missing payto address`
-      : !parsePaytoUri(rawPaytoInput)
-      ? i18n.str`Payto does not follow the pattern`
+      ? i18n.str`required`
+      : !parsed
+      ? i18n.str`does not follow the pattern`
+      : !parsed.params.amount
+      ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
+      : Amounts.parse(parsed.params.amount) === undefined
+      ? i18n.str`the amount is not valid`
+      : !parsed.params.message
+      ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
+      : !parsed.isKnown || parsed.targetType !== "iban"
+      ? i18n.str`only "IBAN" target are supported`
+      : !IBAN_REGEX.test(parsed.iban)
+      ? i18n.str`IBAN should have just uppercased letters and numbers`
       : undefined,
   });
 
@@ -296,25 +295,29 @@ export function PaytoWireTransferForm({
             disabled={!!errorsPayto}
             value={i18n.str`Send`}
             onClick={async () => {
-              // empty string evaluates to false.
               if (!rawPaytoInput) {
                 logger.error("Didn't get any raw Payto string!");
                 return;
               }
-              transactionData = { paytoUri: rawPaytoInput };
-              if (
-                typeof transactionData.paytoUri === "undefined" ||
-                transactionData.paytoUri.length === 0
-              )
-                return;
 
-              return await createTransactionCall(
-                transactionData,
-                backend.state,
-                pageStateSetter,
-                () => rawPaytoInputSetter(undefined),
-                i18n,
-              );
+              try {
+                await createTransaction({
+                  paytoUri: rawPaytoInput,
+                });
+                onSuccess();
+                rawPaytoInputSetter(undefined);
+              } catch (error) {
+                if (error instanceof RequestError) {
+                  const errorData: SandboxBackend.SandboxError =
+                    error.info.error;
+
+                  onError({
+                    title: i18n.str`Transfer creation gave response error`,
+                    description: errorData.error.description,
+                    debug: JSON.stringify(errorData),
+                  });
+                }
+              }
             }}
           />
         </p>
@@ -322,11 +325,7 @@ export function PaytoWireTransferForm({
           <a
             href="/account"
             onClick={() => {
-              logger.trace("switch to wire-transfer-form");
-              pageStateSetter((prevState) => ({
-                ...prevState,
-                isRawPayto: false,
-              }));
+              setIsRawPayto(false);
             }}
           >
             {i18n.str`Use wire-transfer form?`}
@@ -336,115 +335,3 @@ export function PaytoWireTransferForm({
     </div>
   );
 }
-
-/**
- * Stores in the state a object representing a wire transfer,
- * in order to avoid losing the handle of the data entered by
- * the user in <input> fields.  FIXME: name not matching the
- * purpose, as this is not a HTTP request body but rather the
- * state of the <input>-elements.
- */
-type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
-function useWireTransferRequestType(
-  state?: WireTransferRequestType,
-): [WireTransferRequestTypeOpt, StateUpdater<WireTransferRequestTypeOpt>] {
-  const ret = useLocalStorage(
-    "wire-transfer-request-state",
-    JSON.stringify(state),
-  );
-  const retObj: WireTransferRequestTypeOpt = ret[0]
-    ? JSON.parse(ret[0])
-    : ret[0];
-  const retSetter: StateUpdater<WireTransferRequestTypeOpt> = function (val) {
-    const newVal =
-      val instanceof Function
-        ? JSON.stringify(val(retObj))
-        : JSON.stringify(val);
-    ret[1](newVal);
-  };
-  return [retObj, retSetter];
-}
-
-/**
- * This function creates a new transaction.  It reads a Payto
- * address entered by the user and POSTs it to the bank.  No
- * sanity-check of the input happens before the POST as this is
- * already conducted by the backend.
- */
-async function createTransactionCall(
-  req: TransactionRequestType,
-  backendState: BackendState,
-  pageStateSetter: StateUpdater<PageStateType>,
-  /**
-   * Optional since the raw payto form doesn't have
-   * a stateful management of the input data yet.
-   */
-  cleanUpForm: () => void,
-  i18n: InternationalizationAPI,
-): Promise<void> {
-  if (backendState.status === "loggedOut") {
-    logger.error("No credentials found.");
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`No credentials found.`,
-      },
-    }));
-    return;
-  }
-  let res: Response;
-  try {
-    const { username, password } = backendState;
-    const headers = prepareHeaders(username, password);
-    const url = new URL(
-      `access-api/accounts/${backendState.username}/transactions`,
-      backendState.url,
-    );
-    res = await fetch(url.href, {
-      method: "POST",
-      headers,
-      body: JSON.stringify(req),
-    });
-  } catch (error) {
-    logger.error("Could not POST transaction request to the bank", error);
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Could not create the wire transfer`,
-        description: (error as any).error.description,
-        debug: JSON.stringify(error),
-      },
-    }));
-    return;
-  }
-  // POST happened, status not sure yet.
-  if (!res.ok) {
-    const response = await res.json();
-    logger.error(
-      `Transfer creation gave response error: ${response} (${res.status})`,
-    );
-    pageStateSetter((prevState) => ({
-      ...prevState,
-
-      error: {
-        title: i18n.str`Transfer creation gave response error`,
-        description: response.error.description,
-        debug: JSON.stringify(response),
-      },
-    }));
-    return;
-  }
-  // status is 200 OK here, tell the user.
-  logger.trace("Wire transfer created!");
-  pageStateSetter((prevState) => ({
-    ...prevState,
-
-    info: i18n.str`Wire transfer created!`,
-  }));
-
-  // Only at this point the input data can
-  // be discarded.
-  cleanUpForm();
-}
-- 
cgit v1.2.3

