taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit a1c5917e626856f2abd9dbe6ddaa71c1458334c6
parent 6837a9dc6f677babe798bc94c0baa1f11c0edb55
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 26 Apr 2024 14:31:48 -0300

update code to match others

Diffstat:
Mpackages/aml-backoffice-ui/copyleft-header.js | 2+-
Mpackages/aml-backoffice-ui/src/App.tsx | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Dpackages/aml-backoffice-ui/src/Dashboard.tsx | 266-------------------------------------------------------------------------------
Apackages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx | 273+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/aml-backoffice-ui/src/Routing.tsx | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/aml-backoffice-ui/src/context/config.ts | 100-------------------------------------------------------------------------------
Apackages/aml-backoffice-ui/src/context/settings.ts | 44++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/declaration.d.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_11e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_12e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_13e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_15e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_1e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_4e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_5e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/902_9e.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/declaration.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/icons.tsx | 2+-
Mpackages/aml-backoffice-ui/src/forms/index.ts | 2+-
Mpackages/aml-backoffice-ui/src/forms/simplest.ts | 2+-
Apackages/aml-backoffice-ui/src/hooks/form.ts | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/aml-backoffice-ui/src/hooks/officer.ts | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/aml-backoffice-ui/src/hooks/preferences.ts | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/aml-backoffice-ui/src/hooks/useBackend.ts | 48------------------------------------------------
Mpackages/aml-backoffice-ui/src/hooks/useCaseDetails.ts | 8++++----
Mpackages/aml-backoffice-ui/src/hooks/useCases.ts | 50++++++++++++++++++++++++++++++++++++--------------
Dpackages/aml-backoffice-ui/src/hooks/useOfficer.ts | 150-------------------------------------------------------------------------------
Dpackages/aml-backoffice-ui/src/hooks/useSettings.ts | 71-----------------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/i18n/bank.pot | 2+-
Mpackages/aml-backoffice-ui/src/i18n/fr.po | 2+-
Mpackages/aml-backoffice-ui/src/i18n/poheader | 2+-
Mpackages/aml-backoffice-ui/src/i18n/strings-prelude | 2+-
Mpackages/aml-backoffice-ui/src/i18n/strings.ts | 2+-
Mpackages/aml-backoffice-ui/src/index.html | 2+-
Mpackages/aml-backoffice-ui/src/index.tsx | 2+-
Dpackages/aml-backoffice-ui/src/pages.ts | 58----------------------------------------------------------
Mpackages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx | 20+++++++++++++-------
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 6+++---
Apackages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/pages/Cases.stories.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 6+++---
Mpackages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 8++++----
Mpackages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx | 4++--
Dpackages/aml-backoffice-ui/src/pages/NewFormEntry.tsx | 136-------------------------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/pages/Officer.tsx | 27++++++++++++++++++---------
Mpackages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/UnlockAccount.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/index.stories.ts | 2+-
Dpackages/aml-backoffice-ui/src/route.ts | 239-------------------------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/settings.ts | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/aml-backoffice-ui/src/stories.test.ts | 23++++++++++++++++++-----
Mpackages/aml-backoffice-ui/src/stories.tsx | 51++++++++++++++++++++++++++++++++-------------------
Mpackages/aml-backoffice-ui/src/utils/QR.tsx | 2+-
56 files changed, 1268 insertions(+), 1200 deletions(-)

