merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit e84b098ea9b2c60c7ba1db6c82f65ca2079ddb9c
parent 57348c35d8bd6b14d5ed9e1a0715609b616a9de8
Author: ms <ms@taler.net>
Date:   Mon, 31 Jan 2022 12:49:27 +0100

Add wire transfer form

Diffstat:
Mpackages/bank/src/pages/home/index.tsx | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/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 @@ -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. * @@ -37,6 +37,13 @@ interface BackendStateType { } /** + * Request body of POST /transactions. + */ +interface TransactionRequestType { + paytoUri: string; +} + +/** * Request body of /register. */ interface CredentialsRequestType { @@ -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]; @@ -335,14 +390,57 @@ 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. * * 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. * - * 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; /** @@ -585,6 +685,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. */ if (withdrawalOutcome) { @@ -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 @@ -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 />);