merchant-backoffice

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

commit 4d9b35bb4c153b9bf98cd3d8eeccda828d3aea29
parent 5ef91927720395a88fcaaadbc6b364bebb768c09
Author: ms <ms@taler.net>
Date:   Mon, 10 Jan 2022 19:29:50 +0100

transactions list pagination (WIP)

Diffstat:
Mpackages/bank/src/hooks/index.ts | 1+
Mpackages/bank/src/pages/home/index.tsx | 143++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mpackages/bank/tests/__tests__/homepage.js | 55+++++++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 161 insertions(+), 38 deletions(-)

diff --git a/packages/bank/src/hooks/index.ts b/packages/bank/src/hooks/index.ts @@ -115,6 +115,7 @@ export function useNotNullLocalStorage( key: string, initialValue: string, ): [string, StateUpdater<string>] { + const [storedValue, setStoredValue] = useState<string>((): string => { return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue diff --git a/packages/bank/src/pages/home/index.tsx b/packages/bank/src/pages/home/index.tsx @@ -57,6 +57,17 @@ interface AccountStateType { * Helpers. * ***********/ +function useTransactionPageNumber(): [number, StateUpdater<number>] { + + const ret = useNotNullLocalStorage("transaction-page", "0"); + const retObj = JSON.parse(ret[0]); + const retSetter: StateUpdater<number> = function(val) { + const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val) + ret[1](newVal) + } + return [retObj, retSetter]; +} + /** * Craft headers with Authorization and Content-Type. */ @@ -134,6 +145,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) ret[1](newVal) } return [retObj, retSetter]; @@ -296,7 +308,6 @@ async function confirmWithdrawalCall( } else { console.log("Withdrawal operation confirmed!"); pageStateSetter((prevState) => { - delete prevState.talerWithdrawUri; const { talerWithdrawUri, ...rest } = prevState; return { ...rest, @@ -477,14 +488,51 @@ async function registrationCall( *************************/ /** + * Show list of transactions. + */ +function Transactions(Props: any): VNode { + + const { pageNumber, accountLabel } = Props; + const { data, error } = useSWR( + `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}` + ); + if (typeof error !== "undefined") { + console.log("balance error", error); + switch(error.status) { + case 404: { + return <p>Transactions page {pageNumber} was not found.</p> + } + case 401: { + return <p>Wrong credentials given.</p> + } + default: { + return <p>Transaction page {pageNumber} could not be retrieved.</p> + } + } + } + var txsPages = <p>"loading..."</p> + + if (data) { + txsPages = data.map((item: any) => <div>Result</div>) + } + return txsPages; +} + +/** * Show only the account's balance. */ -function Account(props: any) { - const { withdrawalOutcome, talerWithdrawUri, accountLabel } = props; - const { data, error } = useSWR(`access-api/accounts/${props.accountLabel}`); - console.log("account data", data); - console.log("account error", error); +function Account(Props: any): VNode { + const { + withdrawalOutcome, + talerWithdrawUri, + accountLabel } = Props; + const i18n = useTranslator(); + /** + * Getting the bank account balance. + */ + const { data, error } = useSWR(`access-api/accounts/${accountLabel}`); if (typeof error !== "undefined") { + console.log("account error", error); switch(error.status) { case 404: { return <p>Username was not found</p> @@ -497,18 +545,25 @@ function Account(props: any) { } } } - if (!data) return <p>Retrieving the profile page...</p>; + + /** + * Withdrawal reached a final state: show it. + */ if (withdrawalOutcome) { return <Fragment> <p>{withdrawalOutcome}</p> - {props.children} + {Props.children} </Fragment> } + /** - * 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. + * This block shows the withdrawal QR code. + * + * A withdrawal operation replaces everything in the page and + * (ToDo:) 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 @@ -520,12 +575,28 @@ function Account(props: any) { <div>{QR({text: talerWithdrawUri})}</div> <a href={talerWithdrawUri}></a> <p>Withdraw address: <pre>{talerWithdrawUri}</pre></p> - {props.children} + {Props.children} </Fragment>); } + + /** + * This part shows a list of transactions: with 5 elements by + * default and offers a "load more" button. + */ + var [txPageNumber, setTxPageNumber] = useTransactionPageNumber() // Buggy. + var txsPages = [] + for (let i = 0; i <= txPageNumber; i++) { + txsPages.push(<Transactions accountLabel={Props.accountLabel} pageNumber={txPageNumber} />) + } + return (<Fragment> <p>Your balance is {data.balance.amount}.</p> - {props.children} + <div> + <span>{i18n`Last transactions:`}</span> { txsPages } + <button onClick={() => setTxPageNumber(txPageNumber + 1)}>{i18n`Load more`}</button> + <button onClick={() => setTxPageNumber(0)}>{i18n`Reset`}</button> + </div> + {Props.children} </Fragment>); } @@ -596,7 +667,8 @@ export function BankHome(): VNode { withdrawalOutcome={pageState.withdrawalOutcome} talerWithdrawUri={pageState.talerWithdrawUri} accountLabel={backendState.username}> - + + { /* The user is logged in: offer to log out. */ } <button onClick={() => { pageStateSetter((prevState) => { const { @@ -607,30 +679,41 @@ export function BankHome(): VNode { }) }}>Sign out</button> - {!pageState.withdrawalInProgress && <button onClick={() => { - createWithdrawalCall( - "EUR:5", - backendState, - pageStateSetter - )}}>{i18n`Charge Taler wallet`}</button> + { /** + * No withdrawal is happening: offer to start one. + */ + !pageState.withdrawalInProgress && <button onClick={() => { + createWithdrawalCall( + "EUR:5", + backendState, + pageStateSetter + )}}>{i18n`Charge Taler wallet`}</button> } - {pageState.withdrawalOutcome && <button onClick={() => { + { /** + * Withdrawal reached a persisten state: offer to + * return back to the pristine profile page. + */ + pageState.withdrawalOutcome && <button onClick={() => { pageStateSetter((prevState) => { - const { withdrawalOutcome, ...rest } = prevState; + const { withdrawalOutcome, withdrawalId, ...rest } = prevState; return {...rest, withdrawalInProgress: false};})}}>{i18n`Close`}</button> } - {pageState.talerWithdrawUri && <div><button onClick={() => { - confirmWithdrawalCall( - backendState, - pageState.withdrawalId, - pageStateSetter);}}>{i18n`Confirm withdrawal`}</button> - <button onClick={() => { - abortWithdrawalCall( + { /** + * The withdrawal QR code is rendered: offer to confirm + * or abort the operation. + */ + pageState.talerWithdrawUri && <div><button onClick={() => { + confirmWithdrawalCall( backendState, pageState.withdrawalId, - pageStateSetter);}}>{i18n`Abort withdrawal`}</button> + pageStateSetter);}}>{i18n`Confirm withdrawal`}</button> + <button onClick={() => { + abortWithdrawalCall( + backendState, + pageState.withdrawalId, + pageStateSetter);}}>{i18n`Abort withdrawal`}</button> </div>} </Account> </SWRWithCredentials> diff --git a/packages/bank/tests/__tests__/homepage.js b/packages/bank/tests/__tests__/homepage.js @@ -16,6 +16,34 @@ jest.mock("../../src/i18n") const i18n = require("../../src/i18n") i18n.useTranslator.mockImplementation(() => function(arg) {return arg}) +/** + * Mocking local storage, see: + * https://stackoverflow.com/questions/32911630/how-do-i-deal-with-localstorage-in-jest-tests + */ +class LocalStorageMock { + constructor() { + this.store = {}; + } + + clear() { + this.store = {}; + } + + getItem(key) { + return this.store[key] || null; + } + + setItem(key, value) { + this.store[key] = String(value); + } + + removeItem(key) { + delete this.store[key]; + } +} + +global.localStotage = new LocalStorageMock(); + beforeAll(() => { Object.defineProperty(window, 'location', { value: { @@ -23,10 +51,9 @@ beforeAll(() => { pathname: "/demobanks/default" } }) - // Invalidating local storage: makes it more difficult - // to isolate the individual tests, and it doesn't really - // participate in the SPA logic. - global.Storage.prototype.setItem = jest.fn((key, value) => {}) +}) +afterAll(() => { + global.localStorage.clear() }) /** @@ -135,9 +162,13 @@ describe("withdraw", () => { ) // assume wallet POSTed the payment details. const confirmButton = await screen.findByText("confirm withdrawal", {exact: false}) + /** + * Not expecting a new withdrawal possibility while one is being processed. + */ await waitFor(() => expect( screen.queryByText("charge taler wallet", {exact: false})).not.toBeInTheDocument()); fetch.once("{}") + // Confirm currently processed withdrawal. fireEvent.click(confirmButton); /** * After having confirmed above, wait that the @@ -161,15 +192,23 @@ describe("withdraw", () => { await screen.findByText("withdrawal confirmed", {exact: false}) /** - * Click on a "return to homepage" button, and check that - * the withdrawal confirmation is gone, and the option to - * withdraw again reappeared. + * Click on a "return to homepage / close" button, and + * check that the withdrawal confirmation is gone, and + * the option to withdraw again reappeared. */ const closeButton = await screen.findByText("close", {exact: false}) fireEvent.click(closeButton); + + /** + * After closing the operation, the confirmation message is not expected. + */ await waitFor(() => expect( screen.queryByText("withdrawal confirmed", {exact: false})).not.toBeInTheDocument() ); + + /** + * After closing the operation, the possibility to withdraw again should be offered. + */ await waitFor(() => expect( screen.queryByText( "charge taler wallet", @@ -275,7 +314,7 @@ describe("home page", () => { "http://localhost/demobanks/default/access-api/testing/register", expect.anything() // no need to match auth headers. ) - expect(fetch).toHaveBeenLastCalledWith( + expect(fetch).toHaveBeenCalledWith( `http://localhost/demobanks/default/access-api/accounts/${username}`, expect.anything() // no need to match auth headers. )