From c6f228bf142637eb72456aebabd0483d83402373 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Dec 2022 12:38:50 -0300 Subject: no-fix: moved out AccountPage --- packages/demobank-ui/src/components/app.tsx | 17 + packages/demobank-ui/src/pages/Routing.tsx | 2 +- .../demobank-ui/src/pages/home/AccountPage.tsx | 289 ++++ packages/demobank-ui/src/pages/home/LoginForm.tsx | 149 ++ .../demobank-ui/src/pages/home/PaymentOptions.tsx | 54 + .../src/pages/home/PaytoWireTransferForm.tsx | 442 ++++++ .../home/TalerWithdrawalConfirmationQuestion.tsx | 300 ++++ .../src/pages/home/TalerWithdrawalQRCode.tsx | 97 ++ .../src/pages/home/WalletWithdrawForm.tsx | 176 +++ packages/demobank-ui/src/pages/home/index.tsx | 1502 -------------------- packages/demobank-ui/src/utils.ts | 15 + 11 files changed, 1540 insertions(+), 1503 deletions(-) create mode 100644 packages/demobank-ui/src/pages/home/AccountPage.tsx create mode 100644 packages/demobank-ui/src/pages/home/LoginForm.tsx create mode 100644 packages/demobank-ui/src/pages/home/PaymentOptions.tsx create mode 100644 packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx create mode 100644 packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx create mode 100644 packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx create mode 100644 packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx delete mode 100644 packages/demobank-ui/src/pages/home/index.tsx diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index 35681a58c..f3bc3f571 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -3,6 +3,23 @@ import { PageStateProvider } from "../context/pageState.js"; import { TranslationProvider } from "../context/translation.js"; import { Routing } from "../pages/Routing.js"; +/** + * FIXME: + * + * - INPUT elements have their 'required' attribute ignored. + * + * - 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. + * + * - histories 'pages' are grouped in UL elements that cause + * the rendering to visually separate each UL. History elements + * should instead line up without any separation caused by + * a implementation detail. + * + * - Many strings need to be i18n-wrapped. + */ + const App: FunctionalComponent = () => { return ( diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx index 1ef042297..7f079a7de 100644 --- a/packages/demobank-ui/src/pages/Routing.tsx +++ b/packages/demobank-ui/src/pages/Routing.tsx @@ -18,7 +18,7 @@ import { createHashHistory } from "history"; import { h, VNode } from "preact"; import Router, { route, Route } from "preact-router"; import { useEffect } from "preact/hooks"; -import { AccountPage } from "./home/index.js"; +import { AccountPage } from "./home/AccountPage.js"; import { PublicHistoriesPage } from "./home/PublicHistoriesPage.js"; import { RegistrationPage } from "./home/RegistrationPage.js"; diff --git a/packages/demobank-ui/src/pages/home/AccountPage.tsx b/packages/demobank-ui/src/pages/home/AccountPage.tsx new file mode 100644 index 000000000..2bc05c332 --- /dev/null +++ b/packages/demobank-ui/src/pages/home/AccountPage.tsx @@ -0,0 +1,289 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +import { Amounts, HttpStatusCode } from "@gnu-taler/taler-util"; +import { hooks } from "@gnu-taler/web-util/lib/index.browser"; +import { h, Fragment, VNode } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; +import useSWR, { SWRConfig, useSWRConfig } from "swr"; +import { PageStateType, usePageContext } from "../../context/pageState.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { bankUiSettings } from "../../settings.js"; +import { getIbanFromPayto } from "../../utils.js"; +import { BankFrame } from "./BankFrame.js"; +import { LoginForm } from "./LoginForm.js"; +import { PaymentOptions } from "./PaymentOptions.js"; +import { TalerWithdrawalQRCode } from "./TalerWithdrawalQRCode.js"; +import { Transactions } from "./Transactions.js"; + +export function AccountPage(): VNode { + const [backendState, backendStateSetter] = useBackendState(); + const { i18n } = useTranslationContext(); + const { pageState, pageStateSetter } = usePageContext(); + + if (!pageState.isLoggedIn) { + return ( + +

{i18n.str`Welcome to ${bankUiSettings.bankName}!`}

+ +
+ ); + } + + if (typeof backendState === "undefined") { + pageStateSetter((prevState) => ({ + ...prevState, + + isLoggedIn: false, + error: { + title: i18n.str`Page has a problem: logged in but backend state is lost.`, + }, + })); + return

Error: waiting for details...

; + } + console.log("Showing the profile page.."); + return ( + + + + ); +} + +/** + * Factor out login credentials. + */ +function SWRWithCredentials(props: any): VNode { + const { username, password, backendUrl } = props; + const headers = new Headers(); + headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`); + console.log("Likely backend base URL", backendUrl); + return ( + { + return fetch(backendUrl + url || "", { headers }).then((r) => { + if (!r.ok) throw { status: r.status, json: r.json() }; + + return r.json(); + }); + }, + }} + > + {props.children} + + ); +} + +/** + * Show only the account's balance. NOTE: the backend state + * is mostly needed to provide the user's credentials to POST + * to the bank. + */ +function Account(Props: any): VNode { + const { cache } = useSWRConfig(); + const { accountLabel, backendState } = Props; + // Getting the bank account balance: + const endpoint = `access-api/accounts/${accountLabel}`; + const { data, error, mutate } = useSWR(endpoint, { + // refreshInterval: 0, + // revalidateIfStale: false, + // revalidateOnMount: false, + // revalidateOnFocus: false, + // revalidateOnReconnect: false, + }); + const { pageState, pageStateSetter: setPageState } = usePageContext(); + const { + withdrawalInProgress, + withdrawalId, + isLoggedIn, + talerWithdrawUri, + timestamp, + } = pageState; + const { i18n } = useTranslationContext(); + useEffect(() => { + mutate(); + }, [timestamp]); + + /** + * This part shows a list of transactions: with 5 elements by + * default and offers a "load more" button. + */ + const [txPageNumber, setTxPageNumber] = useTransactionPageNumber(); + const txsPages = []; + for (let i = 0; i <= txPageNumber; i++) + txsPages.push(); + + if (typeof error !== "undefined") { + console.log("account error", error, endpoint); + /** + * FIXME: to minimize the code, try only one invocation + * of pageStateSetter, after having decided the error + * message in the case-branch. + */ + switch (error.status) { + case 404: { + setPageState((prevState: PageStateType) => ({ + ...prevState, + + isLoggedIn: false, + error: { + title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`, + }, + })); + + /** + * 404 should never stick to the cache, because they + * taint successful future registrations. How? After + * registering, the user gets navigated to this page, + * therefore a previous 404 on this SWR key (the requested + * resource) would still appear as valid and cause this + * page not to be shown! A typical case is an attempted + * login of a unregistered user X, and then a registration + * attempt of the same user X: in this case, the failed + * login would cache a 404 error to X's profile, resulting + * in the legitimate request after the registration to still + * be flagged as 404. Clearing the cache should prevent + * this. */ + (cache as any).clear(); + return

