From 8e6bf990069b9c3ae033321b5aeb4d46fa6501e6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Dec 2022 09:06:10 -0300 Subject: no-fix: move pagestate provider to app component and move some common hooks to web-utils --- packages/demobank-ui/src/components/app.tsx | 5 +- packages/demobank-ui/src/context/pageState.ts | 121 +++++++ packages/demobank-ui/src/context/translation.ts | 4 +- packages/demobank-ui/src/declaration.d.ts | 49 +++ packages/demobank-ui/src/hooks/index.ts | 10 +- packages/demobank-ui/src/hooks/useLang.ts | 30 -- packages/demobank-ui/src/hooks/useLocalStorage.ts | 80 ----- packages/demobank-ui/src/pages/home/index.tsx | 384 ++++++---------------- packages/demobank-ui/src/settings.ts | 27 ++ 9 files changed, 301 insertions(+), 409 deletions(-) create mode 100644 packages/demobank-ui/src/context/pageState.ts delete mode 100644 packages/demobank-ui/src/hooks/useLang.ts delete mode 100644 packages/demobank-ui/src/hooks/useLocalStorage.ts create mode 100644 packages/demobank-ui/src/settings.ts (limited to 'packages/demobank-ui/src') diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index 49b218205..91410a485 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -1,11 +1,14 @@ import { h, FunctionalComponent } from "preact"; +import { PageStateProvider } from "../context/pageState.js"; import { TranslationProvider } from "../context/translation.js"; import { BankHome } from "../pages/home/index.js"; const App: FunctionalComponent = () => { return ( - + + + ); }; diff --git a/packages/demobank-ui/src/context/pageState.ts b/packages/demobank-ui/src/context/pageState.ts new file mode 100644 index 000000000..3d7ccd85b --- /dev/null +++ b/packages/demobank-ui/src/context/pageState.ts @@ -0,0 +1,121 @@ +/* + 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 { hooks } from "@gnu-taler/web-util/lib/index.browser"; +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { StateUpdater, useContext } from "preact/hooks"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type Type = { + pageState: PageStateType; + pageStateSetter: StateUpdater; +}; +const initial: Type = { + pageState: { + isLoggedIn: false, + isRawPayto: false, + showPublicHistories: false, + withdrawalInProgress: false, + }, + pageStateSetter: () => { + null; + }, +}; +const Context = createContext(initial); + +export const usePageContext = (): Type => useContext(Context); + +export const PageStateProvider = ({ + children, +}: { + children: ComponentChildren; +}): VNode => { + const [pageState, pageStateSetter] = usePageState(); + + return h(Context.Provider, { + value: { pageState, pageStateSetter }, + children, + }); +}; + +/** + * Wrapper providing defaults. + */ +function usePageState( + state: PageStateType = { + isLoggedIn: false, + isRawPayto: false, + showPublicHistories: false, + withdrawalInProgress: false, + }, +): [PageStateType, StateUpdater] { + const ret = hooks.useNotNullLocalStorage("page-state", JSON.stringify(state)); + const retObj: PageStateType = JSON.parse(ret[0]); + + const retSetter: StateUpdater = function (val) { + const newVal = + val instanceof Function + ? JSON.stringify(val(retObj)) + : JSON.stringify(val); + + ret[1](newVal); + }; + + //when moving from one page to another + //clean up the info and error bar + function removeLatestInfo(val: any): ReturnType { + const updater = typeof val === "function" ? val : (c: any) => val; + return retSetter((current: any) => { + const cleanedCurrent: PageStateType = { + ...current, + info: undefined, + errors: undefined, + timestamp: new Date().getTime(), + }; + return updater(cleanedCurrent); + }); + } + + return [retObj, removeLatestInfo]; +} + +/** + * Track page state. + */ +export interface PageStateType { + isLoggedIn: boolean; + isRawPayto: boolean; + showPublicHistories: boolean; + withdrawalInProgress: boolean; + error?: { + description?: string; + title: string; + debug?: string; + }; + + info?: string; + talerWithdrawUri?: string; + /** + * Not strictly a presentational value, could + * be moved in a future "withdrawal state" object. + */ + withdrawalId?: string; + timestamp?: number; +} diff --git a/packages/demobank-ui/src/context/translation.ts b/packages/demobank-ui/src/context/translation.ts index a50f81b86..478bdbde0 100644 --- a/packages/demobank-ui/src/context/translation.ts +++ b/packages/demobank-ui/src/context/translation.ts @@ -22,7 +22,7 @@ import { i18n, setupI18n } from "@gnu-taler/taler-util"; import { createContext, h, VNode } from "preact"; import { useContext, useEffect } from "preact/hooks"; -import { useLang } from "../hooks/useLang.js"; +import { hooks } from "@gnu-taler/web-util/lib/index.browser"; import { strings } from "../i18n/strings.js"; interface Type { @@ -70,7 +70,7 @@ export const TranslationProvider = ({ children, forceLang, }: Props): VNode => { - const [lang, changeLanguage, isSaved] = useLang(initial); + const [lang, changeLanguage, isSaved] = hooks.useLang(initial); useEffect(() => { if (forceLang) { changeLanguage(forceLang); diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index 00b3d41d5..084686bd2 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -18,3 +18,52 @@ declare module "jed" { const x: any; export = x; } + +/********************************************** + * Type definitions for states and API calls. * + *********************************************/ + +/** + * Has the information to reach and + * authenticate at the bank's backend. + */ +interface BackendStateType { + url?: string; + username?: string; + password?: string; +} + +/** + * Request body of POST /transactions. + * + * If the amount appears twice: both as a Payto parameter and + * in the JSON dedicate field, the one on the Payto URI takes + * precedence. + */ +interface TransactionRequestType { + paytoUri: string; + amount?: string; // with currency. +} + +/** + * Request body of /register. + */ +interface CredentialsRequestType { + username?: string; + password?: string; + repeatPassword?: string; +} + +/** + * Request body of /register. + */ +// interface LoginRequestType { +// username: string; +// password: string; +// } + +interface WireTransferRequestType { + iban?: string; + subject?: string; + amount?: string; +} diff --git a/packages/demobank-ui/src/hooks/index.ts b/packages/demobank-ui/src/hooks/index.ts index b4191d182..c6e3fe8c1 100644 --- a/packages/demobank-ui/src/hooks/index.ts +++ b/packages/demobank-ui/src/hooks/index.ts @@ -20,7 +20,7 @@ */ import { StateUpdater } from "preact/hooks"; -import { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js"; +import { hooks } from "@gnu-taler/web-util/lib/index.browser"; export type ValueOrFunction = T | ((p: T) => T); const calculateRootPath = () => { @@ -34,11 +34,11 @@ const calculateRootPath = () => { export function useBackendURL( url?: string, ): [string, boolean, StateUpdater, () => void] { - const [value, setter] = useNotNullLocalStorage( + const [value, setter] = hooks.useNotNullLocalStorage( "backend-url", url || calculateRootPath(), ); - const [triedToLog, setTriedToLog] = useLocalStorage("tried-login"); + const [triedToLog, setTriedToLog] = hooks.useLocalStorage("tried-login"); const checkedSetter = (v: ValueOrFunction) => { setTriedToLog("yes"); @@ -55,13 +55,13 @@ export function useBackendDefaultToken(): [ string | undefined, StateUpdater, ] { - return useLocalStorage("backend-token"); + return hooks.useLocalStorage("backend-token"); } export function useBackendInstanceToken( id: string, ): [string | undefined, StateUpdater] { - const [token, setToken] = useLocalStorage(`backend-token-${id}`); + const [token, setToken] = hooks.useLocalStorage(`backend-token-${id}`); const [defaultToken, defaultSetToken] = useBackendDefaultToken(); // instance named 'default' use the default token diff --git a/packages/demobank-ui/src/hooks/useLang.ts b/packages/demobank-ui/src/hooks/useLang.ts deleted file mode 100644 index 5b02c5255..000000000 --- a/packages/demobank-ui/src/hooks/useLang.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - This file is part of GNU Anastasis - (C) 2021-2022 Anastasis SARL - - GNU Anastasis is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Anastasis 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - GNU Anastasis; see the file COPYING. If not, see - */ - -import { useNotNullLocalStorage } from "./useLocalStorage.js"; - -function getBrowserLang(): string | undefined { - if (window.navigator.languages) return window.navigator.languages[0]; - if (window.navigator.language) return window.navigator.language; - return undefined; -} - -export function useLang( - initial?: string, -): [string, (s: string) => void, boolean] { - const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2); - return useNotNullLocalStorage("lang-preference", defaultLang); -} diff --git a/packages/demobank-ui/src/hooks/useLocalStorage.ts b/packages/demobank-ui/src/hooks/useLocalStorage.ts deleted file mode 100644 index ed5b491f2..000000000 --- a/packages/demobank-ui/src/hooks/useLocalStorage.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - This file is part of GNU Anastasis - (C) 2021-2022 Anastasis SARL - - GNU Anastasis is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Anastasis 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - GNU Anastasis; see the file COPYING. If not, see - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { StateUpdater, useState } from "preact/hooks"; - -export function useLocalStorage( - key: string, - initialValue?: string, -): [string | undefined, StateUpdater] { - const [storedValue, setStoredValue] = useState( - (): string | undefined => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }, - ); - - const setValue = ( - value?: string | ((val?: string) => string | undefined), - ): void => { - setStoredValue((p) => { - const toStore = value instanceof Function ? value(p) : value; - if (typeof window !== "undefined") { - if (!toStore) { - window.localStorage.removeItem(key); - } else { - window.localStorage.setItem(key, toStore); - } - } - return toStore; - }); - }; - - return [storedValue, setValue]; -} - -//TODO: merge with the above function -export function useNotNullLocalStorage( - key: string, - initialValue: string, -): [string, StateUpdater, boolean] { - const [storedValue, setStoredValue] = useState((): string => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }); - - const setValue = (value: string | ((val: string) => string)): void => { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - if (typeof window !== "undefined") { - if (!valueToStore) { - window.localStorage.removeItem(key); - } else { - window.localStorage.setItem(key, valueToStore); - } - } - }; - - const isSaved = window.localStorage.getItem(key) !== null; - return [storedValue, setValue, isSaved]; -} diff --git a/packages/demobank-ui/src/pages/home/index.tsx b/packages/demobank-ui/src/pages/home/index.tsx index 8b2ffefac..a64c4abe3 100644 --- a/packages/demobank-ui/src/pages/home/index.tsx +++ b/packages/demobank-ui/src/pages/home/index.tsx @@ -27,44 +27,14 @@ import { } from "preact/hooks"; import talerLogo from "../../assets/logo-white.svg"; import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js"; -import { - useLocalStorage, - useNotNullLocalStorage, -} from "../../hooks/useLocalStorage.js"; -// import { Translate, useTranslator } from "../../i18n/index.js"; import { useTranslationContext } from "../../context/translation.js"; import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; import { createHashHistory } from "history"; import Router, { Route, route } from "preact-router"; import { QrCodeSection } from "./QrCodeSection.js"; - -interface BankUiSettings { - allowRegistrations: boolean; - showDemoNav: boolean; - bankName: string; - demoSites: [string, string][]; -} - -/** - * Global settings for the demobank UI. - */ -const defaultSettings: BankUiSettings = { - allowRegistrations: true, - bankName: "Taler Bank", - showDemoNav: true, - demoSites: [ - ["Landing", "https://demo.taler.net/"], - ["Bank", "https://bank.demo.taler.net/"], - ["Essay Shop", "https://shop.demo.taler.net/"], - ["Donations", "https://donations.demo.taler.net/"], - ["Survey", "https://survey.demo.taler.net/"], - ], -}; - -const bankUiSettings: BankUiSettings = - "talerDemobankSettings" in globalThis - ? (globalThis as any).talerDemobankSettings - : defaultSettings; +import { hooks } from "@gnu-taler/web-util/lib/index.browser"; +import { bankUiSettings } from "../../settings.js"; +import { PageStateType, usePageContext } from "../../context/pageState.js"; /** * FIXME: @@ -90,94 +60,6 @@ const bankUiSettings: BankUiSettings = /************ * Contexts * ***********/ -const CurrencyContext = createContext(null); -type PageContextType = [PageStateType, StateUpdater]; -const PageContextDefault: PageContextType = [ - { - isLoggedIn: false, - isRawPayto: false, - showPublicHistories: false, - - withdrawalInProgress: false, - }, - () => { - null; - }, -]; -const PageContext = createContext(PageContextDefault); - -/********************************************** - * Type definitions for states and API calls. * - *********************************************/ - -/** - * Has the information to reach and - * authenticate at the bank's backend. - */ -interface BackendStateType { - url?: string; - username?: string; - password?: string; -} - -/** - * Request body of POST /transactions. - * - * If the amount appears twice: both as a Payto parameter and - * in the JSON dedicate field, the one on the Payto URI takes - * precedence. - */ -interface TransactionRequestType { - paytoUri: string; - amount?: string; // with currency. -} - -/** - * Request body of /register. - */ -interface CredentialsRequestType { - username?: string; - password?: string; - repeatPassword?: string; -} - -/** - * Request body of /register. - */ -// interface LoginRequestType { -// username: string; -// password: string; -// } - -interface WireTransferRequestType { - iban?: string; - subject?: string; - amount?: string; -} - -/** - * Track page state. - */ -interface PageStateType { - isLoggedIn: boolean; - isRawPayto: boolean; - showPublicHistories: boolean; - withdrawalInProgress: boolean; - error?: { - description?: string; - title: string; - debug?: string; - }; - - info?: string; - talerWithdrawUri?: string; - /** - * Not strictly a presentational value, could - * be moved in a future "withdrawal state" object. - */ - withdrawalId?: string; - timestamp?: number; -} /** * Bank account specific information. @@ -294,7 +176,7 @@ async function postToBackend( } function useTransactionPageNumber(): [number, StateUpdater] { - const ret = useNotNullLocalStorage("transaction-page", "0"); + const ret = hooks.useNotNullLocalStorage("transaction-page", "0"); const retObj = JSON.parse(ret[0]); const retSetter: StateUpdater = function (val) { const newVal = @@ -347,7 +229,10 @@ const getBankBackendBaseUrl = (): string => { function useShowPublicAccount( state?: string, ): [string | undefined, StateUpdater] { - const ret = useLocalStorage("show-public-account", JSON.stringify(state)); + const ret = hooks.useLocalStorage( + "show-public-account", + JSON.stringify(state), + ); const retObj: string | undefined = ret[0] ? JSON.parse(ret[0]) : ret[0]; const retSetter: StateUpdater = function (val) { const newVal = @@ -367,7 +252,7 @@ type RawPaytoInputTypeOpt = RawPaytoInputType | undefined; function useRawPaytoInputType( state?: RawPaytoInputType, ): [RawPaytoInputTypeOpt, StateUpdater] { - const ret = useLocalStorage("raw-payto-input-state", state); + const ret = hooks.useLocalStorage("raw-payto-input-state", state); const retObj: RawPaytoInputTypeOpt = ret[0]; const retSetter: StateUpdater = function (val) { const newVal = val instanceof Function ? val(retObj) : val; @@ -387,7 +272,7 @@ type WireTransferRequestTypeOpt = WireTransferRequestType | undefined; function useWireTransferRequestType( state?: WireTransferRequestType, ): [WireTransferRequestTypeOpt, StateUpdater] { - const ret = useLocalStorage( + const ret = hooks.useLocalStorage( "wire-transfer-request-state", JSON.stringify(state), ); @@ -413,7 +298,7 @@ type CredentialsRequestTypeOpt = CredentialsRequestType | undefined; function useCredentialsRequestType( state?: CredentialsRequestType, ): [CredentialsRequestTypeOpt, StateUpdater] { - const ret = useLocalStorage( + const ret = hooks.useLocalStorage( "credentials-request-state", JSON.stringify(state), ); @@ -439,7 +324,7 @@ type BackendStateTypeOpt = BackendStateType | undefined; function useBackendState( state?: BackendStateType, ): [BackendStateTypeOpt, StateUpdater] { - const ret = useLocalStorage("backend-state", JSON.stringify(state)); + 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 = @@ -451,67 +336,6 @@ function useBackendState( return [retObj, retSetter]; } -/** - * Keep mere business information, like account balance or - * transactions history. - */ -// type AccountStateTypeOpt = AccountStateType | undefined; -// function useAccountState( -// state?: AccountStateType, -// ): [AccountStateTypeOpt, StateUpdater] { -// const ret = useLocalStorage("account-state", JSON.stringify(state)); -// const retObj: AccountStateTypeOpt = 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]; -// } - -/** - * Wrapper providing defaults. - */ -function usePageState( - state: PageStateType = { - isLoggedIn: false, - isRawPayto: false, - showPublicHistories: false, - withdrawalInProgress: false, - }, -): [PageStateType, StateUpdater] { - const ret = useNotNullLocalStorage("page-state", JSON.stringify(state)); - const retObj: PageStateType = JSON.parse(ret[0]); - - const retSetter: StateUpdater = function (val) { - const newVal = - val instanceof Function - ? JSON.stringify(val(retObj)) - : JSON.stringify(val); - - ret[1](newVal); - }; - - //when moving from one page to another - //clean up the info and error bar - function removeLatestInfo(val: any): ReturnType { - const updater = typeof val === "function" ? val : (c: any) => val; - return retSetter((current: any) => { - const cleanedCurrent: PageStateType = { - ...current, - info: undefined, - errors: undefined, - timestamp: new Date().getTime(), - }; - return updater(cleanedCurrent); - }); - } - - return [retObj, removeLatestInfo]; -} - /** * Request preparators. * @@ -1045,7 +869,7 @@ function StatusBanner(Props: any): VNode | null { function BankFrame(Props: any): VNode { const { i18n } = useTranslationContext(); - const [pageState, pageStateSetter] = useContext(PageContext); + const { pageState, pageStateSetter } = usePageContext(); console.log("BankFrame state", pageState); const logOut = (
@@ -1164,19 +988,16 @@ function ShowInputErrorLabel({ } function PaytoWireTransfer(Props: any): VNode { - const currency = useContext(CurrencyContext); - const [pageState, pageStateSetter] = useContext(PageContext); // NOTE: used for go-back button? + const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button? + const [submitData, submitDataSetter] = useWireTransferRequestType(); - // const [rawPaytoInput, rawPaytoInputSetter] = useRawPaytoInputType(); + const [rawPaytoInput, rawPaytoInputSetter] = useState( undefined, ); const { i18n } = useTranslationContext(); - const { focus, backendState } = Props; - const amountRegex = "^[0-9]+(.[0-9]+)?$"; + const { focus, backendState, currency } = Props; const ibanRegex = "^[A-Z][A-Z][0-9]+$"; - const receiverInput = ""; - const subjectInput = ""; let transactionData: TransactionRequestType; const ref = useRef(null); useEffect(() => { @@ -1213,7 +1034,7 @@ function PaytoWireTransfer(Props: any): VNode { if (!pageState.isRawPayto) return (
-
+

 
  + +   { submitDataSetter((submitData: any) => ({ ...submitData, @@ -1275,16 +1105,6 @@ function PaytoWireTransfer(Props: any): VNode { })); }} /> -   -

-
+

{i18n.str`Confirm Withdrawal`}

-
+

{i18n.str`Authorize withdrawal by solving challenge`}

@@ -1562,8 +1382,8 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode { */ function TalerWithdrawalQRCode(Props: any): VNode { // turns true when the wallet POSTed the reserve details: - const [pageState, pageStateSetter] = useContext(PageContext); - const { withdrawalId, talerWithdrawUri, accountLabel, backendState } = Props; + const { pageState, pageStateSetter } = usePageContext(); + const { withdrawalId, talerWithdrawUri, backendState } = Props; const { i18n } = useTranslationContext(); const abortButton = ( (null); useEffect(() => { if (focus) ref.current?.focus(); }, [focus]); return ( -

+ ); } @@ -1721,8 +1537,7 @@ function WalletWithdraw(Props: any): VNode { * then specify the details trigger the action. */ function PaymentOptions(Props: any): VNode { - const { backendState, pageStateSetter, focus } = Props; - const currency = useContext(CurrencyContext); + const { backendState, pageStateSetter, currency } = Props; const { i18n } = useTranslationContext(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">( @@ -1756,6 +1571,7 @@ function PaymentOptions(Props: any): VNode {
@@ -1766,6 +1582,7 @@ function PaymentOptions(Props: any): VNode {
@@ -1819,7 +1636,7 @@ function LoginForm(Props: any): VNode { return (