gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-backoffice] branch master updated: Add wire transfer for


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: Add wire transfer form
Date: Mon, 31 Jan 2022 12:49:35 +0100

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

ms pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new e84b098  Add wire transfer form
e84b098 is described below

commit e84b098ea9b2c60c7ba1db6c82f65ca2079ddb9c
Author: ms <ms@taler.net>
AuthorDate: Mon Jan 31 12:49:27 2022 +0100

    Add wire transfer form
---
 packages/bank/src/pages/home/index.tsx    | 185 +++++++++++++++++++++++++++---
 packages/bank/tests/__tests__/homepage.js |  56 ++++++---
 2 files changed, 210 insertions(+), 31 deletions(-)

diff --git a/packages/bank/src/pages/home/index.tsx 
b/packages/bank/src/pages/home/index.tsx
index 7c99869..528ed02 100644
--- a/packages/bank/src/pages/home/index.tsx
+++ b/packages/bank/src/pages/home/index.tsx
@@ -10,7 +10,7 @@ import "../../scss/main.scss";
 /**
  * ToDo:
  *
- * - the page needs a "home" button that either redirects
+ * - the page needs a "home" button that either redirects to
  *   the profile page (when the user is logged in), or to
  *   the very initial home page. 
  *
@@ -36,6 +36,13 @@ interface BackendStateType {
   password: string;
 }
 
+/**
+ * Request body of POST /transactions.
+ */
+interface TransactionRequestType {
+  paytoUri: string;
+}
+
 /**
  * Request body of /register.
  */
