gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (50442d335 -> 22709ff4e)


From: gnunet
Subject: [taler-wallet-core] branch master updated (50442d335 -> 22709ff4e)
Date: Mon, 29 Apr 2024 22:23:10 +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 50442d335 allow field by props
     new eeabe64b3 sync field ui
     new 22709ff4e use exchange api type and start using ui_fields

The 2 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:
 .../aml-backoffice-ui/src/forms/declaration.ts     |   5 +-
 packages/aml-backoffice-ui/src/forms/simplest.ts   |  16 +-
 packages/aml-backoffice-ui/src/hooks/form.ts       |  46 +++--
 packages/aml-backoffice-ui/src/hooks/officer.ts    |  12 +-
 packages/aml-backoffice-ui/src/hooks/useCases.ts   |   6 +-
 .../src/pages/AntiMoneyLaunderingForm.stories.tsx  | 104 ------------
 .../src/pages/AntiMoneyLaunderingForm.tsx          | 185 ---------------------
 .../aml-backoffice-ui/src/pages/CaseDetails.tsx    | 107 ++++++++++--
 .../aml-backoffice-ui/src/pages/CaseUpdate.tsx     | 180 +++++++++++++-------
 .../aml-backoffice-ui/src/pages/Cases.stories.tsx  |   7 +-
 packages/aml-backoffice-ui/src/pages/Cases.tsx     | 125 ++++++++------
 .../aml-backoffice-ui/src/pages/CreateAccount.tsx  |  99 ++++-------
 .../src/pages/HandleAccountNotReady.tsx            |  19 +--
 .../src/pages/ShowConsolidated.stories.tsx         |  14 +-
 .../src/pages/ShowConsolidated.tsx                 |  14 +-
 .../aml-backoffice-ui/src/pages/UnlockAccount.tsx  | 145 ++++++++++------
 .../aml-backoffice-ui/src/pages/index.stories.ts   |   1 -
 packages/aml-backoffice-ui/src/utils/converter.ts  |  34 +++-
 packages/aml-backoffice-ui/src/utils/types.ts      | 124 --------------
 packages/web-util/src/forms/DefaultForm.tsx        |   4 +-
 packages/web-util/src/forms/FormProvider.tsx       |   3 +-
 packages/web-util/src/forms/InputLine.tsx          |  31 ++--
 packages/web-util/src/forms/forms.ts               |   2 -
 packages/web-util/src/forms/useField.ts            |  10 +-
 24 files changed, 527 insertions(+), 766 deletions(-)
 delete mode 100644 
packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
 delete mode 100644 
packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
 delete mode 100644 packages/aml-backoffice-ui/src/utils/types.ts

