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 ( - + + + ); 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 + */ + +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(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(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] { - const ret = hooks.useLocalStorage("backend-state", JSON.stringify(state)); - const retObj: BackendStateTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0]; - const retSetter: StateUpdater = function (val) { - const newVal = - val instanceof Function - ? JSON.stringify(val(retObj)) - : JSON.stringify(val); - ret[1](newVal); - }; - return [retObj, retSetter]; +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 (

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

@@ -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

Error: waiting for details...

; - } - console.log("Showing the profile page.."); return ( - - + + ); } @@ -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 ( { - 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} ); } @@ -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(); + // const [txPageNumber, setTxPageNumber] = useTransactionPageNumber(); + // const txsPages = []; + // for (let i = 0; i <= txPageNumber; i++) { + // txsPages.push(); + // } if (typeof error !== "undefined") { console.log("account error", error, endpoint); @@ -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

Wrong credentials...

; } 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 ( @@ -266,7 +243,7 @@ function Account(Props: any): VNode {

{i18n.str`Latest transactions:`}

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 */ -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 {
- - - {pageState.isLoggedIn ? logOut : null} - {Props.children} + + + {backend.state.status === "loggedIn" ? logOut : null} + {children}