[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] branch master updated: bank: create withdraw
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] branch master updated: bank: create withdrawal |
Date: |
Fri, 17 Dec 2021 11:02:25 +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 aa05541 bank: create withdrawal
aa05541 is described below
commit aa05541c627b9fb5ee066980292805e6ee84da34
Author: ms <ms@taler.net>
AuthorDate: Fri Dec 17 11:02:03 2021 +0100
bank: create withdrawal
---
packages/bank/src/pages/home/index.tsx | 140 ++++++++++++++++++++++++++----
packages/bank/tests/__tests__/homepage.js | 101 +++++++++++++++++----
2 files changed, 206 insertions(+), 35 deletions(-)
diff --git a/packages/bank/src/pages/home/index.tsx
b/packages/bank/src/pages/home/index.tsx
index d056021..9ed70ea 100644
--- a/packages/bank/src/pages/home/index.tsx
+++ b/packages/bank/src/pages/home/index.tsx
@@ -1,7 +1,8 @@
-import useSWR, { SWRConfig } from "swr";
+import useSWR, { SWRConfig, useSWRConfig } from "swr";
import { h, Fragment, ComponentChildren, VNode } from "preact";
import { useState, useEffect, StateUpdater } from "preact/hooks";
import { Buffer } from "buffer";
+import { useTranslator } from "../../i18n";
/**********************************************
* Type definitions for states and API calls. *
@@ -30,8 +31,9 @@ interface RegistrationRequestType {
*/
interface PageStateType {
isLoggedIn: boolean;
- hasProblem: boolean;
+ hasError: boolean;
error?: string;
+ talerWithdrawUri?: string;
}
/**
@@ -42,9 +44,9 @@ interface AccountStateType {
/* FIXME: Need history here. */
}
-/*******************
+/************
* Helpers. *
- ******************/
+ ***********/
const getRootPath = () => {
return typeof window !== undefined
@@ -84,7 +86,7 @@ function useAccountState(
function usePageState(
state: PageStateType = {
isLoggedIn: false,
- hasProblem: false,
+ hasError: false,
}
): [PageStateType, StateUpdater<PageStateType>] {
return useState<PageStateType>(state);
@@ -109,6 +111,76 @@ function usePageState(
* on each case.
*/
+/**
+ * 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.
+ */
+async function createWithdrawalOperation(
+ amount: string,
+ backendState: BackendStateTypeOpt,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+ if (typeof backendState === "undefined") {
+ console.log("Page has a problem: no credentials found in the state.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: "No credentials found in the state"}))
+ return;
+ }
+ try {
+ let headers = new Headers();
+ /**
+ * 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");
+ * */
+ headers.append(
+ "Authorization",
+ `Basic ${Buffer.from(backendState.username + ":" +
backendState.password).toString("base64")}`
+ );
+ var res = await fetch(
+ `${backendState.url}accounts/${backendState.username}/withdrawals`, {
+ method: 'POST',
+ headers: headers,
+ body: JSON.stringify({amount: amount}),
+ }
+ );
+ } catch (error) {
+ console.log("Could not POST withdrawal request to the bank", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Could not create withdrawal operation: ${error}`}))
+ return;
+ }
+ if (!res.ok) {
+ console.log(`Withdrawal creation gave response error (${res.status})`,
res.statusText);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ 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,
+ talerWithdrawUri: resp.taler_withdraw_uri}))
+ }
+}
+
/**
* This function requests /register.
*
@@ -118,24 +190,31 @@ function usePageState(
*/
async function registrationCall(
req: RegistrationRequestType,
+ /**
+ * FIXME: figure out if the two following
+ * functions can be retrieved somewhat from
+ * the state.
+ */
backendStateSetter: StateUpdater<BackendStateTypeOpt>,
pageStateSetter: StateUpdater<PageStateType>
) {
let baseUrl = getRootPath();
+ let headersNoCache = new Headers();
try {
var res = await fetch(
- `${baseUrl}testing/register`,
- {method: 'POST', body: JSON.stringify(req)}
- );
+ `${baseUrl}testing/register`, {
+ method: 'POST',
+ body: JSON.stringify(req),
+ });
} catch (error) {
console.log("Could not POST new registration to the bank", error);
- pageStateSetter((prevState) => ({ ...prevState, hasProblem: true }));
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true }));
return;
}
if (!res.ok) {
console.log(`New registration gave response error (${res.status})`,
res.statusText);
- pageStateSetter((prevState) => ({ ...prevState, hasProblem: true }));
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true }));
} else {
console.log("Credentials are valid");
pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
@@ -156,12 +235,28 @@ async function registrationCall(
* Show only the account's balance.
*/
function Account(props: any) {
+ const { talerWithdrawUri, accountLabel } = props;
const { data, error } = useSWR(`accounts/${props.accountLabel}`);
if (typeof error != "undefined") {
return <p>Account information could not be retrieved</p>
}
- if (!data) return <p>Retrieving the balance...</p>;
- return <p>Your balance is {data.balance.amount}.</p>;
+ if (!data) return <p>Retrieving the profile page...</p>;
+ /**
+ * A Taler withdrawal replaces everything in the page and
+ * starts polling the backend until either the wallet selected
+ * a exchange and reserve public key, or a error / abort happened.
+ *
+ * After reaching one of the above states, the user should be
+ * brought to this ("Account") page where they get informed about
+ * the outcome.
+ */
+ if (talerWithdrawUri) {
+ return <p>Give this address to your Taler wallet: {talerWithdrawUri}</p>
+ }
+ return <div>
+ <p>Your balance is {data.balance.amount}.</p>
+ {props.children}
+ </div>
}
/**
@@ -196,8 +291,8 @@ export function BankHome(): VNode {
var [pageState, pageStateSetter] = usePageState();
var [accountState, accountStateSetter] = useAccountState();
- if (pageState.hasProblem) {
- return <p>Page has a problem.</p>;
+ if (pageState.hasError) {
+ return <p>Page has a problem: {pageState.error}</p>;
}
/**
@@ -206,9 +301,8 @@ export function BankHome(): VNode {
* history */
if (pageState.isLoggedIn) {
if (typeof backendState === "undefined") {
- console.log("Credentials not found in state, even after login.",
backendState);
- pageStateSetter((state) => ({ ...state, hasProblem: true }));
- return <p>Page has a problem</p>;
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true }));
+ return <p>Page has a problem: logged in but backend state is lost.</p>;
}
return (
<SWRWithCredentials
@@ -216,7 +310,17 @@ export function BankHome(): VNode {
password={backendState.password}
backendUrl={backendState.url}
>
- <Account accountLabel={backendState.username} />
+ <Account talerWithdrawUri={pageState.talerWithdrawUri}
+ accountLabel={backendState.username}>
+ <button
+ onClick={() => {
+ createWithdrawalOperation(
+ "EUR:5",
+ backendState,
+ pageStateSetter
+ )}}
+ >Charge Taler wallet</button>
+ </Account>
</SWRWithCredentials>
);
diff --git a/packages/bank/tests/__tests__/homepage.js
b/packages/bank/tests/__tests__/homepage.js
index d78bd67..03a909d 100644
--- a/packages/bank/tests/__tests__/homepage.js
+++ b/packages/bank/tests/__tests__/homepage.js
@@ -2,58 +2,117 @@ import "core-js/stable";
import "regenerator-runtime/runtime";
import { BankHome } from '../../src/pages/home';
import { h } from 'preact';
-import { render, fireEvent, screen } from '@testing-library/preact';
+import { cleanup, render, fireEvent, screen } from '@testing-library/preact';
import expect from 'expect';
import fetchMock from "jest-fetch-mock";
-fetchMock.enableMocks();
-
-beforeEach(() => {
- fetch.resetMocks();
-});
-
-// Insert username and password into the registration
-// form and returns the submit button.
+/**
+ * Insert username and password into the registration
+ * form and returns the submit button. NOTE: the username
+ * must be given always fresh, as it acts as a SWR key and
+ * therefore might prevent calls from being made, because of
+ * caching reasons. That is not a problem per-se but can
+ * disrupt ".toHaveLastBeenCalledWith()"-like asserts.
+ *
+ * Return the username and the submit button.
+ */
function fillRegistrationForm() {
+ const username = Math.random().toString().substring(2);
const u = screen.getByPlaceholderText("username");
const p = screen.getByPlaceholderText("password");
- fireEvent.input(u, {target: {value: "foo"}})
+ fireEvent.input(u, {target: {value: username}})
fireEvent.input(p, {target: {value: "bar"}})
- return screen.getByText("Submit");
+ const submitButton = screen.getByText("Submit");
+ return {username: username, submitButton: submitButton};
}
+fetchMock.enableMocks();
+
+describe("withdraw", () => {
+ afterEach(() => {
+ fetch.resetMocks();
+ cleanup();
+ })
+
+ // Register and land on the profile page.
+ beforeEach(() => {
+ render(<BankHome />);
+ const { username, submitButton } = fillRegistrationForm();
+ fetch.once("{}", {
+ status: 200
+ }).once(JSON.stringify({
+ balance: {
+ amount: "EUR:10",
+ credit_debit_indicator: "credit"
+ },
+ paytoUri: "payto://iban/123/ABC"
+ }))
+ fireEvent.click(submitButton);
+ })
+
+ test("network failure before withdrawal creation", async () => {
+ let withdrawButton = screen.getByText("Charge Taler wallet");
+ // mock network failure.
+ fetch.mockReject("API is down");
+ fireEvent.click(withdrawButton);
+ await screen.findByText("could not create withdrawal operation", {exact:
false})
+ })
+
+ test("HTTP response error upon withdrawal creation", async () => {
+ let withdrawButton = screen.getByText("Charge Taler wallet");
+ // mock network failure.
+ fetch.once("{}", {status: 404});
+ fireEvent.click(withdrawButton);
+ await screen.findByText("gave response error", {exact: false})
+ })
+
+ test("Successful withdraw", async () => {
+ let withdrawButton = screen.getByText("Charge Taler wallet");
+ fetch.once(JSON.stringify({taler_withdraw_uri: "taler://foo"}));
+ fireEvent.click(withdrawButton);
+ await screen.findByText("give this address to your Taler wallet", {exact:
false})
+ })
+})
describe("home page", () => {
+ afterEach(() => {
+ fetch.resetMocks();
+ cleanup();
+ })
+
// check page informs about the current balance
// after a successful registration.
test("new registration response error 404", async () => {
render(<BankHome />);
- let submitButton = fillRegistrationForm();
+ let { username, submitButton } = fillRegistrationForm();
fetch.mockResponseOnce("Not found", {status: 404})
fireEvent.click(submitButton);
await screen.findByText("has a problem", {exact: false});
expect(fetch).toHaveBeenCalledWith(
"http://localhost/testing/register",
- {body: JSON.stringify({username: "foo", password: "bar"}), method:
"POST"},
+ {body: JSON.stringify({username: username, password: "bar"}), method:
"POST"},
)
})
test("registration network failure", async () => {
render(<BankHome />);
- let submitButton = fillRegistrationForm();
+ const { username, submitButton } = fillRegistrationForm();
// Mocking network failure.
fetch.mockReject("API is down");
fireEvent.click(submitButton);
await screen.findByText("has a problem", {exact: false});
expect(fetch).toHaveBeenCalledWith(
"http://localhost/testing/register",
- {body: JSON.stringify({username: "foo", password: "bar"}), method:
"POST"},
+ {body: JSON.stringify({username: username, password: "bar"}), method:
"POST"},
)
})
test("registration success", async () => {
render(<BankHome />);
- let submitButton = fillRegistrationForm();
+ const { username, submitButton } = fillRegistrationForm();
+ /**
+ * Mock successful registration and balance request.
+ */
fetch.once("{}", {
status: 200
}).once(JSON.stringify({
@@ -64,9 +123,17 @@ describe("home page", () => {
paytoUri: "payto://iban/123/ABC"
}))
fireEvent.click(submitButton);
+ /**
+ * Tests that a balance is shown after the successful
+ * registration.
+ */
await screen.findByText("balance is EUR:10", {exact: false})
+ /**
+ * The expectation below tests whether the account
+ * balance was requested after the successful registration.
+ */
expect(fetch).toHaveBeenLastCalledWith(
- "http://localhost/accounts/foo",
+ `http://localhost/accounts/${username}`,
expect.anything() // no need to match auth headers.
)
})
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-merchant-backoffice] branch master updated: bank: create withdrawal,
gnunet <=