no-fix: remove 'any' and login status is taken from backend

This commit is contained in:
Sebastian 2022-12-07 15:44:16 -03:00
parent 9112655ef5
commit d2554bedf3
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
16 changed files with 264 additions and 225 deletions

View File

@ -1,4 +1,5 @@
import { h, FunctionalComponent } from "preact"; import { h, FunctionalComponent } from "preact";
import { BackendStateProvider } from "../context/backend.js";
import { PageStateProvider } from "../context/pageState.js"; import { PageStateProvider } from "../context/pageState.js";
import { TranslationProvider } from "../context/translation.js"; import { TranslationProvider } from "../context/translation.js";
import { Routing } from "../pages/Routing.js"; import { Routing } from "../pages/Routing.js";
@ -24,7 +25,9 @@ const App: FunctionalComponent = () => {
return ( return (
<TranslationProvider> <TranslationProvider>
<PageStateProvider> <PageStateProvider>
<Routing /> <BackendStateProvider>
<Routing />
</BackendStateProvider>
</PageStateProvider> </PageStateProvider>
</TranslationProvider> </TranslationProvider>
); );

View File

@ -0,0 +1,52 @@
/*
This file is part of GNU Taler
(C) 2021 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/>
*/
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { BackendStateHandler, defaultState, useBackendState } from "../hooks/backend.js";
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
export type Type = BackendStateHandler;
const initial: Type = {
state: defaultState,
clear() {
null
},
save(info) {
null
},
};
const Context = createContext<Type>(initial);
export const useBackendContext = (): Type => useContext(Context);
export const BackendStateProvider = ({
children,
}: {
children: ComponentChildren;
}): VNode => {
const value = useBackendState();
return h(Context.Provider, {
value,
children,
});
};

View File

@ -29,7 +29,6 @@ export type Type = {
}; };
const initial: Type = { const initial: Type = {
pageState: { pageState: {
isLoggedIn: false,
isRawPayto: false, isRawPayto: false,
withdrawalInProgress: false, withdrawalInProgress: false,
}, },
@ -59,7 +58,6 @@ export const PageStateProvider = ({
*/ */
function usePageState( function usePageState(
state: PageStateType = { state: PageStateType = {
isLoggedIn: false,
isRawPayto: false, isRawPayto: false,
withdrawalInProgress: false, withdrawalInProgress: false,
}, },
@ -98,7 +96,6 @@ function usePageState(
* Track page state. * Track page state.
*/ */
export interface PageStateType { export interface PageStateType {
isLoggedIn: boolean;
isRawPayto: boolean; isRawPayto: boolean;
withdrawalInProgress: boolean; withdrawalInProgress: boolean;
error?: { error?: {

View File

@ -20,7 +20,7 @@
*/ */
import { i18n, setupI18n } from "@gnu-taler/taler-util"; import { i18n, setupI18n } from "@gnu-taler/taler-util";
import { createContext, h, VNode } from "preact"; import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { hooks } from "@gnu-taler/web-util/lib/index.browser";
import { strings } from "../i18n/strings.js"; import { strings } from "../i18n/strings.js";
@ -60,7 +60,7 @@ const Context = createContext<Type>(initial);
interface Props { interface Props {
initial?: string; initial?: string;
children: any; children: ComponentChildren;
forceLang?: string; forceLang?: string;
} }

View File

@ -1,33 +1,55 @@
import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { hooks } from "@gnu-taler/web-util/lib/index.browser";
import { StateUpdater } from "preact/hooks";
/** /**
* Has the information to reach and * Has the information to reach and
* authenticate at the bank's backend. * authenticate at the bank's backend.
*/ */
export interface BackendStateType { export type BackendState = LoggedIn | LoggedOut
url?: string;
username?: string; export interface BackendInfo {
password?: string; url: string;
username: string;
password: string;
} }
interface LoggedIn extends BackendInfo {
status: "loggedIn"
}
interface LoggedOut {
status: "loggedOut"
}
export const defaultState: BackendState = { status: "loggedOut" }
export interface BackendStateHandler {
state: BackendState,
clear(): void;
save(info: BackendInfo): void;
}
/** /**
* Return getters and setters for * Return getters and setters for
* login credentials and backend's * login credentials and backend's
* base URL. * base URL.
*/ */
type BackendStateTypeOpt = BackendStateType | undefined; export function useBackendState(): BackendStateHandler {
export function useBackendState( const [value, update] = hooks.useLocalStorage("backend-state", JSON.stringify(defaultState));
state?: BackendStateType, // const parsed = value !== undefined ? JSON.parse(value) : value;
): [BackendStateTypeOpt, StateUpdater<BackendStateTypeOpt>] { let parsed
const ret = hooks.useLocalStorage("backend-state", JSON.stringify(state)); try {
const retObj: BackendStateTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0]; parsed = JSON.parse(value!)
const retSetter: StateUpdater<BackendStateTypeOpt> = function (val) { } catch {
const newVal = parsed = undefined
val instanceof Function }
? JSON.stringify(val(retObj)) const state: BackendState = !parsed?.status ? defaultState : parsed
: JSON.stringify(val);
ret[1](newVal); return {
}; state,
return [retObj, retSetter]; clear() {
update(JSON.stringify(defaultState))
},
save(info) {
const nextState: BackendState = { status: "loggedIn", ...info }
update(JSON.stringify(nextState))
},
}
} }

View File

@ -16,14 +16,15 @@
import { Amounts, HttpStatusCode } from "@gnu-taler/taler-util"; import { Amounts, HttpStatusCode } from "@gnu-taler/taler-util";
import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { hooks } from "@gnu-taler/web-util/lib/index.browser";
import { h, Fragment, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import { StateUpdater, useEffect, useState } from "preact/hooks"; import { StateUpdater, useEffect } from "preact/hooks";
import useSWR, { SWRConfig, useSWRConfig } from "swr"; import useSWR, { SWRConfig, useSWRConfig } from "swr";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { useBackendState } from "../../hooks/backend.js"; import { BackendInfo } from "../../hooks/backend.js";
import { bankUiSettings } from "../../settings.js"; import { bankUiSettings } from "../../settings.js";
import { getIbanFromPayto } from "../../utils.js"; import { getIbanFromPayto, prepareHeaders } from "../../utils.js";
import { BankFrame } from "./BankFrame.js"; import { BankFrame } from "./BankFrame.js";
import { LoginForm } from "./LoginForm.js"; import { LoginForm } from "./LoginForm.js";
import { PaymentOptions } from "./PaymentOptions.js"; import { PaymentOptions } from "./PaymentOptions.js";
@ -31,11 +32,10 @@ import { TalerWithdrawalQRCode } from "./TalerWithdrawalQRCode.js";
import { Transactions } from "./Transactions.js"; import { Transactions } from "./Transactions.js";
export function AccountPage(): VNode { export function AccountPage(): VNode {
const [backendState, backendStateSetter] = useBackendState(); const backend = useBackendContext();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const { pageState, pageStateSetter } = usePageContext();
if (!pageState.isLoggedIn) { if (backend.state.status === "loggedOut") {
return ( return (
<BankFrame> <BankFrame>
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
@ -44,28 +44,9 @@ export function AccountPage(): VNode {
); );
} }
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 <p>Error: waiting for details...</p>;
}
console.log("Showing the profile page..");
return ( return (
<SWRWithCredentials <SWRWithCredentials info={backend.state}>
username={backendState.username} <Account accountLabel={backend.state.username} />
password={backendState.password}
backendUrl={backendState.url}
>
<Account
accountLabel={backendState.username}
backendState={backendState}
/>
</SWRWithCredentials> </SWRWithCredentials>
); );
} }
@ -73,16 +54,20 @@ export function AccountPage(): VNode {
/** /**
* Factor out login credentials. * Factor out login credentials.
*/ */
function SWRWithCredentials(props: any): VNode { function SWRWithCredentials({
const { username, password, backendUrl } = props; children,
const headers = new Headers(); info,
headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`); }: {
console.log("Likely backend base URL", backendUrl); children: ComponentChildren;
info: BackendInfo;
}): VNode {
const { username, password, url: backendUrl } = info;
const headers = prepareHeaders(username, password);
return ( return (
<SWRConfig <SWRConfig
value={{ value={{
fetcher: (url: string) => { fetcher: (url: string) => {
return fetch(backendUrl + url || "", { headers }).then((r) => { return fetch(new URL(url, backendUrl).href, { headers }).then((r) => {
if (!r.ok) throw { status: r.status, json: r.json() }; if (!r.ok) throw { status: r.status, json: r.json() };
return r.json(); return r.json();
@ -90,7 +75,7 @@ function SWRWithCredentials(props: any): VNode {
}, },
}} }}
> >
{props.children} {children as any}
</SWRConfig> </SWRConfig>
); );
} }
@ -100,9 +85,9 @@ function SWRWithCredentials(props: any): VNode {
* is mostly needed to provide the user's credentials to POST * is mostly needed to provide the user's credentials to POST
* to the bank. * to the bank.
*/ */
function Account(Props: any): VNode { function Account({ accountLabel }: { accountLabel: string }): VNode {
const { cache } = useSWRConfig(); const { cache } = useSWRConfig();
const { accountLabel, backendState } = Props;
// Getting the bank account balance: // Getting the bank account balance:
const endpoint = `access-api/accounts/${accountLabel}`; const endpoint = `access-api/accounts/${accountLabel}`;
const { data, error, mutate } = useSWR(endpoint, { const { data, error, mutate } = useSWR(endpoint, {
@ -112,14 +97,9 @@ function Account(Props: any): VNode {
// revalidateOnFocus: false, // revalidateOnFocus: false,
// revalidateOnReconnect: false, // revalidateOnReconnect: false,
}); });
const backend = useBackendContext();
const { pageState, pageStateSetter: setPageState } = usePageContext(); const { pageState, pageStateSetter: setPageState } = usePageContext();
const { const { withdrawalId, talerWithdrawUri, timestamp } = pageState;
withdrawalInProgress,
withdrawalId,
isLoggedIn,
talerWithdrawUri,
timestamp,
} = pageState;
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
useEffect(() => { useEffect(() => {
mutate(); mutate();
@ -129,10 +109,11 @@ function Account(Props: any): VNode {
* This part shows a list of transactions: with 5 elements by * This part shows a list of transactions: with 5 elements by
* default and offers a "load more" button. * default and offers a "load more" button.
*/ */
const [txPageNumber, setTxPageNumber] = useTransactionPageNumber(); // const [txPageNumber, setTxPageNumber] = useTransactionPageNumber();
const txsPages = []; // const txsPages = [];
for (let i = 0; i <= txPageNumber; i++) // for (let i = 0; i <= txPageNumber; i++) {
txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />); // txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />);
// }
if (typeof error !== "undefined") { if (typeof error !== "undefined") {
console.log("account error", error, endpoint); console.log("account error", error, endpoint);
@ -143,10 +124,10 @@ function Account(Props: any): VNode {
*/ */
switch (error.status) { switch (error.status) {
case 404: { case 404: {
backend.clear();
setPageState((prevState: PageStateType) => ({ setPageState((prevState: PageStateType) => ({
...prevState, ...prevState,
isLoggedIn: false,
error: { error: {
title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`, title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`,
}, },
@ -170,10 +151,9 @@ function Account(Props: any): VNode {
} }
case HttpStatusCode.Unauthorized: case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden: { case HttpStatusCode.Forbidden: {
backend.clear();
setPageState((prevState: PageStateType) => ({ setPageState((prevState: PageStateType) => ({
...prevState, ...prevState,
isLoggedIn: false,
error: { error: {
title: i18n.str`Wrong credentials given.`, title: i18n.str`Wrong credentials given.`,
}, },
@ -181,10 +161,9 @@ function Account(Props: any): VNode {
return <p>Wrong credentials...</p>; return <p>Wrong credentials...</p>;
} }
default: { default: {
backend.clear();
setPageState((prevState: PageStateType) => ({ setPageState((prevState: PageStateType) => ({
...prevState, ...prevState,
isLoggedIn: false,
error: { error: {
title: i18n.str`Account information could not be retrieved.`, title: i18n.str`Account information could not be retrieved.`,
debug: JSON.stringify(error), debug: JSON.stringify(error),
@ -211,13 +190,11 @@ function Account(Props: any): VNode {
* the outcome. * the outcome.
*/ */
console.log(`maybe new withdrawal ${talerWithdrawUri}`); console.log(`maybe new withdrawal ${talerWithdrawUri}`);
if (talerWithdrawUri) { if (talerWithdrawUri && withdrawalId) {
console.log("Bank created a new Taler withdrawal"); console.log("Bank created a new Taler withdrawal");
return ( return (
<BankFrame> <BankFrame>
<TalerWithdrawalQRCode <TalerWithdrawalQRCode
accountLabel={accountLabel}
backendState={backendState}
withdrawalId={withdrawalId} withdrawalId={withdrawalId}
talerWithdrawUri={talerWithdrawUri} talerWithdrawUri={talerWithdrawUri}
/> />
@ -266,7 +243,7 @@ function Account(Props: any): VNode {
<h2>{i18n.str`Latest transactions:`}</h2> <h2>{i18n.str`Latest transactions:`}</h2>
<Transactions <Transactions
balanceValue={balanceValue} balanceValue={balanceValue}
pageNumber="0" pageNumber={0}
accountLabel={accountLabel} accountLabel={accountLabel}
/> />
</article> </article>

View File

@ -14,15 +14,21 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import talerLogo from "../../assets/logo-white.svg"; import talerLogo from "../../assets/logo-white.svg";
import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js"; import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { bankUiSettings } from "../../settings.js"; import { bankUiSettings } from "../../settings.js";
export function BankFrame(Props: any): VNode { export function BankFrame({
children,
}: {
children: ComponentChildren;
}): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const backend = useBackendContext();
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
console.log("BankFrame state", pageState); console.log("BankFrame state", pageState);
const logOut = ( const logOut = (
@ -33,9 +39,9 @@ export function BankFrame(Props: any): VNode {
onClick={() => { onClick={() => {
pageStateSetter((prevState: PageStateType) => { pageStateSetter((prevState: PageStateType) => {
const { talerWithdrawUri, withdrawalId, ...rest } = prevState; const { talerWithdrawUri, withdrawalId, ...rest } = prevState;
backend.clear();
return { return {
...rest, ...rest,
isLoggedIn: false,
withdrawalInProgress: false, withdrawalInProgress: false,
error: undefined, error: undefined,
info: undefined, info: undefined,
@ -98,10 +104,10 @@ export function BankFrame(Props: any): VNode {
</nav> </nav>
</div> </div>
<section id="main" class="content"> <section id="main" class="content">
<ErrorBanner pageState={[pageState, pageStateSetter]} /> <ErrorBanner />
<StatusBanner pageState={[pageState, pageStateSetter]} /> <StatusBanner />
{pageState.isLoggedIn ? logOut : null} {backend.state.status === "loggedIn" ? logOut : null}
{Props.children} {children}
</section> </section>
<section id="footer" class="footer"> <section id="footer" class="footer">
<div class="footer"> <div class="footer">
@ -127,9 +133,9 @@ function maybeDemoContent(content: VNode): VNode {
return <Fragment />; return <Fragment />;
} }
function ErrorBanner(Props: any): VNode | null { function ErrorBanner(): VNode | null {
const [pageState, pageStateSetter] = Props.pageState; const { pageState, pageStateSetter } = usePageContext();
// const { i18n } = useTranslationContext();
if (!pageState.error) return null; if (!pageState.error) return null;
const rval = ( const rval = (
@ -144,7 +150,7 @@ function ErrorBanner(Props: any): VNode | null {
class="pure-button" class="pure-button"
value="Clear" value="Clear"
onClick={async () => { onClick={async () => {
pageStateSetter((prev: any) => ({ ...prev, error: undefined })); pageStateSetter((prev) => ({ ...prev, error: undefined }));
}} }}
/> />
</div> </div>
@ -156,8 +162,8 @@ function ErrorBanner(Props: any): VNode | null {
return rval; return rval;
} }
function StatusBanner(Props: any): VNode | null { function StatusBanner(): VNode | null {
const [pageState, pageStateSetter] = Props.pageState; const { pageState, pageStateSetter } = usePageContext();
if (!pageState.info) return null; if (!pageState.info) return null;
const rval = ( const rval = (
@ -172,7 +178,7 @@ function StatusBanner(Props: any): VNode | null {
class="pure-button" class="pure-button"
value="Clear" value="Clear"
onClick={async () => { onClick={async () => {
pageStateSetter((prev: any) => ({ ...prev, info: undefined })); pageStateSetter((prev) => ({ ...prev, info: undefined }));
}} }}
/> />
</div> </div>

View File

@ -16,10 +16,10 @@
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { route } from "preact-router"; import { route } from "preact-router";
import { StateUpdater, useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { useBackendContext } from "../../context/backend.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { BackendStateType, useBackendState } from "../../hooks/backend.js"; import { BackendStateHandler } from "../../hooks/backend.js";
import { bankUiSettings } from "../../settings.js"; import { bankUiSettings } from "../../settings.js";
import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js"; import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@ -28,8 +28,7 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
* Collect and submit login data. * Collect and submit login data.
*/ */
export function LoginForm(): VNode { export function LoginForm(): VNode {
const [backendState, backendStateSetter] = useBackendState(); const backend = useBackendContext();
const { pageState, pageStateSetter } = usePageContext();
const [username, setUsername] = useState<string | undefined>(); const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -93,11 +92,7 @@ export function LoginForm(): VNode {
disabled={!!errors} disabled={!!errors}
onClick={() => { onClick={() => {
if (!username || !password) return; if (!username || !password) return;
loginCall( loginCall({ username, password }, backend);
{ username, password },
backendStateSetter,
pageStateSetter,
);
setUsername(undefined); setUsername(undefined);
setPassword(undefined); setPassword(undefined);
}} }}
@ -129,21 +124,16 @@ async function loginCall(
* FIXME: figure out if the two following * FIXME: figure out if the two following
* functions can be retrieved from the state. * functions can be retrieved from the state.
*/ */
backendStateSetter: StateUpdater<BackendStateType | undefined>, backend: BackendStateHandler,
pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> { ): Promise<void> {
/** /**
* Optimistically setting the state as 'logged in', and * Optimistically setting the state as 'logged in', and
* let the Account component request the balance to check * let the Account component request the balance to check
* whether the credentials are valid. */ * whether the credentials are valid. */
pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
let baseUrl = getBankBackendBaseUrl();
if (!baseUrl.endsWith("/")) baseUrl += "/";
backendStateSetter((prevState) => ({ backend.save({
...prevState, url: getBankBackendBaseUrl(),
url: baseUrl,
username: req.username, username: req.username,
password: req.password, password: req.password,
})); });
} }

View File

@ -18,9 +18,10 @@ import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { hooks } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { StateUpdater, useEffect, useRef, useState } from "preact/hooks"; import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { BackendStateType, useBackendState } from "../../hooks/backend.js"; import { BackendState } from "../../hooks/backend.js";
import { prepareHeaders, undefinedIfEmpty } from "../../utils.js"; import { prepareHeaders, undefinedIfEmpty } from "../../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@ -31,7 +32,7 @@ export function PaytoWireTransferForm({
focus?: boolean; focus?: boolean;
currency?: string; currency?: string;
}): VNode { }): VNode {
const [backendState, backendStateSetter] = useBackendState(); const backend = useBackendContext();
const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button? const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
const [submitData, submitDataSetter] = useWireTransferRequestType(); const [submitData, submitDataSetter] = useWireTransferRequestType();
@ -81,7 +82,7 @@ export function PaytoWireTransferForm({
required required
pattern={ibanRegex} pattern={ibanRegex}
onInput={(e): void => { onInput={(e): void => {
submitDataSetter((submitData: any) => ({ submitDataSetter((submitData) => ({
...submitData, ...submitData,
iban: e.currentTarget.value, iban: e.currentTarget.value,
})); }));
@ -102,7 +103,7 @@ export function PaytoWireTransferForm({
value={submitData?.subject ?? ""} value={submitData?.subject ?? ""}
required required
onInput={(e): void => { onInput={(e): void => {
submitDataSetter((submitData: any) => ({ submitDataSetter((submitData) => ({
...submitData, ...submitData,
subject: e.currentTarget.value, subject: e.currentTarget.value,
})); }));
@ -133,7 +134,7 @@ export function PaytoWireTransferForm({
required required
value={submitData?.amount ?? ""} value={submitData?.amount ?? ""}
onInput={(e): void => { onInput={(e): void => {
submitDataSetter((submitData: any) => ({ submitDataSetter((submitData) => ({
...submitData, ...submitData,
amount: e.currentTarget.value, amount: e.currentTarget.value,
})); }));
@ -179,7 +180,7 @@ export function PaytoWireTransferForm({
}; };
return await createTransactionCall( return await createTransactionCall(
transactionData, transactionData,
backendState, backend.state,
pageStateSetter, pageStateSetter,
() => () =>
submitDataSetter((p) => ({ submitDataSetter((p) => ({
@ -209,7 +210,7 @@ export function PaytoWireTransferForm({
href="/account" href="/account"
onClick={() => { onClick={() => {
console.log("switch to raw payto form"); console.log("switch to raw payto form");
pageStateSetter((prevState: any) => ({ pageStateSetter((prevState) => ({
...prevState, ...prevState,
isRawPayto: true, isRawPayto: true,
})); }));
@ -283,7 +284,7 @@ export function PaytoWireTransferForm({
return await createTransactionCall( return await createTransactionCall(
transactionData, transactionData,
backendState, backend.state,
pageStateSetter, pageStateSetter,
() => rawPaytoInputSetter(undefined), () => rawPaytoInputSetter(undefined),
); );
@ -295,7 +296,7 @@ export function PaytoWireTransferForm({
href="/account" href="/account"
onClick={() => { onClick={() => {
console.log("switch to wire-transfer-form"); console.log("switch to wire-transfer-form");
pageStateSetter((prevState: any) => ({ pageStateSetter((prevState) => ({
...prevState, ...prevState,
isRawPayto: false, isRawPayto: false,
})); }));
@ -345,7 +346,7 @@ function useWireTransferRequestType(
*/ */
async function createTransactionCall( async function createTransactionCall(
req: TransactionRequestType, req: TransactionRequestType,
backendState: BackendStateType | undefined, backendState: BackendState,
pageStateSetter: StateUpdater<PageStateType>, pageStateSetter: StateUpdater<PageStateType>,
/** /**
* Optional since the raw payto form doesn't have * Optional since the raw payto form doesn't have
@ -353,13 +354,30 @@ async function createTransactionCall(
*/ */
cleanUpForm: () => void, cleanUpForm: () => void,
): Promise<void> { ): Promise<void> {
let res: any; if (backendState.status === "loggedOut") {
console.log("No credentials found.");
pageStateSetter((prevState) => ({
...prevState,
error: {
title: "No credentials found.",
},
}));
return;
}
let res: Response;
try { try {
res = await postToBackend( const { username, password } = backendState;
`access-api/accounts/${getUsername(backendState)}/transactions`, const headers = prepareHeaders(username, password);
backendState, const url = new URL(
JSON.stringify(req), `access-api/accounts/${backendState.username}/transactions`,
backendState.url,
); );
res = await fetch(url.href, {
method: "POST",
headers,
body: JSON.stringify(req),
});
} catch (error) { } catch (error) {
console.log("Could not POST transaction request to the bank", error); console.log("Could not POST transaction request to the bank", error);
pageStateSetter((prevState) => ({ pageStateSetter((prevState) => ({
@ -402,41 +420,3 @@ async function createTransactionCall(
// be discarded. // be discarded.
cleanUpForm(); cleanUpForm();
} }
/**
* Get username from the backend state, and throw
* exception if not found.
*/
function getUsername(backendState: BackendStateType | undefined): string {
if (typeof backendState === "undefined")
throw Error("Username can't be found in a undefined backend state.");
if (!backendState.username) {
throw Error("No username, must login first.");
}
return backendState.username;
}
/**
* Helps extracting the credentials from the state
* and wraps the actual call to 'fetch'. Should be
* enclosed in a try-catch block by the caller.
*/
async function postToBackend(
uri: string,
backendState: BackendStateType | undefined,
body: string,
): Promise<any> {
if (typeof backendState === "undefined")
throw Error("Credentials can't be found in a undefined backend state.");
const { username, password } = backendState;
const headers = prepareHeaders(username, password);
// Backend URL must have been stored _with_ a final slash.
const url = new URL(uri, backendState.url);
return await fetch(url.href, {
method: "POST",
headers,
body,
});
}

View File

@ -15,7 +15,7 @@
*/ */
import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { hooks } from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import { route } from "preact-router"; import { route } from "preact-router";
import { StateUpdater } from "preact/hooks"; import { StateUpdater } from "preact/hooks";
import useSWR, { SWRConfig } from "swr"; import useSWR, { SWRConfig } from "swr";
@ -35,8 +35,13 @@ export function PublicHistoriesPage(): VNode {
); );
} }
function SWRWithoutCredentials(Props: any): VNode { function SWRWithoutCredentials({
const { baseUrl } = Props; baseUrl,
children,
}: {
children: ComponentChildren;
baseUrl: string;
}): VNode {
console.log("Base URL", baseUrl); console.log("Base URL", baseUrl);
return ( return (
<SWRConfig <SWRConfig
@ -49,7 +54,7 @@ function SWRWithoutCredentials(Props: any): VNode {
}), }),
}} }}
> >
{Props.children} {children as any}
</SWRConfig> </SWRConfig>
); );
} }
@ -93,7 +98,7 @@ function PublicHistories(): VNode {
} }
} }
if (!data) return <p>Waiting public accounts list...</p>; if (!data) return <p>Waiting public accounts list...</p>;
const txs: any = {}; const txs: Record<string, h.JSX.Element> = {};
const accountsBar = []; const accountsBar = [];
/** /**

View File

@ -16,9 +16,10 @@
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { route } from "preact-router"; import { route } from "preact-router";
import { StateUpdater, useState } from "preact/hooks"; import { StateUpdater, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { BackendStateType, useBackendState } from "../../hooks/backend.js"; import { BackendStateHandler } from "../../hooks/backend.js";
import { bankUiSettings } from "../../settings.js"; import { bankUiSettings } from "../../settings.js";
import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js"; import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
import { BankFrame } from "./BankFrame.js"; import { BankFrame } from "./BankFrame.js";
@ -44,7 +45,7 @@ export function RegistrationPage(): VNode {
* Collect and submit registration data. * Collect and submit registration data.
*/ */
function RegistrationForm(): VNode { function RegistrationForm(): VNode {
const [backendState, backendStateSetter] = useBackendState(); const backend = useBackendContext();
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
const [username, setUsername] = useState<string | undefined>(); const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>();
@ -132,7 +133,7 @@ function RegistrationForm(): VNode {
if (!username || !password) return; if (!username || !password) return;
registrationCall( registrationCall(
{ username, password }, { username, password },
backendStateSetter, // will store BE URL, if OK. backend, // will store BE URL, if OK.
pageStateSetter, pageStateSetter,
); );
@ -177,23 +178,17 @@ async function registrationCall(
* functions can be retrieved somewhat from * functions can be retrieved somewhat from
* the state. * the state.
*/ */
backendStateSetter: StateUpdater<BackendStateType | undefined>, backend: BackendStateHandler,
pageStateSetter: StateUpdater<PageStateType>, pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> { ): Promise<void> {
let baseUrl = getBankBackendBaseUrl(); const url = getBankBackendBaseUrl();
/**
* If the base URL doesn't end with slash and the path
* is not empty, then the concatenation made by URL()
* drops the last path element.
*/
if (!baseUrl.endsWith("/")) baseUrl += "/";
const headers = new Headers(); const headers = new Headers();
headers.append("Content-Type", "application/json"); headers.append("Content-Type", "application/json");
const url = new URL("access-api/testing/register", baseUrl); const registerEndpoint = new URL("access-api/testing/register", url);
let res: Response; let res: Response;
try { try {
res = await fetch(url.href, { res = await fetch(registerEndpoint.href, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
username: req.username, username: req.username,
@ -203,7 +198,7 @@ async function registrationCall(
}); });
} catch (error) { } catch (error) {
console.log( console.log(
`Could not POST new registration to the bank (${url.href})`, `Could not POST new registration to the bank (${registerEndpoint.href})`,
error, error,
); );
pageStateSetter((prevState) => ({ pageStateSetter((prevState) => ({
@ -239,16 +234,11 @@ async function registrationCall(
} }
} else { } else {
// registration was ok // registration was ok
pageStateSetter((prevState) => ({ backend.save({
...prevState, url,
isLoggedIn: true,
}));
backendStateSetter((prevState) => ({
...prevState,
url: baseUrl,
username: req.username, username: req.username,
password: req.password, password: req.password,
})); });
route("/account"); route("/account");
} }
} }

View File

@ -1,17 +1,18 @@
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { StateUpdater } from "preact/hooks"; import { StateUpdater } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { BackendStateType } from "../../hooks/backend.js"; import { BackendState } from "../../hooks/backend.js";
import { prepareHeaders } from "../../utils.js"; import { prepareHeaders } from "../../utils.js";
/** /**
* Additional authentication required to complete the operation. * Additional authentication required to complete the operation.
* Not providing a back button, only abort. * Not providing a back button, only abort.
*/ */
export function TalerWithdrawalConfirmationQuestion(Props: any): VNode { export function TalerWithdrawalConfirmationQuestion(): VNode {
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
const { backendState } = Props; const backend = useBackendContext();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const captchaNumbers = { const captchaNumbers = {
a: Math.floor(Math.random() * 10), a: Math.floor(Math.random() * 10),
@ -57,7 +58,7 @@ export function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
(captchaNumbers.a + captchaNumbers.b).toString() (captchaNumbers.a + captchaNumbers.b).toString()
) { ) {
confirmWithdrawalCall( confirmWithdrawalCall(
backendState, backend.state,
pageState.withdrawalId, pageState.withdrawalId,
pageStateSetter, pageStateSetter,
); );
@ -79,7 +80,7 @@ export function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
class="pure-button pure-button-secondary btn-cancel" class="pure-button pure-button-secondary btn-cancel"
onClick={async () => onClick={async () =>
await abortWithdrawalCall( await abortWithdrawalCall(
backendState, backend.state,
pageState.withdrawalId, pageState.withdrawalId,
pageStateSetter, pageStateSetter,
) )
@ -116,11 +117,11 @@ export function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
* 'page state' and let the related components refresh. * 'page state' and let the related components refresh.
*/ */
async function confirmWithdrawalCall( async function confirmWithdrawalCall(
backendState: BackendStateType | undefined, backendState: BackendState,
withdrawalId: string | undefined, withdrawalId: string | undefined,
pageStateSetter: StateUpdater<PageStateType>, pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> { ): Promise<void> {
if (typeof backendState === "undefined") { if (backendState.status === "loggedOut") {
console.log("No credentials found."); console.log("No credentials found.");
pageStateSetter((prevState) => ({ pageStateSetter((prevState) => ({
...prevState, ...prevState,
@ -211,11 +212,11 @@ async function confirmWithdrawalCall(
* Abort a withdrawal operation via the Access API's /abort. * Abort a withdrawal operation via the Access API's /abort.
*/ */
async function abortWithdrawalCall( async function abortWithdrawalCall(
backendState: BackendStateType | undefined, backendState: BackendState,
withdrawalId: string | undefined, withdrawalId: string | undefined,
pageStateSetter: StateUpdater<PageStateType>, pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> { ): Promise<void> {
if (typeof backendState === "undefined") { if (backendState.status === "loggedOut") {
console.log("No credentials found."); console.log("No credentials found.");
pageStateSetter((prevState) => ({ pageStateSetter((prevState) => ({
...prevState, ...prevState,
@ -237,7 +238,7 @@ async function abortWithdrawalCall(
})); }));
return; return;
} }
let res: any; let res: Response;
try { try {
const { username, password } = backendState; const { username, password } = backendState;
const headers = prepareHeaders(username, password); const headers = prepareHeaders(username, password);

View File

@ -1,5 +1,6 @@
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import useSWR from "swr"; import useSWR from "swr";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { QrCodeSection } from "./QrCodeSection.js"; import { QrCodeSection } from "./QrCodeSection.js";
@ -10,10 +11,15 @@ import { TalerWithdrawalConfirmationQuestion } from "./TalerWithdrawalConfirmati
* permit the passing of exchange and reserve details to * permit the passing of exchange and reserve details to
* the bank. Poll the backend until such operation is done. * the bank. Poll the backend until such operation is done.
*/ */
export function TalerWithdrawalQRCode(Props: any): VNode { export function TalerWithdrawalQRCode({
withdrawalId,
talerWithdrawUri,
}: {
withdrawalId: string;
talerWithdrawUri: string;
}): VNode {
// turns true when the wallet POSTed the reserve details: // turns true when the wallet POSTed the reserve details:
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
const { withdrawalId, talerWithdrawUri, backendState } = Props;
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const abortButton = ( const abortButton = (
<a <a
@ -93,5 +99,5 @@ export function TalerWithdrawalQRCode(Props: any): VNode {
* Wallet POSTed the withdrawal details! Ask the * Wallet POSTed the withdrawal details! Ask the
* user to authorize the operation (here CAPTCHA). * user to authorize the operation (here CAPTCHA).
*/ */
return <TalerWithdrawalConfirmationQuestion backendState={backendState} />; return <TalerWithdrawalConfirmationQuestion />;
} }

View File

@ -10,14 +10,20 @@ export function Transactions({
pageNumber, pageNumber,
accountLabel, accountLabel,
balanceValue, balanceValue,
}: any): VNode { }: {
pageNumber: number;
accountLabel: string;
balanceValue?: string;
}): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const { data, error, mutate } = useSWR( const { data, error, mutate } = useSWR(
`access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`, `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`,
); );
useEffect(() => { useEffect(() => {
mutate(); if (balanceValue) {
}, [balanceValue]); mutate();
}
}, [balanceValue ?? ""]);
if (typeof error !== "undefined") { if (typeof error !== "undefined") {
console.log("transactions not found error", error); console.log("transactions not found error", error);
switch (error.status) { switch (error.status) {

View File

@ -16,9 +16,10 @@
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { StateUpdater, useEffect, useRef } from "preact/hooks"; import { StateUpdater, useEffect, useRef } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { PageStateType, usePageContext } from "../../context/pageState.js"; import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { BackendStateType, useBackendState } from "../../hooks/backend.js"; import { BackendState } from "../../hooks/backend.js";
import { prepareHeaders, validateAmount } from "../../utils.js"; import { prepareHeaders, validateAmount } from "../../utils.js";
export function WalletWithdrawForm({ export function WalletWithdrawForm({
@ -28,10 +29,10 @@ export function WalletWithdrawForm({
currency?: string; currency?: string;
focus?: boolean; focus?: boolean;
}): VNode { }): VNode {
const [backendState, backendStateSetter] = useBackendState(); const backend = useBackendContext();
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
let submitAmount = "5.00"; let submitAmount: string | undefined = "5.00";
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@ -83,7 +84,7 @@ export function WalletWithdrawForm({
if (!submitAmount && currency) return; if (!submitAmount && currency) return;
createWithdrawalCall( createWithdrawalCall(
`${currency}:${submitAmount}`, `${currency}:${submitAmount}`,
backendState, backend.state,
pageStateSetter, pageStateSetter,
); );
}} }}
@ -105,10 +106,10 @@ export function WalletWithdrawForm({
* the user about the operation's outcome. (2) use POST helper. */ * the user about the operation's outcome. (2) use POST helper. */
async function createWithdrawalCall( async function createWithdrawalCall(
amount: string, amount: string,
backendState: BackendStateType | undefined, backendState: BackendState,
pageStateSetter: StateUpdater<PageStateType>, pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> { ): Promise<void> {
if (typeof backendState === "undefined") { if (backendState?.status === "loggedOut") {
console.log("Page has a problem: no credentials found in the state."); console.log("Page has a problem: no credentials found in the state.");
pageStateSetter((prevState) => ({ pageStateSetter((prevState) => ({
...prevState, ...prevState,
@ -120,7 +121,7 @@ async function createWithdrawalCall(
return; return;
} }
let res: any; let res: Response;
try { try {
const { username, password } = backendState; const { username, password } = backendState;
const headers = prepareHeaders(username, password); const headers = prepareHeaders(username, password);

View File

@ -1,9 +1,11 @@
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
/** /**
* Validate (the number part of) an amount. If needed, * Validate (the number part of) an amount. If needed,
* replace comma with a dot. Returns 'false' whenever * replace comma with a dot. Returns 'false' whenever
* the input is invalid, the valid amount otherwise. * the input is invalid, the valid amount otherwise.
*/ */
export function validateAmount(maybeAmount: string): any { export function validateAmount(maybeAmount: string | undefined): string | undefined {
const amountRegex = "^[0-9]+(.[0-9]+)?$"; const amountRegex = "^[0-9]+(.[0-9]+)?$";
if (!maybeAmount) { if (!maybeAmount) {
console.log(`Entered amount (${maybeAmount}) mismatched <input> pattern.`); console.log(`Entered amount (${maybeAmount}) mismatched <input> pattern.`);
@ -15,7 +17,7 @@ export function validateAmount(maybeAmount: string): any {
const re = RegExp(amountRegex); const re = RegExp(amountRegex);
if (!re.test(maybeAmount)) { if (!re.test(maybeAmount)) {
console.log(`Not using invalid amount '${maybeAmount}'.`); console.log(`Not using invalid amount '${maybeAmount}'.`);
return false; return;
} }
} }
return maybeAmount; return maybeAmount;
@ -33,18 +35,19 @@ export function getIbanFromPayto(url: string): string {
return iban; return iban;
} }
const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/";
export function getBankBackendBaseUrl(): string { export function getBankBackendBaseUrl(): string {
const overrideUrl = localStorage.getItem("bank-base-url"); const overrideUrl = localStorage.getItem("bank-base-url");
if (overrideUrl) { if (overrideUrl) {
console.log( console.log(
`using bank base URL ${overrideUrl} (override via bank-base-url localStorage)`, `using bank base URL ${overrideUrl} (override via bank-base-url localStorage)`,
); );
return overrideUrl; } else {
console.log(`using bank base URL (${maybeRootPath})`);
} }
const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/"; return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath)
if (!maybeRootPath.endsWith("/")) return `${maybeRootPath}/`;
console.log(`using bank base URL (${maybeRootPath})`);
return maybeRootPath;
} }
export function undefinedIfEmpty<T extends object>(obj: T): T | undefined { export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {