diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/AccountPage.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage.tsx | 283 |
1 files changed, 77 insertions, 206 deletions
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx index 8d29bd933..769e85804 100644 --- a/packages/demobank-ui/src/pages/AccountPage.tsx +++ b/packages/demobank-ui/src/pages/AccountPage.tsx @@ -14,206 +14,52 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; -import { ComponentChildren, Fragment, h, VNode } from "preact"; -import { useEffect } from "preact/hooks"; -import useSWR, { SWRConfig, useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { PageStateType, usePageContext } from "../context/pageState.js"; -import { BackendInfo } from "../hooks/backend.js"; -import { bankUiSettings } from "../settings.js"; -import { getIbanFromPayto, prepareHeaders } from "../utils.js"; -import { BankFrame } from "./BankFrame.js"; -import { LoginForm } from "./LoginForm.js"; -import { PaymentOptions } from "./PaymentOptions.js"; +import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; +import { + HttpResponsePaginated, + useTranslationContext, +} from "@gnu-taler/web-util/lib/index.browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Cashouts } from "../components/Cashouts/index.js"; import { Transactions } from "../components/Transactions/index.js"; -import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; - -export function AccountPage(): VNode { - const backend = useBackendContext(); - const { i18n } = useTranslationContext(); - - if (backend.state.status === "loggedOut") { - return ( - <BankFrame> - <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> - <LoginForm /> - </BankFrame> - ); - } - - return ( - <SWRWithCredentials info={backend.state}> - <Account accountLabel={backend.state.username} /> - </SWRWithCredentials> - ); -} - -/** - * Factor out login credentials. - */ -function SWRWithCredentials({ - children, - info, -}: { - children: ComponentChildren; - info: BackendInfo; -}): VNode { - const { username, password, url: backendUrl } = info; - const headers = prepareHeaders(username, password); - return ( - <SWRConfig - value={{ - fetcher: (url: string) => { - return fetch(new URL(url, backendUrl).href, { headers }).then((r) => { - if (!r.ok) throw { status: r.status, json: r.json() }; +import { useAccountDetails } from "../hooks/access.js"; +import { PaymentOptions } from "./PaymentOptions.js"; - return r.json(); - }); - }, - }} - > - {children as any} - </SWRConfig> - ); +interface Props { + account: string; + onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; } - -const logger = new Logger("AccountPage"); - /** - * Show only the account's balance. NOTE: the backend state - * is mostly needed to provide the user's credentials to POST - * to the bank. + * Query account information and show QR code if there is pending withdrawal */ -function Account({ accountLabel }: { accountLabel: string }): VNode { - const { cache } = useSWRConfig(); - - // 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 backend = useBackendContext(); - const { pageState, pageStateSetter: setPageState } = usePageContext(); - const { withdrawalId, talerWithdrawUri, timestamp } = pageState; +export function AccountPage({ account, onLoadNotOk }: Props): VNode { + const result = useAccountDetails(account); 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(<Transactions accountLabel={accountLabel} pageNumber={i} />); - // } - - if (typeof error !== "undefined") { - logger.error("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: { - backend.clear(); - setPageState((prevState: PageStateType) => ({ - ...prevState, - - 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 <p>Profile not found...</p>; - } - case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: { - backend.clear(); - setPageState((prevState: PageStateType) => ({ - ...prevState, - error: { - title: i18n.str`Wrong credentials given.`, - }, - })); - return <p>Wrong credentials...</p>; - } - default: { - backend.clear(); - setPageState((prevState: PageStateType) => ({ - ...prevState, - error: { - title: i18n.str`Account information could not be retrieved.`, - debug: JSON.stringify(error), - }, - })); - return <p>Unknown problem...</p>; - } - } + if (!result.ok) { + return onLoadNotOk(result); } - const balance = !data ? undefined : Amounts.parse(data.balance.amount); - const errorParsingBalance = data && !balance; - 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. - */ - if (talerWithdrawUri && withdrawalId) { - logger.trace("Bank created a new Taler withdrawal"); + const { data } = result; + const balance = Amounts.parse(data.balance.amount); + const errorParsingBalance = !balance; + const payto = parsePaytoUri(data.paytoUri); + if (!payto || !payto.isKnown || payto.targetType !== "iban") { return ( - <BankFrame> - <WithdrawalQRCode - withdrawalId={withdrawalId} - talerWithdrawUri={talerWithdrawUri} - /> - </BankFrame> + <div>Payto from server is not valid "{data.paytoUri}"</div> ); } - const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance); + const accountNumber = payto.iban; + const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; return ( - <BankFrame> + <Fragment> <div> <h1 class="nav welcome-text"> <i18n.Translate> Welcome, - {accountNumber - ? `${accountLabel} (${accountNumber})` - : accountLabel} - ! + {accountNumber ? `${account} (${accountNumber})` : account}! </i18n.Translate> </h1> </div> @@ -239,7 +85,10 @@ function Account({ accountLabel }: { accountLabel: string }): VNode { ) : ( <div class="large-amount amount"> {balanceIsDebit ? <b>-</b> : null} - <span class="value">{`${balanceValue}`}</span> + <span class="value">{`${Amounts.stringifyValue( + balance, + )}`}</span> + <span class="currency">{`${balance.currency}`}</span> </div> )} @@ -248,34 +97,56 @@ function Account({ accountLabel }: { accountLabel: string }): VNode { <section id="payments"> <div class="payments"> <h2>{i18n.str`Payments`}</h2> - <PaymentOptions currency={balance?.currency} /> + <PaymentOptions currency={balance.currency} /> </div> </section> </Fragment> )} - <section id="main"> - <article> - <h2>{i18n.str`Latest transactions:`}</h2> - <Transactions - balanceValue={balanceValue} - pageNumber={0} - accountLabel={accountLabel} - /> - </article> + + <section style={{ marginTop: "2em" }}> + <Moves account={account} /> </section> - </BankFrame> + </Fragment> ); } -// 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]; -// } +function Moves({ account }: { account: string }): VNode { + const [tab, setTab] = useState<"transactions" | "cashouts">("transactions"); + const { i18n } = useTranslationContext(); + return ( + <article> + <div class="payments"> + <div class="tab"> + <button + class={tab === "transactions" ? "tablinks active" : "tablinks"} + onClick={(): void => { + setTab("transactions"); + }} + > + {i18n.str`Transactions`} + </button> + <button + class={tab === "cashouts" ? "tablinks active" : "tablinks"} + onClick={(): void => { + setTab("cashouts"); + }} + > + {i18n.str`Cashouts`} + </button> + </div> + {tab === "transactions" && ( + <div class="active"> + <h3>{i18n.str`Latest transactions`}</h3> + <Transactions account={account} /> + </div> + )} + {tab === "cashouts" && ( + <div class="active"> + <h3>{i18n.str`Latest cashouts`}</h3> + <Cashouts account={account} /> + </div> + )} + </div> + </article> + ); +} |