Profile not found...

; + } + case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: { + setPageState((prevState: PageStateType) => ({ + ...prevState, + + isLoggedIn: false, + error: { + title: i18n.str`Wrong credentials given.`, + }, + })); + return

Wrong credentials...

; + } + default: { + setPageState((prevState: PageStateType) => ({ + ...prevState, + + isLoggedIn: false, + error: { + title: i18n.str`Account information could not be retrieved.`, + debug: JSON.stringify(error), + }, + })); + return

Unknown problem...

; + } + } + } + const balance = !data ? undefined : Amounts.parseOrThrow(data.balance.amount); + const accountNumber = !data ? undefined : getIbanFromPayto(data.paytoUri); + const balanceIsDebit = data && data.balance.credit_debit_indicator == "debit"; + + /** + * 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 + * the outcome. + */ + console.log(`maybe new withdrawal ${talerWithdrawUri}`); + if (talerWithdrawUri) { + console.log("Bank created a new Taler withdrawal"); + return ( + + + + ); + } + const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance); + + return ( + +
+

+ + Welcome, + {accountNumber + ? `${accountLabel} (${accountNumber})` + : accountLabel} + ! + +

+
+
+
+

{i18n.str`Bank account balance`}

+ {!balance ? ( +
+ Waiting server response... +
+ ) : ( +
+ {balanceIsDebit ? - : null} + {`${balanceValue}`}  + {`${balance.currency}`} +
+ )} +
+
+
+
+

{i18n.str`Payments`}

+ +
+
+
+
+

{i18n.str`Latest transactions:`}

+ +
+
+
+ ); +} + +function useTransactionPageNumber(): [number, StateUpdater] { + const ret = hooks.useNotNullLocalStorage("transaction-page", "0"); + const retObj = JSON.parse(ret[0]); + const retSetter: StateUpdater = function (val) { + const newVal = + val instanceof Function + ? JSON.stringify(val(retObj)) + : JSON.stringify(val); + ret[1](newVal); + }; + return [retObj, retSetter]; +} diff --git a/packages/demobank-ui/src/pages/home/LoginForm.tsx b/packages/demobank-ui/src/pages/home/LoginForm.tsx new file mode 100644 index 000000000..f60c9f600 --- /dev/null +++ b/packages/demobank-ui/src/pages/home/LoginForm.tsx @@ -0,0 +1,149 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +import { h, VNode } from "preact"; +import { route } from "preact-router"; +import { StateUpdater, useEffect, useRef, useState } from "preact/hooks"; +import { PageStateType, usePageContext } from "../../context/pageState.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { BackendStateType, useBackendState } from "../../hooks/backend.js"; +import { bankUiSettings } from "../../settings.js"; +import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js"; +import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; + +/** + * Collect and submit login data. + */ +export function LoginForm(): VNode { + const [backendState, backendStateSetter] = useBackendState(); + const { pageState, pageStateSetter } = usePageContext(); + const [username, setUsername] = useState(); + const [password, setPassword] = useState(); + const { i18n } = useTranslationContext(); + const ref = useRef(null); + useEffect(() => { + ref.current?.focus(); + }, []); + + const errors = undefinedIfEmpty({ + username: !username ? i18n.str`Missing username` : undefined, + password: !password ? i18n.str`Missing password` : undefined, + }); + + return ( +