2022-12-07 16:38:50 +01:00
|
|
|
/*
|
|
|
|
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 <http://www.gnu.org/licenses/>
|
|
|
|
*/
|
|
|
|
|
2022-12-07 20:07:42 +01:00
|
|
|
import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
2022-12-09 15:58:39 +01:00
|
|
|
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
2022-12-07 19:44:16 +01:00
|
|
|
import { ComponentChildren, Fragment, h, VNode } from "preact";
|
2022-12-07 22:22:56 +01:00
|
|
|
import { useEffect } from "preact/hooks";
|
2022-12-07 16:38:50 +01:00
|
|
|
import useSWR, { SWRConfig, useSWRConfig } from "swr";
|
2022-12-09 16:15:15 +01:00
|
|
|
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";
|
2022-12-07 16:38:50 +01:00
|
|
|
import { BankFrame } from "./BankFrame.js";
|
|
|
|
import { LoginForm } from "./LoginForm.js";
|
|
|
|
import { PaymentOptions } from "./PaymentOptions.js";
|
2022-12-14 19:35:28 +01:00
|
|
|
import { Transactions } from "../components/Transactions/index.js";
|
2022-12-07 22:22:56 +01:00
|
|
|
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
2022-12-07 16:38:50 +01:00
|
|
|
|
|
|
|
export function AccountPage(): VNode {
|
2022-12-07 19:44:16 +01:00
|
|
|
const backend = useBackendContext();
|
2022-12-07 16:38:50 +01:00
|
|
|
const { i18n } = useTranslationContext();
|
|
|
|
|
2022-12-07 19:44:16 +01:00
|
|
|
if (backend.state.status === "loggedOut") {
|
2022-12-07 16:38:50 +01:00
|
|
|
return (
|
|
|
|
<BankFrame>
|
|
|
|
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
|
|
|
|
<LoginForm />
|
|
|
|
</BankFrame>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2022-12-07 19:44:16 +01:00
|
|
|
<SWRWithCredentials info={backend.state}>
|
|
|
|
<Account accountLabel={backend.state.username} />
|
2022-12-07 16:38:50 +01:00
|
|
|
</SWRWithCredentials>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Factor out login credentials.
|
|
|
|
*/
|
2022-12-07 19:44:16 +01:00
|
|
|
function SWRWithCredentials({
|
|
|
|
children,
|
|
|
|
info,
|
|
|
|
}: {
|
|
|
|
children: ComponentChildren;
|
|
|
|
info: BackendInfo;
|
|
|
|
}): VNode {
|
|
|
|
const { username, password, url: backendUrl } = info;
|
|
|
|
const headers = prepareHeaders(username, password);
|
2022-12-07 16:38:50 +01:00
|
|
|
return (
|
|
|
|
<SWRConfig
|
|
|
|
value={{
|
|
|
|
fetcher: (url: string) => {
|
2022-12-07 19:44:16 +01:00
|
|
|
return fetch(new URL(url, backendUrl).href, { headers }).then((r) => {
|
2022-12-07 16:38:50 +01:00
|
|
|
if (!r.ok) throw { status: r.status, json: r.json() };
|
|
|
|
|
|
|
|
return r.json();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
>
|
2022-12-07 19:44:16 +01:00
|
|
|
{children as any}
|
2022-12-07 16:38:50 +01:00
|
|
|
</SWRConfig>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-07 20:07:42 +01:00
|
|
|
const logger = new Logger("AccountPage");
|
|
|
|
|
2022-12-07 16:38:50 +01:00
|
|
|
/**
|
|
|
|
* Show only the account's balance. NOTE: the backend state
|
|
|
|
* is mostly needed to provide the user's credentials to POST
|
|
|
|
* to the bank.
|
|
|
|
*/
|
2022-12-07 19:44:16 +01:00
|
|
|
function Account({ accountLabel }: { accountLabel: string }): VNode {
|
2022-12-07 16:38:50 +01:00
|
|
|
const { cache } = useSWRConfig();
|
2022-12-07 19:44:16 +01:00
|
|
|
|
2022-12-07 16:38:50 +01:00
|
|
|
// 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,
|
|
|
|
});
|
2022-12-07 19:44:16 +01:00
|
|
|
const backend = useBackendContext();
|
2022-12-07 16:38:50 +01:00
|
|
|
const { pageState, pageStateSetter: setPageState } = usePageContext();
|
2022-12-07 19:44:16 +01:00
|
|
|
const { withdrawalId, talerWithdrawUri, timestamp } = pageState;
|
2022-12-07 16:38:50 +01:00
|
|
|
const { i18n } = useTranslationContext();
|
|
|
|
useEffect(() => {
|
|
|
|
mutate();
|
|
|
|
}, [timestamp]);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This part shows a list of transactions: with 5 elements by
|
|
|
|
* default and offers a "load more" button.
|
|
|
|
*/
|
2022-12-07 19:44:16 +01:00
|
|
|
// const [txPageNumber, setTxPageNumber] = useTransactionPageNumber();
|
|
|
|
// const txsPages = [];
|
|
|
|
// for (let i = 0; i <= txPageNumber; i++) {
|
|
|
|
// txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />);
|
|
|
|
// }
|
2022-12-07 16:38:50 +01:00
|
|
|
|
|
|
|
if (typeof error !== "undefined") {
|
2022-12-07 22:22:56 +01:00
|
|
|
logger.error("account error", error, endpoint);
|
2022-12-07 16:38:50 +01:00
|
|
|
/**
|
|
|
|
* 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: {
|
2022-12-07 19:44:16 +01:00
|
|
|
backend.clear();
|
2022-12-07 16:38:50 +01:00
|
|
|
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: {
|
2022-12-07 19:44:16 +01:00
|
|
|
backend.clear();
|
2022-12-07 16:38:50 +01:00
|
|
|
setPageState((prevState: PageStateType) => ({
|
|
|
|
...prevState,
|
|
|
|
error: {
|
|
|
|
title: i18n.str`Wrong credentials given.`,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
return <p>Wrong credentials...</p>;
|
|
|
|
}
|
|
|
|
default: {
|
2022-12-07 19:44:16 +01:00
|
|
|
backend.clear();
|
2022-12-07 16:38:50 +01:00
|
|
|
setPageState((prevState: PageStateType) => ({
|
|
|
|
...prevState,
|
|
|
|
error: {
|
|
|
|
title: i18n.str`Account information could not be retrieved.`,
|
|
|
|
debug: JSON.stringify(error),
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
return <p>Unknown problem...</p>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-20 14:16:39 +01:00
|
|
|
const balance = !data ? undefined : Amounts.parse(data.balance.amount);
|
|
|
|
const errorParsingBalance = data && !balance;
|
2022-12-07 16:38:50 +01:00
|
|
|
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.
|
|
|
|
*/
|
2022-12-07 19:44:16 +01:00
|
|
|
if (talerWithdrawUri && withdrawalId) {
|
2022-12-07 20:07:42 +01:00
|
|
|
logger.trace("Bank created a new Taler withdrawal");
|
2022-12-07 16:38:50 +01:00
|
|
|
return (
|
|
|
|
<BankFrame>
|
2022-12-07 20:07:42 +01:00
|
|
|
<WithdrawalQRCode
|
2022-12-07 16:38:50 +01:00
|
|
|
withdrawalId={withdrawalId}
|
|
|
|
talerWithdrawUri={talerWithdrawUri}
|
|
|
|
/>
|
|
|
|
</BankFrame>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<BankFrame>
|
|
|
|
<div>
|
|
|
|
<h1 class="nav welcome-text">
|
|
|
|
<i18n.Translate>
|
|
|
|
Welcome,
|
|
|
|
{accountNumber
|
|
|
|
? `${accountLabel} (${accountNumber})`
|
|
|
|
: accountLabel}
|
|
|
|
!
|
|
|
|
</i18n.Translate>
|
|
|
|
</h1>
|
|
|
|
</div>
|
2022-12-20 14:16:39 +01:00
|
|
|
|
|
|
|
{errorParsingBalance ? (
|
|
|
|
<div class="informational informational-fail" style={{ marginTop: 8 }}>
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
<p>
|
|
|
|
<b>Server Error: invalid balance</b>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
<p>Your account is in an invalid state.</p>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<Fragment>
|
|
|
|
<section id="assets">
|
|
|
|
<div class="asset-summary">
|
|
|
|
<h2>{i18n.str`Bank account balance`}</h2>
|
|
|
|
{!balance ? (
|
|
|
|
<div class="large-amount" style={{ color: "gray" }}>
|
|
|
|
Waiting server response...
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<div class="large-amount amount">
|
|
|
|
{balanceIsDebit ? <b>-</b> : null}
|
|
|
|
<span class="value">{`${balanceValue}`}</span>
|
|
|
|
<span class="currency">{`${balance.currency}`}</span>
|
|
|
|
</div>
|
|
|
|
)}
|
2022-12-07 16:38:50 +01:00
|
|
|
</div>
|
2022-12-20 14:16:39 +01:00
|
|
|
</section>
|
|
|
|
<section id="payments">
|
|
|
|
<div class="payments">
|
|
|
|
<h2>{i18n.str`Payments`}</h2>
|
|
|
|
<PaymentOptions currency={balance?.currency} />
|
2022-12-07 16:38:50 +01:00
|
|
|
</div>
|
2022-12-20 14:16:39 +01:00
|
|
|
</section>
|
|
|
|
</Fragment>
|
|
|
|
)}
|
2022-12-07 16:38:50 +01:00
|
|
|
<section id="main">
|
|
|
|
<article>
|
|
|
|
<h2>{i18n.str`Latest transactions:`}</h2>
|
|
|
|
<Transactions
|
|
|
|
balanceValue={balanceValue}
|
2022-12-07 19:44:16 +01:00
|
|
|
pageNumber={0}
|
2022-12-07 16:38:50 +01:00
|
|
|
accountLabel={accountLabel}
|
|
|
|
/>
|
|
|
|
</article>
|
|
|
|
</section>
|
|
|
|
</BankFrame>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-07 20:07:42 +01:00
|
|
|
// function useTransactionPageNumber(): [number, StateUpdater<number>] {
|
2022-12-09 15:58:39 +01:00
|
|
|
// const ret = useNotNullLocalStorage("transaction-page", "0");
|
2022-12-07 20:07:42 +01:00
|
|
|
// 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];
|
|
|
|
// }
|