commit ee6e662d4838f5d0c48d7de589bb1f287ef99807
parent 726c0a03611ca71e4911106dc34e4f8d641f9301
Author: ms <ms@taler.net>
Date: Mon, 20 Dec 2021 10:35:36 +0100
bank: implement withdrawal abort
Diffstat:
2 files changed, 110 insertions(+), 33 deletions(-)
diff --git a/packages/bank/src/pages/home/index.tsx b/packages/bank/src/pages/home/index.tsx
@@ -55,6 +55,22 @@ interface AccountStateType {
* Helpers. *
***********/
+/**
+ * Craft headers with Authorization and Content-Type.
+ */
+function prepareHeaders(username: string, password: string) {
+ let headers = new Headers();
+ headers.append(
+ "Authorization",
+ `Basic ${Buffer.from(username + ":" + password).toString("base64")}`
+ );
+ headers.append(
+ "Content-Type",
+ "application/json"
+ )
+ return headers;
+}
+
const getRootPath = () => {
return typeof window !== undefined
? window.location.origin + window.location.pathname
@@ -123,6 +139,71 @@ function usePageState(
* a helper function.
*/
+/**
+ * Abort a withdrawal operation via the Access API's /abort.
+ */
+async function abortWithdrawalCall(
+ backendState: BackendStateTypeOpt,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+ if (typeof backendState === "undefined") {
+ console.log("No credentials found.");
+ pageStateSetter((prevState) => ({...prevState, hasError: true, error: "No credentials found."}))
+ return;
+ }
+ if (typeof withdrawalId === "undefined") {
+ console.log("No withdrawal ID found.");
+ pageStateSetter((prevState) => ({...prevState, hasError: true, error: "No withdrawal ID found."}))
+ return;
+ }
+
+ try {
+ 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(
+ `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
+ backendState.url
+ )
+ var res = await fetch(url.href, {method: 'POST', headers: headers})
+ } catch (error) {
+ console.log("Could not abort the withdrawal", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Could not abort the withdrawal: ${error}`}))
+ return;
+ }
+ if (!res.ok) {
+ console.log(`Withdrawal abort gave response error (${res.status})`, res.statusText);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Withdrawal abortion gave response error (${res.status})`}))
+ return;
+ } else {
+ console.log("Withdrawal operation aborted!");
+ pageStateSetter((prevState) => {
+ delete prevState.talerWithdrawUri;
+ const { talerWithdrawUri, ...rest } = prevState;
+ return {
+ ...rest,
+ withdrawalOutcome: "Withdrawal aborted!"
+ }})
+ }
+}
/**
* This function confirms a withdrawal operation AFTER
@@ -139,23 +220,21 @@ async function confirmWithdrawalCall(
withdrawalId: string | undefined,
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"}))
+ console.log("No credentials found.");
+ pageStateSetter((prevState) => ({...prevState, hasError: true, error: "No credentials found."}))
return;
}
if (typeof withdrawalId === "undefined") {
- pageStateSetter((prevState) => ({
- ...prevState,
- hasError: true,
- error: "Withdrawal ID wasn't found in the state; cannot confirm it."}))
+ console.log("No withdrawal ID found.");
+ pageStateSetter((prevState) => ({...prevState, hasError: true, error: "No withdrawal ID found."}))
return;
}
+
try {
- let headers = new Headers();
+ 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
@@ -166,14 +245,7 @@ async function confirmWithdrawalCall(
* headers.append("cache-control", "no-cache");
* headers.append("pragma", "no-cache");
* */
- headers.append(
- "Authorization",
- `Basic ${Buffer.from(backendState.username + ":" + backendState.password).toString("base64")}`
- );
- headers.append(
- "Content-Type",
- "application/json"
- )
+
// Backend URL must have been stored _with_ a final slash.
const url = new URL(
`access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
@@ -227,14 +299,12 @@ async function createWithdrawalCall(
) {
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"}))
+ pageStateSetter((prevState) => ({...prevState, hasError: true, error: "No credentials given."}))
return;
}
try {
- let headers = new Headers();
+ 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
@@ -245,14 +315,7 @@ async function createWithdrawalCall(
* headers.append("cache-control", "no-cache");
* headers.append("pragma", "no-cache");
* */
- headers.append(
- "Authorization",
- `Basic ${Buffer.from(backendState.username + ":" + backendState.password).toString("base64")}`
- );
- headers.append(
- "Content-Type",
- "application/json"
- )
+
// Backend URL must have been stored _with_ a final slash.
const url = new URL(
`access-api/accounts/${backendState.username}/withdrawals`,
@@ -501,6 +564,10 @@ export function BankHome(): VNode {
talerWithdrawUri={pageState.talerWithdrawUri}
accountLabel={backendState.username}>
+ <button onClick={() => {
+ pageStateSetter({...pageState, isLoggedIn: false})
+ }}>Sign out</button>
+
{!pageState.withdrawalInProgress && <button onClick={() => {
createWithdrawalCall(
"EUR:5",
@@ -515,12 +582,18 @@ export function BankHome(): VNode {
return {...rest, withdrawalInProgress: false};})}}>Close</button>
}
- {pageState.talerWithdrawUri && <button onClick={() => {
+ {pageState.talerWithdrawUri && <div><button onClick={() => {
confirmWithdrawalCall(
backendState,
pageState.withdrawalId,
pageStateSetter);}}>Confirm withdrawal</button>
- }
+ <button onClick={() => {
+ abortWithdrawalCall(
+ backendState,
+ pageState.withdrawalId,
+ pageStateSetter);}}>Abort withdrawal</button>
+ </div>}
+
</Account>
</SWRWithCredentials>
);
diff --git a/packages/bank/tests/__tests__/homepage.js b/packages/bank/tests/__tests__/homepage.js
@@ -206,6 +206,10 @@ describe("home page", () => {
paytoUri: "payto://iban/123/ABC"
}))
fireEvent.click(signinButton);
+ expect(fetch).toHaveBeenCalledWith(
+ `http://localhost/demobanks/default/access-api/accounts/${username}`,
+ expect.anything()
+ )
await screen.findByText("balance is EUR:10", {exact: false})
})