aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-06-05 10:04:09 -0300
committerSebastian <sebasjm@gmail.com>2023-06-05 10:04:09 -0300
commitc680f5aa71b08e978444df07f93c381f9d47ab82 (patch)
tree81903fac003bb1e202cf69551e06ba41a6e960a5 /packages/aml-backoffice-ui/src
parentdf53866e6b148ea5fd2ab57e906a4aa36b535ed3 (diff)
downloadwallet-core-c680f5aa71b08e978444df07f93c381f9d47ab82.tar.xz
rename aml
Diffstat (limited to 'packages/aml-backoffice-ui/src')
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx12
-rw-r--r--packages/aml-backoffice-ui/src/Dashboard.tsx599
-rw-r--r--packages/aml-backoffice-ui/src/NiceForm.tsx59
-rw-r--r--packages/aml-backoffice-ui/src/account.ts92
-rw-r--r--packages/aml-backoffice-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/aml-backoffice-ui/src/declaration.d.ts30
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_11e.ts154
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_12e.ts440
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_13e.ts527
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_15e.ts197
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_1e.ts695
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_4e.ts822
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_5e.ts281
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_9e.ts138
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts146
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts103
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Caption.tsx35
-rw-r--r--packages/aml-backoffice-ui/src/handlers/FormProvider.tsx99
-rw-r--r--packages/aml-backoffice-ui/src/handlers/Group.tsx41
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputAmount.tsx34
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputArray.tsx183
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx86
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx111
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputDate.tsx37
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputFile.tsx101
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputInteger.tsx23
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputLine.tsx282
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx151
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx134
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputText.tsx8
-rw-r--r--packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx8
-rw-r--r--packages/aml-backoffice-ui/src/handlers/forms.ts135
-rw-r--r--packages/aml-backoffice-ui/src/handlers/useField.ts93
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useOfficer.ts100
-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.html42
-rw-r--r--packages/aml-backoffice-ui/src/index.tsx22
-rw-r--r--packages/aml-backoffice-ui/src/pages.ts55
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx90
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx447
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx288
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx102
-rw-r--r--packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx34
-rw-r--r--packages/aml-backoffice-ui/src/pages/Home.tsx5
-rw-r--r--packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx76
-rw-r--r--packages/aml-backoffice-ui/src/pages/Officer.tsx55
-rw-r--r--packages/aml-backoffice-ui/src/pages/Settings.tsx5
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx81
-rw-r--r--packages/aml-backoffice-ui/src/pages/Welcome.tsx9
-rw-r--r--packages/aml-backoffice-ui/src/route.ts167
-rw-r--r--packages/aml-backoffice-ui/src/scss/main.css3
-rw-r--r--packages/aml-backoffice-ui/src/stories.test.ts56
-rw-r--r--packages/aml-backoffice-ui/src/stories.tsx44
-rw-r--r--packages/aml-backoffice-ui/src/types.ts81
62 files changed, 11169 insertions, 0 deletions
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
new file mode 100644
index 000000000..600131219
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -0,0 +1,12 @@
+import { TranslationProvider } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Dashboard } from "./Dashboard.js";
+import "./scss/main.css";
+
+export function App(): VNode {
+ return (
+ <TranslationProvider source={{}}>
+ <Dashboard />
+ </TranslationProvider>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx
new file mode 100644
index 000000000..6794ca1f8
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/Dashboard.tsx
@@ -0,0 +1,599 @@
+import { useNotifications } from "@gnu-taler/web-util/browser";
+import { Dialog, Transition } from "@headlessui/react";
+import { UserIcon, XCircleIcon } from "@heroicons/react/20/solid";
+import { CheckCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
+import { InformationCircleIcon } from "@heroicons/react/24/solid";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import logo from "./assets/logo-2021.svg";
+import { Pages } from "./pages.js";
+import { Router, useCurrentLocation } from "./route.js";
+
+function classNames(...classes: string[]) {
+ return classes.filter(Boolean).join(" ");
+}
+
+/**
+ * mapping route to view
+ * not found (error page)
+ * nested, index element, relative routes
+ * link interception
+ * form POST interception, call action
+ * fromData => Object.fromEntries
+ * segments in the URL
+ * navigationState: idle, submitting, loading
+ * form GET interception: does a navigateTo
+ * form GET Sync:
+ * 1.- back after submit: useEffect to sync URL to form
+ * 2.- refresh after submit: input default value
+ * useSubmit for form submission onChange, history replace
+ *
+ * post form without redirect
+ *
+ *
+ * @param param0
+ * @returns
+ */
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+const versionText = VERSION
+ ? GIT_HASH
+ ? `v${VERSION} (${GIT_HASH.substring(0, 8)})`
+ : VERSION
+ : "";
+
+/**
+ * TO BE FIXED:
+ *
+ * 1.- when the form change to other form and both form share the same structure
+ * the same input component may be rendered in the same place,
+ * since input are uncontrolled the are not re-rendered and since they are
+ * uncontrolled it will keep the value of the previous form.
+ * One solutions could be to remove the form when unloading and when the new
+ * form load it will start without previous vdom, preventing the cache
+ * to create this behavior.
+ * Other solutions could be using IDs in the fields that are constructed
+ * with the ID of the form, so two fields of different form will need to re-render
+ * cleaning up the state of the previous form.
+ *
+ * 2.- currently the design prop and the behavior prop of the flexible form
+ * are two side of the same coin. From the design point of view, it is important
+ * to design the form in a list-of-field manner and there may be additional
+ * content that is not directly mapped to the form structure (object)
+ * So maybe we want to change the current shape so the computation of the state
+ * of the form is in a field level, but this computation required the field value and
+ * the whole form values and state (since one field may be disabled/hidden) because
+ * of the value of other field.
+ *
+ * 3.- given the previous requirement, maybe the name of the field of the form could be
+ * a function (P: F -> V) where F is the form (or parent object) and V is the type of the
+ * property. That will help with the typing of the forms props
+ *
+ * 4.- tooltip are not placed correctly: the arrow should point the question mark
+ * and the text area should be bigger
+ *
+ * 5.- date field should have the calendar icon clickable so the user can select date without
+ * writing text with the correct format
+ */
+
+function LeftMenu() {
+ const currentLocation = useCurrentLocation(pageList);
+
+ return (
+ <nav class="flex flex-1 flex-col">
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
+ <li>
+ <ul role="list" class="-mx-2 space-y-1">
+ <li>
+ <a
+ href={Pages.info.url}
+ class={classNames(
+ Pages.info.url === currentLocation?.path
+ ? "bg-indigo-700 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",
+ )}
+ >
+ <InformationCircleIcon
+ class={classNames(
+ Pages.info.url === currentLocation?.path
+ ? "text-white"
+ : "text-indigo-200 group-hover:text-white",
+ "h-6 w-6 shrink-0",
+ )}
+ aria-hidden="true"
+ />
+ Cases
+ </a>
+ </li>
+ <li>
+ <a
+ href={Pages.officer.url}
+ class={classNames(
+ Pages.officer.url === currentLocation?.path
+ ? "bg-indigo-700 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",
+ )}
+ >
+ <UserIcon
+ class={classNames(
+ Pages.officer.url === currentLocation?.path
+ ? "text-white"
+ : "text-indigo-200 group-hover:text-white",
+ "h-6 w-6 shrink-0",
+ )}
+ aria-hidden="true"
+ />
+ Account
+ </a>
+ </li>
+ </ul>
+ </li>
+ {/* <li>
+ <div class="text-xs font-semibold leading-6 text-indigo-200">
+ Info
+ </div>
+ <ul role="list" class="-mx-2 mt-2 space-y-1">
+ <li>
+ <a
+ href={Pages.info.url}
+ class={classNames(
+ Pages.info.url === currentLocation?.path
+ ? "bg-indigo-700 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",
+ )}
+ >
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border border-indigo-400 bg-indigo-500 text-[0.625rem] font-medium text-white">
+ asd
+ </span>
+ <span class="truncate">qwe</span>
+ </a>
+ </li>
+ </ul>
+ </li> */}
+ {/* <li class="mt-auto">
+ <a
+ href={Pages.settings.url}
+ class={classNames(
+ Pages.settings.url === currentLocation?.path
+ ? "bg-indigo-700 text-white"
+ : "text-indigo-200 hover:text-white hover:bg-indigo-700",
+ "group -mx-2 flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold",
+ )}
+ >
+ <Cog6ToothIcon
+ class={classNames(
+ Pages.officer.url === currentLocation?.path
+ ? "text-white"
+ : "text-indigo-200 group-hover:text-white",
+ "h-6 w-6 shrink-0",
+ )}
+ aria-hidden="true"
+ />
+ Settings
+ </a>
+ </li> */}
+ </ul>
+ </nav>
+ );
+}
+
+export function Dashboard({
+ children,
+}: {
+ children?: ComponentChildren;
+}): VNode {
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+
+ return (
+ <Fragment>
+ <NavigationBar isOpen={sidebarOpen} setOpen={setSidebarOpen}>
+ <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-indigo-600 px-6 pb-4">
+ <div class="flex h-16 shrink-0 items-center">
+ <header class="flex items-center justify-between border-b border-white/5 ">
+ <h1 class="text-base font-semibold leading-7 text-white">
+ Exchange AML Backoffice
+ </h1>
+ </header>
+ </div>
+ <LeftMenu />
+ <Footer />
+ </div>
+ </NavigationBar>
+ <div class="lg:pl-72">
+ <TopBar
+ onOpenSidebar={() => {
+ setSidebarOpen(true);
+ }}
+ />
+ <Notifications />
+ <main class="py-10 px-4 sm:px-6 lg:px-8">
+ <div class="mx-auto max-w-3xl">
+ <Router
+ pageList={pageList}
+ onNotFound={() => {
+ return <div>not found</div>;
+ }}
+ />
+ </div>
+ </main>
+ </div>
+ </Fragment>
+ );
+}
+
+const pageList = Object.values(Pages);
+
+function NavigationBar({
+ isOpen,
+ setOpen,
+ children,
+}: {
+ isOpen: boolean;
+ setOpen: (v: boolean) => void;
+ children: ComponentChildren;
+}) {
+ return (
+ <Fragment>
+ <Transition.Root show={isOpen} as={Fragment}>
+ <Dialog
+ as="div"
+ /* @ts-ignore */
+ class="relative z-50 lg:hidden"
+ onClose={setOpen}
+ >
+ <Transition.Child
+ as={Fragment}
+ enter="transition-opacity ease-linear duration-300"
+ enterFrom="opacity-0"
+ enterTo="opacity-100"
+ leave="transition-opacity ease-linear duration-300"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <div class="fixed inset-0 bg-gray-900/80" />
+ </Transition.Child>
+
+ <div class="fixed inset-0 flex">
+ <Transition.Child
+ as={Fragment}
+ enter="transition ease-in-out duration-300 transform"
+ enterFrom="-translate-x-full"
+ enterTo="translate-x-0"
+ leave="transition ease-in-out duration-300 transform"
+ leaveFrom="translate-x-0"
+ leaveTo="-translate-x-full"
+ >
+ <Dialog.Panel class="relative mr-16 flex w-full max-w-xs flex-1">
+ <Transition.Child
+ as={Fragment}
+ enter="ease-in-out duration-300"
+ enterFrom="opacity-0"
+ enterTo="opacity-100"
+ leave="ease-in-out duration-300"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <div class="absolute left-full top-0 flex w-16 justify-center pt-5">
+ <button
+ type="button"
+ class="-m-2.5 p-2.5"
+ onClick={() => setOpen(false)}
+ >
+ <span class="sr-only">Close sidebar</span>
+ <XMarkIcon
+ class="h-6 w-6 text-white"
+ aria-hidden="true"
+ />
+ </button>
+ </div>
+ </Transition.Child>
+ {children}
+ </Dialog.Panel>
+ </Transition.Child>
+ </div>
+ </Dialog>
+ </Transition.Root>
+
+ <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
+ {children}
+ </div>
+ </Fragment>
+ );
+}
+
+function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) {
+ return (
+ <div class="relative flex h-16 justify-between">
+ <div class="relative z-10 flex p-2 lg:hidden">
+ <button
+ type="button"
+ onClick={() => {
+ onOpenSidebar();
+ }}
+ class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-900"
+ aria-controls="mobile-menu"
+ aria-expanded="false"
+ >
+ <span class="sr-only">Open menu</span>
+ <svg
+ class="block h-6 w-6"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
+ />
+ </svg>
+ <svg
+ class="hidden h-6 w-6"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M6 18L18 6M6 6l12 12"
+ />
+ </svg>
+ </button>
+ </div>
+ <div class="relative z-0 flex flex-1 items-center justify-center px-2 sm:absolute sm:inset-0">
+ <div class="w-full sm:max-w-xs flex flex-1 items-center justify-center">
+ <img
+ class="h-8 w-auto"
+ src={logo}
+ alt="Taler"
+ style={{ height: 35, margin: 10 }}
+ />
+ </div>
+ </div>
+ {/* <div class="relative z-10 flex items-center lg:hidden">dd</div> */}
+ </div>
+ );
+}
+
+// return (
+// <div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
+// <button
+// type="button"
+// class="-m-2.5 p-2.5 text-gray-700 lg:hidden"
+// onClick={onOpenSidebar}
+// >
+// <span class="sr-only">Open sidebar</span>
+// <Bars3Icon class="h-6 w-6" aria-hidden="true" />
+// </button>
+
+// {/* Separator */}
+// <div class="h-6 w-px bg-gray-900/10 lg:hidden" aria-hidden="true" />
+
+// <div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
+// <div class="relative flex flex-1" />
+// {/* <form class="relative flex flex-1" action="#" method="GET">
+// <label htmlFor="search-field" class="sr-only">
+// Search
+// </label>
+// <MagnifyingGlassIcon
+// class="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400"
+// aria-hidden="true"
+// />
+// <input
+// id="search-field"
+// class="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm"
+// placeholder="Search..."
+// type="search"
+// name="search"
+// />
+// </form> */}
+// <div class="flex items-center gap-x-4 lg:gap-x-6">
+// {/* <button
+// type="button"
+// class="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
+// >
+// <span class="sr-only">View notifications</span>
+// <BellIcon class="h-6 w-6" aria-hidden="true" />
+// </button> */}
+
+// {/* Separator */}
+// <div
+// class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10"
+// aria-hidden="true"
+// />
+
+// {/* {officerName === undefined ? (
+// <div />
+// ) : (
+// <Menu
+// as="div"
+// class="relative"
+// >
+// <Menu.Button class="-m-1.5 flex items-center p-1.5">
+// <span class="sr-only">Open user menu</span>
+// <img
+// class="h-8 w-8 rounded-full bg-gray-50"
+// src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
+// alt=""
+// />
+// <span class="hidden lg:flex lg:items-center">
+// <span
+// class="ml-4 text-sm font-semibold leading-6 text-gray-900"
+// aria-hidden="true"
+// >
+// {officerName}
+// </span>
+// <ChevronDownIcon
+// class="ml-2 h-5 w-5 text-gray-400"
+// aria-hidden="true"
+// />
+// </span>
+// </Menu.Button>
+// <Transition
+// as={Fragment}
+// enter="transition ease-out duration-100"
+// enterFrom="transform opacity-0 scale-95"
+// enterTo="transform opacity-100 scale-100"
+// leave="transition ease-in duration-75"
+// leaveFrom="transform opacity-100 scale-100"
+// leaveTo="transform opacity-0 scale-95"
+// >
+// <Menu.Items class="absolute right-0 z-10 mt-2.5 w-48 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
+// <Menu.Item>
+// {({ active }: { active: boolean }) => (
+// <a
+// onClick={() => {
+// officer.reset();
+// password.reset();
+// }}
+// class={classNames(
+// active ? "bg-gray-50" : "",
+// "block px-3 py-1 text-sm leading-6 text-gray-900",
+// )}
+// >
+// Forget account
+// </a>
+// )}
+// </Menu.Item>
+// </Menu.Items>
+// </Transition>
+// </Menu>
+// )} */}
+// </div>
+// </div>
+// </div>
+// );
+// }
+
+function Footer() {
+ return (
+ <footer class="absolute bottom-4">
+ <div class="mt-8 md:order-1 md:mt-0">
+ <p class="text-xs leading-5 text-gray-300">
+ Taler Systems SA. {versionText}
+ </p>
+ </div>
+ </footer>
+ );
+}
+
+function Notifications() {
+ const ns = useNotifications();
+
+ // useEffect(() => {
+ // if (ns.length) {
+ // // remove notifications after some timeout
+ // }
+ // }, []);
+ {
+ /* <!-- Global notification live region, render this permanently at the end of the document --> */
+ }
+ return (
+ <div
+ aria-live="assertive"
+ class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"
+ >
+ <div class="flex w-full flex-col items-center space-y-4 sm:items-end ">
+ {/* <!--
+ Notification panel, dynamically insert this into the live region when it needs to be displayed
+
+ Entering: "transform ease-out duration-300 transition"
+ From: "translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
+ To: "translate-y-0 opacity-100 sm:translate-x-0"
+ Leaving: "transition ease-in duration-100"
+ From: "opacity-100"
+ To: "opacity-0"
+--> */}
+ {ns.map(({ message, remove }) => {
+ switch (message.type) {
+ case "error": {
+ return (
+ <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 ">
+ <div class="p-4 ">
+ <div class="flex items-start ">
+ <div class="flex-shrink-0">
+ <XCircleIcon class="h-6 w-6 text-red-400" />
+ </div>
+ <div class="ml-3 w-0 flex-1 pt-0.5">
+ <p class="text-sm font-medium text-gray-900">
+ {message.title}
+ </p>
+ {message.description && (
+ <p class="mt-1 text-sm text-gray-500">
+ {message.description}
+ </p>
+ )}
+ </div>
+ <div class="ml-4 flex flex-shrink-0">
+ <button
+ type="button"
+ onClick={remove}
+ class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ >
+ <span class="sr-only">Close</span>
+ <svg
+ class="h-5 w-5"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
+ </svg>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+ case "info": {
+ return (
+ <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 ">
+ <div class="p-4 ">
+ <div class="flex items-start ">
+ <div class="flex-shrink-0">
+ <CheckCircleIcon class="h-6 w-6 text-green-400" />
+ </div>
+ <div class="ml-3 w-0 flex-1 pt-0.5">
+ <p class="text-sm font-medium text-gray-900">
+ {message.title}
+ </p>
+ </div>
+ <div class="ml-4 flex flex-shrink-0">
+ <button
+ type="button"
+ onClick={remove}
+ class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ >
+ <span class="sr-only">Close</span>
+ <svg
+ class="h-5 w-5"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
+ </svg>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+ }
+ })}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/NiceForm.tsx b/packages/aml-backoffice-ui/src/NiceForm.tsx
new file mode 100644
index 000000000..4fc0ea89f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/NiceForm.tsx
@@ -0,0 +1,59 @@
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, h } from "preact";
+import { FlexibleForm } from "./forms/index.js";
+import { FormProvider } from "./handlers/FormProvider.js";
+import { RenderAllFieldsByUiConfig } from "./handlers/forms.js";
+
+export function NiceForm<T extends object>({
+ initial,
+ onUpdate,
+ form,
+ onSubmit,
+ children,
+}: {
+ children?: ComponentChildren;
+ initial: Partial<T>;
+ onSubmit?: (v: Partial<T>) => void;
+ form: FlexibleForm<T>;
+ onUpdate?: (d: Partial<T>) => void;
+}) {
+ return (
+ <FormProvider
+ initialValue={initial}
+ onUpdate={onUpdate}
+ onSubmit={onSubmit}
+ computeFormState={form.behavior}
+ >
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {form.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={section.fields}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ {children}
+ </FormProvider>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/account.ts b/packages/aml-backoffice-ui/src/account.ts
new file mode 100644
index 000000000..bd3c2003e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/account.ts
@@ -0,0 +1,92 @@
+import {
+ bytesToString,
+ createEddsaKeyPair,
+ decodeCrock,
+ decryptWithDerivedKey,
+ eddsaGetPublic,
+ encodeCrock,
+ encryptWithDerivedKey,
+ getRandomBytesF,
+ stringToBytes,
+} from "@gnu-taler/taler-util";
+
+export interface Account {
+ accountId: AccountId;
+ signingKey: SigningKey;
+}
+
+/**
+ * Restore previous session and unlock account with password
+ *
+ * @param salt string from which crypto params will be derived
+ * @param key secured private key
+ * @param password password for the private key
+ * @returns
+ */
+export async function unlockAccount(
+ account: LockedAccount,
+ password: string,
+): Promise<Account> {
+ const rawKey = decodeCrock(account);
+ const rawPassword = stringToBytes(password);
+
+ const signingKey = (await decryptWithDerivedKey(
+ rawKey,
+ rawPassword,
+ password,
+ ).catch((e: Error) => {
+ throw new UnwrapKeyError(e.message);
+ })) as SigningKey;
+
+ const publicKey = eddsaGetPublic(signingKey);
+
+ const accountId = encodeCrock(publicKey) as AccountId;
+
+ return { accountId, signingKey };
+}
+
+declare const opaque_Account: unique symbol;
+export type LockedAccount = string & { [opaque_Account]: true };
+
+declare const opaque_AccountId: unique symbol;
+export type AccountId = string & { [opaque_AccountId]: true };
+
+declare const opaque_SigningKey: unique symbol;
+export type SigningKey = Uint8Array & { [opaque_SigningKey]: true };
+
+/**
+ * Create new account (secured private key)
+ * secured with the given password
+ *
+ * @param sessionId
+ * @param password
+ * @returns
+ */
+export async function createNewAccount(
+ password: string,
+): Promise<Account & { safe: LockedAccount }> {
+ const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
+
+ const key = stringToBytes(password);
+
+ const protectedPrivKey = await encryptWithDerivedKey(
+ getRandomBytesF(24),
+ key,
+ eddsaPriv,
+ password,
+ );
+
+ const signingKey = eddsaPriv as SigningKey;
+ const accountId = encodeCrock(eddsaPub) as AccountId;
+ const safe = encodeCrock(protectedPrivKey) as LockedAccount;
+
+ return { accountId, signingKey, safe };
+}
+
+export class UnwrapKeyError extends Error {
+ public cause: string;
+ constructor(cause: string) {
+ super(`Recovering private key failed on: ${cause}`);
+ this.cause = cause;
+ }
+}
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/declaration.d.ts b/packages/aml-backoffice-ui/src/declaration.d.ts
new file mode 100644
index 000000000..11a10860d
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/declaration.d.ts
@@ -0,0 +1,30 @@
+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/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts
new file mode 100644
index 000000000..24df6a44c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts
@@ -0,0 +1,154 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title:
+ "Establishing of the controlling person of operating legal entities and partnerships both not quoted on the stock exchange" as TranslatedString,
+ description:
+ "for operating legal entities and partnership that are contracting partner as well as analogously for operating legal entities and partnership that are beneficial owners." as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: "Contracting partner" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "declares",
+ label:
+ "The contracting partner hereby declares that" as TranslatedString,
+ required: true,
+ choices: [
+ {
+ label:
+ "the person(s) listed below is/are holding 25% or more of the contracting partner's shares (capital shares or voting rights)" as TranslatedString,
+ value: "25-or-more",
+ },
+ {
+ label:
+ "if the capital shares or voting rights cannot be determined or in case there are no capital shares or voting rights 25% or more, the contracting partner hereby declares that the person(s) listed below is/are controlling the contracting partner in other ways" as TranslatedString,
+ value: "controlling-in-other-ways",
+ },
+ {
+ label:
+ "in case this/these person(s) cannot be determined or this/these person(s) does/do not exist, the contracting partner hereby declares that the person(s) listed below is/are the managing director(s)" as TranslatedString,
+ value: "managing-director",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ name: "businessEstablisher",
+ label: "Persons" as TranslatedString,
+ required: true,
+ tooltip: "hola" as TranslatedString,
+ placeholder: "this is the placeholder" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "lastName",
+ label: "Last name(s)" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "firstName",
+ label: "First name(s)" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label: "Actual address of domicile" as TranslatedString,
+ required: true,
+ },
+ },
+ ],
+ labelField: "lastName",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "fiduciaryAssets",
+ label: "Fiduciary holding assets" as TranslatedString,
+ help: "Is a third person the beneficial owner of the assets held in the account/securities account?" as TranslatedString,
+ required: true,
+ choices: [
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ description:
+ "The relevant information regarding the beneficial owner has to be obtained by filling in a separate VQF doc. No. 902.9" as TranslatedString,
+ },
+ ],
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_11.Form>,
+ ): FormState<Form902_11.Form> {
+ return {
+ person: {
+ hidden:
+ v.declares !== "controlling-in-other-ways" &&
+ v.declares !== "managing-director",
+ },
+ when: {
+ disabled: true,
+ },
+ };
+ },
+});
+
+namespace Form902_11 {
+ interface Person {
+ lastName: string;
+ firstName: string;
+ address: string;
+ }
+ export interface Form extends Simplest.WithResolution {
+ contractingPartner: string;
+ declares: "25-or-more" | "controlling-in-other-ways" | "managing-director";
+ person: 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..c80539511
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts
@@ -0,0 +1,440 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title: "Foundations" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: "Contracting partner" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "knownAs",
+ label:
+ "The undersigned hereby declare(s) that as board member of the foundation, or of the highest supervisory body of an underlying company of a foundation, known as" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "foundation.name",
+ label:
+ "Name and information pertaining to the foundation" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "foundation.type",
+ label: "Type of foundation" as TranslatedString,
+ choices: [
+ {
+ label: "Discretionary foundation" as TranslatedString,
+ value: "discretionary",
+ },
+ {
+ label: "Non-discretionary foundation" as TranslatedString,
+ value: "non-discretionary",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "foundation.revocability",
+ label: "Revocability" as TranslatedString,
+ choices: [
+ {
+ label: "Revocable foundation" as TranslatedString,
+ value: "revocable",
+ },
+ {
+ label: "Irrevocable foundation" as TranslatedString,
+ value: "irrevocable",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies)" as TranslatedString,
+ labelField: "fullName",
+ name: "founders",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfDeath",
+ label: "Date of death" as TranslatedString,
+ help: "if deceased" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToRevoke",
+ required: true,
+ label:
+ "Does the founder have the right to revoke the foundation?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given" as TranslatedString,
+ labelField: "fullName",
+ name: "preExistingFounders",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfDeath",
+ label: "Date of death" as TranslatedString,
+ help: "if deceased" as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Pertaining to the beneficiary/-ies at the time of the signing of this form" as TranslatedString,
+ labelField: "fullName",
+ name: "beneficiaryWhenSigning",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ "Has the beneficiary an actual right to claim distribution?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form" as TranslatedString,
+ name: "beneficiaryExtra",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Information pertaining to further persons having the right to determine or nominate representatives (e.g.) members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries" as TranslatedString,
+ labelField: "fullName",
+ name: "withRightToNominate",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ "has the person the right to revoke the foundation?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form" as TranslatedString,
+ name: "beneficiaryExtra",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: "Signature" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_12.Form>,
+ ): FormState<Form902_12.Form> {
+ return {
+ founders: {
+ elements: (v.founders ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ withRightToNominate: {
+ elements: (v.withRightToNominate ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ when: {
+ disabled: true,
+ },
+ };
+ },
+});
+
+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 Simplest.WithResolution {
+ 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..63870f00a
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts
@@ -0,0 +1,527 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title: "Declaration for trusts" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: "Contracting partner" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "knownAs",
+ label:
+ "The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "trust.name",
+ label:
+ "Name and information pertaining to the trust" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "trust.type",
+ label: "Type of trust" as TranslatedString,
+ choices: [
+ {
+ label: "Discretionary trust" as TranslatedString,
+ value: "discretionary",
+ },
+ {
+ label: "Non-discretionary trust" as TranslatedString,
+ value: "non-discretionary",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "trust.revocability",
+ label: "Revocability" as TranslatedString,
+ choices: [
+ {
+ label: "Revocable foundation" as TranslatedString,
+ value: "revocable",
+ },
+ {
+ label: "Irrevocable foundation" as TranslatedString,
+ value: "irrevocable",
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/ies)" as TranslatedString,
+ labelField: "fullName",
+ name: "settlors",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfDeath",
+ label: "Date of death" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToRevoke",
+ required: true,
+ label:
+ "Does the founder have the right to revoke the trust?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "If the trust results from the restructuring of pre-existing trust (re-settlement) or the merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given" as TranslatedString,
+ labelField: "fullName",
+ name: "preExistingSettlors",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfDeath",
+ label: "Date of death" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "if deceased. format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Pertaining to the beneficiary/-ies at the time of the signing of this form" as TranslatedString,
+ labelField: "fullName",
+ name: "beneficiaryWhenSigning",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ "Has the beneficiary an actual right to claim distribution?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form" as TranslatedString,
+ name: "beneficiaryExtra",
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust" as TranslatedString,
+ labelField: "asd",
+ name: "nothing",
+ fields: [],
+ },
+ },
+
+ {
+ type: "array",
+ props: {
+ label:
+ "Information pertaining to the protectors" as TranslatedString,
+ labelField: "fullName",
+ name: "protectors",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ "Does the protector have the right to revoke the trust?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label:
+ "Information pertaining to further persons" as TranslatedString,
+ labelField: "fullName",
+ name: "furtherPersons",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label:
+ "Actual address of domicile/registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "country",
+ label: "Country" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "rightToClaim",
+ label:
+ "Has this further person the right to revoke the trust?" as TranslatedString,
+ choices: [
+ {
+ label: "Yes" as TranslatedString,
+ value: "yes",
+ },
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ label: "Date" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: "Signature" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_13.Form>,
+ ): FormState<Form902_13.Form> {
+ return {
+ settlors: {
+ elements: (v.settlors ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ protectors: {
+ elements: (v.protectors ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ furtherPersons: {
+ elements: (v.furtherPersons ?? []).map((f) => {
+ return {
+ rightToRevoke: {
+ hidden: v.foundation?.revocability !== "revocable",
+ },
+ };
+ }),
+ },
+ when: {
+ disabled: true,
+ },
+ };
+ },
+});
+
+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 Simplest.WithResolution {
+ 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..19a16d3f2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts
@@ -0,0 +1,197 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title:
+ "Information on life insurance policies with separately managed accounts/securities accounts" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: "Contracting partner" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "contractualRelationship",
+ label:
+ "Name or number of the contractual relationship between the contracting party and the financial intermediary" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "insurancePolicy",
+ label: "Insurance policy" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above." as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "In relation with the above insurance policy, the contracting partner gives the following further details" as TranslatedString,
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "Policy holder" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "holder.fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "holder.address",
+ label:
+ "Actual address of domicile/registered office (incl. country)" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "holder.dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "holder.nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ "Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above)" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "premiumPayer.fullName",
+ label:
+ "Last name(s), first name(s)/entity" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "premiumPayer.address",
+ label:
+ "Actual address of domicile/registered office (incl. country)" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "premiumPayer.dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "premiumPayer.nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: "Signature" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_15.Form>,
+ ): FormState<Form902_15.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ };
+ },
+});
+
+namespace Form902_15 {
+ interface Person {
+ fullName: string;
+ address: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ }
+
+ export interface Form extends Simplest.WithResolution {
+ 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..654085443
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts
@@ -0,0 +1,695 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FlexibleForm, languageList } from "./index.js";
+import { FormState } from "../handlers/FormProvider.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title: "This form was completed by" as TranslatedString,
+ description:
+ "The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold." as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label: "Full name" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Information on customer" as TranslatedString,
+ description:
+ "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer." as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ name: "customerType",
+ label: "Type of customer" as TranslatedString,
+ required: true,
+ choices: [
+ {
+ label: "Natural person" as TranslatedString,
+ value: "natural",
+ },
+ {
+ label: "Legal entity" as TranslatedString,
+ value: "legal",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.fullName",
+ label: "Full name" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.address",
+ label: "Residential address" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "integer",
+ props: {
+ name: "naturalCustomer.telephone",
+ label: "Telephone" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.email",
+ label: "E-mail" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "naturalCustomer.dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.nationality",
+ label: "Nationality" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.document",
+ label: "Identification document" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "naturalCustomer.documentAttachment",
+ label: "Document attachment" as TranslatedString,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.companyName",
+ label: "Company name" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.office",
+ label: "Registered office" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "naturalCustomer.companyDocument",
+ label: "Company identification document" as TranslatedString,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "naturalCustomer.companyDocumentAttachment",
+ label: "Document attachment" as TranslatedString,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.companyName",
+ label: "Company name" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.domicile",
+ label: "Domicile" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.contactPerson",
+ label: "Contact person" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.telephone",
+ label: "Telephone" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.email",
+ label: "E-mail" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "legalCustomer.document",
+ label: "Identification document" as TranslatedString,
+ help: "Not older than 12 month" as TranslatedString,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "legalCustomer.documentAttachment",
+ label: "Document attachment" as TranslatedString,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "Information on the natural persons who establish the business relationship for legal entities and partnerships" as TranslatedString,
+ description:
+ "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified." as TranslatedString,
+ fields: [
+ {
+ type: "array",
+ props: {
+ name: "businessEstablisher",
+ label: "Persons" as TranslatedString,
+ required: true,
+ tooltip: "hola" as TranslatedString,
+ placeholder: "this is the placeholder" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label: "Full name" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label: "Residential address" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ required: true,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "typeOfAuthorization",
+ label:
+ "Type of authorization (signatory of representation)" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "file",
+ props: {
+ name: "documentAttachment",
+ label:
+ "Identification document attachment" as TranslatedString,
+ required: true,
+ maxBites: 2 * 1024 * 1024,
+ accept: ".png",
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "powerOfAttorneyArrangements",
+ label: "Power of attorney arrangements" as TranslatedString,
+ required: true,
+ choices: [
+ {
+ label: "CR extract" as TranslatedString,
+ value: "cr",
+ },
+ {
+ label: "Mandate" as TranslatedString,
+ value: "mandate",
+ },
+ {
+ label: "Other" as TranslatedString,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "powerOfAttorneyArrangementsOther",
+ label: "Power of attorney arrangements" as TranslatedString,
+ required: true,
+ },
+ },
+ ],
+ labelField: "fullName",
+ },
+ },
+ ],
+ },
+ {
+ title: "Acceptance of business relationship" as TranslatedString,
+ fields: [
+ {
+ type: "date",
+ props: {
+ name: "acceptance.when",
+ pattern: "dd/MM/yyyy",
+ label: "Date (conclusion of contract)" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "acceptance.acceptedBy",
+ label: "Accepted by" as TranslatedString,
+ required: true,
+ choices: [
+ {
+ label: "Face-to-face meeting with customer" as TranslatedString,
+ value: "face-to-face",
+ },
+ {
+ label:
+ "Correspondence: authenticated copy of identification document obtained" as TranslatedString,
+ value: "correspondence-document",
+ },
+ {
+ label:
+ "Correspondence: residential address validated" as TranslatedString,
+ value: "correspondence-address",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ name: "acceptance.typeOfCorrespondence",
+ label: "Type of correspondence service" as TranslatedString,
+ choices: [
+ {
+ label: "to the customer" as TranslatedString,
+ value: "customer",
+ },
+ {
+ label: "hold at bank" as TranslatedString,
+ value: "bank",
+ },
+ {
+ label: "to the member" as TranslatedString,
+ value: "member",
+ },
+ {
+ label: "to a third party" as TranslatedString,
+ value: "third-party",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "acceptance.thirdPartyFullName",
+ label: "Third party full name" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "acceptance.thirdPartyAddress",
+ label: "Third party address" as TranslatedString,
+ required: true,
+ },
+ },
+ {
+ type: "selectMultiple",
+ props: {
+ name: "acceptance.language",
+ label: "Languages" as TranslatedString,
+ choices: languageList,
+ unique: true,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ name: "acceptance.furtherInformation",
+ label: "Further information" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "Information on the beneficial owner of the assets and/or controlling person" as TranslatedString,
+ description:
+ "Establishment of the beneficial owner of the assets and/or controlling person" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ name: "establishment",
+ label: "The customer is" as TranslatedString,
+ required: true,
+ choices: [
+ {
+ label:
+ "a natural person and there are no doubts that this person is the sole beneficial owner of the assets" as TranslatedString,
+ value: "natural",
+ },
+ {
+ label:
+ "a foundation (or a similar construct; incl. underlying companies)" as TranslatedString,
+ value: "foundation",
+ },
+ {
+ label:
+ "a trust (incl. underlying companies)" as TranslatedString,
+ value: "trust",
+ },
+ {
+ label:
+ "a life insurance policy with separately managed accounts/securities accounts" as TranslatedString,
+ value: "insurance-wrapper",
+ },
+ {
+ label: "all other cases" as TranslatedString,
+ value: "other",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship" as TranslatedString,
+ description:
+ "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "embargoEvaluation",
+ help: "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated." as TranslatedString,
+ label: "Evaluation" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship" as TranslatedString,
+ description:
+ "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ name: "cashTransactions.typeOfBusiness",
+ label: "Type of business relationship" as TranslatedString,
+ choices: [
+ {
+ label: "Money exchange" as TranslatedString,
+ value: "money-exchange",
+ },
+ {
+ label: "Money and asset transfer" as TranslatedString,
+ value: "money-and-asset-transfer",
+ },
+ {
+ label:
+ "Other cash transactions. Specify below" as TranslatedString,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "cashTransactions.otherTypeOfBusiness",
+ required: true,
+ label: "Specify other cash transactions:" as TranslatedString,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ name: "cashTransactions.purpose",
+ label:
+ "Purpose of the business relationship (purpose of service requested)" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_1.Form>,
+ ): FormState<Form902_1.Form> {
+ return {
+ fullName: {
+ disabled: true,
+ },
+ when: {
+ 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 Simplest.WithResolution {
+ 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..f77a2f63a
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts
@@ -0,0 +1,822 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { ArrowRightIcon } from "@heroicons/react/24/outline";
+import { h as create } from "preact";
+import { ChevronRightIcon } from "@heroicons/react/24/solid";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title: "Risk Profile AMLA" as TranslatedString,
+ description:
+ "Evaluation of business relationship with increased risk and definition of criteria for transaction monitoring." as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations)" as TranslatedString,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "customer",
+ label: "Customer" as TranslatedString,
+ help: "Pursuant identification form (VQF doc. Nr. 902.1) numeral 1" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "This form was completed by" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ label: "Full name" as TranslatedString,
+ name: "fullName",
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "Evaluation of politically exposed persons (PEP-Check)" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "This evaluation has to be completed by all members for every business relationship" as TranslatedString,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Foreign PEP" as TranslatedString,
+ tooltip:
+ "Definition see Art. 7 lit. g numeral 1 SRO Regulations" as TranslatedString,
+ help: "Is the customer, the beneficial owner or the controlling person or authorized representative a foreign PEP or closely related to such a person?" as TranslatedString,
+ name: "pep.foreign",
+ choices: [
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ {
+ label: "Yes" as TranslatedString,
+ description:
+ "The business relationship is compulsory classified as increased risk" as TranslatedString,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label:
+ "Domestic PEP and PEP of International Organizations" as TranslatedString,
+ tooltip:
+ "Definition see Art. 7 lit. g numeral 2 and 3 SRO Regulations " as TranslatedString,
+ help: "Is the customer, the beneficial owner or the controlling person or authorized representative a domestic PEP or PEP in International Organizations or closely related to such a person?" as TranslatedString,
+ name: "pep.domestic",
+ choices: [
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ {
+ label:
+ "Yes, but NOT risk criterion pursuant to numeral 3 subsequently increased." as TranslatedString,
+ value: "yes-but-no-risk",
+ },
+ {
+ label:
+ "Yes, AND a risk criterion pursuant to numeral 3 subsequently increased." as TranslatedString,
+ description:
+ "Classification of the business relationship as increased risk is compulsory" as TranslatedString,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "date",
+ props: {
+ label:
+ "The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
+ name: "pep.when",
+ pattern: "dd/MM/yyyy",
+ placeholder: "dd/MM/yyyy" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ 'Evaluation "high risk" or non-cooperative country' as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "This evaluation has to be completed by all members for every business relationship" as TranslatedString,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: '"High risk" or non-cooperative country' as TranslatedString,
+ help: 'Is the customer, the beneficial owner or the controlling person or authorized representative in a country considered by the FATF "high risk" or non-cooperative and for which FATF requires increased diligence?' as TranslatedString,
+ name: "highRisk.evaluation",
+ choices: [
+ {
+ label: "No" as TranslatedString,
+ value: "no",
+ },
+ {
+ label: "Yes" as TranslatedString,
+ description:
+ "considered as business relationship with increased risk" as TranslatedString,
+ value: "yes",
+ },
+ ],
+ },
+ },
+ {
+ type: "date",
+ props: {
+ label:
+ "The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
+ name: "highRisk.when",
+ pattern: "dd/MM/yyyy",
+ placeholder: "dd/MM/yyyy" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Evaluation of business relationship risk" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed" as TranslatedString,
+ before: create(ArrowRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "a) Country risk (nationality)" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Domicile/residential address" as TranslatedString,
+ name: "evaluation.nationality.address",
+ choices: [
+ {
+ label: "Customer" as TranslatedString,
+ value: "customer",
+ },
+ {
+ label:
+ "Beneficial owner of the assets" as TranslatedString,
+ value: "owner",
+ },
+ {
+ label: "Controlling person" as TranslatedString,
+ value: "controlling",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Nationality" as TranslatedString,
+ name: "evaluation.nationality.nationality",
+ choices: [
+ {
+ label: "Customer" as TranslatedString,
+ value: "customer",
+ },
+ {
+ label:
+ "Beneficial owner of the assets" as TranslatedString,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.nationality.risk",
+ choices: [
+ {
+ label:
+ "Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "low",
+ },
+ {
+ label:
+ "Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "medium",
+ },
+ {
+ label:
+ "Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "b) Country risk (business activity)" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Place of business activity" as TranslatedString,
+ name: "evaluation.business.place",
+ choices: [
+ {
+ label: "Customer" as TranslatedString,
+ value: "customer",
+ },
+ {
+ label:
+ "Beneficial owner of the assets" as TranslatedString,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.business.risk",
+ choices: [
+ {
+ label:
+ "Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "low",
+ },
+ {
+ label:
+ "Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "medium",
+ },
+ {
+ label:
+ "Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "c) Country risk (payments)" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "Country of origin and destination of frequent payments (if known)" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.payments.risk",
+ choices: [
+ {
+ label:
+ "Risk 0 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "low",
+ },
+ {
+ label:
+ "Risk 1 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "medium",
+ },
+ {
+ label:
+ "Risk 2 acc. to VQF country list (VQF doc. no. 902.4.1)" as TranslatedString,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "d) Industry risk" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ props: {
+ label:
+ "Nature of customer's business activity" as TranslatedString,
+ name: "evaluation.industry.nature",
+ choices: [
+ {
+ label: "Customer" as TranslatedString,
+ value: "customer",
+ },
+ {
+ label:
+ "Beneficial owner of the assets" as TranslatedString,
+ value: "owner",
+ },
+ ],
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.payments.risk",
+ choices: [
+ {
+ label:
+ "Clearly defined, transparent, easily comprehensible business activity well known to the member" as TranslatedString,
+ value: "low",
+ },
+ {
+ label:
+ "Business activity with a high level of cash transactions" as TranslatedString,
+ value: "medium-cash",
+ },
+ {
+ label:
+ "Business activity not well known to the member" as TranslatedString,
+ value: "medium-unknown",
+ },
+ {
+ label:
+ "Trade in munitions/arms, raw gem stones/diamonds, jewelry, international trade in exotic animals, casino and lottery business, trade in erotic wares" as TranslatedString,
+ value: "high-restricted",
+ },
+ {
+ label:
+ "Member has no personal knowledge of the customer's industry" as TranslatedString,
+ value: "high-unknown",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "e) Contact risk" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "Types of contact to the customer/ beneficial owner of the assets" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.contact.risk",
+ choices: [
+ {
+ label:
+ "Personal acquaintance between member and customer/beneficial owner of the assets over several years (at least 2) prior to entering into the business relationship" as TranslatedString,
+ value: "low",
+ },
+ {
+ label:
+ "The customer/beneficial owner was not personally known to the member for several years (at least 2) prior to entering into the business relationship; however (a) no business was entered into in the absence of the customer/beneficial owner, or (b) the customer was at least introduced/brokered by a trusted third party" as TranslatedString,
+ value: "medium",
+ },
+ {
+ label:
+ "The customer/beneficial owner was not personally known to the member and business was entered into in the absence of the former (relationship by correspondence) and the customer was not introduced/brokered by a trusted third party" as TranslatedString,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "f) Product risk" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "Nature of services and products requested by the customer" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.product.risk",
+ choices: [
+ {
+ label:
+ "Easy to understand, transparent services and products whose financial background is easy to comprehend and verify" as TranslatedString,
+ value: "low",
+ },
+ {
+ label:
+ "More sophisticated services/products whose financial background is not readily easy to comprehend and verify" as TranslatedString,
+ value: "medium",
+ },
+ {
+ label:
+ "Main focus on offshore business (especially: relationships with domiciliary companies or other such offshore organisations)" as TranslatedString,
+ value: "high-offshore",
+ },
+ {
+ label:
+ "Complex structures in particular by using a domiciliary company with fiduciary shareholders in a non-transparent jurisdiction, without comprehensible reason or for the purpose of short-term asset placement" as TranslatedString,
+ value: "high-structure",
+ },
+ {
+ label:
+ "The customer or beneficial owner of the assets has a large number of accounts with pass-through transactions (pass-through accounts)" as TranslatedString,
+ value: "high-accounts",
+ },
+ {
+ label:
+ "Complex services/products whose financial background can’t be understood or verified with considerable effort" as TranslatedString,
+ value: "high-service",
+ },
+ {
+ label:
+ "Frequent transactions with increased risks" as TranslatedString,
+ value: "high-freq-tx",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before: "g) Criteria defined by the member" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ label: "Criteria definition" as TranslatedString,
+ name: "evaluation.custom.definition",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk level" as TranslatedString,
+ name: "evaluation.custom.risk",
+ choices: [
+ {
+ label: "Low" as TranslatedString,
+ value: "low",
+ },
+ {
+ label: "Medium" as TranslatedString,
+ value: "medium",
+ },
+ {
+ label: "High" as TranslatedString,
+ value: "high",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "Overall assessment of the business relationship" as TranslatedString,
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ "A business relationship is classified as increased risk if:" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "Business relationship with PEP pursuant to numeral 1 (no exception possible)" as TranslatedString,
+ before: create(ChevronRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ 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",
+ props: {
+ label:
+ "Min. one criterion pursuant to numeral 3 was assessed with risk 2 or min. two criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased)" as TranslatedString,
+ before: create(ChevronRightIcon, { class: "h-6 w-6" }),
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Justification for differing risk assessment" as TranslatedString,
+ name: "evaluation.overall.justification",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Risk classified" as TranslatedString,
+ name: "evaluation.overall.risk",
+ choices: [
+ {
+ label:
+ "Business relationship _without_ increased risk" as TranslatedString,
+ value: "without",
+ },
+ {
+ label:
+ "Business relationship __with__ increased risk" as TranslatedString,
+ value: "with",
+ },
+ ],
+ },
+ },
+ {
+ type: "date",
+ props: {
+ label:
+ "The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on" as TranslatedString,
+ name: "evaluation.when",
+ pattern: "dd/MM/yyyy",
+ placeholder: "dd/MM/yyyy" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "Criteria for identification of increased risk transactions (transaction monitoring)" as TranslatedString,
+ fields: [
+ {
+ type: "group",
+ props: {
+ before: "Criteria" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ label:
+ "Classification as as increased risk is compulsory if" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-6 h-6" }),
+ label:
+ "Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ 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",
+ props: {
+ 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",
+ props: {
+ before:
+ "Additional criteria defined by the member" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ before: create(ArrowRightIcon, { class: "w-6 h-6" }),
+ label:
+ "All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions" as TranslatedString,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label: "Description" as TranslatedString,
+ name: "criteria.additional",
+ },
+ },
+ {
+ type: "group",
+ props: {
+ before:
+ "Possible criteria (Art. 59 para. 2 SRO Regulations)" as TranslatedString,
+ fields: [
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ "the amount of inflowing and outflowing assets" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ "type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual)" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ "type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual)" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ before: create(ChevronRightIcon, { class: "w-4 h-4" }),
+ label:
+ "description of expected transaction patterns which the client notify the member of (considerable variance would be unusual)" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ 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(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_4.Form>,
+ ): FormState<Form902_4.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ };
+ },
+});
+
+namespace Form902_4 {
+ export interface Form extends Simplest.WithResolution {
+ 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";
+ };
+ when: AbsoluteTime;
+ };
+ 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..bd27b7a7f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts
@@ -0,0 +1,281 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm, currencyList } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title: "Customer Profile" as TranslatedString,
+ description:
+ "The information below has to refer to the persons from whom the assets originate ultimately (e.g. beneficial owner of the assets, founder/creator of a trust or foundation). Is the customer an operational legal entity or partnership the information may refer to the entity itself (not to the controlling person), unless the entity holds the assets in trust for a third party." as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "customer",
+ label: "Customer" as TranslatedString,
+ help: "Pursuant Identification Form (VQF doc. No. 902.1) numeral 1" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "fullName",
+ label: "Full name" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Business activity" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: "Profession, business activities" as TranslatedString,
+ name: "businessActivity",
+ help: "former, current, potentially planned" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Financial circumstances" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: "Income and assets, liabilities" as TranslatedString,
+ name: "financial",
+ help: "estimated" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Origin of the deposited assets involved" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ props: {
+ label: "Nature" as TranslatedString,
+ name: "originOfAssets.nature",
+ help: "nature of the involved assets" as TranslatedString,
+ },
+ },
+ {
+ type: "selectOne",
+ props: {
+ name: "originOfAssets.currency",
+ label: "Currency" as TranslatedString,
+ choices: currencyList,
+ },
+ },
+ {
+ type: "integer",
+ props: {
+ label: "Amount" as TranslatedString,
+ name: "originOfAssets.amount",
+ },
+ },
+ {
+ type: "choiceStacked",
+ props: {
+ label: "Category" as TranslatedString,
+ name: "originOfAssets.category",
+ choices: [
+ {
+ label: "Savings" as TranslatedString,
+ value: "savings",
+ },
+ {
+ label: "Own business operations" as TranslatedString,
+ value: "own-business",
+ },
+ {
+ label: "Inheritance" as TranslatedString,
+ value: "inheritance",
+ },
+ {
+ label: "Other, what?" as TranslatedString,
+ value: "other",
+ },
+ ],
+ },
+ },
+ {
+ type: "text",
+ props: {
+ label: "Other category" as TranslatedString,
+ name: "originOfAssets.categoryOther",
+ required: true,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Detailed description of the origins/economical background of the assets involved in the business relationship" as TranslatedString,
+ name: "originOfAssets.details",
+ },
+ },
+ ],
+ },
+ {
+ title:
+ "Nature and purpose of the business relationship" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: "Purpose of the business relationship" as TranslatedString,
+ name: "nature.purpose",
+ help: "nature of the involved assets" as TranslatedString,
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Information on the planned development of the business relationship and the assets" as TranslatedString,
+ name: "nature.plan",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Especially in the case of cash or money and asset transfer transactions with regular customers: Details on usual business volume, Information on the beneficiaries, (Full name, address, bank account)" as TranslatedString,
+ name: "nature.cashOrMoneyTransfer",
+ },
+ },
+ ],
+ },
+ {
+ title: "Relationship with third parties" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Relation of the customer to the beneficial owner involved in the business relationship" as TranslatedString,
+ name: "relations.beneficialOwners",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Relation of the customer to the controlling persons involved in the business relationship" as TranslatedString,
+ name: "relations.controllingPersons",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Relation of the customer to the authorized signatories involved in the business relationship" as TranslatedString,
+ name: "relations.authorizedSignatories",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label:
+ "Relation of the customer to other persons involved in the business relationship" as TranslatedString,
+ name: "relations.otherPersons",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label: "Relation to other AMLA-Files" as TranslatedString,
+ name: "relations.withOtherAmlaFiles",
+ },
+ },
+ {
+ type: "textArea",
+ props: {
+ label: "Introducer / agents / references" as TranslatedString,
+ name: "relations.references",
+ },
+ },
+ ],
+ },
+ {
+ title: "Further information" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ label: "Other relevant information" as TranslatedString,
+ name: "furtherInformation",
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_5.Form>,
+ ): FormState<Form902_5.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ originOfAssets: {
+ categoryOther: {
+ hidden: v.originOfAssets?.category !== "other",
+ },
+ },
+ };
+ },
+});
+
+namespace Form902_5 {
+ export interface Form extends Simplest.WithResolution {
+ 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..e79597bfb
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts
@@ -0,0 +1,138 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
+
+export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
+ versionId: "2023-05-15",
+ design: [
+ {
+ title:
+ "Declaration of identity of the beneficial owner" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "contractingPartner",
+ label: "Contracting partner" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "when",
+ pattern: "dd/MM/yyyy",
+ label: "Date" as TranslatedString,
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below" as TranslatedString,
+ },
+ },
+ {
+ type: "array",
+ props: {
+ label: "Persons" as TranslatedString,
+ labelField: "surname",
+ name: "persons",
+ fields: [
+ {
+ type: "text",
+ props: {
+ name: "surname",
+ label: "Surname(s)" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "firstName",
+ label: "First name(s)" as TranslatedString,
+ },
+ },
+ {
+ type: "date",
+ props: {
+ name: "dateOfBirth",
+ label: "Date of birth" as TranslatedString,
+ pattern: "dd/MM/yyyy",
+ help: "format 'dd/MM/yyyy'" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "nationality",
+ label: "Nationality" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "address",
+ label: "Actual address of domicile" as TranslatedString,
+ },
+ },
+ ],
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein" as TranslatedString,
+ },
+ },
+ {
+ type: "text",
+ props: {
+ name: "signature",
+ label: "Signature" as TranslatedString,
+ },
+ },
+ {
+ type: "caption",
+ props: {
+ label:
+ "It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Form902_9.Form>,
+ ): FormState<Form902_9.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ };
+ },
+});
+
+namespace Form902_9 {
+ interface Person {
+ surname: string;
+ firstName: string;
+ dateOfBirth: AbsoluteTime;
+ nationality: string;
+ address: string;
+ }
+ export interface Form extends Simplest.WithResolution {
+ contractingPartner: string;
+ persons: Person;
+ signature: string;
+ }
+}
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..c236bbaa3
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -0,0 +1,146 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { DoubleColumnForm } from "../handlers/forms.js";
+
+export interface FlexibleForm<T extends object> {
+ versionId: string;
+ design: DoubleColumnForm;
+ behavior: (form: Partial<T>) => FormState<T>;
+}
+
+export const languageList = [
+ {
+ label: "Mandarin Chinese" as TranslatedString,
+ value: "cmn",
+ },
+ {
+ label: "Spanish" as TranslatedString,
+ value: "spa",
+ },
+ {
+ label: "English" as TranslatedString,
+ value: "eng",
+ },
+ {
+ label: "Hindi" as TranslatedString,
+ value: "hin",
+ },
+ {
+ label: "Portuguese" as TranslatedString,
+ value: "por",
+ },
+ {
+ label: "Bengali" as TranslatedString,
+ value: "ben",
+ },
+ {
+ label: "Russian" as TranslatedString,
+ value: "rus",
+ },
+ {
+ label: "Japanese" as TranslatedString,
+ value: "jpn",
+ },
+ {
+ label: "Yue" as TranslatedString,
+ value: "yue",
+ },
+ {
+ label: "Vietnamese" as TranslatedString,
+ value: "vie",
+ },
+ {
+ label: "Turkish" as TranslatedString,
+ value: "tur",
+ },
+ {
+ label: "Wu" as TranslatedString,
+ value: "wuu",
+ },
+ {
+ label: "Marathi" as TranslatedString,
+ value: "mar",
+ },
+ {
+ label: "Telugu" as TranslatedString,
+ value: "ten",
+ },
+ {
+ label: "Korean" as TranslatedString,
+ value: "kor",
+ },
+ {
+ label: "French" as TranslatedString,
+ value: "fra",
+ },
+ {
+ label: "Tamil" as TranslatedString,
+ value: "tam",
+ },
+ {
+ label: "Egyptian Arabic" as TranslatedString,
+ value: "arz",
+ },
+ {
+ label: "Standard German" as TranslatedString,
+ value: "deu",
+ },
+ {
+ label: "Urdu" as TranslatedString,
+ value: "urd",
+ },
+ {
+ label: "Javanese" as TranslatedString,
+ value: "jav",
+ },
+ {
+ label: "Punjabi" as TranslatedString,
+ value: "pan",
+ },
+ {
+ label: "Italian" as TranslatedString,
+ value: "ita",
+ },
+ {
+ label: "Gujarati" as TranslatedString,
+ value: "guj",
+ },
+ {
+ label: "Iranian Persian" as TranslatedString,
+ value: "pes",
+ },
+ {
+ label: "Bhojpuri" as TranslatedString,
+ value: "bho",
+ },
+ {
+ label: "Hausa" as TranslatedString,
+ value: "hau",
+ },
+];
+export const currencyList = [
+ {
+ label: "United States dollar" as TranslatedString,
+ value: "usd",
+ },
+ {
+ label: "Euro" as TranslatedString,
+ value: "eur",
+ },
+ {
+ label: "Swiss franc" as TranslatedString,
+ value: "chf",
+ },
+ {
+ label: "Argentine peso" as TranslatedString,
+ value: "ars",
+ },
+ {
+ label: "Mexican peso" as TranslatedString,
+ value: "mxn",
+ },
+ {
+ label: "Brazilian real" as TranslatedString,
+ 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..7eda03fef
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -0,0 +1,103 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { AmlState } from "../types.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { DoubleColumnFormSection, UIFormField } from "../handlers/forms.js";
+
+export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
+ versionId: "2023-05-25",
+ design: [
+ {
+ title: "Simple form" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "comment",
+ label: "Comments" as TranslatedString,
+ },
+ },
+ ],
+ },
+ resolutionSection(current),
+ ],
+ behavior: function formBehavior(
+ v: Partial<Simplest.Form>,
+ ): FormState<Simplest.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ threshold: {
+ disabled: v.state === AmlState.frozen,
+ },
+ };
+ },
+});
+
+export namespace Simplest {
+ export interface WithResolution {
+ when: AbsoluteTime;
+ threshold: AmountJson;
+ state: AmlState;
+ }
+ export interface Form extends WithResolution {
+ comment: string;
+ }
+}
+
+export function resolutionSection(current: State): DoubleColumnFormSection {
+ return {
+ title: "Resolution" as TranslatedString,
+ description: `Current state is ${amlStateConverter.toStringUI(
+ current.state,
+ )} and threshold at ${Amounts.stringifyValue(
+ current.threshold,
+ )}` as TranslatedString,
+ fields: [
+ {
+ type: "date",
+ props: {
+ name: "when",
+ label: "Decision Time" as TranslatedString,
+ },
+ },
+ {
+ type: "choiceHorizontal",
+ props: {
+ name: "state",
+ label: "New state" as TranslatedString,
+ converter: amlStateConverter,
+ choices: [
+ {
+ value: AmlState.frozen,
+ label: "Frozen" as TranslatedString,
+ },
+ {
+ value: AmlState.pending,
+ label: "Pending" as TranslatedString,
+ },
+ {
+ value: AmlState.normal,
+ label: "Normal" as TranslatedString,
+ },
+ ],
+ },
+ },
+ {
+ type: "amount",
+ props: {
+ name: "threshold",
+ label: "New threshold" as TranslatedString,
+ },
+ },
+ ],
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/Caption.tsx b/packages/aml-backoffice-ui/src/handlers/Caption.tsx
new file mode 100644
index 000000000..fbf154d89
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/Caption.tsx
@@ -0,0 +1,35 @@
+import { VNode, h } from "preact";
+import {
+ IconAddon,
+ InputLine,
+ LabelWithTooltipMaybeRequired,
+ UIFormProps,
+} from "./InputLine.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+interface Props {
+ label: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: VNode;
+ after?: VNode;
+}
+
+export function Caption({ before, after, label, tooltip, help }: Props): VNode {
+ return (
+ <div class="sm:col-span-6 flex">
+ {before !== undefined && (
+ <span class="pointer-events-none flex items-center pr-2">{before}</span>
+ )}
+ <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
+ {after !== undefined && (
+ <span class="pointer-events-none flex items-center pl-2">{after}</span>
+ )}
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
new file mode 100644
index 000000000..a195c2051
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/FormProvider.tsx
@@ -0,0 +1,99 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import {
+ MutableRef,
+ StateUpdater,
+ useEffect,
+ useRef,
+ useState,
+} from "preact/hooks";
+
+export interface FormType<T> {
+ value: MutableRef<Partial<T>>;
+ initialValue?: Partial<T>;
+ onUpdate?: StateUpdater<T>;
+ computeFormState?: (v: T) => FormState<T>;
+}
+
+//@ts-ignore
+export const FormContext = createContext<FormType<any>>({});
+
+export type FormState<T> = {
+ [field in keyof T]?: T[field] extends AbsoluteTime
+ ? Partial<InputFieldState>
+ : T[field] extends AmountJson
+ ? Partial<InputFieldState>
+ : T[field] extends Array<infer P>
+ ? Partial<InputArrayFieldState<P>>
+ : T[field] extends object
+ ? FormState<T[field]>
+ : Partial<InputFieldState>;
+};
+
+export interface InputFieldState {
+ /* should show the error */
+ error?: TranslatedString;
+ /* should not allow to edit */
+ readonly: boolean;
+ /* should show as disable */
+ disabled: boolean;
+ /* should not show */
+ hidden: boolean;
+}
+
+export interface InputArrayFieldState<T> extends InputFieldState {
+ elements: FormState<T>[];
+}
+
+export function FormProvider<T>({
+ children,
+ initialValue,
+ onUpdate: notify,
+ onSubmit,
+ computeFormState,
+}: {
+ initialValue?: Partial<T>;
+ onUpdate?: (v: Partial<T>) => void;
+ onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
+ computeFormState?: (v: Partial<T>) => FormState<T>;
+ children: ComponentChildren;
+}): VNode {
+ // const value = useRef(initialValue ?? {});
+ // useEffect(() => {
+ // return function onUnload() {
+ // value.current = initialValue ?? {};
+ // };
+ // });
+ // const onUpdate = notify
+ const [state, setState] = useState<Partial<T>>(initialValue ?? {});
+ const value = { current: state };
+ // console.log("RENDER", initialValue, value);
+ const onUpdate = (v: typeof state) => {
+ // console.log("updated");
+ setState(v);
+ if (notify) notify(v);
+ };
+ return (
+ <FormContext.Provider
+ value={{ initialValue, value, onUpdate, computeFormState }}
+ >
+ <form
+ onSubmit={(e) => {
+ e.preventDefault();
+ //@ts-ignore
+ if (onSubmit)
+ onSubmit(
+ value.current,
+ !computeFormState ? undefined : computeFormState(value.current),
+ );
+ }}
+ >
+ {children}
+ </form>
+ </FormContext.Provider>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/Group.tsx b/packages/aml-backoffice-ui/src/handlers/Group.tsx
new file mode 100644
index 000000000..0645f6d97
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/Group.tsx
@@ -0,0 +1,41 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+
+interface Props {
+ before?: TranslatedString;
+ after?: TranslatedString;
+ tooltipBefore?: TranslatedString;
+ tooltipAfter?: TranslatedString;
+ fields: UIFormField[];
+}
+
+export function Group({
+ before,
+ after,
+ tooltipAfter,
+ tooltipBefore,
+ fields,
+}: Props): VNode {
+ return (
+ <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50">
+ <div class="pb-4">
+ {before && (
+ <LabelWithTooltipMaybeRequired
+ label={before}
+ tooltip={tooltipBefore}
+ />
+ )}
+ </div>
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig fields={fields} />
+ </div>
+ <div class="pt-4">
+ {after && (
+ <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} />
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputAmount.tsx b/packages/aml-backoffice-ui/src/handlers/InputAmount.tsx
new file mode 100644
index 000000000..9be9dd4d0
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputAmount.tsx
@@ -0,0 +1,34 @@
+import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputAmount<T extends object, K extends keyof T>(
+ props: { currency?: string } & UIFormProps<T, K>,
+): VNode {
+ const { value } = useField<T, K>(props.name);
+ const currency =
+ !value || !(value as any).currency
+ ? props.currency
+ : (value as any).currency;
+ return (
+ <InputLine<T, K>
+ type="text"
+ before={{
+ type: "text",
+ text: currency as TranslatedString,
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AmountJson => {
+ return Amounts.parseOrThrow(`${currency}:${v}`);
+ },
+ //@ts-ignore
+ toStringUI: (v: AmountJson) => {
+ return v === undefined ? "" : Amounts.stringifyValue(v);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputArray.tsx b/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
new file mode 100644
index 000000000..00379bed6
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputArray.tsx
@@ -0,0 +1,183 @@
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { FormProvider, InputArrayFieldState } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { useField } from "./useField.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+function Option({
+ label,
+ disabled,
+ isFirst,
+ isLast,
+ isSelected,
+ onClick,
+}: {
+ label: TranslatedString;
+ isFirst?: boolean;
+ isLast?: boolean;
+ isSelected?: boolean;
+ disabled?: boolean;
+ onClick: () => void;
+}): VNode {
+ let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey";
+ if (isFirst) {
+ clazz += " rounded-tl-md rounded-tr-md ";
+ }
+ if (isLast) {
+ clazz += " rounded-bl-md rounded-br-md ";
+ }
+ if (isSelected) {
+ clazz += " z-10 border-indigo-200 bg-indigo-50 ";
+ } else {
+ clazz += " border-gray-200";
+ }
+ if (disabled) {
+ clazz +=
+ " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray";
+ } else {
+ clazz += " cursor-pointer";
+ }
+ return (
+ <label class={clazz}>
+ <input
+ type="radio"
+ name="privacy-setting"
+ checked={isSelected}
+ disabled={disabled}
+ onClick={onClick}
+ class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600"
+ aria-labelledby="privacy-setting-0-label"
+ aria-describedby="privacy-setting-0-description"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ id="privacy-setting-0-label"
+ disabled
+ class="block text-sm font-medium"
+ >
+ {label}
+ </span>
+ {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */}
+ {/* <span
+ id="privacy-setting-0-description"
+ class="block text-sm"
+ >
+ This project would be available to anyone who has the link
+ </span> */}
+ </span>
+ </label>
+ );
+}
+
+export function InputArray<T extends object, K extends keyof T>(
+ props: {
+ fields: UIFormField[];
+ labelField: string;
+ } & UIFormProps<T, K>,
+): VNode {
+ const { fields, labelField, name, label, required, tooltip } = props;
+ const { value, onChange, state } = useField<T, K>(name);
+ const list = (value ?? []) as Array<Record<string, string | undefined>>;
+ const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
+ const selected =
+ selectedIndex === undefined ? undefined : list[selectedIndex];
+
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+
+ <div class="-space-y-px rounded-md bg-white ">
+ {list.map((v, idx) => {
+ return (
+ <Option
+ label={v[labelField] as TranslatedString}
+ isSelected={selectedIndex === idx}
+ isLast={idx === list.length - 1}
+ disabled={selectedIndex !== undefined && selectedIndex !== idx}
+ isFirst={idx === 0}
+ onClick={() => {
+ setSelected(selectedIndex === idx ? undefined : idx);
+ }}
+ />
+ );
+ })}
+ <div class="pt-2">
+ <Option
+ label={"Add..." as TranslatedString}
+ isSelected={selectedIndex === list.length}
+ isLast
+ isFirst
+ disabled={
+ selectedIndex !== undefined && selectedIndex !== list.length
+ }
+ onClick={() => {
+ setSelected(
+ selectedIndex === list.length ? undefined : list.length,
+ );
+ }}
+ />
+ </div>
+ </div>
+ {selectedIndex !== undefined && (
+ /**
+ * This form provider act as a substate of the parent form
+ * Consider creating an InnerFormProvider since not every feature is expected
+ */
+ <FormProvider
+ initialValue={selected}
+ computeFormState={(v) => {
+ // current state is ignored
+ // the state is defined by the parent form
+
+ // elements should be present in the state object since this is expected to be an array
+ //@ts-ignore
+ return state.elements[selectedIndex];
+ }}
+ onSubmit={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as T[K]);
+ setSelected(undefined);
+ }}
+ onUpdate={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as T[K]);
+ }}
+ >
+ <div class="px-4 py-6">
+ <div class="grid grid-cols-1 gap-y-8 ">
+ <RenderAllFieldsByUiConfig fields={fields} />
+ </div>
+ </div>
+ </FormProvider>
+ )}
+ {selectedIndex !== undefined && (
+ <div class="flex items-center pt-3">
+ <div class="flex-auto">
+ {selected !== undefined && (
+ <button
+ type="button"
+ onClick={() => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1);
+ onChange(newValue as T[K]);
+ setSelected(undefined);
+ }}
+ class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ Remove
+ </button>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx b/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
new file mode 100644
index 000000000..fdee35447
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
@@ -0,0 +1,86 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export interface Choice<V> {
+ label: TranslatedString;
+ value: V;
+}
+
+export function InputChoiceHorizontal<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<T[K]>[];
+ } & UIFormProps<T, K>,
+): VNode {
+ const {
+ choices,
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+ if (state.hidden) {
+ return <Fragment />;
+ }
+
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <fieldset class="mt-2">
+ <div class="isolate inline-flex rounded-md shadow-sm">
+ {choices.map((choice, idx) => {
+ const isFirst = idx === 0;
+ const isLast = idx === choices.length - 1;
+ let clazz =
+ "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10";
+ if (choice.value === value) {
+ clazz +=
+ " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500";
+ } else {
+ clazz += " hover:bg-gray-100 border-gray-300";
+ }
+ if (isFirst) {
+ clazz += " rounded-l-md";
+ } else {
+ clazz += " -ml-px";
+ }
+ if (isLast) {
+ clazz += " rounded-r-md";
+ }
+ return (
+ <button
+ type="button"
+ class={clazz}
+ onClick={(e) => {
+ onChange(
+ (value === choice.value ? undefined : choice.value) as T[K],
+ );
+ }}
+ >
+ {(!converter
+ ? (choice.value as string)
+ : converter?.toStringUI(choice.value)) ?? ""}
+ </button>
+ );
+ })}
+ </div>
+ </fieldset>
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx b/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
new file mode 100644
index 000000000..c37984368
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputChoiceStacked.tsx
@@ -0,0 +1,111 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export interface Choice<V> {
+ label: TranslatedString;
+ description?: TranslatedString;
+ value: V;
+}
+
+export function InputChoiceStacked<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<T[K]>[];
+ } & UIFormProps<T, K>,
+): VNode {
+ const {
+ choices,
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+ if (state.hidden) {
+ return <Fragment />;
+ }
+
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <fieldset class="mt-2">
+ <div class="space-y-4">
+ {choices.map((choice) => {
+ // const currentValue = !converter
+ // ? choice.value
+ // : converter.fromStringUI(choice.value) ?? "";
+
+ let clazz =
+ "border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between";
+ if (choice.value === value) {
+ clazz +=
+ " border-transparent border-indigo-600 ring-2 ring-indigo-600";
+ } else {
+ clazz += " border-gray-300";
+ }
+
+ return (
+ <label class={clazz}>
+ <input
+ type="radio"
+ name="server-size"
+ // defaultValue={choice.value}
+ value={
+ (!converter
+ ? (choice.value as string)
+ : converter?.toStringUI(choice.value)) ?? ""
+ }
+ onClick={(e) => {
+ onChange(
+ (value === choice.value
+ ? undefined
+ : choice.value) as T[K],
+ );
+ }}
+ class="sr-only"
+ aria-labelledby="server-size-0-label"
+ aria-describedby="server-size-0-description-0 server-size-0-description-1"
+ />
+ <span class="flex items-center">
+ <span class="flex flex-col text-sm">
+ <span
+ id="server-size-0-label"
+ class="font-medium text-gray-900"
+ >
+ {choice.label}
+ </span>
+ {choice.description !== undefined && (
+ <span
+ id="server-size-0-description-0"
+ class="text-gray-500"
+ >
+ <span class="block sm:inline">
+ {choice.description}
+ </span>
+ </span>
+ )}
+ </span>
+ </span>
+ </label>
+ );
+ })}
+ </div>
+ </fieldset>
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputDate.tsx b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
new file mode 100644
index 000000000..1fd81aad9
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputDate.tsx
@@ -0,0 +1,37 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { InputLine, UIFormProps } from "./InputLine.js";
+import { CalendarIcon } from "@heroicons/react/24/outline";
+import { VNode, h } from "preact";
+import { format, parse } from "date-fns";
+
+export function InputDate<T extends object, K extends keyof T>(
+ props: { pattern?: string } & UIFormProps<T, K>,
+): VNode {
+ const pattern = props.pattern ?? "dd/MM/yyyy";
+ return (
+ <InputLine<T, K>
+ type="text"
+ after={{
+ type: "icon",
+ icon: <CalendarIcon class="h-6 w-6" />,
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AbsoluteTime => {
+ if (!v) return AbsoluteTime.never();
+ const t_ms = parse(v, pattern, Date.now()).getTime();
+ return AbsoluteTime.fromMilliseconds(t_ms);
+ },
+ //@ts-ignore
+ toStringUI: (v: AbsoluteTime) => {
+ return !v || !v.t_ms
+ ? ""
+ : v.t_ms === "never"
+ ? "never"
+ : format(v.t_ms, pattern);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputFile.tsx b/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
new file mode 100644
index 000000000..0d89a98a3
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputFile.tsx
@@ -0,0 +1,101 @@
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputFile<T extends object, K extends keyof T>(
+ props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
+): VNode {
+ const {
+ name,
+ label,
+ placeholder,
+ tooltip,
+ required,
+ help,
+ maxBites,
+ accept,
+ } = props;
+ const { value, onChange, state } = useField<T, K>(name);
+
+ if (state.hidden) {
+ return <div />;
+ }
+ return (
+ <div class="col-span-full">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ tooltip={tooltip}
+ required={required}
+ />
+ {!value || !(value as string).startsWith("data:image/") ? (
+ <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
+ <div class="text-center">
+ <svg
+ class="mx-auto h-12 w-12 text-gray-300"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ <div class="my-2 flex text-sm leading-6 text-gray-600">
+ <label
+ for="file-upload"
+ class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
+ >
+ <span>Upload a file</span>
+ <input
+ id="file-upload"
+ name="file-upload"
+ type="file"
+ class="sr-only"
+ accept={accept}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > maxBites) {
+ return onChange(undefined!);
+ }
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = window.btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ </label>
+ {/* <p class="pl-1">or drag and drop</p> */}
+ </div>
+ </div>
+ </div>
+ ) : (
+ <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
+ <img
+ src={value as string}
+ class=" h-24 w-full object-cover relative"
+ />
+
+ <div
+ class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
+ onClick={() => {
+ onChange(undefined!);
+ }}
+ >
+ Clear
+ </div>
+ </div>
+ )}
+ {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputInteger.tsx b/packages/aml-backoffice-ui/src/handlers/InputInteger.tsx
new file mode 100644
index 000000000..fb04e3852
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputInteger.tsx
@@ -0,0 +1,23 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+
+export function InputInteger<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ return (
+ <InputLine
+ type="number"
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): number => {
+ return !v ? 0 : Number.parseInt(v, 10);
+ },
+ //@ts-ignore
+ toStringUI: (v?: number): string => {
+ return v === undefined ? "" : String(v);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputLine.tsx b/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
new file mode 100644
index 000000000..9448ef5e4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputLine.tsx
@@ -0,0 +1,282 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useField } from "./useField.js";
+
+export interface IconAddon {
+ type: "icon";
+ icon: VNode;
+}
+interface ButtonAddon {
+ type: "button";
+ onClick: () => void;
+ children: ComponentChildren;
+}
+interface TextAddon {
+ type: "text";
+ text: TranslatedString;
+}
+type Addon = IconAddon | ButtonAddon | TextAddon;
+
+interface StringConverter<T> {
+ toStringUI: (v?: T) => string;
+ fromStringUI: (v?: string) => T;
+}
+
+export interface UIFormProps<T extends object, K extends keyof T> {
+ name: K;
+ label: TranslatedString;
+ placeholder?: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: Addon;
+ after?: Addon;
+ required?: boolean;
+ converter?: StringConverter<T[K]>;
+}
+
+export type FormErrors<T> = {
+ [P in keyof T]?: string | FormErrors<T[P]>;
+};
+
+//@ts-ignore
+const TooltipIcon = (
+ <svg
+ class="w-5 h-5"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
+ clip-rule="evenodd"
+ />
+ </svg>
+);
+
+export function LabelWithTooltipMaybeRequired({
+ label,
+ required,
+ tooltip,
+}: {
+ label: TranslatedString;
+ required?: boolean;
+ tooltip?: TranslatedString;
+}): VNode {
+ const Label = (
+ <Fragment>
+ <div class="flex justify-between">
+ <label
+ htmlFor="email"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ {label}
+ </label>
+ </div>
+ </Fragment>
+ );
+ const WithTooltip = tooltip ? (
+ <div class="relative flex flex-grow items-stretch focus-within:z-10">
+ {Label}
+ <span class="relative flex items-center group pl-2">
+ {TooltipIcon}
+ <div class="absolute bottom-0 flex flex-col items-center hidden mb-6 group-hover:flex">
+ <span class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg">
+ {tooltip}
+ </span>
+ <div class="w-3 h-3 -mt-2 rotate-45 bg-black"></div>
+ </div>
+ </span>
+ </div>
+ ) : (
+ Label
+ );
+ if (required) {
+ return (
+ <div class="flex justify-between">
+ {WithTooltip}
+ <span class="text-sm leading-6 text-red-600">*</span>
+ </div>
+ );
+ }
+ return WithTooltip;
+}
+
+function InputWrapper<T extends object, K extends keyof T>({
+ children,
+ label,
+ tooltip,
+ before,
+ after,
+ help,
+ error,
+ required,
+}: { error?: string; children: ComponentChildren } & UIFormProps<T, K>): VNode {
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <div class="relative mt-2 flex rounded-md shadow-sm">
+ {before &&
+ (before.type === "text" ? (
+ <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+ {before.text}
+ </span>
+ ) : before.type === "icon" ? (
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
+ {before.icon}
+ </div>
+ ) : before.type === "button" ? (
+ <button
+ type="button"
+ onClick={before.onClick}
+ class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ {before.children}
+ </button>
+ ) : undefined)}
+
+ {children}
+
+ {after &&
+ (after.type === "text" ? (
+ <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+ {after.text}
+ </span>
+ ) : after.type === "icon" ? (
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
+ {after.icon}
+ </div>
+ ) : after.type === "button" ? (
+ <button
+ type="button"
+ onClick={after.onClick}
+ class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ {after.children}
+ </button>
+ ) : undefined)}
+ </div>
+ {error && (
+ <p class="mt-2 text-sm text-red-600" id="email-error">
+ {error}
+ </p>
+ )}
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
+ </div>
+ );
+}
+
+function defaultToString(v: unknown) {
+ return v === undefined ? "" : typeof v !== "object" ? String(v) : "";
+}
+function defaultFromString(v: string) {
+ return v;
+}
+
+type InputType = "text" | "text-area" | "password" | "email" | "number";
+
+export function InputLine<T extends object, K extends keyof T>(
+ props: { type: InputType } & UIFormProps<T, K>,
+): VNode {
+ const { name, placeholder, before, after, converter, type } = props;
+ const { value, onChange, state, isDirty } = useField<T, K>(name);
+
+ if (state.hidden) return <div />;
+
+ let clazz =
+ "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200";
+ if (before) {
+ switch (before.type) {
+ case "icon": {
+ clazz += " pl-10";
+ break;
+ }
+ case "button": {
+ clazz += " rounded-none rounded-r-md ";
+ break;
+ }
+ case "text": {
+ clazz += " min-w-0 flex-1 rounded-r-md rounded-none ";
+ break;
+ }
+ }
+ }
+ if (after) {
+ switch (after.type) {
+ case "icon": {
+ clazz += " pr-10";
+ break;
+ }
+ case "button": {
+ clazz += " rounded-none rounded-l-md";
+ break;
+ }
+ case "text": {
+ clazz += " min-w-0 flex-1 rounded-l-md rounded-none ";
+ break;
+ }
+ }
+ }
+ const showError = isDirty && state.error;
+ if (showError) {
+ clazz +=
+ " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500";
+ } else {
+ clazz +=
+ " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600";
+ }
+ const fromString: (s: string) => any =
+ converter?.fromStringUI ?? defaultFromString;
+ const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
+
+ if (type === "text-area") {
+ return (
+ <InputWrapper<T, K>
+ {...props}
+ error={showError ? state.error : undefined}
+ >
+ <textarea
+ rows={4}
+ name={String(name)}
+ onChange={(e) => {
+ onChange(fromString(e.currentTarget.value));
+ }}
+ placeholder={placeholder ? placeholder : undefined}
+ value={toString(value) ?? ""}
+ // defaultValue={toString(value)}
+ disabled={state.disabled}
+ aria-invalid={showError}
+ // aria-describedby="email-error"
+ class={clazz}
+ />
+ </InputWrapper>
+ );
+ }
+
+ return (
+ <InputWrapper<T, K> {...props} error={showError ? state.error : undefined}>
+ <input
+ name={String(name)}
+ type={type}
+ onChange={(e) => {
+ onChange(fromString(e.currentTarget.value));
+ }}
+ placeholder={placeholder ? placeholder : undefined}
+ value={toString(value) ?? ""}
+ // defaultValue={toString(value)}
+ disabled={state.disabled}
+ aria-invalid={showError}
+ // aria-describedby="email-error"
+ class={clazz}
+ />
+ </InputWrapper>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
new file mode 100644
index 000000000..837744827
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputSelectMultiple.tsx
@@ -0,0 +1,151 @@
+import { Fragment, VNode, h } from "preact";
+import { Choice } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+import { useState } from "preact/hooks";
+
+export function InputSelectMultiple<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<T[K]>[];
+ unique?: boolean;
+ max?: number;
+ } & UIFormProps<T, K>,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required, unique, max } =
+ props;
+ const { value, onChange } = useField<T, K>(name);
+
+ const [filter, setFilter] = useState<string | undefined>(undefined);
+ const regex = new RegExp(`.*${filter}.*`, "i");
+ const choiceMap = choices.reduce((prev, curr) => {
+ return { ...prev, [curr.value as string]: curr.label };
+ }, {} as Record<string, string>);
+
+ const list = (value ?? []) as string[];
+ const filteredChoices =
+ filter === undefined
+ ? undefined
+ : choices.filter((v) => {
+ return regex.test(v.label);
+ });
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ {list.map((v, idx) => {
+ return (
+ <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600">
+ {choiceMap[v]}
+ <button
+ type="button"
+ onClick={() => {
+ const newValue = [...list];
+ newValue.splice(idx, 1);
+ onChange(newValue as T[K]);
+ setFilter(undefined);
+ }}
+ class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
+ >
+ <span class="sr-only">Remove</span>
+ <svg
+ viewBox="0 0 14 14"
+ class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
+ >
+ <path d="M4 4l6 6m0-6l-6 6" />
+ </svg>
+ <span class="absolute -inset-1"></span>
+ </button>
+ </span>
+ );
+ })}
+
+ <div class="relative mt-2">
+ <input
+ id="combobox"
+ type="text"
+ value={filter ?? ""}
+ onChange={(e) => {
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+ <button
+ type="button"
+ onClick={() => {
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
+ >
+ <svg
+ class="h-5 w-5 text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+
+ {filteredChoices !== undefined && (
+ <ul
+ class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
+ id="options"
+ role="listbox"
+ >
+ {filteredChoices.map((v, idx) => {
+ return (
+ <li
+ class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
+ id="option-0"
+ role="option"
+ onClick={() => {
+ setFilter(undefined);
+ if (unique && list.indexOf(v.value as string) !== -1) {
+ return;
+ }
+ if (max !== undefined && list.length >= max) {
+ return;
+ }
+ const newValue = [...list];
+ newValue.splice(0, 0, v.value as string);
+ onChange(newValue as T[K]);
+ }}
+
+ // tabindex="-1"
+ >
+ {/* <!-- Selected: "font-semibold" --> */}
+ <span class="block truncate">{v.label}</span>
+
+ {/* <!--
+ Checkmark, only display for selected option.
+
+ Active: "text-white", Not Active: "text-indigo-600"
+ --> */}
+ </li>
+ );
+ })}
+
+ {/* <!--
+ Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
+
+ Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
+ --> */}
+
+ {/* <!-- More items... --> */}
+ </ul>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx b/packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx
new file mode 100644
index 000000000..b0e2277d3
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputSelectOne.tsx
@@ -0,0 +1,134 @@
+import { Fragment, VNode, h } from "preact";
+import { Choice } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+import { useState } from "preact/hooks";
+
+export function InputSelectOne<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<T[K]>[];
+ } & UIFormProps<T, K>,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required } = props;
+ const { value, onChange } = useField<T, K>(name);
+
+ const [filter, setFilter] = useState<string | undefined>(undefined);
+ const regex = new RegExp(`.*${filter}.*`, "i");
+ const choiceMap = choices.reduce((prev, curr) => {
+ return { ...prev, [curr.value as string]: curr.label };
+ }, {} as Record<string, string>);
+
+ const filteredChoices =
+ filter === undefined
+ ? undefined
+ : choices.filter((v) => {
+ return regex.test(v.label);
+ });
+ return (
+ <div class="sm:col-span-6">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ {value ? (
+ <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600">
+ {choiceMap[value as string]}
+ <button
+ type="button"
+ onClick={() => {
+ onChange(undefined!);
+ }}
+ class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
+ >
+ <span class="sr-only">Remove</span>
+ <svg
+ viewBox="0 0 14 14"
+ class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
+ >
+ <path d="M4 4l6 6m0-6l-6 6" />
+ </svg>
+ <span class="absolute -inset-1"></span>
+ </button>
+ </span>
+ ) : (
+ <div class="relative mt-2">
+ <input
+ id="combobox"
+ type="text"
+ value={filter ?? ""}
+ onChange={(e) => {
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+ <button
+ type="button"
+ onClick={() => {
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
+ >
+ <svg
+ class="h-5 w-5 text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+
+ {filteredChoices !== undefined && (
+ <ul
+ class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
+ id="options"
+ role="listbox"
+ >
+ {filteredChoices.map((v, idx) => {
+ return (
+ <li
+ class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
+ id="option-0"
+ role="option"
+ onClick={() => {
+ setFilter(undefined);
+ onChange(v.value as T[K]);
+ }}
+
+ // tabindex="-1"
+ >
+ {/* <!-- Selected: "font-semibold" --> */}
+ <span class="block truncate">{v.label}</span>
+
+ {/* <!--
+ Checkmark, only display for selected option.
+
+ Active: "text-white", Not Active: "text-indigo-600"
+ --> */}
+ </li>
+ );
+ })}
+
+ {/* <!--
+ Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
+
+ Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
+ --> */}
+
+ {/* <!-- More items... --> */}
+ </ul>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputText.tsx b/packages/aml-backoffice-ui/src/handlers/InputText.tsx
new file mode 100644
index 000000000..1b37ee6fb
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputText.tsx
@@ -0,0 +1,8 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+
+export function InputText<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ return <InputLine type="text" {...props} />;
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx b/packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx
new file mode 100644
index 000000000..45229951e
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/InputTextArea.tsx
@@ -0,0 +1,8 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+
+export function InputTextArea<T extends object, K extends keyof T>(
+ props: UIFormProps<T, K>,
+): VNode {
+ return <InputLine type="text-area" {...props} />;
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/forms.ts b/packages/aml-backoffice-ui/src/handlers/forms.ts
new file mode 100644
index 000000000..2c90a69ed
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/forms.ts
@@ -0,0 +1,135 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { InputText } from "./InputText.js";
+import { InputDate } from "./InputDate.js";
+import { InputInteger } from "./InputInteger.js";
+import { h as create, Fragment, VNode } from "preact";
+import { InputChoiceStacked } from "./InputChoiceStacked.js";
+import { InputArray } from "./InputArray.js";
+import { InputSelectMultiple } from "./InputSelectMultiple.js";
+import { InputTextArea } from "./InputTextArea.js";
+import { InputFile } from "./InputFile.js";
+import { Caption } from "./Caption.js";
+import { Group } from "./Group.js";
+import { InputSelectOne } from "./InputSelectOne.js";
+import { FormProvider } from "./FormProvider.js";
+import { InputLine } from "./InputLine.js";
+import { InputAmount } from "./InputAmount.js";
+import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
+
+export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
+
+export type DoubleColumnFormSection = {
+ title: TranslatedString;
+ description?: TranslatedString;
+ fields: UIFormField[];
+};
+
+/**
+ * Constrain the type with the ui props
+ */
+type FieldType<T extends object = any, K extends keyof T = any> = {
+ group: Parameters<typeof Group>[0];
+ caption: Parameters<typeof Caption>[0];
+ array: Parameters<typeof InputArray<T, K>>[0];
+ file: Parameters<typeof InputFile<T, K>>[0];
+ selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
+ selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
+ text: Parameters<typeof InputText<T, K>>[0];
+ textArea: Parameters<typeof InputTextArea<T, K>>[0];
+ choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
+ choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
+ date: Parameters<typeof InputDate<T, K>>[0];
+ integer: Parameters<typeof InputInteger<T, K>>[0];
+ amount: Parameters<typeof InputAmount<T, K>>[0];
+};
+
+/**
+ * List all the form fields so typescript can type-check the form instance
+ */
+export type UIFormField =
+ | { type: "group"; props: FieldType["group"] }
+ | { type: "caption"; props: FieldType["caption"] }
+ | { type: "array"; props: FieldType["array"] }
+ | { type: "file"; props: FieldType["file"] }
+ | { type: "amount"; props: FieldType["amount"] }
+ | { type: "selectOne"; props: FieldType["selectOne"] }
+ | { type: "selectMultiple"; props: FieldType["selectMultiple"] }
+ | { type: "text"; props: FieldType["text"] }
+ | { type: "textArea"; props: FieldType["textArea"] }
+ | { type: "choiceStacked"; props: FieldType["choiceStacked"] }
+ | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] }
+ | { type: "integer"; props: FieldType["integer"] }
+ | { type: "date"; props: FieldType["date"] };
+
+type FieldComponentFunction<key extends keyof FieldType> = (
+ props: FieldType[key],
+) => VNode;
+
+type UIFormFieldMap = {
+ [key in keyof FieldType]: FieldComponentFunction<key>;
+};
+
+/**
+ * Maps input type with component implementation
+ */
+const UIFormConfiguration: UIFormFieldMap = {
+ group: Group,
+ caption: Caption,
+ //@ts-ignore
+ array: InputArray,
+ text: InputText,
+ //@ts-ignore
+ file: InputFile,
+ textArea: InputTextArea,
+ //@ts-ignore
+ date: InputDate,
+ //@ts-ignore
+ choiceStacked: InputChoiceStacked,
+ //@ts-ignore
+ choiceHorizontal: InputChoiceHorizontal,
+ integer: InputInteger,
+ //@ts-ignore
+ selectOne: InputSelectOne,
+ //@ts-ignore
+ selectMultiple: InputSelectMultiple,
+ //@ts-ignore
+ amount: InputAmount,
+};
+
+export function RenderAllFieldsByUiConfig({
+ fields,
+}: {
+ fields: UIFormField[];
+}): VNode {
+ return create(
+ Fragment,
+ {},
+ fields.map((field, i) => {
+ const Component = UIFormConfiguration[
+ field.type
+ ] as FieldComponentFunction<any>;
+ return Component(field.props);
+ }),
+ );
+}
+
+type FormSet<T extends object> = {
+ Provider: typeof FormProvider<T>;
+ InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
+ InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<
+ T,
+ K
+ >;
+};
+export function createNewForm<T extends object>() {
+ const res: FormSet<T> = {
+ Provider: FormProvider,
+ InputLine: () => InputLine,
+ InputChoiceHorizontal: () => InputChoiceHorizontal,
+ };
+ return {
+ Provider: res.Provider,
+ InputLine: res.InputLine(),
+ InputChoiceHorizontal: res.InputChoiceHorizontal(),
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/handlers/useField.ts b/packages/aml-backoffice-ui/src/handlers/useField.ts
new file mode 100644
index 000000000..bf94d2f5d
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/handlers/useField.ts
@@ -0,0 +1,93 @@
+import { useContext, useState } from "preact/compat";
+import { FormContext, InputFieldState } from "./FormProvider.js";
+
+export interface InputFieldHandler<Type> {
+ value: Type;
+ onChange: (s: Type) => void;
+ state: InputFieldState;
+ isDirty: boolean;
+}
+
+export function useField<T extends object, K extends keyof T>(
+ name: K,
+): InputFieldHandler<T[K]> {
+ const {
+ initialValue,
+ value: formValue,
+ computeFormState,
+ onUpdate: notifyUpdate,
+ } = useContext(FormContext);
+
+ type P = typeof name;
+ type V = T[P];
+ const formState = computeFormState ? computeFormState(formValue.current) : {};
+
+ const fieldValue = readField(formValue.current, String(name)) as V;
+ // console.log("USE FIELD", String(name), formValue.current, fieldValue);
+ const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue);
+ const fieldState =
+ readField<Partial<InputFieldState>>(formState, String(name)) ?? {};
+
+ //compute default state
+ const state = {
+ disabled: fieldState.disabled ?? false,
+ readonly: fieldState.readonly ?? false,
+ hidden: fieldState.hidden ?? false,
+ error: fieldState.error,
+ elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
+ };
+
+ function onChange(value: V): void {
+ setCurrentValue(value);
+ formValue.current = setValueDeeper(
+ formValue.current,
+ String(name).split("."),
+ value,
+ );
+ if (notifyUpdate) {
+ notifyUpdate(formValue.current);
+ }
+ }
+
+ return {
+ value: fieldValue,
+ onChange,
+ isDirty: currentValue !== undefined,
+ state,
+ };
+}
+
+/**
+ * read the field of an object an support accessing it using '.'
+ *
+ * @param object
+ * @param name
+ * @returns
+ */
+function readField<T>(
+ object: any,
+ name: string,
+ debug?: boolean,
+): T | undefined {
+ return name.split(".").reduce((prev, current) => {
+ if (debug) {
+ console.log(
+ "READ",
+ name,
+ prev,
+ current,
+ prev ? prev[current] : undefined,
+ );
+ }
+ return prev ? prev[current] : undefined;
+ }, object);
+}
+
+function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (object === undefined) {
+ return { [head]: setValueDeeper({}, rest, value) };
+ }
+ return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
new file mode 100644
index 000000000..4ec43569b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
@@ -0,0 +1,100 @@
+import {
+ AbsoluteTime,
+ Codec,
+ buildCodecForObject,
+ codecForAbsoluteTime,
+ codecForString,
+} from "@gnu-taler/taler-util";
+import {
+ Account,
+ LockedAccount,
+ createNewAccount,
+ unlockAccount,
+} from "../account.js";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useMemoryStorage,
+} from "@gnu-taler/web-util/browser";
+
+export interface Officer {
+ account: LockedAccount;
+ when: AbsoluteTime;
+}
+
+const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
+
+export const codecForOfficer = (): Codec<Officer> =>
+ buildCodecForObject<Officer>()
+ .property("account", codecForLockedAccount) // FIXME
+ .property("when", codecForAbsoluteTime) // FIXME
+ .build("Officer");
+
+export type OfficerState = OfficerNotReady | OfficerReady;
+export type OfficerNotReady = OfficerNotFound | OfficerLocked;
+interface OfficerNotFound {
+ state: "not-found";
+ create: (password: string) => Promise<void>;
+}
+interface OfficerLocked {
+ state: "locked";
+ forget: () => void;
+ tryUnlock: (password: string) => Promise<void>;
+}
+interface OfficerReady {
+ state: "ready";
+ account: Account;
+ forget: () => void;
+ lock: () => void;
+}
+
+const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
+const ACCOUNT_KEY = "account";
+
+export function useOfficer(): OfficerState {
+ const accountStorage = useMemoryStorage<Account>(ACCOUNT_KEY);
+ const officerStorage = useLocalStorage(OFFICER_KEY);
+
+ const officer = officerStorage.value;
+ const account = accountStorage.value;
+
+ if (officer === undefined) {
+ return {
+ state: "not-found",
+ create: async (pwd: string) => {
+ const { accountId, safe, signingKey } = await createNewAccount(pwd);
+ officerStorage.update({
+ account: safe,
+ when: AbsoluteTime.now(),
+ });
+
+ accountStorage.update({ accountId, signingKey });
+ },
+ };
+ }
+
+ if (account === undefined) {
+ return {
+ state: "locked",
+ forget: () => {
+ officerStorage.reset();
+ },
+ tryUnlock: async (pwd: string) => {
+ const ac = await unlockAccount(officer.account, pwd);
+ accountStorage.update(ac);
+ },
+ };
+ }
+
+ return {
+ state: "ready",
+ account: account,
+ lock: () => {
+ accountStorage.reset();
+ },
+ forget: () => {
+ officerStorage.reset();
+ accountStorage.reset();
+ },
+ };
+}
diff --git a/packages/aml-backoffice-ui/src/i18n/bank.pot b/packages/aml-backoffice-ui/src/i18n/bank.pot
new file mode 100644
index 000000000..66e98976f
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/bank.pot
@@ -0,0 +1,486 @@
+# 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/>
+#
+#, 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..203d55343
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/fr.po
@@ -0,0 +1,486 @@
+# 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/>
+#
+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..a251e9584
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# 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/>
+#
+#, 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..a0aeb8268
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/strings-prelude
@@ -0,0 +1,19 @@
+/*
+ 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/>
+ */
+
+/*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..a779bbc49
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/i18n/strings.ts
@@ -0,0 +1,510 @@
+/*
+ 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/>
+ */
+
+/*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..703d31da1
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/index.html
@@ -0,0 +1,42 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Exchange Backoffice</title>
+ <!-- Optional customization script. -->
+ <script src="exchange-backofice-ui-settings.js"></script>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+ <body>
+ <div id="app"></div>
+ </body>
+</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..c2ac4c84b
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/index.tsx
@@ -0,0 +1,22 @@
+/*
+ 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 { App } from "./App.js";
+import { h, render } from "preact";
+
+const app = document.getElementById("app");
+
+render(<App />, app as any);
diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts
new file mode 100644
index 000000000..18fb7a158
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages.ts
@@ -0,0 +1,55 @@
+import { Home } from "./pages/Home.js";
+import { Settings } from "./pages/Settings.js";
+import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js";
+import { Welcome } from "./pages/Welcome.js";
+import { PageEntry, pageDefinition } from "./route.js";
+import { Officer } from "./pages/Officer.js";
+import { Cases } from "./pages/Cases.js";
+import { CaseDetails } from "./pages/CaseDetails.js";
+import { NewFormEntry } from "./pages/NewFormEntry.js";
+
+const home: PageEntry = {
+ url: "#/",
+ view: Home,
+};
+const cases: PageEntry = {
+ url: "#/cases",
+ view: Cases,
+};
+const account: PageEntry<{ account?: string }> = {
+ url: pageDefinition("#/account/:account"),
+ view: CaseDetails,
+};
+
+const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
+ url: pageDefinition("#/account/:account/new/:type?"),
+ view: NewFormEntry,
+};
+
+const settings: PageEntry = {
+ url: "#/settings",
+ view: Settings,
+};
+const officer: PageEntry = {
+ url: "#/officer",
+ view: Officer,
+};
+const welcome: PageEntry<{ asd?: string; name?: string }> = {
+ url: pageDefinition("#/welcome/:name?"),
+ view: Welcome,
+};
+const form: PageEntry<{ number?: string }> = {
+ url: pageDefinition("#/form/:number?"),
+ view: AntiMoneyLaunderingForm,
+};
+
+export const Pages = {
+ home,
+ info: cases,
+ officer,
+ details: account,
+ settings,
+ welcome,
+ form,
+ newFormEntry,
+};
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
new file mode 100644
index 000000000..713c0d7c1
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
@@ -0,0 +1,90 @@
+import { h } from "preact";
+import { NiceForm } from "../NiceForm.js";
+import { v1 as form_902_11e_v1 } from "../forms/902_11e.js";
+import { v1 as form_902_12e_v1 } from "../forms/902_12e.js";
+import { v1 as form_902_13e_v1 } from "../forms/902_13e.js";
+import { v1 as form_902_15e_v1 } from "../forms/902_15e.js";
+import { v1 as form_902_1e_v1 } from "../forms/902_1e.js";
+import { v1 as form_902_4e_v1 } from "../forms/902_4e.js";
+import { v1 as form_902_5e_v1 } from "../forms/902_5e.js";
+import { v1 as form_902_9e_v1 } from "../forms/902_9e.js";
+import { v1 as simplest } from "../forms/simplest.js";
+import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { AmlState } from "../types.js";
+import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+
+export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
+ const selectedForm = Number.parseInt(number ?? "0", 10);
+ if (Number.isNaN(selectedForm)) {
+ return <div>WHAT! {number}</div>;
+ }
+ const showingFrom = allForms[selectedForm].impl;
+ const storedValue = {
+ fullName: "loggedIn_user_fullname",
+ when: AbsoluteTime.now(),
+ };
+ return (
+ <NiceForm
+ initial={storedValue}
+ form={showingFrom({
+ state: AmlState.pending,
+ threshold: Amounts.parseOrThrow("USD:10"),
+ })}
+ onUpdate={() => {}}
+ />
+ );
+}
+
+export interface State {
+ state: AmlState;
+ threshold: AmountJson;
+}
+
+export const allForms = [
+ {
+ name: "Simple comment",
+ icon: DocumentDuplicateIcon,
+ impl: simplest,
+ },
+ {
+ name: "Identification form (902.1e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_1e_v1,
+ },
+ {
+ name: "Operational legal entity or partnership (902.11e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_11e_v1,
+ },
+ {
+ name: "Foundations (902.12e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_12e_v1,
+ },
+ {
+ name: "Declaration for trusts (902.13e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_13e_v1,
+ },
+ {
+ name: "Information on life insurance policies (902.15e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_15e_v1,
+ },
+ {
+ name: "Declaration of beneficial owner (902.9e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_9e_v1,
+ },
+ {
+ name: "Customer profile (902.5e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_5e_v1,
+ },
+ {
+ name: "Risk profile (902.4e)",
+ icon: DocumentDuplicateIcon,
+ impl: form_902_4e_v1,
+ },
+];
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
new file mode 100644
index 000000000..e5fb8eaba
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -0,0 +1,447 @@
+import { Fragment, VNode, h } from "preact";
+import {
+ AmlDecisionDetail,
+ AmlDecisionDetails,
+ AmlState,
+ KycDetail,
+} from "../types.js";
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { format } from "date-fns";
+import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid";
+import { useState } from "preact/hooks";
+import { NiceForm } from "../NiceForm.js";
+import { FlexibleForm } from "../forms/index.js";
+import { UIFormField } from "../handlers/forms.js";
+import { Pages } from "../pages.js";
+
+const response: AmlDecisionDetails = {
+ aml_history: [
+ {
+ justification: "Lack of documentation",
+ decider_pub: "ASDASDASD",
+ decision_time: {
+ t_s: Date.now() / 1000,
+ },
+ new_state: 2,
+ new_threshold: "USD:0",
+ },
+ {
+ justification: "Doing a transfer of high amount",
+ decider_pub: "ASDASDASD",
+ decision_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
+ },
+ new_state: 1,
+ new_threshold: "USD:2000",
+ },
+ {
+ justification: "Account is known to the system",
+ decider_pub: "ASDASDASD",
+ decision_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
+ },
+ new_state: 0,
+ new_threshold: "USD:100",
+ },
+ ],
+ kyc_attributes: [
+ {
+ collection_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
+ },
+ expiration_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
+ },
+ provider_section: "asdasd",
+ attributes: {
+ name: "Sebastian",
+ },
+ },
+ {
+ collection_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
+ },
+ expiration_time: {
+ t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
+ },
+ provider_section: "asdasd",
+ attributes: {
+ creditCard: "12312312312",
+ },
+ },
+ ],
+};
+type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent;
+type AmlFormEvent = {
+ type: "aml-form";
+ when: AbsoluteTime;
+ title: TranslatedString;
+ state: 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 getEventsFromAmlHistory(
+ aml: AmlDecisionDetail[],
+ kyc: KycDetail[],
+): AmlEvent[] {
+ const ae: AmlEvent[] = aml.map((a) => {
+ return {
+ type: "aml-form",
+ state: a.new_state,
+ threshold: Amounts.parseOrThrow(a.new_threshold),
+ title: a.justification as TranslatedString,
+ 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: "collection" as TranslatedString,
+ when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
+ values: !k.attributes ? {} : k.attributes,
+ provider: k.provider_section,
+ });
+ prev.push({
+ type: "kyc-expiration",
+ title: "expired" as TranslatedString,
+ 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 events = getEventsFromAmlHistory(
+ response.aml_history,
+ response.kyc_attributes,
+ );
+ console.log("DETAILS", events, events[events.length - 1 - 2]);
+ const [selected, setSelected] = useState<AmlEvent>(
+ events[events.length - 1 - 2],
+ );
+ return (
+ <div>
+ <a
+ href={Pages.newFormEntry.url({ 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"
+ >
+ New AML form
+ </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">
+ Case history
+ </h1>
+ </header>
+ <div class="flow-root">
+ <ul role="list">
+ {events.map((e, idx) => {
+ const isLast = events.length - 1 === idx;
+ return (
+ <li
+ class="hover:bg-gray-200 p-2 rounded cursor-pointer"
+ onClick={() => {
+ setSelected(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": {
+ switch (e.state) {
+ case AmlState.normal: {
+ return (
+ <div>
+ <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>
+ <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 AmlState.pending: {
+ return (
+ <div>
+ <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>
+ <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 AmlState.frozen: {
+ return (
+ <div>
+ <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>
+ <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" />
+ );
+ }
+ case "kyc-expiration": {
+ return <ClockIcon class="h-8 w-8 text-gray-700" />;
+ }
+ }
+ })()}
+ <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
+ <div>
+ <p class="text-sm text-gray-900">{e.title}</p>
+ </div>
+ <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>
+ {selected && <ShowEventDetails event={selected} />}
+ {selected && <ShowConsolidated history={events} until={selected} />}
+ </div>
+ );
+}
+
+function ShowEventDetails({ event }: { event: AmlEvent }): VNode {
+ return <div>type {event.type}</div>;
+}
+
+function ShowConsolidated({
+ history,
+ until,
+}: {
+ history: AmlEvent[];
+ until: AmlEvent;
+}): VNode {
+ console.log("UNTIL", until);
+ const cons = getConsolidated(history, until.when);
+
+ const form: FlexibleForm<Consolidated> = {
+ versionId: "1",
+ behavior: (form) => {
+ return {};
+ },
+ design: [
+ {
+ title: "AML" as TranslatedString,
+ fields: [
+ {
+ type: "amount",
+ props: {
+ label: "Threshold" as TranslatedString,
+ name: "aml.threshold",
+ },
+ },
+ {
+ type: "choiceHorizontal",
+ props: {
+ label: "State" as TranslatedString,
+ name: "aml.state",
+ converter: amlStateConverter,
+ choices: [
+ {
+ label: "Frozen" as TranslatedString,
+ value: AmlState.frozen,
+ },
+ {
+ label: "Pending" as TranslatedString,
+ value: AmlState.pending,
+ },
+ {
+ label: "Normal" as TranslatedString,
+ value: AmlState.normal,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ Object.entries(cons.kyc).length > 0
+ ? {
+ title: "KYC" as TranslatedString,
+ fields: Object.entries(cons.kyc).map(([key, field]) => {
+ const result: UIFormField = {
+ type: "text",
+ props: {
+ label: key as TranslatedString,
+ name: `kyc.${key}.value`,
+ help: `${field.provider} since ${
+ field.since.t_ms === "never"
+ ? "never"
+ : format(field.since.t_ms, "dd/MM/yyyy")
+ }` as TranslatedString,
+ },
+ };
+ return result;
+ }),
+ }
+ : undefined,
+ ],
+ };
+ return (
+ <Fragment>
+ <h1 class="text-base font-semibold leading-7 text-black">
+ Consolidated information after{" "}
+ {until.when.t_ms === "never"
+ ? "never"
+ : format(until.when.t_ms, "dd MMMM yyyy")}
+ </h1>
+ <NiceForm
+ key={`${String(Date.now())}`}
+ form={form}
+ initial={cons}
+ onUpdate={() => {}}
+ />
+ </Fragment>
+ );
+}
+
+interface Consolidated {
+ aml: {
+ state?: AmlState;
+ threshold?: AmountJson;
+ since: AbsoluteTime;
+ };
+ kyc: {
+ [field: string]: {
+ value: any;
+ provider: string;
+ since: AbsoluteTime;
+ };
+ };
+}
+
+function getConsolidated(
+ history: AmlEvent[],
+ when: AbsoluteTime,
+): Consolidated {
+ const initial: Consolidated = {
+ aml: {
+ 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.threshold = cur.threshold;
+ prev.aml.state = cur.state;
+ prev.aml.since = cur.when;
+ break;
+ }
+ case "kyc-collection": {
+ Object.keys(cur.values).forEach((field) => {
+ prev.kyc[field] = {
+ value: (cur.values as any)[field],
+ provider: cur.provider,
+ since: cur.when,
+ };
+ });
+ break;
+ }
+ }
+ return prev;
+ }, initial);
+}
+
+export const amlStateConverter = {
+ toStringUI: stringifyAmlState,
+ fromStringUI: parseAmlState,
+};
+
+function stringifyAmlState(s: AmlState | undefined): string {
+ if (s === undefined) return "";
+ switch (s) {
+ case AmlState.normal:
+ return "normal";
+ case AmlState.pending:
+ return "pending";
+ case AmlState.frozen:
+ return "frozen";
+ }
+}
+
+function parseAmlState(s: string | undefined): AmlState {
+ switch (s) {
+ case "normal":
+ return AmlState.normal;
+ case "pending":
+ return AmlState.pending;
+ case "frozen":
+ return AmlState.frozen;
+ default:
+ throw Error(`unknown AML state: ${s}`);
+ }
+}
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..28b9d2a88
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -0,0 +1,288 @@
+import { VNode, h } from "preact";
+import { Pages } from "../pages.js";
+import { AmlRecords, AmlState } from "../types.js";
+import { InputChoiceHorizontal } from "../handlers/InputChoiceHorizontal.js";
+import { createNewForm } from "../handlers/forms.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { amlStateConverter as amlStateConverter } from "./CaseDetails.js";
+import { useState } from "preact/hooks";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { useOfficer } from "../hooks/useOfficer.js";
+
+const response: AmlRecords = {
+ records: [
+ {
+ current_state: 0,
+ h_payto: "QWEQWEQWEQWEWQE",
+ rowid: 1,
+ threshold: "USD 100",
+ },
+ {
+ current_state: 1,
+ h_payto: "ASDASDASD",
+ rowid: 1,
+ threshold: "USD 100",
+ },
+ {
+ current_state: 2,
+ h_payto: "ZXCZXCZXCXZC",
+ rowid: 1,
+ threshold: "USD 1000",
+ },
+ {
+ current_state: 0,
+ h_payto: "QWEQWEQWEQWEWQE",
+ rowid: 1,
+ threshold: "USD 100",
+ },
+ {
+ current_state: 1,
+ h_payto: "ASDASDASD",
+ rowid: 1,
+ threshold: "USD 100",
+ },
+ {
+ current_state: 2,
+ h_payto: "ZXCZXCZXCXZC",
+ rowid: 1,
+ threshold: "USD 1000",
+ },
+ ].map((e, idx) => {
+ e.rowid = idx;
+ e.threshold = `${e.threshold}${idx}`;
+ return e;
+ }),
+};
+
+function doFilter(
+ list: typeof response.records,
+ filter: AmlState | undefined,
+): typeof response.records {
+ if (filter === undefined) return list;
+ return list.filter((r) => r.current_state === filter);
+}
+
+export function Cases() {
+ const officer = useOfficer();
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+ const form = createNewForm<{
+ state: AmlState;
+ }>();
+ const initial = { state: AmlState.pending };
+ const [list, setList] = useState(doFilter(response.records, initial.state));
+ return (
+ <div>
+ <div class="px-4 sm:px-6 lg:px-8">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ Cases
+ </h1>
+ <p class="mt-2 text-sm text-gray-700">
+ A list of all the account with the status
+ </p>
+ </div>
+ <form.Provider
+ initialValue={initial}
+ onUpdate={(v) => {
+ setList(doFilter(response.records, v.state));
+ }}
+ onSubmit={(v) => {}}
+ >
+ <form.InputChoiceHorizontal
+ name="state"
+ label={"Filter" as TranslatedString}
+ converter={amlStateConverter}
+ choices={[
+ {
+ label: "Pending" as TranslatedString,
+ value: AmlState.pending,
+ },
+ {
+ label: "Frozen" as TranslatedString,
+ value: AmlState.frozen,
+ },
+ {
+ label: "Normal" as TranslatedString,
+ value: AmlState.normal,
+ },
+ ]}
+ />
+ </form.Provider>
+ </div>
+ <div class="mt-8 flow-root">
+ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ <Pagination />
+ <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"
+ >
+ Account Id
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >
+ Status
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >
+ Threshold
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {list.map((r) => {
+ return (
+ <tr class="hover:bg-gray-100 ">
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
+ <div class="text-gray-900">
+ <a
+ href={Pages.details.url({ account: r.h_payto })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {r.h_payto}
+ </a>
+ </div>
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
+ {((state: AmlState): VNode => {
+ switch (state) {
+ case 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 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 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 />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function Pagination() {
+ return (
+ <nav class="flex items-center justify-between px-4 sm:px-0">
+ <div class="-mt-px flex w-0 flex-1">
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent pr-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ <svg
+ class="mr-3 h-5 w-5 text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M18 10a.75.75 0 01-.75.75H4.66l2.1 1.95a.75.75 0 11-1.02 1.1l-3.5-3.25a.75.75 0 010-1.1l3.5-3.25a.75.75 0 111.02 1.1l-2.1 1.95h12.59A.75.75 0 0118 10z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ Previous
+ </a>
+ </div>
+ <div class="hidden md:-mt-px md:flex">
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ 1
+ </a>
+ {/* <!-- Current: "border-indigo-500 text-indigo-600", Default: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" --> */}
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500"
+ aria-current="page"
+ >
+ 2
+ </a>
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ 3
+ </a>
+ <span class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500">
+ ...
+ </span>
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ 8
+ </a>
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ 9
+ </a>
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ 10
+ </a>
+ </div>
+ <div class="-mt-px flex w-0 flex-1 justify-end">
+ <a
+ href="#"
+ class="inline-flex items-center border-t-2 border-transparent pl-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
+ >
+ Next
+ <svg
+ class="ml-3 h-5 w-5 text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </a>
+ </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..5dcb8b21d
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -0,0 +1,102 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ notifyError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { createNewForm } from "../handlers/forms.js";
+
+export function CreateAccount({
+ onNewAccount,
+}: {
+ onNewAccount: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const Form = createNewForm<{
+ password: string;
+ repeat: string;
+ }>();
+
+ return (
+ <div class="flex min-h-full flex-col ">
+ <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">
+ Create account
+ </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.Provider
+ computeFormState={(v) => {
+ return {
+ password: {
+ error: !v.password
+ ? i18n.str`required`
+ : v.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !v.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !v.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+ },
+ repeat: {
+ error: !v.repeat
+ ? i18n.str`required`
+ : v.repeat !== v.password
+ ? i18n.str`doesn't match`
+ : undefined,
+ },
+ };
+ }}
+ onSubmit={async (v, s) => {
+ console.log(v, s);
+ const error = s?.password?.error ?? s?.repeat?.error;
+ console.log(error);
+ if (error) {
+ notifyError(
+ "Can't create account" as TranslatedString,
+ error as TranslatedString,
+ );
+ } else {
+ onNewAccount(v.password!);
+ }
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Password" as TranslatedString}
+ name="password"
+ type="password"
+ help={
+ "lower and upper case letters, number and special character" as TranslatedString
+ }
+ required
+ />
+ </div>
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Repeat password" as TranslatedString}
+ name="repeat"
+ type="password"
+ required
+ />
+ </div>
+
+ <div class="mt-8">
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ Create
+ </button>
+ </div>
+ </Form.Provider>
+ </div>
+ </div>
+ </div>
+ );
+}
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..05fd0a019
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -0,0 +1,34 @@
+import { VNode, h } from "preact";
+import { OfficerNotReady } from "../hooks/useOfficer.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
+ onNewAccount={(password) => {
+ officer.create(password);
+ }}
+ />
+ );
+ }
+
+ if (officer.state === "locked") {
+ return (
+ <UnlockAccount
+ onRemoveAccount={() => {
+ officer.forget();
+ }}
+ onAccountUnlocked={(pwd) => {
+ officer.tryUnlock(pwd);
+ }}
+ />
+ );
+ }
+ throw Error(`unexpected account state ${(officer as any).state}`);
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Home.tsx b/packages/aml-backoffice-ui/src/pages/Home.tsx
new file mode 100644
index 000000000..838032d63
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Home.tsx
@@ -0,0 +1,5 @@
+import { h } from "preact";
+
+export function Home() {
+ return <div>Home</div>;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
new file mode 100644
index 000000000..fdb255701
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
@@ -0,0 +1,76 @@
+import { VNode, h } from "preact";
+import { allForms } from "./AntiMoneyLaunderingForm.js";
+import { Pages } from "../pages.js";
+import { NiceForm } from "../NiceForm.js";
+import { AmlState } from "../types.js";
+import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
+
+export function NewFormEntry({
+ account,
+ type,
+}: {
+ account?: string;
+ type?: string;
+}): VNode {
+ if (!account) {
+ return <div>no account</div>;
+ }
+ if (!type) {
+ return <SelectForm account={account} />;
+ }
+
+ const selectedForm = Number.parseInt(type ?? "0", 10);
+ if (Number.isNaN(selectedForm)) {
+ return <div>WHAT! {type}</div>;
+ }
+ const showingFrom = allForms[selectedForm].impl;
+ const initial = {
+ fullName: "loggedIn_user_fullname",
+ when: AbsoluteTime.now(),
+ state: AmlState.pending,
+ threshold: Amounts.parseOrThrow("USD:10"),
+ };
+ return (
+ <NiceForm
+ initial={initial}
+ form={showingFrom(initial)}
+ onSubmit={(v) => {
+ alert(JSON.stringify(v));
+ }}
+ >
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <a
+ // type="button"
+ href={Pages.details.url({ account })}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ Cancel
+ </a>
+ <button
+ type="submit"
+ class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ Confirm
+ </button>
+ </div>
+ </NiceForm>
+ );
+}
+
+function SelectForm({ account }: { account: string }) {
+ return (
+ <div>
+ <pre>New form for account: {account}</pre>
+ {allForms.map((form, idx) => {
+ return (
+ <a
+ href={Pages.newFormEntry.url({ account, type: String(idx) })}
+ 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.name}
+ </a>
+ );
+ })}
+ </div>
+ );
+}
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..5320369e4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx
@@ -0,0 +1,55 @@
+import { Fragment, h } from "preact";
+import { useOfficer } from "../hooks/useOfficer.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+
+export function Officer() {
+ const officer = useOfficer();
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ return (
+ <div>
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ Public key
+ </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.accountId}</p>
+ </div>
+ <p>
+ <a
+ href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent(
+ `I want my AML account\n\n\nPubKey: ${officer.account.accountId}`,
+ )}`}
+ 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"
+ >
+ Request account activation
+ </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 "
+ >
+ Lock account
+ </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 "
+ >
+ Remove account
+ </button>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Settings.tsx b/packages/aml-backoffice-ui/src/pages/Settings.tsx
new file mode 100644
index 000000000..ccff3b210
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Settings.tsx
@@ -0,0 +1,5 @@
+import { h } from "preact";
+
+export function Settings() {
+ return <div>Settings</div>;
+}
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..2ebac0718
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -0,0 +1,81 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { UnwrapKeyError } from "../account.js";
+import { createNewForm } from "../handlers/forms.js";
+
+export function UnlockAccount({
+ onAccountUnlocked,
+ onRemoveAccount,
+}: {
+ onAccountUnlocked: (password: string) => void;
+ onRemoveAccount: () => void;
+}): VNode {
+ const Form = createNewForm<{
+ password: string;
+ }>();
+
+ return (
+ <div class="flex min-h-full flex-col ">
+ <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">
+ Account locked
+ </h2>
+ <p class="mt-6 text-lg leading-8 text-gray-600">
+ Your account is normally locked anytime you reload. To unlock type
+ your password again.
+ </p>
+ </div>
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+ <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+ <Form.Provider
+ onSubmit={async (v) => {
+ try {
+ await onAccountUnlocked(v.password!);
+
+ notifyInfo("Account unlocked" as TranslatedString);
+ } catch (e) {
+ if (e instanceof UnwrapKeyError) {
+ notifyError(
+ "Could not unlock account" as any,
+ e.message as any,
+ );
+ } else {
+ throw e;
+ }
+ }
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Password" as TranslatedString}
+ name="password"
+ type="password"
+ required
+ />
+ </div>
+
+ <div class="mt-8">
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ Unlock
+ </button>
+ </div>
+ </Form.Provider>
+ </div>
+ <button
+ type="button"
+ onClick={() => {
+ onRemoveAccount();
+ }}
+ class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ Remove account
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Welcome.tsx b/packages/aml-backoffice-ui/src/pages/Welcome.tsx
new file mode 100644
index 000000000..433fbcf59
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Welcome.tsx
@@ -0,0 +1,9 @@
+import { h } from "preact";
+
+export function Welcome({ name, asd }: { asd?: string; name?: string }) {
+ return (
+ <div>
+ {asd} Hello {name}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts
new file mode 100644
index 000000000..d54f9be83
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/route.ts
@@ -0,0 +1,167 @@
+import { createHashHistory } from "history";
+import { h as create, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+const history = createHashHistory();
+
+type PageDefinition<DynamicPart extends Record<string, string>> = {
+ pattern: string;
+ (params: DynamicPart): string;
+};
+
+function replaceAll(
+ pattern: string,
+ vars: Record<string, string>,
+ values: Record<string, string>,
+): string {
+ let result = pattern;
+ for (const v in vars) {
+ result = result.replace(vars[v], !values[v] ? "" : values[v]);
+ }
+ return result;
+}
+
+export function pageDefinition<T extends Record<string, string>>(
+ pattern: string,
+): PageDefinition<T> {
+ const patternParams = pattern.match(/(:[\w?]*)/g);
+ if (!patternParams)
+ throw Error(
+ `page definition pattern ${pattern} doesn't have any parameter`,
+ );
+
+ const vars = patternParams.reduce((prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
+
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ }, {} as Record<string, string>);
+
+ const f = (values: T): string => replaceAll(pattern, vars, values);
+ f.pattern = pattern;
+ return f;
+}
+
+export type PageEntry<T = unknown> = T extends Record<string, string>
+ ? {
+ url: PageDefinition<T>;
+ view: (props: T) => VNode;
+ }
+ : T extends unknown
+ ? {
+ url: string;
+ view: (props: {}) => VNode;
+ }
+ : never;
+
+export function Router({
+ pageList,
+ onNotFound,
+}: {
+ pageList: Array<PageEntry<any>>;
+ onNotFound: () => VNode;
+}): VNode {
+ const current = useCurrentLocation(pageList);
+ if (current !== undefined) {
+ return create(current.page.view, current.values);
+ }
+ return onNotFound();
+}
+
+type Location = {
+ page: PageEntry<any>;
+ path: string;
+ values: Record<string, string>;
+};
+export function useCurrentLocation(pageList: Array<PageEntry<any>>) {
+ const [currentLocation, setCurrentLocation] = useState<Location>();
+ /**
+ * Search path in the pageList
+ * get the values from the path found
+ * add params from searchParams
+ *
+ * @param path
+ * @param params
+ */
+ function doSync(path: string, params: URLSearchParams) {
+ let result: typeof currentLocation;
+ for (let idx = 0; idx < pageList.length; idx++) {
+ const page = pageList[idx];
+ if (typeof page.url === "string") {
+ if (page.url === path) {
+ const values: Record<string, string> = {};
+ params.forEach((v, k) => {
+ values[k] = v;
+ });
+ result = { page, values, path };
+ break;
+ }
+ } else {
+ const values = doestUrlMatchToRoute(path, page.url.pattern);
+ if (values !== undefined) {
+ params.forEach((v, k) => {
+ values[k] = v;
+ });
+ result = { page, values, path };
+ break;
+ }
+ }
+ }
+ setCurrentLocation(result);
+ }
+ useEffect(() => {
+ doSync(window.location.hash, new URLSearchParams(window.location.search));
+ return history.listen(() => {
+ doSync(window.location.hash, new URLSearchParams(window.location.search));
+ });
+ }, []);
+ return currentLocation;
+}
+
+function doestUrlMatchToRoute(
+ url: string,
+ route: string,
+): undefined | Record<string, string> {
+ const paramsPattern = /(?:\?([^#]*))?$/;
+ // const paramsPattern = /(?:\?([^#]*))?(#.*)?$/;
+ const params = url.match(paramsPattern);
+ const urlWithoutParams = url.replace(paramsPattern, "");
+
+ const result: Record<string, string> = {};
+ if (params && params[1]) {
+ const paramList = params[1].split("&");
+ for (let i = 0; i < paramList.length; i++) {
+ const idx = paramList[i].indexOf("=");
+ const name = paramList[i].substring(0, idx);
+ const value = paramList[i].substring(idx + 1);
+ result[decodeURIComponent(name)] = decodeURIComponent(value);
+ }
+ }
+ const urlSeg = urlWithoutParams.split("/");
+ const routeSeg = route.split("/");
+ let max = Math.max(urlSeg.length, routeSeg.length);
+ for (let i = 0; i < max; i++) {
+ if (routeSeg[i] && routeSeg[i].charAt(0) === ":") {
+ const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, "");
+
+ const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || "";
+ const plus = ~flags.indexOf("+");
+ const star = ~flags.indexOf("*");
+ const val = urlSeg[i] || "";
+
+ if (!val && !star && (flags.indexOf("?") < 0 || plus)) {
+ return undefined;
+ }
+ result[param] = decodeURIComponent(val);
+ if (plus || star) {
+ result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/");
+ break;
+ }
+ } else if (routeSeg[i] !== urlSeg[i]) {
+ return undefined;
+ }
+ }
+ return result;
+}
+const EMPTY: Record<string, string> = {};
diff --git a/packages/aml-backoffice-ui/src/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/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
new file mode 100644
index 000000000..4e24967e4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/stories.test.ts
@@ -0,0 +1,56 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { setupI18n } from "@gnu-taler/taler-util";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+
+// import * as components from "./components/index.examples.js";
+// import * as pages from "./pages/index.stories.js";
+
+import { ComponentChildren, Fragment, VNode, h as create } from "preact";
+// import { BackendStateProviderTesting } from "./context/backend.js";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({});
+ 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 {
+ return create(Fragment, {});
+}
diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx
new file mode 100644
index 000000000..b6c0c1f07
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/stories.tsx
@@ -0,0 +1,44 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { strings } from "./i18n/strings.js";
+
+// import * as pages from "./pages/index.stories.js";
+// import * as components from "./components/index.examples.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+
+// import "./scss/main.scss";
+
+function main(): void {
+ renderStories(
+ // { pages, components },
+ {},
+ {
+ strings,
+ },
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/aml-backoffice-ui/src/types.ts b/packages/aml-backoffice-ui/src/types.ts
new file mode 100644
index 000000000..1197b6b35
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/types.ts
@@ -0,0 +1,81 @@
+export interface AmlDecisionDetails {
+ // Array of AML decisions made for this account. Possibly
+ // contains only the most recent decision if "history" was
+ // not set to 'true'.
+ aml_history: AmlDecisionDetail[];
+
+ // Array of KYC attributes obtained for this account.
+ kyc_attributes: KycDetail[];
+}
+
+type AmlOfficerPublicKeyP = string;
+
+export interface AmlDecisionDetail {
+ // What was the justification given?
+ justification: string;
+
+ // What is the new AML state.
+ new_state: Integer;
+
+ // When was this decision made?
+ decision_time: Timestamp;
+
+ // What is the new AML decision threshold (in monthly transaction volume)?
+ new_threshold: Amount;
+
+ // Who made the decision?
+ decider_pub: AmlOfficerPublicKeyP;
+}
+export interface KycDetail {
+ // Name of the configuration section that specifies the provider
+ // which was used to collect the KYC details
+ provider_section: string;
+
+ // The collected KYC data. NULL if the attribute data could not
+ // be decrypted (internal error of the exchange, likely the
+ // attribute key was changed).
+ attributes?: Object;
+
+ // Time when the KYC data was collected
+ collection_time: Timestamp;
+
+ // Time when the validity of the KYC data will expire
+ expiration_time: Timestamp;
+}
+
+interface Timestamp {
+ // Seconds since epoch, or the special
+ // value "never" to represent an event that will
+ // never happen.
+ t_s: number | "never";
+}
+
+type PaytoHash = string;
+type Integer = number;
+type Amount = string;
+
+export interface AmlRecords {
+ // Array of AML records matching the query.
+ records: AmlRecord[];
+}
+
+interface AmlRecord {
+ // Which payto-address is this record about.
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the current AML state.
+ current_state: AmlState;
+
+ // Monthly transaction threshold before a review will be triggered
+ threshold: Amount;
+
+ // RowID of the record.
+ rowid: Integer;
+}
+
+export enum AmlState {
+ normal = 0,
+ pending = 1,
+ frozen = 2,
+}