aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/aml-backoffice-ui/src')
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx150
-rw-r--r--packages/aml-backoffice-ui/src/Dashboard.tsx229
-rw-r--r--packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx273
-rw-r--r--packages/aml-backoffice-ui/src/NiceForm.tsx61
-rw-r--r--packages/aml-backoffice-ui/src/Routing.tsx151
-rw-r--r--packages/aml-backoffice-ui/src/context/config.ts97
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-forms.ts507
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-settings.ts110
-rw-r--r--packages/aml-backoffice-ui/src/declaration.d.ts16
-rw-r--r--packages/aml-backoffice-ui/src/forms.ts (renamed from packages/aml-backoffice-ui/src/settings.ts)19
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_11e.ts75
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_12e.ts248
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_13e.ts318
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_15e.ts107
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_1e.ts493
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_4e.ts366
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_5e.ts159
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_9e.ts78
-rw-r--r--packages/aml-backoffice-ui/src/forms/icons.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts147
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts109
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Calendar.tsx116
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Caption.tsx32
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Dialog.tsx15
-rw-r--r--packages/aml-backoffice-ui/src/handlers/FormProvider.tsx140
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Group.tsx41
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputAmount.tsx36
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputArray.tsx186
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx88
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx113
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputDate.tsx66
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputFile.tsx106
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputInteger.tsx24
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputLine.tsx268
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx154
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx135
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputText.tsx9
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx9
-rw-r--r--packages/aml-backoffice-ui/src/handlers/forms.ts135
-rw-r--r--packages/aml-backoffice-ui/src/handlers/useField.ts95
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts138
-rw-r--r--packages/aml-backoffice-ui/src/hooks/officer.ts163
-rw-r--r--packages/aml-backoffice-ui/src/hooks/preferences.ts (renamed from packages/aml-backoffice-ui/src/hooks/useSettings.ts)75
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useBackend.ts92
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts82
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts193
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useOfficer.ts131
-rw-r--r--packages/aml-backoffice-ui/src/i18n/bank.pot2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/fr.po2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/poheader2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings-prelude2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings.ts2
-rw-r--r--packages/aml-backoffice-ui/src/index.html46
-rw-r--r--packages/aml-backoffice-ui/src/index.tsx9
-rw-r--r--packages/aml-backoffice-ui/src/pages.ts44
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx104
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx224
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx463
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx205
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx458
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx222
-rw-r--r--packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx40
-rw-r--r--packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx98
-rw-r--r--packages/aml-backoffice-ui/src/pages/Officer.tsx42
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx175
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx153
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx162
-rw-r--r--packages/aml-backoffice-ui/src/pages/index.stories.ts16
-rw-r--r--packages/aml-backoffice-ui/src/route.ts197
-rw-r--r--packages/aml-backoffice-ui/src/stories.test.ts30
-rw-r--r--packages/aml-backoffice-ui/src/stories.tsx61
-rw-r--r--packages/aml-backoffice-ui/src/types.ts124
-rw-r--r--packages/aml-backoffice-ui/src/utils/QR.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts47
75 files changed, 4241 insertions, 5096 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index d461934c0..9ccf21755 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -1,32 +1,132 @@
-import { TranslationProvider } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
-import { ExchangeAmlFrame } from "./Dashboard.js";
-import "./scss/main.css";
-import { ExchangeApiProvider } from "./context/config.js";
-import { getInitialBackendBaseURL } from "./hooks/useBackend.js";
-import { HashPathProvider, Router } from "./route.js";
-import { Pages } from "./pages.js";
+/*
+ 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.
-const pageList = Object.values(Pages);
+ 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 {
+ 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 { ExchangeAmlFrame } from "./ExchangeAmlFrame.js";
+import { Routing } from "./Routing.js";
+import { UiSettingsProvider } from "./context/ui-settings.js";
+import { strings } from "./i18n/strings.js";
+import "./scss/main.css";
+import { UiSettings, fetchUiSettings } from "./context/ui-settings.js";
+const WITH_LOCAL_STORAGE_CACHE = false;
export function App(): VNode {
- const baseUrl = getInitialBackendBaseURL();
+ const [settings, setSettings] = useState<UiSettings>();
+ useEffect(() => {
+ fetchUiSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
return (
- <TranslationProvider source={{}}>
- <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}>
- <HashPathProvider>
- <ExchangeAmlFrame>
- <Router
- pageList={pageList}
- onNotFound={() => {
- window.location.href = Pages.cases.url
- return <div>not found</div>;
- }}
- />
- </ExchangeAmlFrame>
- </HashPathProvider>
- </ExchangeApiProvider>
- </TranslationProvider>
+ <UiSettingsProvider 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
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </SWRConfig>
+ </ExchangeApiProvider>
+ </TranslationProvider>
+ </UiSettingsProvider>
);
}
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ 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
deleted file mode 100644
index f791906b9..000000000
--- a/packages/aml-backoffice-ui/src/Dashboard.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Footer, GlobalNotificationsBanner, Header, notifyError, notifyException, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, 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";
-
-function classNames(...classes: string[]) {
- return classes.filter(Boolean).join(" ");
-}
-
-/**
- * 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;
-
-const versionText = VERSION
- ? GIT_HASH
- ? `v${VERSION} (${GIT_HASH.substring(0, 8)})`
- : VERSION
- : "";
-
-/**
- * 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, resetError] = 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)
- }
- 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 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>
-
- <GlobalNotificationsBanner />
-
- <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: 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>
- <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
new file mode 100644
index 000000000..772fd1b70
--- /dev/null
+++ 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 { useUiSettingsContext } from "./context/ui-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 = useUiSettingsContext()
+
+ 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/NiceForm.tsx b/packages/aml-backoffice-ui/src/NiceForm.tsx
deleted file mode 100644
index 53f29e580..000000000
--- a/packages/aml-backoffice-ui/src/NiceForm.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { ComponentChildren, Fragment, h } from "preact";
-import { FlexibleForm } from "./forms/index.js";
-import { FormProvider } from "./handlers/FormProvider.js";
-import { RenderAllFieldsByUiConfig } from "./handlers/forms.js";
-
-export function NiceForm<T extends object>({
- initial,
- onUpdate,
- form,
- onSubmit,
- children,
- readOnly,
-}: {
- children?: ComponentChildren;
- initial: Partial<T>;
- onSubmit?: (v: Partial<T>) => void;
- form: FlexibleForm<T>;
- readOnly?: boolean;
- onUpdate?: (d: Partial<T>) => void;
-}) {
- return (
- <FormProvider
- initialValue={initial}
- onUpdate={onUpdate}
- onSubmit={onSubmit}
- readOnly={readOnly}
- computeFormState={form.behavior}
- >
- <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
- {form.design.map((section, i) => {
- if (!section) return <Fragment />;
- return (
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- {section.title}
- </h2>
- {section.description && (
- <p class="mt-1 text-sm leading-6 text-gray-600">
- {section.description}
- </p>
- )}
- </div>
- <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
- <div class="p-3">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <RenderAllFieldsByUiConfig
- key={i}
- fields={section.fields}
- />
- </div>
- </div>
- </div>
- </div>
- );
- })}
- </div>
- {children}
- </FormProvider>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx
new file mode 100644
index 000000000..f38fc29c2
--- /dev/null
+++ 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"),
+ 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`,
+ ),
+ caseDetails: urlPattern<{ cid: string }>(
+ /\/case\/(?<cid>[a-zA-Z0-9]+)/,
+ ({ cid }) => `#/case/${cid}`,
+ ),
+};
+
+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
deleted file mode 100644
index 2df7ff40d..000000000
--- a/packages/aml-backoffice-ui/src/context/config.ts
+++ /dev/null
@@ -1,97 +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 { BrowserHttpLib, 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 as any);
-
-export const useExchangeApiContext = (): Type => 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 as any },
- 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 BrowserHttpLib())
- useEffect(() => {
- api.getConfig()
- .then((resp) => {
- 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/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts
new file mode 100644
index 000000000..2e0b8a76d
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts
@@ -0,0 +1,507 @@
+/*
+ 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 {
+ buildCodecForObject,
+ buildCodecForUnion,
+ Codec,
+ codecForBoolean,
+ codecForConstString,
+ codecForList,
+ codecForNumber,
+ codecForString,
+ codecForTimestamp,
+ codecOptional,
+ Integer,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = UiForms;
+
+const defaultForms: UiForms = {
+ forms: []
+};
+const Context = createContext<Type>(defaultForms);
+
+export type BaseForm = Record<string, unknown>;
+
+export const useUiFormsContext = (): Type => useContext(Context);
+
+export const UiFormsProvider = ({
+ children,
+ value,
+}: {
+ value: UiForms;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+export type FormMetadata = {
+ label: string;
+ id: string;
+ version: number;
+ config: FlexibleForm;
+};
+
+type FlexibleForm = DoubleColumnForm;
+
+export interface DoubleColumnForm {
+ type: "double-column";
+ design: Array<DoubleColumnFormSection>;
+ // behavior?: (form: Partial<T>) => FormState<T>;
+}
+
+export type DoubleColumnFormSection = {
+ title: string;
+ description?: string;
+ fields: UIFormFieldConfig[];
+};
+
+// export interface BaseForm {
+// state: TalerExchangeApi.AmlState;
+// threshold: AmountJson;
+// }
+
+export interface UiForms {
+ // Where libeufin backend is localted
+ // default: window.origin without "webui/"
+ forms: Array<FormMetadata>;
+}
+
+export type UIFormFieldConfig =
+ | UIFormFieldConfigAbsoluteTime
+ | UIFormFieldConfigAmount
+ | UIFormFieldConfigArray
+ | UIFormFieldConfigCaption
+ | UIFormFieldConfigChoiseHorizontal
+ | UIFormFieldConfigChoiseStacked
+ | UIFormFieldConfigFile
+ | UIFormFieldConfigGroup
+ | UIFormFieldConfigInteger
+ | UIFormFieldConfigSelectMultiple
+ | UIFormFieldConfigSelectOne
+ | UIFormFieldConfigText
+ | UIFormFieldConfigTextArea
+ | UIFormFieldConfigToggle;
+
+type UIFormFieldConfigAbsoluteTime = {
+ type: "absoluteTime";
+ properties: UIFormFieldBaseConfig & {
+ max?: TalerProtocolTimestamp;
+ min?: TalerProtocolTimestamp;
+ pattern: string;
+ };
+};
+
+type UIFormFieldConfigAmount = {
+ type: "amount";
+ properties: UIFormFieldBaseConfig & {
+ max?: Integer;
+ min?: Integer;
+ currency: string;
+ };
+};
+
+type UIFormFieldConfigArray = {
+ type: "array";
+ properties: UIFormFieldBaseConfig & {
+ // id of the field shown when the array is collapsed
+ labelFieldId: UIHandlerId;
+ fields: UIFormFieldConfig[];
+ };
+};
+
+type UIFormFieldConfigCaption = {
+ type: "caption";
+ properties: UIFieldBaseDescription;
+};
+
+type UIFormFieldConfigGroup = {
+ type: "group";
+ properties: UIFormFieldBaseConfig & {
+ fields: UIFormFieldConfig[];
+ };
+};
+
+type UIFormFieldConfigChoiseHorizontal = {
+ type: "choiceHorizontal";
+ properties: UIFormFieldBaseConfig & {
+ choices: Array<SelectUiChoice>;
+ };
+};
+
+type UIFormFieldConfigChoiseStacked = {
+ type: "choiceStacked";
+ properties: UIFormFieldBaseConfig & {
+ choices: Array<SelectUiChoice>;
+ };
+};
+
+type UIFormFieldConfigFile = {
+ type: "file";
+ properties: UIFormFieldBaseConfig & {
+ maxBytes?: Integer;
+ minBytes?: Integer;
+ // comma-separated list of one or more file types
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers
+ accept?: string;
+ };
+};
+type UIFormFieldConfigInteger = {
+ type: "integer";
+ properties: UIFormFieldBaseConfig & {
+ max?: Integer;
+ min?: Integer;
+ };
+};
+
+interface SelectUiChoice {
+ label: string;
+ description?: string;
+ value: string;
+}
+
+type UIFormFieldConfigSelectMultiple = {
+ type: "selectMultiple";
+ properties: UIFormFieldBaseConfig & {
+ max?: Integer;
+ min?: Integer;
+ unique?: boolean;
+ choices: Array<SelectUiChoice>;
+ };
+};
+type UIFormFieldConfigSelectOne = {
+ type: "selectOne";
+ properties: UIFormFieldBaseConfig & {
+ choices: Array<SelectUiChoice>;
+ };
+};
+type UIFormFieldConfigText = {
+ type: "text";
+ properties: UIFormFieldBaseConfig;
+};
+type UIFormFieldConfigTextArea = {
+ type: "textArea";
+ properties: UIFormFieldBaseConfig;
+};
+type UIFormFieldConfigToggle = {
+ type: "toggle";
+ properties: UIFormFieldBaseConfig;
+};
+
+type UIFieldBaseDescription = {
+ /* label if the field, visible for the user */
+ label: string;
+ /* long text to be shown on user demand */
+ tooltip?: string;
+
+ /* short text to be shown close to the field */
+ help?: string;
+
+ /* if the field should be initialy hidden */
+ hidden?: boolean;
+ /* ui element to show before */
+ addonBeforeId?: string;
+ /* ui element to show after */
+ addonAfterId?: string;
+};
+
+type UIFormFieldBaseConfig = UIFieldBaseDescription & {
+ /* example to be shown inside the field */
+ placeholder?: string;
+
+ /* show a mark as required */
+ required?: boolean;
+
+ /* readonly and dim */
+ disabled?: boolean;
+
+ /* name of the field, useful for a11y */
+ name: string;
+
+ /* conversion id to conver the string into the value type
+ the id should be known to the ui impl
+ */
+ converterId?: string;
+
+ /* property id of the form */
+ id: UIHandlerId;
+};
+
+declare const __handlerId: unique symbol;
+export type UIHandlerId = string & { [__handlerId]: true };
+
+// FIXME: validate well formed ui field id
+const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>;
+
+const codecForUIFormFieldBaseConfigTemplate = <
+ T extends UIFormFieldBaseConfig,
+>() =>
+ buildCodecForObject<T>()
+ .property("id", codecForUiFieldId())
+ .property("addonAfterId", codecOptional(codecForString()))
+ .property("addonBeforeId", codecOptional(codecForString()))
+ .property("converterId", codecOptional(codecForString()))
+ .property("disabled", codecOptional(codecForBoolean()))
+ .property("hidden", codecOptional(codecForBoolean()))
+ .property("required", codecOptional(codecForBoolean()))
+ .property("help", codecOptional(codecForString()))
+ .property("label", codecForString())
+ .property("name", codecForString())
+ .property("placeholder", codecOptional(codecForString()))
+ .property("tooltip", codecOptional(codecForString()));
+
+const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> =>
+ codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties");
+
+const codecForUIFormFieldAbsoluteTimeConfig = (): Codec<
+ UIFormFieldConfigAbsoluteTime["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<
+ UIFormFieldConfigAbsoluteTime["properties"]
+ >()
+ .property("pattern", codecForString())
+ .property("max", codecOptional(codecForTimestamp))
+ .property("min", codecOptional(codecForTimestamp))
+ .build("UIFormFieldConfigAbsoluteTime.properties");
+
+const codecForUiFormFieldAbsoluteTime =
+ (): Codec<UIFormFieldConfigAbsoluteTime> =>
+ buildCodecForObject<UIFormFieldConfigAbsoluteTime>()
+ .property("type", codecForConstString("absoluteTime"))
+ .property("properties", codecForUIFormFieldAbsoluteTimeConfig())
+ .build("UIFormFieldConfigAbsoluteTime");
+
+const codecForUIFormFieldAmountConfig = (): Codec<
+ UIFormFieldConfigAmount["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigAmount["properties"]>()
+ .property("currency", codecForString())
+ .property("max", codecOptional(codecForNumber()))
+ .property("min", codecOptional(codecForNumber()))
+ .build("UIFormFieldConfigAmount.properties");
+
+const codecForUiFormFieldAmount = (): Codec<UIFormFieldConfigAmount> =>
+ buildCodecForObject<UIFormFieldConfigAmount>()
+ .property("type", codecForConstString("amount"))
+ .property("properties", codecForUIFormFieldAmountConfig())
+ .build("UIFormFieldConfigAmount");
+
+const codecForUIFormFieldArrayConfig = (): Codec<
+ UIFormFieldConfigArray["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigArray["properties"]>()
+ .property("labelFieldId", codecForUiFieldId())
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("UIFormFieldConfigArray.properties");
+
+const codecForUiFormFieldArray = (): Codec<UIFormFieldConfigArray> =>
+ buildCodecForObject<UIFormFieldConfigArray>()
+ .property("type", codecForConstString("array"))
+ .property("properties", codecForUIFormFieldArrayConfig())
+ .build("UIFormFieldConfigArray");
+
+const codecForUiFormFieldCaption = (): Codec<UIFormFieldConfigCaption> =>
+ buildCodecForObject<UIFormFieldConfigCaption>()
+ .property("type", codecForConstString("caption"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigCaption");
+
+const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> =>
+ buildCodecForObject<SelectUiChoice>()
+ .property("description", codecForString())
+ .property("label", codecForString())
+ .property("value", codecForString())
+ .build("SelectUiChoice");
+
+const codecForUIFormFieldWithChoiseConfig = (): Codec<
+ UIFormFieldConfigChoiseHorizontal["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<
+ UIFormFieldConfigChoiseHorizontal["properties"]
+ >()
+ .property("choices", codecForList(codecForUiFormSelectUiChoice()))
+ .build("UIFormFieldConfigChoiseHorizontal.properties");
+
+const codecForUiFormFieldChoiceHorizontal =
+ (): Codec<UIFormFieldConfigChoiseHorizontal> =>
+ buildCodecForObject<UIFormFieldConfigChoiseHorizontal>()
+ .property("type", codecForConstString("choiceHorizontal"))
+ .property("properties", codecForUIFormFieldWithChoiseConfig())
+ .build("UIFormFieldConfigChoiseHorizontal");
+
+const codecForUiFormFieldChoiceStacked =
+ (): Codec<UIFormFieldConfigChoiseStacked> =>
+ buildCodecForObject<UIFormFieldConfigChoiseStacked>()
+ .property("type", codecForConstString("choiceStacked"))
+ .property("properties", codecForUIFormFieldWithChoiseConfig())
+ .build("UIFormFieldConfigChoiseStacked");
+
+const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> =>
+ buildCodecForObject<UIFormFieldConfigFile>()
+ .property("type", codecForConstString("file"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigFile");
+
+const codecForUIFormFieldWithFieldsConfig = (): Codec<
+ UIFormFieldConfigGroup["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigGroup["properties"]>()
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("UIFormFieldConfigGroup.properties");
+
+const codecForUiFormFieldGroup = (): Codec<UIFormFieldConfigGroup> =>
+ buildCodecForObject<UIFormFieldConfigGroup>()
+ .property("type", codecForConstString("group"))
+ .property("properties", codecForUIFormFieldWithFieldsConfig())
+ .build("UiFormFieldGroup");
+
+const codecForUiFormFieldInteger = (): Codec<UIFormFieldConfigInteger> =>
+ buildCodecForObject<UIFormFieldConfigInteger>()
+ .property("type", codecForConstString("integer"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigInteger");
+
+const codecForUIFormFieldSelectMultipleConfig = (): Codec<
+ UIFormFieldConfigSelectMultiple["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<
+ UIFormFieldConfigSelectMultiple["properties"]
+ >()
+ .property("max", codecOptional(codecForNumber()))
+ .property("min", codecOptional(codecForNumber()))
+ .property("unique", codecOptional(codecForBoolean()))
+ .property("choices", codecForList(codecForUiFormSelectUiChoice()))
+ .build("UIFormFieldConfigSelectMultiple.properties");
+
+const codecForUiFormFieldSelectMultiple =
+ (): Codec<UIFormFieldConfigSelectMultiple> =>
+ buildCodecForObject<UIFormFieldConfigSelectMultiple>()
+ .property("type", codecForConstString("selectMultiple"))
+ .property("properties", codecForUIFormFieldSelectMultipleConfig())
+ .build("UiFormFieldSelectMultiple");
+
+const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldConfigSelectOne> =>
+ buildCodecForObject<UIFormFieldConfigSelectOne>()
+ .property("type", codecForConstString("selectOne"))
+ .property("properties", codecForUIFormFieldWithChoiseConfig())
+ .build("UIFormFieldConfigSelectOne");
+
+const codecForUiFormFieldText = (): Codec<UIFormFieldConfigText> =>
+ buildCodecForObject<UIFormFieldConfigText>()
+ .property("type", codecForConstString("text"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigText");
+
+const codecForUiFormFieldTextArea = (): Codec<UIFormFieldConfigTextArea> =>
+ buildCodecForObject<UIFormFieldConfigTextArea>()
+ .property("type", codecForConstString("textArea"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigTextArea");
+
+const codecForUiFormFieldToggle = (): Codec<UIFormFieldConfigToggle> =>
+ buildCodecForObject<UIFormFieldConfigToggle>()
+ .property("type", codecForConstString("toggle"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigToggle");
+
+const codecForUiFormField = (): Codec<UIFormFieldConfig> =>
+ buildCodecForUnion<UIFormFieldConfig>()
+ .discriminateOn("type")
+ .alternative("absoluteTime", codecForUiFormFieldAbsoluteTime())
+ .alternative("amount", codecForUiFormFieldAmount())
+ .alternative("array", codecForUiFormFieldArray())
+ .alternative("caption", codecForUiFormFieldCaption())
+ .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal())
+ .alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
+ .alternative("file", codecForUiFormFieldFile())
+ .alternative("group", codecForUiFormFieldGroup())
+ .alternative("integer", codecForUiFormFieldInteger())
+ .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
+ .alternative("selectOne", codecForUiFormFieldSelectOne())
+ .alternative("text", codecForUiFormFieldText())
+ .alternative("textArea", codecForUiFormFieldTextArea())
+ .alternative("toggle", codecForUiFormFieldToggle())
+ .build("UIFormField");
+
+const codecForDoubleColumnFormSection = (): Codec<DoubleColumnFormSection> =>
+ buildCodecForObject<DoubleColumnFormSection>()
+ .property("title", codecForString())
+ .property("description", codecForString())
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("DoubleColumnFormSection");
+
+const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
+ buildCodecForObject<DoubleColumnForm>()
+ .property("type", codecForConstString("double-column"))
+ .property("design", codecForList(codecForDoubleColumnFormSection()))
+ .build("DoubleColumnForm");
+
+const codecForFlexibleForm = (): Codec<FlexibleForm> =>
+ buildCodecForUnion<FlexibleForm>()
+ .discriminateOn("type")
+ .alternative("double-column", codecForDoubleColumnForm())
+ .build<FlexibleForm>("FlexibleForm");
+
+const codecForFormMetadata = (): Codec<FormMetadata> =>
+ buildCodecForObject<FormMetadata>()
+ .property("label", codecForString())
+ .property("id", codecForString())
+ .property("version", codecForNumber())
+ .property("config", codecForFlexibleForm())
+ .build("FormMetadata");
+
+const codecForUIForms = (): Codec<UiForms> =>
+ buildCodecForObject<UiForms>()
+ .property("forms", codecForList(codecForFormMetadata()))
+ .build("UiForms");
+
+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 fetchUiForms(listener: (s: UiForms) => void): void {
+ fetch("./forms.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForUIForms().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultForms,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch forms", e);
+ listener(defaultForms);
+ });
+}
diff --git a/packages/aml-backoffice-ui/src/context/ui-settings.ts b/packages/aml-backoffice-ui/src/context/ui-settings.ts
new file mode 100644
index 000000000..aa318a918
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/ui-settings.ts
@@ -0,0 +1,110 @@
+/*
+ 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 { buildCodecForObject, canonicalizeBaseUrl, Codec, codecForString, codecOptional } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = UiSettings;
+
+/**
+ * Global settings for the UI.
+ */
+const defaultSettings: UiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+ signupEmail: undefined,
+};
+
+const Context = createContext<Type>(defaultSettings);
+
+export const useUiSettingsContext = (): Type => useContext(Context);
+
+export const UiSettingsProvider = ({
+ children,
+ value,
+}: {
+ value: UiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+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;
+}
+
+const codecForUISettings = (): 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 fetchUiSettings(listener: (s: UiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForUISettings().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, backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
+
+
diff --git a/packages/aml-backoffice-ui/src/declaration.d.ts b/packages/aml-backoffice-ui/src/declaration.d.ts
index 11a10860d..7868e41bd 100644
--- a/packages/aml-backoffice-ui/src/declaration.d.ts
+++ b/packages/aml-backoffice-ui/src/declaration.d.ts
@@ -1,3 +1,19 @@
+/*
+ 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/>
+ */
+
declare const __VERSION__: string;
declare const __GIT_HASH__: string;
diff --git a/packages/aml-backoffice-ui/src/settings.ts b/packages/aml-backoffice-ui/src/forms.ts
index 68f44b4df..3ecec2bb0 100644
--- a/packages/aml-backoffice-ui/src/settings.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
@@ -14,18 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-export interface UiSettings {
- backendBaseURL?: string;
- signupEmail?: string;
-}
+export * from "./forms/index.js";
/**
- * Global settings for the UI.
+ * this file is here to have a flat dist folder
+ *
+ * this file is being build in a bundle separated
+ * from the main one.
*/
-const defaultSettings: UiSettings = {
-};
-
-export const uiSettings: UiSettings =
- "talerExchangeAmlSettings" in globalThis
- ? (globalThis as any).talerExchangeAmlSettings
- : defaultSettings;
diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts
index 7ed3ea42b..7cf710741 100644
--- a/packages/aml-backoffice-ui/src/forms/902_11e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts
@@ -1,45 +1,58 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
-export const v1 = (current: BaseForm): FlexibleForm<Form902_11.Form> => ({
+ 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 type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title:
- "Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange" as TranslatedString,
+ i18n.str`Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange`,
description:
- "for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners." as TranslatedString,
+ i18n.str`for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners.`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
- label: "Contracting partner" as TranslatedString,
+ label: i18n.str`Contracting partner`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "declares",
label:
- "The contracting partner hereby declares that" as TranslatedString,
+ i18n.str`The contracting partner hereby declares that`,
required: true,
choices: [
{
label:
- "the person(s) listed below is/are holding 25% or more of the contracting partner's shares (capital shares or voting rights)" as TranslatedString,
+ i18n.str`the person(s) listed below is/are holding 25% or more of the contracting partner's shares (capital shares or voting rights)`,
value: "25-or-more",
},
{
label:
- "if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below is/are controlling the contracting partner in other ways" as TranslatedString,
+ i18n.str`if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below is/are controlling the contracting partner in other ways`,
value: "controlling-in-other-ways",
},
{
label:
- "in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the managing director(s)" as TranslatedString,
+ i18n.str`in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the managing director(s)`,
value: "managing-director",
},
],
@@ -47,33 +60,33 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_11.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
name: "people",
- label: "People" as TranslatedString,
+ label: i18n.str`People`,
required: true,
- placeholder: "this is the placeholder" as TranslatedString,
+ placeholder: i18n.str`this is the placeholder`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "lastName",
- label: "Last name(s)" as TranslatedString,
+ label: i18n.str`Last name(s)`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "firstName",
- label: "First name(s)" as TranslatedString,
+ label: i18n.str`First name(s)`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
- label: "Actual address of domicile" as TranslatedString,
+ label: i18n.str`Actual address of domicile`,
required: true,
},
},
@@ -83,28 +96,28 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_11.Form> => ({
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "fiduciaryAssets",
- label: "Fiduciary holding assets" as TranslatedString,
- help: "Is a third person the beneficial owner of the assets held in the account/securities account?" as TranslatedString,
+ label: i18n.str`Fiduciary holding assets`,
+ help: i18n.str`Is a third person the beneficial owner of the assets held in the account/securities account?`,
required: true,
choices: [
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
description:
- "The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9" as TranslatedString,
+ i18n.str`The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9`,
},
],
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
behavior: function formBehavior(
v: Partial<Form902_11.Form>,
diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts
index 3cfe69f88..5aa3f4cf9 100644
--- a/packages/aml-backoffice-ui/src/forms/902_12e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts
@@ -1,49 +1,63 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm } from "./index.js";
+/*
+ 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 type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
- title: "Foundations" as TranslatedString,
+ title: i18n.str`Foundations`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
- label: "Contracting partner" as TranslatedString,
+ label: i18n.str`Contracting partner`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "knownAs",
label:
- "The undersigned hereby declare(s) that as board member of the foundation, or of the highest supervisory body of an underlying company of a foundation, known as" as TranslatedString,
+ i18n.str`The undersigned hereby declare(s) that as board member of the foundation, or of the highest supervisory body of an underlying company of a foundation, known as`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "foundation.name",
label:
- "Name and information pertaining to the foundation" as TranslatedString,
+ i18n.str`Name and information pertaining to the foundation`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "foundation.type",
- label: "Type of foundation" as TranslatedString,
+ label: i18n.str`Type of foundation`,
choices: [
{
- label: "Discretionary foundation" as TranslatedString,
+ label: i18n.str`Discretionary foundation`,
value: "discretionary",
},
{
- label: "Non-discretionary foundation" as TranslatedString,
+ label: i18n.str`Non-discretionary foundation`,
value: "non-discretionary",
},
],
@@ -51,16 +65,16 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "foundation.revocability",
- label: "Revocability" as TranslatedString,
+ label: i18n.str`Revocability`,
choices: [
{
- label: "Revocable foundation" as TranslatedString,
+ label: i18n.str`Revocable foundation`,
value: "revocable",
},
{
- label: "Irrevocable foundation" as TranslatedString,
+ label: i18n.str`Irrevocable foundation`,
value: "irrevocable",
},
],
@@ -68,71 +82,71 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies)" as TranslatedString,
+ i18n.str`Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies)`,
labelField: "fullName",
name: "founders",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfDeath",
- label: "Date of death" as TranslatedString,
- help: "if deceased" as TranslatedString,
+ label: i18n.str`Date of death`,
+ help: i18n.str`if deceased`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToRevoke",
required: true,
label:
- "Does the founder have the right to revoke the foundation?" as TranslatedString,
+ i18n.str`Does the founder have the right to revoke the foundation?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -143,55 +157,55 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given" as TranslatedString,
+ i18n.str`If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given`,
labelField: "fullName",
name: "preExistingFounders",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfDeath",
- label: "Date of death" as TranslatedString,
- help: "if deceased" as TranslatedString,
+ label: i18n.str`Date of death`,
+ help: i18n.str`if deceased`,
},
},
],
@@ -199,62 +213,62 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Pertaining to the beneficiary/-ies at the time of the signing of this form" as TranslatedString,
+ i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
labelField: "fullName",
name: "beneficiaryWhenSigning",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
- "Has the beneficiary an actual right to claim distribution?" as TranslatedString,
+ i18n.str`Has the beneficiary an actual right to claim distribution?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -262,9 +276,9 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form" as TranslatedString,
+ i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form`,
name: "beneficiaryExtra",
},
},
@@ -273,62 +287,62 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Information pertaining to further persons having the right to determine or nominate representatives (e.g.) members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries" as TranslatedString,
+ i18n.str`Information pertaining to further persons having the right to determine or nominate representatives (e.g.) members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries`,
labelField: "fullName",
name: "withRightToNominate",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
- "has the person the right to revoke the foundation?" as TranslatedString,
+ i18n.str`has the person the right to revoke the foundation?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -336,9 +350,9 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form" as TranslatedString,
+ i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form`,
name: "beneficiaryExtra",
},
},
@@ -347,39 +361,39 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
- label: "Signature" as TranslatedString,
+ label: i18n.str`Signature`,
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_12.Form>,
- ): FormState<Form902_12.Form> {
- return {
- founders: {
- elements: (v.founders ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- withRightToNominate: {
- elements: (v.withRightToNominate ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_12.Form>,
+ // ): FormState<Form902_12.Form> {
+ // return {
+ // founders: {
+ // elements: (v.founders ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // withRightToNominate: {
+ // elements: (v.withRightToNominate ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // };
+ // },
});
namespace Form902_12 {
diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts
index 045246fc6..d71266489 100644
--- a/packages/aml-backoffice-ui/src/forms/902_13e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts
@@ -1,49 +1,63 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm, } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm } from "./index.js";
+/*
+ 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 type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
- title: "Declaration for trusts" as TranslatedString,
+ title: i18n.str`Declaration for trusts`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
- label: "Contracting partner" as TranslatedString,
+ label: i18n.str`Contracting partner`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "knownAs",
label:
- "The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as" as TranslatedString,
+ i18n.str`The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "trust.name",
label:
- "Name and information pertaining to the trust" as TranslatedString,
+ i18n.str`Name and information pertaining to the trust`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "trust.type",
- label: "Type of trust" as TranslatedString,
+ label: i18n.str`Type of trust`,
choices: [
{
- label: "Discretionary trust" as TranslatedString,
+ label: i18n.str`Discretionary trust`,
value: "discretionary",
},
{
- label: "Non-discretionary trust" as TranslatedString,
+ label: i18n.str`Non-discretionary trust`,
value: "non-discretionary",
},
],
@@ -51,16 +65,16 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "trust.revocability",
- label: "Revocability" as TranslatedString,
+ label: i18n.str`Revocability`,
choices: [
{
- label: "Revocable foundation" as TranslatedString,
+ label: i18n.str`Revocable foundation`,
value: "revocable",
},
{
- label: "Irrevocable foundation" as TranslatedString,
+ label: i18n.str`Irrevocable foundation`,
value: "irrevocable",
},
],
@@ -68,75 +82,75 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/ies)" as TranslatedString,
+ i18n.str`Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/ies)`,
labelField: "fullName",
name: "settlors",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfDeath",
- label: "Date of death" as TranslatedString,
+ label: i18n.str`Date of death`,
pattern: "dd/MM/yyyy",
- // help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
- help: "if deceased'" as TranslatedString,
+ // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
+ help: i18n.str`if deceased'`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToRevoke",
required: true,
label:
- "Does the founder have the right to revoke the trust?" as TranslatedString,
+ i18n.str`Does the founder have the right to revoke the trust?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -147,59 +161,59 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "If the trust results from the restructuring of pre-existing trust (re-settlement) or the merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given" as TranslatedString,
+ i18n.str`If the trust results from the restructuring of pre-existing trust (re-settlement) or the merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given`,
labelField: "fullName",
name: "preExistingSettlors",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfDeath",
- label: "Date of death" as TranslatedString,
+ label: i18n.str`Date of death`,
pattern: "dd/MM/yyyy",
- help: "if deceased." as TranslatedString,
- // help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
+ help: i18n.str`if deceased.`,
+ // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
},
},
],
@@ -207,64 +221,64 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Pertaining to the beneficiary/-ies at the time of the signing of this form" as TranslatedString,
+ i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
labelField: "fullName",
name: "beneficiaryWhenSigning",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
- "Has the beneficiary an actual right to claim distribution?" as TranslatedString,
+ i18n.str`Has the beneficiary an actual right to claim distribution?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -272,9 +286,9 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form" as TranslatedString,
+ i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form`,
name: "beneficiaryExtra",
},
},
@@ -283,9 +297,9 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust" as TranslatedString,
+ i18n.str`Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust`,
labelField: "asd",
name: "nothing",
fields: [],
@@ -294,62 +308,62 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
{
type: "array",
- props: {
+ properties: {
label:
- "Information pertaining to the protectors" as TranslatedString,
+ i18n.str`Information pertaining to the protectors`,
labelField: "fullName",
name: "protectors",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
- "Does the protector have the right to revoke the trust?" as TranslatedString,
+ i18n.str`Does the protector have the right to revoke the trust?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -360,62 +374,62 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "array",
- props: {
+ properties: {
label:
- "Information pertaining to further persons" as TranslatedString,
+ i18n.str`Information pertaining to further persons`,
labelField: "fullName",
name: "furtherPersons",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
- "Actual address of domicile/registered office" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "country",
- label: "Country" as TranslatedString,
+ label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
- "Has this further person the right to revoke the trust?" as TranslatedString,
+ i18n.str`Has this further person the right to revoke the trust?`,
choices: [
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
value: "yes",
},
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
],
@@ -426,48 +440,48 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
- label: "Signature" as TranslatedString,
+ label: i18n.str`Signature`,
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_13.Form>,
- ): FormState<Form902_13.Form> {
- return {
- settlors: {
- elements: (v.settlors ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- protectors: {
- elements: (v.protectors ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- furtherPersons: {
- elements: (v.furtherPersons ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_13.Form>,
+ // ): FormState<Form902_13.Form> {
+ // return {
+ // settlors: {
+ // elements: (v.settlors ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // protectors: {
+ // elements: (v.protectors ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // furtherPersons: {
+ // elements: (v.furtherPersons ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // };
+ // },
});
namespace Form902_13 {
diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts
index 6689896ab..eeda166c1 100644
--- a/packages/aml-backoffice-ui/src/forms/902_15e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts
@@ -1,85 +1,100 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm } from "./index.js";
+/*
+ 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 type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_15.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title:
- "Information on life insurance policies with separately managed accounts/securities accounts" as TranslatedString,
+ i18n.str`Information on life insurance policies with separately managed accounts/securities accounts`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
- label: "Contracting partner" as TranslatedString,
+ label: i18n.str`Contracting partner`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "contractualRelationship",
label:
- "Name or number of the contractual relationship between the contracting party and the financial intermediary" as TranslatedString,
+ i18n.str`Name or number of the contractual relationship between the contracting party and the financial intermediary`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "insurancePolicy",
- label: "Insurance policy" as TranslatedString,
+ label: i18n.str`Insurance policy`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
- "The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above." as TranslatedString,
+ i18n.str`The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above.`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
- "In relation with the above insurance policy, the contracting partner gives the following further details" as TranslatedString,
+ i18n.str`In relation with the above insurance policy, the contracting partner gives the following further details`,
},
},
{
type: "group",
- props: {
- before: "Policy holder" as TranslatedString,
+ properties: {
+ before: i18n.str`Policy holder`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "holder.fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "holder.address",
label:
- "Actual address of domicile/registered office (incl. country)" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office (incl. country)`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "holder.dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "holder.nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
],
@@ -87,40 +102,40 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_15.Form> => ({
},
{
type: "group",
- props: {
+ properties: {
before:
- "Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above)" as TranslatedString,
+ i18n.str`Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above)`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "premiumPayer.fullName",
label:
- "Last name(s), first name(s)/entity" as TranslatedString,
+ i18n.str`Last name(s), first name(s)/entity`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "premiumPayer.address",
label:
- "Actual address of domicile/registered office (incl. country)" as TranslatedString,
+ i18n.str`Actual address of domicile/registered office (incl. country)`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "premiumPayer.dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "premiumPayer.nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
],
@@ -128,28 +143,28 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_15.Form> => ({
},
{
type: "caption",
- props: {
+ properties: {
label:
- "The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary" as TranslatedString,
+ i18n.str`The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
- label: "Signature" as TranslatedString,
+ label: i18n.str`Signature`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
- "It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)" as TranslatedString,
+ i18n.str`It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)`,
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
});
diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts
index 7fcabe829..58ef7e2e8 100644
--- a/packages/aml-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts
@@ -1,29 +1,42 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm, languageList } from "./index.js";
+/*
+ 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 type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
- title: "Information on customer" as TranslatedString,
- description:
- "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer." as TranslatedString,
+ title: i18n.str`Information on customer`,
+ description: i18n.str`The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
name: "customerType",
- label: "Type of customer" as TranslatedString,
+ label: i18n.str`Type of customer`,
required: true,
choices: [
{
- label: "Natural person" as TranslatedString,
+ label: i18n.str`Natural person`,
value: "natural",
},
{
- label: "Legal entity" as TranslatedString,
+ label: i18n.str`Legal entity`,
value: "legal",
},
],
@@ -31,245 +44,241 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.fullName",
- label: "Full name" as TranslatedString,
+ label: i18n.str`Full name`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.address",
- label: "Residential address" as TranslatedString,
+ label: i18n.str`Residential address`,
required: true,
},
},
{
type: "integer",
- props: {
+ properties: {
name: "naturalCustomer.telephone",
- label: "Telephone" as TranslatedString,
+ label: i18n.str`Telephone`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.email",
- label: "E-mail" as TranslatedString,
+ label: i18n.str`E-mail`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "naturalCustomer.dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
required: true,
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.document",
- label: "Identification document" as TranslatedString,
+ label: i18n.str`Identification document`,
required: true,
},
},
{
type: "file",
- props: {
+ properties: {
name: "naturalCustomer.documentAttachment",
- label: "Document attachment" as TranslatedString,
+ label: i18n.str`Document attachment`,
required: true,
maxBites: 2 * 1024 * 1024,
accept: ".png",
- help: "Max size of 2 mega bytes" as TranslatedString,
+ help: i18n.str`Max size of 2 mega bytes`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.companyName",
- label: "Company name" as TranslatedString,
+ label: i18n.str`Company name`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.office",
- label: "Registered office" as TranslatedString,
+ label: i18n.str`Registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.companyDocument",
- label: "Company identification document" as TranslatedString,
+ label: i18n.str`Company identification document`,
},
},
{
type: "file",
- props: {
+ properties: {
name: "naturalCustomer.companyDocumentAttachment",
- label: "Document attachment" as TranslatedString,
+ label: i18n.str`Document attachment`,
required: true,
maxBites: 2 * 1024 * 1024,
accept: ".png",
- help: "Max size of 2 mega bytes" as TranslatedString,
+ help: i18n.str`Max size of 2 mega bytes`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.companyName",
- label: "Company name" as TranslatedString,
+ label: i18n.str`Company name`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.domicile",
- label: "Domicile" as TranslatedString,
+ label: i18n.str`Domicile`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.contactPerson",
- label: "Contact person" as TranslatedString,
+ label: i18n.str`Contact person`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.telephone",
- label: "Telephone" as TranslatedString,
+ label: i18n.str`Telephone`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.email",
- label: "E-mail" as TranslatedString,
+ label: i18n.str`E-mail`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.document",
- label: "Identification document" as TranslatedString,
- help: "Not older than 12 month" as TranslatedString,
+ label: i18n.str`Identification document`,
+ help: i18n.str`Not older than 12 month`,
},
},
{
type: "file",
- props: {
+ properties: {
name: "legalCustomer.documentAttachment",
- label: "Document attachment" as TranslatedString,
+ label: i18n.str`Document attachment`,
required: true,
maxBites: 2 * 1024 * 1024,
accept: ".png",
- help: "Max size of 2 mega bytes" as TranslatedString,
+ help: i18n.str`Max size of 2 mega bytes`,
},
},
],
},
{
- title:
- "Information on the natural persons who establish the business relationship for legal entities and partnerships" as TranslatedString,
- description:
- "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified." as TranslatedString,
+ title: i18n.str`Information on the natural persons who establish the business relationship for legal entities and partnerships`,
+ description: i18n.str`For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.`,
fields: [
{
type: "array",
- props: {
+ properties: {
name: "businessEstablisher",
- label: "Persons" as TranslatedString,
+ label: i18n.str`Persons`,
required: true,
- placeholder: "this is the placeholder" as TranslatedString,
+ placeholder: i18n.str`this is the placeholder`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
- label: "Full name" as TranslatedString,
+ label: i18n.str`Full name`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
- label: "Residential address" as TranslatedString,
+ label: i18n.str`Residential address`,
required: true,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
required: true,
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "typeOfAuthorization",
- label:
- "Type of authorization (signatory of representation)" as TranslatedString,
+ label: i18n.str`Type of authorization (signatory of representation)`,
required: true,
},
},
{
type: "file",
- props: {
+ properties: {
name: "documentAttachment",
- label:
- "Identification document attachment" as TranslatedString,
+ label: i18n.str`Identification document attachment`,
required: true,
maxBites: 2 * 1024 * 1024,
accept: ".png",
- help: "Max size of 2 mega bytes" as TranslatedString,
+ help: i18n.str`Max size of 2 mega bytes`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "powerOfAttorneyArrangements",
- label: "Power of attorney arrangements" as TranslatedString,
+ label: i18n.str`Power of attorney arrangements`,
required: true,
choices: [
{
- label: "CR extract" as TranslatedString,
+ label: i18n.str`CR extract`,
value: "cr",
},
{
- label: "Mandate" as TranslatedString,
+ label: i18n.str`Mandate`,
value: "mandate",
},
{
- label: "Other" as TranslatedString,
+ label: i18n.str`Other`,
value: "other",
},
],
@@ -277,9 +286,9 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
},
{
type: "text",
- props: {
+ properties: {
name: "powerOfAttorneyArrangementsOther",
- label: "Power of attorney arrangements" as TranslatedString,
+ label: i18n.str`Power of attorney arrangements`,
required: true,
},
},
@@ -290,36 +299,34 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
],
},
{
- title: "Acceptance of business relationship" as TranslatedString,
+ title: i18n.str`Acceptance of business relationship`,
fields: [
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "acceptance.when",
pattern: "dd/MM/yyyy",
- label: "Date (conclusion of contract)" as TranslatedString,
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ label: i18n.str`Date (conclusion of contract)`,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "acceptance.acceptedBy",
- label: "Accepted by" as TranslatedString,
+ label: i18n.str`Accepted by`,
required: true,
choices: [
{
- label: "Face-to-face meeting with customer" as TranslatedString,
+ label: i18n.str`Face-to-face meeting with customer`,
value: "face-to-face",
},
{
- label:
- "Correspondence: authenticated copy of identification document obtained" as TranslatedString,
+ label: i18n.str`Correspondence: authenticated copy of identification document obtained`,
value: "correspondence-document",
},
{
- label:
- "Correspondence: residential address validated" as TranslatedString,
+ label: i18n.str`Correspondence: residential address validated`,
value: "correspondence-address",
},
],
@@ -327,24 +334,24 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "acceptance.typeOfCorrespondence",
- label: "Type of correspondence service" as TranslatedString,
+ label: i18n.str`Type of correspondence service`,
choices: [
{
- label: "to the customer" as TranslatedString,
+ label: i18n.str`to the customer`,
value: "customer",
},
{
- label: "hold at bank" as TranslatedString,
+ label: i18n.str`hold at bank`,
value: "bank",
},
{
- label: "to the member" as TranslatedString,
+ label: i18n.str`to the member`,
value: "member",
},
{
- label: "to a third party" as TranslatedString,
+ label: i18n.str`to a third party`,
value: "third-party",
},
],
@@ -352,73 +359,67 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
},
{
type: "text",
- props: {
+ properties: {
name: "acceptance.thirdPartyFullName",
- label: "Third party full name" as TranslatedString,
+ label: i18n.str`Third party full name`,
required: true,
},
},
{
type: "text",
- props: {
+ properties: {
name: "acceptance.thirdPartyAddress",
- label: "Third party address" as TranslatedString,
+ label: i18n.str`Third party address`,
required: true,
},
},
{
type: "selectMultiple",
- props: {
+ properties: {
name: "acceptance.language",
- label: "Languages" as TranslatedString,
- choices: languageList,
+ label: i18n.str`Languages`,
+ choices: ["asd"],
unique: true,
},
},
{
type: "textArea",
- props: {
+ properties: {
name: "acceptance.furtherInformation",
- label: "Further information" as TranslatedString,
+ label: i18n.str`Further information`,
},
},
],
},
{
- title:
- "Information on the beneficial owner of the assets and/or controlling person" as TranslatedString,
- description:
- "Establishment of the beneficial owner of the assets and/or controlling person" as TranslatedString,
+ title: i18n.str`Information on the beneficial owner of the assets and/or controlling person`,
+ description: i18n.str`Establishment of the beneficial owner of the assets and/or controlling person`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
name: "establishment",
- label: "The customer is" as TranslatedString,
+ label: i18n.str`The customer is`,
required: true,
choices: [
{
- label:
- "a natural person and there are no doubts that this person is the sole beneficial owner of the assets" as TranslatedString,
+ label: i18n.str`a natural person and there are no doubts that this person is the sole beneficial owner of the assets`,
value: "natural",
},
{
- label:
- "a foundation (or a similar construct; incl. underlying companies)" as TranslatedString,
+ label: i18n.str`a foundation (or a similar construct; incl. underlying companies)`,
value: "foundation",
},
{
- label:
- "a trust (incl. underlying companies)" as TranslatedString,
+ label: i18n.str`a trust (incl. underlying companies)`,
value: "trust",
},
{
- label:
- "a life insurance policy with separately managed accounts/securities accounts" as TranslatedString,
+ label: i18n.str`a life insurance policy with separately managed accounts/securities accounts`,
value: "insurance-wrapper",
},
{
- label: "all other cases" as TranslatedString,
+ label: i18n.str`all other cases`,
value: "other",
},
],
@@ -427,44 +428,39 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
],
},
{
- title:
- "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship" as TranslatedString,
- description:
- "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)" as TranslatedString,
+ title: i18n.str`Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship`,
+ description: i18n.str`Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "embargoEvaluation",
- help: "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated." as TranslatedString,
- label: "Evaluation" as TranslatedString,
+ help: i18n.str`The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.`,
+ label: i18n.str`Evaluation`,
},
},
],
},
{
- title:
- "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship" as TranslatedString,
- description:
- "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created" as TranslatedString,
+ title: i18n.str`In the case of cash transactions/occasional customers: Information on type and purpose of business relationship`,
+ description: i18n.str`These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
name: "cashTransactions.typeOfBusiness",
- label: "Type of business relationship" as TranslatedString,
+ label: i18n.str`Type of business relationship`,
choices: [
{
- label: "Money exchange" as TranslatedString,
+ label: i18n.str`Money exchange`,
value: "money-exchange",
},
{
- label: "Money and asset transfer" as TranslatedString,
+ label: i18n.str`Money and asset transfer`,
value: "money-and-asset-transfer",
},
{
- label:
- "Other cash transactions. Specify below" as TranslatedString,
+ label: i18n.str`Other cash transactions. Specify below`,
value: "other",
},
],
@@ -472,116 +468,115 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
},
{
type: "text",
- props: {
+ properties: {
name: "cashTransactions.otherTypeOfBusiness",
required: true,
- label: "Specify other cash transactions:" as TranslatedString,
+ label: i18n.str`Specify other cash transactions:`,
},
},
{
type: "textArea",
- props: {
+ properties: {
name: "cashTransactions.purpose",
- label:
- "Purpose of the business relationship (purpose of service requested)" as TranslatedString,
+ label: i18n.str`Purpose of the business relationship (purpose of service requested)`,
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_1.Form>,
- ): FormState<Form902_1.Form> {
- return {
- fullName: {
- disabled: true,
- },
- businessEstablisher: {
- elements: (v.businessEstablisher ?? []).map((be) => {
- return {
- powerOfAttorneyArrangementsOther: {
- hidden: be.powerOfAttorneyArrangements !== "other",
- },
- };
- }),
- },
- acceptance: {
- thirdPartyFullName: {
- hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
- },
- thirdPartyAddress: {
- hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
- },
- },
- cashTransactions: {
- otherTypeOfBusiness: {
- hidden: v.cashTransactions?.typeOfBusiness !== "other",
- },
- },
- naturalCustomer: {
- fullName: {
- hidden: v.customerType !== "natural",
- },
- address: {
- hidden: v.customerType !== "natural",
- },
- telephone: {
- hidden: v.customerType !== "natural",
- },
- email: {
- hidden: v.customerType !== "natural",
- },
- dateOfBirth: {
- hidden: v.customerType !== "natural",
- },
- nationality: {
- hidden: v.customerType !== "natural",
- },
- document: {
- hidden: v.customerType !== "natural",
- },
- companyName: {
- hidden: v.customerType !== "natural",
- },
- office: {
- hidden: v.customerType !== "natural",
- },
- companyDocument: {
- hidden: v.customerType !== "natural",
- },
- companyDocumentAttachment: {
- hidden: v.customerType !== "natural",
- },
- documentAttachment: {
- hidden: v.customerType !== "natural",
- },
- },
- legalCustomer: {
- companyName: {
- hidden: v.customerType !== "legal",
- },
- contactPerson: {
- hidden: v.customerType !== "legal",
- },
- document: {
- hidden: v.customerType !== "legal",
- },
- domicile: {
- hidden: v.customerType !== "legal",
- },
- email: {
- hidden: v.customerType !== "legal",
- },
- telephone: {
- hidden: v.customerType !== "legal",
- },
- documentAttachment: {
- hidden: v.customerType !== "legal",
- },
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_1.Form>,
+ // ): FormState<Form902_1.Form> {
+ // return {
+ // fullName: {
+ // disabled: true,
+ // },
+ // businessEstablisher: {
+ // elements: (v.businessEstablisher ?? []).map((be) => {
+ // return {
+ // powerOfAttorneyArrangementsOther: {
+ // hidden: be.powerOfAttorneyArrangements !== "other",
+ // },
+ // };
+ // }),
+ // },
+ // acceptance: {
+ // thirdPartyFullName: {
+ // hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
+ // },
+ // thirdPartyAddress: {
+ // hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
+ // },
+ // },
+ // cashTransactions: {
+ // otherTypeOfBusiness: {
+ // hidden: v.cashTransactions?.typeOfBusiness !== "other",
+ // },
+ // },
+ // naturalCustomer: {
+ // fullName: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // address: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // telephone: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // email: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // dateOfBirth: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // nationality: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // document: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // companyName: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // office: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // companyDocument: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // companyDocumentAttachment: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // documentAttachment: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // },
+ // legalCustomer: {
+ // companyName: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // contactPerson: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // document: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // domicile: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // email: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // telephone: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // documentAttachment: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // },
+ // };
+ // },
});
namespace Form902_1 {
@@ -633,11 +628,11 @@ namespace Form902_1 {
interface BeneficialOwner {
establishment:
- | "natural-person"
- | "foundation"
- | "trust"
- | "insurance-wrapper"
- | "other";
+ | "natural-person"
+ | "foundation"
+ | "trust"
+ | "insurance-wrapper"
+ | "other";
}
interface CashTransactions {
diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts
index 6f82302f3..7a3af8731 100644
--- a/packages/aml-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts
@@ -1,65 +1,79 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+/*
+ 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 type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
import { h as create } from "preact";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm } from "./index.js";
-import { Simplest, resolutionSection } from "./simplest.js";
-import { ArrowRightIcon, ChevronRightIcon } from "../pages/Cases.js";
+import { BaseForm } from "../context/ui-forms.js";
+import { ArrowRightIcon, ChevronRightIcon } from "./icons.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
- title: "Risk Profile AMLA" as TranslatedString,
+ title: i18n.str`Risk Profile AMLA`,
description:
- "Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring." as TranslatedString,
+ i18n.str`Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring.`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations)" as TranslatedString,
+ i18n.str`The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations)`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
},
},
{
type: "text",
- props: {
+ properties: {
name: "customer",
- label: "Customer" as TranslatedString,
- help: "Pursuant identification form (VQF doc. Nr. 902.1) numeral 1" as TranslatedString,
+ label: i18n.str`Customer`,
+ help: i18n.str`Pursuant identification form (VQF doc. Nr. 902.1) numeral 1`,
},
},
],
},
{
title:
- "Evaluation of politically exposed persons (PEP-Check)" as TranslatedString,
+ i18n.str`Evaluation of politically exposed persons (PEP-Check)`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "This evaluation has to be completed by all members for every business relationship" as TranslatedString,
+ i18n.str`This evaluation has to be completed by all members for every business relationship`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
},
},
{
type: "choiceStacked",
- props: {
- label: "Foreign PEP" as TranslatedString,
+ properties: {
+ label: i18n.str`Foreign PEP`,
// tooltip:
- // "Definition see Art. 7 lit. g numeral 1 SRO Regulations" as TranslatedString,
- help: "Is the customer, the beneficial owner or the controlling person or authorized representative a foreign PEP or closely related to such a person?" as TranslatedString,
+ // i18n.str`Definition see Art. 7 lit. g numeral 1 SRO Regulations`,
+ help: i18n.str`Is the customer, the beneficial owner or the controlling person or authorized representative a foreign PEP or closely related to such a person?`,
name: "pep.foreign",
choices: [
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
description:
- "The business relationship is compulsory classified as increased risk" as TranslatedString,
+ i18n.str`The business relationship is compulsory classified as increased risk`,
value: "yes",
},
],
@@ -67,41 +81,41 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "choiceStacked",
- props: {
+ properties: {
label:
- "Domestic PEP and PEP of International Organizations" as TranslatedString,
+ i18n.str`Domestic PEP and PEP of International Organizations`,
// tooltip:
- // "Definition see Art. 7 lit. g numeral 2 and 3 SRO Regulations " as TranslatedString,
- help: "Is the customer, the beneficial owner or the controlling person or authorized representative a domestic PEP or PEP in International Organizations or closely related to such a person?" as TranslatedString,
+ // i18n.str`Definition see Art. 7 lit. g numeral 2 and 3 SRO Regulations `,
+ help: i18n.str`Is the customer, the beneficial owner or the controlling person or authorized representative a domestic PEP or PEP in International Organizations or closely related to such a person?`,
name: "pep.domestic",
choices: [
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
{
label:
- "Yes, but NOT risk criterion pursuant to numeral 3 subsequently increased." as TranslatedString,
+ i18n.str`Yes, but NOT risk criterion pursuant to numeral 3 subsequently increased.`,
value: "yes-but-no-risk",
},
{
label:
- "Yes, AND a risk criterion pursuant to numeral 3 subsequently increased." as TranslatedString,
+ i18n.str`Yes, AND a risk criterion pursuant to numeral 3 subsequently increased.`,
description:
- "Classification of the business relationship as increased risk is compulsory" as TranslatedString,
+ i18n.str`Classification of the business relationship as increased risk is compulsory`,
value: "yes",
},
],
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
label:
- "The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
+ i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
name: "pep.when",
pattern: "dd/MM/yyyy",
- // placeholder: "dd/MM/yyyy" as TranslatedString,
+ // placeholder: i18n.str`dd/MM/yyyy`,
},
},
],
@@ -112,77 +126,77 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "This evaluation has to be completed by all members for every business relationship" as TranslatedString,
+ i18n.str`This evaluation has to be completed by all members for every business relationship`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: '"High risk" or non-cooperative country' as TranslatedString,
help: 'Is the customer, the beneficial owner or the controlling person or authorized representative in a country considered by the FATF "high risk" or non-cooperative and for which FATF requires increased diligence?' as TranslatedString,
name: "highRisk.evaluation",
choices: [
{
- label: "No" as TranslatedString,
+ label: i18n.str`No`,
value: "no",
},
{
- label: "Yes" as TranslatedString,
+ label: i18n.str`Yes`,
description:
- "considered as business relationship with increased risk" as TranslatedString,
+ i18n.str`considered as business relationship with increased risk`,
value: "yes",
},
],
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
label:
- "The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
+ i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
name: "highRisk.when",
pattern: "dd/MM/yyyy",
- // placeholder: "dd/MM/yyyy" as TranslatedString,
+ // placeholder: i18n.str`dd/MM/yyyy`,
},
},
],
},
{
- title: "Evaluation of business relationship risk" as TranslatedString,
+ title: i18n.str`Evaluation of business relationship risk`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed" as TranslatedString,
+ i18n.str`This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
},
},
{
type: "group",
- props: {
- before: "a) Country risk (nationality)" as TranslatedString,
+ properties: {
+ before: i18n.str`a) Country risk (nationality)`,
fields: [
{
type: "choiceStacked",
- props: {
- label: "Domicile/residential address" as TranslatedString,
+ properties: {
+ label: i18n.str`Domicile/residential address`,
name: "evaluation.nationality.address",
choices: [
{
- label: "Customer" as TranslatedString,
+ label: i18n.str`Customer`,
value: "customer",
},
{
label:
- "Beneficial owner of the assets" as TranslatedString,
+ i18n.str`Beneficial owner of the assets`,
value: "owner",
},
{
- label: "Controlling person" as TranslatedString,
+ label: i18n.str`Controlling person`,
value: "controlling",
},
],
@@ -190,17 +204,17 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "choiceStacked",
- props: {
- label: "Nationality" as TranslatedString,
+ properties: {
+ label: i18n.str`Nationality`,
name: "evaluation.nationality.nationality",
choices: [
{
- label: "Customer" as TranslatedString,
+ label: i18n.str`Customer`,
value: "customer",
},
{
label:
- "Beneficial owner of the assets" as TranslatedString,
+ i18n.str`Beneficial owner of the assets`,
value: "owner",
},
],
@@ -208,23 +222,23 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.nationality.risk",
choices: [
{
label:
- "Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "low",
},
{
label:
- "Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "medium",
},
{
label:
- "Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "high",
},
],
@@ -235,22 +249,22 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
- before: "b) Country risk (business activity)" as TranslatedString,
+ properties: {
+ before: i18n.str`b) Country risk (business activity)`,
fields: [
{
type: "choiceStacked",
- props: {
- label: "Place of business activity" as TranslatedString,
+ properties: {
+ label: i18n.str`Place of business activity`,
name: "evaluation.business.place",
choices: [
{
- label: "Customer" as TranslatedString,
+ label: i18n.str`Customer`,
value: "customer",
},
{
label:
- "Beneficial owner of the assets" as TranslatedString,
+ i18n.str`Beneficial owner of the assets`,
value: "owner",
},
],
@@ -258,23 +272,23 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.business.risk",
choices: [
{
label:
- "Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "low",
},
{
label:
- "Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "medium",
},
{
label:
- "Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "high",
},
],
@@ -285,35 +299,35 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
- before: "c) Country risk (payments)" as TranslatedString,
+ properties: {
+ before: i18n.str`c) Country risk (payments)`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "Country of origin and destination of frequent payments (if known)" as TranslatedString,
+ i18n.str`Country of origin and destination of frequent payments (if known)`,
},
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.payments.risk",
choices: [
{
label:
- "Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "low",
},
{
label:
- "Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "medium",
},
{
label:
- "Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
value: "high",
},
],
@@ -324,23 +338,23 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
- before: "d) Industry risk" as TranslatedString,
+ properties: {
+ before: i18n.str`d) Industry risk`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
label:
- "Nature of customer's business activity" as TranslatedString,
+ i18n.str`Nature of customer's business activity`,
name: "evaluation.industry.nature",
choices: [
{
- label: "Customer" as TranslatedString,
+ label: i18n.str`Customer`,
value: "customer",
},
{
label:
- "Beneficial owner of the assets" as TranslatedString,
+ i18n.str`Beneficial owner of the assets`,
value: "owner",
},
],
@@ -348,33 +362,33 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.payments.risk",
choices: [
{
label:
- "Clearly defined, transparent, easily comprehensible business activity well known to the member" as TranslatedString,
+ i18n.str`Clearly defined, transparent, easily comprehensible business activity well known to the member`,
value: "low",
},
{
label:
- "Business activity with a high level of cash transactions" as TranslatedString,
+ i18n.str`Business activity with a high level of cash transactions`,
value: "medium-cash",
},
{
label:
- "Business activity not well known to the member" as TranslatedString,
+ i18n.str`Business activity not well known to the member`,
value: "medium-unknown",
},
{
label:
- "Trade in munitions/arms, raw gem stones/diamonds, jewelry, international trade in exotic animals, casino and lottery business, trade in erotic wares" as TranslatedString,
+ i18n.str`Trade in munitions/arms, raw gem stones/diamonds, jewelry, international trade in exotic animals, casino and lottery business, trade in erotic wares`,
value: "high-restricted",
},
{
label:
- "Member has no personal knowledge of the customer's industry" as TranslatedString,
+ i18n.str`Member has no personal knowledge of the customer's industry`,
value: "high-unknown",
},
],
@@ -385,35 +399,35 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
- before: "e) Contact risk" as TranslatedString,
+ properties: {
+ before: i18n.str`e) Contact risk`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "Types of contact to the customer/ beneficial owner of the assets" as TranslatedString,
+ i18n.str`Types of contact to the customer/ beneficial owner of the assets`,
},
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.contact.risk",
choices: [
{
label:
- "Personal acquaintance between member and customer/beneficial owner of the assets over several years (at least 2) prior to entering into the business relationship" as TranslatedString,
+ i18n.str`Personal acquaintance between member and customer/beneficial owner of the assets over several years (at least 2) prior to entering into the business relationship`,
value: "low",
},
{
label:
- "The customer/beneficial owner was not personally known to the member for several years (at least 2) prior to entering into the business relationship; however (a) no business was entered into in the absence of the customer/beneficial owner, or (b) the customer was at least introduced/brokered by a trusted third party" as TranslatedString,
+ i18n.str`The customer/beneficial owner was not personally known to the member for several years (at least 2) prior to entering into the business relationship; however (a) no business was entered into in the absence of the customer/beneficial owner, or (b) the customer was at least introduced/brokered by a trusted third party`,
value: "medium",
},
{
label:
- "The customer/beneficial owner was not personally known to the member and business was entered into in the absence of the former (relationship by correspondence) and the customer was not introduced/brokered by a trusted third party" as TranslatedString,
+ i18n.str`The customer/beneficial owner was not personally known to the member and business was entered into in the absence of the former (relationship by correspondence) and the customer was not introduced/brokered by a trusted third party`,
value: "high",
},
],
@@ -424,55 +438,55 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
- before: "f) Product risk" as TranslatedString,
+ properties: {
+ before: i18n.str`f) Product risk`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "Nature of services and products requested by the customer" as TranslatedString,
+ i18n.str`Nature of services and products requested by the customer`,
},
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.product.risk",
choices: [
{
label:
- "Easy to understand, transparent services and products whose financial background is easy to comprehend and verify" as TranslatedString,
+ i18n.str`Easy to understand, transparent services and products whose financial background is easy to comprehend and verify`,
value: "low",
},
{
label:
- "More sophisticated services/products whose financial background is not readily easy to comprehend and verify" as TranslatedString,
+ i18n.str`More sophisticated services/products whose financial background is not readily easy to comprehend and verify`,
value: "medium",
},
{
label:
- "Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations)" as TranslatedString,
+ i18n.str`Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations)`,
value: "high-offshore",
},
{
label:
- "Complex structures in particular by using a domiciliary company with fiduciary shareholders in a non-transparent jurisdiction, without comprehensible reason or for the purpose of short-term asset placement" as TranslatedString,
+ i18n.str`Complex structures in particular by using a domiciliary company with fiduciary shareholders in a non-transparent jurisdiction, without comprehensible reason or for the purpose of short-term asset placement`,
value: "high-structure",
},
{
label:
- "The customer or beneficial owner of the assets has a large number of accounts with pass-through transactions (pass-through accounts)" as TranslatedString,
+ i18n.str`The customer or beneficial owner of the assets has a large number of accounts with pass-through transactions (pass-through accounts)`,
value: "high-accounts",
},
{
label:
- "Complex services/products whose financial background can’t be understood or verified with considerable effort" as TranslatedString,
+ i18n.str`Complex services/products whose financial background can’t be understood or verified with considerable effort`,
value: "high-service",
},
{
label:
- "Frequent transactions with increased risks" as TranslatedString,
+ i18n.str`Frequent transactions with increased risks`,
value: "high-freq-tx",
},
],
@@ -483,32 +497,32 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
- before: "g) Criteria defined by the member" as TranslatedString,
+ properties: {
+ before: i18n.str`g) Criteria defined by the member`,
fields: [
{
type: "text",
- props: {
- label: "Criteria definition" as TranslatedString,
+ properties: {
+ label: i18n.str`Criteria definition`,
name: "evaluation.custom.definition",
},
},
{
type: "choiceStacked",
- props: {
- label: "Risk level" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk level`,
name: "evaluation.custom.risk",
choices: [
{
- label: "Low" as TranslatedString,
+ label: i18n.str`Low`,
value: "low",
},
{
- label: "Medium" as TranslatedString,
+ label: i18n.str`Medium`,
value: "medium",
},
{
- label: "High" as TranslatedString,
+ label: i18n.str`High`,
value: "high",
},
],
@@ -519,28 +533,28 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "caption",
- props: {
+ properties: {
label:
- "Overall assessment of the business relationship" as TranslatedString,
+ i18n.str`Overall assessment of the business relationship`,
},
},
{
type: "group",
- props: {
+ properties: {
before:
- "A business relationship is classified as increased risk if:" as TranslatedString,
+ i18n.str`A business relationship is classified as increased risk if:`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "Business relationship with PEP pursuant to numeral 1 (no exception possible)" as TranslatedString,
+ i18n.str`Business relationship with PEP pursuant to numeral 1 (no exception possible)`,
before: create(ChevronRightIcon, { class: "h-6 w-6" }),
},
},
{
type: "caption",
- props: {
+ properties: {
label:
'Relationship with a person from a "high risk" or non-cooperative country according to numeral 2 (no exceptions possible)' as TranslatedString,
before: create(ChevronRightIcon, { class: "h-6 w-6" }),
@@ -548,9 +562,9 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "caption",
- props: {
+ properties: {
label:
- "Min. one criterion pursuant to numeral 3 was assessed with risk 2 or min. two criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased)" as TranslatedString,
+ i18n.str`Min. one criterion pursuant to numeral 3 was assessed with risk 2 or min. two criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased)`,
before: create(ChevronRightIcon, { class: "h-6 w-6" }),
},
},
@@ -559,70 +573,70 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Justification for differing risk assessment" as TranslatedString,
+ i18n.str`Justification for differing risk assessment`,
name: "evaluation.overall.justification",
},
},
{
type: "choiceStacked",
- props: {
- label: "Risk classified" as TranslatedString,
+ properties: {
+ label: i18n.str`Risk classified`,
name: "evaluation.overall.risk",
choices: [
{
label:
- "Business relationship _without_ increased risk" as TranslatedString,
+ i18n.str`Business relationship _without_ increased risk`,
value: "without",
},
{
label:
- "Business relationship __with__ increased risk" as TranslatedString,
+ i18n.str`Business relationship __with__ increased risk`,
value: "with",
},
],
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
label:
- "The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
+ i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
name: "evaluation.when",
pattern: "dd/MM/yyyy",
- // placeholder: "dd/MM/yyyy" as TranslatedString,
+ // placeholder: i18n.str`dd/MM/yyyy`,
},
},
],
},
{
title:
- "Criteria for identification of increased risk transactions (transaction monitoring)" as TranslatedString,
+ i18n.str`Criteria for identification of increased risk transactions (transaction monitoring)`,
fields: [
{
type: "group",
- props: {
- before: "Criteria" as TranslatedString,
+ properties: {
+ before: i18n.str`Criteria`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
- "Classification as as increased risk is compulsory if" as TranslatedString,
+ i18n.str`Classification as as increased risk is compulsory if`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-6 h-6" }),
label:
- "Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner" as TranslatedString,
+ i18n.str`Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-6 h-6" }),
label:
'Money and asset transfers ("money transfer") whereby a single transaction or multiple transactions which appear to be related reach or exceed the amount of CHF 5,000.-' as TranslatedString,
@@ -630,7 +644,7 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-6 h-6" }),
label:
'Payments from or to a country that is considered to be "high risk" or non-cooperative by the FATF and for which increased diligence is required' as TranslatedString,
@@ -641,66 +655,66 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
{
type: "group",
- props: {
+ properties: {
before:
- "Additional criteria defined by the member" as TranslatedString,
+ i18n.str`Additional criteria defined by the member`,
fields: [
{
type: "caption",
- props: {
+ properties: {
before: create(ArrowRightIcon, { class: "w-6 h-6" }),
label:
- "All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions" as TranslatedString,
+ i18n.str`All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions`,
},
},
{
type: "textArea",
- props: {
- label: "Description" as TranslatedString,
+ properties: {
+ label: i18n.str`Description`,
name: "criteria.additional",
},
},
{
type: "group",
- props: {
+ properties: {
before:
- "Possible criteria (Art. 59 para. 2 SRO Regulations)" as TranslatedString,
+ i18n.str`Possible criteria (Art. 59 para. 2 SRO Regulations)`,
fields: [
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
- "the amount of inflowing and outflowing assets" as TranslatedString,
+ i18n.str`the amount of inflowing and outflowing assets`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
- "type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual)" as TranslatedString,
+ i18n.str`type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual)`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
- "type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual)" as TranslatedString,
+ i18n.str`type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual)`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
- "description of expected transaction patterns which the client notify the member of (considerable variance would be unusual)" as TranslatedString,
+ i18n.str`description of expected transaction patterns which the client notify the member of (considerable variance would be unusual)`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
'The country of origin or destination of payments, especially in the case of payments from or to a country considered by the FATF as "high risk" or non-cooperative' as TranslatedString,
@@ -714,10 +728,10 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
behavior: function formBehavior(
- v: Partial<Form902_4.Form>,
+ // v: Partial<Form902_4.Form>,
): FormState<Form902_4.Form> {
return {
};
diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts
index d695c94b4..e66a4f94d 100644
--- a/packages/aml-backoffice-ui/src/forms/902_5e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts
@@ -1,98 +1,111 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm, currencyList } from "./index.js";
+/*
+ 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 type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_5.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
- title: "Customer Profile" as TranslatedString,
+ title: i18n.str`Customer Profile`,
description:
- "The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party." as TranslatedString,
+ i18n.str`The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party.`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "customer",
- label: "Customer" as TranslatedString,
- help: "Pursuant Identification Form (VQF doc. No. 902.1) numeral 1" as TranslatedString,
+ label: i18n.str`Customer`,
+ help: i18n.str`Pursuant Identification Form (VQF doc. No. 902.1) numeral 1`,
},
},
],
},
{
- title: "Business activity" as TranslatedString,
+ title: i18n.str`Business activity`,
fields: [
{
type: "textArea",
- props: {
- label: "Profession, business activities" as TranslatedString,
+ properties: {
+ label: i18n.str`Profession, business activities`,
name: "businessActivity",
- help: "former, current, potentially planned" as TranslatedString,
+ help: i18n.str`former, current, potentially planned`,
},
},
],
},
{
- title: "Financial circumstances" as TranslatedString,
+ title: i18n.str`Financial circumstances`,
fields: [
{
type: "textArea",
- props: {
- label: "Income and assets, liabilities" as TranslatedString,
+ properties: {
+ label: i18n.str`Income and assets, liabilities`,
name: "financial",
- help: "estimated" as TranslatedString,
+ help: i18n.str`estimated`,
},
},
],
},
{
- title: "Origin of the deposited assets involved" as TranslatedString,
+ title: i18n.str`Origin of the deposited assets involved`,
fields: [
{
type: "text",
- props: {
- label: "Nature" as TranslatedString,
+ properties: {
+ label: i18n.str`Nature`,
name: "originOfAssets.nature",
- help: "nature of the involved assets" as TranslatedString,
+ help: i18n.str`nature of the involved assets`,
},
},
{
type: "selectOne",
- props: {
+ properties: {
name: "originOfAssets.currency",
- label: "Currency" as TranslatedString,
- choices: currencyList,
+ label: i18n.str`Currency`,
+ choices: ["change me"],
},
},
{
type: "integer",
- props: {
- label: "Amount" as TranslatedString,
+ properties: {
+ label: i18n.str`Amount`,
name: "originOfAssets.amount",
},
},
{
type: "choiceStacked",
- props: {
- label: "Category" as TranslatedString,
+ properties: {
+ label: i18n.str`Category`,
name: "originOfAssets.category",
choices: [
{
- label: "Savings" as TranslatedString,
+ label: i18n.str`Savings`,
value: "savings",
},
{
- label: "Own business operations" as TranslatedString,
+ label: i18n.str`Own business operations`,
value: "own-business",
},
{
- label: "Inheritance" as TranslatedString,
+ label: i18n.str`Inheritance`,
value: "inheritance",
},
{
- label: "Other, what?" as TranslatedString,
+ label: i18n.str`Other, what?`,
value: "other",
},
],
@@ -100,17 +113,17 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_5.Form> => ({
},
{
type: "text",
- props: {
- label: "Other category" as TranslatedString,
+ properties: {
+ label: i18n.str`Other category`,
name: "originOfAssets.categoryOther",
required: true,
},
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Detailed description of the origins/economical background of the assets involved in the business relationship" as TranslatedString,
+ i18n.str`Detailed description of the origins/economical background of the assets involved in the business relationship`,
name: "originOfAssets.details",
},
},
@@ -118,110 +131,110 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_5.Form> => ({
},
{
title:
- "Nature and purpose of the business relationship" as TranslatedString,
+ i18n.str`Nature and purpose of the business relationship`,
fields: [
{
type: "textArea",
- props: {
- label: "Purpose of the business relationship" as TranslatedString,
+ properties: {
+ label: i18n.str`Purpose of the business relationship`,
name: "nature.purpose",
- help: "nature of the involved assets" as TranslatedString,
+ help: i18n.str`nature of the involved assets`,
},
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Information on the planned development of the business relationship and the assets" as TranslatedString,
+ i18n.str`Information on the planned development of the business relationship and the assets`,
name: "nature.plan",
},
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Especially in the case of cash or money and asset transfer transactions with regular customers: Details on usual business volume, Information on the beneficiaries, (Full name, address, bank account)" as TranslatedString,
+ i18n.str`Especially in the case of cash or money and asset transfer transactions with regular customers: Details on usual business volume, Information on the beneficiaries, (Full name, address, bank account)`,
name: "nature.cashOrMoneyTransfer",
},
},
],
},
{
- title: "Relationship with third parties" as TranslatedString,
+ title: i18n.str`Relationship with third parties`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
label:
- "Relation of the customer to the beneficial owner involved in the business relationship" as TranslatedString,
+ i18n.str`Relation of the customer to the beneficial owner involved in the business relationship`,
name: "relations.beneficialOwners",
},
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Relation of the customer to the controlling persons involved in the business relationship" as TranslatedString,
+ i18n.str`Relation of the customer to the controlling persons involved in the business relationship`,
name: "relations.controllingPersons",
},
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Relation of the customer to the authorized signatories involved in the business relationship" as TranslatedString,
+ i18n.str`Relation of the customer to the authorized signatories involved in the business relationship`,
name: "relations.authorizedSignatories",
},
},
{
type: "textArea",
- props: {
+ properties: {
label:
- "Relation of the customer to other persons involved in the business relationship" as TranslatedString,
+ i18n.str`Relation of the customer to other persons involved in the business relationship`,
name: "relations.otherPersons",
},
},
{
type: "textArea",
- props: {
- label: "Relation to other AMLA-Files" as TranslatedString,
+ properties: {
+ label: i18n.str`Relation to other AMLA-Files`,
name: "relations.withOtherAmlaFiles",
},
},
{
type: "textArea",
- props: {
- label: "Introducer / agents / references" as TranslatedString,
+ properties: {
+ label: i18n.str`Introducer / agents / references`,
name: "relations.references",
},
},
],
},
{
- title: "Further information" as TranslatedString,
+ title: i18n.str`Further information`,
fields: [
{
type: "textArea",
- props: {
- label: "Other relevant information" as TranslatedString,
+ properties: {
+ label: i18n.str`Other relevant information`,
name: "furtherInformation",
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_5.Form>,
- ): FormState<Form902_5.Form> {
- return {
- originOfAssets: {
- categoryOther: {
- hidden: v.originOfAssets?.category !== "other",
- },
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_5.Form>,
+ // ): FormState<Form902_5.Form> {
+ // return {
+ // originOfAssets: {
+ // categoryOther: {
+ // hidden: v.originOfAssets?.category !== "other",
+ // },
+ // },
+ // };
+ // },
});
namespace Form902_5 {
diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts
index e52531bcb..297ec86b1 100644
--- a/packages/aml-backoffice-ui/src/forms/902_9e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts
@@ -1,71 +1,85 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
-import { FlexibleForm } from "./index.js";
+/*
+ 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 } from "@gnu-taler/taler-util";
+import { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (current: BaseForm): FlexibleForm<Form902_9.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) =>({
design: [
{
title:
- "Declaration of identity of the beneficial owner" as TranslatedString,
+ i18n.str`Declaration of identity of the beneficial owner`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
- label: "Contracting partner" as TranslatedString,
+ label: i18n.str`Contracting partner`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
- "The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below" as TranslatedString,
+ i18n.str`The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below`,
},
},
{
type: "array",
- props: {
- label: "Persons" as TranslatedString,
+ properties: {
+ label: i18n.str`Persons`,
labelField: "surname",
name: "persons",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "surname",
- label: "Surname(s)" as TranslatedString,
+ label: i18n.str`Surname(s)`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "firstName",
- label: "First name(s)" as TranslatedString,
+ label: i18n.str`First name(s)`,
},
},
{
- type: "date",
- props: {
+ type: "absoluteTime",
+ properties: {
name: "dateOfBirth",
- label: "Date of birth" as TranslatedString,
+ label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
- // help: "format 'dd/MM/yyyy'" as TranslatedString,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
- label: "Nationality" as TranslatedString,
+ label: i18n.str`Nationality`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
- label: "Actual address of domicile" as TranslatedString,
+ label: i18n.str`Actual address of domicile`,
},
},
],
@@ -73,31 +87,31 @@ export const v1 = (current: BaseForm): FlexibleForm<Form902_9.Form> => ({
},
{
type: "caption",
- props: {
+ properties: {
label:
- "The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein" as TranslatedString,
+ i18n.str`The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
- label: "Signature" as TranslatedString,
+ label: i18n.str`Signature`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
- "It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)" as TranslatedString,
+ i18n.str`It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)`,
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
behavior: function formBehavior(
- v: Partial<Form902_9.Form>,
+ // v: Partial<Form902_9.Form>,
): FormState<Form902_9.Form> {
return {
};
diff --git a/packages/aml-backoffice-ui/src/forms/icons.tsx b/packages/aml-backoffice-ui/src/forms/icons.tsx
new file mode 100644
index 000000000..8bd369c4f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/icons.tsx
@@ -0,0 +1,25 @@
+/*
+ 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 { h } from "preact";
+
+export const ChevronRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
+</svg>
+
+
+export const ArrowRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
+</svg>
diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts
index 74407ed82..b32978c29 100644
--- a/packages/aml-backoffice-ui/src/forms/index.ts
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -1,145 +1,208 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { DoubleColumnForm } from "../handlers/forms.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
-export interface FlexibleForm<T extends object> {
- design: DoubleColumnForm;
- behavior?: (form: Partial<T>) => FormState<T>;
-}
+ 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.
-export const languageList = [
+ 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 type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { FormMetadata } from "../context/ui-forms.js";
+import { v1 as simplest } from "./simplest.js";
+
+const languages = (i18n: InternationalizationAPI) => [
{
- label: "Mandarin Chinese" as TranslatedString,
+ label: i18n.str`Mandarin Chinese`,
value: "cmn",
},
{
- label: "Spanish" as TranslatedString,
+ label: i18n.str`Spanish`,
value: "spa",
},
{
- label: "English" as TranslatedString,
+ label: i18n.str`English`,
value: "eng",
},
{
- label: "Hindi" as TranslatedString,
+ label: i18n.str`Hindi`,
value: "hin",
},
{
- label: "Portuguese" as TranslatedString,
+ label: i18n.str`Portuguese`,
value: "por",
},
{
- label: "Bengali" as TranslatedString,
+ label: i18n.str`Bengali`,
value: "ben",
},
{
- label: "Russian" as TranslatedString,
+ label: i18n.str`Russian`,
value: "rus",
},
{
- label: "Japanese" as TranslatedString,
+ label: i18n.str`Japanese`,
value: "jpn",
},
{
- label: "Yue" as TranslatedString,
+ label: i18n.str`Yue`,
value: "yue",
},
{
- label: "Vietnamese" as TranslatedString,
+ label: i18n.str`Vietnamese`,
value: "vie",
},
{
- label: "Turkish" as TranslatedString,
+ label: i18n.str`Turkish`,
value: "tur",
},
{
- label: "Wu" as TranslatedString,
+ label: i18n.str`Wu`,
value: "wuu",
},
{
- label: "Marathi" as TranslatedString,
+ label: i18n.str`Marathi`,
value: "mar",
},
{
- label: "Telugu" as TranslatedString,
+ label: i18n.str`Telugu`,
value: "ten",
},
{
- label: "Korean" as TranslatedString,
+ label: i18n.str`Korean`,
value: "kor",
},
{
- label: "French" as TranslatedString,
+ label: i18n.str`French`,
value: "fra",
},
{
- label: "Tamil" as TranslatedString,
+ label: i18n.str`Tamil`,
value: "tam",
},
{
- label: "Egyptian Arabic" as TranslatedString,
+ label: i18n.str`Egyptian Arabic`,
value: "arz",
},
{
- label: "Standard German" as TranslatedString,
+ label: i18n.str`Standard German`,
value: "deu",
},
{
- label: "Urdu" as TranslatedString,
+ label: i18n.str`Urdu`,
value: "urd",
},
{
- label: "Javanese" as TranslatedString,
+ label: i18n.str`Javanese`,
value: "jav",
},
{
- label: "Punjabi" as TranslatedString,
+ label: i18n.str`Punjabi`,
value: "pan",
},
{
- label: "Italian" as TranslatedString,
+ label: i18n.str`Italian`,
value: "ita",
},
{
- label: "Gujarati" as TranslatedString,
+ label: i18n.str`Gujarati`,
value: "guj",
},
{
- label: "Iranian Persian" as TranslatedString,
+ label: i18n.str`Iranian Persian`,
value: "pes",
},
{
- label: "Bhojpuri" as TranslatedString,
+ label: i18n.str`Bhojpuri`,
value: "bho",
},
{
- label: "Hausa" as TranslatedString,
+ label: i18n.str`Hausa`,
value: "hau",
},
];
-export const currencyList = [
+
+
+const forms: (i18n: InternationalizationAPI) => Array<FormMetadata> = (i18n) => [
+ {
+ label: i18n.str`Simple comment`,
+ id: "simple_comment",
+ version: 1,
+ config: simplest(i18n),
+ // }, {
+ // label: i18n.str`Identification form`,
+ // id: "902.1e",
+ // version: 1,
+ // config: form_902_1e_v1(i18n),
+ // }, {
+ // label: i18n.str`Operational legal entity or partnership`,
+ // id: "902.11e",
+ // version: 1,
+ // config: form_902_11e_v1(i18n),
+ // }, {
+ // label: i18n.str`Foundations`,
+ // id: "902.12e",
+ // version: 1,
+ // config: form_902_12e_v1(i18n),
+ // }, {
+ // label: i18n.str`Declaration for trusts`,
+ // id: "902.13e",
+ // version: 1,
+ // config: form_902_13e_v1(i18n),
+ // }, {
+ // label: i18n.str`Information on life insurance policies`,
+ // id: "902.15e",
+ // version: 1,
+ // config: form_902_15e_v1(i18n),
+ // }, {
+ // label: i18n.str`Declaration of beneficial owner`,
+ // id: "902.9e",
+ // version: 1,
+ // config: form_902_9e_v1(i18n),
+ // }, {
+ // label: i18n.str`Customer profile`,
+ // id: "902.5e",
+ // version: 1,
+ // config: form_902_5e_v1(i18n),
+ // }, {
+ // label: i18n.str`Risk profile`,
+ // id: "902.4e",
+ // version: 1,
+ // config: form_902_4e_v1(i18n),
+ },
+];
+
+
+const currencies = (i18n: InternationalizationAPI) => [
{
- label: "United States dollar" as TranslatedString,
+ label: i18n.str`United States dollar`,
value: "usd",
},
{
- label: "Euro" as TranslatedString,
+ label: i18n.str`Euro`,
value: "eur",
},
{
- label: "Swiss franc" as TranslatedString,
+ label: i18n.str`Swiss franc`,
value: "chf",
},
{
- label: "Argentine peso" as TranslatedString,
+ label: i18n.str`Argentine peso`,
value: "ars",
},
{
- label: "Mexican peso" as TranslatedString,
+ label: i18n.str`Mexican peso`,
value: "mxn",
},
{
- label: "Brazilian real" as TranslatedString,
+ label: i18n.str`Brazilian real`,
value: "brl",
},
];
+
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index 778d20b75..d32c759cb 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -1,89 +1,92 @@
-import {
- AbsoluteTime,
- AmountJson,
- Amounts,
- TranslatedString,
-} from "@gnu-taler/taler-util";
-import { FormState } from "../handlers/FormProvider.js";
-import { DoubleColumnFormSection } from "../handlers/forms.js";
-import { BaseForm } from "../pages/AntiMoneyLaunderingForm.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
-import { AmlExchangeBackend } from "../types.js";
-import { FlexibleForm } from "./index.js";
-import { amlStateConverter } from "../pages/ShowConsolidated.js";
+ 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.
-export const v1 = (current: BaseForm): FlexibleForm<Simplest.Form> => ({
+ 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 type {
+ InternationalizationAPI
+} from "@gnu-taler/web-util/browser";
+import { BaseForm, DoubleColumnForm, DoubleColumnFormSection, UIHandlerId } from "../context/ui-forms.js";
+
+export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
design: [
{
- title: "Simple form" as TranslatedString,
+ title: i18n.str`Simple form`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
+ id: ".comment" as UIHandlerId,
name: "comment",
- label: "Comments" as TranslatedString,
+ label: i18n.str`Comments`,
},
},
],
},
- resolutionSection(current),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Simplest.Form>,
- ): FormState<Simplest.Form> {
- return {
- comment: {
- help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as TranslatedString
- },
- threshold: {
- disabled: v.state === AmlExchangeBackend.AmlState.frozen,
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Simplest.Form>,
+ // ): FormState<Simplest.Form> {
+ // return {
+ // comment: {
+ // help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as TranslatedString,
+ // },
+ // threshold: {
+ // disabled: v.state === TalerExchangeApi.AmlState.frozen,
+ // },
+ // };
+ // },
});
-export namespace Simplest {
- export interface Form extends BaseForm {
- comment: string;
- }
-}
-
-export function resolutionSection(current: BaseForm): DoubleColumnFormSection {
+export function resolutionSection(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormSection {
return {
- title: "Resolution" as TranslatedString,
- description: `Current state is ${amlStateConverter.toStringUI(
- current.state,
- )} and threshold at ${Amounts.stringifyValue(
- current.threshold,
- )}` as TranslatedString,
+ title: i18n.str`Resolution`,
fields: [
{
type: "choiceHorizontal",
- props: {
+ properties: {
+ id: ".state" as UIHandlerId,
name: "state",
- label: "New state" as TranslatedString,
- converter: amlStateConverter,
+ label: i18n.str`New state`,
+ converterId: "TalerExchangeApi.AmlState",
choices: [
{
- value: AmlExchangeBackend.AmlState.frozen,
- label: "Frozen" as TranslatedString,
+ value: "frozen",
+ label: i18n.str`Frozen`,
},
{
- value: AmlExchangeBackend.AmlState.pending,
- label: "Pending" as TranslatedString,
+ value: "pending",
+ label: i18n.str`Pending`,
},
{
- value: AmlExchangeBackend.AmlState.normal,
- label: "Normal" as TranslatedString,
+ value: "normal",
+ label: i18n.str`Normal`,
},
],
},
},
{
type: "amount",
- props: {
+ properties: {
+ id: ".threshold" as UIHandlerId,
+ currency: "USD",
name: "threshold",
- label: "New threshold" as TranslatedString,
+ label: i18n.str`New threshold`,
},
},
],
diff --git a/packages/aml-backoffice-ui/src/handlers/Calendar.tsx b/packages/aml-backoffice-ui/src/handlers/Calendar.tsx
deleted file mode 100644
index 9da6e1757..000000000
--- a/packages/aml-backoffice-ui/src/handlers/Calendar.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { AbsoluteTime } from "@gnu-taler/taler-util"
-import { useTranslationContext } from "@gnu-taler/web-util/browser"
-import { add as dateAdd, sub as dateSub, eachDayOfInterval, endOfMonth, endOfWeek, format, getMonth, getYear, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek } from "date-fns"
-import { VNode, h } from "preact"
-import { useState } from "preact/hooks"
-
-export function Calendar({ value, onChange }: { value: AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void }): VNode {
- const today = startOfDay(new Date())
- const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value))
- const [showingDate, setShowingDate] = useState(selected)
- const month = getMonth(showingDate)
- const year = getYear(showingDate)
-
- const start = startOfWeek(startOfMonth(showingDate));
- const end = endOfWeek(endOfMonth(showingDate));
- const daysInMonth = eachDayOfInterval({ start, end });
- const { i18n } = useTranslationContext()
- const monthNames = [
- i18n.str`January`,
- i18n.str`February`,
- i18n.str`March`,
- i18n.str`April`,
- i18n.str`May`,
- i18n.str`June`,
- i18n.str`July`,
- i18n.str`August`,
- i18n.str`September`,
- i18n.str`October`,
- i18n.str`November`,
- i18n.str`December`,
- ]
- return <div class="text-center p-2">
- <div class="flex items-center text-gray-900">
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
- onClick={() => {
- setShowingDate(dateSub(showingDate, { years: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Previous year`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
- </svg>
- </button>
- <div class="flex-auto text-sm font-semibold">{year}</div>
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
- onClick={() => {
- setShowingDate(dateAdd(showingDate, { years: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Next year`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
- </svg>
- </button>
- </div>
- <div class="mt-4 flex items-center text-gray-900">
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
- onClick={() => {
- setShowingDate(dateSub(showingDate, { months: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Previous month`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
- </svg>
- </button>
- <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
- onClick={() => {
- setShowingDate(dateAdd(showingDate, { months: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Next month`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
- </svg>
- </button>
- </div>
- <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
- <div>M</div>
- <div>T</div>
- <div>W</div>
- <div>T</div>
- <div>F</div>
- <div>S</div>
- <div>S</div>
- </div>
- <div class="isolate mt-2 grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm shadow ring-1 ring-gray-200">
- {daysInMonth.map(current => (
- <button type="button"
- data-month={isSameMonth(current, showingDate)}
- data-today={isSameDay(current, today)}
- data-selected={isSameDay(current, selected)}
- onClick={() => {
- onChange(AbsoluteTime.fromStampMs(current.getTime()))
- }}
- class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5
- data-[month=false]:bg-gray-100 data-[month=true]:bg-white
- data-[today=true]:font-semibold
- data-[month=true]:text-gray-900
- data-[today=true]:bg-red-300 data-[today=true]:hover:bg-red-200
- data-[month=true]:hover:bg-gray-200
- data-[selected=true]:!bg-blue-400 data-[selected=true]:hover:!bg-blue-300 ">
- <time dateTime={format(current, "yyyy-MM-dd")}
- class="mx-auto flex h-7 w-7 items-center justify-center rounded-full">
- {format(current, "dd")}
- </time>
- </button>
- ))}
- </div>
- </div>
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/Caption.tsx b/packages/aml-backoffice-ui/src/handlers/Caption.tsx
deleted file mode 100644
index 8facddec3..000000000
--- a/packages/aml-backoffice-ui/src/handlers/Caption.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import {
- LabelWithTooltipMaybeRequired
-} from "./InputLine.js";
-
-interface Props {
- label: TranslatedString;
- tooltip?: TranslatedString;
- help?: TranslatedString;
- before?: VNode;
- after?: VNode;
-}
-
-export function Caption({ before, after, label, tooltip, help }: Props): VNode {
- return (
- <div class="sm:col-span-6 flex">
- {before !== undefined && (
- <span class="pointer-events-none flex items-center pr-2">{before}</span>
- )}
- <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
- {after !== undefined && (
- <span class="pointer-events-none flex items-center pl-2">{after}</span>
- )}
- {help && (
- <p class="mt-2 text-sm text-gray-500" id="email-description">
- {help}
- </p>
- )}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/Dialog.tsx b/packages/aml-backoffice-ui/src/handlers/Dialog.tsx
deleted file mode 100644
index f9899e94e..000000000
--- a/packages/aml-backoffice-ui/src/handlers/Dialog.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ComponentChildren, VNode, h } from "preact";
-
-export function Dialog({ children, onClose }: { onClose?: () => void; children: ComponentChildren }): VNode {
- return <div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true" onClick={onClose}>
- <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
-
- <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
- <div class="flex min-h-full items-center justify-center p-4 text-center sm:items-center sm:p-0">
- <div class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
- {children}
- </div>
- </div>
- </div>
- </div>
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
deleted file mode 100644
index b3cb7a972..000000000
--- a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import {
- AbsoluteTime,
- AmountJson,
- TranslatedString,
-} from "@gnu-taler/taler-util";
-import { ComponentChildren, VNode, createContext, h } from "preact";
-import {
- MutableRef,
- StateUpdater,
- useState
-} from "preact/hooks";
-
-export interface FormType<T extends object> {
- value: MutableRef<Partial<T>>;
- initialValue?: Partial<T>;
- readOnly?: boolean;
- onUpdate?: StateUpdater<T>;
- computeFormState?: (v: T) => FormState<T>;
-}
-
-//@ts-ignore
-export const FormContext = createContext<FormType<any>>({});
-
-/**
- * Map of {[field]:BehaviorResult}
- * for every field of type
- * - any native (string, number, etc...)
- * - absoluteTime
- * - amountJson
- *
- * except for:
- * - object => recurse into
- * - array => behavior result and element field
- */
-export type FormState<T extends object> = {
- [field in keyof T]?: T[field] extends AbsoluteTime
- ? BehaviorResult
- : T[field] extends AmountJson
- ? BehaviorResult
- : T[field] extends Array<infer P extends object>
- ? InputArrayFieldState<P>
- : T[field] extends (object)
- ? FormState<T[field]>
- : BehaviorResult;
-};
-
-export type BehaviorResult = Partial<InputFieldState> & FieldUIOptions
-
-export interface InputFieldState {
- /* should show the error */
- error?: TranslatedString;
- /* should not allow to edit */
- readonly: boolean;
- /* should show as disable */
- disabled: boolean;
- /* should not show */
- hidden: boolean;
-}
-
-export interface IconAddon {
- type: "icon";
- icon: VNode;
-}
-export interface ButtonAddon {
- type: "button";
- onClick: () => void;
- children: ComponentChildren;
-}
-export interface TextAddon {
- type: "text";
- text: TranslatedString;
-}
-export type Addon = IconAddon | ButtonAddon | TextAddon;
-
-export interface StringConverter<T> {
- toStringUI: (v?: T) => string;
- fromStringUI: (v?: string) => T;
-}
-
-type FieldUIOptions = {
- placeholder?: TranslatedString;
- tooltip?: TranslatedString;
- help?: TranslatedString;
- required?: boolean;
-}
-
-export interface UIFormProps<T extends object, K extends keyof T> extends FieldUIOptions {
- name: K;
- label: TranslatedString;
- before?: Addon;
- after?: Addon;
- converter?: StringConverter<T[K]>;
-}
-
-export interface InputArrayFieldState<P extends object> extends BehaviorResult {
- elements?: FormState<P>[];
-}
-
-export function FormProvider<T extends object>({
- children,
- initialValue,
- onUpdate: notify,
- onSubmit,
- computeFormState,
- readOnly,
-}: {
- initialValue?: Partial<T>;
- onUpdate?: (v: Partial<T>) => void;
- onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
- computeFormState?: (v: Partial<T>) => FormState<T>;
- readOnly?: boolean;
- children: ComponentChildren;
-}): VNode {
-
- const [state, setState] = useState<Partial<T>>(initialValue ?? {});
- const value = { current: state };
- const onUpdate = (v: typeof state) => {
- setState(v);
- if (notify) notify(v);
- };
- return (
- <FormContext.Provider
- value={{ initialValue, value, onUpdate, computeFormState, readOnly }}
- >
- <form
- onSubmit={(e) => {
- e.preventDefault();
- //@ts-ignore
- if (onSubmit)
- onSubmit(
- value.current,
- !computeFormState ? undefined : computeFormState(value.current),
- );
- }}
- >
- {children}
- </form>
- </FormContext.Provider>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/Group.tsx b/packages/aml-backoffice-ui/src/handlers/Group.tsx
deleted file mode 100644
index 0645f6d97..000000000
--- a/packages/aml-backoffice-ui/src/handlers/Group.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
-
-interface Props {
- before?: TranslatedString;
- after?: TranslatedString;
- tooltipBefore?: TranslatedString;
- tooltipAfter?: TranslatedString;
- fields: UIFormField[];
-}
-
-export function Group({
- before,
- after,
- tooltipAfter,
- tooltipBefore,
- fields,
-}: Props): VNode {
- return (
- <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50">
- <div class="pb-4">
- {before && (
- <LabelWithTooltipMaybeRequired
- label={before}
- tooltip={tooltipBefore}
- />
- )}
- </div>
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6">
- <RenderAllFieldsByUiConfig fields={fields} />
- </div>
- <div class="pt-4">
- {after && (
- <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} />
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputAmount.tsx b/packages/aml-backoffice-ui/src/handlers/InputAmount.tsx
deleted file mode 100644
index 29ec43525..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputAmount.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { InputLine } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputAmount<T extends object, K extends keyof T>(
- props: { currency?: string } & UIFormProps<T, K>,
-): VNode {
- const { value } = useField<T, K>(props.name);
- const currency =
- !value || !(value as any).currency
- ? props.currency
- : (value as any).currency;
- return (
- <InputLine<T, K>
- type="text"
- before={{
- type: "text",
- text: currency as TranslatedString,
- }}
- converter={{
- //@ts-ignore
- fromStringUI: (v): AmountJson => {
-
- return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency);
- },
- //@ts-ignore
- toStringUI: (v: AmountJson) => {
- return v === undefined ? "" : Amounts.stringifyValue(v);
- },
- }}
- {...props}
- />
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputArray.tsx b/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
deleted file mode 100644
index 38c399e66..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { FormProvider, UIFormProps } from "./FormProvider.js";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
-import { useField } from "./useField.js";
-
-function Option({
- label,
- disabled,
- isFirst,
- isLast,
- isSelected,
- onClick,
-}: {
- label: TranslatedString;
- isFirst?: boolean;
- isLast?: boolean;
- isSelected?: boolean;
- disabled?: boolean;
- onClick: () => void;
-}): VNode {
- let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey";
- if (isFirst) {
- clazz += " rounded-tl-md rounded-tr-md ";
- }
- if (isLast) {
- clazz += " rounded-bl-md rounded-br-md ";
- }
- if (isSelected) {
- clazz += " z-10 border-indigo-200 bg-indigo-50 ";
- } else {
- clazz += " border-gray-200";
- }
- if (disabled) {
- clazz +=
- " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray";
- } else {
- clazz += " cursor-pointer";
- }
- return (
- <label class={clazz}>
- <input
- type="radio"
- name="privacy-setting"
- checked={isSelected}
- disabled={disabled}
- onClick={onClick}
- class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600"
- aria-labelledby="privacy-setting-0-label"
- aria-describedby="privacy-setting-0-description"
- />
- <span class="ml-3 flex flex-col">
- <span
- id="privacy-setting-0-label"
- disabled
- class="block text-sm font-medium"
- >
- {label}
- </span>
- {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */}
- {/* <span
- id="privacy-setting-0-description"
- class="block text-sm"
- >
- This project would be available to anyone who has the link
- </span> */}
- </span>
- </label>
- );
-}
-
-export function InputArray<T extends object, K extends keyof T>(
- props: {
- fields: UIFormField[];
- labelField: string;
- } & UIFormProps<T, K>,
-): VNode {
- const { fields, labelField, name, label, required, tooltip } = props;
- const { value, onChange, state } = useField<T, K>(name);
- const list = (value ?? []) as Array<Record<string, string | undefined>>;
- const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
- const selected =
- selectedIndex === undefined ? undefined : list[selectedIndex];
-
- return (
- <div class="sm:col-span-6">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
-
- <div class="-space-y-px rounded-md bg-white ">
- {list.map((v, idx) => {
- return (
- <Option
- label={v[labelField] as TranslatedString}
- isSelected={selectedIndex === idx}
- isLast={idx === list.length - 1}
- disabled={selectedIndex !== undefined && selectedIndex !== idx}
- isFirst={idx === 0}
- onClick={() => {
- setSelected(selectedIndex === idx ? undefined : idx);
- }}
- />
- );
- })}
- {!state.disabled &&
- <div class="pt-2">
- <Option
- label={"Add..." as TranslatedString}
- isSelected={selectedIndex === list.length}
- isLast
- isFirst
- disabled={
- selectedIndex !== undefined && selectedIndex !== list.length
- }
- onClick={() => {
- setSelected(
- selectedIndex === list.length ? undefined : list.length,
- );
- }}
- />
- </div>
- }
- </div>
- {selectedIndex !== undefined && (
- /**
- * This form provider act as a substate of the parent form
- * Consider creating an InnerFormProvider since not every feature is expected
- */
- <FormProvider
- initialValue={selected}
- readOnly={state.disabled}
- computeFormState={(v) => {
- // current state is ignored
- // the state is defined by the parent form
-
- // elements should be present in the state object since this is expected to be an array
- //@ts-ignore
- return state.elements[selectedIndex];
- }}
- onSubmit={(v) => {
- const newValue = [...list];
- newValue.splice(selectedIndex, 1, v);
- onChange(newValue as T[K]);
- setSelected(undefined);
- }}
- onUpdate={(v) => {
- const newValue = [...list];
- newValue.splice(selectedIndex, 1, v);
- onChange(newValue as T[K]);
- }}
- >
- <div class="px-4 py-6">
- <div class="grid grid-cols-1 gap-y-8 ">
- <RenderAllFieldsByUiConfig fields={fields} />
- </div>
- </div>
- </FormProvider>
- )}
- {selectedIndex !== undefined && (
- <div class="flex items-center pt-3">
- <div class="flex-auto">
- {selected !== undefined && (
- <button
- type="button"
- onClick={() => {
- const newValue = [...list];
- newValue.splice(selectedIndex, 1);
- onChange(newValue as T[K]);
- setSelected(undefined);
- }}
- class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
- >
- Remove
- </button>
- )}
- </div>
- </div>
- )}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx b/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
deleted file mode 100644
index 312e014c5..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export interface Choice<V> {
- label: TranslatedString;
- value: V;
-}
-
-export function InputChoiceHorizontal<T extends object, K extends keyof T>(
- props: {
- choices: Choice<T[K]>[];
- } & UIFormProps<T, K>,
-): VNode {
- const {
- choices,
- name,
- label,
- tooltip,
- help,
- placeholder,
- required,
- before,
- after,
- converter,
- } = props;
- const { value, onChange, state, isDirty } = useField<T, K>(name);
- if (state.hidden) {
- return <Fragment />;
- }
-
- return (
- <div class="sm:col-span-6">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
- <fieldset class="mt-2">
- <div class="isolate inline-flex rounded-md shadow-sm">
- {choices.map((choice, idx) => {
- const isFirst = idx === 0;
- const isLast = idx === choices.length - 1;
- let clazz =
- "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10";
- if (choice.value === value) {
- clazz +=
- " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500";
- } else {
- clazz += " hover:bg-gray-100 border-gray-300";
- }
- if (isFirst) {
- clazz += " rounded-l-md";
- } else {
- clazz += " -ml-px";
- }
- if (isLast) {
- clazz += " rounded-r-md";
- }
- return (
- <button
- type="button"
- disabled={state.disabled}
- class={clazz}
- onClick={(e) => {
- onChange(
- (value === choice.value ? undefined : choice.value) as T[K],
- );
- }}
- >
- {(!converter
- ? (choice.value as string)
- : converter?.toStringUI(choice.value)) ?? ""}
- </button>
- );
- })}
- </div>
- </fieldset>
- {help && (
- <p class="mt-2 text-sm text-gray-500" id="email-description">
- {help}
- </p>
- )}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx b/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
deleted file mode 100644
index 48d367ff2..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export interface Choice<V> {
- label: TranslatedString;
- description?: TranslatedString;
- value: V;
-}
-
-export function InputChoiceStacked<T extends object, K extends keyof T>(
- props: {
- choices: Choice<T[K]>[];
- } & UIFormProps<T, K>,
-): VNode {
- const {
- choices,
- name,
- label,
- tooltip,
- help,
- placeholder,
- required,
- before,
- after,
- converter,
- } = props;
- const { value, onChange, state, isDirty } = useField<T, K>(name);
- if (state.hidden) {
- return <Fragment />;
- }
-
- return (
- <div class="sm:col-span-6">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
- <fieldset class="mt-2">
- <div class="space-y-4">
- {choices.map((choice) => {
- // const currentValue = !converter
- // ? choice.value
- // : converter.fromStringUI(choice.value) ?? "";
-
- let clazz =
- "border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between";
- if (choice.value === value) {
- clazz +=
- " border-transparent border-indigo-600 ring-2 ring-indigo-600";
- } else {
- clazz += " border-gray-300";
- }
-
- return (
- <label class={clazz}>
- <input
- type="radio"
- name="server-size"
- // defaultValue={choice.value}
- disabled={state.disabled}
- value={
- (!converter
- ? (choice.value as string)
- : converter?.toStringUI(choice.value)) ?? ""
- }
- onClick={(e) => {
- onChange(
- (value === choice.value
- ? undefined
- : choice.value) as T[K],
- );
- }}
- class="sr-only"
- aria-labelledby="server-size-0-label"
- aria-describedby="server-size-0-description-0 server-size-0-description-1"
- />
- <span class="flex items-center">
- <span class="flex flex-col text-sm">
- <span
- id="server-size-0-label"
- class="font-medium text-gray-900"
- >
- {choice.label}
- </span>
- {choice.description !== undefined && (
- <span
- id="server-size-0-description-0"
- class="text-gray-500"
- >
- <span class="block sm:inline">
- {choice.description}
- </span>
- </span>
- )}
- </span>
- </span>
- </label>
- );
- })}
- </div>
- </fieldset>
- {help && (
- <p class="mt-2 text-sm text-gray-500" id="email-description">
- {help}
- </p>
- )}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
deleted file mode 100644
index 794bfd7a2..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { AbsoluteTime } from "@gnu-taler/taler-util";
-import { InputLine } from "./InputLine.js";
-import { Fragment, VNode, h } from "preact";
-import { format, parse } from "date-fns";
-import { Dialog } from "./Dialog.js";
-import { Calendar } from "./Calendar.js";
-import { useState } from "preact/hooks";
-import { useField } from "./useField.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputDate<T extends object, K extends keyof T>(
- props: { pattern?: string } & UIFormProps<T, K>,
-): VNode {
- const pattern = props.pattern ?? "dd/MM/yyyy";
- const [open, setOpen] = useState(false)
- const { value, onChange } = useField<T, K>(props.name);
- return (
- <Fragment>
-
- <InputLine<T, K>
- type="text"
- after={{
- type: "button",
- onClick: () => {
- setOpen(true)
- },
- // icon: <CalendarIcon class="h-6 w-6" />,
- children: (
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
- </svg>)
- }}
- converter={{
- //@ts-ignore
- fromStringUI: (v): AbsoluteTime | undefined => {
- if (!v) return undefined;
- try {
- const t_ms = parse(v, pattern, Date.now()).getTime();
- return AbsoluteTime.fromMilliseconds(t_ms);
- } catch (e) {
- return undefined;
- }
- },
- //@ts-ignore
- toStringUI: (v: AbsoluteTime | undefined) => {
- return !v || !v.t_ms
- ? undefined
- : v.t_ms === "never"
- ? "never"
- : format(v.t_ms, pattern);
- },
- }}
- {...props}
- />
- {open &&
- <Dialog onClose={() => setOpen(false)}>
- <Calendar value={value as AbsoluteTime ?? AbsoluteTime.now()}
- onChange={(v) => {
- onChange(v as any)
- setOpen(false)
- }} />
- </Dialog>
- }
- </Fragment>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputFile.tsx b/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
deleted file mode 100644
index bc460f370..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { Fragment, VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { UIFormProps, BehaviorResult } from "./FormProvider.js";
-
-export function InputFile<T extends object, K extends keyof T>(
- props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
-): VNode {
- const {
- name,
- label,
- placeholder,
- tooltip,
- required,
- help: propsHelp,
- maxBites,
- accept,
- } = props;
- const { value, onChange, state } = useField<T, K>(name);
- const help = propsHelp ?? state.help
- if (state.hidden) {
- return <div />;
- }
- return (
- <div class="col-span-full">
- <LabelWithTooltipMaybeRequired
- label={label}
- tooltip={tooltip}
- required={required}
- />
- {!value || !(value as string).startsWith("data:image/") ? (
- <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
- <div class="text-center">
- <svg
- class="mx-auto h-12 w-12 text-gray-300"
- viewBox="0 0 24 24"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z"
- clip-rule="evenodd"
- />
- </svg>
- {!state.disabled &&
- <div class="my-2 flex text-sm leading-6 text-gray-600">
- <label
- for="file-upload"
- class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
- >
- <span>Upload a file</span>
- <input
- id="file-upload"
- name="file-upload"
- type="file"
- class="sr-only"
- accept={accept}
- onChange={(e) => {
- const f: FileList | null = e.currentTarget.files;
- if (!f || f.length != 1) {
- return onChange(undefined!);
- }
- if (f[0].size > maxBites) {
- return onChange(undefined!);
- }
- return f[0].arrayBuffer().then((b) => {
- const b64 = window.btoa(
- new Uint8Array(b).reduce(
- (data, byte) => data + String.fromCharCode(byte),
- "",
- ),
- );
- return onChange(`data:${f[0].type};base64,${b64}` as any);
- });
- }}
- />
- </label>
- {/* <p class="pl-1">or drag and drop</p> */}
- </div>
- }
- </div>
- </div>
- ) : (
- <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
- <img
- src={value as string}
- class=" h-24 w-full object-cover relative"
- />
-
- {!state.disabled &&
- <div
- class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
- onClick={() => {
- onChange(undefined!);
- }}
- >
- Clear
- </div>
- }
- </div>
- )}
- {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputInteger.tsx b/packages/aml-backoffice-ui/src/handlers/InputInteger.tsx
deleted file mode 100644
index a6a02ad43..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputInteger.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { VNode, h } from "preact";
-import { InputLine } from "./InputLine.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputInteger<T extends object, K extends keyof T>(
- props: UIFormProps<T, K>,
-): VNode {
- return (
- <InputLine
- type="number"
- converter={{
- //@ts-ignore
- fromStringUI: (v): number => {
- return !v ? 0 : Number.parseInt(v, 10);
- },
- //@ts-ignore
- toStringUI: (v?: number): string => {
- return v === undefined ? "" : String(v);
- },
- }}
- {...props}
- />
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputLine.tsx b/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
deleted file mode 100644
index 890bb54cb..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
+++ /dev/null
@@ -1,268 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useField } from "./useField.js";
-import { useEffect, useState } from "preact/hooks";
-import { UIFormProps } from "./FormProvider.js";
-
-//@ts-ignore
-const TooltipIcon = (
- <svg
- class="w-5 h-5"
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path
- fill-rule="evenodd"
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
- clip-rule="evenodd"
- />
- </svg>
-);
-
-export function LabelWithTooltipMaybeRequired({
- label,
- required,
- tooltip,
-}: {
- label: TranslatedString;
- required?: boolean;
- tooltip?: TranslatedString;
-}): VNode {
- const Label = (
- <Fragment>
- <div class="flex justify-between">
- <label
- htmlFor="email"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- {label}
- </label>
- </div>
- </Fragment>
- );
- const WithTooltip = tooltip ? (
- <div class="relative flex flex-grow items-stretch focus-within:z-10">
- {Label}
- <span class="relative flex items-center group pl-2">
- {TooltipIcon}
- <div class="absolute bottom-0 flex flex-col items-center mb-6 group-hover:flex">
- <span class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg">
- {tooltip}
- </span>
- <div class="w-3 h-3 -mt-2 rotate-45 bg-black"></div>
- </div>
- </span>
- </div>
- ) : (
- Label
- );
- if (required) {
- return (
- <div class="flex justify-between">
- {WithTooltip}
- <span class="text-sm leading-6 text-red-600">*</span>
- </div>
- );
- }
- return WithTooltip;
-}
-
-function InputWrapper<T extends object, K extends keyof T>({
- children,
- label,
- tooltip,
- before,
- after,
- help,
- error,
- disabled,
- required,
-}: { error?: string; disabled: boolean, children: ComponentChildren } & UIFormProps<T, K>): VNode {
- return (
- <div class="sm:col-span-6">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
- <div class="relative mt-2 flex rounded-md shadow-sm">
- {before &&
- (before.type === "text" ? (
- <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
- {before.text}
- </span>
- ) : before.type === "icon" ? (
- <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
- {before.icon}
- </div>
- ) : before.type === "button" ? (
- <button
- type="button"
- disabled={disabled}
- onClick={before.onClick}
- class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- {before.children}
- </button>
- ) : undefined)}
-
- {children}
-
- {after &&
- (after.type === "text" ? (
- <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
- {after.text}
- </span>
- ) : after.type === "icon" ? (
- <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
- {after.icon}
- </div>
- ) : after.type === "button" ? (
- <button
- type="button"
- disabled={disabled}
- onClick={after.onClick}
- class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- {after.children}
- </button>
- ) : undefined)}
- </div>
- {error && (
- <p class="mt-2 text-sm text-red-600" id="email-error">
- {error}
- </p>
- )}
- {help && (
- <p class="mt-2 text-sm text-gray-500" id="email-description">
- {help}
- </p>
- )}
- </div>
- );
-}
-
-function defaultToString(v: unknown) {
- return v === undefined ? "" : typeof v !== "object" ? String(v) : "";
-}
-function defaultFromString(v: string) {
- return v;
-}
-
-type InputType = "text" | "text-area" | "password" | "email" | "number";
-
-export function InputLine<T extends object, K extends keyof T>(
- props: { type: InputType } & UIFormProps<T, K>,
-): VNode {
- const { name, placeholder, before, after, converter, type } = props;
- const { value, onChange, state, isDirty } = useField<T, K>(name);
-
- const [text, setText] = useState("")
- const fromString: (s: string) => any =
- converter?.fromStringUI ?? defaultFromString;
- const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
-
- useEffect(() => {
- const newValue = toString(value)
- if (newValue) {
- setText(newValue)
- }
- }, [value])
-
- if (state.hidden) return <div />;
-
- let clazz =
- "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200";
- if (before) {
- switch (before.type) {
- case "icon": {
- clazz += " pl-10";
- break;
- }
- case "button": {
- clazz += " rounded-none rounded-r-md ";
- break;
- }
- case "text": {
- clazz += " min-w-0 flex-1 rounded-r-md rounded-none ";
- break;
- }
- }
- }
- if (after) {
- switch (after.type) {
- case "icon": {
- clazz += " pr-10";
- break;
- }
- case "button": {
- clazz += " rounded-none rounded-l-md";
- break;
- }
- case "text": {
- clazz += " min-w-0 flex-1 rounded-l-md rounded-none ";
- break;
- }
- }
- }
- const showError = isDirty && state.error;
- if (showError) {
- clazz +=
- " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500";
- } else {
- clazz +=
- " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600";
- }
-
- if (type === "text-area") {
- return (
- <InputWrapper<T, K>
- {...props}
- help={props.help ?? state.help}
- disabled={state.disabled ?? false}
- error={showError ? state.error : undefined}
- >
- <textarea
- rows={4}
- name={String(name)}
- onChange={(e) => {
- onChange(fromString(e.currentTarget.value));
- }}
- placeholder={placeholder ? placeholder : undefined}
- value={toString(value) ?? ""}
- // defaultValue={toString(value)}
- disabled={state.disabled}
- aria-invalid={showError}
- // aria-describedby="email-error"
- class={clazz}
- />
- </InputWrapper>
- );
- }
-
- return (
- <InputWrapper<T, K> {...props}
- help={props.help ?? state.help}
- disabled={state.disabled ?? false} error={showError ? state.error : undefined}
- >
- <input
- name={String(name)}
- type={type}
- onChange={(e) => {
- setText(e.currentTarget.value)
- }}
- placeholder={placeholder ? placeholder : undefined}
- value={text}
- onBlur={() => {
- onChange(fromString(text));
- }}
- // defaultValue={toString(value)}
- disabled={state.disabled}
- aria-invalid={showError}
- // aria-describedby="email-error"
- class={clazz}
- />
- </InputWrapper>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
deleted file mode 100644
index 06eb91bb3..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import { Fragment, VNode, h } from "preact";
-import { Choice } from "./InputChoiceStacked.js";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { useState } from "preact/hooks";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputSelectMultiple<T extends object, K extends keyof T>(
- props: {
- choices: Choice<T[K]>[];
- unique?: boolean;
- max?: number;
- } & UIFormProps<T, K>,
-): VNode {
- const { name, label, choices, placeholder, tooltip, required, unique, max } =
- props;
- const { value, onChange, state } = useField<T, K>(name);
-
- const [filter, setFilter] = useState<string | undefined>(undefined);
- const regex = new RegExp(`.*${filter}.*`, "i");
- const choiceMap = choices.reduce((prev, curr) => {
- return { ...prev, [curr.value as string]: curr.label };
- }, {} as Record<string, string>);
-
- const list = (value ?? []) as string[];
- const filteredChoices =
- filter === undefined
- ? undefined
- : choices.filter((v) => {
- return regex.test(v.label);
- });
- return (
- <div class="sm:col-span-6">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
- {list.map((v, idx) => {
- return (
- <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600">
- {choiceMap[v]}
- <button
- type="button"
- disabled={state.disabled}
- onClick={() => {
- const newValue = [...list];
- newValue.splice(idx, 1);
- onChange(newValue as T[K]);
- setFilter(undefined);
- }}
- class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
- >
- <span class="sr-only">Remove</span>
- <svg
- viewBox="0 0 14 14"
- class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
- >
- <path d="M4 4l6 6m0-6l-6 6" />
- </svg>
- <span class="absolute -inset-1"></span>
- </button>
- </span>
- );
- })}
-
- {!state.disabled && <div class="relative mt-2">
- <input
- id="combobox"
- type="text"
- value={filter ?? ""}
- onChange={(e) => {
- setFilter(e.currentTarget.value);
- }}
- placeholder={placeholder}
- class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- role="combobox"
- aria-controls="options"
- aria-expanded="false"
- />
- <button
- type="button"
- disabled={state.disabled}
- onClick={() => {
- setFilter(filter === undefined ? "" : undefined);
- }}
- class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
- >
- <svg
- class="h-5 w-5 text-gray-400"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
- clip-rule="evenodd"
- />
- </svg>
- </button>
-
- {filteredChoices !== undefined && (
- <ul
- class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
- id="options"
- role="listbox"
- >
- {filteredChoices.map((v, idx) => {
- return (
- <li
- class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
- id="option-0"
- role="option"
- onClick={() => {
- setFilter(undefined);
- if (unique && list.indexOf(v.value as string) !== -1) {
- return;
- }
- if (max !== undefined && list.length >= max) {
- return;
- }
- const newValue = [...list];
- newValue.splice(0, 0, v.value as string);
- onChange(newValue as T[K]);
- }}
-
- // tabindex="-1"
- >
- {/* <!-- Selected: "font-semibold" --> */}
- <span class="block truncate">{v.label}</span>
-
- {/* <!--
- Checkmark, only display for selected option.
-
- Active: "text-white", Not Active: "text-indigo-600"
- --> */}
- </li>
- );
- })}
-
- {/* <!--
- Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
-
- Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
- --> */}
-
- {/* <!-- More items... --> */}
- </ul>
- )}
- </div>}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx b/packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx
deleted file mode 100644
index 98430306e..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { Fragment, VNode, h } from "preact";
-import { Choice } from "./InputChoiceStacked.js";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { useState } from "preact/hooks";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputSelectOne<T extends object, K extends keyof T>(
- props: {
- choices: Choice<T[K]>[];
- } & UIFormProps<T, K>,
-): VNode {
- const { name, label, choices, placeholder, tooltip, required } = props;
- const { value, onChange } = useField<T, K>(name);
-
- const [filter, setFilter] = useState<string | undefined>(undefined);
- const regex = new RegExp(`.*${filter}.*`, "i");
- const choiceMap = choices.reduce((prev, curr) => {
- return { ...prev, [curr.value as string]: curr.label };
- }, {} as Record<string, string>);
-
- const filteredChoices =
- filter === undefined
- ? undefined
- : choices.filter((v) => {
- return regex.test(v.label);
- });
- return (
- <div class="sm:col-span-6">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
- {value ? (
- <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600">
- {choiceMap[value as string]}
- <button
- type="button"
- onClick={() => {
- onChange(undefined!);
- }}
- class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
- >
- <span class="sr-only">Remove</span>
- <svg
- viewBox="0 0 14 14"
- class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
- >
- <path d="M4 4l6 6m0-6l-6 6" />
- </svg>
- <span class="absolute -inset-1"></span>
- </button>
- </span>
- ) : (
- <div class="relative mt-2">
- <input
- id="combobox"
- type="text"
- value={filter ?? ""}
- onChange={(e) => {
- setFilter(e.currentTarget.value);
- }}
- placeholder={placeholder}
- class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- role="combobox"
- aria-controls="options"
- aria-expanded="false"
- />
- <button
- type="button"
- onClick={() => {
- setFilter(filter === undefined ? "" : undefined);
- }}
- class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
- >
- <svg
- class="h-5 w-5 text-gray-400"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
- clip-rule="evenodd"
- />
- </svg>
- </button>
-
- {filteredChoices !== undefined && (
- <ul
- class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
- id="options"
- role="listbox"
- >
- {filteredChoices.map((v, idx) => {
- return (
- <li
- class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
- id="option-0"
- role="option"
- onClick={() => {
- setFilter(undefined);
- onChange(v.value as T[K]);
- }}
-
- // tabindex="-1"
- >
- {/* <!-- Selected: "font-semibold" --> */}
- <span class="block truncate">{v.label}</span>
-
- {/* <!--
- Checkmark, only display for selected option.
-
- Active: "text-white", Not Active: "text-indigo-600"
- --> */}
- </li>
- );
- })}
-
- {/* <!--
- Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
-
- Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
- --> */}
-
- {/* <!-- More items... --> */}
- </ul>
- )}
- </div>
- )}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputText.tsx b/packages/aml-backoffice-ui/src/handlers/InputText.tsx
deleted file mode 100644
index 7ad36b737..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputText.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { VNode, h } from "preact";
-import { InputLine } from "./InputLine.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputText<T extends object, K extends keyof T>(
- props: UIFormProps<T, K>,
-): VNode {
- return <InputLine type="text" {...props} />;
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx b/packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx
deleted file mode 100644
index 6b76d8329..000000000
--- a/packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { VNode, h } from "preact";
-import { InputLine } from "./InputLine.js";
-import { UIFormProps } from "./FormProvider.js";
-
-export function InputTextArea<T extends object, K extends keyof T>(
- props: UIFormProps<T, K>,
-): VNode {
- return <InputLine type="text-area" {...props} />;
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/forms.ts b/packages/aml-backoffice-ui/src/handlers/forms.ts
deleted file mode 100644
index 2c90a69ed..000000000
--- a/packages/aml-backoffice-ui/src/handlers/forms.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { InputText } from "./InputText.js";
-import { InputDate } from "./InputDate.js";
-import { InputInteger } from "./InputInteger.js";
-import { h as create, Fragment, VNode } from "preact";
-import { InputChoiceStacked } from "./InputChoiceStacked.js";
-import { InputArray } from "./InputArray.js";
-import { InputSelectMultiple } from "./InputSelectMultiple.js";
-import { InputTextArea } from "./InputTextArea.js";
-import { InputFile } from "./InputFile.js";
-import { Caption } from "./Caption.js";
-import { Group } from "./Group.js";
-import { InputSelectOne } from "./InputSelectOne.js";
-import { FormProvider } from "./FormProvider.js";
-import { InputLine } from "./InputLine.js";
-import { InputAmount } from "./InputAmount.js";
-import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
-
-export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
-
-export type DoubleColumnFormSection = {
- title: TranslatedString;
- description?: TranslatedString;
- fields: UIFormField[];
-};
-
-/**
- * Constrain the type with the ui props
- */
-type FieldType<T extends object = any, K extends keyof T = any> = {
- group: Parameters<typeof Group>[0];
- caption: Parameters<typeof Caption>[0];
- array: Parameters<typeof InputArray<T, K>>[0];
- file: Parameters<typeof InputFile<T, K>>[0];
- selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
- selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
- text: Parameters<typeof InputText<T, K>>[0];
- textArea: Parameters<typeof InputTextArea<T, K>>[0];
- choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
- choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
- date: Parameters<typeof InputDate<T, K>>[0];
- integer: Parameters<typeof InputInteger<T, K>>[0];
- amount: Parameters<typeof InputAmount<T, K>>[0];
-};
-
-/**
- * List all the form fields so typescript can type-check the form instance
- */
-export type UIFormField =
- | { type: "group"; props: FieldType["group"] }
- | { type: "caption"; props: FieldType["caption"] }
- | { type: "array"; props: FieldType["array"] }
- | { type: "file"; props: FieldType["file"] }
- | { type: "amount"; props: FieldType["amount"] }
- | { type: "selectOne"; props: FieldType["selectOne"] }
- | { type: "selectMultiple"; props: FieldType["selectMultiple"] }
- | { type: "text"; props: FieldType["text"] }
- | { type: "textArea"; props: FieldType["textArea"] }
- | { type: "choiceStacked"; props: FieldType["choiceStacked"] }
- | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] }
- | { type: "integer"; props: FieldType["integer"] }
- | { type: "date"; props: FieldType["date"] };
-
-type FieldComponentFunction<key extends keyof FieldType> = (
- props: FieldType[key],
-) => VNode;
-
-type UIFormFieldMap = {
- [key in keyof FieldType]: FieldComponentFunction<key>;
-};
-
-/**
- * Maps input type with component implementation
- */
-const UIFormConfiguration: UIFormFieldMap = {
- group: Group,
- caption: Caption,
- //@ts-ignore
- array: InputArray,
- text: InputText,
- //@ts-ignore
- file: InputFile,
- textArea: InputTextArea,
- //@ts-ignore
- date: InputDate,
- //@ts-ignore
- choiceStacked: InputChoiceStacked,
- //@ts-ignore
- choiceHorizontal: InputChoiceHorizontal,
- integer: InputInteger,
- //@ts-ignore
- selectOne: InputSelectOne,
- //@ts-ignore
- selectMultiple: InputSelectMultiple,
- //@ts-ignore
- amount: InputAmount,
-};
-
-export function RenderAllFieldsByUiConfig({
- fields,
-}: {
- fields: UIFormField[];
-}): VNode {
- return create(
- Fragment,
- {},
- fields.map((field, i) => {
- const Component = UIFormConfiguration[
- field.type
- ] as FieldComponentFunction<any>;
- return Component(field.props);
- }),
- );
-}
-
-type FormSet<T extends object> = {
- Provider: typeof FormProvider<T>;
- InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
- InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<
- T,
- K
- >;
-};
-export function createNewForm<T extends object>() {
- const res: FormSet<T> = {
- Provider: FormProvider,
- InputLine: () => InputLine,
- InputChoiceHorizontal: () => InputChoiceHorizontal,
- };
- return {
- Provider: res.Provider,
- InputLine: res.InputLine(),
- InputChoiceHorizontal: res.InputChoiceHorizontal(),
- };
-}
diff --git a/packages/aml-backoffice-ui/src/handlers/useField.ts b/packages/aml-backoffice-ui/src/handlers/useField.ts
deleted file mode 100644
index 651778628..000000000
--- a/packages/aml-backoffice-ui/src/handlers/useField.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useContext, useState } from "preact/compat";
-import { BehaviorResult, FormContext, InputFieldState } from "./FormProvider.js";
-
-export interface InputFieldHandler<Type> {
- value: Type;
- onChange: (s: Type) => void;
- state: BehaviorResult;
- isDirty: boolean;
-}
-
-export function useField<T extends object, K extends keyof T>(
- name: K,
-): InputFieldHandler<T[K]> {
- const {
- initialValue,
- value: formValue,
- computeFormState,
- onUpdate: notifyUpdate,
- readOnly: readOnlyForm,
- } = useContext(FormContext);
-
- type P = typeof name;
- type V = T[P];
- const formState = computeFormState ? computeFormState(formValue.current) : {};
-
- const fieldValue = readField(formValue.current, String(name)) as V;
- // console.log("USE FIELD", String(name), formValue.current, fieldValue);
- const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue);
- const fieldState =
- readField<Partial<BehaviorResult>>(formState, String(name)) ?? {};
-
- //compute default state
- const state = {
- disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
- readonly: readOnlyForm ? true : (fieldState.readonly ?? false),
- hidden: fieldState.hidden ?? false,
- error: fieldState.error,
- help: fieldState.help,
- elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
- };
-
- function onChange(value: V): void {
- setCurrentValue(value);
- formValue.current = setValueDeeper(
- formValue.current,
- String(name).split("."),
- value,
- );
- if (notifyUpdate) {
- notifyUpdate(formValue.current);
- }
- }
-
- return {
- value: fieldValue,
- onChange,
- isDirty: currentValue !== undefined,
- state,
- };
-}
-
-/**
- * read the field of an object an support accessing it using '.'
- *
- * @param object
- * @param name
- * @returns
- */
-function readField<T>(
- object: any,
- name: string,
- debug?: boolean,
-): T | undefined {
- return name.split(".").reduce((prev, current) => {
- if (debug) {
- console.log(
- "READ",
- name,
- prev,
- current,
- prev ? prev[current] : undefined,
- );
- }
- return prev ? prev[current] : undefined;
- }, object);
-}
-
-function setValueDeeper(object: any, names: string[], value: any): any {
- if (names.length === 0) return value;
- const [head, ...rest] = names;
- if (object === undefined) {
- return { [head]: setValueDeeper({}, rest, value) };
- }
- return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
new file mode 100644
index 000000000..e14e29819
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -0,0 +1,138 @@
+/*
+ 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,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+import { UIField } from "@gnu-taler/web-util/browser";
+
+// 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
+ : T[k] extends TalerExchangeApi.AmlState
+ ? UIField
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TalerExchangeApi.AmlState
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : T[k] extends TalerExchangeApi.AmlState
+ ? 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: RecursivePartial<FormValues<T>>,
+ updateForm: (d: RecursivePartial<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 !== undefined ? errors[fieldName] : undefined;
+ function updater(newValue: unknown) {
+ updateForm({ ...form, [fieldName]: newValue });
+ }
+ /**
+ * There is no clear way to know if this object is a custom field
+ * or a group of fields
+ */
+ 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,
+ onChange: updater,
+ state: {},
+ };
+ // @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: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] =
+ useState<RecursivePartial<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
new file mode 100644
index 000000000..1bb73b8fc
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -0,0 +1,163 @@
+/*
+ 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,
+ OperationOk,
+ SigningKey,
+ buildCodecForObject,
+ codecForAbsoluteTime,
+ codecForString,
+ createNewOfficerAccount,
+ decodeCrock,
+ encodeCrock,
+ opFixedSuccess,
+ 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<OperationOk<OfficerId>>;
+}
+interface OfficerLocked {
+ state: "locked";
+ forget: () => OperationOk<void>;
+ tryUnlock: (password: string) => Promise<OperationOk<void>>;
+}
+interface OfficerReady {
+ state: "ready";
+ account: OfficerAccount;
+ forget: () => OperationOk<void>;
+ lock: () => OperationOk<void>;
+}
+
+const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
+const DEV_ACCOUNT_KEY = buildStorageKey(
+ "account-dev",
+ codecForOfficerAccount(),
+);
+
+export function useOfficer(): OfficerState {
+ const {lib:{exchange: api}} = 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 resp = await api.getSeed()
+ const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array();
+
+ const { id, safe, signingKey } = await createNewOfficerAccount(
+ pwd,
+ extraEntropy,
+ );
+ officerStorage.update({
+ account: safe,
+ when: AbsoluteTime.now(),
+ });
+
+ // accountStorage.update({ id, signingKey });
+ const strKey = encodeCrock(signingKey);
+ accountStorage.update({ id, strKey });
+
+ return opFixedSuccess(id)
+ },
+ };
+ }
+
+ if (account === undefined) {
+ return {
+ state: "locked",
+ forget: () => {
+ officerStorage.reset();
+ return opFixedSuccess(undefined)
+ },
+ tryUnlock: async (pwd: string) => {
+ const ac = await unlockOfficerAccount(officer.account, pwd);
+ // accountStorage.update(ac);
+ accountStorage.update({
+ id: ac.id,
+ strKey: encodeCrock(ac.signingKey),
+ });
+ return opFixedSuccess(undefined)
+ },
+ };
+ }
+
+ return {
+ state: "ready",
+ account,
+ lock: () => {
+ accountStorage.reset();
+ return opFixedSuccess(undefined)
+ },
+ forget: () => {
+ officerStorage.reset();
+ accountStorage.reset();
+ return opFixedSuccess(undefined)
+ },
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useSettings.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
index f1610576e..12e85d249 100644
--- a/packages/aml-backoffice-ui/src/hooks/useSettings.ts
+++ b/packages/aml-backoffice-ui/src/hooks/preferences.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
@@ -18,57 +18,68 @@ import {
Codec,
TranslatedString,
buildCodecForObject,
- codecForBoolean,
- codecForNumber,
- codecForString,
- codecOptional
+ codecForBoolean
} from "@gnu-taler/taler-util";
-import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
-interface Settings {
+interface Preferences {
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>()
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
.property("allowInsecurePassword", (codecForBoolean()))
.property("keepSessionAfterReload", (codecForBoolean()))
- .build("Settings");
+ .build("Preferences");
-const defaultSettings: Settings = {
+const defaultPreferences: Preferences = {
allowInsecurePassword: false,
keepSessionAfterReload: false,
};
-const EXCHANGE_SETTINGS_KEY = buildStorageKey(
- "exchange-settings",
- codecForSettings(),
+const PREFERENCES_KEY = buildStorageKey(
+ "exchange-preferences",
+ codecForPreferences(),
);
-
-export function useSettings(): [
- Readonly<Settings>,
- <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+/**
+ * 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(
- EXCHANGE_SETTINGS_KEY,
- defaultSettings,
+ PREFERENCES_KEY,
+ defaultPreferences,
);
- function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ 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
deleted file mode 100644
index 0615c9c99..000000000
--- a/packages/aml-backoffice-ui/src/hooks/useBackend.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-import {
- HttpResponseOk,
- RequestOptions,
- useApiContext,
-} from "@gnu-taler/web-util/browser";
-import { useCallback } from "preact/hooks";
-import { uiSettings } from "../settings.js";
-
-interface useBackendType {
- request: <T>(
- path: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
- fetcher: <T>(args: [string, string]) => Promise<HttpResponseOk<T>>;
- paginatedFetcher: <T>(
- args: [string, number, number, string],
- ) => Promise<HttpResponseOk<T>>;
-}
-export function usePublicBackend(): useBackendType {
- const { request: requestHandler } = useApiContext();
-
- const baseUrl = getInitialBackendBaseURL();
-
- const request = useCallback(
- function requestImpl<T>(
- path: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, path, options);
- },
- [baseUrl],
- );
-
- const fetcher = useCallback(
- function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string, string]): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- talerAmlOfficerSignature
- });
- },
- [baseUrl],
- );
- const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page, size, talerAmlOfficerSignature]: [
- string,
- number,
- number,
- string,
- ]): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- params: { page: page || 1, size },
- talerAmlOfficerSignature,
- });
- },
- [baseUrl],
- );
- return {
- request,
- fetcher,
- paginatedFetcher,
- };
-}
-
-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 as any) !== "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
index dbc6763ba..78574ada4 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
@@ -1,16 +1,30 @@
-
+/*
+ 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 { OfficerAccount, PaytoString, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AmountString, OfficerAccount, PaytoString, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
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)
@@ -34,62 +48,4 @@ export function useCaseDetails(paytoHash: string) {
return undefined;
}
-const example1: TalerExchangeApi.AmlDecisionDetails = {
- aml_history: [
- {
- justification: "Lack of documentation",
- decider_pub: "ASDASDASD",
- decision_time: {
- t_s: Date.now() / 1000,
- },
- new_state: 2,
- new_threshold: "USD:0" as AmountString,
- },
- {
- justification: "Doing a transfer of high amount",
- decider_pub: "ASDASDASD",
- decision_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
- },
- new_state: 1,
- new_threshold: "USD:2000" as AmountString,
- },
- {
- justification: "Account is known to the system",
- decider_pub: "ASDASDASD",
- decision_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
- },
- new_state: 0,
- new_threshold: "USD:100" as AmountString,
- },
- ],
- kyc_attributes: [
- {
- collection_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
- },
- expiration_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
- },
- provider_section: "asdasd",
- attributes: {
- name: "Sebastian",
- },
- },
- {
- collection_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
- },
- expiration_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
- },
- provider_section: "asdasd",
- attributes: {
- creditCard: "12312312312",
- },
- },
- ],
-};
-
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index 81ca2755a..d3a1c1018 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -1,144 +1,115 @@
+/*
+ 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 { useState } from "preact/hooks";
-import {
- HttpResponsePaginated
-} from "@gnu-taler/web-util/browser";
-import { AmlExchangeBackend } from "../types.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AmountString, OfficerAccount, OperationFail, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
+import {
+ OfficerAccount,
+ OperationOk,
+ TalerExchangeApi,
+ TalerExchangeResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
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;
-const PAGE_SIZE = 10;
+export const PAGINATED_LIST_SIZE = 10;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
+
/**
* FIXME: mutate result when balance change (transaction )
* @param account
* @param args
* @returns
*/
-export function useCases(state: AmlExchangeBackend.AmlState) {
+export function useCases(state: TalerExchangeApi.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,
+ TalerExchangeApi.AmlState,
+ string | undefined,
+ ]) {
return await api.getDecisionsByState(officer, state, {
- order: "asc", offset, limit: PAGE_SIZE + 1
- })
+ order: "asc",
+ offset,
+ limit: PAGINATED_LIST_REQUEST,
+ });
}
- const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>(
- !session ? undefined : [session, state, offset],
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getDecisionsByState">,
+ TalerHttpError
+ >(
+ !session ? undefined : [session, state, offset, "getDecisionsByState"],
fetcher,
);
- // const [lastAfter, setLastAfter] = useState<
- // HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError>
- // >({ loading: true });
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- // useEffect(() => {
- // if (afterData) setLastAfter(afterData);
- // }, [afterData]);
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
+}
- // if (afterError) {
- // return afterError.cause;
- // }
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+};
- // if the query returns less that we ask, then we have reach the end or beginning
- const isLastPage =
- data && data.type === "ok" && data.body.records.length <= PAGE_SIZE;
- const isFirstPage = !offset;
+//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[]> {
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
- const pagination = {
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
isLastPage,
isFirstPage,
- loadMore: () => {
- if (isLastPage || data?.type !== "ok") return;
- const list = data.body.records
- setOffset(String(list[list.length - 1].rowid));
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1]);
+ setOffset(id);
},
- reset: () => {
- setOffset(undefined)
+ loadFirst: () => {
+ setOffset(undefined);
},
};
-
- // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts;
- if (!session) {
- return {
- data: {
- type: "fail",
- case: "unauthorized",
- detail: {}
- } as OperationFail<never>
- }
- }
-
- if (data) {
- if (data.type === "fail") {
- return { data }
- }
- const records = isLastPage ? data.body.records : removeLastElement(data.body.records)
- return { data: { type: "ok" as const, body: { records } }, pagination }
- }
- if (error) {
- return error;
- }
- return undefined;
}
-
-const example1: TalerExchangeApi.AmlRecords = {
- records: [
- {
- current_state: 0,
- h_payto: "QWEQWEQWEQWEWQE",
- rowid: 1,
- threshold: "USD 100" as AmountString,
- },
- {
- current_state: 1,
- h_payto: "ASDASDASD",
- rowid: 1,
- threshold: "USD 100" as AmountString,
- },
- {
- current_state: 2,
- h_payto: "ZXCZXCZXCXZC",
- rowid: 1,
- threshold: "USD 1000" as AmountString,
- },
- {
- current_state: 0,
- h_payto: "QWEQWEQWEQWEWQE",
- rowid: 1,
- threshold: "USD 100" as AmountString,
- },
- {
- current_state: 1,
- h_payto: "ASDASDASD",
- rowid: 1,
- threshold: "USD 100" as AmountString,
- },
- {
- current_state: 2,
- h_payto: "ZXCZXCZXCXZC",
- rowid: 1,
- threshold: "USD 1000" as AmountString,
- },
- ].map((e, idx) => {
- e.rowid = idx;
- e.threshold = `${e.threshold}${idx}` as AmountString;
- return e;
- }),
-};
-
-
-function removeLastElement<T>(list: Array<T>): Array<T> {
- if (list.length === 0) {
- return list;
- }
- return list.slice(0, -1)
-} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
deleted file mode 100644
index 64cf79cc9..000000000
--- a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import {
- AbsoluteTime,
- Codec,
- LockedAccount,
- OfficerAccount,
- OfficerId,
- SigningKey,
- buildCodecForObject,
- codecForAbsoluteTime,
- codecForString,
- codecOptional,
- createNewOfficerAccount,
- decodeCrock,
- encodeCrock,
- unlockOfficerAccount,
-} from "@gnu-taler/taler-util";
-import {
- buildStorageKey,
- useLocalStorage,
- useMemoryStorage,
-} from "@gnu-taler/web-util/browser";
-import { useMemo } from "preact/hooks";
-
-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());
-const ACCOUNT_KEY = "account";
-
-export function useOfficer(): OfficerState {
- // 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])
-
-
- // const accountStorage = useMemoryStorage<OfficerAccount>(ACCOUNT_KEY);
- // const account = accountStorage.value;
-
- const officerStorage = useLocalStorage(OFFICER_KEY);
- const officer = officerStorage.value;
-
-
- if (officer === undefined) {
- return {
- state: "not-found",
- create: async (pwd: string) => {
- const { id, safe, signingKey } = await createNewOfficerAccount(pwd);
- 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/i18n/bank.pot b/packages/aml-backoffice-ui/src/i18n/bank.pot
index 66e98976f..39f9de5ce 100644
--- 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
index 203d55343..8148f6a0c 100644
--- 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
index a251e9584..d7a371934 100644
--- 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
index a0aeb8268..3ab0fd1e5 100644
--- 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
index a779bbc49..4f7419eb4 100644
--- 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
index 703d31da1..b7f73d0a2 100644
--- 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
@@ -17,26 +17,26 @@
-->
<!DOCTYPE html>
<html lang="en">
- <head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <meta name="mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-capable" content="yes" />
- <link
- rel="icon"
- href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
- />
- <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
- <title>Exchange Backoffice</title>
- <!-- Optional customization script. -->
- <script src="exchange-backofice-ui-settings.js"></script>
- <!-- Entry point for the SPA. -->
- <script type="module" src="index.js"></script>
- <link rel="stylesheet" href="index.css" />
- </head>
- <body>
- <div id="app"></div>
- </body>
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Exchange Backoffice</title>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="forms.js"></script>
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+</head>
+
+<body>
+ <div id="app"></div>
+</body>
+
</html>
diff --git a/packages/aml-backoffice-ui/src/index.tsx b/packages/aml-backoffice-ui/src/index.tsx
index c2ac4c84b..c6f6b4a8f 100644
--- 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
@@ -18,5 +18,8 @@ import { App } from "./App.js";
import { h, render } from "preact";
const app = document.getElementById("app");
-
-render(<App />, app as any);
+if (!app) {
+ console.error("could not found app element")
+} else {
+ render(<App />, app);
+}
diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts
deleted file mode 100644
index 109cd31d0..000000000
--- a/packages/aml-backoffice-ui/src/pages.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js";
-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
deleted file mode 100644
index 0b055f682..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
+++ /dev/null
@@ -1,104 +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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import {
- AntiMoneyLaunderingForm as TestedComponent,
-} from "./AntiMoneyLaunderingForm.js";
-
-export default {
- title: "aml form",
-};
-
-export const SimpleComment = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "simple_comment",
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const Identification = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.1e",
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const OperationalLegalEntity = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.11e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const Foundations = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.12e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const DelcarationOfTrusts = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.13e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const InformationOnLifeInsurance = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.15e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.9e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const CustomerProfile = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.5e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const RiskProfile = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.4e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
deleted file mode 100644
index 381f00ebb..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import { AbsoluteTime, AmountJson, Amounts, Codec, OperationResult, buildCodecForObject, codecForNumber, codecForString, codecOptional } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h } from "preact";
-import { NiceForm } from "../NiceForm.js";
-import { v1 as form_902_11e_v1 } from "../forms/902_11e.js";
-import { v1 as form_902_12e_v1 } from "../forms/902_12e.js";
-import { v1 as form_902_13e_v1 } from "../forms/902_13e.js";
-import { v1 as form_902_15e_v1 } from "../forms/902_15e.js";
-import { v1 as form_902_1e_v1 } from "../forms/902_1e.js";
-import { v1 as form_902_4e_v1 } from "../forms/902_4e.js";
-import { v1 as form_902_5e_v1 } from "../forms/902_5e.js";
-import { v1 as form_902_9e_v1 } from "../forms/902_9e.js";
-import { v1 as simplest } from "../forms/simplest.js";
-import { Pages } from "../pages.js";
-import { AmlExchangeBackend } from "../types.js";
-import { useExchangeApiContext } from "../context/config.js";
-import { FlexibleForm } from "../forms/index.js";
-
-export function AntiMoneyLaunderingForm({ account, formId, onSubmit }: { account: string, formId: string, onSubmit: (justification: Justification, state: AmlExchangeBackend.AmlState, threshold: AmountJson) => Promise<void>; }) {
- const { i18n } = useTranslationContext()
- const theForm = allForms.find((v) => v.id === formId)
- if (!theForm) {
- return <div>form with id {formId} not found</div>
- }
-
- const { config } = useExchangeApiContext()
-
- const initial = {
- when: AbsoluteTime.now(),
- state: AmlExchangeBackend.AmlState.pending,
- threshold: Amounts.zeroOfCurrency(config.currency),
- };
- return (
- <NiceForm
- initial={initial}
- form={theForm.impl(initial)}
- onUpdate={() => { }}
- onSubmit={(formValue) => {
- if (formValue.state === undefined || formValue.threshold === undefined) return;
- const st = formValue.state;
- const amount = formValue.threshold;
-
- const justification: Justification = {
- id: theForm.id,
- label: theForm.label,
- version: theForm.version,
- value: formValue
- }
-
- onSubmit(justification, st, amount);
- }}
- >
- <div class="mt-6 flex items-center justify-end gap-x-6">
- <a
- href={Pages.account.url({ account })}
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </button>
- </div>
- </NiceForm>
- );
-}
-
-export interface BaseForm {
- state: AmlExchangeBackend.AmlState;
- threshold: AmountJson;
-}
-
-const DocumentDuplicateIcon = <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
-</svg>
-
-
-export type FormMetadata<T extends BaseForm> = {
- label: string,
- id: string,
- version: number,
- icon: h.JSX.Element,
- impl: (current: BaseForm) => FlexibleForm<T>
-}
-
-export type Justification<T = any> = {
- // form values
- value: T;
-} & Omit<Omit<FormMetadata<any>, "icon">, "impl">
-
-export function stringifyJustification(j: Justification): string {
- return JSON.stringify(j)
-}
-
-
-type SimpleFormMetadata = {
- version?: number,
- id?: string,
-}
-
-export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
- buildCodecForObject<SimpleFormMetadata>()
- .property("id", codecOptional(codecForString()))
- .property("version", codecOptional(codecForNumber()))
- .build("SimpleFormMetadata");
-
-type ParseJustificationFail =
- "not-json" |
- "id-not-found" |
- "form-not-found" |
- "version-not-found";
-
-export function parseJustification(s: string, listOfAllKnownForms: FormMetadata<any>[]): OperationResult<{ justification: Justification, metadata: FormMetadata<any> }, ParseJustificationFail> {
- try {
- const justification = JSON.parse(s)
- const info = codecForSimpleFormMetadata().decode(justification)
- if (!info.id) {
- return {
- type: "fail",
- case: "id-not-found",
- detail: {} as any
- }
- }
- if (!info.version) {
- return {
- type: "fail",
- case: "version-not-found",
- detail: {} as any
- }
- }
- const found = listOfAllKnownForms.find((f) => {
- return f.id === info.id && f.version === info.version
- })
- if (!found) {
- return {
- type: "fail",
- case: "form-not-found",
- detail: {} as any
- }
- }
- return {
- type: "ok",
- body: {
- justification, metadata: found
- }
- }
- } catch (e) {
- return {
- type: "fail",
- case: "not-json",
- detail: {} as any
- }
- }
-
-}
-
-export const allForms: Array<FormMetadata<BaseForm>> = [
- {
- label: "Simple comment",
- id: "simple_comment",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: simplest,
- },
- {
- label: "Identification form",
- id: "902.1e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_1e_v1,
- },
- {
- label: "Operational legal entity or partnership",
- id: "902.11e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_11e_v1,
- },
- {
- label: "Foundations",
- id: "902.12e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_12e_v1,
- },
- {
- label: "Declaration for trusts",
- id: "902.13e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_13e_v1,
- },
- {
- label: "Information on life insurance policies",
- id: "902.15e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_15e_v1,
- },
- {
- label: "Declaration of beneficial owner",
- id: "902.9e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_9e_v1,
- },
- {
- label: "Customer profile",
- id: "902.5e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_5e_v1,
- },
- {
- label: "Risk profile",
- id: "902.4e",
- version: 1,
- icon: DocumentDuplicateIcon,
- impl: form_902_4e_v1,
- },
-];
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 9d6126ce3..1ad8c9453 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -1,39 +1,73 @@
+/*
+ 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,
AmountJson,
Amounts,
+ Codec,
+ HttpStatusCode,
+ OperationFail,
+ OperationOk,
TalerError,
+ TalerErrorDetail,
+ TalerExchangeApi,
TranslatedString,
- assertUnreachable
+ assertUnreachable,
+ buildCodecForObject,
+ codecForNumber,
+ codecForString,
+ codecOptional,
} from "@gnu-taler/taler-util";
-import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ DefaultForm,
+ ErrorLoading,
+ FlexibleForm,
+ InternationalizationAPI,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { NiceForm } from "../NiceForm.js";
+import { privatePages } from "../Routing.js";
import { useCaseDetails } from "../hooks/useCaseDetails.js";
-import { Pages } from "../pages.js";
-import { AmlExchangeBackend } from "../types.js";
-import { FormMetadata, Justification, allForms, parseJustification } from "./AntiMoneyLaunderingForm.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
+import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js";
-export type AmlEvent = AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent;
+export type AmlEvent =
+ | AmlFormEvent
+ | AmlFormEventError
+ | KycCollectionEvent
+ | KycExpirationEvent;
type AmlFormEvent = {
type: "aml-form";
when: AbsoluteTime;
title: TranslatedString;
justification: Justification;
- metadata: FormMetadata<any>;
- state: AmlExchangeBackend.AmlState;
+ metadata: FormMetadata;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
};
type AmlFormEventError = {
type: "aml-form-error";
when: AbsoluteTime;
title: TranslatedString;
- justification: undefined,
- metadata: undefined,
- state: AmlExchangeBackend.AmlState;
+ justification: undefined;
+ metadata: undefined;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
};
type KycCollectionEvent = {
@@ -56,30 +90,41 @@ function selectSooner(a: WithTime, b: WithTime) {
return AbsoluteTime.cmp(a.when, b.when);
}
-function titleForJustification(op: ReturnType<typeof parseJustification>): TranslatedString {
+function titleForJustification(
+ op: ReturnType<typeof parseJustification>,
+ i18n: InternationalizationAPI,
+): TranslatedString {
if (op.type === "ok") {
return op.body.justification.label as TranslatedString;
}
switch (op.case) {
- case "not-json": return "error: the justification is not a form" as TranslatedString
- case "id-not-found": return "error: justification form's id not found" as TranslatedString
- case "version-not-found": return "error: justification form's version not found" as TranslatedString
- case "form-not-found": return `error: justification form not found` as TranslatedString
+ case "not-json":
+ return i18n.str`error: the justification is not a form`;
+ case "id-not-found":
+ return i18n.str`error: justification form's id not found`;
+ case "version-not-found":
+ return i18n.str`error: justification form's version not found`;
+ case "form-not-found":
+ return i18n.str`error: justification form not found`;
+ default: {
+ assertUnreachable(op.case);
+ }
}
- assertUnreachable(op.case)
}
export function getEventsFromAmlHistory(
- aml: AmlExchangeBackend.AmlDecisionDetail[],
- kyc: AmlExchangeBackend.KycDetail[],
+ aml: TalerExchangeApi.AmlDecisionDetail[],
+ kyc: TalerExchangeApi.KycDetail[],
+ i18n: InternationalizationAPI,
+ forms: FormMetadata[],
): AmlEvent[] {
const ae: AmlEvent[] = aml.map((a) => {
- const just = parseJustification(a.justification, allForms)
+ const just = parseJustification(a.justification, forms);
return {
type: just.type === "ok" ? "aml-form" : "aml-form-error",
state: a.new_state,
threshold: Amounts.parseOrThrow(a.new_threshold),
- title: titleForJustification(just),
+ title: titleForJustification(just, i18n),
metadata: just.type === "ok" ? just.body.metadata : undefined,
justification: just.type === "ok" ? just.body.justification : undefined,
when: {
@@ -93,14 +138,14 @@ export function getEventsFromAmlHistory(
const ke = kyc.reduce((prev, k) => {
prev.push({
type: "kyc-collection",
- title: "collection" as TranslatedString,
+ title: i18n.str`collection`,
when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
values: !k.attributes ? {} : k.attributes,
provider: k.provider_section,
});
prev.push({
type: "kyc-expiration",
- title: "expiration" as TranslatedString,
+ title: i18n.str`expiration`,
when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
fields: !k.attributes ? [] : Object.keys(k.attributes),
});
@@ -111,103 +156,114 @@ export function getEventsFromAmlHistory(
export function CaseDetails({ account }: { account: string }) {
const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
- const [showForm, setShowForm] = useState<{ justification: Justification, metadata: FormMetadata<any> }>()
+ const [showForm, setShowForm] = useState<{
+ justification: Justification;
+ metadata: FormMetadata;
+ }>();
const { i18n } = useTranslationContext();
- const details = useCaseDetails(account)
+ const details = useCaseDetails(account);
+ const {forms} = useUiFormsContext()
+
if (!details) {
- return <Loading />
+ return <Loading />;
}
if (details instanceof TalerError) {
- return <ErrorLoading error={details} />
+ return <ErrorLoading error={details} />;
}
if (details.type === "fail") {
switch (details.case) {
- case "unauthorized":
- case "officer-not-found":
- case "officer-disabled": return <div />
- default: assertUnreachable(details)
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return <div />;
+ default:
+ assertUnreachable(details);
}
}
- const { aml_history, kyc_attributes } = details.body
+ const { aml_history, kyc_attributes } = details.body;
- const events = getEventsFromAmlHistory(aml_history, kyc_attributes);
+ const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, forms);
if (showForm !== undefined) {
- return <NiceForm
- readOnly={true}
- initial={showForm.justification.value}
- form={showForm.metadata.impl(showForm.justification.value)}
- >
- <div class="mt-6 flex items-center justify-end gap-x-6">
- <button
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={() => {
- setShowForm(undefined)
- }}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- </div>
-
- </NiceForm>
+ return (
+ <DefaultForm
+ readOnly={true}
+ initial={showForm.justification.value}
+ form={showForm.metadata as any} // FIXME: HERE
+ >
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <button
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={() => {
+ setShowForm(undefined);
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
+ </DefaultForm>
+ );
}
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>
+ <i18n.Translate>New AML form</i18n.Translate>
</a>
<header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
<h1 class="text-base font-semibold leading-7 text-black">
<i18n.Translate>
- Case history for account <span title={account}>{account.substring(0, 16)}...</span>
+ Case history for account{" "}
+ <span title={account}>{account.substring(0, 16)}...</span>
</i18n.Translate>
</h1>
</header>
- <ShowTimeline history={events} onSelect={(e) => {
- switch (e.type) {
- case "aml-form": {
- const { justification, metadata } = e
- setShowForm({ justification, metadata })
- break;
- }
- case "kyc-collection":
- case "kyc-expiration": {
- setSelected(e.when);
- break;
+ <ShowTimeline
+ history={events}
+ onSelect={(e) => {
+ switch (e.type) {
+ case "aml-form": {
+ const { justification, metadata } = e;
+ setShowForm({ justification, metadata });
+ break;
+ }
+ case "kyc-collection":
+ case "kyc-expiration": {
+ setSelected(e.when);
+ break;
+ }
+ case "aml-form-error":
}
- case "aml-form-error":
- }
- }} />
+ }}
+ />
{/* {selected && <ShowEventDetails event={selected} />} */}
{selected && <ShowConsolidated history={events} until={selected} />}
</div>
);
}
-function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode {
+function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode {
switch (state) {
- case AmlExchangeBackend.AmlState.normal: {
+ case TalerExchangeApi.AmlState.normal: {
return (
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
Normal
</span>
);
}
- case AmlExchangeBackend.AmlState.pending: {
+ case TalerExchangeApi.AmlState.pending: {
return (
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
Pending
</span>
);
}
- case AmlExchangeBackend.AmlState.frozen: {
+ case TalerExchangeApi.AmlState.frozen: {
return (
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
Frozen
@@ -215,93 +271,194 @@ function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode
);
}
}
- assertUnreachable(state)
+ assertUnreachable(state);
}
-function ShowTimeline({ history, onSelect }: { onSelect: (e: AmlEvent) => void, history: AmlEvent[] }): VNode {
- return <div class="flow-root">
- <ul role="list">
- {history.map((e, idx) => {
- const isLast = history.length - 1 === idx;
- return (
- <li
- data-ok={e.type !== "aml-form-error"}
- class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer"
- onClick={() => {
- onSelect(e);
- }}
- >
- <div class="relative pb-6">
- {!isLast ? (
- <span
- class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
- aria-hidden="true"
- ></span>
- ) : undefined}
- <div class="relative flex space-x-3">
- {(() => {
- switch (e.type) {
- case "aml-form-error":
- case "aml-form": {
- return <div>
- <AmlStateBadge state={e.state} />
- <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
- {e.threshold.currency}{" "}
- {Amounts.stringifyValue(e.threshold)}
- </span>
- </div>
- }
- case "kyc-collection": {
- return (
- // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- );
- }
- case "kyc-expiration": {
- // return <ClockIcon class="h-8 w-8 text-gray-700" />;
- return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
-
+function ShowTimeline({
+ history,
+ onSelect,
+}: {
+ onSelect: (e: AmlEvent) => void;
+ history: AmlEvent[];
+}): VNode {
+ return (
+ <div class="flow-root">
+ <ul role="list">
+ {history.map((e, idx) => {
+ const isLast = history.length - 1 === idx;
+ return (
+ <li
+ key={idx}
+ data-ok={e.type !== "aml-form-error"}
+ class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer"
+ onClick={() => {
+ onSelect(e);
+ }}
+ >
+ <div class="relative pb-6">
+ {!isLast ? (
+ <span
+ class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
+ aria-hidden="true"
+ ></span>
+ ) : undefined}
+ <div class="relative flex space-x-3">
+ {(() => {
+ switch (e.type) {
+ case "aml-form-error":
+ case "aml-form": {
+ return (
+ <div>
+ <AmlStateBadge state={e.state} />
+ <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
+ {e.threshold.currency}{" "}
+ {Amounts.stringifyValue(e.threshold)}
+ </span>
+ </div>
+ );
+ }
+ case "kyc-collection": {
+ return (
+ // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+ />
+ </svg>
+ );
+ }
+ case "kyc-expiration": {
+ // return <ClockIcon class="h-8 w-8 text-gray-700" />;
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
+ />
+ </svg>
+ );
+ }
}
- }
- assertUnreachable(e)
- })()}
- <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
- {e.type === "aml-form" ?
- <span
- // href={Pages.newFormEntry.url({ account })}
- class="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"
- >
- {e.title}
- </span>
- :
- <p class="text-sm text-gray-900">{e.title}</p>
- }
- <div class="whitespace-nowrap text-right text-sm text-gray-500">
- {e.when.t_ms === "never" ? (
- "never"
+ assertUnreachable(e);
+ })()}
+ <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
+ {e.type === "aml-form" ? (
+ <span
+ // href={Pages.newFormEntry.url({ account })}
+ class="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"
+ >
+ {e.title}
+ </span>
) : (
- <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
- {format(e.when.t_ms, "dd MMM yyyy")}
- </time>
+ <p class="text-sm text-gray-900">{e.title}</p>
)}
+ <div class="whitespace-nowrap text-right text-sm text-gray-500">
+ {e.when.t_ms === "never" ? (
+ "never"
+ ) : (
+ <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
+ {format(e.when.t_ms, "dd MMM yyyy")}
+ </time>
+ )}
+ </div>
</div>
</div>
</div>
- </div>
- </li>
- );
- })}
- </ul>
- </div>
-
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ );
}
-function ShowEventDetails({ event }: { event: AmlEvent }): VNode {
- return <div>type {event.type}</div>;
-}
+export type Justification<T = Record<string, unknown>> = {
+ // form values
+ value: T;
+} & Omit<Omit<FormMetadata, "icon">, "config">;
+type SimpleFormMetadata = {
+ version?: number;
+ id?: string;
+};
+
+export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
+ buildCodecForObject<SimpleFormMetadata>()
+ .property("id", codecOptional(codecForString()))
+ .property("version", codecOptional(codecForNumber()))
+ .build("SimpleFormMetadata");
+type ParseJustificationFail =
+ | "not-json"
+ | "id-not-found"
+ | "form-not-found"
+ | "version-not-found";
+
+function parseJustification(
+ s: string,
+ listOfAllKnownForms: FormMetadata[],
+):
+ | OperationOk<{
+ justification: Justification;
+ metadata: FormMetadata;
+ }>
+ | OperationFail<ParseJustificationFail> {
+ try {
+ const justification = JSON.parse(s);
+ const info = codecForSimpleFormMetadata().decode(justification);
+ if (!info.id) {
+ return {
+ type: "fail",
+ case: "id-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ if (!info.version) {
+ return {
+ type: "fail",
+ case: "version-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ const found = listOfAllKnownForms.find((f) => {
+ return f.id === info.id && f.version === info.version;
+ });
+ if (!found) {
+ return {
+ type: "fail",
+ case: "form-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ return {
+ type: "ok",
+ body: {
+ justification,
+ metadata: found,
+ },
+ };
+ } catch (e) {
+ return {
+ type: "fail",
+ case: "not-json",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
new file mode 100644
index 000000000..64bfb90f1
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -0,0 +1,205 @@
+/*
+ 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,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ TalerExchangeApi,
+ TalerProtocolTimestamp,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Button,
+ LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
+ UIFormField,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { privatePages } from "../Routing.js";
+import { UIFormFieldConfig, useUiFormsContext } from "../context/ui-forms.js";
+import { useFormState } from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { Justification } from "./CaseDetails.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+
+export function CaseUpdate({
+ account,
+ type: formId,
+}: {
+ account: string;
+ type: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const officer = useOfficer();
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ // const [notification, notify, handleError] = useLocalNotification();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { config } = useExchangeApiContext();
+ const { forms } = useUiFormsContext();
+ const initial = {
+ when: AbsoluteTime.now(),
+ state: TalerExchangeApi.AmlState.pending,
+ threshold: Amounts.zeroOfCurrency(config.currency),
+ };
+
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+ const theForm = forms.find((v) => v.id === formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
+
+ const [form, state] = useFormState(initial, (st) => {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ });
+
+ const validatedForm = state.status === "fail" ? undefined : state.result;
+
+ const submitHandler =
+ validatedForm === undefined
+ ? undefined
+ : withErrorHandler(
+ () => {
+ const justification: Justification = {
+ id: theForm.id,
+ label: theForm.label,
+ version: theForm.version,
+ value: validatedForm,
+ };
+
+ const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> =
+ {
+ justification: JSON.stringify(justification),
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: account,
+ new_state: justification.value
+ .state as TalerExchangeApi.AmlState,
+ new_threshold: Amounts.stringify(
+ justification.value.threshold as AmountJson,
+ ),
+ kyc_requirements: undefined,
+ };
+
+ return api.addDecisionDetails(officer.account, decision);
+ },
+ () => {
+ window.location.href = privatePages.cases.url({});
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Wrong credentials for "${officer.account}"`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Officer or account not found`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`Officer disabled or more recent decision was already submitted.`;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
+
+ function convertUiField(_f: UIFormFieldConfig[]): UIFormField[] {
+ return [];
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {theForm.config.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={convertUiField(section.fields)}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <a
+ href={privatePages.caseDetails.url({ cid: account })}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <Button
+ type="submit"
+ handler={submitHandler}
+ class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </Button>
+ </div>
+ </Fragment>
+ );
+}
+
+export function SelectForm({ account }: { account: string }) {
+ const { forms } = useUiFormsContext();
+ return (
+ <div>
+ <pre>New form for account: {account.substring(0, 16)}...</pre>
+ {forms.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
index 0355d5a31..22a6d1867 100644
--- 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
@@ -20,23 +20,22 @@
*/
import * as tests from "@gnu-taler/web-util/testing";
-import { AmlExchangeBackend } from "../types.js";
-import {
- CasesUI as TestedComponent,
-} from "./Cases.js";
-import { AmountString } from "@gnu-taler/taler-util";
+import { CasesUI as TestedComponent } from "./Cases.js";
+import { AmountString, TalerExchangeApi } from "@gnu-taler/taler-util";
export default {
title: "cases",
};
export const OneRow = tests.createExample(TestedComponent, {
- filter: AmlExchangeBackend.AmlState.normal,
+ filter: TalerExchangeApi.AmlState.normal,
onChangeFilter: () => null,
- records: [{
- current_state: AmlExchangeBackend.AmlState.normal,
- h_payto: "QWEQWEQWEQWE",
- rowid: 1,
- threshold: "USD:1" as AmountString
- }]
+ records: [
+ {
+ current_state: TalerExchangeApi.AmlState.normal,
+ h_payto: "QWEQWEQWEQWE",
+ rowid: 1,
+ threshold: "USD:1" as AmountString,
+ },
+ ],
});
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 32e162e5b..2e92c111e 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -1,214 +1,327 @@
-import { TalerError, TalerExchangeApi, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util";
-import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { createNewForm } from "../handlers/forms.js";
+/*
+ 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 {
+ HttpStatusCode,
+ TalerError,
+ TalerExchangeApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ ErrorLoading,
+ InputChoiceHorizontal,
+ Loading,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
import { useCases } from "../hooks/useCases.js";
-import { Pages } from "../pages.js";
-import { AmlExchangeBackend } from "../types.js";
+import { privatePages } from "../Routing.js";
+import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
-import { amlStateConverter } from "./ShowConsolidated.js";
-export function CasesUI({ records, filter, onChangeFilter, onFirstPage, onNext }: { onFirstPage?: () => void, onNext?: () => void, filter: AmlExchangeBackend.AmlState, onChangeFilter: (f: AmlExchangeBackend.AmlState) => void, records: TalerExchangeApi.AmlRecord[] }): VNode {
+type FormType = {
+ state: TalerExchangeApi.AmlState;
+};
+
+export function CasesUI({
+ records,
+ filter,
+ onChangeFilter,
+ onFirstPage,
+ onNext,
+}: {
+ onFirstPage?: () => void;
+ onNext?: () => void;
+ filter: TalerExchangeApi.AmlState;
+ onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
+ records: TalerExchangeApi.AmlRecord[];
+}): VNode {
const { i18n } = useTranslationContext();
- const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>();
-
- return <div>
- <div class="sm:flex sm:items-center">
- <div class="px-2 sm:flex-auto">
- <h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>
- Cases
- </i18n.Translate>
- </h1>
- <p class="mt-2 text-sm text-gray-700">
- <i18n.Translate>
- A list of all the account with the status
- </i18n.Translate>
- </p>
- </div>
- <div class="px-2">
- <form.Provider
- initialValue={{ state: filter }}
- onUpdate={(v) => {
- onChangeFilter(v.state ?? filter);
- }}
- onSubmit={(v) => { }}
- >
- <form.InputChoiceHorizontal
+ const [form, status] = useFormState<FormType>(
+ {
+ state: filter,
+ },
+ (state) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ state: state.state === undefined ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ const result: FormType = {
+ state: state.state!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ state: state.state,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ },
+ );
+ useEffect(() => {
+ if (status.status === "ok" && filter !== status.result.state) {
+ onChangeFilter(status.result.state);
+ }
+ }, [form?.state?.value]);
+
+ return (
+ <div>
+ <div class="sm:flex sm:items-center">
+ <div class="px-2 sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cases</i18n.Translate>
+ </h1>
+ <p class="mt-2 text-sm text-gray-700 w-80">
+ <i18n.Translate>
+ A list of all the account with the status
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="px-2">
+ <InputChoiceHorizontal<FormType, "state">
name="state"
label={i18n.str`Filter`}
- converter={amlStateConverter}
+ handler={form.state}
choices={[
{
- label: "Pending" as TranslatedString,
- value: AmlExchangeBackend.AmlState.pending,
+ label: i18n.str`Pending`,
+ value: TalerExchangeApi.AmlState.pending,
},
{
- label: "Frozen" as TranslatedString,
- value: AmlExchangeBackend.AmlState.frozen,
+ label: i18n.str`Frozen`,
+ value: TalerExchangeApi.AmlState.frozen,
},
{
- label: "Normal" as TranslatedString,
- value: AmlExchangeBackend.AmlState.normal,
+ label: i18n.str`Normal`,
+ value: TalerExchangeApi.AmlState.normal,
},
]}
/>
-
- </form.Provider>
+ </div>
</div>
- </div>
- <div class="mt-8 flow-root">
- <div class="overflow-x-auto">
- {!records.length ? (
- <div>empty result </div>
- ) : (
- <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
- <table class="min-w-full divide-y divide-gray-300">
- <thead>
- <tr>
- <th
- scope="col"
- class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
- >
- <i18n.Translate>
- Account Id
- </i18n.Translate>
- </th>
- <th
- scope="col"
- class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
- >
- <i18n.Translate>
- Status
- </i18n.Translate>
- </th>
- <th
- scope="col"
- class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
- >
- <i18n.Translate>
- Threshold
- </i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200 bg-white">
- {records.map((r) => {
- return (
- <tr class="hover:bg-gray-100 ">
- <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 })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- {r.h_payto.substring(0, 16)}...
- </a>
- </div>
- </td>
- <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
- {((state: AmlExchangeBackend.AmlState): VNode => {
- switch (state) {
- case AmlExchangeBackend.AmlState.normal: {
- return (
- <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
- Normal
- </span>
- );
- }
- case AmlExchangeBackend.AmlState.pending: {
- return (
- <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
- Pending
- </span>
- );
- }
- case AmlExchangeBackend.AmlState.frozen: {
- return (
- <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
- Frozen
- </span>
- );
+ <div class="mt-8 flow-root">
+ <div class="overflow-x-auto">
+ {!records.length ? (
+ <div>empty result </div>
+ ) : (
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
+ >
+ <i18n.Translate>Account Id</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Threshold</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {records.map((r) => {
+ return (
+ <tr key={r.h_payto} class="hover:bg-gray-100 ">
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
+ <div class="text-gray-900">
+ <a
+ href={privatePages.caseDetails.url({
+ cid: r.h_payto,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {r.h_payto.substring(0, 16)}...
+ </a>
+ </div>
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
+ {((state: TalerExchangeApi.AmlState): VNode => {
+ switch (state) {
+ case TalerExchangeApi.AmlState.normal: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
+ Normal
+ </span>
+ );
+ }
+ case TalerExchangeApi.AmlState.pending: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
+ Pending
+ </span>
+ );
+ }
+ case TalerExchangeApi.AmlState.frozen: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
+ Frozen
+ </span>
+ );
+ }
}
- }
- })(r.current_state)}
- </td>
- <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
- {r.threshold}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- <Pagination onFirstPage={onFirstPage} onNext={onNext} />
- </div>
- )}
+ })(r.current_state)}
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
+ {r.threshold}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ <Pagination onFirstPage={onFirstPage} onNext={onNext} />
+ </div>
+ )}
+ </div>
</div>
</div>
- </div>
-
+ );
}
-
export function Cases() {
- const [stateFilter, setStateFilter] = useState(AmlExchangeBackend.AmlState.pending);
+ const [stateFilter, setStateFilter] = useState(
+ TalerExchangeApi.AmlState.pending,
+ );
const list = useCases(stateFilter);
+ const { i18n } = useTranslationContext();
if (!list) {
- return <Loading />
+ return <Loading />;
}
if (list instanceof TalerError) {
- return <ErrorLoading error={list} />
+ return <ErrorLoading error={list} />;
}
- if (list.data.type === "fail") {
- switch (list.data.case) {
- case "unauthorized": return <Officer />
- case "officer-not-found": return <Officer />
- case "officer-disabled": return <Officer />
- default: assertUnreachable(list.data)
+ if (list.type === "fail") {
+ switch (list.case) {
+ case HttpStatusCode.Forbidden: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesnt have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account is not allowed to perform list the cases.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return <Officer />;
+ default:
+ assertUnreachable(list);
}
}
- const { records } = list.data.body
-
- return <CasesUI
- records={records}
- onFirstPage={list.pagination && !list.pagination.isFirstPage ? list.pagination.reset : undefined}
- onNext={list.pagination && !list.pagination.isLastPage ? list.pagination.loadMore : undefined}
- filter={stateFilter}
- onChangeFilter={setStateFilter}
- />
+ return (
+ <CasesUI
+ records={list.body}
+ onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
+ onNext={list.isLastPage ? undefined : list.loadNext}
+ filter={stateFilter}
+ onChangeFilter={(d) => {
+ setStateFilter(d)
+ }}
+ />
+ );
}
-export const PeopleIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
-</svg>
-
-export const HomeIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
-</svg>
+export const PeopleIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
+ />
+ </svg>
+);
+export const HomeIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
+ />
+ </svg>
+);
-export const ChevronRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
-</svg>
-
-
-export const ArrowRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
-</svg>
-
-
-function Pagination({ onFirstPage, onNext }: { onFirstPage?: () => void, onNext?: () => void, }) {
- const { i18n } = useTranslationContext()
+function Pagination({
+ onFirstPage,
+ onNext,
+}: {
+ onFirstPage?: () => void;
+ onNext?: () => void;
+}) {
+ const { i18n } = useTranslationContext();
return (
- <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
+ <nav
+ class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg"
+ aria-label="Pagination"
+ >
<div class="flex flex-1 justify-between sm:justify-end">
<button
class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
@@ -226,6 +339,5 @@ function Pagination({ onFirstPage, onNext }: { onFirstPage?: () => void, onNext?
</button>
</div>
</nav>
-
- )
+ );
}
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 9b8c3c046..abcaaa2a6 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -1,26 +1,127 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+/*
+ 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 {
- notifyError,
+ Button,
+ InputLine,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { createNewForm } from "../handlers/forms.js";
-import { useSettings } from "../hooks/useSettings.js";
-
-export function CreateAccount({
- onNewAccount,
-}: {
- onNewAccount: (password: string) => void;
-}): VNode {
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ RecursivePartial,
+ useFormState,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { usePreferences } from "../hooks/preferences.js";
+
+type FormType = {
+ password: string;
+ repeat: string;
+};
+function createFormValidator(
+ i18n: InternationalizationAPI,
+ allowInsecurePassword: boolean,
+) {
+ return function check(
+ state: RecursivePartial<FormValues<FormType>>,
+ ): FormStatus<FormType> {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password
+ ? i18n.str`required`
+ : allowInsecurePassword
+ ? undefined
+ : state.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !state.password.match(/[a-z]/) && state.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !state.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !state.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+
+ repeat: !state.repeat
+ ? i18n.str`required`
+ : state.password !== state.repeat
+ ? i18n.str`doesn't match`
+ : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: FormType = {
+ password: state.password!,
+ repeat: state.repeat!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ password: state.password,
+ repeat: state.repeat,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
+
+export function CreateAccount(): VNode {
const { i18n } = useTranslationContext();
- const Form = createNewForm<{
- password: string;
- repeat: string;
- }>();
- const [settings] = useSettings()
+ const [settings] = usePreferences();
+ const officer = useOfficer();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [form, status] = useFormState<FormType>(
+ {
+ password: undefined,
+ repeat: undefined,
+ },
+ createFormValidator(i18n, settings.allowInsecurePassword),
+ );
+
+ const createAccountHandler =
+ status.status === "fail" || officer.state !== "not-found"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.create(form.password!.value!),
+ () => {},
+ );
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Create account</i18n.Translate>
@@ -29,78 +130,65 @@ export function CreateAccount({
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- computeFormState={(v) => {
- return {
- password: {
- error: !v.password
- ? i18n.str`required`
- : settings.allowInsecurePassword
- ? undefined
- : v.password.length < 8
- ? i18n.str`should have at least 8 characters`
- : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
- ? i18n.str`should have lowercase and uppercase characters`
- : !v.password.match(/\d/)
- ? i18n.str`should have numbers`
- : !v.password.match(/[^a-zA-Z\d]/)
- ? i18n.str`should have at least one character which is not a number or letter`
- : undefined,
- },
- repeat: {
- error: !v.repeat
- ? i18n.str`required`
- : v.repeat !== v.password
- ? i18n.str`doesn't match`
- : undefined,
- },
- };
- }}
- onSubmit={async (v, s) => {
- const error = s?.password?.error ?? s?.repeat?.error;
- if (error) {
- notifyError(
- "Can't create account" as TranslatedString,
- error as TranslatedString,
- );
- } else {
- onNewAccount(v.password!);
- }
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
}}
+ autoCapitalize="none"
+ autoCorrect="off"
>
- <div class="mb-4">
- <Form.InputLine
- label={"Password" as TranslatedString}
+ <div class="mt-2">
+ <InputLine<FormType, "password">
+ label={i18n.str`Password`}
name="password"
type="password"
- help={
- settings.allowInsecurePassword
- ? i18n.str`short password are insecure, turn off insecure password in settings`
- : i18n.str`lower and upper case letters, number and special character`
- }
required
+ handler={form.password}
/>
</div>
- <div class="mb-4">
- <Form.InputLine
- label={"Repeat password" as TranslatedString}
+
+ <div class="mt-2">
+ <InputLine<FormType, "repeat">
+ label={i18n.str`Repeat password`}
name="repeat"
type="password"
required
+ handler={form.repeat}
/>
</div>
<div class="mt-8">
- <button
+ <Button
type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!createAccountHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={createAccountHandler}
>
<i18n.Translate>Create</i18n.Translate>
- </button>
+ </Button>
</div>
- </Form.Provider>
+ </form>
</div>
</div>
</div>
);
}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
index ff800ebdc..3d6e14f22 100644
--- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -1,8 +1,23 @@
+/*
+ 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 { 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";
-import { assertUnreachable } from "@gnu-taler/taler-util";
export function HandleAccountNotReady({
officer,
@@ -10,26 +25,11 @@ export function HandleAccountNotReady({
officer: OfficerNotReady;
}): VNode {
if (officer.state === "not-found") {
- return (
- <CreateAccount
- onNewAccount={(password) => {
- officer.create(password);
- }}
- />
- );
+ return <CreateAccount />;
}
if (officer.state === "locked") {
- return (
- <UnlockAccount
- onRemoveAccount={() => {
- officer.forget();
- }}
- onAccountUnlocked={async (pwd) => {
- await officer.tryUnlock(pwd);
- }}
- />
- );
+ return <UnlockAccount />;
}
- assertUnreachable(officer)
+ assertUnreachable(officer);
}
diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
deleted file mode 100644
index d53ac27c1..000000000
--- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Amounts, 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 { useOfficer } from "../hooks/useOfficer.js";
-import { Pages } from "../pages.js";
-import { AntiMoneyLaunderingForm, allForms } 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 "unauthorized": return notify({
- type: "error",
- title: i18n.str`Wrong credentials for "${officer.account}"`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "officer-or-account-not-found": return notify({
- type: "error",
- title: i18n.str`Officer or account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "officer-disabled-or-recent-decision": 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,
- })
- }
- })
- }}
- />
- </Fragment>
- );
-}
-
-function SelectForm({ account }: { account: string }) {
- return (
- <div>
- <pre>New form for account: {account.substring(0, 16)}...</pre>
- {allForms.map((form, idx) => {
- return (
- <a
- 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
index ec8327814..39359cd5e 100644
--- a/packages/aml-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx
@@ -1,19 +1,39 @@
-import { Fragment, h } from "preact";
-import { useOfficer } from "../hooks/useOfficer.js";
+/*
+ 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 {
+ useExchangeApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useOfficer } from "../hooks/officer.js";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { uiSettings } from "../settings.js";
-import { getInitialBackendBaseURL } from "../hooks/useBackend.js";
+import { useUiSettingsContext } from "../context/ui-settings.js";
export function Officer() {
const officer = useOfficer();
- const { i18n } = useTranslationContext()
+ const settings = useUiSettingsContext();
+ 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>
@@ -25,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
index dc073a5f5..714bf6580 100644
--- 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
@@ -19,91 +19,114 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { addDays } from "date-fns";
import {
- ShowConsolidated as TestedComponent,
-} from "./ShowConsolidated.js";
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { InternationalizationAPI } from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
import { getEventsFromAmlHistory } from "./CaseDetails.js";
-import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
+import { ShowConsolidated as TestedComponent } from "./ShowConsolidated.js";
export default {
title: "show consolidated",
};
+const nullTranslator: InternationalizationAPI = {
+ str: (str: TemplateStringsArray) => str.join() as TranslatedString,
+ singular: (str: TemplateStringsArray) => str.join() as TranslatedString,
+ translate: (str: TemplateStringsArray) => [str.join()] as TranslatedString[],
+ Translate: () => undefined as unknown,
+};
+
export const WithEmptyHistory = tests.createExample(TestedComponent, {
- history: getEventsFromAmlHistory([], []),
- until: AbsoluteTime.now()
+ history: getEventsFromAmlHistory([], [], nullTranslator, []),
+ until: AbsoluteTime.now(),
});
export const WithSomeEvents = tests.createExample(TestedComponent, {
- history: getEventsFromAmlHistory([
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208199
- }
- },
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208211
- }
- },
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208220
- }
- },
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":4,\"name\":\"Declaration for trusts (902.13e)\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700208362854},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"contractingPartner\":\"f\",\"knownAs\":\"a\",\"trust\":{\"name\":\"b\",\"type\":\"discretionary\",\"revocability\":\"irrevocable\"}}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208385
- }
- },
- {
- "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488420810},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"qwe\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700488423
- }
- },
- {
- "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488671251},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"asd asd asd \"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700488677
- }
- }
- ], [{
- collection_time: AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromPrettyString("1d"))
- ),
- expiration_time: { t_s: "never" },
- provider_section: "asd",
- attributes: {
- email: "sebasjm@qwdde.com"
- }
- }]),
- until: AbsoluteTime.now()
+ history: getEventsFromAmlHistory(
+ [
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208199,
+ },
+ },
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208211,
+ },
+ },
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208220,
+ },
+ },
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":4,"name":"Declaration for trusts (902.13e)","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700208362854},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"contractingPartner":"f","knownAs":"a","trust":{"name":"b","type":"discretionary","revocability":"irrevocable"}}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208385,
+ },
+ },
+ {
+ decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
+ justification:
+ '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488420810},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"qwe"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700488423,
+ },
+ },
+ {
+ decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
+ justification:
+ '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488671251},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"asd asd asd "}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700488677,
+ },
+ },
+ ],
+ [
+ {
+ collection_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.subtractDuraction(
+ AbsoluteTime.now(),
+ Duration.fromPrettyString("1d"),
+ ),
+ ),
+ expiration_time: { t_s: "never" },
+ provider_section: "asd",
+ attributes: {
+ email: "sebasjm@qwdde.com",
+ },
+ },
+ ],
+ nullTranslator,
+ [],
+ ),
+ until: AbsoluteTime.now(),
});
-
-
-
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index bd25ce958..0169572bf 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -1,12 +1,33 @@
+/*
+ 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,
+ AmountJson,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ DefaultForm,
+ FlexibleForm,
+ UIFormField,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { NiceForm } from "../NiceForm.js";
-import { FlexibleForm } from "../forms/index.js";
-import { UIFormField } from "../handlers/forms.js";
import { AmlEvent } from "./CaseDetails.js";
-import { AmlExchangeBackend } from "../types.js";
-import { AbsoluteTime, AmountJson, TranslatedString } from "@gnu-taler/taler-util";
-import { format } from "date-fns";
export function ShowConsolidated({
history,
@@ -15,6 +36,8 @@ export function ShowConsolidated({
history: AmlEvent[];
until: AbsoluteTime;
}): VNode {
+ const { i18n } = useTranslationContext();
+
const cons = getConsolidated(history, until);
const form: FlexibleForm<Consolidated> = {
@@ -22,46 +45,45 @@ export function ShowConsolidated({
return {
aml: {
threshold: {
- hidden: !form.aml
+ hidden: !form.aml,
},
since: {
- hidden: !form.aml
+ hidden: !form.aml,
},
state: {
- hidden: !form.aml
- }
- }
+ hidden: !form.aml,
+ },
+ },
};
},
design: [
{
- title: "AML" as TranslatedString,
+ title: i18n.str`AML`,
fields: [
{
type: "amount",
- props: {
- label: "Threshold" as TranslatedString,
+ properties: {
+ label: i18n.str`Threshold`,
name: "aml.threshold",
},
},
{
type: "choiceHorizontal",
- props: {
- label: "State" as TranslatedString,
+ properties: {
+ label: i18n.str`State`,
name: "aml.state",
- converter: amlStateConverter,
choices: [
{
- label: "Frozen" as TranslatedString,
- value: AmlExchangeBackend.AmlState.frozen,
+ label: i18n.str`Frozen`,
+ value: TalerExchangeApi.AmlState.frozen,
},
{
- label: "Pending" as TranslatedString,
- value: AmlExchangeBackend.AmlState.pending,
+ label: i18n.str`Pending`,
+ value: TalerExchangeApi.AmlState.pending,
},
{
- label: "Normal" as TranslatedString,
- value: AmlExchangeBackend.AmlState.normal,
+ label: i18n.str`Normal`,
+ value: TalerExchangeApi.AmlState.normal,
},
],
},
@@ -70,38 +92,40 @@ export function ShowConsolidated({
},
Object.entries(cons.kyc).length > 0
? {
- title: "KYC" as TranslatedString,
- fields: Object.entries(cons.kyc).map(([key, field]) => {
- const result: UIFormField = {
- type: "text",
- props: {
- label: key as TranslatedString,
- name: `kyc.${key}.value`,
- help: `${field.provider} since ${field.since.t_ms === "never"
- ? "never"
- : format(field.since.t_ms, "dd/MM/yyyy")
+ title: i18n.str`KYC`,
+ fields: Object.entries(cons.kyc).map(([key, field]) => {
+ const result: UIFormField = {
+ type: "text",
+ properties: {
+ label: key as TranslatedString,
+ name: `kyc.${key}.value`,
+ help: `${field.provider} since ${
+ field.since.t_ms === "never"
+ ? "never"
+ : format(field.since.t_ms, "dd/MM/yyyy")
}` as TranslatedString,
- },
- };
- return result;
- }),
- }
+ },
+ };
+ return result;
+ }),
+ }
: undefined,
],
};
return (
<Fragment>
<h1 class="text-base font-semibold leading-7 text-black">
- Consolidated information
+ Consolidated information{" "}
{until.t_ms === "never"
? ""
: `after ${format(until.t_ms, "dd MMMM yyyy")}`}
</h1>
- <NiceForm
+ <DefaultForm
key={`${String(Date.now())}`}
form={form}
initial={cons}
- onUpdate={() => { }}
+ readOnly
+ onUpdate={() => {}}
/>
</Fragment>
);
@@ -109,13 +133,13 @@ export function ShowConsolidated({
interface Consolidated {
aml: {
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
since: AbsoluteTime;
};
kyc: {
[field: string]: {
- value: any;
+ value: unknown;
provider: string;
since: AbsoluteTime;
};
@@ -128,13 +152,13 @@ function getConsolidated(
): Consolidated {
const initial: Consolidated = {
aml: {
- state: AmlExchangeBackend.AmlState.normal,
+ state: TalerExchangeApi.AmlState.normal,
threshold: {
currency: "ARS",
value: 1000,
fraction: 0,
},
- since: AbsoluteTime.never()
+ since: AbsoluteTime.never(),
},
kyc: {},
};
@@ -153,14 +177,15 @@ function getConsolidated(
prev.aml = {
since: cur.when,
state: cur.state,
- threshold: cur.threshold
- }
+ threshold: cur.threshold,
+ };
break;
}
case "kyc-collection": {
Object.keys(cur.values).forEach((field) => {
+ const value = (cur.values as Record<string, unknown>)[field];
prev.kyc[field] = {
- value: (cur.values as any)[field],
+ value,
provider: cur.provider,
since: cur.when,
};
@@ -171,33 +196,3 @@ function getConsolidated(
return prev;
}, initial);
}
-
-export const amlStateConverter = {
- toStringUI: stringifyAmlState,
- fromStringUI: parseAmlState,
-};
-
-function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string {
- if (s === undefined) return "";
- switch (s) {
- case AmlExchangeBackend.AmlState.normal:
- return "normal";
- case AmlExchangeBackend.AmlState.pending:
- return "pending";
- case AmlExchangeBackend.AmlState.frozen:
- return "frozen";
- }
-}
-
-function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState {
- switch (s) {
- case "normal":
- return AmlExchangeBackend.AmlState.normal;
- case "pending":
- return AmlExchangeBackend.AmlState.pending;
- case "frozen":
- return AmlExchangeBackend.AmlState.frozen;
- default:
- throw Error(`unknown AML state: ${s}`);
- }
-}
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index ba5aa7b1f..9552f2b0c 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -1,80 +1,128 @@
-import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util";
-import { notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ 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 {
+ Button,
+ InputLine,
+ LocalNotificationBanner,
+ useLocalNotificationHandler,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { createNewForm } from "../handlers/forms.js";
+import { FormErrors, useFormState } from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+
+type FormType = {
+ password: string;
+};
+
+export function UnlockAccount(): VNode {
+ const { i18n } = useTranslationContext();
+
+ const officer = useOfficer();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
-export function UnlockAccount({
- onAccountUnlocked,
- onRemoveAccount,
-}: {
- onAccountUnlocked: (password: string) => Promise<void>;
- onRemoveAccount: () => void;
-}): VNode {
- const { i18n } = useTranslationContext()
- const Form = createNewForm<{
- password: string;
- }>();
+ const [form, status] = useFormState<FormType>(
+ {
+ password: undefined,
+ },
+ (state) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: state as FormType,
+ errors,
+ };
+ }
+ return {
+ status: "fail",
+ result: state,
+ errors,
+ };
+ },
+ );
+
+ const unlockHandler =
+ status.status === "fail" || officer.state !== "locked"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.tryUnlock(form.password!.value!),
+ () => {},
+ );
+
+ const forgetHandler =
+ status.status === "fail" || officer.state !== "locked"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.forget(),
+ () => {},
+ );
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Account locked</i18n.Translate>
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600">
- <i18n.Translate>Your account is normally locked anytime you reload. To unlock type
- your password again.</i18n.Translate>
+ <i18n.Translate>
+ Your account is normally locked anytime you reload. To unlock type
+ your password again.
+ </i18n.Translate>
</p>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- initialValue={{}}
- onSubmit={async (v) => {
- try {
- await onAccountUnlocked(v.password!);
- notifyInfo("Account unlocked" as TranslatedString);
- } catch (e) {
- if (e instanceof UnwrapKeyError) {
- notifyError(
- "Could not unlock account" as any,
- e.message as any,
- );
- } else {
- throw e;
- }
- }
- }}
- >
- <div class="mb-4">
- <Form.InputLine
- label={"Password" as TranslatedString}
- name="password"
- type="password"
- required
- />
- </div>
- <div class="mt-8">
- <button
- type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Unlock</i18n.Translate>
- </button>
- </div>
- </Form.Provider>
+ <div class="mb-4">
+ <InputLine<FormType, "password">
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ required
+ handler={form.password}
+ />
+ </div>
+
+ <div class="mt-8">
+ <Button
+ type="submit"
+ handler={unlockHandler}
+ disabled={!unlockHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Unlock</i18n.Translate>
+ </Button>
+ </div>
+
</div>
- <button
+ <Button
type="button"
- onClick={() => {
- onRemoveAccount();
- }}
- class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ handler={forgetHandler}
+ disabled={!forgetHandler}
+ class="disabled:opacity-50 disabled:cursor-default m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
>
<i18n.Translate>Forget account</i18n.Translate>
- </button>
+ </Button>
</div>
</div>
);
diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts
index afe73227a..f11028de8 100644
--- a/packages/aml-backoffice-ui/src/pages/index.stories.ts
+++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts
@@ -1,3 +1,17 @@
+/*
+ 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/>
+ */
export * as a1 from "./ShowConsolidated.stories.js";
-export * as a2 from "./AntiMoneyLaunderingForm.stories.js";
export * as a3 from "./Cases.stories.js";
diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts
deleted file mode 100644
index f515a590a..000000000
--- a/packages/aml-backoffice-ui/src/route.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-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/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
index eca66cb18..a4f32cf43 100644
--- 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, Fragment, VNode, h as create } from "preact";
-import { ExchangeApiContextTesting } from "./context/config.js";
-// import { BackendStateProviderTesting } from "./context/backend.js";
+import { ComponentChildren, VNode, h as create } from "preact";
setupI18n("en", { en: {} });
@@ -48,7 +50,6 @@ describe("All the examples:", () => {
});
});
-
function DefaultTestingContext({
children,
}: {
@@ -61,11 +62,22 @@ function DefaultTestingContext({
name: "ARS",
num_fractional_input_digits: 2,
num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2
+ num_fractional_trailing_zero_digits: 2,
},
name: "taler-exchange",
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
index 7685195e5..a66396696 100644
--- 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
@@ -21,43 +21,58 @@
import { strings } from "./i18n/strings.js";
import * as pages from "./pages/index.stories.js";
-// import * as components from "./components/index.examples.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 "./scss/main.css";
-import { h, ComponentChildren, FunctionComponent, VNode } from "preact";
-import { ExchangeApiContextTesting } from "./context/config.js";
function main(): void {
renderStories(
{ pages },
{
strings,
- getWrapperForGroup
+ getWrapperForGroup,
},
);
}
function getWrapperForGroup(): FunctionComponent {
return function All({ children }: { children?: ComponentChildren }): VNode {
- 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",
- }}>
- {children}
- </ExchangeApiContextTesting>
- }
+ 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 (
+ <ExchangeApiProviderTesting value={value}>
+ {children}
+ </ExchangeApiProviderTesting>
+ );
+ };
}
if (document.readyState === "loading") {
diff --git a/packages/aml-backoffice-ui/src/types.ts b/packages/aml-backoffice-ui/src/types.ts
deleted file mode 100644
index 429b538e7..000000000
--- a/packages/aml-backoffice-ui/src/types.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-export namespace AmlExchangeBackend {
- // FIXME: placeholder
- export interface AmlError {
- code: number;
- hint: string;
- }
- export interface AmlDecisionDetails {
- // Array of AML decisions made for this account. Possibly
- // contains only the most recent decision if "history" was
- // not set to 'true'.
- aml_history: AmlDecisionDetail[];
-
- // Array of KYC attributes obtained for this account.
- kyc_attributes: KycDetail[];
- }
-
- type AmlOfficerPublicKeyP = string;
-
- export interface AmlDecisionDetail {
- // What was the justification given?
- justification: string;
-
- // What is the new AML state.
- new_state: Integer;
-
- // When was this decision made?
- decision_time: Timestamp;
-
- // What is the new AML decision threshold (in monthly transaction volume)?
- new_threshold: Amount;
-
- // Who made the decision?
- decider_pub: AmlOfficerPublicKeyP;
- }
- export interface KycDetail {
- // Name of the configuration section that specifies the provider
- // which was used to collect the KYC details
- provider_section: string;
-
- // The collected KYC data. NULL if the attribute data could not
- // be decrypted (internal error of the exchange, likely the
- // attribute key was changed).
- attributes?: Object;
-
- // Time when the KYC data was collected
- collection_time: Timestamp;
-
- // Time when the validity of the KYC data will expire
- expiration_time: Timestamp;
- }
-
- interface Timestamp {
- // Seconds since epoch, or the special
- // value "never" to represent an event that will
- // never happen.
- t_s: number | "never";
- }
-
- type PaytoHash = string;
- type Integer = number;
- type Amount = string;
- // EdDSA signatures are transmitted as 64-bytes base32
- // binary-encoded objects with just the R and S values (base32_ binary-only).
- type EddsaSignature = string;
-
- export interface AmlRecords {
- // Array of AML records matching the query.
- records: AmlRecord[];
- }
-
- interface AmlRecord {
- // Which payto-address is this record about.
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the current AML state.
- current_state: AmlState;
-
- // Monthly transaction threshold before a review will be triggered
- threshold: Amount;
-
- // RowID of the record.
- rowid: Integer;
- }
-
- export enum AmlState {
- normal = 0,
- pending = 1,
- frozen = 2,
- }
-
-
- export interface AmlDecision {
-
- // Human-readable justification for the decision.
- justification: string;
-
- // At what monthly transaction volume should the
- // decision be automatically reviewed?
- new_threshold: Amount;
-
- // Which payto-address is the decision about?
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the new AML state (e.g. frozen, unfrozen, etc.)
- // Numerical values are defined in AmlDecisionState.
- new_state: Integer;
-
- // Signature by the AML officer over a
- // TALER_MasterAmlOfficerStatusPS.
- // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
- officer_sig: EddsaSignature;
-
- // When was the decision made?
- decision_time: Timestamp;
-
- // Optional argument to impose new KYC requirements
- // that the customer has to satisfy to unblock transactions.
- kyc_requirements?: string[];
- }
-
-
-}
diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx
index 1dc1712b7..b382348a3 100644
--- 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
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
new file mode 100644
index 000000000..cca764a81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -0,0 +1,47 @@
+/*
+ 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 { TalerExchangeApi } from "@gnu-taler/taler-util";
+
+export const amlStateConverter = {
+ toStringUI: stringifyAmlState,
+ fromStringUI: parseAmlState,
+};
+
+function stringifyAmlState(s: TalerExchangeApi.AmlState | undefined): string {
+ if (s === undefined) return "";
+ switch (s) {
+ case TalerExchangeApi.AmlState.normal:
+ return "normal";
+ case TalerExchangeApi.AmlState.pending:
+ return "pending";
+ case TalerExchangeApi.AmlState.frozen:
+ return "frozen";
+ }
+}
+
+function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
+ switch (s) {
+ case "normal":
+ return TalerExchangeApi.AmlState.normal;
+ case "pending":
+ return TalerExchangeApi.AmlState.pending;
+ case "frozen":
+ return TalerExchangeApi.AmlState.frozen;
+ default:
+ throw Error(`unknown AML state: ${s}`);
+ }
+}