gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/03: cases, account details and new-form screen


From: gnunet
Subject: [taler-wallet-core] 01/03: cases, account details and new-form screen
Date: Fri, 26 May 2023 14:56:24 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit 64e3705669e7c12b8013704654f17cf8eaf659d4
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu May 25 18:08:20 2023 -0300

    cases, account details and new-form screen
---
 packages/exchange-backoffice-ui/src/Dashboard.tsx  | 344 +++++++++-------
 packages/exchange-backoffice-ui/src/NiceForm.tsx   |  13 +-
 packages/exchange-backoffice-ui/src/account.ts     |  48 ++-
 .../src/assets/logo-2021.svg                       |   9 +
 .../exchange-backoffice-ui/src/declaration.d.ts    |  28 ++
 .../exchange-backoffice-ui/src/forms/902_11e.ts    |  11 +-
 .../exchange-backoffice-ui/src/forms/902_12e.ts    |  11 +-
 .../exchange-backoffice-ui/src/forms/902_13e.ts    |  11 +-
 .../exchange-backoffice-ui/src/forms/902_15e.ts    |  11 +-
 .../exchange-backoffice-ui/src/forms/902_1e.ts     |  11 +-
 .../exchange-backoffice-ui/src/forms/902_4e.ts     |   5 +-
 .../exchange-backoffice-ui/src/forms/902_5e.ts     |  11 +-
 .../exchange-backoffice-ui/src/forms/902_9e.ts     |  11 +-
 .../exchange-backoffice-ui/src/forms/simplest.ts   |  96 +++++
 .../src/handlers/FormProvider.tsx                  |  43 +-
 .../src/handlers/InputAmount.tsx                   |  34 ++
 .../src/handlers/InputChoiceHorizontal.tsx         |  86 ++++
 .../src/handlers/InputChoiceStacked.tsx            |  18 +-
 .../src/handlers/InputLine.tsx                     |   6 +-
 .../exchange-backoffice-ui/src/handlers/forms.ts   |  32 +-
 .../src/handlers/useField.ts                       |  31 +-
 packages/exchange-backoffice-ui/src/index.html     |   2 +
 packages/exchange-backoffice-ui/src/pages.ts       |  30 +-
 .../src/pages/AccountDetails.tsx                   | 457 +++++++++++++++++++++
 .../src/pages/AntiMoneyLaunderingForm.tsx          |  22 +-
 .../exchange-backoffice-ui/src/pages/Cases.tsx     | 282 +++++++++++++
 packages/exchange-backoffice-ui/src/pages/Info.tsx |   5 -
 .../src/pages/NewFormEntry.tsx                     |  78 ++++
 .../exchange-backoffice-ui/src/pages/Officer.tsx   | 204 ++++-----
 packages/exchange-backoffice-ui/src/route.ts       |   4 +-
 packages/exchange-backoffice-ui/src/types.ts       |  81 ++++
 packages/web-util/src/hooks/useLang.ts             |   4 +-
 packages/web-util/src/hooks/useLocalStorage.ts     |  64 ++-
 33 files changed, 1722 insertions(+), 381 deletions(-)

diff --git a/packages/exchange-backoffice-ui/src/Dashboard.tsx 
b/packages/exchange-backoffice-ui/src/Dashboard.tsx
index 9be86c533..9f4a43513 100644
--- a/packages/exchange-backoffice-ui/src/Dashboard.tsx
+++ b/packages/exchange-backoffice-ui/src/Dashboard.tsx
@@ -23,39 +23,14 @@ import {
   useMemoryStorage,
   useNotifications,
 } from "@gnu-taler/web-util/browser";
-
-/**
- * references between forms
- *
- * 902.1e
- *  --> 902.11 (operational legal entity or partnership)
- *  --> 902.12 (a foundation)
- *  --> 902.13 (a trust)
- *  --> 902.15 (life insurance policy)
- *  --> 902.9 (all other cases)
- *  --> 902.5 (cash transaction with no customer profile)
- *  --> 902.4 (risk profile)
- *
- * 902.11
- *  --> 902.9 (beneficial owner in fiduciary holding assets)
- *
- * 902.12
- *
- * 902.13
- *
- * 902.15
- *
- * 902.9
- *
- * 902.5
- *
- * 902.4
- */
-
-const userNavigation = [
-  { name: "Your profile", href: "#" },
-  { name: "Sign out", href: "#" },
-];
+import {
+  AbsoluteTime,
+  Codec,
+  buildCodecForObject,
+  codecForAbsoluteTime,
+  codecForString,
+} from "@gnu-taler/taler-util";
+import logo from "./assets/logo-2021.svg";
 
 function classNames(...classes: string[]) {
   return classes.filter(Boolean).join(" ");
@@ -153,7 +128,7 @@ function LeftMenu() {
                   )}
                   aria-hidden="true"
                 />
-                Info
+                Cases
               </a>
             </li>
             <li>
@@ -175,7 +150,7 @@ function LeftMenu() {
                   )}
                   aria-hidden="true"
                 />
-                Officer
+                Account
               </a>
             </li>
           </ul>
@@ -203,7 +178,7 @@ function LeftMenu() {
             </li>
           </ul>
         </li> */}