diff --git a/packages/aml-backoffice-ui/src/forms/declaration.ts 
b/packages/aml-backoffice-ui/src/forms/declaration.ts
index fb7b8f334..c467f537b 100644
--- a/packages/aml-backoffice-ui/src/forms/declaration.ts
+++ b/packages/aml-backoffice-ui/src/forms/declaration.ts
@@ -14,12 +14,11 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import type { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import type { AmountJson, TalerExchangeApi, TranslatedString } from 
"@gnu-taler/taler-util";
 import type {
   FlexibleForm,
   InternationalizationAPI,
 } from "@gnu-taler/web-util/browser";
-import { AmlExchangeBackend } from "../utils/types.js";
 
 /**
  * import entry point without hard reference.
@@ -32,7 +31,7 @@ import { AmlExchangeBackend } from "../utils/types.js";
  */
 
 export interface BaseForm {
-  state: AmlExchangeBackend.AmlState;
+  state: TalerExchangeApi.AmlState;
   threshold: AmountJson;
 }
 
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts 
b/packages/aml-backoffice-ui/src/forms/simplest.ts
index bd512546d..6455b6f41 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -13,13 +13,13 @@
  You should have received a copy of the GNU General Public License along with
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
-import type {
-  TranslatedString
+import {
+  TalerExchangeApi,
+  type TranslatedString
 } from "@gnu-taler/taler-util";
 
 import type { DoubleColumnFormSection, FlexibleForm, FormState, 
InternationalizationAPI } from "@gnu-taler/web-util/browser";
 import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
 import { BaseForm } from "./declaration.js";
 
 export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): 
FlexibleForm<Simplest.Form> => ({
@@ -44,10 +44,9 @@ export const v1 = (i18n: InternationalizationAPI) => 
(current: BaseForm): Flexib
     return {
       comment: {
         help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as 
TranslatedString,
-
       },
       threshold: {
-        disabled: v.state === AmlExchangeBackend.AmlState.frozen,
+        disabled: v.state === TalerExchangeApi.AmlState.frozen,
       },
     };
   },
@@ -71,18 +70,17 @@ export function resolutionSection(current: BaseForm, i18n: 
InternationalizationA
         props: {
           name: "state",
           label: i18n.str`New state`,
-          converter: amlStateConverter,
           choices: [
             {
-              value: AmlExchangeBackend.AmlState.frozen,
+              value: TalerExchangeApi.AmlState.frozen,
               label: i18n.str`Frozen`,
             },
             {
-              value: AmlExchangeBackend.AmlState.pending,
+              value: TalerExchangeApi.AmlState.pending,
               label: i18n.str`Pending`,
             },
             {
-              value: AmlExchangeBackend.AmlState.normal,
+              value: TalerExchangeApi.AmlState.normal,
               label: i18n.str`Normal`,
             },
           ],
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts 
b/packages/aml-backoffice-ui/src/hooks/form.ts
index e3d97db8c..e14e29819 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -14,29 +14,32 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AmountJson,
+  TalerExchangeApi,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { useState } from "preact/hooks";
+import { UIField } from "@gnu-taler/web-util/browser";
 
-export type UIField = {
-  value: string | undefined;
-  onUpdate: (s: string) => void;
-  error: TranslatedString | undefined;
-};
+// export type UIField = {
+//   value: string | undefined;
+//   onUpdate: (s: string) => void;
+//   error: TranslatedString | undefined;
+// };
 
 type FormHandler<T> = {
   [k in keyof T]?: T[k] extends string
     ? UIField
     : T[k] extends AmountJson
       ? UIField
-      : FormHandler<T[k]>;
+      : T[k] extends TalerExchangeApi.AmlState
+        ? UIField
+        : FormHandler<T[k]>;
 };
 
 export type FormValues<T> = {
-  [k in keyof T]: T[k] extends string
-    ? string | undefined
-    : T[k] extends AmountJson
-      ? string | undefined
-      : FormValues<T[k]>;
+  [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
 };
 
 export type RecursivePartial<T> = {
@@ -44,7 +47,9 @@ export type RecursivePartial<T> = {
     ? string
     : T[k] extends AmountJson
       ? AmountJson
-      : RecursivePartial<T[k]>;
+      : T[k] extends TalerExchangeApi.AmlState
+        ? TalerExchangeApi.AmlState
+        : RecursivePartial<T[k]>;
 };
 
 export type FormErrors<T> = {
@@ -52,7 +57,9 @@ export type FormErrors<T> = {
     ? TranslatedString
     : T[k] extends AmountJson
       ? TranslatedString
-      : FormErrors<T[k]>;
+      : T[k] extends TalerExchangeApi.AmlState
+        ? TranslatedString
+        : FormErrors<T[k]>;
 };
 
 export type FormStatus<T> =
@@ -76,10 +83,15 @@ function constructFormHandler<T>(
 
   const handler = keys.reduce((prev, fieldName) => {
     const currentValue: unknown = form[fieldName];
-    const currentError: unknown = errors ? errors[fieldName] : undefined;
+    const currentError: unknown =
+      errors !== undefined ? errors[fieldName] : undefined;
     function updater(newValue: unknown) {
       updateForm({ ...form, [fieldName]: newValue });
     }
+    /**
+     * There is no clear way to know if this object is a custom field
+     * or a group of fields
+     */
     if (typeof currentValue === "object") {
       // @ts-expect-error FIXME better typing
       const group = constructFormHandler(currentValue, updater, currentError);
@@ -87,12 +99,14 @@ function constructFormHandler<T>(
       prev[fieldName] = group;
       return prev;
     }
+
     const field: UIField = {
       // @ts-expect-error FIXME better typing
       error: currentError,
       // @ts-expect-error FIXME better typing
       value: currentValue,
-      onUpdate: updater,
+      onChange: updater,
+      state: {},
     };
     // @ts-expect-error FIXME better typing
     prev[fieldName] = field;
diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts 
b/packages/aml-backoffice-ui/src/hooks/officer.ts
index dabe866d3..1bb73b8fc 100644
--- a/packages/aml-backoffice-ui/src/hooks/officer.ts
+++ b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -66,14 +66,14 @@ interface OfficerNotFound {
 }
 interface OfficerLocked {
   state: "locked";
-  forget: () => void;
-  tryUnlock: (password: string) => Promise<void>;
+  forget: () => OperationOk<void>;
+  tryUnlock: (password: string) => Promise<OperationOk<void>>;
 }
 interface OfficerReady {
   state: "ready";
   account: OfficerAccount;
-  forget: () => void;
-  lock: () => void;
+  forget: () => OperationOk<void>;
+  lock: () => OperationOk<void>;
 }
 
 const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
@@ -133,6 +133,7 @@ export function useOfficer(): OfficerState {
       state: "locked",
       forget: () => {
         officerStorage.reset();
+        return opFixedSuccess(undefined)
       },
       tryUnlock: async (pwd: string) => {
         const ac = await unlockOfficerAccount(officer.account, pwd);
@@ -141,6 +142,7 @@ export function useOfficer(): OfficerState {
           id: ac.id,
           strKey: encodeCrock(ac.signingKey),
         });
+        return opFixedSuccess(undefined)
       },
     };
   }
@@ -150,10 +152,12 @@ export function useOfficer(): OfficerState {
     account,
     lock: () => {
       accountStorage.reset();
+      return opFixedSuccess(undefined)
     },
     forget: () => {
       officerStorage.reset();
       accountStorage.reset();
+      return opFixedSuccess(undefined)
     },
   };
 }
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts 
b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index 59d1c9001..d3a1c1018 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -19,11 +19,11 @@ import { useState } from "preact/hooks";
 import {
   OfficerAccount,
   OperationOk,
+  TalerExchangeApi,
   TalerExchangeResultByMethod,
   TalerHttpError,
 } from "@gnu-taler/taler-util";
 import _useSWR, { SWRHook } from "swr";
-import { AmlExchangeBackend } from "../utils/types.js";
 import { useOfficer } from "./officer.js";
 import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
 const useSWR = _useSWR as unknown as SWRHook;
@@ -39,7 +39,7 @@ export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
  * @param args
  * @returns
  */
-export function useCases(state: AmlExchangeBackend.AmlState) {
+export function useCases(state: TalerExchangeApi.AmlState) {
   const officer = useOfficer();
   const session = officer.state === "ready" ? officer.account : undefined;
   const {
@@ -50,7 +50,7 @@ export function useCases(state: AmlExchangeBackend.AmlState) {
 
   async function fetcher([officer, state, offset]: [
     OfficerAccount,
-    AmlExchangeBackend.AmlState,
+    TalerExchangeApi.AmlState,
     string | undefined,
   ]) {
     return await api.getDecisionsByState(officer, state, {
diff --git 
a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx 
b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
deleted file mode 100644
index 0c82a4a0e..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import {
-  AntiMoneyLaunderingForm as TestedComponent,
-} from "./AntiMoneyLaunderingForm.js";
-
-export default {
-  title: "aml form",
-};
-
-export const SimpleComment = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "simple_comment",
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-
-export const Identification = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.1e",
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-
-export const OperationalLegalEntity = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.11e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-export const Foundations = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.12e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-export const DelcarationOfTrusts = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.13e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-
-export const InformationOnLifeInsurance = tests.createExample(TestedComponent, 
{
-  account: "the_account",
-  formId: "902.15e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-export const DeclarationOfBeneficialOwner = 
tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.9e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-export const CustomerProfile = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.5e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-export const RiskProfile = tests.createExample(TestedComponent, {
-  account: "the_account",
-  formId: "902.4e",
-
-  onSubmit: async (justification, newState, newThreshold) => {
-    alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 
2))
-  }
-});
-
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx 
b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
deleted file mode 100644
index db034c996..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-import {
-  AbsoluteTime,
-  AmountJson,
-  Amounts,
-  Codec,
-  OperationFail,
-  OperationOk,
-  TalerErrorDetail,
-  buildCodecForObject,
-  codecForNumber,
-  codecForString,
-  codecOptional,
-} from "@gnu-taler/taler-util";
-import {
-  DefaultForm,
-  useExchangeApiContext,
-  useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { h } from "preact";
-import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-import { privatePages } from "../Routing.js";
-
-export function AntiMoneyLaunderingForm({
-  account,
-  formId,
-  onSubmit,
-}: {
-  account: string;
-  formId: string;
-  onSubmit: (
-    justification: Justification,
-    state: AmlExchangeBackend.AmlState,
-    threshold: AmountJson,
-  ) => Promise<void>;
-}) {
-  const { i18n } = useTranslationContext();
-  const theForm = uiForms.forms(i18n).find((v) => v.id === formId);
-  if (!theForm) {
-    return <div>form with id {formId} not found</div>;
-  }
-
-  const { config } = useExchangeApiContext();
-
-  const initial = {
-    when: AbsoluteTime.now(),
-    state: AmlExchangeBackend.AmlState.pending,
-    threshold: Amounts.zeroOfCurrency(config.currency),
-  };
-  return (
-    <DefaultForm
-      initial={initial}
-      form={theForm.impl(initial)}
-      onUpdate={() => {}}
-      onSubmit={(formValue) => {
-        if (
-          formValue.state === undefined ||
-          formValue.threshold === undefined
-        ) {
-          return;
-        }
-        const validatedForm = formValue as BaseForm;
-        const st = formValue.state;
-        const amount = formValue.threshold;
-
-        const justification: Justification = {
-          id: theForm.id,
-          label: theForm.label,
-          version: theForm.version,
-          value: validatedForm,
-        };
-
-        onSubmit(justification, st, amount);
-      }}
-    >
-      <div class="mt-6 flex items-center justify-end gap-x-6">
-        <a
-          href={privatePages.caseDetails.url({ cid: account })}
-          class="text-sm font-semibold leading-6 text-gray-900"
-        >
-          <i18n.Translate>Cancel</i18n.Translate>
-        </a>
-        <button
-          type="submit"
-          class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
text-white shadow-sm hover:bg-indigo-500 focus-visible:outline 
focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-indigo-600"
-        >
-          <i18n.Translate>Confirm</i18n.Translate>
-        </button>
-      </div>
-    </DefaultForm>
-  );
-}
-
-export type Justification<T extends BaseForm = BaseForm> = {
-  // form values
-  value: T;
-} & Omit<Omit<FormMetadata<BaseForm>, "icon">, "impl">;
-
-export function stringifyJustification(j: Justification): string {
-  return JSON.stringify(j);
-}
-
-type SimpleFormMetadata = {
-  version?: number;
-  id?: string;
-};
-
-export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
-  buildCodecForObject<SimpleFormMetadata>()
-    .property("id", codecOptional(codecForString()))
-    .property("version", codecOptional(codecForNumber()))
-    .build("SimpleFormMetadata");
-
-type ParseJustificationFail =
-  | "not-json"
-  | "id-not-found"
-  | "form-not-found"
-  | "version-not-found";
-
-export function parseJustification(
-  s: string,
-  listOfAllKnownForms: FormMetadata<BaseForm>[],
-):
-  | OperationOk<{
-      justification: Justification;
-      metadata: FormMetadata<BaseForm>;
-    }>
-  | OperationFail<ParseJustificationFail> {
-  try {
-    const justification = JSON.parse(s);
-    const info = codecForSimpleFormMetadata().decode(justification);
-    if (!info.id) {
-      return {
-        type: "fail",
-        case: "id-not-found",
-        detail: {} as TalerErrorDetail,
-      };
-    }
-    if (!info.version) {
-      return {
-        type: "fail",
-        case: "version-not-found",
-        detail: {} as TalerErrorDetail,
-      };
-    }
-    const found = listOfAllKnownForms.find((f) => {
-      return f.id === info.id && f.version === info.version;
-    });
-    if (!found) {
-      return {
-        type: "fail",
-        case: "form-not-found",
-        detail: {} as TalerErrorDetail,
-      };
-    }
-    return {
-      type: "ok",
-      body: {
-        justification,
-        metadata: found,
-      },
-    };
-  } catch (e) {
-    return {
-      type: "fail",
-      case: "not-json",
-      detail: {} as TalerErrorDetail,
-    };
-  }
-}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx 
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 576cdbbb9..e16a6a103 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -17,10 +17,19 @@ import {
   AbsoluteTime,
   AmountJson,
   Amounts,
+  Codec,
   HttpStatusCode,
+  OperationFail,
+  OperationOk,
   TalerError,
+  TalerErrorDetail,
+  TalerExchangeApi,
   TranslatedString,
   assertUnreachable,
+  buildCodecForObject,
+  codecForNumber,
+  codecForString,
+  codecOptional,
 } from "@gnu-taler/taler-util";
 import {
   DefaultForm,
@@ -32,15 +41,10 @@ import {
 import { format } from "date-fns";
 import { VNode, h } from "preact";
 import { useState } from "preact/hooks";
+import { privatePages } from "../Routing.js";
 import { BaseForm, FormMetadata, uiForms } from "../forms/declaration.js";
 import { useCaseDetails } from "../hooks/useCaseDetails.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-import {
-  Justification,
-  parseJustification,
-} from "./AntiMoneyLaunderingForm.js";
 import { ShowConsolidated } from "./ShowConsolidated.js";
-import { privatePages } from "../Routing.js";
 
 export type AmlEvent =
   | AmlFormEvent
@@ -53,7 +57,7 @@ type AmlFormEvent = {
   title: TranslatedString;
   justification: Justification;
   metadata: FormMetadata<BaseForm>;
-  state: AmlExchangeBackend.AmlState;
+  state: TalerExchangeApi.AmlState;
   threshold: AmountJson;
 };
 type AmlFormEventError = {
@@ -62,7 +66,7 @@ type AmlFormEventError = {
   title: TranslatedString;
   justification: undefined;
   metadata: undefined;
-  state: AmlExchangeBackend.AmlState;
+  state: TalerExchangeApi.AmlState;
   threshold: AmountJson;
 };
 type KycCollectionEvent = {
@@ -108,8 +112,8 @@ function titleForJustification(
 }
 
 export function getEventsFromAmlHistory(
-  aml: AmlExchangeBackend.AmlDecisionDetail[],
-  kyc: AmlExchangeBackend.KycDetail[],
+  aml: TalerExchangeApi.AmlDecisionDetail[],
+  kyc: TalerExchangeApi.KycDetail[],
   i18n: InternationalizationAPI,
 ): AmlEvent[] {
   const ae: AmlEvent[] = aml.map((a) => {
@@ -242,24 +246,24 @@ export function CaseDetails({ account }: { account: 
string }) {
 function AmlStateBadge({
   state,
 }: {
-  state: AmlExchangeBackend.AmlState;
+  state: TalerExchangeApi.AmlState;
 }): VNode {
   switch (state) {
-    case AmlExchangeBackend.AmlState.normal: {
+    case TalerExchangeApi.AmlState.normal: {
       return (
         <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 
text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
           Normal
         </span>
       );
     }
-    case AmlExchangeBackend.AmlState.pending: {
+    case TalerExchangeApi.AmlState.pending: {
       return (
         <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 
py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
           Pending
         </span>
       );
     }
-    case AmlExchangeBackend.AmlState.frozen: {
+    case TalerExchangeApi.AmlState.frozen: {
       return (
         <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 
text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
           Frozen
@@ -384,3 +388,78 @@ function ShowTimeline({
     </div>
   );
 }
+
+
+export type Justification<T extends BaseForm = BaseForm> = {
+  // form values
+  value: T;
+} & Omit<Omit<FormMetadata<BaseForm>, "icon">, "impl">;
+
+type SimpleFormMetadata = {
+  version?: number;
+  id?: string;
+};
+
+export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
+  buildCodecForObject<SimpleFormMetadata>()
+    .property("id", codecOptional(codecForString()))
+    .property("version", codecOptional(codecForNumber()))
+    .build("SimpleFormMetadata");
+
+type ParseJustificationFail =
+  | "not-json"
+  | "id-not-found"
+  | "form-not-found"
+  | "version-not-found";
+
+function parseJustification(
+  s: string,
+  listOfAllKnownForms: FormMetadata<BaseForm>[],
+):
+  | OperationOk<{
+      justification: Justification;
+      metadata: FormMetadata<BaseForm>;
+    }>
+  | OperationFail<ParseJustificationFail> {
+  try {
+    const justification = JSON.parse(s);
+    const info = codecForSimpleFormMetadata().decode(justification);
+    if (!info.id) {
+      return {
+        type: "fail",
+        case: "id-not-found",
+        detail: {} as TalerErrorDetail,
+      };
+    }
+    if (!info.version) {
+      return {
+        type: "fail",
+        case: "version-not-found",
+        detail: {} as TalerErrorDetail,
+      };
+    }
+    const found = listOfAllKnownForms.find((f) => {
+      return f.id === info.id && f.version === info.version;
+    });
+    if (!found) {
+      return {
+        type: "fail",
+        case: "form-not-found",
+        detail: {} as TalerErrorDetail,
+      };
+    }
+    return {
+      type: "ok",
+      body: {
+        justification,
+        metadata: found,
+      },
+    };
+  } catch (e) {
+    return {
+      type: "fail",
+      case: "not-json",
+      detail: {} as TalerErrorDetail,
+    };
+  }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx 
b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index c4bff1f9f..47c8f8ab4 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -19,24 +19,27 @@ import {
   HttpStatusCode,
   TalerExchangeApi,
   TalerProtocolTimestamp,
-  TranslatedString,
+  assertUnreachable
 } from "@gnu-taler/taler-util";
 import {
+  Button,
   LocalNotificationBanner,
+  RenderAllFieldsByUiConfig,
   useExchangeApiContext,
-  useLocalNotification,
-  useTranslationContext,
+  useLocalNotificationHandler,
+  useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { privatePages } from "../Routing.js";
-import { uiForms } from "../forms/declaration.js";
+import { BaseForm, uiForms } from "../forms/declaration.js";
+import { useFormState } from "../hooks/form.js";
 import { useOfficer } from "../hooks/officer.js";
-import { AntiMoneyLaunderingForm } from "./AntiMoneyLaunderingForm.js";
 import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { Justification } from "./CaseDetails.js";
 
 export function CaseUpdate({
   account,
-  type,
+  type: formId,
 }: {
   account: string;
   type: string;
@@ -46,67 +49,132 @@ export function CaseUpdate({
   const {
     lib: { exchange: api },
   } = useExchangeApiContext();
-  const [notification, notify, handleError] = useLocalNotification();
+
+  // const [notification, notify, handleError] = useLocalNotification();
+  const [notification, withErrorHandler] = useLocalNotificationHandler();
+  const { config } = useExchangeApiContext();
+
+  const initial = {
+    when: AbsoluteTime.now(),
+    state: TalerExchangeApi.AmlState.pending,
+    threshold: Amounts.zeroOfCurrency(config.currency),
+  };
 
   if (officer.state !== "ready") {
     return <HandleAccountNotReady officer={officer} />;
   }
+  const theForm = uiForms.forms(i18n).find((v) => v.id === formId);
+  if (!theForm) {
+    return <div>form with id {formId} not found</div>;
+  }
 
-  return (
-    <Fragment>
-      <LocalNotificationBanner notification={notification} />
+  const [form, state] = useFormState<BaseForm>(initial, (st) => {
+    return {
+      status: "ok",
+      result: st as any,
+      errors: undefined,
+    };
+  });
 
-      <AntiMoneyLaunderingForm
-        account={account}
-        formId={type}
-        onSubmit={async (justification, new_state, new_threshold) => {
-          const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = {
-            justification: JSON.stringify(justification),
-            decision_time: TalerProtocolTimestamp.now(),
-            h_payto: account,
-            new_state,
-            new_threshold: Amounts.stringify(new_threshold),
-            kyc_requirements: undefined,
-          };
-          await handleError(async () => {
-            const resp = await api.addDecisionDetails(
-              officer.account,
-              decision,
-            );
-            if (resp.type === "ok") {
-              window.location.href = privatePages.cases.url({});
-              return;
-            }
-            switch (resp.case) {
+  const ff = theForm.impl(state.result as any);
+
+  const validatedForm = state.status === "fail" ? undefined : state.result;
+
+  const submitHandler =
+    validatedForm === undefined
+      ? undefined
+      : withErrorHandler(
+          () => {
+            const justification: Justification = {
+              id: theForm.id,
+              label: theForm.label,
+              version: theForm.version,
+              value: validatedForm,
+            };
+
+            const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> =
+              {
+                justification: JSON.stringify(justification),
+                decision_time: TalerProtocolTimestamp.now(),
+                h_payto: account,
+                new_state: justification.value.state,
+                new_threshold: 
Amounts.stringify(justification.value.threshold),
+                kyc_requirements: undefined,
+              };
+
+            return api.addDecisionDetails(officer.account, decision);
+          },
+          () => {
+            window.location.href = privatePages.cases.url({});
+          },
+          (fail) => {
+            switch (fail.case) {
               case HttpStatusCode.Forbidden:
               case HttpStatusCode.Unauthorized:
-                return notify({
-                  type: "error",
-                  title: i18n.str`Wrong credentials for "${officer.account}"`,
-                  description: resp.detail.hint as TranslatedString,
-                  debug: resp.detail,
-                  when: AbsoluteTime.now(),
-                });
+                return i18n.str`Wrong credentials for "${officer.account}"`;
               case HttpStatusCode.NotFound:
-                return notify({
-                  type: "error",
-                  title: i18n.str`Officer or account not found`,
-                  description: resp.detail.hint as TranslatedString,
-                  debug: resp.detail,
-                  when: AbsoluteTime.now(),
-                });
+                return i18n.str`Officer or account not found`;
               case HttpStatusCode.Conflict:
-                return notify({
-                  type: "error",
-                  title: i18n.str`Officer disabled or more recent decision was 
already submitted.`,
-                  description: resp.detail.hint as TranslatedString,
-                  debug: resp.detail,
-                  when: AbsoluteTime.now(),
-                });
+                return i18n.str`Officer disabled or more recent decision was 
already submitted.`;
+              default:
+                assertUnreachable(fail);
             }
-          });
-        }}
-      />
+          },
+        );
+
+  // const asd = ff.design[0]?.fields[0]?.props
+  
+  return (
+    <Fragment>
+      <LocalNotificationBanner notification={notification} />
+      <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+        {ff.design.map((section, i) => {
+          if (!section) return <Fragment />;
+          return (
+            <div
+              key={i}
+              class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+            >
+              <div class="px-4 sm:px-0">
+                <h2 class="text-base font-semibold leading-7 text-gray-900">
+                  {section.title}
+                </h2>
+                {section.description && (
+                  <p class="mt-1 text-sm leading-6 text-gray-600">
+                    {section.description}
+                  </p>
+                )}
+              </div>
+              <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md 
md:col-span-2">
+                <div class="p-3">
+                  <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                    <RenderAllFieldsByUiConfig
+                      key={i}
+                      fields={section.fields}
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+
+      <div class="mt-6 flex items-center justify-end gap-x-6">
+        <a
+          href={privatePages.caseDetails.url({ cid: account })}
+          class="text-sm font-semibold leading-6 text-gray-900"
+        >
+          <i18n.Translate>Cancel</i18n.Translate>
+        </a>
+        <Button
+          type="submit"
+          handler={submitHandler}
+          class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
text-white shadow-sm hover:bg-indigo-500 focus-visible:outline 
focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-indigo-600"
+        >
+          <i18n.Translate>Confirm</i18n.Translate>
+        </Button>
+      </div>
     </Fragment>
   );
 }
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx 
b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index dcbd366a4..22a6d1867 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -21,19 +21,18 @@
 
 import * as tests from "@gnu-taler/web-util/testing";
 import { CasesUI as TestedComponent } from "./Cases.js";
-import { AmountString } from "@gnu-taler/taler-util";
-import { AmlExchangeBackend } from "../utils/types.js";
+import { AmountString, TalerExchangeApi } from "@gnu-taler/taler-util";
 
 export default {
   title: "cases",
 };
 
 export const OneRow = tests.createExample(TestedComponent, {
-  filter: AmlExchangeBackend.AmlState.normal,
+  filter: TalerExchangeApi.AmlState.normal,
   onChangeFilter: () => null,
   records: [
     {
-      current_state: AmlExchangeBackend.AmlState.normal,
+      current_state: TalerExchangeApi.AmlState.normal,
       h_payto: "QWEQWEQWEQWE",
       rowid: 1,
       threshold: "USD:1" as AmountString,
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx 
b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 6b59b2736..2e92c111e 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -22,19 +22,23 @@ import {
 import {
   Attention,
   ErrorLoading,
+  InputChoiceHorizontal,
   Loading,
-  createNewForm,
-  useTranslationContext,
+  useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
 import { useCases } from "../hooks/useCases.js";
 
 import { privatePages } from "../Routing.js";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
+import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
 import { Officer } from "./Officer.js";
 
+type FormType = {
+  state: TalerExchangeApi.AmlState;
+};
+
 export function CasesUI({
   records,
   filter,
@@ -44,13 +48,45 @@ export function CasesUI({
 }: {
   onFirstPage?: () => void;
   onNext?: () => void;
-  filter: AmlExchangeBackend.AmlState;
-  onChangeFilter: (f: AmlExchangeBackend.AmlState) => void;
+  filter: TalerExchangeApi.AmlState;
+  onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
   records: TalerExchangeApi.AmlRecord[];
 }): VNode {
   const { i18n } = useTranslationContext();
 
-  const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>();
+  const [form, status] = useFormState<FormType>(
+    {
+      state: filter,
+    },
+    (state) => {
+      const errors = undefinedIfEmpty<FormErrors<FormType>>({
+        state: state.state === undefined ? i18n.str`required` : undefined,
+      });
+      if (errors === undefined) {
+        const result: FormType = {
+          state: state.state!,
+        };
+        return {
+          status: "ok",
+          result,
+          errors,
+        };
+      }
+      const result: RecursivePartial<FormType> = {
+        state: state.state,
+      };
+      return {
+        status: "fail",
+        result,
+        errors,
+      };
+    },
+  );
+  useEffect(() => {
+    if (status.status === "ok" && filter !== status.result.state) {
+      onChangeFilter(status.result.state);
+    }
+  }, [form?.state?.value]);
 
   return (
     <div>
@@ -66,33 +102,25 @@ export function CasesUI({
           </p>
         </div>
         <div class="px-2">
-          <form.Provider
-            initial={{ state: filter }}
-            onUpdate={(v) => {
-              onChangeFilter(v.state ?? filter);
-            }}
-            onSubmit={(_v) => {}}
-          >
-            <form.InputChoiceHorizontal
-              name="state"
-              label={i18n.str`Filter`}
-              converter={amlStateConverter}
-              choices={[
-                {
-                  label: i18n.str`Pending`,
-                  value: AmlExchangeBackend.AmlState.pending,
-                },
-                {
-                  label: i18n.str`Frozen`,
-                  value: AmlExchangeBackend.AmlState.frozen,
-                },
-                {
-                  label: i18n.str`Normal`,
-                  value: AmlExchangeBackend.AmlState.normal,
-                },
-              ]}
-            />
-          </form.Provider>
+          <InputChoiceHorizontal<FormType, "state">
+            name="state"
+            label={i18n.str`Filter`}
+            handler={form.state}
+            choices={[
+              {
+                label: i18n.str`Pending`,
+                value: TalerExchangeApi.AmlState.pending,
+              },
+              {
+                label: i18n.str`Frozen`,
+                value: TalerExchangeApi.AmlState.frozen,
+              },
+              {
+                label: i18n.str`Normal`,
+                value: TalerExchangeApi.AmlState.normal,
+              },
+            ]}
+          />
         </div>
       </div>
       <div class="mt-8 flow-root">
@@ -141,23 +169,23 @@ export function CasesUI({
                           </div>
                         </td>
                         <td class="whitespace-nowrap px-3 py-5 text-sm 
text-gray-500">
-                          {((state: AmlExchangeBackend.AmlState): VNode => {
+                          {((state: TalerExchangeApi.AmlState): VNode => {
                             switch (state) {
-                              case AmlExchangeBackend.AmlState.normal: {
+                              case TalerExchangeApi.AmlState.normal: {
                                 return (
                                   <span class="inline-flex items-center 
rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 
ring-inset ring-green-600/20">
                                     Normal
                                   </span>
                                 );
                               }
-                              case AmlExchangeBackend.AmlState.pending: {
+                              case TalerExchangeApi.AmlState.pending: {
                                 return (
                                   <span class="inline-flex items-center 
rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 
ring-inset ring-green-600/20">
                                     Pending
                                   </span>
                                 );
                               }
-                              case AmlExchangeBackend.AmlState.frozen: {
+                              case TalerExchangeApi.AmlState.frozen: {
                                 return (
                                   <span class="inline-flex items-center 
rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 
ring-inset ring-green-600/20">
                                     Frozen
@@ -186,7 +214,7 @@ export function CasesUI({
 
 export function Cases() {
   const [stateFilter, setStateFilter] = useState(
-    AmlExchangeBackend.AmlState.pending,
+    TalerExchangeApi.AmlState.pending,
   );
 
   const list = useCases(stateFilter);
@@ -204,12 +232,10 @@ export function Cases() {
       case HttpStatusCode.Forbidden: {
         return (
           <Fragment>
-            <Attention
-              type="danger"
-              title={i18n.str`Operation denied`}
-            >
+            <Attention type="danger" title={i18n.str`Operation denied`}>
               <i18n.Translate>
-                This account doesnt have access. Request account activation 
sending your public key.
+                This account doesnt have access. Request account activation
+                sending your public key.
               </i18n.Translate>
             </Attention>
             <Officer />
@@ -219,10 +245,7 @@ export function Cases() {
       case HttpStatusCode.Unauthorized: {
         return (
           <Fragment>
-            <Attention
-              type="danger"
-              title={i18n.str`Operation denied`}
-            >
+            <Attention type="danger" title={i18n.str`Operation denied`}>
               <i18n.Translate>
                 This account is not allowed to perform list the cases.
               </i18n.Translate>
@@ -245,7 +268,9 @@ export function Cases() {
       onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
       onNext={list.isLastPage ? undefined : list.loadNext}
       filter={stateFilter}
-      onChangeFilter={setStateFilter}
+      onChangeFilter={(d) => {
+        setStateFilter(d)
+      }}
     />
   );
 }
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx 
b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 094e78531..a8a853bc1 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -15,9 +15,9 @@
  */
 import {
   Button,
+  InputLine,
   InternationalizationAPI,
   LocalNotificationBanner,
-  ShowInputErrorLabel,
   useLocalNotificationHandler,
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
@@ -66,15 +66,23 @@ function createFormValidator(
     });
 
     if (errors === undefined) {
+      const result: FormType = {
+        password: state.password!,
+        repeat: state.repeat!,
+      };
       return {
         status: "ok",
-        result: state as FormType,
+        result,
         errors,
       };
     }
+    const result: RecursivePartial<FormType> = {
+      password: state.password,
+      repeat: state.repeat,
+    };
     return {
       status: "fail",
-      result: state,
+      result,
       errors,
     };
   };
@@ -88,13 +96,8 @@ export function undefinedIfEmpty<T extends object>(obj: T): 
T | undefined {
     : undefined;
 }
 
-export function CreateAccount({
-  onNewAccount,
-}: {
-  onNewAccount: () => void;
-}): VNode {
+export function CreateAccount(): VNode {
   const { i18n } = useTranslationContext();
-  // const Form = createNewForm<FormType>();
   const [settings] = usePreferences();
   const officer = useOfficer();
 
@@ -113,9 +116,9 @@ export function CreateAccount({
       ? undefined
       : withErrorHandler(
           async () => officer.create(form.password!.value!),
-          onNewAccount,
+          () => {},
         );
-
+  form.password;
   return (
     <div class="flex min-h-full flex-col ">
       <LocalNotificationBanner notification={notification} />
@@ -137,66 +140,24 @@ export function CreateAccount({
             autoCapitalize="none"
             autoCorrect="off"
           >
-            <div>
-              <label
-                for="password"
-                class="block text-sm font-medium leading-6 text-gray-900"
-              >
-                <i18n.Translate>Password</i18n.Translate>
-              </label>
-              <div class="mt-2">
-                <input
-                  ref={doAutoFocus}
-                  type="text"
-                  name="password"
-                  id="password"
-                  class="block w-full disabled:bg-gray-200 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
-                  value={form.password?.value ?? ""}
-                  enterkeyhint="next"
-                  placeholder="strong password"
-                  autocomplete="password"
-                  title={i18n.str`Password`}
-                  required
-                  onChange={(e) => {
-                    console.log("ASDASD", form.password?.onUpdate);
-                    form.password?.onUpdate(e.currentTarget.value);
-                  }}
-                />
-                <ShowInputErrorLabel
-                  message={form.password?.error}
-                  isDirty={form.password?.value !== undefined}
-                />
-              </div>
+            <div class="mt-2">
+              <InputLine<FormType, "password">
+                label={i18n.str`Password`}
+                name="password"
+                type="password"
+                required
+                handler={form.password}
+              />
             </div>
 
-            <div>
-              <label
-                for="repeat"
-                class="block text-sm font-medium leading-6 text-gray-900"
-              >
-                <i18n.Translate>Repeat password</i18n.Translate>
-              </label>
-              <div class="mt-2">
-                <input
-                  type="text"
-                  name="repeat"
-                  id="repeat"
-                  class="block w-full disabled:bg-gray-200 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
-                  value={form.repeat?.value ?? ""}
-                  enterkeyhint="next"
-                  placeholder="identification"
-                  autocomplete="repeat"
-                  title={i18n.str`Repeat password`}
-                  required
-                  onChange={(e): void => {
-                    form.repeat?.onUpdate(e.currentTarget.value);
-                  }}
-                />
-                <ShowInputErrorLabel
-                  message={form.repeat?.error}
-                  isDirty={form.repeat?.value !== undefined}
-                />
-              </div>
+            <div class="mt-2">
+              <InputLine<FormType, "repeat">
+                label={i18n.str`Repeat password`}
+                name="repeat"
+                type="password"
+                required
+                handler={form.repeat}
+              />
             </div>
 
             <div class="mt-8">
diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx 
b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
index b23798172..3d6e14f22 100644
--- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -25,26 +25,11 @@ export function HandleAccountNotReady({
   officer: OfficerNotReady;
 }): VNode {
   if (officer.state === "not-found") {
-    return (
-      <CreateAccount
-        onNewAccount={(password) => {
-          officer.create(password);
-        }}
-      />
-    );
+    return <CreateAccount />;
   }
 
   if (officer.state === "locked") {
-    return (
-      <UnlockAccount
-        onRemoveAccount={() => {
-          officer.forget();
-        }}
-        onAccountUnlocked={async (pwd) => {
-          await officer.tryUnlock(pwd);
-        }}
-      />
-    );
+    return <UnlockAccount />;
   }
   assertUnreachable(officer);
 }
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx 
b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
index 1cb50efd2..11b25575b 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -19,7 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { AbsoluteTime, Duration, TranslatedString } from 
"@gnu-taler/taler-util";
+import { AbsoluteTime, AmountString, Duration, TranslatedString } from 
"@gnu-taler/taler-util";
 import { InternationalizationAPI } from "@gnu-taler/web-util/browser";
 import * as tests from "@gnu-taler/web-util/testing";
 import { getEventsFromAmlHistory } from "./CaseDetails.js";
@@ -48,7 +48,7 @@ export const WithSomeEvents = 
tests.createExample(TestedComponent, {
     {
       "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
       "justification": "{\"index\":0,\"name\":\"Simple 
comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
-      "new_threshold": "STATER:0",
+      "new_threshold": "STATER:0" as AmountString,
       "new_state": 1,
       "decision_time": {
         "t_s": 1700208199
@@ -57,7 +57,7 @@ export const WithSomeEvents = 
tests.createExample(TestedComponent, {
     {
       "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
       "justification": "{\"index\":0,\"name\":\"Simple 
comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
-      "new_threshold": "STATER:0",
+      "new_threshold": "STATER:0" as AmountString,
       "new_state": 1,
       "decision_time": {
         "t_s": 1700208211
@@ -66,7 +66,7 @@ export const WithSomeEvents = 
tests.createExample(TestedComponent, {
     {
       "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
       "justification": "{\"index\":0,\"name\":\"Simple 
comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
-      "new_threshold": "STATER:0",
+      "new_threshold": "STATER:0" as AmountString,
       "new_state": 1,
       "decision_time": {
         "t_s": 1700208220
@@ -75,7 +75,7 @@ export const WithSomeEvents = 
tests.createExample(TestedComponent, {
     {
       "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
       "justification": "{\"index\":4,\"name\":\"Declaration for trusts 
(902.13e)\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700208362854},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"contractingPartner\":\"f\",\"knownAs\":\"a\",\"trust\":{\"name\":\"b\",\"type\":\"discretionary\",\"revocability\":\"irrevocable\"}}}",
-      "new_threshold": "STATER:0",
+      "new_threshold": "STATER:0" as AmountString,
       "new_state": 1,
       "decision_time": {
         "t_s": 1700208385
@@ -84,7 +84,7 @@ export const WithSomeEvents = 
tests.createExample(TestedComponent, {
     {
       "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
       "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple 
comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488420810},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"qwe\"}}",
-      "new_threshold": "STATER:0",
+      "new_threshold": "STATER:0" as AmountString,
       "new_state": 1,
       "decision_time": {
         "t_s": 1700488423
@@ -93,7 +93,7 @@ export const WithSomeEvents = 
tests.createExample(TestedComponent, {
     {
       "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
       "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple 
comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488671251},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"asd
 asd asd \"}}",
-      "new_threshold": "STATER:0",
+      "new_threshold": "STATER:0" as AmountString,
       "new_state": 1,
       "decision_time": {
         "t_s": 1700488677
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx 
b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index c1f7e02cb..1115414c0 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -16,6 +16,7 @@
 import {
   AbsoluteTime,
   AmountJson,
+  TalerExchangeApi,
   TranslatedString,
 } from "@gnu-taler/taler-util";
 import {
@@ -26,8 +27,6 @@ import {
 } from "@gnu-taler/web-util/browser";
 import { format } from "date-fns";
 import { Fragment, VNode, h } from "preact";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
 import { AmlEvent } from "./CaseDetails.js";
 
 export function ShowConsolidated({
@@ -73,19 +72,18 @@ export function ShowConsolidated({
             props: {
               label: i18n.str`State`,
               name: "aml.state",
-              converter: amlStateConverter,
               choices: [
                 {
                   label: i18n.str`Frozen`,
-                  value: AmlExchangeBackend.AmlState.frozen,
+                  value: TalerExchangeApi.AmlState.frozen,
                 },
                 {
                   label: i18n.str`Pending`,
-                  value: AmlExchangeBackend.AmlState.pending,
+                  value: TalerExchangeApi.AmlState.pending,
                 },
                 {
                   label: i18n.str`Normal`,
-                  value: AmlExchangeBackend.AmlState.normal,
+                  value: TalerExchangeApi.AmlState.normal,
                 },
               ],
             },
@@ -135,7 +133,7 @@ export function ShowConsolidated({
 
 interface Consolidated {
   aml: {
-    state: AmlExchangeBackend.AmlState;
+    state: TalerExchangeApi.AmlState;
     threshold: AmountJson;
     since: AbsoluteTime;
   };
@@ -154,7 +152,7 @@ function getConsolidated(
 ): Consolidated {
   const initial: Consolidated = {
     aml: {
-      state: AmlExchangeBackend.AmlState.normal,
+      state: TalerExchangeApi.AmlState.normal,
       threshold: {
         currency: "ARS",
         value: 1000,
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx 
b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index de634c9e0..9552f2b0c 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -13,81 +13,116 @@
  You should have received a copy of the GNU General Public License along with
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
-import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util";
-import { createNewForm, notifyError, notifyInfo, useTranslationContext } from 
"@gnu-taler/web-util/browser";
+import {
+  Button,
+  InputLine,
+  LocalNotificationBanner,
+  useLocalNotificationHandler,
+  useTranslationContext
+} from "@gnu-taler/web-util/browser";
 import { VNode, h } from "preact";
+import { FormErrors, useFormState } from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
 
-export function UnlockAccount({
-  onAccountUnlocked,
-  onRemoveAccount,
-}: {
-  onAccountUnlocked: (password: string) => Promise<void>;
-  onRemoveAccount: () => void;
-}): VNode {
-  const { i18n } = useTranslationContext()
-  const Form = createNewForm<{
-    password: string;
-  }>();
+type FormType = {
+  password: string;
+};
+
+export function UnlockAccount(): VNode {
+  const { i18n } = useTranslationContext();
+
+  const officer = useOfficer();
+  const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+  const [form, status] = useFormState<FormType>(
+    {
+      password: undefined,
+    },
+    (state) => {
+      const errors = undefinedIfEmpty<FormErrors<FormType>>({
+        password: !state.password ? i18n.str`required` : undefined,
+      });
+      if (errors === undefined) {
+        return {
+          status: "ok",
+          result: state as FormType,
+          errors,
+        };
+      }
+      return {
+        status: "fail",
+        result: state,
+        errors,
+      };
+    },
+  );
+
+  const unlockHandler =
+    status.status === "fail" || officer.state !== "locked"
+      ? undefined
+      : withErrorHandler(
+          async () => officer.tryUnlock(form.password!.value!),
+          () => {},
+        );
+
+  const forgetHandler =
+    status.status === "fail" || officer.state !== "locked"
+      ? undefined
+      : withErrorHandler(
+          async () => officer.forget(),
+          () => {},
+        );
 
   return (
     <div class="flex min-h-full flex-col ">
+      <LocalNotificationBanner notification={notification} />
+
       <div class="sm:mx-auto sm:w-full sm:max-w-md">
         <h1 class="mt-6 text-center text-2xl font-bold leading-9 
tracking-tight text-gray-900">
           <i18n.Translate>Account locked</i18n.Translate>
         </h1>
         <p class="mt-6 text-lg leading-8 text-gray-600">
-          <i18n.Translate>Your account is normally locked anytime you reload. 
To unlock type
-            your password again.</i18n.Translate>
+          <i18n.Translate>
+            Your account is normally locked anytime you reload. To unlock type
+            your password again.
+          </i18n.Translate>
         </p>
       </div>
 
       <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
         <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
-          <Form.Provider
-            onSubmit={async (v) => {
-              try {
-                await onAccountUnlocked(v.password!);
-                notifyInfo(i18n.str`Account unlocked`);
-              } catch (e) {
-                if (e instanceof UnwrapKeyError) {
-                  notifyError(
-                    i18n.str`Could not unlock account`,
-                    e.message as TranslatedString,
-                  );
-                } else {
-                  throw e;
-                }
-              }
-            }}
-          >
-            <div class="mb-4">
-              <Form.InputLine
-                label={i18n.str`Password`}
-                name="password"
-                type="password"
-                required
-              />
-            </div>
 
-            <div class="mt-8">
-              <button
-                type="submit"
-                class="flex w-full justify-center rounded-md bg-indigo-600 
px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
-              >
-                <i18n.Translate>Unlock</i18n.Translate>
-              </button>
-            </div>
-          </Form.Provider>
+          <div class="mb-4">
+            <InputLine<FormType, "password">
+              label={i18n.str`Password`}
+              name="password"
+              type="password"
+              required
+              handler={form.password}
+            />
+          </div>
+
+          <div class="mt-8">
+            <Button
+              type="submit"
+              handler={unlockHandler}
+              disabled={!unlockHandler}
+              class="disabled:opacity-50 disabled:cursor-default  flex w-full 
justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold 
leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline 
focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-indigo-600"
+            >
+              <i18n.Translate>Unlock</i18n.Translate>
+            </Button>
+          </div>
+
         </div>
-        <button
+        <Button
           type="button"
-          onClick={() => {
-            onRemoveAccount();
-          }}
-          class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm 
 text-white shadow-sm hover:bg-red-500 "
+          handler={forgetHandler}
+          disabled={!forgetHandler}
+          class="disabled:opacity-50 disabled:cursor-default m-4 block 
rounded-md bg-red-600 px-3 py-2 text-center text-sm  text-white shadow-sm 
hover:bg-red-500 "
         >
           <i18n.Translate>Forget account</i18n.Translate>
-        </button>
+        </Button>
       </div>
     </div>
   );
diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts 
b/packages/aml-backoffice-ui/src/pages/index.stories.ts
index b2cbf485e..f11028de8 100644
--- a/packages/aml-backoffice-ui/src/pages/index.stories.ts
+++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts
@@ -14,5 +14,4 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 export * as a1 from "./ShowConsolidated.stories.js";
-export * as a2 from "./AntiMoneyLaunderingForm.stories.js";
 export * as a3 from "./Cases.stories.js";
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts 
b/packages/aml-backoffice-ui/src/utils/converter.ts
index d2f05ed84..cca764a81 100644
--- a/packages/aml-backoffice-ui/src/utils/converter.ts
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -1,30 +1,46 @@
-import { AmlExchangeBackend } from "./types.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerExchangeApi } from "@gnu-taler/taler-util";
 
 export const amlStateConverter = {
   toStringUI: stringifyAmlState,
   fromStringUI: parseAmlState,
 };
 
-function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string 
{
+function stringifyAmlState(s: TalerExchangeApi.AmlState | undefined): string {
   if (s === undefined) return "";
   switch (s) {
-    case AmlExchangeBackend.AmlState.normal:
+    case TalerExchangeApi.AmlState.normal:
       return "normal";
-    case AmlExchangeBackend.AmlState.pending:
+    case TalerExchangeApi.AmlState.pending:
       return "pending";
-    case AmlExchangeBackend.AmlState.frozen:
+    case TalerExchangeApi.AmlState.frozen:
       return "frozen";
   }
 }
 
-function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState {
+function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
   switch (s) {
     case "normal":
-      return AmlExchangeBackend.AmlState.normal;
+      return TalerExchangeApi.AmlState.normal;
     case "pending":
-      return AmlExchangeBackend.AmlState.pending;
+      return TalerExchangeApi.AmlState.pending;
     case "frozen":
-      return AmlExchangeBackend.AmlState.frozen;
+      return TalerExchangeApi.AmlState.frozen;
     default:
       throw Error(`unknown AML state: ${s}`);
   }
diff --git a/packages/aml-backoffice-ui/src/utils/types.ts 
b/packages/aml-backoffice-ui/src/utils/types.ts
deleted file mode 100644
index fd70d4e4d..000000000
--- a/packages/aml-backoffice-ui/src/utils/types.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-export namespace AmlExchangeBackend {
-  // FIXME: placeholder
-  export interface AmlError {
-    code: number;
-    hint: string;
-  }
-  export interface AmlDecisionDetails {
-    // Array of AML decisions made for this account. Possibly
-    // contains only the most recent decision if "history" was
-    // not set to 'true'.
-    aml_history: AmlDecisionDetail[];
-
-    // Array of KYC attributes obtained for this account.
-    kyc_attributes: KycDetail[];
-  }
-
-  type AmlOfficerPublicKeyP = string;
-
-  export interface AmlDecisionDetail {
-    // What was the justification given?
-    justification: string;
-
-    // What is the new AML state.
-    new_state: Integer;
-
-    // When was this decision made?
-    decision_time: Timestamp;
-
-    // What is the new AML decision threshold (in monthly transaction volume)?
-    new_threshold: Amount;
-
-    // Who made the decision?
-    decider_pub: AmlOfficerPublicKeyP;
-  }
-  export interface KycDetail {
-    // Name of the configuration section that specifies the provider
-    // which was used to collect the KYC details
-    provider_section: string;
-
-    // The collected KYC data.  NULL if the attribute data could not
-    // be decrypted (internal error of the exchange, likely the
-    // attribute key was changed).
-    attributes?: Object;
-
-    // Time when the KYC data was collected
-    collection_time: Timestamp;
-
-    // Time when the validity of the KYC data will expire
-    expiration_time: Timestamp;
-  }
-
-  interface Timestamp {
-    // Seconds since epoch, or the special
-    // value "never" to represent an event that will
-    // never happen.
-    t_s: number | "never";
-  }
-
-  type PaytoHash = string;
-  type Integer = number;
-  type Amount = string;
-  // EdDSA signatures are transmitted as 64-bytes base32
-  // binary-encoded objects with just the R and S values (base32_ binary-only).
-  type EddsaSignature = string;
-
-  export interface AmlRecords {
-    // Array of AML records matching the query.
-    records: AmlRecord[];
-  }
-
-  interface AmlRecord {
-    // Which payto-address is this record about.
-    // Identifies a GNU Taler wallet or an affected bank account.
-    h_payto: PaytoHash;
-
-    // What is the current AML state.
-    current_state: AmlState;
-
-    // Monthly transaction threshold before a review will be triggered
-    threshold: Amount;
-
-    // RowID of the record.
-    rowid: Integer;
-  }
-
-  export enum AmlState {
-    normal = 0,
-    pending = 1,
-    frozen = 2,
-  }
-
-
-  export interface AmlDecision {
-
-    // Human-readable justification for the decision.
-    justification: string;
-
-    // At what monthly transaction volume should the
-    // decision be automatically reviewed?
-    new_threshold: Amount;
-
-    // Which payto-address is the decision about?
-    // Identifies a GNU Taler wallet or an affected bank account.
-    h_payto: PaytoHash;
-
-    // What is the new AML state (e.g. frozen, unfrozen, etc.)
-    // Numerical values are defined in AmlDecisionState.
-    new_state: Integer;
-
-    // Signature by the AML officer over a
-    // TALER_MasterAmlOfficerStatusPS.
-    // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
-    officer_sig: EddsaSignature;
-
-    // When was the decision made?
-    decision_time: Timestamp;
-
-    // Optional argument to impose new KYC requirements
-    // that the customer has to satisfy to unblock transactions.
-    kyc_requirements?: string[];
-  }
-
-
-}
diff --git a/packages/web-util/src/forms/DefaultForm.tsx 
b/packages/web-util/src/forms/DefaultForm.tsx
index 118699487..1c635e089 100644
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -1,4 +1,4 @@
-import { Fragment, h } from "preact";
+import { Fragment, VNode, h } from "preact";
 import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
 import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
 import { TranslatedString } from "@gnu-taler/taler-util";
@@ -39,7 +39,7 @@ export function DefaultForm<T extends object>({
   onSubmit,
   children,
   readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm<T> }) 
{
+}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm<T> 
}): VNode {
   return (
     <FormProvider
       initial={initial}
diff --git a/packages/web-util/src/forms/FormProvider.tsx 
b/packages/web-util/src/forms/FormProvider.tsx
index de19a6315..f4cdf8a68 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -43,8 +43,6 @@ export type FormState<T extends object | undefined> = {
  * Properties that can be defined by design or by computing state
  */
 export type FieldUIOptions = {
-  /* text to be shown next to the field */
-  error?: TranslatedString;
   /* instruction to be shown in the field */
   placeholder?: TranslatedString;
   /* long text help to be shown on demand */
@@ -84,6 +82,7 @@ export type UIField = {
   value: string | undefined;
   onChange: (s: string) => void;
   state: FieldUIOptions;
+  error?: TranslatedString;
 };
 
 export interface IconAddon {
diff --git a/packages/web-util/src/forms/InputLine.tsx 
b/packages/web-util/src/forms/InputLine.tsx
index c7f69dd8a..ee9150492 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -1,9 +1,8 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
 import { UIFormProps } from "./FormProvider.js";
-import { useField } from "./useField.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
+import { useField } from "./useField.js";
 
 //@ts-ignore
 const TooltipIcon = (
@@ -163,20 +162,20 @@ export function InputLine<T extends object, K extends 
keyof T>(
   const { name, placeholder, before, after, converter, type } = props;
   //FIXME: remove deprecated
   const fieldCtx = useField<T, K>(props.name);
-  const { value, onChange, state } =
+  const { value, onChange, state, error } =
     props.handler ?? fieldCtx ?? 
noHandlerPropsAndNoContextForField(props.name);
 
-  const [text, setText] = useState("");
+  // const [text, setText] = useState("");
   const fromString: (s: string) => any =
     converter?.fromStringUI ?? defaultFromString;
   const toString: (s: any) => string = converter?.toStringUI ?? 
defaultToString;
 
-  useEffect(() => {
-    const newValue = toString(value);
-    if (newValue) {
-      setText(newValue);
-    }
-  }, [value]);
+  // useEffect(() => {
+  //   const newValue = toString(value);
+  //   if (newValue) {
+  //     setText(newValue);
+  //   }
+  // }, [value]);
 
   if (state.hidden) return <div />;
 
@@ -214,7 +213,7 @@ export function InputLine<T extends object, K extends keyof 
T>(
       }
     }
   }
-  const showError = value !== undefined && state.error;
+  const showError = value !== undefined && error;
   if (showError) {
     clazz +=
       " text-red-900 ring-red-300  placeholder:text-red-300 
focus:ring-red-500";
@@ -229,7 +228,7 @@ export function InputLine<T extends object, K extends keyof 
T>(
         {...props}
         help={props.help ?? state.help}
         disabled={state.disabled ?? false}
-        error={showError ? state.error : undefined}
+        error={showError ? error : undefined}
       >
         <textarea
           rows={4}
@@ -254,18 +253,18 @@ export function InputLine<T extends object, K extends 
keyof T>(
       {...props}
       help={props.help ?? state.help}
       disabled={state.disabled ?? false}
-      error={showError ? state.error : undefined}
+      error={showError ? error : undefined}
     >
       <input
         name={String(name)}
         type={type}
         onChange={(e) => {
-          setText(e.currentTarget.value);
+          onChange(e.currentTarget.value as any);
         }}
         placeholder={placeholder ? placeholder : undefined}
-        value={text}
+        value={value as string}
         onBlur={() => {
-          onChange(fromString(text));
+          onChange(fromString(value as any));
         }}
         // defaultValue={toString(value)}
         disabled={state.disabled}
diff --git a/packages/web-util/src/forms/forms.ts 
b/packages/web-util/src/forms/forms.ts
index 1ad3508ae..d2ff9c37e 100644
--- a/packages/web-util/src/forms/forms.ts
+++ b/packages/web-util/src/forms/forms.ts
@@ -1,6 +1,5 @@
 import { h as create, Fragment, VNode } from "preact";
 import { Caption } from "./Caption.js";
-import { FormProvider } from "./FormProvider.js";
 import { Group } from "./Group.js";
 import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
 import { InputAmount } from "./InputAmount.js";
@@ -9,7 +8,6 @@ import { InputChoiceHorizontal } from 
"./InputChoiceHorizontal.js";
 import { InputChoiceStacked } from "./InputChoiceStacked.js";
 import { InputFile } from "./InputFile.js";
 import { InputInteger } from "./InputInteger.js";
-import { InputLine } from "./InputLine.js";
 import { InputSelectMultiple } from "./InputSelectMultiple.js";
 import { InputSelectOne } from "./InputSelectOne.js";
 import { InputText } from "./InputText.js";
diff --git a/packages/web-util/src/forms/useField.ts 
b/packages/web-util/src/forms/useField.ts
index fad53ebac..d30962c6f 100644
--- a/packages/web-util/src/forms/useField.ts
+++ b/packages/web-util/src/forms/useField.ts
@@ -1,11 +1,12 @@
-import { useContext, useState } from "preact/compat";
+import { useContext } from "preact/compat";
 import { FieldUIOptions, FormContext } from "./FormProvider.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
 
 export interface InputFieldHandler<Type> {
   value: Type;
   onChange: (s: Type) => void;
   state: FieldUIOptions;
-  isDirty: boolean;
+  error?: TranslatedString | undefined;
 }
 
 /**
@@ -33,8 +34,7 @@ export function useField<T extends object, K extends keyof T>(
   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<FieldUIOptions>>(formState, String(name)) ?? {};
 
@@ -42,7 +42,6 @@ export function useField<T extends object, K extends keyof T>(
   const state = {
     disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
     hidden: fieldState.hidden ?? false,
-    error: fieldState.error,
     help: fieldState.help,
     elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
   };
@@ -62,7 +61,6 @@ export function useField<T extends object, K extends keyof T>(
   return {
     value: fieldValue,
     onChange,
-    isDirty: fieldValue !== undefined,
     state,
   };
 }

-- 
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]