@@ -54,8 +61,9 @@ interface PageStateType {
   error?: string;
   talerWithdrawUri?: string;
   withdrawalOutcome?: string;
+  transferOutcome?: string;
   /**
-   * Not strictly a presentational element, could
+   * Not strictly a presentational value, could
    * be moved in a future "withdrawal state" object.
    */
   withdrawalId?: string;
@@ -73,6 +81,53 @@ interface AccountStateType {
  * Helpers. *
  ***********/
 
+/**
+ * Get username from the backend state, and throw
+ * exception if not found.
+ */
+function getUsername(backendState: BackendStateTypeOpt): string {
+  if (typeof backendState === "undefined") {
+    throw Error("Username can't be found in a undefined backend state.")
+  }
+  return backendState.username;
+}
+
+/**
+ * Helps extracting the credentials from the state
+ * and wraps the actual call to 'fetch'.  Should be
+ * enclosed in a try-catch block by the caller.
+ */
+async function postToBackend(
+  uri: string,
+  backendState: BackendStateTypeOpt,
+  body: string
+): Promise<any> {
+  if (typeof backendState === "undefined") {
+    throw Error("Credentials can't be found in a undefined backend state.")
+  }
+  const { username, password } = backendState;
+  let 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(uri, backendState.url)
+  return await fetch(url.href, {
+      method: 'POST',
+      headers: headers,
+      body: body,
+    }
+  );
+}
+
 function useTransactionPageNumber(): [number, StateUpdater<number>] {
 
   const ret = useNotNullLocalStorage("transaction-page", "0");
@@ -163,7 +218,7 @@ function usePageState(
   const retObj: PageStateType = JSON.parse(ret[0]);
   const retSetter: StateUpdater<PageStateType> = function(val) {
     const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : 
JSON.stringify(val)
-    console.log("setting new page state", newVal)
+    console.log("Setting new page state", newVal)
     ret[1](newVal)
   }
   return [retObj, retSetter];
@@ -334,6 +389,49 @@ async function confirmWithdrawalCall(
   }
 }
 
+/**
+ * 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: BackendStateTypeOpt,
+  pageStateSetter: StateUpdater<PageStateType>
+) {
+  try {
+    var res = await postToBackend(
+      `access-api/accounts/${getUsername(backendState)}/withdrawals`,
+      backendState,
+      JSON.stringify(req)
+    )   
+  }
+  catch (error) {
+    console.log("Could not POST transaction request to the bank", error);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+      hasError: true,
+      error: `Could not create the wire transfer: ${error}`}))
+    return;
+  }
+  // POST happened, status not sure yet.
+  if (!res.ok) {
+    console.log(`Transfer creation gave response error (${res.status})`, 
res.statusText);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+      hasError: true,
+      error: `Transfer creation gave response error (${res.status})`}))
+      return;
+  }
+  // status is 200 OK here, tell the user.
+  console.log("Wire transfer created!");
+  pageStateSetter((prevState) => ({
+    ...prevState,
+    transferOutcome: "Wire transfer created!"
+  }))
+}
+
 /**
  * This function creates a withdrawal operation via the Access API.
  *
@@ -341,8 +439,8 @@ async function confirmWithdrawalCall(
  * user should receive a QR code of the "taler://withdraw/" type and
  * supposed to scan it with their phone.
  *
- * After the scan, the page should refresh itself and inform the user
- * about the operation's outcome.
+ * 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,
@@ -394,15 +492,15 @@ async function createWithdrawalCall(
       hasError: true,
       error: `Withdrawal creation gave response error (${res.status})`}))
       return;
-  } else {
-    console.log("Withdrawal operation created!");
-    let resp = await res.json();
-    pageStateSetter((prevState) => ({
-      ...prevState,
-      withdrawalInProgress: true,
-      talerWithdrawUri: resp.taler_withdraw_uri,
-      withdrawalId: resp.withdrawal_id}))
-  }
+  } 
+
+  console.log("Withdrawal operation created!");
+  let resp = await res.json();
+  pageStateSetter((prevState) => ({
+    ...prevState,
+    withdrawalInProgress: true,
+    talerWithdrawUri: resp.taler_withdraw_uri,
+    withdrawalId: resp.withdrawal_id}))
 }
 
 async function loginCall(
@@ -515,7 +613,7 @@ function Transactions(Props: any): VNode {
     `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`
   );
   if (typeof error !== "undefined") {
-    console.log("balance error", error);
+    console.log("transactions not found error", error);
     switch(error.status) {
       case 404: {
         return <p>Transactions page {pageNumber} was not found.</p>
@@ -550,8 +648,10 @@ function Transactions(Props: any): VNode {
  * Show only the account's balance.
  */
 function Account(Props: any): VNode {
+  console.log("DUMPING PROPS", Props)
   const {
     withdrawalOutcome,
+    transferOutcome,
     talerWithdrawUri,
     accountLabel } = Props;
   /**
@@ -584,6 +684,18 @@ function Account(Props: any): VNode {
   }
   if (!data) return <p>Retrieving the profile page...</p>;
 
+  /**
+   * Wire transfer reached a final state: show it.  Note:
+   * such state is usually successful, as errors should
+   * have been reported earlier.
+   */
+  if (transferOutcome) {
+    return <Fragment>
+      <p>{transferOutcome}</p>
+      {Props.children}
+    </Fragment>
+  }
+
   /**
    * Withdrawal reached a final state: show it.
    */
@@ -737,6 +849,8 @@ export function BankHome(): VNode {
       }));
       return <p>Error: waiting for details...</p>;
     }
+
+    var transactionData: TransactionRequestType;
     return (
       <SWRWithCredentials
         username={backendState.username}
@@ -746,6 +860,7 @@ export function BankHome(): VNode {
           <Account
             withdrawalOutcome={pageState.withdrawalOutcome}
             talerWithdrawUri={pageState.talerWithdrawUri}
+            transferOutcome={pageState.transferOutcome}
             accountLabel={backendState.username}>
   
             { /**
@@ -758,6 +873,16 @@ export function BankHome(): VNode {
                   pageStateSetter
                 )}}>{i18n`Charge Taler wallet`}</button>
             }
+
+            { /**
+               * Wire transfer reached a persisten state: offer to
+               * return back to the pristine profile page.
+               */
+              pageState.transferOutcome && <button onClick={() => {
+                pageStateSetter((prevState) => {
+                  const { transferOutcome, ...rest } = prevState;
+                  return {...rest};})}}>{i18n`Close wire transfer`}</button>
+            }
   
             { /**
                * Withdrawal reached a persisten state: offer to
@@ -766,7 +891,7 @@ export function BankHome(): VNode {
               pageState.withdrawalOutcome && <button onClick={() => {
                 pageStateSetter((prevState) => {
                   const { withdrawalOutcome, withdrawalId, ...rest } = 
prevState;
-                  return {...rest, withdrawalInProgress: 
false};})}}>{i18n`Close`}</button>
+                  return {...rest, withdrawalInProgress: 
false};})}}>{i18n`Close Taler withdrawal`}</button>
             }
   
             { /**
@@ -783,7 +908,33 @@ export function BankHome(): VNode {
                     backendState,
                     pageState.withdrawalId,
                     pageStateSetter);}}>{i18n`Abort withdrawal`}</button>
-            </div>}
+              </div>
+           }
+
+           { /**
+              * Offer wire transfer, if no withdrawal is in progress.
+              */
+              !pageState.withdrawalInProgress && <Fragment>
+               <p>Please, include the 'amount' query parameter.</p>
+               <input
+                  type="text"
+                  placeholder="payto address" // changing this breaks tests.
+                  required
+                  onInput={(e): void => {
+                    transactionData = {
+                      ...transactionData,
+                      paytoUri: e.currentTarget.value,
+                    };
+                  }} />
+               <button onClick={() => {
+                  createTransactionCall(
+                   transactionData,
+                    backendState,
+                    pageStateSetter
+                 ); 
+               }}>{i18n`Create wire transfer`}</button>
+             </Fragment>
+           }
           </Account>
           { /* The user is logged in: offer to log out.  */ }
           <button onClick={() => {
diff --git a/packages/bank/tests/__tests__/homepage.js 
b/packages/bank/tests/__tests__/homepage.js
index d489d8e..039bab1 100644
--- a/packages/bank/tests/__tests__/homepage.js
+++ b/packages/bank/tests/__tests__/homepage.js
@@ -52,6 +52,46 @@ function fillCredentialsForm() {
 }
 fetchMock.enableMocks();
 
+function signUp(context) {
+  render(<BankHome />);
+  const { username, signupButton } = fillCredentialsForm();
+  fetch.once("{}", {
+    status: 200
+  }).once(JSON.stringify({
+    balance: {
+      amount: "EUR:10",
+      credit_debit_indicator: "credit"
+    },
+    paytoUri: "payto://iban/123/ABC"
+  }))
+  fireEvent.click(signupButton);
+  context.username = username;
+}
+
+describe("wire transfer", () => {
+  beforeEach(() => {
+    signUp({}); // context unused
+  })
+  test("Wire transfer success", async () => {
+    const transferButton = screen.getByText("Create wire transfer");
+    const payto = screen.getByPlaceholderText("payto address");
+    fireEvent.input(payto, {target: {value: 
"payto://only-checked-by-the-backend!"}})
+    fetch.once("{}"); // 200 OK
+    fireEvent.click(transferButton);
+    await screen.findByText("wire transfer created", {exact: false})
+  })
+  test("Wire transfer fail", async () => {
+    const transferButton = screen.getByText("Create wire transfer");
+    const payto = screen.getByPlaceholderText("payto address");
+    fireEvent.input(payto, {target: {value: 
"payto://only-checked-by-the-backend!"}})
+    fetch.once("{}", {status: 400});
+    fireEvent.click(transferButton);
+    // assert this below does NOT appear.
+    await waitFor(() => expect(
+      screen.queryByText("wire transfer created", {exact: 
false})).not.toBeInTheDocument());
+  })
+})
+
 describe("withdraw", () => {
   afterEach(() => {
     fetch.resetMocks();
@@ -60,19 +100,7 @@ describe("withdraw", () => {
 
   // Register and land on the profile page.
   beforeEach(() => {
-    render(<BankHome />);
-    const { username, signupButton } = fillCredentialsForm();
-    fetch.once("{}", {
-      status: 200
-    }).once(JSON.stringify({
-      balance: {
-        amount: "EUR:10",
-       credit_debit_indicator: "credit"
-      },
-      paytoUri: "payto://iban/123/ABC"
-    }))
-    fireEvent.click(signupButton);
-    context.username = username;
+    signUp(context); 
   })
 
   let context = {username: null};
@@ -308,7 +336,7 @@ describe("home page", () => {
     const { username, signinButton } = fillCredentialsForm();
     fetch.once("{}", {status: 404});
     fireEvent.click(signinButton);
-    await screen.findByText("username was not found", {exact: false})
+    await screen.findByText("username or account label not found", {exact: 
false})
   })
   test("login wrong credentials", async () => {
     render(<BankHome />);

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