-        <li class="mt-auto">
+        {/* <li class="mt-auto">
           <a
             href={Pages.settings.url}
             class={classNames(
@@ -224,7 +199,7 @@ function LeftMenu() {
             />
             Settings
           </a>
-        </li>
+        </li> */}
       </ul>
     </nav>
   );
@@ -237,26 +212,18 @@ export function Dashboard({
 }): VNode {
   const [sidebarOpen, setSidebarOpen] = useState(false);
 
-  const logRef = useRef<HTMLPreElement>(null);
-  function showFormOnSidebar(v: any) {
-    if (!logRef.current) return;
-    logRef.current.innerHTML = JSON.stringify(v, undefined, 1);
-  }
   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">
-            <img
-              class="h-8 w-auto"
-              src="https://tailwindui.com/img/logos/mark.svg?color=white";
-              alt="Taler"
-            />
+            <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 />
-          <div class="text-white text-sm">
-            <pre ref={logRef}></pre>
-          </div>
           <Footer />
         </div>
       </NavigationBar>
@@ -362,123 +329,193 @@ function NavigationBar({
   );
 }
 
+export interface Officer {
+  salt: string;
+  when: AbsoluteTime;
+  key: string;
+}
+
+export const codecForOfficer = (): Codec<Officer> =>
+  buildCodecForObject<Officer>()
+    .property("salt", codecForString()) // FIXME
+    .property("when", codecForAbsoluteTime) // FIXME
+    .property("key", codecForString())
+    .build("Officer");
+
 function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) {
   const password = useMemoryStorage("password");
-  const officer = useLocalStorage("officer");
+  const officer = useLocalStorage("officer", {
+    codec: codecForOfficer(),
+  });
 
   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"
+    <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"
-          />
-          <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"
+            <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 }}
           />
