summaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/aml-backoffice-ui')
-rw-r--r--packages/aml-backoffice-ui/.eslintrc.cjs28
-rw-r--r--packages/aml-backoffice-ui/.gitignore4
-rw-r--r--packages/aml-backoffice-ui/Makefile36
-rw-r--r--packages/aml-backoffice-ui/README.md4
-rwxr-xr-xpackages/aml-backoffice-ui/build.mjs28
-rw-r--r--packages/aml-backoffice-ui/copyleft-header.js15
-rwxr-xr-xpackages/aml-backoffice-ui/dev.mjs40
-rw-r--r--packages/aml-backoffice-ui/package.json59
-rw-r--r--packages/aml-backoffice-ui/postcss.config.js6
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx138
-rw-r--r--packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx273
-rw-r--r--packages/aml-backoffice-ui/src/Routing.tsx151
-rw-r--r--packages/aml-backoffice-ui/src/assets/home.svg3
-rw-r--r--packages/aml-backoffice-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/aml-backoffice-ui/src/assets/people.svg3
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-forms.ts76
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-settings.ts110
-rw-r--r--packages/aml-backoffice-ui/src/declaration.d.ts46
-rw-r--r--packages/aml-backoffice-ui/src/forms.json529
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_11e.ts147
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_12e.ts436
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_13e.ts525
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_15e.ts187
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_1e.ts656
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_4e.ts802
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_5e.ts269
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_9e.ts134
-rw-r--r--packages/aml-backoffice-ui/src/forms/icons.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts207
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts91
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts227
-rw-r--r--packages/aml-backoffice-ui/src/hooks/officer.ts163
-rw-r--r--packages/aml-backoffice-ui/src/hooks/preferences.ts85
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts51
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts115
-rw-r--r--packages/aml-backoffice-ui/src/i18n/bank.pot486
-rw-r--r--packages/aml-backoffice-ui/src/i18n/de.po486
-rw-r--r--packages/aml-backoffice-ui/src/i18n/en.po511
-rw-r--r--packages/aml-backoffice-ui/src/i18n/es.po497
-rw-r--r--packages/aml-backoffice-ui/src/i18n/fr.po486
-rw-r--r--packages/aml-backoffice-ui/src/i18n/it.po521
-rw-r--r--packages/aml-backoffice-ui/src/i18n/poheader26
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings-prelude19
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings.ts510
-rw-r--r--packages/aml-backoffice-ui/src/index.html41
-rw-r--r--packages/aml-backoffice-ui/src/index.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx472
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx284
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx41
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx347
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx197
-rw-r--r--packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx35
-rw-r--r--packages/aml-backoffice-ui/src/pages/Officer.tsx84
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx132
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx183
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx131
-rw-r--r--packages/aml-backoffice-ui/src/pages/index.stories.ts17
-rw-r--r--packages/aml-backoffice-ui/src/scss/main.css3
-rw-r--r--packages/aml-backoffice-ui/src/settings.json4
-rw-r--r--packages/aml-backoffice-ui/src/stories.test.ts83
-rw-r--r--packages/aml-backoffice-ui/src/stories.tsx82
-rw-r--r--packages/aml-backoffice-ui/src/utils/QR.tsx54
-rw-r--r--packages/aml-backoffice-ui/tailwind.config.js14
-rwxr-xr-xpackages/aml-backoffice-ui/test.mjs31
-rw-r--r--packages/aml-backoffice-ui/tsconfig.json46
65 files changed, 11526 insertions, 0 deletions
diff --git a/packages/aml-backoffice-ui/.eslintrc.cjs b/packages/aml-backoffice-ui/.eslintrc.cjs
new file mode 100644
index 000000000..05618b499
--- /dev/null
+++ b/packages/aml-backoffice-ui/.eslintrc.cjs
@@ -0,0 +1,28 @@
+module.exports = {
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ plugins: ['@typescript-eslint', 'header'],
+ root: true,
+ rules: {
+ "react/no-unknown-property": 0,
+ "react/no-unescaped-entities": 0,
+ "@typescript-eslint/no-namespace": 0,
+ "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}],
+ "header/header": [2,"copyleft-header.js"]
+ },
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ jsx: true,
+ },
+ settings: {
+ react: {
+ version: "18",
+ pragma: "h",
+ }
+ },
+};
diff --git a/packages/aml-backoffice-ui/.gitignore b/packages/aml-backoffice-ui/.gitignore
new file mode 100644
index 000000000..30cb2774c
--- /dev/null
+++ b/packages/aml-backoffice-ui/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/build
+/*.log
+/demobank-ui-settings.js
diff --git a/packages/aml-backoffice-ui/Makefile b/packages/aml-backoffice-ui/Makefile
new file mode 100644
index 000000000..64f9f83d1
--- /dev/null
+++ b/packages/aml-backoffice-ui/Makefile
@@ -0,0 +1,36 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/aml-backoffice-ui
+
+.PHONY: install-nodeps
+install-nodeps:
+ install -d $(spa_dir)
+ install ./dist/prod/* $(spa_dir)
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/aml-backoffice-ui...
+ pnpm run check
+ pnpm run build
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/aml-backoffice-ui/README.md b/packages/aml-backoffice-ui/README.md
new file mode 100644
index 000000000..855addd74
--- /dev/null
+++ b/packages/aml-backoffice-ui/README.md
@@ -0,0 +1,4 @@
+# Taler Exchange Backoffice UI
+
+Web-based user interface for the GNU Taler exchange.
+
diff --git a/packages/aml-backoffice-ui/build.mjs b/packages/aml-backoffice-ui/build.mjs
new file mode 100755
index 000000000..b0742c692
--- /dev/null
+++ b/packages/aml-backoffice-ui/build.mjs
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+/*
+ 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 { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx"],
+ assets: [{ base: "src", files: ["src/index.html","src/forms.json"] }],
+ },
+ destination: "./dist/prod",
+ css: "postcss",
+});
diff --git a/packages/aml-backoffice-ui/copyleft-header.js b/packages/aml-backoffice-ui/copyleft-header.js
new file mode 100644
index 000000000..7fa276bea
--- /dev/null
+++ b/packages/aml-backoffice-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ 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/>
+ */
diff --git a/packages/aml-backoffice-ui/dev.mjs b/packages/aml-backoffice-ui/dev.mjs
new file mode 100755
index 000000000..e91b48f9d
--- /dev/null
+++ b/packages/aml-backoffice-ui/dev.mjs
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+/*
+ 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 { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
+
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{ base: "src", files: ["src/index.html","src/forms.json","src/settings.json"] }],
+ },
+ destination: "./dist/dev",
+ css: "postcss",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
new file mode 100644
index 000000000..749565946
--- /dev/null
+++ b/packages/aml-backoffice-ui/package.json
@@ -0,0 +1,59 @@
+{
+ "private": true,
+ "name": "@gnu-taler/aml-backoffice-ui",
+ "version": "0.10.7",
+ "author": "sebasjm",
+ "license": "AGPL-3.0-OR-LATER",
+ "description": "Back-office SPA for GNU Taler Exchange.",
+ "type": "module",
+ "scripts": {
+ "build": "./build.mjs",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "check": "tsc",
+ "clean": "rm -rf dist lib",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "i18n:extract": "pogen extract",
+ "i18n:merge": "pogen merge",
+ "i18n:emit": "pogen emit",
+ "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit",
+ "pretty": "prettier --write src"
+ },
+ "dependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "@headlessui/react": "^1.7.14",
+ "@heroicons/react": "^2.0.17",
+ "date-fns": "2.29.3",
+ "history": "4.10.1",
+ "jed": "1.1.1",
+ "preact": "10.11.3",
+ "swr": "2.2.2"
+ },
+ "devDependencies": {
+ "@gnu-taler/pogen": "workspace:*",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.9",
+ "eslint": "^8.56.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "mocha": "^9.2.0",
+ "po2json": "^0.4.5",
+ "postcss": "^8.4.23",
+ "postcss-cli": "^10.1.0",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "aml-backoffice"
+ }
+}
diff --git a/packages/aml-backoffice-ui/postcss.config.js b/packages/aml-backoffice-ui/postcss.config.js
new file mode 100644
index 000000000..2e7af2b7f
--- /dev/null
+++ b/packages/aml-backoffice-ui/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
new file mode 100644
index 000000000..e9be84441
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -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 { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ExchangeApiProvider,
+ Loading,
+ TranslationProvider,
+ UiForms,
+} 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";
+import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js";
+
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+export function App(): VNode {
+ const [settings, setSettings] = useState<UiSettings>();
+ const [forms, setForms] = useState<UiForms>();
+ useEffect(() => {
+ fetchUiSettings(setSettings);
+ fetchUiForms(setForms);
+ }, []);
+ if (!settings || !forms) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <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>
+ <UiFormsProvider value={forms}>
+ <Routing />
+ </UiFormsProvider>
+ </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/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/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/assets/home.svg b/packages/aml-backoffice-ui/src/assets/home.svg
new file mode 100644
index 000000000..35f340162
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/assets/home.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 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="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> \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/assets/logo-2021.svg b/packages/aml-backoffice-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/assets/people.svg b/packages/aml-backoffice-ui/src/assets/people.svg
new file mode 100644
index 000000000..1dc878b81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/assets/people.svg
@@ -0,0 +1,3 @@
+<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="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
+</svg> \ No newline at end of file
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..3a25234d2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts
@@ -0,0 +1,76 @@
+/*
+ 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 { codecForUIForms, UiForms } from "@gnu-taler/web-util/browser";
+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,
+ });
+};
+
+
+
+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
new file mode 100644
index 000000000..7868e41bd
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/declaration.d.ts
@@ -0,0 +1,46 @@
+/*
+ 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;
+
+declare module "*.po" {
+ const content: any;
+ export default content;
+}
+declare module "jed" {
+ const x: any;
+ export = x;
+}
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
+declare module "*.png" {
+ const content: any;
+ export default content;
+}
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
+
+declare module "*.scss" {
+ const content: Record<string, string>;
+ export default content;
+}
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json
new file mode 100644
index 000000000..94dcda317
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms.json
@@ -0,0 +1,529 @@
+{
+ "forms": [
+ {
+ "label": "Information on customer",
+ "id": "902_1e",
+ "version": 1,
+ "config": {
+ "type": "double-column",
+ "design": [
+ {
+ "title": "Information on customer",
+ "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.",
+ "fields": [
+ {
+ "type": "choiceStacked",
+
+ "name": "customerType",
+ "id": ".customerType",
+ "label": "Type of customer",
+ "help": "Select one and complete the next form",
+ "required": true,
+ "choices": [
+ {
+ "label": "Natural person",
+ "value": "natural"
+ },
+ {
+ "label": "Legal entity",
+ "value": "legal"
+ }
+ ]
+ },
+ {
+ "type": "group",
+
+ "label": "Natural customer form",
+ "name": "algo",
+ "id": "algo",
+ "before": "a) Country risk (nationality)",
+ "after": "a) Country risk (nationality)",
+ "fields": [
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.fullName",
+ "id": ".naturalCustomer.fullName",
+ "label": "Full name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.address",
+ "id": ".naturalCustomer.address",
+ "label": "Residential address",
+ "required": true
+ },
+ {
+ "type": "integer",
+
+ "name": "naturalCustomer.telephone",
+ "id": ".naturalCustomer.telephone",
+ "label": "Telephone"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.email",
+ "id": ".naturalCustomer.email",
+ "label": "E-mail"
+ },
+ {
+ "type": "absoluteTimeText",
+
+ "pattern": "dd/MM/yyyy",
+ "name": "naturalCustomer.dateOfBirth",
+ "id": ".naturalCustomer.dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.nationality",
+ "id": ".naturalCustomer.nationality",
+ "label": "Nationality",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.document",
+ "id": ".naturalCustomer.document",
+ "label": "Identification document",
+ "required": true
+ },
+ {
+ "type": "file",
+
+ "name": "naturalCustomer.documentAttachment",
+ "id": ".naturalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.companyName",
+ "id": ".naturalCustomer.companyName",
+ "label": "Company name"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.office",
+ "id": ".naturalCustomer.office",
+ "label": "Registered office"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.companyDocument",
+ "id": ".naturalCustomer.companyDocument",
+ "label": "Company identification document"
+ },
+ {
+ "type": "file",
+
+ "name": "naturalCustomer.companyDocumentAttachment",
+ "id": ".naturalCustomer.companyDocumentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ ]
+ },
+
+ {
+ "type": "group",
+
+ "label": "Natural customer form",
+ "name": "algo",
+ "id": "algo",
+ "before": "a) Country risk (nationality)",
+ "after": "a) Country risk (nationality)",
+ "fields": [
+ {
+ "type": "text",
+
+ "name": "legalCustomer.companyName",
+ "id": ".legalCustomer.companyName",
+ "label": "Company name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.domicile",
+ "id": ".legalCustomer.domicile",
+ "label": "Domicile",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.contactPerson",
+ "id": ".legalCustomer.contactPerson",
+ "label": "Contact person"
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.telephone",
+ "id": ".legalCustomer.telephone",
+ "label": "Telephone"
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.email",
+ "id": ".legalCustomer.email",
+ "label": "E-mail"
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.document",
+ "id": ".legalCustomer.document",
+ "label": "Identification document",
+ "help": "Not older than 12 month"
+ },
+ {
+ "type": "file",
+
+ "name": "legalCustomer.documentAttachment",
+ "id": ".legalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Information on the natural persons who establish the business relationship for legal entities and partnerships",
+ "description": "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.",
+ "fields": [
+ {
+ "type": "array",
+
+ "name": "businessEstablisher",
+ "id": ".businessEstablisher",
+ "label": "Persons",
+ "required": true,
+ "labelFieldId": "fullName",
+ "placeholder": "this is the placeholder",
+ "fields": [
+ {
+ "type": "text",
+
+ "name": "fullName",
+ "id": ".fullName",
+ "label": "Full name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "address",
+ "id": ".address",
+ "label": "Residential address",
+ "required": true
+ },
+ {
+ "type": "absoluteTimeText",
+
+ "pattern": "dd/MM/yyyy",
+ "name": "dateOfBirth",
+ "id": ".dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ },
+
+ {
+ "type": "text",
+
+ "name": "nationality",
+ "id": ".nationality",
+ "label": "Nationality",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "typeOfAuthorization",
+ "id": ".typeOfAuthorization",
+ "label": "Type of authorization (signatory of representation)",
+ "required": true
+ },
+ {
+ "type": "file",
+
+ "name": "documentAttachment",
+ "id": ".documentAttachment",
+ "label": "Identification document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ },
+ {
+ "type": "choiceStacked",
+
+ "name": "powerOfAttorneyArrangements",
+ "id": ".powerOfAttorneyArrangements",
+ "label": "Power of attorney arrangements",
+ "required": true,
+ "choices": [
+ {
+ "label": "CR extract",
+ "value": "cr"
+ },
+ {
+ "label": "Mandate",
+ "value": "mandate"
+ },
+ {
+ "label": "Other",
+ "value": "other"
+ }
+ ]
+ },
+ {
+ "type": "text",
+
+ "name": "powerOfAttorneyArrangementsOther",
+ "id": ".powerOfAttorneyArrangementsOther",
+ "label": "Power of attorney arrangements",
+ "required": true
+ }
+ ],
+ "labelField": "fullName"
+ }
+ ]
+ },
+ {
+ "title": "Acceptance of business relationship",
+ "fields": [
+ {
+ "type": "absoluteTimeText",
+
+ "name": "acceptance.when",
+ "id": ".acceptance.when",
+ "pattern": "dd/MM/yyyy",
+ "converterId": "Taler.AbsoluteTime",
+ "label": "Date (conclusion of contract)"
+ },
+ {
+ "type": "choiceStacked",
+
+ "name": "acceptance.acceptedBy",
+ "id": ".acceptance.acceptedBy",
+ "label": "Accepted by",
+ "required": true,
+ "choices": [
+ {
+ "label": "Face-to-face meeting with customer",
+ "value": "face-to-face"
+ },
+ {
+ "label": "Correspondence: authenticated copy of identification document obtained",
+ "value": "correspondence-document"
+ },
+ {
+ "label": "Correspondence: residential address validated",
+ "value": "correspondence-address"
+ }
+ ]
+ },
+ {
+ "type": "choiceStacked",
+
+ "name": "acceptance.typeOfCorrespondence",
+ "id": ".acceptance.typeOfCorrespondence",
+ "label": "Type of correspondence service",
+ "choices": [
+ {
+ "label": "to the customer",
+ "value": "customer"
+ },
+ {
+ "label": "hold at bank",
+ "value": "bank"
+ },
+ {
+ "label": "to the member",
+ "value": "member"
+ },
+ {
+ "label": "to a third party",
+ "value": "third-party"
+ }
+ ]
+ },
+ {
+ "type": "text",
+
+ "name": "acceptance.thirdPartyFullName",
+ "id": ".acceptance.thirdPartyFullName",
+ "label": "Third party full name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "acceptance.thirdPartyAddress",
+ "id": ".acceptance.thirdPartyAddress",
+ "label": "Third party address",
+ "required": true
+ },
+ {
+ "type": "selectMultiple",
+
+ "name": "acceptance.language",
+ "id": ".acceptance.language",
+ "label": "Languages",
+ "choices": [
+ {
+ "label": "Espanol",
+ "value": "es"
+ }
+ ],
+ "unique": true
+ },
+ {
+ "type": "textArea",
+
+ "name": "acceptance.furtherInformation",
+ "id": ".acceptance.furtherInformation",
+ "label": "Further information"
+ }
+ ]
+ },
+ {
+ "title": "Information on the beneficial owner of the assets and/or controlling person",
+ "description": "Establishment of the beneficial owner of the assets and/or controlling person",
+ "fields": [
+ {
+ "type": "choiceStacked",
+
+ "name": "establishment",
+ "id": ".establishment",
+ "label": "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",
+ "value": "natural"
+ },
+ {
+ "label": "a foundation (or a similar construct; incl. underlying companies)",
+ "value": "foundation"
+ },
+ {
+ "label": "a trust (incl. underlying companies)",
+ "value": "trust"
+ },
+ {
+ "label": "a life insurance policy with separately managed accounts/securities accounts",
+ "value": "insurance-wrapper"
+ },
+ {
+ "label": "all other cases",
+ "value": "other"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship",
+ "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)",
+ "fields": [
+ {
+ "type": "textArea",
+
+ "name": "embargoEvaluation",
+ "id": ".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.",
+ "label": "Evaluation"
+ }
+ ]
+ },
+ {
+ "title": "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship",
+ "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",
+ "fields": [
+ {
+ "type": "choiceStacked",
+
+ "name": "cashTransactions.typeOfBusiness",
+ "id": ".cashTransactions.typeOfBusiness",
+ "label": "Type of business relationship",
+ "choices": [
+ {
+ "label": "Money exchange",
+ "value": "money-exchange"
+ },
+ {
+ "label": "Money and asset transfer",
+ "value": "money-and-asset-transfer"
+ },
+ {
+ "label": "Other cash transactions. Specify below",
+ "value": "other"
+ }
+ ]
+ },
+ {
+ "type": "text",
+
+ "name": "cashTransactions.otherTypeOfBusiness",
+ "id": ".cashTransactions.otherTypeOfBusiness",
+ "required": true,
+ "label": "Specify other cash transactions:"
+ },
+ {
+ "type": "textArea",
+ "name": "cashTransactions.purpose",
+ "id": ".cashTransactions.purpose",
+ "label": "Purpose of the business relationship (purpose of service requested)"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "label": "Example form",
+ "id": "example",
+ "version": 1,
+ "config": {
+ "type": "double-column",
+ "design": [
+ {
+ "title": "Boolean inputs",
+ "fields": [
+ {
+ "type": "toggle",
+ "name": "yes",
+ "id": ".yes",
+ "label": "Yes or no?"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "not_yet_supported": []
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts
new file mode 100644
index 000000000..7cf710741
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts
@@ -0,0 +1,147 @@
+/*
+ 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 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ title:
+ i18n.str`Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange`,
+ description:
+ 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",
+ properties: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "declares",
+ label:
+ i18n.str`The contracting partner hereby declares that`,
+ required: true,
+ choices: [
+ {
+ label:
+ 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:
+ 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:
+ 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",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ name: "people",
+ label: i18n.str`People`,
+ required: true,
+ placeholder: i18n.str`this is the placeholder`,
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "lastName",
+ label: i18n.str`Last name(s)`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "firstName",
+ label: i18n.str`First name(s)`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label: i18n.str`Actual address of domicile`,
+ required: true,
+ },
+ },
+ ],
+ labelField: "lastName",
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "fiduciaryAssets",
+ 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: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ description:
+ i18n.str`The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9`,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_11.Form>,
+ ): FormState<Form902_11.Form> {
+ return {
+ people: {
+ hidden: v.declares !== "controlling-in-other-ways" &&
+ v.declares !== "managing-director",
+ }
+ };
+ }
+});
+
+namespace Form902_11 {
+ interface Person {
+ lastName: string;
+ firstName: string;
+ address: string;
+ }
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ declares: "25-or-more" | "controlling-in-other-ways" | "managing-director";
+ people: Person[];
+ fiduciaryAssets: "no" | "yes";
+ signature: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts
new file mode 100644
index 000000000..5aa3f4cf9
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts
@@ -0,0 +1,436 @@
+/*
+ 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 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ title: i18n.str`Foundations`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "knownAs",
+ label:
+ 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",
+ properties: {
+ name: "foundation.name",
+ label:
+ i18n.str`Name and information pertaining to the foundation`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "foundation.type",
+ label: i18n.str`Type of foundation`,
+ choices: [
+ {
+ label: i18n.str`Discretionary foundation`,
+ value: "discretionary",
+ },
+ {
+ label: i18n.str`Non-discretionary foundation`,
+ value: "non-discretionary",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "foundation.revocability",
+ label: i18n.str`Revocability`,
+ choices: [
+ {
+ label: i18n.str`Revocable foundation`,
+ value: "revocable",
+ },
+ {
+ label: i18n.str`Irrevocable foundation`,
+ value: "irrevocable",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ i18n.str`Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies)`,
+ labelField: "fullName",
+ name: "founders",
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ help: i18n.str`if deceased`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToRevoke",
+ required: true,
+ label:
+ i18n.str`Does the founder have the right to revoke the foundation?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ 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",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ help: i18n.str`if deceased`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
+ labelField: "fullName",
+ name: "beneficiaryWhenSigning",
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Has the beneficiary an actual right to claim distribution?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ 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",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ 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",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToClaim",
+ label:
+ i18n.str`has the person the right to revoke the foundation?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ 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",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+ // 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 {
+ interface Foundation {
+ name: string;
+ type: "discretionary" | "non-discretionary";
+ revocability: "revocable" | "irrevocable";
+ }
+ interface Person {
+ fullName: string;
+ address: string;
+ country: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+ type WithRevoke<T> = {
+ rightToRevoke: "yes" | "no";
+ } & T;
+ type WithClaim<T> = {
+ rightToClaim: "yes" | "no";
+ } & T;
+ type WithDeath<T> = {
+ dateOfDeath: AbsoluteTime;
+ } & T;
+
+ type Founder = WithRevoke<WithDeath<Person>>;
+ type Beneficiary = WithClaim<Person>;
+
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ knownAs: string;
+ boardMember: string;
+ foundation: Foundation;
+ founders: Array<Founder>;
+ preExistingFounders: Array<Founder>;
+ beneficiaryWhenSigning: Array<Beneficiary>;
+ beneficiaryExtra: Array<Beneficiary>;
+ withRightToNominate: Array<WithRevoke<Person>>;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts
new file mode 100644
index 000000000..d71266489
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts
@@ -0,0 +1,525 @@
+/*
+ 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 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ title: i18n.str`Declaration for trusts`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "knownAs",
+ label:
+ 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",
+ properties: {
+ name: "trust.name",
+ label:
+ i18n.str`Name and information pertaining to the trust`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "trust.type",
+ label: i18n.str`Type of trust`,
+ choices: [
+ {
+ label: i18n.str`Discretionary trust`,
+ value: "discretionary",
+ },
+ {
+ label: i18n.str`Non-discretionary trust`,
+ value: "non-discretionary",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "trust.revocability",
+ label: i18n.str`Revocability`,
+ choices: [
+ {
+ label: i18n.str`Revocable foundation`,
+ value: "revocable",
+ },
+ {
+ label: i18n.str`Irrevocable foundation`,
+ value: "irrevocable",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ 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",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
+ help: i18n.str`if deceased'`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToRevoke",
+ required: true,
+ label:
+ i18n.str`Does the founder have the right to revoke the trust?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ 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",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfDeath",
+ label: i18n.str`Date of death`,
+ pattern: "dd/MM/yyyy",
+ help: i18n.str`if deceased.`,
+ // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
+ labelField: "fullName",
+ name: "beneficiaryWhenSigning",
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Has the beneficiary an actual right to claim distribution?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ 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",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ 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: [],
+ },
+ },
+
+ {
+ type: "array",
+ properties: {
+ label:
+ i18n.str`Information pertaining to the protectors`,
+ labelField: "fullName",
+ name: "protectors",
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Does the protector have the right to revoke the trust?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ properties: {
+ label:
+ i18n.str`Information pertaining to further persons`,
+ labelField: "fullName",
+ name: "furtherPersons",
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label:
+ i18n.str`Actual address of domicile/registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "country",
+ label: i18n.str`Country`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "rightToClaim",
+ label:
+ i18n.str`Has this further person the right to revoke the trust?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+ // 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 {
+ interface Foundation {
+ name: string;
+ type: "discretionary" | "non-discretionary";
+ revocability: "revocable" | "irrevocable";
+ }
+ interface Person {
+ fullName: string;
+ address: string;
+ country: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+ type WithRevoke<T> = {
+ rightToRevoke: "yes" | "no";
+ } & T;
+ type WithClaim<T> = {
+ rightToClaim: "yes" | "no";
+ } & T;
+ type WithDeath<T> = {
+ dateOfDeath: AbsoluteTime;
+ } & T;
+
+ type Founder = WithRevoke<WithDeath<Person>>;
+ type Beneficiary = WithClaim<Person>;
+
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ knownAs: string;
+ boardMember: string;
+ foundation: Foundation;
+ settlors: Array<Founder>;
+ preExistingSettlors: Array<Founder>;
+ beneficiaryWhenSigning: Array<Beneficiary>;
+ beneficiaryExtra: Array<Beneficiary>;
+ protectors: Array<WithRevoke<Person>>;
+ furtherPersons: Array<WithRevoke<Person>>;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts
new file mode 100644
index 000000000..eeda166c1
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts
@@ -0,0 +1,187 @@
+/*
+ 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 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ title:
+ i18n.str`Information on life insurance policies with separately managed accounts/securities accounts`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "contractualRelationship",
+ label:
+ i18n.str`Name or number of the contractual relationship between the contracting party and the financial intermediary`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "insurancePolicy",
+ label: i18n.str`Insurance policy`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ properties: {
+ label:
+ i18n.str`In relation with the above insurance policy, the contracting partner gives the following further details`,
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`Policy holder`,
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "holder.fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "holder.address",
+ label:
+ i18n.str`Actual address of domicile/registered office (incl. country)`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "holder.dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "holder.nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before:
+ 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",
+ properties: {
+ name: "premiumPayer.fullName",
+ label:
+ i18n.str`Last name(s), first name(s)/entity`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "premiumPayer.address",
+ label:
+ i18n.str`Actual address of domicile/registered office (incl. country)`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "premiumPayer.dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "premiumPayer.nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ properties: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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(i18n),
+ ],
+});
+
+namespace Form902_15 {
+ interface Person {
+ fullName: string;
+ address: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ contractualRelationship: string;
+ insurancePolicy: string;
+ holder: Person;
+ premiumsPayer: Person;
+ signature: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts
new file mode 100644
index 000000000..58ef7e2e8
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts
@@ -0,0 +1,656 @@
+/*
+ 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 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ 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",
+ properties: {
+ name: "customerType",
+ label: i18n.str`Type of customer`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`Natural person`,
+ value: "natural",
+ },
+ {
+ label: i18n.str`Legal entity`,
+ value: "legal",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.fullName",
+ label: i18n.str`Full name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.address",
+ label: i18n.str`Residential address`,
+ required: true,
+ },
+ },
+ {
+ type: "integer",
+ properties: {
+ name: "naturalCustomer.telephone",
+ label: i18n.str`Telephone`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.email",
+ label: i18n.str`E-mail`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "naturalCustomer.dateOfBirth",
+ label: i18n.str`Date of birth`,
+ required: true,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.nationality",
+ label: i18n.str`Nationality`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.document",
+ label: i18n.str`Identification document`,
+ required: true,
+ },
+ },
+ {
+ type: "file",
+ properties: {
+ name: "naturalCustomer.documentAttachment",
+ label: i18n.str`Document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.companyName",
+ label: i18n.str`Company name`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.office",
+ label: i18n.str`Registered office`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "naturalCustomer.companyDocument",
+ label: i18n.str`Company identification document`,
+ },
+ },
+ {
+ type: "file",
+ properties: {
+ name: "naturalCustomer.companyDocumentAttachment",
+ label: i18n.str`Document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "legalCustomer.companyName",
+ label: i18n.str`Company name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "legalCustomer.domicile",
+ label: i18n.str`Domicile`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "legalCustomer.contactPerson",
+ label: i18n.str`Contact person`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "legalCustomer.telephone",
+ label: i18n.str`Telephone`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "legalCustomer.email",
+ label: i18n.str`E-mail`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "legalCustomer.document",
+ label: i18n.str`Identification document`,
+ help: i18n.str`Not older than 12 month`,
+ },
+ },
+ {
+ type: "file",
+ properties: {
+ name: "legalCustomer.documentAttachment",
+ label: i18n.str`Document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ ],
+ },
+ {
+ 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",
+ properties: {
+ name: "businessEstablisher",
+ label: i18n.str`Persons`,
+ required: true,
+ placeholder: i18n.str`this is the placeholder`,
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "fullName",
+ label: i18n.str`Full name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label: i18n.str`Residential address`,
+ required: true,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ required: true,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "typeOfAuthorization",
+ label: i18n.str`Type of authorization (signatory of representation)`,
+ required: true,
+ },
+ },
+ {
+ type: "file",
+ properties: {
+ name: "documentAttachment",
+ label: i18n.str`Identification document attachment`,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: i18n.str`Max size of 2 mega bytes`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "powerOfAttorneyArrangements",
+ label: i18n.str`Power of attorney arrangements`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`CR extract`,
+ value: "cr",
+ },
+ {
+ label: i18n.str`Mandate`,
+ value: "mandate",
+ },
+ {
+ label: i18n.str`Other`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "powerOfAttorneyArrangementsOther",
+ label: i18n.str`Power of attorney arrangements`,
+ required: true,
+ },
+ },
+ ],
+ labelField: "fullName",
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Acceptance of business relationship`,
+ fields: [
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "acceptance.when",
+ pattern: "dd/MM/yyyy",
+ label: i18n.str`Date (conclusion of contract)`,
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "acceptance.acceptedBy",
+ label: i18n.str`Accepted by`,
+ required: true,
+ choices: [
+ {
+ label: i18n.str`Face-to-face meeting with customer`,
+ value: "face-to-face",
+ },
+ {
+ label: i18n.str`Correspondence: authenticated copy of identification document obtained`,
+ value: "correspondence-document",
+ },
+ {
+ label: i18n.str`Correspondence: residential address validated`,
+ value: "correspondence-address",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ name: "acceptance.typeOfCorrespondence",
+ label: i18n.str`Type of correspondence service`,
+ choices: [
+ {
+ label: i18n.str`to the customer`,
+ value: "customer",
+ },
+ {
+ label: i18n.str`hold at bank`,
+ value: "bank",
+ },
+ {
+ label: i18n.str`to the member`,
+ value: "member",
+ },
+ {
+ label: i18n.str`to a third party`,
+ value: "third-party",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "acceptance.thirdPartyFullName",
+ label: i18n.str`Third party full name`,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "acceptance.thirdPartyAddress",
+ label: i18n.str`Third party address`,
+ required: true,
+ },
+ },
+ {
+ type: "selectMultiple",
+ properties: {
+ name: "acceptance.language",
+ label: i18n.str`Languages`,
+ choices: ["asd"],
+ unique: true,
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ name: "acceptance.furtherInformation",
+ label: i18n.str`Further information`,
+ },
+ },
+ ],
+ },
+ {
+ 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",
+ properties: {
+ name: "establishment",
+ label: i18n.str`The customer is`,
+ required: true,
+ choices: [
+ {
+ 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: i18n.str`a foundation (or a similar construct; incl. underlying companies)`,
+ value: "foundation",
+ },
+ {
+ label: i18n.str`a trust (incl. underlying companies)`,
+ value: "trust",
+ },
+ {
+ label: i18n.str`a life insurance policy with separately managed accounts/securities accounts`,
+ value: "insurance-wrapper",
+ },
+ {
+ label: i18n.str`all other cases`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ 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",
+ properties: {
+ name: "embargoEvaluation",
+ 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: 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",
+ properties: {
+ name: "cashTransactions.typeOfBusiness",
+ label: i18n.str`Type of business relationship`,
+ choices: [
+ {
+ label: i18n.str`Money exchange`,
+ value: "money-exchange",
+ },
+ {
+ label: i18n.str`Money and asset transfer`,
+ value: "money-and-asset-transfer",
+ },
+ {
+ label: i18n.str`Other cash transactions. Specify below`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "cashTransactions.otherTypeOfBusiness",
+ required: true,
+ label: i18n.str`Specify other cash transactions:`,
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ name: "cashTransactions.purpose",
+ label: i18n.str`Purpose of the business relationship (purpose of service requested)`,
+ },
+ },
+ ],
+ },
+ 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",
+ // },
+ // },
+ // };
+ // },
+});
+
+namespace Form902_1 {
+ interface LegalEntityCustomer {
+ companyName: string;
+ domicile: string;
+ contactPerson: string;
+ telephone: string;
+ email: string;
+ document: string;
+ documentAttachment: string;
+ }
+ interface NaturalCustomer {
+ fullName: string;
+ address: string;
+ telephone: string;
+ email: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ document: string;
+ documentAttachment: string;
+ companyName: string;
+ office: string;
+ companyDocument: string;
+ companyDocumentAttachment: string;
+ }
+
+ interface Person {
+ fullName: string;
+ address: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ typeOfAuthorization: string;
+ document: string;
+ documentAttachment: string;
+ powerOfAttorneyArrangements: "cr" | "mandate" | "other";
+ powerOfAttorneyArrangementsOther: string;
+ }
+
+ interface Acceptance {
+ when: AbsoluteTime;
+ acceptedBy: "face-to-face" | "authenticated-copy";
+ typeOfCorrespondence: string;
+ language: string[];
+ furtherInformation: string;
+ thirdPartyFullName: string;
+ thirdPartyAddress: string;
+ }
+
+ interface BeneficialOwner {
+ establishment:
+ | "natural-person"
+ | "foundation"
+ | "trust"
+ | "insurance-wrapper"
+ | "other";
+ }
+
+ interface CashTransactions {
+ typeOfBusiness: "money-exchange" | "money-and-asset-transfer" | "other";
+ otherTypeOfBusiness: string;
+ purpose: string;
+ }
+
+ export interface Form extends BaseForm {
+ fullName: string;
+ customerType: "natural" | "legal";
+ naturalCustomer: NaturalCustomer;
+ legalCustomer: LegalEntityCustomer;
+ businessEstablisher: Array<Person>;
+ acceptance: Acceptance;
+ beneficialOwner: BeneficialOwner;
+ embargoEvaluation: string;
+ cashTransactions: CashTransactions;
+ // enclosures: Enclosures;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts
new file mode 100644
index 000000000..7a3af8731
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts
@@ -0,0 +1,802 @@
+/*
+ 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 { BaseForm } from "../context/ui-forms.js";
+import { ArrowRightIcon, ChevronRightIcon } from "./icons.js";
+import { resolutionSection } from "./simplest.js";
+
+export const v1 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ title: i18n.str`Risk Profile AMLA`,
+ description:
+ i18n.str`Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring.`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ properties: {
+ name: "customer",
+ label: i18n.str`Customer`,
+ help: i18n.str`Pursuant identification form (VQF doc. Nr. 902.1) numeral 1`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Evaluation of politically exposed persons (PEP-Check)`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ properties: {
+ label: i18n.str`Foreign PEP`,
+ // tooltip:
+ // 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: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label: i18n.str`Yes`,
+ description:
+ i18n.str`The business relationship is compulsory classified as increased risk`,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label:
+ i18n.str`Domestic PEP and PEP of International Organizations`,
+ // tooltip:
+ // 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: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label:
+ i18n.str`Yes, but NOT risk criterion pursuant to numeral 3 subsequently increased.`,
+ value: "yes-but-no-risk",
+ },
+ {
+ label:
+ i18n.str`Yes, AND a risk criterion pursuant to numeral 3 subsequently increased.`,
+ description:
+ i18n.str`Classification of the business relationship as increased risk is compulsory`,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ label:
+ 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: i18n.str`dd/MM/yyyy`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ 'Evaluation "high risk" or non-cooperative country' as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ 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: i18n.str`No`,
+ value: "no",
+ },
+ {
+ label: i18n.str`Yes`,
+ description:
+ i18n.str`considered as business relationship with increased risk`,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ label:
+ 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: i18n.str`dd/MM/yyyy`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Evaluation of business relationship risk`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ properties: {
+ before: i18n.str`a) Country risk (nationality)`,
+ fields: [
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Domicile/residential address`,
+ name: "evaluation.nationality.address",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ {
+ label: i18n.str`Controlling person`,
+ value: "controlling",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Nationality`,
+ name: "evaluation.nationality.nationality",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.nationality.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`b) Country risk (business activity)`,
+ fields: [
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Place of business activity`,
+ name: "evaluation.business.place",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.business.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`c) Country risk (payments)`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`Country of origin and destination of frequent payments (if known)`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.payments.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`d) Industry risk`,
+ fields: [
+ {
+ type: "choiceStacked",
+ properties: {
+ label:
+ i18n.str`Nature of customer's business activity`,
+ name: "evaluation.industry.nature",
+ choices: [
+ {
+ label: i18n.str`Customer`,
+ value: "customer",
+ },
+ {
+ label:
+ i18n.str`Beneficial owner of the assets`,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.payments.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Clearly defined, transparent, easily comprehensible business activity well known to the member`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`Business activity with a high level of cash transactions`,
+ value: "medium-cash",
+ },
+ {
+ label:
+ i18n.str`Business activity not well known to the member`,
+ value: "medium-unknown",
+ },
+ {
+ label:
+ 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:
+ i18n.str`Member has no personal knowledge of the customer's industry`,
+ value: "high-unknown",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`e) Contact risk`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`Types of contact to the customer/ beneficial owner of the assets`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.contact.risk",
+ choices: [
+ {
+ label:
+ 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:
+ 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:
+ 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",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`f) Product risk`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`Nature of services and products requested by the customer`,
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.product.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Easy to understand, transparent services and products whose financial background is easy to comprehend and verify`,
+ value: "low",
+ },
+ {
+ label:
+ i18n.str`More sophisticated services/products whose financial background is not readily easy to comprehend and verify`,
+ value: "medium",
+ },
+ {
+ label:
+ i18n.str`Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations)`,
+ value: "high-offshore",
+ },
+ {
+ label:
+ 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:
+ 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:
+ i18n.str`Complex services/products whose financial background can’t be understood or verified with considerable effort`,
+ value: "high-service",
+ },
+ {
+ label:
+ i18n.str`Frequent transactions with increased risks`,
+ value: "high-freq-tx",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`g) Criteria defined by the member`,
+ fields: [
+ {
+ type: "text",
+ properties: {
+ label: i18n.str`Criteria definition`,
+ name: "evaluation.custom.definition",
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk level`,
+ name: "evaluation.custom.risk",
+ choices: [
+ {
+ label: i18n.str`Low`,
+ value: "low",
+ },
+ {
+ label: i18n.str`Medium`,
+ value: "medium",
+ },
+ {
+ label: i18n.str`High`,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`Overall assessment of the business relationship`,
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before:
+ i18n.str`A business relationship is classified as increased risk if:`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`Business relationship with PEP pursuant to numeral 1 (no exception possible)`,
+ before: create(ChevronRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "caption",
+ 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" }),
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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" }),
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Justification for differing risk assessment`,
+ name: "evaluation.overall.justification",
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Risk classified`,
+ name: "evaluation.overall.risk",
+ choices: [
+ {
+ label:
+ i18n.str`Business relationship _without_ increased risk`,
+ value: "without",
+ },
+ {
+ label:
+ i18n.str`Business relationship __with__ increased risk`,
+ value: "with",
+ },
+ ],
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ label:
+ 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: i18n.str`dd/MM/yyyy`,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Criteria for identification of increased risk transactions (transaction monitoring)`,
+ fields: [
+ {
+ type: "group",
+ properties: {
+ before: i18n.str`Criteria`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`Classification as as increased risk is compulsory if`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ before: create(ChevronRightIcon, { class: "w-6 h-6" }),
+ label:
+ 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",
+ 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,
+ },
+ },
+ {
+ type: "caption",
+ 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,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before:
+ i18n.str`Additional criteria defined by the member`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ before: create(ArrowRightIcon, { class: "w-6 h-6" }),
+ label:
+ i18n.str`All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions`,
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Description`,
+ name: "criteria.additional",
+ },
+ },
+ {
+ type: "group",
+ properties: {
+ before:
+ i18n.str`Possible criteria (Art. 59 para. 2 SRO Regulations)`,
+ fields: [
+ {
+ type: "caption",
+ properties: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`the amount of inflowing and outflowing assets`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual)`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual)`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ i18n.str`description of expected transaction patterns which the client notify the member of (considerable variance would be unusual)`,
+ },
+ },
+ {
+ type: "caption",
+ 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,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+ behavior: function formBehavior(
+ // v: Partial<Form902_4.Form>,
+ ): FormState<Form902_4.Form> {
+ return {
+ };
+ },
+});
+
+namespace Form902_4 {
+ export interface Form extends BaseForm {
+ customer: string;
+ fullName: string;
+ pep: {
+ foreign: "yes" | "no";
+ domestic: "yes" | "no" | "yes-but-no-risk";
+ when: AbsoluteTime;
+ };
+ highRisk: {
+ evaluation: "yes" | "no";
+ when: AbsoluteTime;
+ };
+ evaluation: {
+ nationality: {
+ address: "customer" | "owner" | "controlling";
+ nationality: "customer" | "owner";
+ risk: "low" | "medium" | "high";
+ };
+ business: {
+ place: "customer" | "owner";
+ risk: "low" | "medium" | "high";
+ };
+ payments: {
+ risk: "low" | "medium" | "high";
+ };
+ industry: {
+ nature: "customer" | "owner";
+ risk:
+ | "low"
+ | "medium-cash"
+ | "medium-unknown"
+ | "high-restricted"
+ | "high-unknown";
+ };
+ contact: {
+ risk: "low" | "medium" | "high";
+ };
+ product: {
+ risk:
+ | "low"
+ | "medium"
+ | "high-offshore"
+ | "high-structure"
+ | "high-accounts"
+ | "high-service"
+ | "high-freq-tx";
+ };
+ custom: {
+ definition: string;
+ risk: "low" | "medium" | "high";
+ };
+ overall: {
+ justification: string;
+ risk: "with" | "without";
+ };
+ };
+ criteria: {
+ additional: string;
+ };
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts
new file mode 100644
index 000000000..e66a4f94d
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts
@@ -0,0 +1,269 @@
+/*
+ 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 = (i18n: InternationalizationAPI) => ({
+ design: [
+ {
+ title: i18n.str`Customer Profile`,
+ description:
+ 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",
+ properties: {
+ name: "customer",
+ label: i18n.str`Customer`,
+ help: i18n.str`Pursuant Identification Form (VQF doc. No. 902.1) numeral 1`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Business activity`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Profession, business activities`,
+ name: "businessActivity",
+ help: i18n.str`former, current, potentially planned`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Financial circumstances`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Income and assets, liabilities`,
+ name: "financial",
+ help: i18n.str`estimated`,
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Origin of the deposited assets involved`,
+ fields: [
+ {
+ type: "text",
+ properties: {
+ label: i18n.str`Nature`,
+ name: "originOfAssets.nature",
+ help: i18n.str`nature of the involved assets`,
+ },
+ },
+ {
+ type: "selectOne",
+ properties: {
+ name: "originOfAssets.currency",
+ label: i18n.str`Currency`,
+ choices: ["change me"],
+ },
+ },
+ {
+ type: "integer",
+ properties: {
+ label: i18n.str`Amount`,
+ name: "originOfAssets.amount",
+ },
+ },
+ {
+ type: "choiceStacked",
+ properties: {
+ label: i18n.str`Category`,
+ name: "originOfAssets.category",
+ choices: [
+ {
+ label: i18n.str`Savings`,
+ value: "savings",
+ },
+ {
+ label: i18n.str`Own business operations`,
+ value: "own-business",
+ },
+ {
+ label: i18n.str`Inheritance`,
+ value: "inheritance",
+ },
+ {
+ label: i18n.str`Other, what?`,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ label: i18n.str`Other category`,
+ name: "originOfAssets.categoryOther",
+ required: true,
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Detailed description of the origins/economical background of the assets involved in the business relationship`,
+ name: "originOfAssets.details",
+ },
+ },
+ ],
+ },
+ {
+ title:
+ i18n.str`Nature and purpose of the business relationship`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Purpose of the business relationship`,
+ name: "nature.purpose",
+ help: i18n.str`nature of the involved assets`,
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Information on the planned development of the business relationship and the assets`,
+ name: "nature.plan",
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ 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: i18n.str`Relationship with third parties`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Relation of the customer to the beneficial owner involved in the business relationship`,
+ name: "relations.beneficialOwners",
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Relation of the customer to the controlling persons involved in the business relationship`,
+ name: "relations.controllingPersons",
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Relation of the customer to the authorized signatories involved in the business relationship`,
+ name: "relations.authorizedSignatories",
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label:
+ i18n.str`Relation of the customer to other persons involved in the business relationship`,
+ name: "relations.otherPersons",
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Relation to other AMLA-Files`,
+ name: "relations.withOtherAmlaFiles",
+ },
+ },
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Introducer / agents / references`,
+ name: "relations.references",
+ },
+ },
+ ],
+ },
+ {
+ title: i18n.str`Further information`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ label: i18n.str`Other relevant information`,
+ name: "furtherInformation",
+ },
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+ // behavior: function formBehavior(
+ // v: Partial<Form902_5.Form>,
+ // ): FormState<Form902_5.Form> {
+ // return {
+ // originOfAssets: {
+ // categoryOther: {
+ // hidden: v.originOfAssets?.category !== "other",
+ // },
+ // },
+ // };
+ // },
+});
+
+namespace Form902_5 {
+ export interface Form extends BaseForm {
+ customer: string;
+ fullName: string;
+ businessActivity: string;
+ financial: string;
+ originOfAssets: {
+ nature: string;
+ currency: string;
+ amount: number;
+ category: "savings" | "own-business" | "inheritance" | "other";
+ categoryOther: string;
+ details: string;
+ };
+ nature: {
+ purpose: string;
+ plan: string;
+ cashOrMoneyTransfer: string;
+ };
+ relations: {
+ beneficialOwners: string;
+ controllingPersons: string;
+ authorizedSignatories: string;
+ otherPersons: string;
+ withOtherAmlaFiles: string;
+ references: string;
+ };
+ furtherInformation: string;
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts
new file mode 100644
index 000000000..297ec86b1
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts
@@ -0,0 +1,134 @@
+/*
+ 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 = (i18n: InternationalizationAPI) =>({
+ design: [
+ {
+ title:
+ i18n.str`Declaration of identity of the beneficial owner`,
+ fields: [
+ {
+ type: "textArea",
+ properties: {
+ name: "contractingPartner",
+ label: i18n.str`Contracting partner`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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",
+ properties: {
+ label: i18n.str`Persons`,
+ labelField: "surname",
+ name: "persons",
+ fields: [
+ {
+ type: "text",
+ properties: {
+ name: "surname",
+ label: i18n.str`Surname(s)`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "firstName",
+ label: i18n.str`First name(s)`,
+ },
+ },
+ {
+ type: "absoluteTime",
+ properties: {
+ name: "dateOfBirth",
+ label: i18n.str`Date of birth`,
+ pattern: "dd/MM/yyyy",
+ // help: i18n.str`format 'dd/MM/yyyy'`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "nationality",
+ label: i18n.str`Nationality`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "address",
+ label: i18n.str`Actual address of domicile`,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ i18n.str`The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein`,
+ },
+ },
+ {
+ type: "text",
+ properties: {
+ name: "signature",
+ label: i18n.str`Signature`,
+ },
+ },
+ {
+ type: "caption",
+ properties: {
+ label:
+ 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(i18n),
+ ],
+ behavior: function formBehavior(
+ // v: Partial<Form902_9.Form>,
+ ): FormState<Form902_9.Form> {
+ return {
+ };
+ },
+});
+
+namespace Form902_9 {
+ interface Person {
+ surname: string;
+ firstName: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ address: string;
+ }
+ export interface Form extends BaseForm {
+ contractingPartner: string;
+ persons: Person;
+ signature: string;
+ }
+}
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
new file mode 100644
index 000000000..e89a8fb10
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -0,0 +1,207 @@
+/*
+ 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 { FormMetadata, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { v1 as simplest } from "./simplest.js";
+
+const languages = (i18n: InternationalizationAPI) => [
+ {
+ label: i18n.str`Mandarin Chinese`,
+ value: "cmn",
+ },
+ {
+ label: i18n.str`Spanish`,
+ value: "spa",
+ },
+ {
+ label: i18n.str`English`,
+ value: "eng",
+ },
+ {
+ label: i18n.str`Hindi`,
+ value: "hin",
+ },
+ {
+ label: i18n.str`Portuguese`,
+ value: "por",
+ },
+ {
+ label: i18n.str`Bengali`,
+ value: "ben",
+ },
+ {
+ label: i18n.str`Russian`,
+ value: "rus",
+ },
+ {
+ label: i18n.str`Japanese`,
+ value: "jpn",
+ },
+ {
+ label: i18n.str`Yue`,
+ value: "yue",
+ },
+ {
+ label: i18n.str`Vietnamese`,
+ value: "vie",
+ },
+ {
+ label: i18n.str`Turkish`,
+ value: "tur",
+ },
+ {
+ label: i18n.str`Wu`,
+ value: "wuu",
+ },
+ {
+ label: i18n.str`Marathi`,
+ value: "mar",
+ },
+ {
+ label: i18n.str`Telugu`,
+ value: "ten",
+ },
+ {
+ label: i18n.str`Korean`,
+ value: "kor",
+ },
+ {
+ label: i18n.str`French`,
+ value: "fra",
+ },
+ {
+ label: i18n.str`Tamil`,
+ value: "tam",
+ },
+ {
+ label: i18n.str`Egyptian Arabic`,
+ value: "arz",
+ },
+ {
+ label: i18n.str`Standard German`,
+ value: "deu",
+ },
+ {
+ label: i18n.str`Urdu`,
+ value: "urd",
+ },
+ {
+ label: i18n.str`Javanese`,
+ value: "jav",
+ },
+ {
+ label: i18n.str`Punjabi`,
+ value: "pan",
+ },
+ {
+ label: i18n.str`Italian`,
+ value: "ita",
+ },
+ {
+ label: i18n.str`Gujarati`,
+ value: "guj",
+ },
+ {
+ label: i18n.str`Iranian Persian`,
+ value: "pes",
+ },
+ {
+ label: i18n.str`Bhojpuri`,
+ value: "bho",
+ },
+ {
+ label: i18n.str`Hausa`,
+ value: "hau",
+ },
+];
+
+
+export const preloadedForms: (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: i18n.str`United States dollar`,
+ value: "usd",
+ },
+ {
+ label: i18n.str`Euro`,
+ value: "eur",
+ },
+ {
+ label: i18n.str`Swiss franc`,
+ value: "chf",
+ },
+ {
+ label: i18n.str`Argentine peso`,
+ value: "ars",
+ },
+ {
+ label: i18n.str`Mexican peso`,
+ value: "mxn",
+ },
+ {
+ 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
new file mode 100644
index 000000000..4cd781b74
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -0,0 +1,91 @@
+/*
+ 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 {
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+
+export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ id: ".comment" as UIHandlerId,
+ name: "comment",
+ label: i18n.str`Comment`,
+ },
+ ],
+ },
+ 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 === TalerExchangeApi.AmlState.frozen,
+ // },
+ // };
+ // },
+});
+
+export function resolutionSection(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormSection {
+ return {
+ title: i18n.str`Resolution`,
+ fields: [
+ {
+ type: "choiceHorizontal",
+ id: ".state" as UIHandlerId,
+ name: "state",
+ label: i18n.str`New state`,
+ converterId: "TalerExchangeApi.AmlState",
+ choices: [
+ {
+ value: "frozen",
+ label: i18n.str`Frozen`,
+ },
+ {
+ value: "pending",
+ label: i18n.str`Pending`,
+ },
+ {
+ value: "normal",
+ label: i18n.str`Normal`,
+ },
+ ],
+ },
+ {
+ type: "amount",
+ id: ".threshold" as UIHandlerId,
+ currency: "NETZBON",
+ name: "threshold",
+ converterId: "Taler.Amount",
+ label: i18n.str`New threshold`,
+ },
+ ],
+ };
+}
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..70b2db571
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -0,0 +1,227 @@
+/*
+ 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 {
+ UIFieldHandler,
+ UIFormElementConfig,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { undefinedIfEmpty } from "../pages/CreateAccount.js";
+
+// export type UIField = {
+// value: string | undefined;
+// onUpdate: (s: string) => void;
+// error: TranslatedString | undefined;
+// };
+
+export type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIFieldHandler
+ : T[k] extends AmountJson
+ ? UIFieldHandler
+ : T[k] extends TalerExchangeApi.AmlState
+ ? UIFieldHandler
+ : 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 AbsoluteTime
+ ? 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>(
+ shape: Array<UIHandlerId>,
+ form: RecursivePartial<FormValues<T>>,
+ updateForm: (d: RecursivePartial<FormValues<T>>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ const handler = shape.reduce((handleForm, fieldId) => {
+ const path = fieldId.split(".");
+
+ function updater(newValue: unknown) {
+ updateForm(setValueDeeper(form, path, newValue));
+ }
+
+ const currentValue = getValueDeeper<string>(form as any, path, undefined);
+ const currentError = getValueDeeper<TranslatedString>(
+ errors as any,
+ path,
+ undefined,
+ );
+ const field: UIFieldHandler = {
+ error: currentError,
+ value: currentValue,
+ onChange: updater,
+ state: {}, //FIXME: add the state of the field (hidden, )
+ };
+
+ return setValueDeeper(handleForm, path, field);
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ shape: Array<UIHandlerId>,
+ 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(shape, form, updateForm, status.errors);
+
+ return [handler, status];
+}
+
+interface Tree<T> extends Record<string, Tree<T> | T> {}
+
+export function getValueDeeper<T>(
+ object: Tree<T> | undefined,
+ names: string[],
+ notFoundValue?: T,
+): T | undefined {
+ if (names.length === 0) return object as T;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest, notFoundValue);
+ }
+ if (object === undefined) {
+ return notFoundValue;
+ }
+ return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+}
+
+export function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (!head) {
+ return setValueDeeper(object, rest, value);
+ }
+ if (object === undefined) {
+ return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+ }
+ return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) });
+}
+
+export function getShapeFromFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getShapeFromFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+
+export function getRequiredFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ if (!field.required) {
+ return;
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getRequiredFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+export function validateRequiredFields<FormType>(
+ errors: FormErrors<FormType> | undefined,
+ form: object,
+ fields: Array<UIHandlerId>,
+): FormErrors<FormType> | undefined {
+ let result: FormErrors<FormType> | undefined = errors;
+ fields.forEach((f) => {
+ const path = f.split(".");
+ const v = getValueDeeper(form as any, path);
+ result = setValueDeeper(result, path, !v ? "required" : undefined);
+ });
+ return result;
+}
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/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
new file mode 100644
index 000000000..12e85d249
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -0,0 +1,85 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Codec,
+ TranslatedString,
+ buildCodecForObject,
+ codecForBoolean
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+interface Preferences {
+ allowInsecurePassword: boolean;
+ keepSessionAfterReload: boolean;
+}
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("allowInsecurePassword", (codecForBoolean()))
+ .property("keepSessionAfterReload", (codecForBoolean()))
+ .build("Preferences");
+
+const defaultPreferences: Preferences = {
+ allowInsecurePassword: false,
+ keepSessionAfterReload: false,
+};
+
+const PREFERENCES_KEY = buildStorageKey(
+ "exchange-preferences",
+ codecForPreferences(),
+);
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+] {
+ const { value, update } = useLocalStorage(
+ PREFERENCES_KEY,
+ defaultPreferences,
+ );
+
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ return [value, updateField];
+}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "allowInsecurePassword",
+ "keepSessionAfterReload",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "allowInsecurePassword": return i18n.str`Allow Insecure password`
+ case "keepSessionAfterReload": return i18n.str`Keep session after reload`
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
new file mode 100644
index 000000000..78574ada4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
@@ -0,0 +1,51 @@
+/*
+ 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 _useSWR, { SWRHook } from "swr";
+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 { lib: {exchange: api} } = useExchangeApiContext();
+
+ async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
+ return await api.getDecisionDetails(officer, account)
+ }
+
+ const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>(
+ !session ? undefined : [session, paytoHash], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
new file mode 100644
index 000000000..d3a1c1018
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -0,0 +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";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import {
+ OfficerAccount,
+ OperationOk,
+ TalerExchangeApi,
+ TalerExchangeResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook } from "swr";
+import { useOfficer } from "./officer.js";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export const PAGINATED_LIST_SIZE = 10;
+// 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: TalerExchangeApi.AmlState) {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ const [offset, setOffset] = useState<string>();
+
+ async function fetcher([officer, state, offset]: [
+ OfficerAccount,
+ TalerExchangeApi.AmlState,
+ string | undefined,
+ ]) {
+ return await api.getDecisionsByState(officer, state, {
+ order: "asc",
+ offset,
+ limit: PAGINATED_LIST_REQUEST,
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getDecisionsByState">,
+ TalerHttpError
+ >(
+ !session ? undefined : [session, state, offset, "getDecisionsByState"],
+ fetcher,
+ );
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
+}
+
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+};
+
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<R, OffId>(
+ data: R[],
+ offset: OffId | undefined,
+ setOffset: (o: OffId | undefined) => void,
+ getId: (r: R) => OffId,
+): PaginatedResult<R[]> {
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1]);
+ setOffset(id);
+ },
+ loadFirst: () => {
+ setOffset(undefined);
+ },
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/i18n/bank.pot b/packages/aml-backoffice-ui/src/i18n/bank.pot
new file mode 100644
index 000000000..39f9de5ce
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/bank.pot
@@ -0,0 +1,486 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would work. "
+"In addition to using your own bank account, you can also see the transaction "
+"history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
diff --git a/packages/aml-backoffice-ui/src/i18n/de.po b/packages/aml-backoffice-ui/src/i18n/de.po
new file mode 100644
index 000000000..dc76f83e2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/de.po
@@ -0,0 +1,486 @@
+# This file is part of GNU Taler
+# (C) 2021 Taler Systems S.A.
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-12-26 23:30+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr "Abmelden"
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr "Navigationsmenü überspringen"
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr "Taler-Logo"
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr "Bitte melden Sie sich an!"
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr "payto-Adresse"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr "Datum"
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr "Betrag"
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Empfänger"
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr "Verwendungszweck"
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr "Kontostand"
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr "Buchungen auf öffentlich sichtbaren Konten"
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
diff --git a/packages/aml-backoffice-ui/src/i18n/en.po b/packages/aml-backoffice-ui/src/i18n/en.po
new file mode 100644
index 000000000..83778f785
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/en.po
@@ -0,0 +1,511 @@
+# This file is part of GNU Taler
+# (C) 2021 Taler Systems S.A.
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-01-08 09:57+0100\n"
+"Last-Translator: <translate@taler.net>\n"
+"Language-Team: English\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, fuzzy, c-format
+msgid "Amount to withdraw:"
+msgstr "Amount to withdraw"
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr "Confirm withdrawal"
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, fuzzy, c-format
+msgid "Transfer to Taler Wallet"
+msgstr "Charge Taler wallet"
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, fuzzy, c-format
+msgid "Confirm Withdrawal"
+msgstr "Confirm withdrawal"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, fuzzy, c-format
+msgid "Could not confirm the withdrawal"
+msgstr "Confirm withdrawal"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, fuzzy, c-format
+msgid "Could not abort the withdrawal."
+msgstr "Close Taler withdrawal"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr "days"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "hours"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutes"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "seconds"
+
+#~ msgid "Go back"
+#~ msgstr "Go back"
+
+#, fuzzy
+#~ msgid "Start withdrawal"
+#~ msgstr "Start withdrawal"
+
+#, fuzzy
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr "Charge Taler wallet"
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr "Page has a problem: logged in but backend state is lost."
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr "Welcome to euFin bank: Taler+IBAN now possible!"
+
+#~ msgid "Page has a problem:"
+#~ msgstr "Page has a problem:"
+
+#~ msgid "Close"
+#~ msgstr "Close"
+
+#~ msgid "Sign in"
+#~ msgstr "Sign in"
diff --git a/packages/aml-backoffice-ui/src/i18n/es.po b/packages/aml-backoffice-ui/src/i18n/es.po
new file mode 100644
index 000000000..0787b1035
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/es.po
@@ -0,0 +1,497 @@
+# This file is part of GNU Taler
+# (C) 2021 Taler Systems S.A.
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-12-09 14:13+0000\n"
+"Last-Translator: Sebastian Marchano <sebasjm@gmail.com>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/taler-"
+"bank-spa/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr "Cierre de sesión"
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr "Saltar el menú de navegación"
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+"Esta parte de la demostración muestra cómo funciona un banco que soporta "
+"Taler directamente. Además de usar tu propia cuenta de banco, también podrás "
+"ver el historial de transacciones de algunas %1$s."
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr "Logo Taler"
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr "Falta nombre de usuario"
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr "Falta contraseña"
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr "Por favor inicia sesión!"
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr "Nombre de usuario:"
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr "Password:"
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr "Iniciar sesión"
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr "Registrarse"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr "Falta IBAN"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr "IBAN debería tener letras mayúsculas y números"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr "Falta asunto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr "Falta monto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr "Monto no válido"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr "Debería ser mas grande que 0"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr "IBAN receptor:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr "Asunto de transferencia:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr "Monto:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr "Faltan campo(s)."
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr "Quieres probar el formato payto:// ?"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr "Falta direccion payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr "Payto no sigue el patrón"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr "Transferir dinero a la cuenta identificada por la URI payto://:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr "payto URI:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr "direccion payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr "Envíar"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr "Usar el formulario de transferencia bancaria?"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr "Se dieron las credenciales incorrectas."
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr "No se pudo create la transferencia bancaria"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr "La creación de la transferencia dió una respuesta erronea"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr "Transferencia bancaria creada!"
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr "Monto a retirar:"
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr "Retirar"
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr "Se dieron las credenciales incorrectas."
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr "No se pude create la operación de retiro"
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr "La creación de retiro dió una respuesta errónea"
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr "Obtener dinero digital"
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr "Transferir a una cuenta bancaria"
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Contraparte"
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr "Transferir a una cartera Taler"
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr "Usar el código QR para retirar a tu cartera móvil:"
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr "Click %1$s para abrir una cartera Taler!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr "Confirmar retirada"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr "Autorizar retiro resolviendo una pregunta"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr "Cuanto es"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr "La respuesta es incorrecta."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+"En este punto, un banco %1$s preguntaría por una prueba adicional de "
+"autenticación (PIN/TAN, password de un solo uso, ....), en vez de un simple "
+"cálculo."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr "No ID de retiro encontrado."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr "No se pudo confirmar la retirada"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr "La confirmación de retiro dió una respuesta errónea"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr "El retiro fue confirmado!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr "No se pudo cancelar el retiro."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr "La cancelación del retiro falló."
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr "Este retiro fue cancelado!"
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr "Cancelar"
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr "retiro (%1$s) nunca fue (correctamente) generado en el banco..."
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr "Esperando que el banco genere la operación...."
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr "Este retiro fue cancelado!"
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr "Bienvenido a %1$s!"
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+"Nombre de usuario o etiqueta de cuenta '%1$s' no encontrada. No se iniciará "
+"sesión."
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr "Se dieron las credenciales incorrectas."
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr "La información de la cuenta no pudo ser accedida."
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr "Bienvenido/a, %1$s!"
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr "Balance de cuenta bancaria"
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr "Pagos"
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr "Últimas transacciones:"
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr "La lista de cuentas públicas no fue encontrada."
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr "La lista de cuentas públicas no pudo ser accedida."
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr "Historial de cuentas públicas"
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr "Actualmente, el banco no está aceptado nuevos registros!"
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr "Solo use letras y números comenzando con una letra minúscula"
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr "La contraseña no coincide"
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr "Por favor, registrese!"
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr "Repita la contraseña:"
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr "El registro falló, por favor reportelo"
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr "El nombre del usuario ya está tomado"
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr "Nuevo registro dió una respuesta errónea"
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr "Menu del banco"
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr "Seleccione opción 1"
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr "Seleccione opción 2"
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr "días"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "horas"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutos"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "segundos"
+
+#~ msgid "this link"
+#~ msgstr "este link"
diff --git a/packages/aml-backoffice-ui/src/i18n/fr.po b/packages/aml-backoffice-ui/src/i18n/fr.po
new file mode 100644
index 000000000..8148f6a0c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/fr.po
@@ -0,0 +1,486 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would work. "
+"In addition to using your own bank account, you can also see the transaction "
+"history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, c-format
+msgid "Amount:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, c-format
+msgid "Missing payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, c-format
+msgid "Use wire-transfer form?"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, c-format
+msgid "No credentials found."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, c-format
+msgid "Amount to withdraw:"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, c-format
+msgid "No credentials given."
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, c-format
+msgid "Transfer to bank account"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, c-format
+msgid "Transfer to Taler Wallet"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr ""
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, c-format
+msgid "Could not confirm the withdrawal"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, c-format
+msgid "Withdrawal confirmed!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, c-format
+msgid "Could not abort the withdrawal."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, c-format
+msgid "Withdrawal abortion failed."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, c-format
+msgid "Withdrawal aborted!"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, c-format
+msgid "Bank account balance"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr ""
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, c-format
+msgid "Please register!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, c-format
+msgid "Registration failed, please report"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
diff --git a/packages/aml-backoffice-ui/src/i18n/it.po b/packages/aml-backoffice-ui/src/i18n/it.po
new file mode 100644
index 000000000..a3a599376
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/it.po
@@ -0,0 +1,521 @@
+# This file is part of GNU Taler
+# (C) 2021 Taler Systems S.A.
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2022-12-26 23:30+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/pages/home/BankFrame.tsx:55
+#, c-format
+msgid "Logout"
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:73
+#, c-format
+msgid "Skip to main content"
+msgstr "Saltare il menu di navigazione"
+
+#: src/pages/home/BankFrame.tsx:82
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work. In addition to using your own bank account, you can also see the "
+"transaction history of some %1$s."
+msgstr ""
+
+#: src/pages/home/BankFrame.tsx:94
+#, c-format
+msgid "Taler logo"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:41
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:42
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:49
+#, c-format
+msgid "Please login!"
+msgstr "Accedi!"
+
+#: src/pages/home/LoginForm.tsx:51
+#, c-format
+msgid "Username:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:71
+#, c-format
+msgid "Password:"
+msgstr ""
+
+#: src/pages/home/LoginForm.tsx:100
+#, c-format
+msgid "Login"
+msgstr "Accedi"
+
+#: src/pages/home/LoginForm.tsx:110
+#, c-format
+msgid "Register"
+msgstr "Registrati"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:60
+#, c-format
+msgid "Missing IBAN"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:62
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:64
+#, c-format
+msgid "Missing subject"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:66
+#, c-format
+msgid "Missing amount"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:68
+#, c-format
+msgid "Amount is not valid"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:70
+#, c-format
+msgid "Should be greater than 0"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:79
+#, c-format
+msgid "Receiver IBAN:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "Transfer subject:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:123
+#, fuzzy, c-format
+msgid "Amount:"
+msgstr "Somma"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:177
+#, c-format
+msgid "Field(s) missing."
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:227
+#, c-format
+msgid "Want to try the raw payto://-format?"
+msgstr "Prova il trasferimento tramite il formato Payto!"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:235
+#, fuzzy, c-format
+msgid "Missing payto address"
+msgstr "indirizzo Payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:237
+#, c-format
+msgid "Payto does not follow the pattern"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:243
+#, fuzzy, c-format
+msgid "Transfer money to account identified by payto:// URI:"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:246
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:255
+#, c-format
+msgid "payto address"
+msgstr "indirizzo Payto"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:279
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:314
+#, fuzzy, c-format
+msgid "Use wire-transfer form?"
+msgstr "Chiudi il bonifico"
+
+#: src/pages/home/PaytoWireTransferForm.tsx:373
+#, fuzzy, c-format
+msgid "No credentials found."
+msgstr "Credenziali invalide."
+
+#: src/pages/home/PaytoWireTransferForm.tsx:397
+#, c-format
+msgid "Could not create the wire transfer"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:414
+#, c-format
+msgid "Transfer creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaytoWireTransferForm.tsx:426
+#, fuzzy, c-format
+msgid "Wire transfer created!"
+msgstr "Bonifico"
+
+#: src/pages/home/WalletWithdrawForm.tsx:50
+#, fuzzy, c-format
+msgid "Amount to withdraw:"
+msgstr "Somma da ritirare"
+
+#: src/pages/home/WalletWithdrawForm.tsx:84
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr "Conferma il ritiro"
+
+#: src/pages/home/WalletWithdrawForm.tsx:128
+#, fuzzy, c-format
+msgid "No credentials given."
+msgstr "Credenziali invalide."
+
+#: src/pages/home/WalletWithdrawForm.tsx:155
+#, c-format
+msgid "Could not create withdrawal operation"
+msgstr ""
+
+#: src/pages/home/WalletWithdrawForm.tsx:171
+#, c-format
+msgid "Withdrawal creation gave response error"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:44
+#, c-format
+msgid "Obtain digital cash"
+msgstr ""
+
+#: src/pages/home/PaymentOptions.tsx:52
+#, fuzzy, c-format
+msgid "Transfer to bank account"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/home/Transactions.tsx:69
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/pages/home/Transactions.tsx:70
+#, c-format
+msgid "Amount"
+msgstr "Somma"
+
+#: src/pages/home/Transactions.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Controparte"
+
+#: src/pages/home/Transactions.tsx:72
+#, c-format
+msgid "Subject"
+msgstr "Causale"
+
+#: src/pages/home/QrCodeSection.tsx:41
+#, fuzzy, c-format
+msgid "Transfer to Taler Wallet"
+msgstr "Ritira contante nel portafoglio Taler"
+
+#: src/pages/home/QrCodeSection.tsx:44
+#, fuzzy, c-format
+msgid "Use this QR code to withdraw to your mobile wallet:"
+msgstr "Usa questo codice QR per ritirare contante nel tuo wallet:"
+
+#: src/pages/home/QrCodeSection.tsx:47
+#, c-format
+msgid "Click %1$s to open your Taler wallet!"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:47
+#, c-format
+msgid "Confirm Withdrawal"
+msgstr "Conferma il ritiro"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:52
+#, c-format
+msgid "Authorize withdrawal by solving challenge"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:55
+#, c-format
+msgid "What is"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:94
+#, c-format
+msgid "Answer is wrong."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:99
+#, c-format
+msgid "Confirm"
+msgstr "Conferma"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:113
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:120
+#, c-format
+msgid ""
+"A this point, a %1$s bank would ask for an additional authentication proof "
+"(PIN/TAN, one time password, ..), instead of a simple calculation."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:166
+#, c-format
+msgid "No withdrawal ID found."
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:201
+#, fuzzy, c-format
+msgid "Could not confirm the withdrawal"
+msgstr "Conferma il ritiro"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:219
+#, c-format
+msgid "Withdrawal confirmation gave response error"
+msgstr ""
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:231
+#, fuzzy, c-format
+msgid "Withdrawal confirmed!"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:294
+#, fuzzy, c-format
+msgid "Could not abort the withdrawal."
+msgstr "Chiudi il ritiro Taler"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:311
+#, fuzzy, c-format
+msgid "Withdrawal abortion failed."
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/WithdrawalConfirmationQuestion.tsx:324
+#, fuzzy, c-format
+msgid "Withdrawal aborted!"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/WithdrawalQRCode.tsx:54
+#, c-format
+msgid "Abort"
+msgstr "Annulla"
+
+#: src/pages/home/WithdrawalQRCode.tsx:74
+#, c-format
+msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+msgstr ""
+
+#: src/pages/home/WithdrawalQRCode.tsx:88
+#, fuzzy, c-format
+msgid "Waiting the bank to create the operation..."
+msgstr "La banca sta creando l'operazione..."
+
+#: src/pages/home/WithdrawalQRCode.tsx:102
+#, c-format
+msgid "This withdrawal was aborted!"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/home/AccountPage.tsx:40
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:133
+#, c-format
+msgid "Username or account label '%1$s' not found. Won't login."
+msgstr "L'utente '%1$s' non esiste. Login impossibile"
+
+#: src/pages/home/AccountPage.tsx:159
+#, c-format
+msgid "Wrong credentials given."
+msgstr "Credenziali invalide."
+
+#: src/pages/home/AccountPage.tsx:169
+#, c-format
+msgid "Account information could not be retrieved."
+msgstr "Impossibile ricevere le informazioni relative al conto."
+
+#: src/pages/home/AccountPage.tsx:210
+#, c-format
+msgid "Welcome, %1$s !"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:221
+#, fuzzy, c-format
+msgid "Bank account balance"
+msgstr "Bilancio:"
+
+#: src/pages/home/AccountPage.tsx:237
+#, c-format
+msgid "Payments"
+msgstr ""
+
+#: src/pages/home/AccountPage.tsx:243
+#, c-format
+msgid "Latest transactions:"
+msgstr "Ultime transazioni:"
+
+#: src/pages/home/PublicHistoriesPage.tsx:83
+#, c-format
+msgid "List of public accounts was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/home/PublicHistoriesPage.tsx:95
+#, c-format
+msgid "List of public accounts could not be retrieved."
+msgstr "Lista conti pubblici non pervenuta."
+
+#: src/pages/home/PublicHistoriesPage.tsx:143
+#, c-format
+msgid "History of public accounts"
+msgstr "Storico dei conti pubblici"
+
+#: src/pages/home/RegistrationPage.tsx:39
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:68
+#, c-format
+msgid "Use only letter and numbers starting with a lower case letter"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:78
+#, c-format
+msgid "Password don't match"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:89
+#, fuzzy, c-format
+msgid "Please register!"
+msgstr "Accedi!"
+
+#: src/pages/home/RegistrationPage.tsx:126
+#, c-format
+msgid "Repeat Password:"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:226
+#, fuzzy, c-format
+msgid "Registration failed, please report"
+msgstr "Registrazione"
+
+#: src/pages/home/RegistrationPage.tsx:239
+#, c-format
+msgid "That username is already taken"
+msgstr ""
+
+#: src/pages/home/RegistrationPage.tsx:248
+#, c-format
+msgid "New registration gave response error"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:53
+#, c-format
+msgid "Bank menu"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:59
+#, c-format
+msgid "Select option1"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:66
+#, c-format
+msgid "Select option2"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#~ msgid "this link"
+#~ msgstr "questo link"
+
+#~ msgid "Clear"
+#~ msgstr "Cancella"
+
+#~ msgid "Demo Bank"
+#~ msgstr "Banca 'demo'"
+
+#~ msgid "Go back"
+#~ msgstr "Indietro"
+
+#~ msgid "Transfer money via the Payto system:"
+#~ msgstr "Effettua un bonifico tramite il sistema Payto:"
+
+#~ msgid "Start withdrawal"
+#~ msgstr "Ritira contante"
+
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr "Ritira contante nel portafoglio Taler"
+
+#~ msgid "Register to the euFin bank!"
+#~ msgstr "Apri un conto in banca euFin!"
+
+#~ msgid "Transfer money manually"
+#~ msgstr "Effettua un bonifico"
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr ""
+#~ "Stato inconsistente: accesso utente effettuato ma stato con server perso."
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr "Benvenuti in banca euFin!"
diff --git a/packages/aml-backoffice-ui/src/i18n/poheader b/packages/aml-backoffice-ui/src/i18n/poheader
new file mode 100644
index 000000000..d7a371934
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/aml-backoffice-ui/src/i18n/strings-prelude b/packages/aml-backoffice-ui/src/i18n/strings-prelude
new file mode 100644
index 000000000..3ab0fd1e5
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/strings-prelude
@@ -0,0 +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/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: {[s: string]: any} = {};
+
diff --git a/packages/aml-backoffice-ui/src/i18n/strings.ts b/packages/aml-backoffice-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..4f7419eb4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/strings.ts
@@ -0,0 +1,510 @@
+/*
+ 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/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: { [s: string]: any } = {};
+
+strings["de"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "de",
+ },
+ Logout: [""],
+ "Skip to main content": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "Taler logo": [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ "Please login!": [""],
+ "Username:": [""],
+ "Password:": [""],
+ Login: [""],
+ Register: [""],
+ "Missing IBAN": [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "Missing subject": [""],
+ "Missing amount": [""],
+ "Amount is not valid": [""],
+ "Should be greater than 0": [""],
+ "Receiver IBAN:": [""],
+ "Transfer subject:": [""],
+ "Amount:": [""],
+ "Field(s) missing.": [""],
+ "Want to try the raw payto://-format?": [""],
+ "Missing payto address": [""],
+ "Payto does not follow the pattern": [""],
+ "Transfer money to account identified by payto:// URI:": [""],
+ "payto URI:": [""],
+ "payto address": [""],
+ Send: [""],
+ "Use wire-transfer form?": [""],
+ "No credentials found.": [""],
+ "Could not create the wire transfer": [""],
+ "Transfer creation gave response error": [""],
+ "Wire transfer created!": [""],
+ "Amount to withdraw:": [""],
+ Withdraw: [""],
+ "No credentials given.": [""],
+ "Could not create withdrawal operation": [""],
+ "Withdrawal creation gave response error": [""],
+ "Obtain digital cash": [""],
+ "Transfer to bank account": [""],
+ Date: [""],
+ Amount: [""],
+ Counterpart: [""],
+ Subject: [""],
+ "Transfer to Taler Wallet": [""],
+ "Use this QR code to withdraw to your mobile wallet:": [""],
+ "Click %1$s to open your Taler wallet!": [""],
+ "Confirm Withdrawal": [""],
+ "Authorize withdrawal by solving challenge": [""],
+ "What is": [""],
+ "Answer is wrong.": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [""],
+ "No withdrawal ID found.": [""],
+ "Could not confirm the withdrawal": [""],
+ "Withdrawal confirmation gave response error": [""],
+ "Withdrawal confirmed!": [""],
+ "Could not abort the withdrawal.": [""],
+ "Withdrawal abortion failed.": [""],
+ "Withdrawal aborted!": [""],
+ Abort: [""],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [""],
+ "Waiting the bank to create the operation...": [""],
+ "This withdrawal was aborted!": [""],
+ "Welcome to %1$s!": [""],
+ "Username or account label '%1$s' not found. Won't login.": [""],
+ "Wrong credentials given.": [""],
+ "Account information could not be retrieved.": [""],
+ "Welcome, %1$s !": [""],
+ "Bank account balance": [""],
+ Payments: [""],
+ "Latest transactions:": [""],
+ "List of public accounts was not found.": [""],
+ "List of public accounts could not be retrieved.": [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Use only letter and numbers starting with a lower case letter": [""],
+ "Password don't match": [""],
+ "Please register!": [""],
+ "Repeat Password:": [""],
+ "Registration failed, please report": [""],
+ "That username is already taken": [""],
+ "New registration gave response error": [""],
+ "Bank menu": [""],
+ "Select option1": [""],
+ "Select option2": [""],
+ days: [""],
+ hours: [""],
+ minutes: [""],
+ seconds: [""],
+ },
+ },
+};
+
+strings["en"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ },
+ Logout: [""],
+ "Skip to main content": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "Taler logo": [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ "Please login!": [""],
+ "Username:": [""],
+ "Password:": [""],
+ Login: [""],
+ Register: [""],
+ "Missing IBAN": [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "Missing subject": [""],
+ "Missing amount": [""],
+ "Amount is not valid": [""],
+ "Should be greater than 0": [""],
+ "Receiver IBAN:": [""],
+ "Transfer subject:": [""],
+ "Amount:": [""],
+ "Field(s) missing.": [""],
+ "Want to try the raw payto://-format?": [""],
+ "Missing payto address": [""],
+ "Payto does not follow the pattern": [""],
+ "Transfer money to account identified by payto:// URI:": [""],
+ "payto URI:": [""],
+ "payto address": [""],
+ Send: [""],
+ "Use wire-transfer form?": [""],
+ "No credentials found.": [""],
+ "Could not create the wire transfer": [""],
+ "Transfer creation gave response error": [""],
+ "Wire transfer created!": [""],
+ "Amount to withdraw:": ["Amount to withdraw"],
+ Withdraw: ["Confirm withdrawal"],
+ "No credentials given.": [""],
+ "Could not create withdrawal operation": [""],
+ "Withdrawal creation gave response error": [""],
+ "Obtain digital cash": [""],
+ "Transfer to bank account": [""],
+ Date: [""],
+ Amount: [""],
+ Counterpart: [""],
+ Subject: [""],
+ "Transfer to Taler Wallet": ["Charge Taler wallet"],
+ "Use this QR code to withdraw to your mobile wallet:": [""],
+ "Click %1$s to open your Taler wallet!": [""],
+ "Confirm Withdrawal": ["Confirm withdrawal"],
+ "Authorize withdrawal by solving challenge": [""],
+ "What is": [""],
+ "Answer is wrong.": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [""],
+ "No withdrawal ID found.": [""],
+ "Could not confirm the withdrawal": ["Confirm withdrawal"],
+ "Withdrawal confirmation gave response error": [""],
+ "Withdrawal confirmed!": [""],
+ "Could not abort the withdrawal.": ["Close Taler withdrawal"],
+ "Withdrawal abortion failed.": [""],
+ "Withdrawal aborted!": [""],
+ Abort: [""],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [""],
+ "Waiting the bank to create the operation...": [""],
+ "This withdrawal was aborted!": [""],
+ "Welcome to %1$s!": [""],
+ "Username or account label '%1$s' not found. Won't login.": [""],
+ "Wrong credentials given.": [""],
+ "Account information could not be retrieved.": [""],
+ "Welcome, %1$s !": [""],
+ "Bank account balance": [""],
+ Payments: [""],
+ "Latest transactions:": [""],
+ "List of public accounts was not found.": [""],
+ "List of public accounts could not be retrieved.": [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Use only letter and numbers starting with a lower case letter": [""],
+ "Password don't match": [""],
+ "Please register!": [""],
+ "Repeat Password:": [""],
+ "Registration failed, please report": [""],
+ "That username is already taken": [""],
+ "New registration gave response error": [""],
+ "Bank menu": [""],
+ "Select option1": [""],
+ "Select option2": [""],
+ days: ["days"],
+ hours: ["hours"],
+ minutes: ["minutes"],
+ seconds: ["seconds"],
+ },
+ },
+};
+
+strings["es"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ Logout: ["Cierre de sesión"],
+ "Skip to main content": ["Saltar el menú de navegación"],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [
+ "Esta parte de la demostración muestra cómo funciona un banco que soporta Taler directamente. Además de usar tu propia cuenta de banco, también podrás ver el historial de transacciones de algunas %1$s.",
+ ],
+ "Taler logo": ["Logo Taler"],
+ "Missing username": ["Falta nombre de usuario"],
+ "Missing password": ["Falta contraseña"],
+ "Please login!": ["Por favor inicia sesión!"],
+ "Username:": ["Nombre de usuario:"],
+ "Password:": ["Password:"],
+ Login: ["Iniciar sesión"],
+ Register: ["Registrarse"],
+ "Missing IBAN": ["Falta IBAN"],
+ "IBAN should have just uppercased letters and numbers": [
+ "IBAN debería tener letras mayúsculas y números",
+ ],
+ "Missing subject": ["Falta asunto"],
+ "Missing amount": ["Falta monto"],
+ "Amount is not valid": ["Monto no válido"],
+ "Should be greater than 0": ["Debería ser mas grande que 0"],
+ "Receiver IBAN:": ["IBAN receptor:"],
+ "Transfer subject:": ["Asunto de transferencia:"],
+ "Amount:": ["Monto:"],
+ "Field(s) missing.": ["Faltan campo(s)."],
+ "Want to try the raw payto://-format?": [
+ "Quieres probar el formato payto:// ?",
+ ],
+ "Missing payto address": ["Falta direccion payto"],
+ "Payto does not follow the pattern": ["Payto no sigue el patrón"],
+ "Transfer money to account identified by payto:// URI:": [
+ "Transferir dinero a la cuenta identificada por la URI payto://:",
+ ],
+ "payto URI:": ["payto URI:"],
+ "payto address": ["direccion payto"],
+ Send: ["Envíar"],
+ "Use wire-transfer form?": [
+ "Usar el formulario de transferencia bancaria?",
+ ],
+ "No credentials found.": ["Se dieron las credenciales incorrectas."],
+ "Could not create the wire transfer": [
+ "No se pudo create la transferencia bancaria",
+ ],
+ "Transfer creation gave response error": [
+ "La creación de la transferencia dió una respuesta erronea",
+ ],
+ "Wire transfer created!": ["Transferencia bancaria creada!"],
+ "Amount to withdraw:": ["Monto a retirar:"],
+ Withdraw: ["Retirar"],
+ "No credentials given.": ["Se dieron las credenciales incorrectas."],
+ "Could not create withdrawal operation": [
+ "No se pude create la operación de retiro",
+ ],
+ "Withdrawal creation gave response error": [
+ "La creación de retiro dió una respuesta errónea",
+ ],
+ "Obtain digital cash": ["Obtener dinero digital"],
+ "Transfer to bank account": ["Transferir a una cuenta bancaria"],
+ Date: ["Fecha"],
+ Amount: ["Monto"],
+ Counterpart: ["Contraparte"],
+ Subject: ["Asunto"],
+ "Transfer to Taler Wallet": ["Transferir a una cartera Taler"],
+ "Use this QR code to withdraw to your mobile wallet:": [
+ "Usar el código QR para retirar a tu cartera móvil:",
+ ],
+ "Click %1$s to open your Taler wallet!": [
+ "Click %1$s para abrir una cartera Taler!",
+ ],
+ "Confirm Withdrawal": ["Confirmar retirada"],
+ "Authorize withdrawal by solving challenge": [
+ "Autorizar retiro resolviendo una pregunta",
+ ],
+ "What is": ["Cuanto es"],
+ "Answer is wrong.": ["La respuesta es incorrecta."],
+ Confirm: ["Confirmar"],
+ Cancel: ["Cancelar"],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [
+ "En este punto, un banco %1$s preguntaría por una prueba adicional de autenticación (PIN/TAN, password de un solo uso, ....), en vez de un simple cálculo.",
+ ],
+ "No withdrawal ID found.": ["No ID de retiro encontrado."],
+ "Could not confirm the withdrawal": ["No se pudo confirmar la retirada"],
+ "Withdrawal confirmation gave response error": [
+ "La confirmación de retiro dió una respuesta errónea",
+ ],
+ "Withdrawal confirmed!": ["El retiro fue confirmado!"],
+ "Could not abort the withdrawal.": ["No se pudo cancelar el retiro."],
+ "Withdrawal abortion failed.": ["La cancelación del retiro falló."],
+ "Withdrawal aborted!": ["Este retiro fue cancelado!"],
+ Abort: ["Cancelar"],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [
+ "retiro (%1$s) nunca fue (correctamente) generado en el banco...",
+ ],
+ "Waiting the bank to create the operation...": [
+ "Esperando que el banco genere la operación....",
+ ],
+ "This withdrawal was aborted!": ["Este retiro fue cancelado!"],
+ "Welcome to %1$s!": ["Bienvenido a %1$s!"],
+ "Username or account label '%1$s' not found. Won't login.": [
+ "Nombre de usuario o etiqueta de cuenta '%1$s' no encontrada. No se iniciará sesión.",
+ ],
+ "Wrong credentials given.": ["Se dieron las credenciales incorrectas."],
+ "Account information could not be retrieved.": [
+ "La información de la cuenta no pudo ser accedida.",
+ ],
+ "Welcome, %1$s !": ["Bienvenido/a, %1$s!"],
+ "Bank account balance": ["Balance de cuenta bancaria"],
+ Payments: ["Pagos"],
+ "Latest transactions:": ["Últimas transacciones:"],
+ "List of public accounts was not found.": [
+ "La lista de cuentas públicas no fue encontrada.",
+ ],
+ "List of public accounts could not be retrieved.": [
+ "La lista de cuentas públicas no pudo ser accedida.",
+ ],
+ "History of public accounts": ["Historial de cuentas públicas"],
+ "Currently, the bank is not accepting new registrations!": [
+ "Actualmente, el banco no está aceptado nuevos registros!",
+ ],
+ "Use only letter and numbers starting with a lower case letter": [
+ "Solo use letras y números comenzando con una letra minúscula",
+ ],
+ "Password don't match": ["La contraseña no coincide"],
+ "Please register!": ["Por favor, registrese!"],
+ "Repeat Password:": ["Repita la contraseña:"],
+ "Registration failed, please report": [
+ "El registro falló, por favor reportelo",
+ ],
+ "That username is already taken": [
+ "El nombre del usuario ya está tomado",
+ ],
+ "New registration gave response error": [
+ "Nuevo registro dió una respuesta errónea",
+ ],
+ "Bank menu": ["Menu del banco"],
+ "Select option1": ["Seleccione opción 1"],
+ "Select option2": ["Seleccione opción 2"],
+ days: ["días"],
+ hours: ["horas"],
+ minutes: ["minutos"],
+ seconds: ["segundos"],
+ },
+ },
+};
+
+strings["it"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "it",
+ },
+ Logout: [""],
+ "Skip to main content": [""],
+ "This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some %1$s.":
+ [""],
+ "Taler logo": [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ "Please login!": ["Accedi!"],
+ "Username:": [""],
+ "Password:": [""],
+ Login: ["Accedi"],
+ Register: ["Registrati"],
+ "Missing IBAN": [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "Missing subject": [""],
+ "Missing amount": [""],
+ "Amount is not valid": [""],
+ "Should be greater than 0": [""],
+ "Receiver IBAN:": [""],
+ "Transfer subject:": [""],
+ "Amount:": ["Somma"],
+ "Field(s) missing.": [""],
+ "Want to try the raw payto://-format?": [
+ "Prova il trasferimento tramite il formato Payto!",
+ ],
+ "Missing payto address": ["indirizzo Payto"],
+ "Payto does not follow the pattern": [""],
+ "Transfer money to account identified by payto:// URI:": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ "payto URI:": [""],
+ "payto address": ["indirizzo Payto"],
+ Send: [""],
+ "Use wire-transfer form?": ["Chiudi il bonifico"],
+ "No credentials found.": ["Credenziali invalide."],
+ "Could not create the wire transfer": [""],
+ "Transfer creation gave response error": [""],
+ "Wire transfer created!": ["Bonifico"],
+ "Amount to withdraw:": ["Somma da ritirare"],
+ Withdraw: ["Conferma il ritiro"],
+ "No credentials given.": ["Credenziali invalide."],
+ "Could not create withdrawal operation": [""],
+ "Withdrawal creation gave response error": [""],
+ "Obtain digital cash": [""],
+ "Transfer to bank account": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ Date: [""],
+ Amount: ["Somma"],
+ Counterpart: ["Controparte"],
+ Subject: ["Causale"],
+ "Transfer to Taler Wallet": ["Ritira contante nel portafoglio Taler"],
+ "Use this QR code to withdraw to your mobile wallet:": [
+ "Usa questo codice QR per ritirare contante nel tuo wallet:",
+ ],
+ "Click %1$s to open your Taler wallet!": [""],
+ "Confirm Withdrawal": ["Conferma il ritiro"],
+ "Authorize withdrawal by solving challenge": [""],
+ "What is": [""],
+ "Answer is wrong.": [""],
+ Confirm: ["Conferma"],
+ Cancel: [""],
+ "A this point, a %1$s bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation.":
+ [""],
+ "No withdrawal ID found.": [""],
+ "Could not confirm the withdrawal": ["Conferma il ritiro"],
+ "Withdrawal confirmation gave response error": [""],
+ "Withdrawal confirmed!": ["Questo ritiro è stato annullato!"],
+ "Could not abort the withdrawal.": ["Chiudi il ritiro Taler"],
+ "Withdrawal abortion failed.": ["Questo ritiro è stato annullato!"],
+ "Withdrawal aborted!": ["Questo ritiro è stato annullato!"],
+ Abort: ["Annulla"],
+ "withdrawal (%1$s) was never (correctly) created at the bank...": [""],
+ "Waiting the bank to create the operation...": [
+ "La banca sta creando l'operazione...",
+ ],
+ "This withdrawal was aborted!": ["Questo ritiro è stato annullato!"],
+ "Welcome to %1$s!": [""],
+ "Username or account label '%1$s' not found. Won't login.": [
+ "L'utente '%1$s' non esiste. Login impossibile",
+ ],
+ "Wrong credentials given.": ["Credenziali invalide."],
+ "Account information could not be retrieved.": [
+ "Impossibile ricevere le informazioni relative al conto.",
+ ],
+ "Welcome, %1$s !": [""],
+ "Bank account balance": ["Bilancio:"],
+ Payments: [""],
+ "Latest transactions:": ["Ultime transazioni:"],
+ "List of public accounts was not found.": [
+ "Lista conti pubblici non trovata.",
+ ],
+ "List of public accounts could not be retrieved.": [
+ "Lista conti pubblici non pervenuta.",
+ ],
+ "History of public accounts": ["Storico dei conti pubblici"],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Use only letter and numbers starting with a lower case letter": [""],
+ "Password don't match": [""],
+ "Please register!": ["Accedi!"],
+ "Repeat Password:": [""],
+ "Registration failed, please report": ["Registrazione"],
+ "That username is already taken": [""],
+ "New registration gave response error": [""],
+ "Bank menu": [""],
+ "Select option1": [""],
+ "Select option2": [""],
+ days: [""],
+ hours: [""],
+ minutes: [""],
+ seconds: [""],
+ },
+ },
+};
diff --git a/packages/aml-backoffice-ui/src/index.html b/packages/aml-backoffice-ui/src/index.html
new file mode 100644
index 000000000..0ed2f8178
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (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
+ 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
+-->
+<!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>
+ <!-- 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>
+
+</html>
diff --git a/packages/aml-backoffice-ui/src/index.tsx b/packages/aml-backoffice-ui/src/index.tsx
new file mode 100644
index 000000000..c6f6b4a8f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/index.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 { App } from "./App.js";
+import { h, render } from "preact";
+
+const app = document.getElementById("app");
+if (!app) {
+ console.error("could not found app element")
+} else {
+ render(<App />, app);
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
new file mode 100644
index 000000000..bb936cebf
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -0,0 +1,472 @@
+/*
+ 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,
+ buildCodecForObject,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import {
+ DefaultForm,
+ ErrorLoading,
+ FormMetadata,
+ InternationalizationAPI,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { privatePages } from "../Routing.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
+import { useCaseDetails } from "../hooks/useCaseDetails.js";
+import { ShowConsolidated } from "./ShowConsolidated.js";
+
+export type AmlEvent =
+ | AmlFormEvent
+ | AmlFormEventError
+ | KycCollectionEvent
+ | KycExpirationEvent;
+
+type AmlFormEvent = {
+ type: "aml-form";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ justification: Justification;
+ metadata: FormMetadata;
+ state: TalerExchangeApi.AmlState;
+ threshold: AmountJson;
+};
+type AmlFormEventError = {
+ type: "aml-form-error";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ justification: undefined;
+ metadata: undefined;
+ state: TalerExchangeApi.AmlState;
+ threshold: AmountJson;
+};
+type KycCollectionEvent = {
+ type: "kyc-collection";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ values: object;
+ provider: string;
+};
+type KycExpirationEvent = {
+ type: "kyc-expiration";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ fields: string[];
+};
+
+type WithTime = { when: AbsoluteTime };
+
+function selectSooner(a: WithTime, b: WithTime) {
+ return AbsoluteTime.cmp(a.when, b.when);
+}
+
+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 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);
+ }
+ }
+}
+
+export function getEventsFromAmlHistory(
+ aml: TalerExchangeApi.AmlDecisionDetail[],
+ kyc: TalerExchangeApi.KycDetail[],
+ i18n: InternationalizationAPI,
+ forms: FormMetadata[],
+): AmlEvent[] {
+ const ae: AmlEvent[] = aml.map((a) => {
+ 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, i18n),
+ metadata: just.type === "ok" ? just.body.metadata : undefined,
+ justification: just.type === "ok" ? just.body.justification : undefined,
+ when: {
+ t_ms:
+ a.decision_time.t_s === "never"
+ ? "never"
+ : a.decision_time.t_s * 1000,
+ },
+ } as AmlEvent;
+ });
+ const ke = kyc.reduce((prev, k) => {
+ prev.push({
+ type: "kyc-collection",
+ 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: i18n.str`expiration`,
+ when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
+ fields: !k.attributes ? [] : Object.keys(k.attributes),
+ });
+ return prev;
+ }, [] as AmlEvent[]);
+ return ae.concat(ke).sort(selectSooner);
+}
+
+export function CaseDetails({ account }: { account: string }) {
+ const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
+ const [showForm, setShowForm] = useState<{
+ justification: Justification;
+ metadata: FormMetadata;
+ }>();
+
+ const { i18n } = useTranslationContext();
+ const details = useCaseDetails(account);
+ const { forms } = useUiFormsContext();
+
+ const allForms = [...forms, ...preloadedForms(i18n)];
+ if (!details) {
+ return <Loading />;
+ }
+ if (details instanceof TalerError) {
+ return <ErrorLoading error={details} />;
+ }
+ if (details.type === "fail") {
+ switch (details.case) {
+ 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 events = getEventsFromAmlHistory(
+ aml_history,
+ kyc_attributes,
+ i18n,
+ allForms,
+ );
+
+ if (showForm !== undefined) {
+ 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={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>
+ </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>
+ </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;
+ }
+ case "aml-form-error":
+ }
+ }}
+ />
+ {/* {selected && <ShowEventDetails event={selected} />} */}
+ {selected && <ShowConsolidated history={events} until={selected} />}
+ </div>
+ );
+}
+
+function AmlStateBadge({ state }: { 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>
+ );
+ }
+ }
+ 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
+ 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"
+ ) : (
+ <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
+ {format(e.when.t_ms, "dd MMM yyyy")}
+ </time>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </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..7801625d0
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -0,0 +1,284 @@
+/*
+ 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,
+ FormMetadata,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
+ UIHandlerId,
+ convertUiField,
+ getConverterById,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { privatePages } from "../Routing.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
+import {
+ FormErrors,
+ getRequiredFields,
+ getShapeFromFields,
+ useFormState,
+ validateRequiredFields,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { Justification } from "./CaseDetails.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+
+function searchForm(
+ i18n: InternationalizationAPI,
+ forms: FormMetadata[],
+ formId: string,
+): FormMetadata | undefined {
+ {
+ const found = forms.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ {
+ const pf = preloadedForms(i18n);
+ const found = pf.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ return undefined;
+}
+
+type FormType = {
+ when: AbsoluteTime;
+ state: TalerExchangeApi.AmlState;
+ threshold: AmountJson;
+ comment: string;
+};
+
+export function CaseUpdate({
+ account,
+ type: formId,
+}: {
+ account: string;
+ type: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const officer = useOfficer();
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { config } = useExchangeApiContext();
+ const { forms } = useUiFormsContext();
+ const initial: FormType = {
+ when: AbsoluteTime.now(),
+ state: TalerExchangeApi.AmlState.pending,
+ threshold: Amounts.zeroOfCurrency(config.currency),
+ comment: "",
+ };
+
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+ const theForm = searchForm(i18n, forms, formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
+
+ const shape: Array<UIHandlerId> = [];
+ const requiredFields: Array<UIHandlerId> = [];
+
+ theForm.config.design.forEach((section) => {
+ Array.prototype.push.apply(shape, getShapeFromFields(section.fields));
+ Array.prototype.push.apply(
+ requiredFields,
+ getRequiredFields(section.fields),
+ );
+ });
+
+ const [form, state] = useFormState<FormType>(shape, initial, (st) => {
+ const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({
+ state: st.state === undefined ? i18n.str`required` : undefined,
+ threshold: !st.threshold ? i18n.str`required` : undefined,
+ when: !st.when ? i18n.str`required` : undefined,
+ });
+
+ const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
+ validateRequiredFields(partialErrors, st, requiredFields),
+ );
+
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ }
+
+ return {
+ status: "fail",
+ result: st as any,
+ errors,
+ };
+ });
+
+ const validatedForm = state.status !== "ok" ? 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);
+ }
+ },
+ );
+ 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(
+ i18n,
+ section.fields,
+ form,
+ getConverterById,
+ )}
+ />
+ </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}
+ disabled={!submitHandler}
+ class="disabled:opacity-50 disabled:cursor-default 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 { i18n } = useTranslationContext();
+ const { forms } = useUiFormsContext();
+ const pf = preloadedForms(i18n);
+ 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>
+ );
+ })}
+ {pf.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
new file mode 100644
index 000000000..22a6d1867
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -0,0 +1,41 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+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: TalerExchangeApi.AmlState.normal,
+ onChangeFilter: () => null,
+ 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
new file mode 100644
index 000000000..f66eca33f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -0,0 +1,347 @@
+/*
+ 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,
+ UIHandlerId,
+ amlStateConverter,
+ 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 { privatePages } from "../Routing.js";
+import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+import { Officer } from "./Officer.js";
+
+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, status] = useFormState<FormType>(
+ [".state"] as Array<UIHandlerId>,
+ {
+ 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`}
+ handler={form.state}
+ converter={amlStateConverter}
+ choices={[
+ {
+ label: i18n.str`Pending`,
+ value: "pending",
+ },
+ {
+ label: i18n.str`Frozen`,
+ value: "frozen",
+ },
+ {
+ label: i18n.str`Normal`,
+ value: "normal",
+ },
+ ]}
+ />
+ </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 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>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export function Cases() {
+ const [stateFilter, setStateFilter] = useState(
+ TalerExchangeApi.AmlState.pending,
+ );
+
+ const list = useCases(stateFilter);
+ const { i18n } = useTranslationContext();
+
+ if (!list) {
+ return <Loading />;
+ }
+ if (list instanceof TalerError) {
+ return <ErrorLoading error={list} />;
+ }
+
+ if (list.type === "fail") {
+ switch (list.case) {
+ case HttpStatusCode.Forbidden: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't 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);
+ }
+ }
+
+ 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>
+);
+
+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"
+ >
+ <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"
+ disabled={!onFirstPage}
+ onClick={onFirstPage}
+ >
+ <i18n.Translate>First page</i18n.Translate>
+ </button>
+ <button
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 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"
+ disabled={!onNext}
+ onClick={onNext}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </button>
+ </div>
+ </nav>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
new file mode 100644
index 000000000..87310aa27
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -0,0 +1,197 @@
+/*
+ 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,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ UIHandlerId,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+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 | undefined>(obj: T): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
+
+export function CreateAccount(): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreferences();
+ const officer = useOfficer();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const [form, status] = useFormState<FormType>(
+ [".password", ".repeat"] as Array<UIHandlerId>,
+ {
+ 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>
+ </h2>
+ </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
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="mt-2">
+ <InputLine<FormType, "password">
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ required
+ handler={form.password}
+ />
+ </div>
+
+ <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
+ type="submit"
+ 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>
+ </div>
+ </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
new file mode 100644
index 000000000..3d6e14f22
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -0,0 +1,35 @@
+/*
+ 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/officer.js";
+import { CreateAccount } from "./CreateAccount.js";
+import { UnlockAccount } from "./UnlockAccount.js";
+
+export function HandleAccountNotReady({
+ officer,
+}: {
+ officer: OfficerNotReady;
+}): VNode {
+ if (officer.state === "not-found") {
+ return <CreateAccount />;
+ }
+
+ if (officer.state === "locked") {
+ return <UnlockAccount />;
+ }
+ assertUnreachable(officer);
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx
new file mode 100644
index 000000000..39359cd5e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx
@@ -0,0 +1,84 @@
+/*
+ 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 { useUiSettingsContext } from "../context/ui-settings.js";
+
+export function Officer() {
+ const officer = useOfficer();
+ const settings = useUiSettingsContext();
+ const { lib } = useExchangeApiContext();
+
+ const { i18n } = useTranslationContext();
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ const url = new URL("./", lib.exchange.baseUrl);
+ const signupEmail = settings.signupEmail ?? `aml-signup@${url.hostname}`;
+
+ return (
+ <div>
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ <i18n.Translate>Public key</i18n.Translate>
+ </h1>
+ <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
+ <p class="mt-6 font-mono break-all">{officer.account.id}</p>
+ </div>
+ <p>
+ <a
+ 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"
+ >
+ <i18n.Translate>Request account activation</i18n.Translate>
+ </a>
+ </p>
+ <p>
+ <button
+ type="button"
+ onClick={() => {
+ officer.lock();
+ }}
+ class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 text-center text-sm text-black shadow-sm "
+ >
+ <i18n.Translate>Lock account</i18n.Translate>
+ </button>
+ </p>
+ <p>
+ <button
+ type="button"
+ onClick={() => {
+ officer.forget();
+ }}
+ 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 "
+ >
+ <i18n.Translate>Forget account</i18n.Translate>
+ </button>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
new file mode 100644
index 000000000..714bf6580
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -0,0 +1,132 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ 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 { 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([], [], 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" 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
new file mode 100644
index 000000000..cdc5d0bc1
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -0,0 +1,183 @@
+/*
+ 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,
+ FormConfiguration,
+ UIFormElementConfig,
+ UIHandlerId,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { AmlEvent } from "./CaseDetails.js";
+
+export function ShowConsolidated({
+ history,
+ until,
+}: {
+ history: AmlEvent[];
+ until: AbsoluteTime;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const cons = getConsolidated(history, until);
+
+ const form: FormConfiguration = {
+ type: "double-column",
+ design: [
+ {
+ title: i18n.str`AML`,
+ fields: [
+ {
+ type: "amount",
+ id: ".aml.threshold" as UIHandlerId,
+ currency: "NETZBON",
+ label: i18n.str`Threshold`,
+ name: "aml.threshold",
+ },
+ {
+ type: "choiceHorizontal",
+ label: i18n.str`State`,
+ name: "aml.state",
+ id: ".aml.state" as UIHandlerId,
+ choices: [
+ {
+ label: i18n.str`Frozen`,
+ value: "frozen",
+ },
+ {
+ label: i18n.str`Pending`,
+ value: "pending",
+ },
+ {
+ label: i18n.str`Normal`,
+ value: "normal",
+ },
+ ],
+ },
+ ],
+ },
+ Object.entries(cons.kyc).length > 0
+ ? {
+ title: i18n.str`KYC`,
+ fields: Object.entries(cons.kyc).map(([key, field]) => {
+ const result: UIFormElementConfig = {
+ type: "text",
+ label: key as TranslatedString,
+ id: `kyc.${key}.value` as UIHandlerId,
+ 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;
+ }),
+ }
+ : undefined!,
+ ],
+ };
+ return (
+ <Fragment>
+ <h1 class="text-base font-semibold leading-7 text-black">
+ Consolidated information{" "}
+ {until.t_ms === "never"
+ ? ""
+ : `after ${format(until.t_ms, "dd MMMM yyyy")}`}
+ </h1>
+ <DefaultForm
+ key={`${String(Date.now())}`}
+ form={form as any}
+ initial={cons}
+ readOnly
+ onUpdate={() => {}}
+ />
+ </Fragment>
+ );
+}
+
+interface Consolidated {
+ aml: {
+ state: TalerExchangeApi.AmlState;
+ threshold: AmountJson;
+ since: AbsoluteTime;
+ };
+ kyc: {
+ [field: string]: {
+ value: unknown;
+ provider: string;
+ since: AbsoluteTime;
+ };
+ };
+}
+
+function getConsolidated(
+ history: AmlEvent[],
+ when: AbsoluteTime,
+): Consolidated {
+ const initial: Consolidated = {
+ aml: {
+ state: TalerExchangeApi.AmlState.normal,
+ threshold: {
+ currency: "ARS",
+ value: 1000,
+ fraction: 0,
+ },
+ since: AbsoluteTime.never(),
+ },
+ kyc: {},
+ };
+ return history.reduce((prev, cur) => {
+ if (AbsoluteTime.cmp(when, cur.when) < 0) {
+ return prev;
+ }
+ switch (cur.type) {
+ case "kyc-expiration": {
+ cur.fields.forEach((field) => {
+ delete prev.kyc[field];
+ });
+ break;
+ }
+ case "aml-form": {
+ prev.aml = {
+ since: cur.when,
+ state: cur.state,
+ 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,
+ provider: cur.provider,
+ since: cur.when,
+ };
+ });
+ break;
+ }
+ }
+ return prev;
+ }, initial);
+}
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
new file mode 100644
index 000000000..084e639bf
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -0,0 +1,131 @@
+/*
+ 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,
+ UIHandlerId,
+ useLocalNotificationHandler,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+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();
+
+ const [form, status] = useFormState<FormType>(
+ [".password"] as Array<UIHandlerId>,
+ {
+ 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>
+ </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">
+
+ <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
+ type="button"
+ 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>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts
new file mode 100644
index 000000000..f11028de8
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts
@@ -0,0 +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 a3 from "./Cases.stories.js";
diff --git a/packages/aml-backoffice-ui/src/scss/main.css b/packages/aml-backoffice-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/aml-backoffice-ui/src/settings.json b/packages/aml-backoffice-ui/src/settings.json
new file mode 100644
index 000000000..932202b81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/settings.json
@@ -0,0 +1,4 @@
+{
+ "backendBaseURL": "http://exchange.taler.test:1180/",
+ "signupEmail": "do-not-contact-me@exchange.taler.test"
+} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
new file mode 100644
index 000000000..265a2165b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/stories.test.ts
@@ -0,0 +1,83 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { TalerExchangeApi, setupI18n } from "@gnu-taler/taler-util";
+import {
+ ExchangeApiProviderTesting,
+ ExchangeContextType,
+ parseGroupImport,
+} from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+
+// import * as components from "./components/index.examples.js";
+import * as pages from "./pages/index.stories.js";
+
+import { ComponentChildren, VNode, h as create } from "preact";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ pages });
+ cms.forEach((group) => {
+ describe(`Example for group "${group.title}:"`, () => {
+ group.list.forEach((component) => {
+ describe(`Component ${component.name}:`, () => {
+ component.examples.forEach((example) => {
+ it(`should render example: ${example.name}`, () => {
+ tests.renderUI(example.render, DefaultTestingContext);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+function DefaultTestingContext({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode {
+ const config: TalerExchangeApi.ExchangeVersionResponse = {
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ };
+ const value: ExchangeContextType = {
+ cancelRequest: () => null,
+ config,
+ url: new URL("/", "http://localhost"),
+ 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
new file mode 100644
index 000000000..9a23d82fa
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/stories.tsx
@@ -0,0 +1,82 @@
+/*
+ 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { strings } from "./i18n/strings.js";
+
+import * as pages from "./pages/index.stories.js";
+
+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";
+
+function main(): void {
+ renderStories(
+ { pages },
+ {
+ strings,
+ getWrapperForGroup,
+ },
+ );
+}
+
+function getWrapperForGroup(): FunctionComponent {
+ return function All({ children }: { children?: ComponentChildren }): VNode {
+ const config: TalerExchangeApi.ExchangeVersionResponse = {
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ };
+ const value: ExchangeContextType = {
+ cancelRequest: () => null,
+ config,
+ url: new URL("/", "http://localhost"),
+ hints: [],
+ lib: {
+ exchange: undefined!, //FIXME: mock
+ },
+ onActivity: () => null!,
+ };
+ return (
+ <ExchangeApiProviderTesting value={value}>
+ {children}
+ </ExchangeApiProviderTesting>
+ );
+ };
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx
new file mode 100644
index 000000000..b382348a3
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/utils/QR.tsx
@@ -0,0 +1,54 @@
+/*
+ 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, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+// import qrcode from "qrcode-generator";
+
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ // const qr = qrcode(0, "L");
+ // qr.addData(text);
+ // qr.make();
+ // if (divRef.current)
+ // divRef.current.innerHTML = qr.createSvgTag({
+ // scalable: true,
+ // });
+ });
+
+ return (
+ <div
+ style={{
+ width: "100%",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "left",
+ }}
+ >
+ <div
+ style={{
+ width: "50%",
+ minWidth: 200,
+ maxWidth: 300,
+ marginRight: "auto",
+ marginLeft: "auto",
+ }}
+ ref={divRef}
+ />
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/tailwind.config.js b/packages/aml-backoffice-ui/tailwind.config.js
new file mode 100644
index 000000000..ec51dfbb8
--- /dev/null
+++ b/packages/aml-backoffice-ui/tailwind.config.js
@@ -0,0 +1,14 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: {
+ relative: true,
+ files: [
+ "./src/**/*.{html,tsx}",
+ "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}"
+ ],
+ },
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/packages/aml-backoffice-ui/test.mjs b/packages/aml-backoffice-ui/test.mjs
new file mode 100755
index 000000000..9df844fce
--- /dev/null
+++ b/packages/aml-backoffice-ui/test.mjs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+/*
+ 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 { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
+
+const allTestFiles = getFilesInDirectory("./src", /.test.tsx?$/);
+
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/test",
+ css: "postcss",
+});
diff --git a/packages/aml-backoffice-ui/tsconfig.json b/packages/aml-backoffice-ui/tsconfig.json
new file mode 100644
index 000000000..9826fac07
--- /dev/null
+++ b/packages/aml-backoffice-ui/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
+ "allowJs": true /* Allow javascript files to be compiled. */,
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*"]
+}