diff --git a/packages/aml-backoffice-ui/copyleft-header.js b/packages/aml-backoffice-ui/copyleft-header.js @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -13,26 +13,46 @@ 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 { TranslationProvider } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; +import { + BrowserHashNavigationProvider, + ExchangeApiProvider, + Loading, + TranslationProvider, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; import { SWRConfig } from "swr"; -import { ExchangeApiProvider } from "./context/config.js"; -import { ExchangeAmlFrame } from "./Dashboard.js"; -import { getInitialBackendBaseURL } from "./hooks/useBackend.js"; -import { Pages } from "./pages.js"; -import { HashPathProvider, Router } from "./route.js"; +import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js"; +import { Routing } from "./Routing.js"; +import { SettingsProvider } from "./context/settings.js"; +import { strings } from "./i18n/strings.js"; import "./scss/main.css"; +import { UiSettings, fetchSettings } from "./settings.js"; const WITH_LOCAL_STORAGE_CACHE = false; -const pageList = Object.values(Pages); - export function App(): VNode { - const baseUrl = getInitialBackendBaseURL(); + const [settings, setSettings] = useState<UiSettings>(); + useEffect(() => { + fetchSettings(setSettings); + }, []); + if (!settings) return <Loading />; + + const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( - <TranslationProvider source={{}}> - <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}> - <HashPathProvider> + <SettingsProvider value={settings}> + <TranslationProvider + source={strings} + completeness={{ + es: strings["es"].completeness, + de: strings["de"].completeness, + }} + > + <ExchangeApiProvider + baseUrl={new URL("/", baseUrl)} + frameOnError={ExchangeAmlFrame} + > <SWRConfig value={{ provider: WITH_LOCAL_STORAGE_CACHE @@ -60,19 +80,13 @@ export function App(): VNode { keepPreviousData: true, }} > - <ExchangeAmlFrame> - <Router - pageList={pageList} - onNotFound={() => { - window.location.href = Pages.cases.url; - return <div>not found</div>; - }} - /> - </ExchangeAmlFrame> + <BrowserHashNavigationProvider> + <Routing /> + </BrowserHashNavigationProvider> </SWRConfig> - </HashPathProvider> - </ExchangeApiProvider> - </TranslationProvider> + </ExchangeApiProvider> + </TranslationProvider> + </SettingsProvider> ); } @@ -85,3 +99,34 @@ function localStorageProvider(): Map<unknown, unknown> { }); return map; } + +function getInitialBackendBaseURL( + backendFromSettings: string | undefined, +): string { + const overrideUrl = + typeof localStorage !== "undefined" + ? localStorage.getItem("exchange-base-url") + : undefined; + let result: string; + + if (!overrideUrl) { + // normal path + if (!backendFromSettings) { + console.error( + "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", + ); + result = window.origin; + } else { + result = backendFromSettings; + } + } else { + // testing/development path + result = overrideUrl; + } + try { + return canonicalizeBaseUrl(result); + } catch (e) { + // fall back + return canonicalizeBaseUrl(window.origin); + } +} diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx @@ -1,266 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { - Footer, - Header, - ToastBanner, - notifyError, - notifyException, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { ComponentChildren, VNode, h } from "preact"; -import { useEffect, useErrorBoundary } from "preact/hooks"; -import { useOfficer } from "./hooks/useOfficer.js"; -import { - getAllBooleanSettings, - getLabelForSetting, - useSettings, -} from "./hooks/useSettings.js"; -import { Pages } from "./pages.js"; -import { PageEntry, useChangeLocation } from "./route.js"; -import { uiSettings } from "./settings.js"; - -/** - * mapping route to view - * not found (error page) - * nested, index element, relative routes - * link interception - * form POST interception, call action - * fromData => Object.fromEntries - * segments in the URL - * navigationState: idle, submitting, loading - * form GET interception: does a navigateTo - * form GET Sync: - * 1.- back after submit: useEffect to sync URL to form - * 2.- refresh after submit: input default value - * useSubmit for form submission onChange, history replace - * - * post form without redirect - * - * - * @param param0 - * @returns - */ - -const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; -const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; - -/** - * TO BE FIXED: - * - * 1.- when the form change to other form and both form share the same structure - * the same input component may be rendered in the same place, - * since input are uncontrolled the are not re-rendered and since they are - * uncontrolled it will keep the value of the previous form. - * One solutions could be to remove the form when unloading and when the new - * form load it will start without previous vdom, preventing the cache - * to create this behavior. - * Other solutions could be using IDs in the fields that are constructed - * with the ID of the form, so two fields of different form will need to re-render - * cleaning up the state of the previous form. - * - * 2.- currently the design prop and the behavior prop of the flexible form - * are two side of the same coin. From the design point of view, it is important - * to design the form in a list-of-field manner and there may be additional - * content that is not directly mapped to the form structure (object) - * So maybe we want to change the current shape so the computation of the state - * of the form is in a field level, but this computation required the field value and - * the whole form values and state (since one field may be disabled/hidden) because - * of the value of other field. - * - * 3.- given the previous requirement, maybe the name of the field of the form could be - * a function (P: F -> V) where F is the form (or parent object) and V is the type of the - * property. That will help with the typing of the forms props - * - * 4.- tooltip are not placed correctly: the arrow should point the question mark - * and the text area should be bigger - * - */ - -/** - * check this fields - * - * Signature of Contracting partner, 902_9e - * Currency and amount of deposited assets, 902_5e - * Signature on declaration of trust, 902.13e - * also fundations - * also life insurance - * - * no all state are handled by all the inputs - * all the input implementation should respect - * ui props and state - */ - -export function ExchangeAmlFrame({ - children, -}: { - children?: ComponentChildren; -}): VNode { - const { i18n } = useTranslationContext(); - - const [error] = useErrorBoundary(); - - useEffect(() => { - if (error) { - if (error instanceof Error) { - notifyException(i18n.str`Internal error, please report.`, error); - } else { - notifyError( - i18n.str`Internal error, please report.`, - String(error) as TranslatedString, - ); - } - console.log(error); - // resetError() - } - }, [error]); - - const officer = useOfficer(); - const [settings, updateSettings] = useSettings(); - - return ( - <div - class="min-h-full flex flex-col m-0 bg-slate-200" - style="min-height: 100vh;" - > - <div class="bg-indigo-600 pb-32"> - <Header - title="Exchange" - iconLinkURL={uiSettings.backendBaseURL ?? "#"} - onLogout={ - officer.state !== "ready" - ? undefined - : () => { - officer.lock(); - } - } - sites={[]} - supportedLangs={["en", "es", "de"]} - > - <li> - <div class="text-xs font-semibold leading-6 text-gray-400"> - <i18n.Translate>Preferences</i18n.Translate> - </div> - <ul role="list" class="space-y-1"> - {getAllBooleanSettings().map((set) => { - const isOn: boolean = !!settings[set]; - return ( - <li key={set} class="mt-2 pl-2"> - <div class="flex items-center justify-between"> - <span class="flex flex-grow flex-col"> - <span - class="text-sm text-black font-medium leading-6 " - id="availability-label" - > - {getLabelForSetting(set, i18n)} - </span> - </span> - <button - type="button" - data-enabled={isOn} - class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" - role="switch" - aria-checked="false" - aria-labelledby="availability-label" - aria-describedby="availability-description" - onClick={() => { - updateSettings(set, !isOn); - }} - > - <span - aria-hidden="true" - data-enabled={isOn} - class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" - ></span> - </button> - </div> - </li> - ); - })} - </ul> - </li> - </Header> - </div> - - <div class="fixed z-20 w-full"> - <div class="mx-auto w-4/5"> - <ToastBanner /> - </div> - </div> - - <div class="-mt-32 flex grow "> - {officer.state !== "ready" ? undefined : <Navigation />} - <div class="flex mx-auto my-4"> - <main class="rounded-lg bg-white px-5 py-6 shadow">{children}</main> - </div> - </div> - - <Footer - testingUrlKey="exchange-base-url" - GIT_HASH={GIT_HASH} - VERSION={VERSION} - /> - </div> - ); -} - -function Navigation(): VNode { - const pageList: Array<PageEntry> = [Pages.officer, Pages.cases]; - const location = useChangeLocation(); - return ( - <div class="hidden sm:block min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip"> - <nav class="flex flex-1 flex-col mx-4 mt-4 mb-2"> - <ul role="list" class="flex flex-1 flex-col gap-y-7"> - <li> - <ul role="list" class="-mx-2 space-y-1"> - {pageList.map((p) => { - return ( - <li key={p.url}> - <a - href={p.url} - data-selected={location == p.url} - class="data-[selected=true]:bg-indigo-700 pr-4 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" - > - {p.Icon && <p.Icon />} - <span class="hidden md:inline">{p.name}</span> - </a> - </li> - ); - })} - {/* <li> - <a href="#" class="text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> - - <i18n.Translate>Officer</i18n.Translate> - </a> - </li> */} - </ul> - </li> - - {/* <li class="mt-auto "> - <a href="#" class="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 hover:bg-indigo-700 hover:text-white"> - <svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> - <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> - </svg> - Settings - </a> - </li> */} - </ul> - </nav> - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -0,0 +1,273 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { TranslatedString } from "@gnu-taler/taler-util"; +import { + Footer, + Header, + ToastBanner, + notifyError, + notifyException, + useNavigationContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { ComponentChildren, VNode, h } from "preact"; +import { useEffect, useErrorBoundary } from "preact/hooks"; +import { privatePages } from "./Routing.js"; +import { useSettingsContext } from "./context/settings.js"; +import { OfficerState } from "./hooks/officer.js"; +import { + getAllBooleanPreferences, + getLabelForPreferences, + usePreferences, +} from "./hooks/preferences.js"; +import { HomeIcon } from "./pages/Cases.js"; + +/** + * mapping route to view + * not found (error page) + * nested, index element, relative routes + * link interception + * form POST interception, call action + * fromData => Object.fromEntries + * segments in the URL + * navigationState: idle, submitting, loading + * form GET interception: does a navigateTo + * form GET Sync: + * 1.- back after submit: useEffect to sync URL to form + * 2.- refresh after submit: input default value + * useSubmit for form submission onChange, history replace + * + * post form without redirect + * + * + * @param param0 + * @returns + */ + +const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; +const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; + +/** + * TO BE FIXED: + * + * 1.- when the form change to other form and both form share the same structure + * the same input component may be rendered in the same place, + * since input are uncontrolled the are not re-rendered and since they are + * uncontrolled it will keep the value of the previous form. + * One solutions could be to remove the form when unloading and when the new + * form load it will start without previous vdom, preventing the cache + * to create this behavior. + * Other solutions could be using IDs in the fields that are constructed + * with the ID of the form, so two fields of different form will need to re-render + * cleaning up the state of the previous form. + * + * 2.- currently the design prop and the behavior prop of the flexible form + * are two side of the same coin. From the design point of view, it is important + * to design the form in a list-of-field manner and there may be additional + * content that is not directly mapped to the form structure (object) + * So maybe we want to change the current shape so the computation of the state + * of the form is in a field level, but this computation required the field value and + * the whole form values and state (since one field may be disabled/hidden) because + * of the value of other field. + * + * 3.- given the previous requirement, maybe the name of the field of the form could be + * a function (P: F -> V) where F is the form (or parent object) and V is the type of the + * property. That will help with the typing of the forms props + * + * 4.- tooltip are not placed correctly: the arrow should point the question mark + * and the text area should be bigger + * + */ + +/** + * check this fields + * + * Signature of Contracting partner, 902_9e + * Currency and amount of deposited assets, 902_5e + * Signature on declaration of trust, 902.13e + * also fundations + * also life insurance + * + * no all state are handled by all the inputs + * all the input implementation should respect + * ui props and state + */ + +export function ExchangeAmlFrame({ + children, + officer, +}: { + officer?: OfficerState, + children?: ComponentChildren; +}): VNode { + const { i18n } = useTranslationContext(); + + const [error] = useErrorBoundary(); + + useEffect(() => { + if (error) { + if (error instanceof Error) { + notifyException(i18n.str`Internal error, please report.`, error); + } else { + notifyError( + i18n.str`Internal error, please report.`, + String(error) as TranslatedString, + ); + } + console.log(error); + // resetError() + } + }, [error]); + + const [preferences, updatePreferences] = usePreferences(); + const settings = useSettingsContext() + + return ( + <div + class="min-h-full flex flex-col m-0 bg-slate-200" + style="min-height: 100vh;" + > + <div class="bg-indigo-600 pb-32"> + <Header + title="Exchange" + iconLinkURL={settings.backendBaseURL ?? "#"} + onLogout={ + officer?.state !== "ready" + ? undefined + : () => { + officer.lock(); + } + } + sites={[]} + supportedLangs={["en", "es", "de"]} + > + <li> + <div class="text-xs font-semibold leading-6 text-gray-400"> + <i18n.Translate>Preferences</i18n.Translate> + </div> + <ul role="list" class="space-y-1"> + {getAllBooleanPreferences().map((set) => { + const isOn: boolean = !!preferences[set]; + return ( + <li key={set} class="mt-2 pl-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span + class="text-sm text-black font-medium leading-6 " + id="availability-label" + > + {getLabelForPreferences(set, i18n)} + </span> + </span> + <button + type="button" + data-enabled={isOn} + class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" + role="switch" + aria-checked="false" + aria-labelledby="availability-label" + aria-describedby="availability-description" + onClick={() => { + updatePreferences(set, !isOn); + }} + > + <span + aria-hidden="true" + data-enabled={isOn} + class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" + ></span> + </button> + </div> + </li> + ); + })} + </ul> + </li> + </Header> + </div> + + <div class="fixed z-20 w-full"> + <div class="mx-auto w-4/5"> + <ToastBanner /> + </div> + </div> + + <div class="-mt-32 flex grow "> + {officer?.state !== "ready" ? undefined : <Navigation />} + <div class="flex mx-auto my-4"> + <main class="rounded-lg bg-white px-5 py-6 shadow">{children}</main> + </div> + </div> + + <Footer + testingUrlKey="exchange-base-url" + GIT_HASH={GIT_HASH} + VERSION={VERSION} + /> + </div> + ); +} + +function Navigation(): VNode { + const { i18n } = useTranslationContext(); + const pageList = [ + { route: privatePages.account, Icon: HomeIcon, label: i18n.str`Account` }, + { route: privatePages.cases, Icon: HomeIcon, label: i18n.str`Cases` }, + ]; + const { path } = useNavigationContext(); + return ( + <div class="hidden sm:block min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip"> + <nav class="flex flex-1 flex-col mx-4 mt-4 mb-2"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + <li> + <ul role="list" class="-mx-2 space-y-1"> + {pageList.map((p, idx) => { + return ( + <li key={idx}> + <a + href={p.route.url({})} + data-selected={path == p.route.url({})} + class="data-[selected=true]:bg-indigo-700 pr-4 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" + > + {p.Icon && <p.Icon />} + <span class="hidden md:inline">{p.label}</span> + </a> + </li> + ); + })} + {/* <li> + <a href="#" class="text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> + + <i18n.Translate>Officer</i18n.Translate> + </a> + </li> */} + </ul> + </li> + + {/* <li class="mt-auto "> + <a href="#" class="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 hover:bg-indigo-700 hover:text-white"> + <svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> + <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> + </svg> + Settings + </a> + </li> */} + </ul> + </nav> + </div> + ); +} diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -0,0 +1,151 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + urlPattern, + useCurrentLocation, + useNavigationContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; + +import { assertUnreachable } from "@gnu-taler/taler-util"; +import { useEffect } from "preact/hooks"; +import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js"; +import { useOfficer } from "./hooks/officer.js"; +import { Cases } from "./pages/Cases.js"; +import { Officer } from "./pages/Officer.js"; +import { CaseDetails } from "./pages/CaseDetails.js"; +import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js"; +import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js"; + +export function Routing(): VNode { + const session = useOfficer(); + + if (session.state === "ready") { + return ( + <ExchangeAmlFrame officer={session}> + <PrivateRouting /> + </ExchangeAmlFrame> + ); + } + return ( + <ExchangeAmlFrame> + <PublicRounting /> + </ExchangeAmlFrame> + ); +} + +const publicPages = { + config: urlPattern(/\/config/, () => "#/config"), + login: urlPattern(/\/login/, () => "#/login"), +}; + +function PublicRounting(): VNode { + const { i18n } = useTranslationContext(); + const location = useCurrentLocation(publicPages); + // const { navigateTo } = useNavigationContext(); + // const { config, lib } = useExchangeApiContext(); + // const [notification, notify, handleError] = useLocalNotification(); + const session = useOfficer(); + + if (location === undefined) { + if (session.state !== "ready") { + return <HandleAccountNotReady officer={session}/>; + } else { + return <div /> + } + } + + switch (location.name) { + case "config": { + return ( + <Fragment> + <div class="sm:mx-auto sm:w-full sm:max-w-sm"> + <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to exchange config!`}</h2> + </div> + </Fragment> + ); + } + case "login": { + return ( + <Fragment> + <div class="sm:mx-auto sm:w-full sm:max-w-sm"> + <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to exchange config!`}</h2> + </div> + </Fragment> + ); + } + default: + assertUnreachable(location); + } +} + +export const privatePages = { + account: urlPattern(/\/account/, () => "#/account"), + cases: urlPattern(/\/cases/, () => "#/cases"), + caseDetails: urlPattern<{ cid: string }>( + /\/case\/(?<cid>[a-zA-Z0-9]+)/, + ({ cid }) => `#/case/${cid}`, + ), + caseUpdate: urlPattern<{ cid: string; type: string }>( + /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9]+)/, + ({ cid, type }) => `#/case/${cid}/new/${type}`, + ), + caseNew: urlPattern<{ cid: string }>( + /\/case\/(?<cid>[a-zA-Z0-9]+)\/new/, + ({ cid }) => `#/case/${cid}/new`, + ), +}; + +function PrivateRouting(): VNode { + const { navigateTo } = useNavigationContext(); + const location = useCurrentLocation(privatePages); + useEffect(() => { + if (location === undefined) { + navigateTo(privatePages.account.url({})); + } + }, [location]); + + if (location === undefined) { + return <Fragment />; + } + + switch (location.name) { + case "account": { + return <Officer />; + } + case "caseDetails": { + return <CaseDetails account={location.values.cid} />; + } + case "caseUpdate": { + return ( + <CaseUpdate + account={location.values.cid} + type={location.values.type} + /> + ); + } + case "caseNew": { + return <SelectForm account={location.values.cid} />; + } + case "cases": { + return <Cases />; + } + default: + assertUnreachable(location); + } +} diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts @@ -1,100 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { TalerExchangeApi, TalerExchangeHttpClient, TalerError } from "@gnu-taler/taler-util"; -import { BrowserFetchHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact"; -import { useContext, useEffect, useState } from "preact/hooks"; -import { ErrorLoading } from "@gnu-taler/web-util/browser"; - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -export type Type = { - url: URL, - config: TalerExchangeApi.ExchangeVersionResponse, - api: TalerExchangeHttpClient, -}; - -const Context = createContext<Type>(undefined!); - -export const useExchangeApiContext = (): Type => useContext(Context); -export const useMaybeExchangeApiContext = (): Type | undefined => useContext(Context); - -export function ExchangeApiContextTesting({ config, children }: { config: TalerExchangeApi.ExchangeVersionResponse, children?: ComponentChildren; }): VNode { - return h(Context.Provider, { - value: { url: new URL("http://testing"), config, api: null! }, - children - } - ) -} - -export type ConfigResult = undefined - | { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse } - | { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string } - | { type: "error", error: TalerError } - -export const ExchangeApiProvider = ({ - baseUrl, - children, - frameOnError, -}: { - baseUrl: string, - children: ComponentChildren; - frameOnError: FunctionComponent<{ children: ComponentChildren }>, -}): VNode => { - const [checked, setChecked] = useState<ConfigResult>() - const { i18n } = useTranslationContext(); - const url = new URL(baseUrl) - const api = new TalerExchangeHttpClient(url.href, new BrowserFetchHttpLib()) - useEffect(() => { - api.getConfig() - .then((resp) => { - if (resp.type === "fail") { - setChecked({ type: "error", error: TalerError.fromUncheckedDetail(resp.detail) }); - } else if (api.isCompatible(resp.body.version)) { - setChecked({ type: "ok", config: resp.body }); - } else { - setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION }) - } - }) - .catch((error: unknown) => { - if (error instanceof TalerError) { - setChecked({ type: "error", error }); - } - }); - }, []); - - if (checked === undefined) { - return h(frameOnError, { children: h("div", {}, "loading...") }) - } - if (checked.type === "error") { - return h(frameOnError, { children: h(ErrorLoading, { error: checked.error, showDetail: true }) }) - } - if (checked.type === "incompatible") { - return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) }) - } - const value: Type = { - url, config: checked.config, api - } - return h(Context.Provider, { - value, - children, - }); -}; - diff --git a/packages/aml-backoffice-ui/src/context/settings.ts b/packages/aml-backoffice-ui/src/context/settings.ts @@ -0,0 +1,44 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { UiSettings } from "../settings.js"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type Type = UiSettings; + +const initial: UiSettings = {}; +const Context = createContext<Type>(initial); + +export const useSettingsContext = (): Type => useContext(Context); + +export const SettingsProvider = ({ + children, + value, +}: { + value: UiSettings; + children: ComponentChildren; +}): VNode => { + return h(Context.Provider, { + value, + children, + }); +}; diff --git a/packages/aml-backoffice-ui/src/declaration.d.ts b/packages/aml-backoffice-ui/src/declaration.d.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms.ts b/packages/aml-backoffice-ui/src/forms.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/declaration.ts b/packages/aml-backoffice-ui/src/forms/declaration.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/icons.tsx b/packages/aml-backoffice-ui/src/forms/icons.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -0,0 +1,124 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { AmountJson, TranslatedString } from "@gnu-taler/taler-util"; +import { useState } from "preact/hooks"; + +export type UIField = { + value: string | undefined; + onUpdate: (s: string) => void; + error: TranslatedString | undefined; +}; + +type FormHandler<T> = { + [k in keyof T]?: T[k] extends string + ? UIField + : T[k] extends AmountJson + ? UIField + : FormHandler<T[k]>; +}; + +export type FormValues<T> = { + [k in keyof T]: T[k] extends string + ? string | undefined + : T[k] extends AmountJson + ? string | undefined + : FormValues<T[k]>; +}; + +export type RecursivePartial<T> = { + [k in keyof T]?: T[k] extends string + ? string + : T[k] extends AmountJson + ? AmountJson + : RecursivePartial<T[k]>; +}; + +export type FormErrors<T> = { + [k in keyof T]?: T[k] extends string + ? TranslatedString + : T[k] extends AmountJson + ? TranslatedString + : FormErrors<T[k]>; +}; + +export type FormStatus<T> = + | { + status: "ok"; + result: T; + errors: undefined; + } + | { + status: "fail"; + result: RecursivePartial<T>; + errors: FormErrors<T>; + }; + +function constructFormHandler<T>( + form: FormValues<T>, + updateForm: (d: FormValues<T>) => void, + errors: FormErrors<T> | undefined, +): FormHandler<T> { + + const keys = Object.keys(form) as Array<keyof T>; + + const handler = keys.reduce((prev, fieldName) => { + const currentValue: unknown = form[fieldName]; + const currentError: unknown = errors ? errors[fieldName] : undefined; + function updater(newValue: unknown) { + updateForm({ ...form, [fieldName]: newValue }); + } + if (typeof currentValue === "object") { + // @ts-expect-error FIXME better typing + const group = constructFormHandler(currentValue, updater, currentError); + // @ts-expect-error FIXME better typing + prev[fieldName] = group; + return prev; + } + const field: UIField = { + // @ts-expect-error FIXME better typing + error: currentError, + // @ts-expect-error FIXME better typing + value: currentValue, + onUpdate: updater, + }; + // @ts-expect-error FIXME better typing + prev[fieldName] = field; + return prev; + }, {} as FormHandler<T>); + + return handler; +} + +/** + * FIXME: Consider sending this to web-utils + * + * + * @param defaultValue + * @param check + * @returns + */ +export function useFormState<T>( + defaultValue: FormValues<T>, + check: (f: FormValues<T>) => FormStatus<T>, +): [FormHandler<T>, FormStatus<T>] { + const [form, updateForm] = useState<FormValues<T>>(defaultValue); + + const status = check(form); + const handler = constructFormHandler(form, updateForm, status.errors); + + return [handler, status]; +} diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts @@ -0,0 +1,159 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + AbsoluteTime, + Codec, + LockedAccount, + OfficerAccount, + OfficerId, + SigningKey, + buildCodecForObject, + codecForAbsoluteTime, + codecForString, + createNewOfficerAccount, + decodeCrock, + encodeCrock, + unlockOfficerAccount, +} from "@gnu-taler/taler-util"; +import { buildStorageKey, useExchangeApiContext, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { useMemo } from "preact/hooks"; +import { usePreferences } from "./preferences.js"; + +export interface Officer { + account: LockedAccount; + when: AbsoluteTime; +} + +const codecForLockedAccount = codecForString() as Codec<LockedAccount>; + +type OfficerAccountString = { + id: string; + strKey: string; +}; + +export const codecForOfficerAccount = (): Codec<OfficerAccountString> => + buildCodecForObject<OfficerAccountString>() + .property("id", codecForString()) // FIXME + .property("strKey", codecForString()) // FIXME + .build("OfficerAccount"); + +export const codecForOfficer = (): Codec<Officer> => + buildCodecForObject<Officer>() + .property("account", codecForLockedAccount) // FIXME + .property("when", codecForAbsoluteTime) // FIXME + .build("Officer"); + +export type OfficerState = OfficerNotReady | OfficerReady; +export type OfficerNotReady = OfficerNotFound | OfficerLocked; +interface OfficerNotFound { + state: "not-found"; + create: (password: string) => Promise<void>; +} +interface OfficerLocked { + state: "locked"; + forget: () => void; + tryUnlock: (password: string) => Promise<void>; +} +interface OfficerReady { + state: "ready"; + account: OfficerAccount; + forget: () => void; + lock: () => void; +} + +const OFFICER_KEY = buildStorageKey("officer", codecForOfficer()); +const DEV_ACCOUNT_KEY = buildStorageKey( + "account-dev", + codecForOfficerAccount(), +); + +export function useOfficer(): OfficerState { + const exchangeContext = useExchangeApiContext(); + const [pref] = usePreferences(); + pref.keepSessionAfterReload; + // dev account, is kept on reloaded. + const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY); + const account = useMemo(() => { + if (!accountStorage.value) return undefined; + + return { + id: accountStorage.value.id as OfficerId, + signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey, + }; + }, [accountStorage.value?.id, accountStorage.value?.strKey]); + + const officerStorage = useLocalStorage(OFFICER_KEY); + const officer = useMemo(() => { + if (!officerStorage.value) return undefined; + return officerStorage.value; + }, [officerStorage.value?.account, officerStorage.value?.when.t_ms]); + + if (officer === undefined) { + return { + state: "not-found", + create: async (pwd: string) => { + const req = await fetch( + new URL("seed", exchangeContext.lib.exchange.baseUrl).href, + ); + const b = await req.blob(); + const ar = await b.arrayBuffer(); + const uintar = new Uint8Array(ar); + + const { id, safe, signingKey } = await createNewOfficerAccount( + pwd, + uintar, + ); + officerStorage.update({ + account: safe, + when: AbsoluteTime.now(), + }); + + // accountStorage.update({ id, signingKey }); + const strKey = encodeCrock(signingKey); + accountStorage.update({ id, strKey }); + }, + }; + } + + if (account === undefined) { + return { + state: "locked", + forget: () => { + officerStorage.reset(); + }, + tryUnlock: async (pwd: string) => { + const ac = await unlockOfficerAccount(officer.account, pwd); + // accountStorage.update(ac); + accountStorage.update({ + id: ac.id, + strKey: encodeCrock(ac.signingKey), + }); + }, + }; + } + + return { + state: "ready", + account, + lock: () => { + accountStorage.reset(); + }, + forget: () => { + officerStorage.reset(); + accountStorage.reset(); + }, + }; +} diff --git a/packages/aml-backoffice-ui/src/hooks/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts @@ -0,0 +1,85 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + Codec, + TranslatedString, + buildCodecForObject, + codecForBoolean +} from "@gnu-taler/taler-util"; +import { + buildStorageKey, + useLocalStorage, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; + +interface Preferences { + allowInsecurePassword: boolean; + keepSessionAfterReload: boolean; +} + +export const codecForPreferences = (): Codec<Preferences> => + buildCodecForObject<Preferences>() + .property("allowInsecurePassword", (codecForBoolean())) + .property("keepSessionAfterReload", (codecForBoolean())) + .build("Preferences"); + +const defaultPreferences: Preferences = { + allowInsecurePassword: false, + keepSessionAfterReload: false, +}; + +const PREFERENCES_KEY = buildStorageKey( + "exchange-preferences", + codecForPreferences(), +); +/** + * User preferences. + * + * @returns tuple of [state, update()] + */ +export function usePreferences(): [ + Readonly<Preferences>, + <T extends keyof Preferences>(key: T, value: Preferences[T]) => void, +] { + const { value, update } = useLocalStorage( + PREFERENCES_KEY, + defaultPreferences, + ); + + function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + return [value, updateField]; +} + +export function getAllBooleanPreferences(): Array<keyof Preferences> { + return [ + "allowInsecurePassword", + "keepSessionAfterReload", + ]; +} + +export function getLabelForPreferences( + k: keyof Preferences, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): TranslatedString { + switch (k) { + case "allowInsecurePassword": return i18n.str`Allow Insecure password` + case "keepSessionAfterReload": return i18n.str`Keep session after reload` + } +} diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts @@ -1,48 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; -import { uiSettings } from "../settings.js"; - - -export function getInitialBackendBaseURL(): string { - const overrideUrl = - typeof localStorage !== "undefined" - ? localStorage.getItem("exchange-base-url") - : undefined; - - let result: string; - - if (!overrideUrl) { - //normal path - if (!uiSettings.backendBaseURL) { - console.error( - "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", - ); - result = typeof window !== "undefined" ? window.origin : "localhost" - } else { - result = uiSettings.backendBaseURL; - } - } else { - // testing/development path - result = overrideUrl - } - try { - return canonicalizeBaseUrl(result) - } catch (e) { - //fall back - return canonicalizeBaseUrl(window.origin) - } -} diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -16,15 +16,15 @@ import { OfficerAccount, PaytoString, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import _useSWR, { SWRHook } from "swr"; -import { useExchangeApiContext } from "../context/config.js"; -import { useOfficer } from "./useOfficer.js"; +import { useOfficer } from "./officer.js"; +import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export function useCaseDetails(paytoHash: string) { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; - const { api } = useExchangeApiContext(); + const { lib: {exchange: api} } = useExchangeApiContext(); async function fetcher([officer, account]: [OfficerAccount, PaytoString]) { return await api.getDecisionDetails(officer, account) diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -16,11 +16,16 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { OfficerAccount, OperationOk, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; +import { + OfficerAccount, + OperationOk, + TalerExchangeResultByMethod, + TalerHttpError, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook } from "swr"; -import { useExchangeApiContext } from "../context/config.js"; import { AmlExchangeBackend } from "../utils/types.js"; -import { useOfficer } from "./useOfficer.js"; +import { useOfficer } from "./officer.js"; +import { useExchangeApiContext } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export const PAGINATED_LIST_SIZE = 10; @@ -37,17 +42,28 @@ export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; export function useCases(state: AmlExchangeBackend.AmlState) { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; - const { api } = useExchangeApiContext(); + const { + lib: { exchange: api }, + } = useExchangeApiContext(); const [offset, setOffset] = useState<string>(); - async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) { + async function fetcher([officer, state, offset]: [ + OfficerAccount, + AmlExchangeBackend.AmlState, + string | undefined, + ]) { return await api.getDecisionsByState(officer, state, { - order: "asc", offset, limit: PAGINATED_LIST_REQUEST - }) + order: "asc", + offset, + limit: PAGINATED_LIST_REQUEST, + }); } - const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>( + const { data, error } = useSWR< + TalerExchangeResultByMethod<"getDecisionsByState">, + TalerHttpError + >( !session ? undefined : [session, state, offset, "getDecisionsByState"], fetcher, ); @@ -56,7 +72,9 @@ export function useCases(state: AmlExchangeBackend.AmlState) { if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult(data.body.records, offset, setOffset, (d) => String(d.rowid)); + return buildPaginatedResult(data.body.records, offset, setOffset, (d) => + String(d.rowid), + ); } type PaginatedResult<T> = OperationOk<T> & { @@ -64,11 +82,15 @@ type PaginatedResult<T> = OperationOk<T> & { isFirstPage: boolean; loadNext(): void; loadFirst(): void; -} +}; //TODO: consider sending this to web-util -export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> { - +export function buildPaginatedResult<R, OffId>( + data: R[], + offset: OffId | undefined, + setOffset: (o: OffId | undefined) => void, + getId: (r: R) => OffId, +): PaginatedResult<R[]> { const isLastPage = data.length < PAGINATED_LIST_REQUEST; const isFirstPage = offset === undefined; @@ -83,7 +105,7 @@ export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefi isFirstPage, loadNext: () => { if (!result.length) return; - const id = getId(result[result.length - 1]) + const id = getId(result[result.length - 1]); setOffset(id); }, loadFirst: () => { diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts @@ -1,150 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - AbsoluteTime, - Codec, - LockedAccount, - OfficerAccount, - OfficerId, - SigningKey, - buildCodecForObject, - codecForAbsoluteTime, - codecForString, - createNewOfficerAccount, - decodeCrock, - encodeCrock, - unlockOfficerAccount -} from "@gnu-taler/taler-util"; -import { - buildStorageKey, - useLocalStorage -} from "@gnu-taler/web-util/browser"; -import { useMemo } from "preact/hooks"; -import { useMaybeExchangeApiContext } from "../context/config.js"; - -export interface Officer { - account: LockedAccount; - when: AbsoluteTime; -} - -const codecForLockedAccount = codecForString() as Codec<LockedAccount>; - -type OfficerAccountString = { - id: string, - strKey: string; -} - -export const codecForOfficerAccount = (): Codec<OfficerAccountString> => - buildCodecForObject<OfficerAccountString>() - .property("id", codecForString()) // FIXME - .property("strKey", codecForString()) // FIXME - .build("OfficerAccount"); - -export const codecForOfficer = (): Codec<Officer> => - buildCodecForObject<Officer>() - .property("account", codecForLockedAccount) // FIXME - .property("when", codecForAbsoluteTime) // FIXME - .build("Officer"); - -export type OfficerState = OfficerNotReady | OfficerReady; -export type OfficerNotReady = OfficerNotFound | OfficerLocked; -interface OfficerNotFound { - state: "not-found"; - create: (password: string) => Promise<void>; -} -interface OfficerLocked { - state: "locked"; - forget: () => void; - tryUnlock: (password: string) => Promise<void>; -} -interface OfficerReady { - state: "ready"; - account: OfficerAccount; - forget: () => void; - lock: () => void; -} - -const OFFICER_KEY = buildStorageKey("officer", codecForOfficer()); -const DEV_ACCOUNT_KEY = buildStorageKey("account-dev", codecForOfficerAccount()); - -export function useOfficer(): OfficerState { - const exchangeContext = useMaybeExchangeApiContext(); - // dev account, is save when reloaded. - const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY); - const account = useMemo(() => { - if (!accountStorage.value) return undefined - - return { - id: accountStorage.value.id as OfficerId, - signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey - } - }, [accountStorage.value?.id, accountStorage.value?.strKey]) - - const officerStorage = useLocalStorage(OFFICER_KEY); - const officer = useMemo(() => { - if (!officerStorage.value) return undefined - return officerStorage.value - }, [officerStorage.value?.account, officerStorage.value?.when.t_ms]) - - if (officer === undefined) { - return { - state: "not-found", - create: async (pwd: string) => { - if (!exchangeContext) return; - const req = await fetch(new URL("seed", exchangeContext.api.baseUrl).href) - const b = await req.blob() - const ar = await b.arrayBuffer() - const uintar = new Uint8Array(ar) - - const { id, safe, signingKey } = await createNewOfficerAccount(pwd, uintar); - officerStorage.update({ - account: safe, - when: AbsoluteTime.now(), - }); - - // accountStorage.update({ id, signingKey }); - const strKey = encodeCrock(signingKey) - accountStorage.update({ id, strKey }) - }, - }; - } - - if (account === undefined) { - return { - state: "locked", - forget: () => { - officerStorage.reset(); - }, - tryUnlock: async (pwd: string) => { - const ac = await unlockOfficerAccount(officer.account, pwd); - // accountStorage.update(ac); - accountStorage.update({ id: ac.id, strKey: encodeCrock(ac.signingKey) }) - }, - }; - } - - return { - state: "ready", - account, - lock: () => { - accountStorage.reset(); - }, - forget: () => { - officerStorage.reset(); - accountStorage.reset(); - }, - }; -} diff --git a/packages/aml-backoffice-ui/src/hooks/useSettings.ts b/packages/aml-backoffice-ui/src/hooks/useSettings.ts @@ -1,71 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { - Codec, - TranslatedString, - buildCodecForObject, - codecForBoolean -} from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser"; - -interface Settings { - allowInsecurePassword: boolean; - keepSessionAfterReload: boolean; -} - -export function getAllBooleanSettings(): Array<keyof Settings> { - return ["allowInsecurePassword", "keepSessionAfterReload"] -} - -export function getLabelForSetting(k: keyof Settings, i18n: ReturnType<typeof useTranslationContext>["i18n"]): TranslatedString { - switch (k) { - case "allowInsecurePassword": return i18n.str`Allow Insecure password` - case "keepSessionAfterReload": return i18n.str`Keep session after reload` - } -} - -export const codecForSettings = (): Codec<Settings> => - buildCodecForObject<Settings>() - .property("allowInsecurePassword", (codecForBoolean())) - .property("keepSessionAfterReload", (codecForBoolean())) - .build("Settings"); - -const defaultSettings: Settings = { - allowInsecurePassword: false, - keepSessionAfterReload: false, -}; - -const EXCHANGE_SETTINGS_KEY = buildStorageKey( - "exchange-settings", - codecForSettings(), -); - -export function useSettings(): [ - Readonly<Settings>, - <T extends keyof Settings>(key: T, value: Settings[T]) => void, -] { - const { value, update } = useLocalStorage( - EXCHANGE_SETTINGS_KEY, - defaultSettings, - ); - - function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { - const newValue = { ...value, [k]: v }; - update(newValue); - } - return [value, updateField]; -} diff --git a/packages/aml-backoffice-ui/src/i18n/bank.pot b/packages/aml-backoffice-ui/src/i18n/bank.pot @@ -1,5 +1,5 @@ # This file is part of GNU Taler -# (C) 2022 Taler Systems S.A. +# (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/i18n/fr.po b/packages/aml-backoffice-ui/src/i18n/fr.po @@ -1,5 +1,5 @@ # This file is part of GNU Taler -# (C) 2022 Taler Systems S.A. +# (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/i18n/poheader b/packages/aml-backoffice-ui/src/i18n/poheader @@ -1,5 +1,5 @@ # This file is part of GNU Taler -# (C) 2022 Taler Systems S.A. +# (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/i18n/strings-prelude b/packages/aml-backoffice-ui/src/i18n/strings-prelude @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/i18n/strings.ts b/packages/aml-backoffice-ui/src/i18n/strings.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/index.html b/packages/aml-backoffice-ui/src/index.html @@ -1,6 +1,6 @@ <!-- This file is part of GNU Taler - (C) 2021--2022 Taler Systems S.A. + (C) 2021--2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/index.tsx b/packages/aml-backoffice-ui/src/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts @@ -1,58 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { CaseDetails } from "./pages/CaseDetails.js"; -import { Cases, HomeIcon, PeopleIcon } from "./pages/Cases.js"; -import { NewFormEntry } from "./pages/NewFormEntry.js"; -import { Officer } from "./pages/Officer.js"; -import { PageEntry, pageDefinition } from "./route.js"; -// import homeLogo from "./assets/home.svg"; -// import peopleLogo from "./assets/people.svg"; -const cases: PageEntry = { - url: "#/cases", - view: Cases, - name: "Cases" as TranslatedString, - Icon: HomeIcon, -}; - -const officer: PageEntry = { - url: "#/officer", - view: Officer, - name: "Officer" as TranslatedString, - Icon: PeopleIcon, -}; - -const account: PageEntry<{ account: string }> = { - url: pageDefinition("#/account/:account"), - view: CaseDetails, - name: "Account" as TranslatedString, - // icon: () => undefined, -}; - -const newFormEntry: PageEntry<{ account?: string; type?: string }> = { - url: pageDefinition("#/account/:account/new/:type?"), - view: NewFormEntry, - name: "New Form" as TranslatedString, - // icon: () => undefined, -}; - - -export const Pages = { - cases, - officer, - account, - newFormEntry, -}; diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -24,17 +24,17 @@ import { buildCodecForObject, codecForNumber, codecForString, - codecOptional + codecOptional, } from "@gnu-taler/taler-util"; import { DefaultForm, + useExchangeApiContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h } from "preact"; -import { useExchangeApiContext } from "../context/config.js"; import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js"; -import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../utils/types.js"; +import { privatePages } from "../Routing.js"; export function AntiMoneyLaunderingForm({ account, @@ -68,7 +68,10 @@ export function AntiMoneyLaunderingForm({ form={theForm.impl(initial)} onUpdate={() => {}} onSubmit={(formValue) => { - if (formValue.state === undefined || formValue.threshold === undefined) { + if ( + formValue.state === undefined || + formValue.threshold === undefined + ) { return; } const validatedForm = formValue as BaseForm; @@ -87,7 +90,7 @@ export function AntiMoneyLaunderingForm({ > <div class="mt-6 flex items-center justify-end gap-x-6"> <a - href={Pages.account.url({ account })} + href={privatePages.caseDetails.url({ cid: account })} class="text-sm font-semibold leading-6 text-gray-900" > <i18n.Translate>Cancel</i18n.Translate> @@ -133,7 +136,10 @@ export function parseJustification( s: string, listOfAllKnownForms: FormMetadata<BaseForm>[], ): - | OperationOk<{ justification: Justification; metadata: FormMetadata<BaseForm> }> + | OperationOk<{ + justification: Justification; + metadata: FormMetadata<BaseForm>; + }> | OperationFail<ParseJustificationFail> { try { const justification = JSON.parse(s); diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -34,13 +34,13 @@ import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; -import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../utils/types.js"; import { Justification, parseJustification, } from "./AntiMoneyLaunderingForm.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; +import { privatePages } from "../Routing.js"; export type AmlEvent = | AmlFormEvent @@ -201,7 +201,7 @@ export function CaseDetails({ account }: { account: string }) { return ( <div> <a - href={Pages.newFormEntry.url({ account })} + href={privatePages.caseNew.url({ cid: account })} class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > <i18n.Translate>New AML form</i18n.Translate> diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -0,0 +1,132 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + AbsoluteTime, + Amounts, + HttpStatusCode, + TalerExchangeApi, + TalerProtocolTimestamp, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { + LocalNotificationBanner, + useExchangeApiContext, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { privatePages } from "../Routing.js"; +import { uiForms } from "../forms/declaration.js"; +import { useOfficer } from "../hooks/officer.js"; +import { AntiMoneyLaunderingForm } from "./AntiMoneyLaunderingForm.js"; +import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; + +export function CaseUpdate({ + account, + type, +}: { + account: string; + type: string; +}): VNode { + const { i18n } = useTranslationContext(); + const officer = useOfficer(); + const { + lib: { exchange: api }, + } = useExchangeApiContext(); + const [notification, notify, handleError] = useLocalNotification(); + + if (officer.state !== "ready") { + return <HandleAccountNotReady officer={officer} />; + } + + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + + <AntiMoneyLaunderingForm + account={account} + formId={type} + onSubmit={async (justification, new_state, new_threshold) => { + const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = { + justification: JSON.stringify(justification), + decision_time: TalerProtocolTimestamp.now(), + h_payto: account, + new_state, + new_threshold: Amounts.stringify(new_threshold), + kyc_requirements: undefined, + }; + await handleError(async () => { + const resp = await api.addDecisionDetails( + officer.account, + decision, + ); + if (resp.type === "ok") { + window.location.href = privatePages.cases.url({}); + return; + } + switch (resp.case) { + case HttpStatusCode.Forbidden: + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${officer.account}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Officer or account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.Conflict: + return notify({ + type: "error", + title: i18n.str`Officer disabled or more recent decision was already submitted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + } + }); + }} + /> + </Fragment> + ); +} + +export function SelectForm({ account }: { account: string }) { + const { i18n } = useTranslationContext(); + return ( + <div> + <pre>New form for account: {account.substring(0, 16)}...</pre> + {uiForms.forms(i18n).map((form) => { + return ( + <a + key={form.id} + href={privatePages.caseUpdate.url({ cid: account, type: form.id })} + class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600" + > + {form.label} + </a> + ); + })} + </div> + ); +} diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -28,11 +28,11 @@ import { import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useCases } from "../hooks/useCases.js"; -import { Pages } from "../pages.js"; import { amlStateConverter } from "../utils/converter.js"; import { AmlExchangeBackend } from "../utils/types.js"; import { Officer } from "./Officer.js"; +import { privatePages } from "../Routing.js"; export function CasesUI({ records, @@ -130,7 +130,7 @@ export function CasesUI({ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> <div class="text-gray-900"> <a - href={Pages.account.url({ account: r.h_payto })} + href={privatePages.caseDetails.url({ cid: r.h_payto })} class="text-indigo-600 hover:text-indigo-900" > {r.h_payto.substring(0, 16)}... diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -20,7 +20,7 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { useSettings } from "../hooks/useSettings.js"; +import { usePreferences } from "../hooks/preferences.js"; export function CreateAccount({ onNewAccount, @@ -32,8 +32,8 @@ export function CreateAccount({ password: string; repeat: string; }>(); - const [settings] = useSettings(); - + const [settings] = usePreferences(); + return ( <div class="flex min-h-full flex-col "> <div class="sm:mx-auto sm:w-full sm:max-w-md"> diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -15,7 +15,7 @@ */ import { assertUnreachable } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; -import { OfficerNotReady } from "../hooks/useOfficer.js"; +import { OfficerNotReady } from "../hooks/officer.js"; import { CreateAccount } from "./CreateAccount.js"; import { UnlockAccount } from "./UnlockAccount.js"; diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -1,136 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - AbsoluteTime, - Amounts, - HttpStatusCode, - TalerExchangeApi, - TalerProtocolTimestamp, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { - LocalNotificationBanner, - useLocalNotification, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useExchangeApiContext } from "../context/config.js"; -import { uiForms } from "../forms/declaration.js"; -import { useOfficer } from "../hooks/useOfficer.js"; -import { Pages } from "../pages.js"; -import { AntiMoneyLaunderingForm } from "./AntiMoneyLaunderingForm.js"; -import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; - -export function NewFormEntry({ - account, - type, -}: { - account?: string; - type?: string; -}): VNode { - const { i18n } = useTranslationContext(); - const officer = useOfficer(); - const { api } = useExchangeApiContext(); - const [notification, notify, handleError] = useLocalNotification(); - - if (!account) { - return <div>no account</div>; - } - if (!type) { - return <SelectForm account={account} />; - } - if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; - } - - return ( - <Fragment> - <LocalNotificationBanner notification={notification} /> - - <AntiMoneyLaunderingForm - account={account} - formId={type} - onSubmit={async (justification, new_state, new_threshold) => { - const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = { - justification: JSON.stringify(justification), - decision_time: TalerProtocolTimestamp.now(), - h_payto: account, - new_state, - new_threshold: Amounts.stringify(new_threshold), - kyc_requirements: undefined, - }; - await handleError(async () => { - const resp = await api.addDecisionDetails( - officer.account, - decision, - ); - if (resp.type === "ok") { - window.location.href = Pages.cases.url; - return; - } - switch (resp.case) { - case HttpStatusCode.Forbidden: - case HttpStatusCode.Unauthorized: - return notify({ - type: "error", - title: i18n.str`Wrong credentials for "${officer.account}"`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case HttpStatusCode.NotFound: - return notify({ - type: "error", - title: i18n.str`Officer or account not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case HttpStatusCode.Conflict: - return notify({ - type: "error", - title: i18n.str`Officer disabled or more recent decision was already submitted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - } - }); - }} - /> - </Fragment> - ); -} - -function SelectForm({ account }: { account: string }) { - const { i18n } = useTranslationContext(); - return ( - <div> - <pre>New form for account: {account.substring(0, 16)}...</pre> - {uiForms.forms(i18n).map((form) => { - return ( - <a - key={form.id} - href={Pages.newFormEntry.url({ account, type: form.id })} - class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600" - > - {form.label} - </a> - ); - })} - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -13,22 +13,27 @@ 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 { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useExchangeApiContext, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { h } from "preact"; -import { getInitialBackendBaseURL } from "../hooks/useBackend.js"; -import { useOfficer } from "../hooks/useOfficer.js"; -import { uiSettings } from "../settings.js"; +import { useOfficer } from "../hooks/officer.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { useSettingsContext } from "../context/settings.js"; export function Officer() { const officer = useOfficer(); - const { i18n } = useTranslationContext() + const settings = useSettingsContext(); + const { lib } = useExchangeApiContext(); + + const { i18n } = useTranslationContext(); if (officer.state !== "ready") { return <HandleAccountNotReady officer={officer} />; } - const url = new URL(getInitialBackendBaseURL()) - const signupEmail = uiSettings.signupEmail ?? `aml-signup@${url.hostname}` + const url = new URL("./", lib.exchange.baseUrl); + const signupEmail = settings.signupEmail ?? `aml-signup@${url.hostname}`; return ( <div> @@ -40,7 +45,11 @@ export function Officer() { </div> <p> <a - href={`mailto:${signupEmail}?subject=${encodeURIComponent("Request AML signup")}&body=${encodeURIComponent(`I want my AML account\n\n\nPubKey: ${officer.account.id}`)}`} + href={`mailto:${signupEmail}?subject=${encodeURIComponent( + "Request AML signup", + )}&body=${encodeURIComponent( + `I want my AML account\n\n\nPubKey: ${officer.account.id}`, + )}`} target="_blank" rel="noreferrer" class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts @@ -1,239 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { createHashHistory } from "history"; -import { ComponentChildren, h as create, createContext, VNode } from "preact"; -import { useContext, useEffect, useState } from "preact/hooks"; - -type ContextType = { - onChange: (listener: () => void) => VoidFunction; -}; -const nullChangeListener = { onChange: () => () => {} }; -const Context = createContext<ContextType>(nullChangeListener); - -export const usePathChangeContext = (): ContextType => useContext(Context); - -export function HashPathProvider({ - children, -}: { - children: ComponentChildren; -}): VNode { - const history = createHashHistory(); - return create( - Context.Provider, - { value: { onChange: history.listen }, children }, - children, - ); -} - -type PageDefinition<DynamicPart extends Record<string, string>> = { - pattern: string; - (params: DynamicPart): string; -}; - -function replaceAll( - pattern: string, - vars: Record<string, string>, - values: Record<string, string>, -): string { - let result = pattern; - for (const v in vars) { - result = result.replace(vars[v], !values[v] ? "" : values[v]); - } - return result; -} - -export function pageDefinition<T extends Record<string, string>>( - pattern: string, -): PageDefinition<T> { - const patternParams = pattern.match(/(:[\w?]*)/g); - if (!patternParams) - throw Error( - `page definition pattern ${pattern} doesn't have any parameter`, - ); - - const vars = patternParams.reduce( - (prev, cur) => { - const pName = cur.match(/(\w+)/g); - - //skip things like :? in the path pattern - if (!pName || !pName[0]) return prev; - const name = pName[0]; - return { ...prev, [name]: cur }; - }, - {} as Record<string, string>, - ); - - const f = (values: T): string => replaceAll(pattern, vars, values); - f.pattern = pattern; - return f; -} - -export type PageEntry<T = unknown> = T extends Record<string, string> - ? { - url: PageDefinition<T>; - view: (props: T) => VNode; - name: TranslatedString; - Icon?: () => VNode; - } - : T extends unknown - ? { - url: string; - view: (props: {}) => VNode; - name: TranslatedString; - Icon?: () => VNode; - } - : never; - -export function Router({ - pageList, - onNotFound, -}: { - pageList: Array<PageEntry<any>>; - onNotFound: () => VNode; -}): VNode { - const current = useCurrentLocation(pageList); - if (current !== undefined) { - return create(current.page.view, current.values); - } - return onNotFound(); -} - -type Location = { - page: PageEntry<any>; - path: string; - values: Record<string, string>; -}; -export function useCurrentLocation( - pageList: Array<PageEntry<any>>, -): Location | undefined { - const [currentLocation, setCurrentLocation] = useState< - Location | null | undefined - >(null); - const path = usePathChangeContext(); - useEffect(() => { - return path.onChange(() => { - const result = doSync( - window.location.hash, - new URLSearchParams(window.location.search), - pageList, - ); - setCurrentLocation(result); - }); - }, []); - if (currentLocation === null) { - return doSync( - window.location.hash, - new URLSearchParams(window.location.search), - pageList, - ); - } - return currentLocation; -} - -export function useChangeLocation() { - const [location, setLocation] = useState(window.location.hash); - const path = usePathChangeContext(); - useEffect(() => { - return path.onChange(() => { - setLocation(window.location.hash); - }); - }, []); - return location; -} - -/** - * Search path in the pageList - * get the values from the path found - * add params from searchParams - * - * @param path - * @param params - */ -export function doSync( - path: string, - params: URLSearchParams, - pageList: Array<PageEntry<any>>, -): Location | undefined { - for (let idx = 0; idx < pageList.length; idx++) { - const page = pageList[idx]; - if (typeof page.url === "string") { - if (page.url === path) { - const values: Record<string, string> = {}; - params.forEach((v, k) => { - values[k] = v; - }); - return { page, values, path }; - } - } else { - const values = doestUrlMatchToRoute(path, page.url.pattern); - if (values !== undefined) { - params.forEach((v, k) => { - values[k] = v; - }); - return { page, values, path }; - } - } - } - return undefined; -} - -function doestUrlMatchToRoute( - url: string, - route: string, -): undefined | Record<string, string> { - const paramsPattern = /(?:\?([^#]*))?$/; - // const paramsPattern = /(?:\?([^#]*))?(#.*)?$/; - const params = url.match(paramsPattern); - const urlWithoutParams = url.replace(paramsPattern, ""); - - const result: Record<string, string> = {}; - if (params && params[1]) { - const paramList = params[1].split("&"); - for (let i = 0; i < paramList.length; i++) { - const idx = paramList[i].indexOf("="); - const name = paramList[i].substring(0, idx); - const value = paramList[i].substring(idx + 1); - result[decodeURIComponent(name)] = decodeURIComponent(value); - } - } - const urlSeg = urlWithoutParams.split("/"); - const routeSeg = route.split("/"); - let max = Math.max(urlSeg.length, routeSeg.length); - for (let i = 0; i < max; i++) { - if (routeSeg[i] && routeSeg[i].charAt(0) === ":") { - const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, ""); - - const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || ""; - const plus = ~flags.indexOf("+"); - const star = ~flags.indexOf("*"); - const val = urlSeg[i] || ""; - - if (!val && !star && (flags.indexOf("?") < 0 || plus)) { - return undefined; - } - result[param] = decodeURIComponent(val); - if (plus || star) { - result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/"); - break; - } - } else if (routeSeg[i] !== urlSeg[i]) { - return undefined; - } - } - return result; -} -const EMPTY: Record<string, string> = {}; diff --git a/packages/aml-backoffice-ui/src/settings.ts b/packages/aml-backoffice-ui/src/settings.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -14,17 +14,77 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ + import { + Codec, + buildCodecForObject, + canonicalizeBaseUrl, + codecForString, + codecOptional +} from "@gnu-taler/taler-util"; + export interface UiSettings { + // Where libeufin backend is localted + // default: window.origin without "webui/" backendBaseURL?: string; + // Shows a button "create random account" in the registration form + // Useful for testing + // default: false signupEmail?: string; } /** - * Global settings for the UI. + * Global settings for the bank UI. */ -const defaultSettings: UiSettings = {}; +const defaultSettings: UiSettings = { + backendBaseURL: buildDefaultBackendBaseURL(), + signupEmail: undefined, +}; + +const codecForBankUISettings = (): Codec<UiSettings> => + buildCodecForObject<UiSettings>() + .property("backendBaseURL", codecOptional(codecForString())) + .property("signupEmail", codecOptional(codecForString())) + .build("UiSettings"); + +function removeUndefineField<T extends object>(obj: T): T { + const keys = Object.keys(obj) as Array<keyof T>; + return keys.reduce((prev, cur) => { + if (typeof prev[cur] === "undefined") { + delete prev[cur]; + } + return prev; + }, obj); +} + +export function fetchSettings(listener: (s: UiSettings) => void): void { + fetch("./settings.json") + .then((resp) => resp.json()) + .then((json) => codecForBankUISettings().decode(json)) + .then((result) => + listener({ + ...defaultSettings, + ...removeUndefineField(result), + }), + ) + .catch((e) => { + console.log("failed to fetch settings", e); + listener(defaultSettings); + }); +} + +function buildDefaultBackendBaseURL(): string | undefined { + if (typeof window !== "undefined") { + const currentLocation = new URL( + window.location.pathname, + window.location.origin, + ).href; + /** + * By default, bank backend serves the html content + * from the /webui root. + */ + return canonicalizeBaseUrl(currentLocation.replace("/webui", "")); + } + throw Error("No default URL"); +} + -export const uiSettings: UiSettings = - "talerExchangeAmlSettings" in globalThis - ? (globalThis as any).talerExchangeAmlSettings - : defaultSettings; diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -19,15 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ import { TalerExchangeApi, setupI18n } from "@gnu-taler/taler-util"; -import { parseGroupImport } from "@gnu-taler/web-util/browser"; +import { + ExchangeApiProviderTesting, + ExchangeContextType, + parseGroupImport, +} from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; // import * as components from "./components/index.examples.js"; import * as pages from "./pages/index.stories.js"; import { ComponentChildren, VNode, h as create } from "preact"; -import { ExchangeApiContextTesting } from "./context/config.js"; -// import { BackendStateProviderTesting } from "./context/backend.js"; setupI18n("en", { en: {} }); @@ -66,5 +68,16 @@ function DefaultTestingContext({ supported_kyc_requirements: [], version: "asd", }; - return create(ExchangeApiContextTesting, { config, children }); + const value: ExchangeContextType = { + cancelRequest: () => null, + config, + url: new URL("/", "http://locahost"), + hints: [], + lib: { + exchange: undefined!, //FIXME: mock + }, + onActivity: () => null!, + }; + + return create(ExchangeApiProviderTesting, { value, children }); } diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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 @@ -22,10 +22,14 @@ import { strings } from "./i18n/strings.js"; import * as pages from "./pages/index.stories.js"; -import { renderStories } from "@gnu-taler/web-util/browser"; +import { + ExchangeApiProviderTesting, + ExchangeContextType, + renderStories, +} from "@gnu-taler/web-util/browser"; +import { TalerExchangeApi } from "@gnu-taler/taler-util"; import { ComponentChildren, FunctionComponent, VNode, h } from "preact"; -import { ExchangeApiContextTesting } from "./context/config.js"; import "./scss/main.css"; function main(): void { @@ -40,24 +44,33 @@ function main(): void { function getWrapperForGroup(): FunctionComponent { return function All({ children }: { children?: ComponentChildren }): VNode { + const config: TalerExchangeApi.ExchangeVersionResponse = { + currency: "ARS", + currency_specification: { + alt_unit_names: {}, + name: "ARS", + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + }, + name: "taler-exchange", + supported_kyc_requirements: [], + version: "asd", + }; + const value: ExchangeContextType = { + cancelRequest: () => null, + config, + url: new URL("/", "http://locahost"), + hints: [], + lib: { + exchange: undefined!, //FIXME: mock + }, + onActivity: () => null!, + }; return ( - <ExchangeApiContextTesting - config={{ - currency: "ARS", - currency_specification: { - alt_unit_names: {}, - name: "ARS", - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - }, - name: "taler-exchange", - supported_kyc_requirements: [], - version: "asd", - }} - > + <ExchangeApiProviderTesting value={value}> {children} - </ExchangeApiContextTesting> + </ExchangeApiProviderTesting> ); }; } diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 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