-
-          {officer.value === undefined ? (
-            <div />
-          ) : (
-            <Menu
-              as="div"
-              /* @ts-ignore */
-              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"
-                  >
-                    {/* Tom Cook */}
-                    {officer.value?.substring(0, 6)}
-                  </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
-                        // href={item.href}
-                        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 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">
@@ -502,7 +539,6 @@ function Notifications() {
   {
     /* <!-- Global notification live region, render this permanently at the 
end of the document --> */
   }
-  console.log("render", ns.length);
   return (
     <div
       aria-live="assertive"
diff --git a/packages/exchange-backoffice-ui/src/NiceForm.tsx 
b/packages/exchange-backoffice-ui/src/NiceForm.tsx
index 593a373c1..69b977ee0 100644
--- a/packages/exchange-backoffice-ui/src/NiceForm.tsx
+++ b/packages/exchange-backoffice-ui/src/NiceForm.tsx
@@ -1,5 +1,5 @@
 import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h } from "preact";
+import { ComponentChildren, Fragment, h } from "preact";
 import { FlexibleForm } from "./forms/index.js";
 import { FormProvider } from "./handlers/FormProvider.js";
 import { RenderAllFieldsByUiConfig } from "./handlers/forms.js";
@@ -8,21 +8,25 @@ export function NiceForm<T extends object>({
   initial,
   onUpdate,
   form,
+  onSubmit,
+  children,
 }: {
+  children?: ComponentChildren;
   initial: Partial<T>;
+  onSubmit?: (v: T) => void;
   form: FlexibleForm<T>;
-  onUpdate: (d: Partial<T>) => void;
+  onUpdate?: (d: Partial<T>) => void;
 }) {
-  const { i18n } = useTranslationContext();
   return (
     <FormProvider
       initialValue={initial}
       onUpdate={onUpdate}
-      onSubmit={() => {}}
+      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">
@@ -49,6 +53,7 @@ export function NiceForm<T extends object>({
           );
         })}
       </div>
+      {children}
     </FormProvider>
   );
 }
diff --git a/packages/exchange-backoffice-ui/src/account.ts 
b/packages/exchange-backoffice-ui/src/account.ts
index 1e770794a..019c0bb43 100644
--- a/packages/exchange-backoffice-ui/src/account.ts
+++ b/packages/exchange-backoffice-ui/src/account.ts
@@ -7,28 +7,33 @@ import { decodeCrock, encodeCrock } from 
"@gnu-taler/taler-util";
  *
  * @returns session id as string
  */
-export function createNewSessionId(): string {
+export function createSalt(): string {
   const salt = crypto.getRandomValues(new Uint8Array(8));
   const iv = crypto.getRandomValues(new Uint8Array(12));
   return encodeCrock(salt.buffer) + "-" + encodeCrock(iv.buffer);
 }
 
+export interface Account {
+  accountId: string;
+  secret: CryptoKey;
+}
+
 /**
  * Restore previous session and unlock account
  *
- * @param sessionId string from which crypto params will be derived
- * @param accountId secured private key
+ * @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(
-  sessionId: string,
-  accountId: string,
+  salt: string,
+  key: string,
   password: string,
-) {
-  const key = str2ab(window.atob(accountId));
+): Promise<Account> {
+  const rawKey = str2ab(window.atob(key));
 
-  const privateKey = await recoverWithPassword(key, sessionId, password);
+  const privateKey = await recoverWithPassword(rawKey, salt, password);
 
   const publicKey = await getPublicFromPrivate(privateKey);
 
@@ -36,9 +41,9 @@ export async function unlockAccount(
     throw new Error(String(e));
   });
 
-  const pub = btoa(ab2str(pubRaw));
+  const accountId = btoa(ab2str(pubRaw));
 
-  return { accountId, pub };
+  return { accountId, secret: privateKey };
 }
 
 /**
@@ -49,12 +54,13 @@ export async function unlockAccount(
  * @param password
  * @returns
  */
-export async function createNewAccount(sessionId: string, password: string) {
-  const { privateKey, publicKey } = await createPair();
+export async function createNewAccount(password: string) {
+  const { privateKey } = await createPair();
+  const salt = createSalt();
 
   const protectedPrivKey = await protectWithPassword(
     privateKey,
-    sessionId,
+    salt,
     password,
   );
 
@@ -64,14 +70,14 @@ export async function createNewAccount(sessionId: string, 
password: string) {
   //       throw new Error(String(e));
   //     });
 
-  const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => 
{
-    throw new Error(String(e));
-  });
+  // const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) 
=> {
+  //   throw new Error(String(e));
+  // });
 
-  const pub = btoa(ab2str(pubRaw));
+  // const pub = btoa(ab2str(pubRaw));
   const protectedPriv = btoa(ab2str(protectedPrivKey));
 
-  return { accountId: protectedPriv, pub };
+  return { accountId: protectedPriv, salt };
 }
 
 const rsaAlgorithm: RsaHashedKeyGenParams = {
@@ -97,7 +103,7 @@ async function protectWithPassword(
   sessionId: string,
   password: string,
 ): Promise<ArrayBuffer> {
-  const { salt, initVector: iv } = getCryptoPArameters(sessionId);
+  const { salt, initVector: iv } = getCryptoParameters(sessionId);
   const passwordAsKey = await crypto.subtle
     .importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, 
[
       "deriveBits",
@@ -139,7 +145,7 @@ async function recoverWithPassword(
   sessionId: string,
   password: string,
 ): Promise<CryptoKey> {
-  const { salt, initVector: iv } = getCryptoPArameters(sessionId);
+  const { salt, initVector: iv } = getCryptoParameters(sessionId);
 
   const master = await crypto.subtle
     .importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, 
[
@@ -231,7 +237,7 @@ function str2ab(str: string) {
   return buf;
 }
 
-function getCryptoPArameters(sessionId: string): {
+function getCryptoParameters(sessionId: string): {
   salt: Uint8Array;
   initVector: Uint8Array;
 } {
diff --git a/packages/exchange-backoffice-ui/src/assets/logo-2021.svg 
b/packages/exchange-backoffice-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/exchange-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.4 [...]
+</svg>
\ No newline at end of file
diff --git a/packages/exchange-backoffice-ui/src/declaration.d.ts 
b/packages/exchange-backoffice-ui/src/declaration.d.ts
index c1e9addbc..11a10860d 100644
--- a/packages/exchange-backoffice-ui/src/declaration.d.ts
+++ b/packages/exchange-backoffice-ui/src/declaration.d.ts
@@ -1,2 +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/exchange-backoffice-ui/src/forms/902_11e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_11e.ts
index 0e9a28dce..267b5b52d 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_11e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_11e.ts
@@ -1,8 +1,9 @@
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_11e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -115,8 +116,8 @@ export const v1: FlexibleForm<Form902_11e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_11e.Form>,
-  ): FormState<Form902_11e.Form> {
+    v: Partial<Form902_11.Form>,
+  ): FormState<Form902_11.Form> {
     return {
       person: {
         hidden:
@@ -128,9 +129,9 @@ export const v1: FlexibleForm<Form902_11e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_11e {
+namespace Form902_11 {
   interface Person {
     lastName: string;
     firstName: string;
diff --git a/packages/exchange-backoffice-ui/src/forms/902_12e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_12e.ts
index e58850660..56a3986ee 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_12e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_12e.ts
@@ -1,8 +1,9 @@
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_12e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -364,8 +365,8 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_12e.Form>,
-  ): FormState<Form902_12e.Form> {
+    v: Partial<Form902_12.Form>,
+  ): FormState<Form902_12.Form> {
     return {
       founders: {
         elements: (v.founders ?? []).map((f) => {
@@ -390,9 +391,9 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_12e {
+namespace Form902_12 {
   interface Foundation {
     name: string;
     type: "discretionary" | "non-discretionary";
diff --git a/packages/exchange-backoffice-ui/src/forms/902_13e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_13e.ts
index bca96e842..e933432e4 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_13e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_13e.ts
@@ -1,8 +1,9 @@
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_13e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -441,8 +442,8 @@ export const v1: FlexibleForm<Form902_13e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_13e.Form>,
-  ): FormState<Form902_13e.Form> {
+    v: Partial<Form902_13.Form>,
+  ): FormState<Form902_13.Form> {
     return {
       settlors: {
         elements: (v.settlors ?? []).map((f) => {
@@ -476,9 +477,9 @@ export const v1: FlexibleForm<Form902_13e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_13e {
+namespace Form902_13 {
   interface Foundation {
     name: string;
     type: "discretionary" | "non-discretionary";
diff --git a/packages/exchange-backoffice-ui/src/forms/902_15e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_15e.ts
index 8e3fa1350..be304f357 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_15e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_15e.ts
@@ -1,8 +1,9 @@
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_15e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -160,17 +161,17 @@ export const v1: FlexibleForm<Form902_15e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_15e.Form>,
-  ): FormState<Form902_15e.Form> {
+    v: Partial<Form902_15.Form>,
+  ): FormState<Form902_15.Form> {
     return {
       when: {
         disabled: true,
       },
     };
   },
-};
+});
 
-namespace Form902_15e {
+namespace Form902_15 {
   interface Person {
     fullName: string;
     address: string;
diff --git a/packages/exchange-backoffice-ui/src/forms/902_1e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
index cd65cfedc..0f60c23d8 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
@@ -1,8 +1,9 @@
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import { FlexibleForm, languageList } from "./index.js";
 import { FormState } from "../handlers/FormProvider.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_1e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -512,8 +513,8 @@ export const v1: FlexibleForm<Form902_1e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_1e.Form>,
-  ): FormState<Form902_1e.Form> {
+    v: Partial<Form902_1.Form>,
+  ): FormState<Form902_1.Form> {
     return {
       fullName: {
         disabled: true,
@@ -606,9 +607,9 @@ export const v1: FlexibleForm<Form902_1e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_1e {
+namespace Form902_1 {
   interface LegalEntityCustomer {
     companyName: string;
     domicile: string;
diff --git a/packages/exchange-backoffice-ui/src/forms/902_4e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
index ca7ef8505..ffe3b28a2 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
@@ -4,8 +4,9 @@ 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";
 
-export const v1: FlexibleForm<Form902_4.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -745,7 +746,7 @@ export const v1: FlexibleForm<Form902_4.Form> = {
       },
     };
   },
-};
+});
 
 namespace Form902_4 {
   export interface Form {
diff --git a/packages/exchange-backoffice-ui/src/forms/902_5e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_5e.ts
index 60bd551d5..8ff72e0c5 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_5e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_5e.ts
@@ -5,8 +5,9 @@ import {
 } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm, currencyList } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_12e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -230,8 +231,8 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_12e.Form>,
-  ): FormState<Form902_12e.Form> {
+    v: Partial<Form902_5.Form>,
+  ): FormState<Form902_5.Form> {
     return {
       when: {
         disabled: true,
@@ -243,9 +244,9 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_12e {
+namespace Form902_5 {
   export interface Form {
     customer: string;
     fullName: string;
diff --git a/packages/exchange-backoffice-ui/src/forms/902_9e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_9e.ts
index 6d88f8578..831fcc9f9 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_9e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_9e.ts
@@ -1,8 +1,9 @@
 import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
 
-export const v1: FlexibleForm<Form902_9e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -104,17 +105,17 @@ export const v1: FlexibleForm<Form902_9e.Form> = {
     },
   ],
   behavior: function formBehavior(
-    v: Partial<Form902_9e.Form>,
-  ): FormState<Form902_9e.Form> {
+    v: Partial<Form902_9.Form>,
+  ): FormState<Form902_9.Form> {
     return {
       when: {
         disabled: true,
       },
     };
   },
-};
+});
 
-namespace Form902_9e {
+namespace Form902_9 {
   interface Person {
     surname: string;
     firstName: string;
diff --git a/packages/exchange-backoffice-ui/src/forms/simplest.ts 
b/packages/exchange-backoffice-ui/src/forms/simplest.ts
new file mode 100644
index 000000000..a395410c3
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/forms/simplest.ts
@@ -0,0 +1,96 @@
+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/AccountDetails.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.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,
+          },
+        },
+      ],
+    },
+    {
+      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,
+          },
+        },
+      ],
+    },
+  ],
+  behavior: function formBehavior(
+    v: Partial<Simplest.Form>,
+  ): FormState<Simplest.Form> {
+    return {
+      when: {
+        disabled: true,
+      },
+      threshold: {
+        disabled: v.state === AmlState.frozen,
+      },
+    };
+  },
+});
+
+namespace Simplest {
+  export interface Form {
+    when: AbsoluteTime;
+    threshold: AmountJson;
+    state: AmlState;
+    comment: string;
+  }
+}
diff --git a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx 
b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx
index 87c4c43fb..4ac90ad57 100644
--- a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx
+++ b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx
@@ -1,6 +1,16 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { ComponentChildren, VNode, createContext, h } from "preact";
-import { MutableRef, StateUpdater, useEffect, useRef } from "preact/hooks";
+import {
+  MutableRef,
+  StateUpdater,
+  useEffect,
+  useRef,
+  useState,
+} from "preact/hooks";
 
 export interface FormType<T> {
   value: MutableRef<Partial<T>>;
@@ -14,6 +24,8 @@ 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>>
@@ -40,22 +52,31 @@ export interface InputArrayFieldState<T> extends 
InputFieldState {
 export function FormProvider<T>({
   children,
   initialValue,
-  onUpdate,
+  onUpdate: notify,
   onSubmit,
   computeFormState,
 }: {
   initialValue?: Partial<T>;
   onUpdate?: (v: Partial<T>) => void;
-  onSubmit: (v: T) => void;
+  onSubmit?: (v: T) => void;
   computeFormState?: (v: T) => FormState<T>;
   children: ComponentChildren;
 }): VNode {
-  const value = useRef(initialValue ?? {});
-  useEffect(() => {
-    return function onUnload() {
-      value.current = initialValue ?? {};
-    };
-  });
+  // 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 }}
@@ -64,7 +85,7 @@ export function FormProvider<T>({
         onSubmit={(e) => {
           e.preventDefault();
           //@ts-ignore
-          onSubmit(value.current);
+          if (onSubmit) onSubmit(value.current);
         }}
       >
         {children}
diff --git a/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx 
b/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx
new file mode 100644
index 000000000..9be9dd4d0
--- /dev/null
+++ b/packages/exchange-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/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx 
b/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
new file mode 100644
index 000000000..fdee35447
--- /dev/null
+++ b/packages/exchange-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/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx 
b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx
index 3bce0123f..c37984368 100644
--- a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx
+++ b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx
@@ -3,15 +3,15 @@ import { Fragment, VNode, h } from "preact";
 import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
 import { useField } from "./useField.js";
 
-export interface Choice {
+export interface Choice<V> {
   label: TranslatedString;
   description?: TranslatedString;
-  value: string;
+  value: V;
 }
 
 export function InputChoiceStacked<T extends object, K extends keyof T>(
   props: {
-    choices: Choice[];
+    choices: Choice<T[K]>[];
   } & UIFormProps<T, K>,
 ): VNode {
   const {
@@ -41,6 +41,10 @@ export function InputChoiceStacked<T extends object, K 
extends keyof T>(
       <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) {
@@ -49,12 +53,18 @@ export function InputChoiceStacked<T extends object, K 
extends keyof T>(
             } else {
               clazz += " border-gray-300";
             }
+
             return (
               <label class={clazz}>
                 <input
                   type="radio"
                   name="server-size"
-                  defaultValue={choice.value}
+                  // defaultValue={choice.value}
+                  value={
+                    (!converter
+                      ? (choice.value as string)
+                      : converter?.toStringUI(choice.value)) ?? ""
+                  }
                   onClick={(e) => {
                     onChange(
                       (value === choice.value
diff --git a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx 
b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx
index 8e847a273..9448ef5e4 100644
--- a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx
+++ b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx
@@ -250,7 +250,8 @@ export function InputLine<T extends object, K extends keyof 
T>(
             onChange(fromString(e.currentTarget.value));
           }}
           placeholder={placeholder ? placeholder : undefined}
-          defaultValue={toString(value)}
+          value={toString(value) ?? ""}
+          // defaultValue={toString(value)}
           disabled={state.disabled}
           aria-invalid={showError}
           // aria-describedby="email-error"
@@ -269,7 +270,8 @@ export function InputLine<T extends object, K extends keyof 
T>(
           onChange(fromString(e.currentTarget.value));
         }}
         placeholder={placeholder ? placeholder : undefined}
-        defaultValue={toString(value)}
+        value={toString(value) ?? ""}
+        // defaultValue={toString(value)}
         disabled={state.disabled}
         aria-invalid={showError}
         // aria-describedby="email-error"
diff --git a/packages/exchange-backoffice-ui/src/handlers/forms.ts 
b/packages/exchange-backoffice-ui/src/handlers/forms.ts
index 115127cc3..4eb188a09 100644
--- a/packages/exchange-backoffice-ui/src/handlers/forms.ts
+++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts
@@ -13,8 +13,10 @@ 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 = DoubleColumnFormSection[];
+export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
 
 type DoubleColumnFormSection = {
   title: TranslatedString;
@@ -35,8 +37,10 @@ type FieldType<T extends object = any, K extends keyof T = 
any> = {
   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];
 };
 
 /**
@@ -47,11 +51,13 @@ export type UIFormField =
   | { 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"] };
 
@@ -79,11 +85,15 @@ const UIFormConfiguration: UIFormFieldMap = {
   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({
@@ -103,13 +113,23 @@ export function RenderAllFieldsByUiConfig({
   );
 }
 
-type FormSet<T extends object, K extends keyof T = any> = {
+type FormSet<T extends object> = {
   Provider: typeof FormProvider<T>;
-  InputLine: typeof InputLine<T, K>;
+  InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
+  InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<
+    T,
+    K
+  >;
 };
-export function createNewForm<T extends object>(): FormSet<T> {
-  return {
+export function createNewForm<T extends object>() {
+  const res: FormSet<T> = {
     Provider: FormProvider,
-    InputLine: InputLine,
+    InputLine: () => InputLine,
+    InputChoiceHorizontal: () => InputChoiceHorizontal,
+  };
+  return {
+    Provider: res.Provider,
+    InputLine: res.InputLine(),
+    InputChoiceHorizontal: res.InputChoiceHorizontal(),
   };
 }
diff --git a/packages/exchange-backoffice-ui/src/handlers/useField.ts 
b/packages/exchange-backoffice-ui/src/handlers/useField.ts
index 60e65f435..bf94d2f5d 100644
--- a/packages/exchange-backoffice-ui/src/handlers/useField.ts
+++ b/packages/exchange-backoffice-ui/src/handlers/useField.ts
@@ -1,9 +1,5 @@
-import { TargetedEvent, useContext, useState } from "preact/compat";
-import {
-  FormContext,
-  InputArrayFieldState,
-  InputFieldState,
-} from "./FormProvider.js";
+import { useContext, useState } from "preact/compat";
+import { FormContext, InputFieldState } from "./FormProvider.js";
 
 export interface InputFieldHandler<Type> {
   value: Type;
@@ -21,11 +17,13 @@ export function useField<T extends object, K extends keyof 
T>(
     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)) ?? {};
@@ -66,10 +64,23 @@ export function useField<T extends object, K extends keyof 
T>(
  * @param name
  * @returns
  */
-function readField<T>(object: any, name: string): T | undefined {
-  return name
-    .split(".")
-    .reduce((prev, current) => prev && prev[current], object);
+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 {
diff --git a/packages/exchange-backoffice-ui/src/index.html 
b/packages/exchange-backoffice-ui/src/index.html
index 3cf38851f..703d31da1 100644
--- a/packages/exchange-backoffice-ui/src/index.html
+++ b/packages/exchange-backoffice-ui/src/index.html
@@ -30,6 +30,8 @@
     />
     <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" />
diff --git a/packages/exchange-backoffice-ui/src/pages.ts 
b/packages/exchange-backoffice-ui/src/pages.ts
index a78a137a0..2b13ce585 100644
--- a/packages/exchange-backoffice-ui/src/pages.ts
+++ b/packages/exchange-backoffice-ui/src/pages.ts
@@ -4,15 +4,26 @@ 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 { Info } from "./pages/Info.js";
+import { Cases } from "./pages/Cases.js";
+import { AccountDetails } from "./pages/AccountDetails.js";
+import { NewFormEntry } from "./pages/NewFormEntry.js";
 
 const home: PageEntry = {
   url: "#/",
   view: Home,
 };
-const info: PageEntry = {
-  url: "#/info",
-  view: Info,
+const cases: PageEntry = {
+  url: "#/cases",
+  view: Cases,
+};
+const account: PageEntry<{ account?: string }> = {
+  url: pageDefinition("#/account/:account"),
+  view: AccountDetails,
+};
+
+const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
+  url: pageDefinition("#/account/:account/new/:type?"),
+  view: NewFormEntry,
 };
 
 const settings: PageEntry = {
@@ -32,4 +43,13 @@ const form: PageEntry<{ number?: string }> = {
   view: AntiMoneyLaunderingForm,
 };
 
-export const Pages = { home, info, officer, settings, welcome, form };
+export const Pages = {
+  home,
+  info: cases,
+  officer,
+  details: account,
+  settings,
+  welcome,
+  form,
+  newFormEntry,
+};
diff --git a/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx 
b/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
new file mode 100644
index 000000000..8b9b01ae6
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
@@ -0,0 +1,457 @@
+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: {
+        t_ms:
+          k.collection_time.t_s === "never"
+            ? "never"
+            : k.collection_time.t_s * 1000,
+      },
+      values: !k.attributes ? {} : k.attributes,
+      provider: k.provider_section,
+    });
+    prev.push({
+      type: "kyc-expiration",
+      title: "expired" as TranslatedString,
+      when: {
+        t_ms:
+          k.expiration_time.t_s === "never"
+            ? "never"
+            : k.expiration_time.t_s * 1000,
+      },
+      fields: !k.attributes ? [] : Object.keys(k.attributes),
+    });
+    return prev;
+  }, [] as AmlEvent[]);
+  return ae.concat(ke).sort(selectSooner);
+}
+
+export function AccountDetails({ 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/exchange-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx 
b/packages/exchange-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
index fc5838dd9..713c0d7c1 100644
--- a/packages/exchange-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
@@ -8,8 +8,11 @@ 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);
@@ -22,11 +25,28 @@ export function AntiMoneyLaunderingForm({ number }: { 
number?: string }) {
     when: AbsoluteTime.now(),
   };
   return (
-    <NiceForm initial={storedValue} form={showingFrom} onUpdate={() => {}} />
+    <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,
diff --git a/packages/exchange-backoffice-ui/src/pages/Cases.tsx 
b/packages/exchange-backoffice-ui/src/pages/Cases.tsx
new file mode 100644
index 000000000..1983769ed
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/Cases.tsx
@@ -0,0 +1,282 @@
+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 "./AccountDetails.js";
+import { useState } from "preact/hooks";
+
+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 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/exchange-backoffice-ui/src/pages/Info.tsx 
b/packages/exchange-backoffice-ui/src/pages/Info.tsx
deleted file mode 100644
index 661ab02a7..000000000
--- a/packages/exchange-backoffice-ui/src/pages/Info.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { h } from "preact";
-
-export function Info() {
-  return <div>Show key and wire info</div>;
-}
diff --git a/packages/exchange-backoffice-ui/src/pages/NewFormEntry.tsx 
b/packages/exchange-backoffice-ui/src/pages/NewFormEntry.tsx
new file mode 100644
index 000000000..9c143addd
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/NewFormEntry.tsx
@@ -0,0 +1,78 @@
+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 { 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: {
+      t_ms: new Date().getTime(),
+    },
+    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/exchange-backoffice-ui/src/pages/Officer.tsx 
b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
index 4d8b90228..79dd8bace 100644
--- a/packages/exchange-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
@@ -4,69 +4,60 @@ import {
   notifyInfo,
   useLocalStorage,
   useMemoryStorage,
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import { VNode, h } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import {
+  Account,
   UnwrapKeyError,
   createNewAccount,
-  createNewSessionId,
   unlockAccount,
 } from "../account.js";
 import { createNewForm } from "../handlers/forms.js";
+import { Officer, codecForOfficer } from "../Dashboard.js";
 
 export function Officer() {
   const password = useMemoryStorage("password");
-  const session = useLocalStorage("session");
-  const officer = useLocalStorage("officer");
-  const [keys, setKeys] = useState({ accountId: "", pub: "" });
+  const officer = useLocalStorage("officer", {
+    codec: codecForOfficer(),
+  });
+  const [keys, setKeys] = useState<Account>();
 
   useEffect(() => {
-    if (
-      officer.value === undefined ||
-      session.value === undefined ||
-      password.value === undefined
-    ) {
+    if (officer.value === undefined || password.value === undefined) {
       return;
     }
-    unlockAccount(session.value, officer.value, password.value)
+
+    unlockAccount(officer.value.salt, officer.value.key, password.value)
       .then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
       .catch((e) => {
         if (e instanceof UnwrapKeyError) {
           console.log(e);
         }
       });
-  }, [officer.value, session.value, password.value]);
-
-  useEffect(() => {
-    if (!session.value) {
-      session.update(createNewSessionId());
-    }
-  }, []);
-
-  const { value: sessionId } = session;
-  if (!sessionId) {
-    return <div>loading...</div>;
-  }
+  }, [officer.value, password.value]);
 
-  if (officer.value === undefined) {
+  if (
+    officer.value === undefined ||
+    !officer.value.key ||
+    !officer.value.salt
+  ) {
     return (
       <CreateAccount
-        sessionId={sessionId}
-        onNewAccount={(id) => {
-          password.reset();
-          officer.update(id);
+        onNewAccount={(salt, key, pwd) => {
+          password.update(pwd);
+          officer.update({ salt, when: { t_ms: Date.now() }, key });
         }}
       />
     );
   }
 
-  console.log("pwd", password.value);
   if (password.value === undefined) {
     return (
       <UnlockAccount
-        sessionId={sessionId}
-        accountId={officer.value}
+        salt={officer.value.salt}
+        sealedKey={officer.value.key}
         onAccountUnlocked={(pwd) => {
           password.update(pwd);
         }}
@@ -76,42 +67,59 @@ export function Officer() {
 
   return (
     <div>
-      <div>Officer</div>
-      <h1>{sessionId}</h1>
       <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
         Public key
       </h1>
-      <div>
-        <p class="mt-6 leading-8 text-gray-700 break-all">
-          -----BEGIN PUBLIC KEY-----
-          <div>{keys.pub}</div>
-          -----END PUBLIC KEY-----
-        </p>
-      </div>
-      <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
-        Private key
-      </h1>
-      <div>
-        <p class="mt-6 leading-8 text-gray-700 break-all">
-          -----BEGIN PRIVATE KEY-----
-          <div>{keys.accountId}</div>
-          -----END PRIVATE KEY-----
-        </p>
+      <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
+        <p class="mt-6 font-mono break-all">{keys?.accountId}</p>
       </div>
+      <p>
+        <a
+          href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent(
+            `I want my AML account\n\n\nPubKey: ${keys?.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={() => {
+            password.reset();
+          }}
+          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.reset();
+          }}
+          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>
   );
 }
 
 function CreateAccount({
-  sessionId,
   onNewAccount,
 }: {
-  sessionId: string;
-  onNewAccount: (accountId: string) => void;
+  onNewAccount: (salt: string, accountId: string, password: string) => void;
 }): VNode {
+  const { i18n } = useTranslationContext();
   const Form = createNewForm<{
-    email: string;
     password: string;
+    repeat: string;
   }>();
 
   return (
@@ -125,24 +133,50 @@ function CreateAccount({
       <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
         <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
           <Form.Provider
+            computeFormState={(v) => {
+              return {
+                password: {
+                  error: !v.password
+                    ? i18n.str`required`
+                    : 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) => {
-              const keys = await createNewAccount(sessionId, v.password);
-              onNewAccount(keys.accountId);
+              const keys = await createNewAccount(v.password);
+              onNewAccount(keys.salt, keys.accountId, v.password);
             }}
           >
             <div class="mb-4">
               <Form.InputLine
-                label={"Email" as TranslatedString}
-                name="email"
-                type="email"
+                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={"Password" as TranslatedString}
-                name="password"
+                label={"Repeat password" as TranslatedString}
+                name="repeat"
                 type="password"
                 required
               />
@@ -164,17 +198,15 @@ function CreateAccount({
 }
 
 function UnlockAccount({
-  sessionId,
-  accountId,
+  salt,
+  sealedKey,
   onAccountUnlocked,
 }: {
-  sessionId: string;
-  accountId: string;
+  salt: string;
+  sealedKey: string;
   onAccountUnlocked: (password: string) => void;
 }): VNode {
   const Form = createNewForm<{
-    sessionId: string;
-    accountId: string;
     password: string;
   }>();
 
@@ -182,34 +214,21 @@ function UnlockAccount({
     <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">
-          Unlock account
+          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
-            initialValue={{
-              sessionId,
-              accountId:
-                accountId.substring(0, 6) +
-                "..." +
-                accountId.substring(accountId.length - 6),
-            }}
-            computeFormState={(v) => {
-              return {
-                accountId: {
-                  disabled: true,
-                },
-                sessionId: {
-                  disabled: true,
-                },
-              };
-            }}
             onSubmit={async (v) => {
               try {
                 // test login
-                await unlockAccount(sessionId, accountId, v.password);
+                await unlockAccount(salt, sealedKey, v.password);
 
                 onAccountUnlocked(v.password ?? "");
                 notifyInfo("Account unlocked" as TranslatedString);
@@ -225,21 +244,6 @@ function UnlockAccount({
               }
             }}
           >
-            <div class="mb-4">
-              <Form.InputLine
-                label={"Session" as TranslatedString}
-                name="sessionId"
-                type="text"
-              />
-            </div>
-            <div class="mb-4">
-              <Form.InputLine
-                label={"AccountId" as TranslatedString}
-                name="accountId"
-                type="text"
-              />
-            </div>
-
             <div class="mb-4">
               <Form.InputLine
                 label={"Password" as TranslatedString}
diff --git a/packages/exchange-backoffice-ui/src/route.ts 
b/packages/exchange-backoffice-ui/src/route.ts
index ed6d8058d..d54f9be83 100644
--- a/packages/exchange-backoffice-ui/src/route.ts
+++ b/packages/exchange-backoffice-ui/src/route.ts
@@ -1,5 +1,5 @@
 import { createHashHistory } from "history";
-import { VNode } from "preact";
+import { h as create, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
 const history = createHashHistory();
 
@@ -64,7 +64,7 @@ export function Router({
 }): VNode {
   const current = useCurrentLocation(pageList);
   if (current !== undefined) {
-    return current.page.view(current.values ?? {});
+    return create(current.page.view, current.values);
   }
   return onNotFound();
 }
diff --git a/packages/exchange-backoffice-ui/src/types.ts 
b/packages/exchange-backoffice-ui/src/types.ts
new file mode 100644
index 000000000..1197b6b35
--- /dev/null
+++ b/packages/exchange-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,
+}
diff --git a/packages/web-util/src/hooks/useLang.ts 
b/packages/web-util/src/hooks/useLang.ts
index 9888cc51a..d64cf6e1a 100644
--- a/packages/web-util/src/hooks/useLang.ts
+++ b/packages/web-util/src/hooks/useLang.ts
@@ -24,6 +24,6 @@ function getBrowserLang(): string | undefined {
 }
 
 export function useLang(initial?: string): Required<LocalStorageState> {
-  const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
-  return useLocalStorage("lang-preference", defaultLang);
+  const defaultValue = (getBrowserLang() || initial || "en").substring(0, 2);
+  return useLocalStorage("lang-preference", { defaultValue: defaultValue });
 }
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts 
b/packages/web-util/src/hooks/useLocalStorage.ts
index 131825736..55efd01cb 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -19,6 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { Codec } from "@gnu-taler/taler-util";
 import { useEffect, useState } from "preact/hooks";
 import {
   ObservableMap,
@@ -27,9 +28,9 @@ import {
   memoryMap,
 } from "../utils/observable.js";
 
-export interface LocalStorageState {
-  value?: string;
-  update: (s: string) => void;
+export interface LocalStorageState<Type = string> {
+  value?: Type;
+  update: (s: Type) => void;
   reset: () => void;
 }
 
@@ -47,33 +48,62 @@ const storage: ObservableMap<string, string> = (function 
buildStorage() {
   }
 })();
 
-export function useLocalStorage(
+//with initial value
+export function useLocalStorage<Type = string>(
   key: string,
-  initialValue: string,
-): Required<LocalStorageState>;
-export function useLocalStorage(key: string): LocalStorageState;
-export function useLocalStorage(
+  options?: {
+    defaultValue: Type;
+    codec?: Codec<Type>;
+  },
+): Required<LocalStorageState<Type>>;
+//without initial value
+export function useLocalStorage<Type = string>(
   key: string,
-  initialValue?: string,
-): LocalStorageState {
-  const [storedValue, setStoredValue] = useState<string | undefined>(
-    (): string | undefined => {
-      return storage.get(key) ?? initialValue;
+  options?: {
+    codec?: Codec<Type>;
+  },
+): LocalStorageState<Type>;
+// impl
+export function useLocalStorage<Type = string>(
+  key: string,
+  options?: {
+    defaultValue?: Type;
+    codec?: Codec<Type>;
+  },
+): LocalStorageState<Type> {
+  function convert(updated: string | undefined): Type | undefined {
+    if (updated === undefined) return options?.defaultValue; //optional
+    try {
+      return !options?.codec
+        ? (updated as Type)
+        : options.codec.decode(JSON.parse(updated));
+    } catch (e) {
+      //decode error
+      return options?.defaultValue;
+    }
+  }
+  const [storedValue, setStoredValue] = useState<Type | undefined>(
+    (): Type | undefined => {
+      const prev = storage.get(key);
+      return convert(prev);
     },
   );
 
   useEffect(() => {
     return storage.onUpdate(key, () => {
       const newValue = storage.get(key);
-      setStoredValue(newValue ?? initialValue);
+      setStoredValue(convert(newValue));
     });
   }, []);
 
-  const setValue = (value?: string): void => {
+  const setValue = (value?: Type): void => {
     if (value === undefined) {
       storage.delete(key);
     } else {
-      storage.set(key, value);
+      storage.set(
+        key,
+        options?.codec ? JSON.stringify(value) : (value as string),
+      );
     }
   };
 
@@ -81,7 +111,7 @@ export function useLocalStorage(
     value: storedValue,
     update: setValue,
     reset: () => {
-      setValue(initialValue);
+      setValue(options?.defaultValue);
     },
   };
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]