diff options
16 files changed, 264 insertions, 225 deletions
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index f3bc3f571..b6b88f910 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -1,4 +1,5 @@ import { h, FunctionalComponent } from "preact"; +import { BackendStateProvider } from "../context/backend.js"; import { PageStateProvider } from "../context/pageState.js"; import { TranslationProvider } from "../context/translation.js"; import { Routing } from "../pages/Routing.js"; @@ -24,7 +25,9 @@ const App: FunctionalComponent = () => { return ( <TranslationProvider> <PageStateProvider> - <Routing /> + <BackendStateProvider> + <Routing /> + </BackendStateProvider> </PageStateProvider> </TranslationProvider> ); diff --git a/packages/demobank-ui/src/context/backend.ts b/packages/demobank-ui/src/context/backend.ts new file mode 100644 index 000000000..b9b7f8527 --- /dev/null +++ b/packages/demobank-ui/src/context/backend.ts @@ -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, + }); +};
\ No newline at end of file diff --git a/packages/demobank-ui/src/context/pageState.ts b/packages/demobank-ui/src/context/pageState.ts index 4ef21b8f0..b954ad20e 100644 --- a/packages/demobank-ui/src/context/pageState.ts +++ b/packages/demobank-ui/src/context/pageState.ts @@ -29,7 +29,6 @@ export type Type = { }; const initial: Type = { pageState: { - isLoggedIn: false, isRawPayto: false, withdrawalInProgress: false, }, @@ -59,7 +58,6 @@ export const PageStateProvider = ({ */ function usePageState( state: PageStateType = { - isLoggedIn: false, isRawPayto: false, withdrawalInProgress: false, }, @@ -98,7 +96,6 @@ function usePageState( * Track page state. */ export interface PageStateType { - isLoggedIn: boolean; isRawPayto: boolean; withdrawalInProgress: boolean; error?: { diff --git a/packages/demobank-ui/src/context/translation.ts b/packages/demobank-ui/src/context/translation.ts index 478bdbde0..0a7e9429d 100644 --- a/packages/demobank-ui/src/context/translation.ts +++ b/packages/demobank-ui/src/context/translation.ts @@ -20,7 +20,7 @@ */ 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 { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { strings } from "../i18n/strings.js"; @@ -60,7 +60,7 @@ const Context = createContext<Type>(initial); interface Props { initial?: string; - children: any; + children: ComponentChildren; forceLang?: string; } diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 3b00edee3..967d5ee85 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -1,33 +1,55 @@ import { hooks } from "@gnu-taler/web-util/lib/index.browser"; -import { StateUpdater } from "preact/hooks"; /** * Has the information to reach and * authenticate at the bank's backend. */ -export interface BackendStateType { - url?: string; - username?: string; - password?: string; +export type BackendState = LoggedIn | LoggedOut + +export interface BackendInfo { + 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 * login credentials and backend's * base URL. */ -type BackendStateTypeOpt = BackendStateType | undefined; -export function useBackendState( - state?: BackendStateType, -): [BackendStateTypeOpt, StateUpdater<BackendStateTypeOpt>] { - const ret = hooks.useLocalStorage("backend-state", JSON.stringify(state)); - const retObj: BackendStateTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0]; - const retSetter: StateUpdater<BackendStateTypeOpt> = function (val) { - const newVal = - val instanceof Function - ? JSON.stringify(val(retObj)) - : JSON.stringify(val); - ret[1](newVal); - }; - return [retObj, retSetter]; +export function useBackendState(): BackendStateHandler { + const [value, update] = hooks.useLocalStorage("backend-state", JSON.stringify(defaultState)); + // const parsed = value !== undefined ? JSON.parse(value) : value; + let parsed + try { + parsed = JSON.parse(value!) + } catch { + parsed = undefined + } + const state: BackendState = !parsed?.status ? defaultState : parsed + + return { + state, + clear() { + update(JSON.stringify(defaultState)) + }, + save(info) { + const nextState: BackendState = { status: "loggedIn", ...info } + update(JSON.stringify(nextState)) + }, + } } diff --git a/packages/demobank-ui/src/pages/home/AccountPage.tsx b/packages/demobank-ui/src/pages/home/AccountPage.tsx index 2bc05c332..16ff601ec 100644 --- a/packages/demobank-ui/src/pages/home/AccountPage.tsx +++ b/packages/demobank-ui/src/pages/home/AccountPage.tsx @@ -16,14 +16,15 @@ 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 { ComponentChildren, Fragment, h, VNode } from "preact"; +import { StateUpdater, useEffect } from "preact/hooks"; import useSWR, { SWRConfig, useSWRConfig } from "swr"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.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 { getIbanFromPayto } from "../../utils.js"; +import { getIbanFromPayto, prepareHeaders } from "../../utils.js"; import { BankFrame } from "./BankFrame.js"; import { LoginForm } from "./LoginForm.js"; import { PaymentOptions } from "./PaymentOptions.js"; @@ -31,11 +32,10 @@ import { TalerWithdrawalQRCode } from "./TalerWithdrawalQRCode.js"; import { Transactions } from "./Transactions.js"; export function AccountPage(): VNode { - const [backendState, backendStateSetter] = useBackendState(); + const backend = useBackendContext(); const { i18n } = useTranslationContext(); - const { pageState, pageStateSetter } = usePageContext(); - if (!pageState.isLoggedIn) { + if (backend.state.status === "loggedOut") { return ( <BankFrame> <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 ( - <SWRWithCredentials - username={backendState.username} - password={backendState.password} - backendUrl={backendState.url} - > - <Account - accountLabel={backendState.username} - backendState={backendState} - /> + <SWRWithCredentials info={backend.state}> + <Account accountLabel={backend.state.username} /> </SWRWithCredentials> ); } @@ -73,16 +54,20 @@ export function AccountPage(): VNode { /** * 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); +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(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() }; return r.json(); @@ -90,7 +75,7 @@ function SWRWithCredentials(props: any): VNode { }, }} > - {props.children} + {children as any} </SWRConfig> ); } @@ -100,9 +85,9 @@ function SWRWithCredentials(props: any): VNode { * is mostly needed to provide the user's credentials to POST * to the bank. */ -function Account(Props: any): VNode { +function Account({ accountLabel }: { accountLabel: string }): 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, { @@ -112,14 +97,9 @@ function Account(Props: any): VNode { // revalidateOnFocus: false, // revalidateOnReconnect: false, }); + const backend = useBackendContext(); const { pageState, pageStateSetter: setPageState } = usePageContext(); - const { - withdrawalInProgress, - withdrawalId, - isLoggedIn, - talerWithdrawUri, - timestamp, - } = pageState; + const { withdrawalId, talerWithdrawUri, timestamp } = pageState; const { i18n } = useTranslationContext(); useEffect(() => { mutate(); @@ -129,10 +109,11 @@ function Account(Props: any): VNode { * 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} />); + // const [txPageNumber, setTxPageNumber] = useTransactionPageNumber(); + // const txsPages = []; + // for (let i = 0; i <= txPageNumber; i++) { + // txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />); + // } if (typeof error !== "undefined") { console.log("account error", error, endpoint); @@ -143,10 +124,10 @@ function Account(Props: any): VNode { */ switch (error.status) { case 404: { + backend.clear(); setPageState((prevState: PageStateType) => ({ ...prevState, - isLoggedIn: false, error: { 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.Forbidden: { + backend.clear(); setPageState((prevState: PageStateType) => ({ ...prevState, - - isLoggedIn: false, error: { title: i18n.str`Wrong credentials given.`, }, @@ -181,10 +161,9 @@ function Account(Props: any): VNode { return <p>Wrong credentials...</p>; } default: { + backend.clear(); setPageState((prevState: PageStateType) => ({ ...prevState, - - isLoggedIn: false, error: { title: i18n.str`Account information could not be retrieved.`, debug: JSON.stringify(error), @@ -211,13 +190,11 @@ function Account(Props: any): VNode { * the outcome. */ console.log(`maybe new withdrawal ${talerWithdrawUri}`); - if (talerWithdrawUri) { + if (talerWithdrawUri && withdrawalId) { console.log("Bank created a new Taler withdrawal"); return ( <BankFrame> <TalerWithdrawalQRCode - accountLabel={accountLabel} - backendState={backendState} withdrawalId={withdrawalId} talerWithdrawUri={talerWithdrawUri} /> @@ -266,7 +243,7 @@ function Account(Props: any): VNode { <h2>{i18n.str`Latest transactions:`}</h2> <Transactions balanceValue={balanceValue} - pageNumber="0" + pageNumber={0} accountLabel={accountLabel} /> </article> diff --git a/packages/demobank-ui/src/pages/home/BankFrame.tsx b/packages/demobank-ui/src/pages/home/BankFrame.tsx index 3b099e34b..f6b8fbd96 100644 --- a/packages/demobank-ui/src/pages/home/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/home/BankFrame.tsx @@ -14,15 +14,21 @@ 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 { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.js"; import { useTranslationContext } from "../../context/translation.js"; import { bankUiSettings } from "../../settings.js"; -export function BankFrame(Props: any): VNode { +export function BankFrame({ + children, +}: { + children: ComponentChildren; +}): VNode { const { i18n } = useTranslationContext(); + const backend = useBackendContext(); const { pageState, pageStateSetter } = usePageContext(); console.log("BankFrame state", pageState); const logOut = ( @@ -33,9 +39,9 @@ export function BankFrame(Props: any): VNode { onClick={() => { pageStateSetter((prevState: PageStateType) => { const { talerWithdrawUri, withdrawalId, ...rest } = prevState; + backend.clear(); return { ...rest, - isLoggedIn: false, withdrawalInProgress: false, error: undefined, info: undefined, @@ -98,10 +104,10 @@ export function BankFrame(Props: any): VNode { </nav> </div> <section id="main" class="content"> - <ErrorBanner pageState={[pageState, pageStateSetter]} /> - <StatusBanner pageState={[pageState, pageStateSetter]} /> - {pageState.isLoggedIn ? logOut : null} - {Props.children} + <ErrorBanner /> + <StatusBanner /> + {backend.state.status === "loggedIn" ? logOut : null} + {children} </section> <section id="footer" class="footer"> <div class="footer"> @@ -127,9 +133,9 @@ function maybeDemoContent(content: VNode): VNode { return <Fragment />; } -function ErrorBanner(Props: any): VNode | null { - const [pageState, pageStateSetter] = Props.pageState; - // const { i18n } = useTranslationContext(); +function ErrorBanner(): VNode | null { + const { pageState, pageStateSetter } = usePageContext(); + if (!pageState.error) return null; const rval = ( @@ -144,7 +150,7 @@ function ErrorBanner(Props: any): VNode | null { class="pure-button" value="Clear" onClick={async () => { - pageStateSetter((prev: any) => ({ ...prev, error: undefined })); + pageStateSetter((prev) => ({ ...prev, error: undefined })); }} /> </div> @@ -156,8 +162,8 @@ function ErrorBanner(Props: any): VNode | null { return rval; } -function StatusBanner(Props: any): VNode | null { - const [pageState, pageStateSetter] = Props.pageState; +function StatusBanner(): VNode | null { + const { pageState, pageStateSetter } = usePageContext(); if (!pageState.info) return null; const rval = ( @@ -172,7 +178,7 @@ function StatusBanner(Props: any): VNode | null { class="pure-button" value="Clear" onClick={async () => { - pageStateSetter((prev: any) => ({ ...prev, info: undefined })); + pageStateSetter((prev) => ({ ...prev, info: undefined })); }} /> </div> diff --git a/packages/demobank-ui/src/pages/home/LoginForm.tsx b/packages/demobank-ui/src/pages/home/LoginForm.tsx index f60c9f600..f31f91190 100644 --- a/packages/demobank-ui/src/pages/home/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/home/LoginForm.tsx @@ -16,10 +16,10 @@ 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 { useEffect, useRef, useState } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.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 { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; @@ -28,8 +28,7 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; * Collect and submit login data. */ export function LoginForm(): VNode { - const [backendState, backendStateSetter] = useBackendState(); - const { pageState, pageStateSetter } = usePageContext(); + const backend = useBackendContext(); const [username, setUsername] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>(); const { i18n } = useTranslationContext(); @@ -93,11 +92,7 @@ export function LoginForm(): VNode { disabled={!!errors} onClick={() => { if (!username || !password) return; - loginCall( - { username, password }, - backendStateSetter, - pageStateSetter, - ); + loginCall({ username, password }, backend); setUsername(undefined); setPassword(undefined); }} @@ -129,21 +124,16 @@ async function loginCall( * FIXME: figure out if the two following * functions can be retrieved from the state. */ - backendStateSetter: StateUpdater<BackendStateType | undefined>, - pageStateSetter: StateUpdater<PageStateType>, + backend: BackendStateHandler, ): Promise<void> { /** * Optimistically setting the state as 'logged in', and * let the Account component request the balance to check * whether the credentials are valid. */ - pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true })); - let baseUrl = getBankBackendBaseUrl(); - if (!baseUrl.endsWith("/")) baseUrl += "/"; - backendStateSetter((prevState) => ({ - ...prevState, - url: baseUrl, + backend.save({ + url: getBankBackendBaseUrl(), username: req.username, password: req.password, - })); + }); } diff --git a/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx index 45e7cf5ca..e4fe386ff 100644 --- a/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx @@ -18,9 +18,10 @@ import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { h, VNode } from "preact"; import { StateUpdater, useEffect, useRef, useState } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.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 { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; @@ -31,7 +32,7 @@ export function PaytoWireTransferForm({ focus?: boolean; currency?: string; }): VNode { - const [backendState, backendStateSetter] = useBackendState(); + const backend = useBackendContext(); const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button? const [submitData, submitDataSetter] = useWireTransferRequestType(); @@ -81,7 +82,7 @@ export function PaytoWireTransferForm({ required pattern={ibanRegex} onInput={(e): void => { - submitDataSetter((submitData: any) => ({ + submitDataSetter((submitData) => ({ ...submitData, iban: e.currentTarget.value, })); @@ -102,7 +103,7 @@ export function PaytoWireTransferForm({ value={submitData?.subject ?? ""} required onInput={(e): void => { - submitDataSetter((submitData: any) => ({ + submitDataSetter((submitData) => ({ ...submitData, subject: e.currentTarget.value, })); @@ -133,7 +134,7 @@ export function PaytoWireTransferForm({ required value={submitData?.amount ?? ""} onInput={(e): void => { - submitDataSetter((submitData: any) => ({ + submitDataSetter((submitData) => ({ ...submitData, amount: e.currentTarget.value, })); @@ -179,7 +180,7 @@ export function PaytoWireTransferForm({ }; return await createTransactionCall( transactionData, - backendState, + backend.state, pageStateSetter, () => submitDataSetter((p) => ({ @@ -209,7 +210,7 @@ export function PaytoWireTransferForm({ href="/account" onClick={() => { console.log("switch to raw payto form"); - pageStateSetter((prevState: any) => ({ + pageStateSetter((prevState) => ({ ...prevState, isRawPayto: true, })); @@ -283,7 +284,7 @@ export function PaytoWireTransferForm({ return await createTransactionCall( transactionData, - backendState, + backend.state, pageStateSetter, () => rawPaytoInputSetter(undefined), ); @@ -295,7 +296,7 @@ export function PaytoWireTransferForm({ href="/account" onClick={() => { console.log("switch to wire-transfer-form"); - pageStateSetter((prevState: any) => ({ + pageStateSetter((prevState) => ({ ...prevState, isRawPayto: false, })); @@ -345,7 +346,7 @@ function useWireTransferRequestType( */ async function createTransactionCall( req: TransactionRequestType, - backendState: BackendStateType | undefined, + backendState: BackendState, pageStateSetter: StateUpdater<PageStateType>, /** * Optional since the raw payto form doesn't have @@ -353,13 +354,30 @@ async function createTransactionCall( */ cleanUpForm: () => 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 { - res = await postToBackend( - `access-api/accounts/${getUsername(backendState)}/transactions`, - backendState, - JSON.stringify(req), + const { username, password } = backendState; + const headers = prepareHeaders(username, password); + const url = new URL( + `access-api/accounts/${backendState.username}/transactions`, + backendState.url, ); + res = await fetch(url.href, { + method: "POST", + headers, + body: JSON.stringify(req), + }); } catch (error) { console.log("Could not POST transaction request to the bank", error); pageStateSetter((prevState) => ({ @@ -402,41 +420,3 @@ async function createTransactionCall( // be discarded. 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, - }); -} diff --git a/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx index 215dc7321..a8028f3bf 100644 --- a/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx +++ b/packages/demobank-ui/src/pages/home/PublicHistoriesPage.tsx @@ -15,7 +15,7 @@ */ 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 { StateUpdater } from "preact/hooks"; import useSWR, { SWRConfig } from "swr"; @@ -35,8 +35,13 @@ export function PublicHistoriesPage(): VNode { ); } -function SWRWithoutCredentials(Props: any): VNode { - const { baseUrl } = Props; +function SWRWithoutCredentials({ + baseUrl, + children, +}: { + children: ComponentChildren; + baseUrl: string; +}): VNode { console.log("Base URL", baseUrl); return ( <SWRConfig @@ -49,7 +54,7 @@ function SWRWithoutCredentials(Props: any): VNode { }), }} > - {Props.children} + {children as any} </SWRConfig> ); } @@ -93,7 +98,7 @@ function PublicHistories(): VNode { } } if (!data) return <p>Waiting public accounts list...</p>; - const txs: any = {}; + const txs: Record<string, h.JSX.Element> = {}; const accountsBar = []; /** diff --git a/packages/demobank-ui/src/pages/home/RegistrationPage.tsx b/packages/demobank-ui/src/pages/home/RegistrationPage.tsx index 9a120cb4f..08e9bd480 100644 --- a/packages/demobank-ui/src/pages/home/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/home/RegistrationPage.tsx @@ -16,9 +16,10 @@ import { Fragment, h, VNode } from "preact"; import { route } from "preact-router"; import { StateUpdater, useState } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.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 { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js"; import { BankFrame } from "./BankFrame.js"; @@ -44,7 +45,7 @@ export function RegistrationPage(): VNode { * Collect and submit registration data. */ function RegistrationForm(): VNode { - const [backendState, backendStateSetter] = useBackendState(); + const backend = useBackendContext(); const { pageState, pageStateSetter } = usePageContext(); const [username, setUsername] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>(); @@ -132,7 +133,7 @@ function RegistrationForm(): VNode { if (!username || !password) return; registrationCall( { username, password }, - backendStateSetter, // will store BE URL, if OK. + backend, // will store BE URL, if OK. pageStateSetter, ); @@ -177,23 +178,17 @@ async function registrationCall( * functions can be retrieved somewhat from * the state. */ - backendStateSetter: StateUpdater<BackendStateType | undefined>, + backend: BackendStateHandler, pageStateSetter: StateUpdater<PageStateType>, ): Promise<void> { - let baseUrl = 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 url = getBankBackendBaseUrl(); const headers = new Headers(); 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; try { - res = await fetch(url.href, { + res = await fetch(registerEndpoint.href, { method: "POST", body: JSON.stringify({ username: req.username, @@ -203,7 +198,7 @@ async function registrationCall( }); } catch (error) { console.log( - `Could not POST new registration to the bank (${url.href})`, + `Could not POST new registration to the bank (${registerEndpoint.href})`, error, ); pageStateSetter((prevState) => ({ @@ -239,16 +234,11 @@ async function registrationCall( } } else { // registration was ok - pageStateSetter((prevState) => ({ - ...prevState, - isLoggedIn: true, - })); - backendStateSetter((prevState) => ({ - ...prevState, - url: baseUrl, + backend.save({ + url, username: req.username, password: req.password, - })); + }); route("/account"); } } diff --git a/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx index e3d8957b8..4fd46878b 100644 --- a/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx @@ -1,17 +1,18 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.js"; import { useTranslationContext } from "../../context/translation.js"; -import { BackendStateType } from "../../hooks/backend.js"; +import { BackendState } from "../../hooks/backend.js"; import { prepareHeaders } from "../../utils.js"; /** * Additional authentication required to complete the operation. * Not providing a back button, only abort. */ -export function TalerWithdrawalConfirmationQuestion(Props: any): VNode { +export function TalerWithdrawalConfirmationQuestion(): VNode { const { pageState, pageStateSetter } = usePageContext(); - const { backendState } = Props; + const backend = useBackendContext(); const { i18n } = useTranslationContext(); const captchaNumbers = { a: Math.floor(Math.random() * 10), @@ -57,7 +58,7 @@ export function TalerWithdrawalConfirmationQuestion(Props: any): VNode { (captchaNumbers.a + captchaNumbers.b).toString() ) { confirmWithdrawalCall( - backendState, + backend.state, pageState.withdrawalId, pageStateSetter, ); @@ -79,7 +80,7 @@ export function TalerWithdrawalConfirmationQuestion(Props: any): VNode { class="pure-button pure-button-secondary btn-cancel" onClick={async () => await abortWithdrawalCall( - backendState, + backend.state, pageState.withdrawalId, pageStateSetter, ) @@ -116,11 +117,11 @@ export function TalerWithdrawalConfirmationQuestion(Props: any): VNode { * 'page state' and let the related components refresh. */ async function confirmWithdrawalCall( - backendState: BackendStateType | undefined, + backendState: BackendState, withdrawalId: string | undefined, pageStateSetter: StateUpdater<PageStateType>, ): Promise<void> { - if (typeof backendState === "undefined") { + if (backendState.status === "loggedOut") { console.log("No credentials found."); pageStateSetter((prevState) => ({ ...prevState, @@ -211,11 +212,11 @@ async function confirmWithdrawalCall( * Abort a withdrawal operation via the Access API's /abort. */ async function abortWithdrawalCall( - backendState: BackendStateType | undefined, + backendState: BackendState, withdrawalId: string | undefined, pageStateSetter: StateUpdater<PageStateType>, ): Promise<void> { - if (typeof backendState === "undefined") { + if (backendState.status === "loggedOut") { console.log("No credentials found."); pageStateSetter((prevState) => ({ ...prevState, @@ -237,7 +238,7 @@ async function abortWithdrawalCall( })); return; } - let res: any; + let res: Response; try { const { username, password } = backendState; const headers = prepareHeaders(username, password); diff --git a/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx index da4ccc45e..848a9c45c 100644 --- a/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx @@ -1,5 +1,6 @@ import { Fragment, h, VNode } from "preact"; import useSWR from "swr"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.js"; import { useTranslationContext } from "../../context/translation.js"; import { QrCodeSection } from "./QrCodeSection.js"; @@ -10,10 +11,15 @@ import { TalerWithdrawalConfirmationQuestion } from "./TalerWithdrawalConfirmati * permit the passing of exchange and reserve details to * 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: const { pageState, pageStateSetter } = usePageContext(); - const { withdrawalId, talerWithdrawUri, backendState } = Props; const { i18n } = useTranslationContext(); const abortButton = ( <a @@ -93,5 +99,5 @@ export function TalerWithdrawalQRCode(Props: any): VNode { * Wallet POSTed the withdrawal details! Ask the * user to authorize the operation (here CAPTCHA). */ - return <TalerWithdrawalConfirmationQuestion backendState={backendState} />; + return <TalerWithdrawalConfirmationQuestion />; } diff --git a/packages/demobank-ui/src/pages/home/Transactions.tsx b/packages/demobank-ui/src/pages/home/Transactions.tsx index eb344403f..c0bb86024 100644 --- a/packages/demobank-ui/src/pages/home/Transactions.tsx +++ b/packages/demobank-ui/src/pages/home/Transactions.tsx @@ -10,14 +10,20 @@ export function Transactions({ pageNumber, accountLabel, balanceValue, -}: any): VNode { +}: { + pageNumber: number; + accountLabel: string; + balanceValue?: string; +}): VNode { const { i18n } = useTranslationContext(); const { data, error, mutate } = useSWR( `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`, ); useEffect(() => { - mutate(); - }, [balanceValue]); + if (balanceValue) { + mutate(); + } + }, [balanceValue ?? ""]); if (typeof error !== "undefined") { console.log("transactions not found error", error); switch (error.status) { diff --git a/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx index 842f14a5f..ee43d2006 100644 --- a/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx @@ -16,9 +16,10 @@ import { h, VNode } from "preact"; import { StateUpdater, useEffect, useRef } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.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"; export function WalletWithdrawForm({ @@ -28,10 +29,10 @@ export function WalletWithdrawForm({ currency?: string; focus?: boolean; }): VNode { - const [backendState, backendStateSetter] = useBackendState(); + const backend = useBackendContext(); const { pageState, pageStateSetter } = usePageContext(); const { i18n } = useTranslationContext(); - let submitAmount = "5.00"; + let submitAmount: string | undefined = "5.00"; const ref = useRef<HTMLInputElement>(null); useEffect(() => { @@ -83,7 +84,7 @@ export function WalletWithdrawForm({ if (!submitAmount && currency) return; createWithdrawalCall( `${currency}:${submitAmount}`, - backendState, + backend.state, pageStateSetter, ); }} @@ -105,10 +106,10 @@ export function WalletWithdrawForm({ * the user about the operation's outcome. (2) use POST helper. */ async function createWithdrawalCall( amount: string, - backendState: BackendStateType | undefined, + backendState: BackendState, pageStateSetter: StateUpdater<PageStateType>, ): Promise<void> { - if (typeof backendState === "undefined") { + if (backendState?.status === "loggedOut") { console.log("Page has a problem: no credentials found in the state."); pageStateSetter((prevState) => ({ ...prevState, @@ -120,7 +121,7 @@ async function createWithdrawalCall( return; } - let res: any; + let res: Response; try { const { username, password } = backendState; const headers = prepareHeaders(username, password); diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 23cade0e8..d74f4d129 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -1,9 +1,11 @@ +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; + /** * Validate (the number part of) an amount. If needed, * replace comma with a dot. Returns 'false' whenever * 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]+)?$"; if (!maybeAmount) { console.log(`Entered amount (${maybeAmount}) mismatched <input> pattern.`); @@ -15,7 +17,7 @@ export function validateAmount(maybeAmount: string): any { const re = RegExp(amountRegex); if (!re.test(maybeAmount)) { console.log(`Not using invalid amount '${maybeAmount}'.`); - return false; + return; } } return maybeAmount; @@ -33,18 +35,19 @@ export function getIbanFromPayto(url: string): string { return iban; } +const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/"; + export function getBankBackendBaseUrl(): string { const overrideUrl = localStorage.getItem("bank-base-url"); if (overrideUrl) { console.log( `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/"; - if (!maybeRootPath.endsWith("/")) return `${maybeRootPath}/`; - console.log(`using bank base URL (${maybeRootPath})`); - return maybeRootPath; + return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath) + } export function undefinedIfEmpty<T extends object>(obj: T): T | undefined { |