gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (dad7d48ed -> e3d046457)


From: gnunet
Subject: [taler-wallet-core] branch master updated (dad7d48ed -> e3d046457)
Date: Fri, 26 May 2023 14:56:23 +0200

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

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

    from dad7d48ed wallet-core: fix withdrawal abort
     new 64e370566 cases, account details and new-form screen
     new 562067a28 resolution field for all forms
     new e3d046457 moving into taler-crpto

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/exchange-backoffice-ui/src/Dashboard.tsx  | 344 +++++++++-------
 packages/exchange-backoffice-ui/src/NiceForm.tsx   |  13 +-
 packages/exchange-backoffice-ui/src/account.ts     |  72 ++--
 .../src/assets/logo-2021.svg                       |   0
 .../exchange-backoffice-ui/src/declaration.d.ts    |  28 ++
 .../exchange-backoffice-ui/src/forms/902_11e.ts    |  23 +-
 .../exchange-backoffice-ui/src/forms/902_12e.ts    |  23 +-
 .../exchange-backoffice-ui/src/forms/902_13e.ts    |  23 +-
 .../exchange-backoffice-ui/src/forms/902_15e.ts    |  23 +-
 .../exchange-backoffice-ui/src/forms/902_1e.ts     |  25 +-
 .../exchange-backoffice-ui/src/forms/902_4e.ts     |  19 +-
 .../exchange-backoffice-ui/src/forms/902_5e.ts     |  17 +-
 .../exchange-backoffice-ui/src/forms/902_9e.ts     |  23 +-
 .../exchange-backoffice-ui/src/forms/simplest.ts   | 103 +++++
 .../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   |  34 +-
 .../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/taler-util/src/taler-crypto.ts            |   2 +-
 packages/web-util/src/hooks/useLang.ts             |   4 +-
 packages/web-util/src/hooks/useLocalStorage.ts     |  64 ++-
 34 files changed, 1805 insertions(+), 418 deletions(-)
 copy packages/{merchant-backoffice-ui => 
exchange-backoffice-ui}/src/assets/logo-2021.svg (100%)
 create mode 100644 packages/exchange-backoffice-ui/src/forms/simplest.ts
 create mode 100644 packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx
 create mode 100644 
packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx
 create mode 100644 packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
 create mode 100644 packages/exchange-backoffice-ui/src/pages/Cases.tsx
 delete mode 100644 packages/exchange-backoffice-ui/src/pages/Info.tsx
 create mode 100644 packages/exchange-backoffice-ui/src/pages/NewFormEntry.tsx
 create mode 100644 packages/exchange-backoffice-ui/src/types.ts

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..6c3766940 100644
--- a/packages/exchange-backoffice-ui/src/account.ts
+++ b/packages/exchange-backoffice-ui/src/account.ts
@@ -1,4 +1,12 @@
-import { decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
+import {
+  bytesToString,
+  createEddsaKeyPair,
+  decodeCrock,
+  encodeCrock,
+  encryptWithDerivedKey,
+  getRandomBytesF,
+  stringToBytes,
+} from "@gnu-taler/taler-util";
 
 /**
  * Create a new session id from which it will
@@ -7,28 +15,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 +49,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,29 +62,22 @@ export async function unlockAccount(
  * @param password
  * @returns
  */
-export async function createNewAccount(sessionId: string, password: string) {
-  const { privateKey, publicKey } = await createPair();
-
-  const protectedPrivKey = await protectWithPassword(
-    privateKey,
-    sessionId,
-    password,
-  );
+export async function createNewAccount(password: string) {
+  const { eddsaPriv } = createEddsaKeyPair();
+  const salt = createSalt();
 
-  //   const privRaw = await crypto.subtle
-  //     .exportKey("pkcs8", privateKey)
-  //     .catch((e) => {
-  //       throw new Error(String(e));
-  //     });
+  const key = stringToBytes(password);
 
-  const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => 
{
-    throw new Error(String(e));
-  });
+  const protectedPrivKey = await encryptWithDerivedKey(
+    getRandomBytesF(24),
+    key,
+    eddsaPriv,
+    salt,
+  );
 
-  const pub = btoa(ab2str(pubRaw));
-  const protectedPriv = btoa(ab2str(protectedPrivKey));
+  const protectedPriv = bytesToString(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/merchant-backoffice-ui/src/assets/logo-2021.svg 
b/packages/exchange-backoffice-ui/src/assets/logo-2021.svg
similarity index 100%
copy from packages/merchant-backoffice-ui/src/assets/logo-2021.svg
copy to packages/exchange-backoffice-ui/src/assets/logo-2021.svg
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..24df6a44c 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_11e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_11e.ts
@@ -1,8 +1,15 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_11e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -113,10 +120,11 @@ export const v1: FlexibleForm<Form902_11e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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,20 +136,19 @@ export const v1: FlexibleForm<Form902_11e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_11e {
+namespace Form902_11 {
   interface Person {
     lastName: string;
     firstName: string;
     address: string;
   }
-  export interface Form {
+  export interface Form extends Simplest.WithResolution {
     contractingPartner: string;
     declares: "25-or-more" | "controlling-in-other-ways" | "managing-director";
     person: Person[];
     fiduciaryAssets: "no" | "yes";
-    when: AbsoluteTime;
     signature: 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..c80539511 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_12e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_12e.ts
@@ -1,8 +1,15 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_12e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -362,10 +369,11 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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 +398,9 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_12e {
+namespace Form902_12 {
   interface Foundation {
     name: string;
     type: "discretionary" | "non-discretionary";
@@ -418,7 +426,7 @@ namespace Form902_12e {
   type Founder = WithRevoke<WithDeath<Person>>;
   type Beneficiary = WithClaim<Person>;
 
-  export interface Form {
+  export interface Form extends Simplest.WithResolution {
     contractingPartner: string;
     knownAs: string;
     boardMember: string;
@@ -428,6 +436,5 @@ namespace Form902_12e {
     beneficiaryWhenSigning: Array<Beneficiary>;
     beneficiaryExtra: Array<Beneficiary>;
     withRightToNominate: Array<WithRevoke<Person>>;
-    when: AbsoluteTime;
   }
 }
diff --git a/packages/exchange-backoffice-ui/src/forms/902_13e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_13e.ts
index bca96e842..63870f00a 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_13e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_13e.ts
@@ -1,8 +1,15 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_13e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -439,10 +446,11 @@ export const v1: FlexibleForm<Form902_13e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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 +484,9 @@ export const v1: FlexibleForm<Form902_13e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_13e {
+namespace Form902_13 {
   interface Foundation {
     name: string;
     type: "discretionary" | "non-discretionary";
@@ -504,7 +512,7 @@ namespace Form902_13e {
   type Founder = WithRevoke<WithDeath<Person>>;
   type Beneficiary = WithClaim<Person>;
 
-  export interface Form {
+  export interface Form extends Simplest.WithResolution {
     contractingPartner: string;
     knownAs: string;
     boardMember: string;
@@ -515,6 +523,5 @@ namespace Form902_13e {
     beneficiaryExtra: Array<Beneficiary>;
     protectors: Array<WithRevoke<Person>>;
     furtherPersons: Array<WithRevoke<Person>>;
-    when: AbsoluteTime;
   }
 }
diff --git a/packages/exchange-backoffice-ui/src/forms/902_15e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_15e.ts
index 8e3fa1350..19a16d3f2 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_15e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_15e.ts
@@ -1,8 +1,15 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_15e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -158,19 +165,20 @@ export const v1: FlexibleForm<Form902_15e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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;
@@ -178,13 +186,12 @@ namespace Form902_15e {
     nationality: string;
   }
 
-  export interface Form {
+  export interface Form extends Simplest.WithResolution {
     contractingPartner: string;
     contractualRelationship: string;
     insurancePolicy: string;
     holder: Person;
     premiumsPayer: Person;
-    when: AbsoluteTime;
     signature: 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..04952a985 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
@@ -1,8 +1,17 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  Amounts,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FlexibleForm, languageList } from "./index.js";
 import { FormState } from "../handlers/FormProvider.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { amlStateConverter } from "../pages/AccountDetails.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_1e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -510,10 +519,11 @@ export const v1: FlexibleForm<Form902_1e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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 +616,9 @@ export const v1: FlexibleForm<Form902_1e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_1e {
+namespace Form902_1 {
   interface LegalEntityCustomer {
     companyName: string;
     domicile: string;
@@ -670,8 +680,7 @@ namespace Form902_1e {
     purpose: string;
   }
 
-  export interface Form {
-    when: AbsoluteTime;
+  export interface Form extends Simplest.WithResolution {
     fullName: string;
     customerType: "natural" | "legal";
     naturalCustomer: NaturalCustomer;
diff --git a/packages/exchange-backoffice-ui/src/forms/902_4e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
index ca7ef8505..15ad17144 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
@@ -1,11 +1,20 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  Amounts,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
 import { ArrowRightIcon } from "@heroicons/react/24/outline";
 import { h as create } from "preact";
 import { ChevronRightIcon } from "@heroicons/react/24/solid";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { amlStateConverter } from "../pages/AccountDetails.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_4.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -735,6 +744,7 @@ export const v1: FlexibleForm<Form902_4.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   behavior: function formBehavior(
     v: Partial<Form902_4.Form>,
@@ -745,13 +755,12 @@ export const v1: FlexibleForm<Form902_4.Form> = {
       },
     };
   },
-};
+});
 
 namespace Form902_4 {
-  export interface Form {
+  export interface Form extends Simplest.WithResolution {
     customer: string;
     fullName: string;
-    when: AbsoluteTime;
     pep: {
       foreign: "yes" | "no";
       domestic: "yes" | "no" | "yes-but-no-risk";
diff --git a/packages/exchange-backoffice-ui/src/forms/902_5e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_5e.ts
index 60bd551d5..bd27b7a7f 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_5e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_5e.ts
@@ -5,8 +5,11 @@ import {
 } from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm, currencyList } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_12e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -228,10 +231,11 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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,13 +247,12 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
       },
     };
   },
-};
+});
 
-namespace Form902_12e {
-  export interface Form {
+namespace Form902_5 {
+  export interface Form extends Simplest.WithResolution {
     customer: string;
     fullName: string;
-    when: AbsoluteTime;
     businessActivity: string;
     financial: string;
     originOfAssets: {
diff --git a/packages/exchange-backoffice-ui/src/forms/902_9e.ts 
b/packages/exchange-backoffice-ui/src/forms/902_9e.ts
index 6d88f8578..e79597bfb 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_9e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_9e.ts
@@ -1,8 +1,15 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  AmountJson,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { FormState } from "../handlers/FormProvider.js";
 import { FlexibleForm } from "./index.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { AmlState } from "../types.js";
+import { Simplest, resolutionSection } from "./simplest.js";
 
-export const v1: FlexibleForm<Form902_9e.Form> = {
+export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
   versionId: "2023-05-15",
   design: [
     {
@@ -102,19 +109,20 @@ export const v1: FlexibleForm<Form902_9e.Form> = {
         },
       ],
     },
+    resolutionSection(current),
   ],
   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;
@@ -122,10 +130,9 @@ namespace Form902_9e {
     nationality: string;
     address: string;
   }
-  export interface Form {
+  export interface Form extends Simplest.WithResolution {
     contractingPartner: string;
     persons: Person;
-    when: AbsoluteTime;
     signature: 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..5da01961b
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/forms/simplest.ts
@@ -0,0 +1,103 @@
+import {
+  AbsoluteTime,
+  AmountJson,
+  Amounts,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
+import { FormState } from "../handlers/FormProvider.js";
+import { FlexibleForm } from "./index.js";
+import { AmlState } from "../types.js";
+import { amlStateConverter } from "../pages/AccountDetails.js";
+import { State } from "../pages/AntiMoneyLaunderingForm.js";
+import { DoubleColumnFormSection, UIFormField } from "../handlers/forms.js";
+
+export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
+  versionId: "2023-05-25",
+  design: [
+    {
+      title: "Simple form" as TranslatedString,
+      fields: [
+        {
+          type: "textArea",
+          props: {
+            name: "comment",
+            label: "Comments" as TranslatedString,
+          },
+        },
+      ],
+    },
+    resolutionSection(current),
+  ],
+  behavior: function formBehavior(
+    v: Partial<Simplest.Form>,
+  ): FormState<Simplest.Form> {
+    return {
+      when: {
+        disabled: true,
+      },
+      threshold: {
+        disabled: v.state === AmlState.frozen,
+      },
+    };
+  },
+});
+
+export namespace Simplest {
+  export interface WithResolution {
+    when: AbsoluteTime;
+    threshold: AmountJson;
+    state: AmlState;
+  }
+  export interface Form extends WithResolution {
+    comment: string;
+  }
+}
+
+export function resolutionSection(current: State): DoubleColumnFormSection {
+  return {
+    title: "Resolution" as TranslatedString,
+    description: `Current state is ${amlStateConverter.toStringUI(
+      current.state,
+    )} and threshold at ${Amounts.stringifyValue(
+      current.threshold,
+    )}` as TranslatedString,
+    fields: [
+      {
+        type: "date",
+        props: {
+          name: "when",
+          label: "Decision Time" as TranslatedString,
+        },
+      },
+      {
+        type: "choiceHorizontal",
+        props: {
+          name: "state",
+          label: "New state" as TranslatedString,
+          converter: amlStateConverter,
+          choices: [
+            {
+              value: AmlState.frozen,
+              label: "Frozen" as TranslatedString,
+            },
+            {
+              value: AmlState.pending,
+              label: "Pending" as TranslatedString,
+            },
+            {
+              value: AmlState.normal,
+              label: "Normal" as TranslatedString,
+            },
+          ],
+        },
+      },
+      {
+        type: "amount",
+        props: {
+          name: "threshold",
+          label: "New threshold" as TranslatedString,
+        },
+      },
+    ],
+  };
+}
diff --git a/packages/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..2c90a69ed 100644
--- a/packages/exchange-backoffice-ui/src/handlers/forms.ts
+++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts
@@ -13,10 +13,12 @@ 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 = {
+export type DoubleColumnFormSection = {
   title: TranslatedString;
   description?: TranslatedString;
   fields: UIFormField[];
@@ -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/taler-util/src/taler-crypto.ts 
b/packages/taler-util/src/taler-crypto.ts
index 3cd482bfa..6fc6d14f6 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -1393,7 +1393,7 @@ async function deriveKey(
   });
 }
 
-async function encryptWithDerivedKey(
+export async function encryptWithDerivedKey(
   nonce: EncryptionNonce,
   keySeed: OpaqueData,
   plaintext: OpaqueData,
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]