gnunet-svn
[Top][All Lists]
Advanced

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

[taler-typescript-core] branch master updated (f119f4aae -> 35b6e5931)


From: gnunet
Subject: [taler-typescript-core] branch master updated (f119f4aae -> 35b6e5931)
Date: Wed, 15 Jan 2025 22:32:46 +0100

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

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

    from f119f4aae wallet-core: use proper GANA error code when ToS download 
fails
     new 6dc70f3df new input type: secret
     new 411dcd6b3 input duration
     new e1aae7f0f fix some input array problems
     new d32df801e tospec
     new 140bef374 update measures
     new 9810c2747 sync types with docs
     new 6a744c253 use gana for account properties
     new 35b6e5931 properties form

The 8 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/aml-backoffice-ui/src/Routing.tsx         |   3 +
 packages/aml-backoffice-ui/src/forms.json          | 528 +--------------------
 packages/aml-backoffice-ui/src/hooks/account.ts    |   8 +-
 .../src/hooks/decision-request.ts                  |  86 +++-
 packages/aml-backoffice-ui/src/hooks/decisions.ts  |  63 ++-
 .../src/pages/AmlDecisionRequestWizard.tsx         | 476 +++++++++++++++++--
 .../aml-backoffice-ui/src/pages/CaseDetails.tsx    |   4 +-
 .../aml-backoffice-ui/src/pages/CreateAccount.tsx  |   4 +-
 packages/taler-util/src/time.ts                    |  52 +-
 packages/taler-util/src/types-taler-common.ts      |   1 +
 packages/taler-util/src/types-taler-exchange.ts    |  36 ++
 packages/web-util/src/forms/field-types.ts         |  11 +
 .../src/forms/fields/InputArray.stories.tsx        |  36 ++
 packages/web-util/src/forms/fields/InputArray.tsx  | 183 ++-----
 ...teger.stories.tsx => InputDuration.stories.tsx} |  22 +-
 .../web-util/src/forms/fields/InputDuration.tsx    | 183 +++++++
 packages/web-util/src/forms/fields/InputLine.tsx   |   2 +-
 ...Integer.stories.tsx => InputSecret.stories.tsx} |  13 +-
 .../fields/{InputText.tsx => InputSecret.tsx}      |   6 +-
 .../src/forms/fields/InputSelectMultiple.tsx       |  41 +-
 .../src/forms/fields/InputSelectOne.stories.tsx    |  37 ++
 .../src/forms/fields/InputText.stories.tsx         |   1 +
 .../src/forms/fields/InputToggle.stories.tsx       |   1 +
 packages/web-util/src/forms/fields/InputToggle.tsx |  17 +-
 packages/web-util/src/forms/forms-types.ts         |  26 +-
 packages/web-util/src/forms/forms-ui.tsx           |  15 +-
 packages/web-util/src/forms/forms-utils.ts         |  26 +
 .../src/forms/gana/taler_form_attributes.ts        |  51 ++
 packages/web-util/src/forms/index.stories.ts       |   2 +
 packages/web-util/src/forms/index.ts               |   1 +
 packages/web-util/src/hooks/useForm.ts             |   4 +-
 31 files changed, 1150 insertions(+), 789 deletions(-)
 copy packages/web-util/src/forms/fields/{InputInteger.stories.tsx => 
InputDuration.stories.tsx} (80%)
 create mode 100644 packages/web-util/src/forms/fields/InputDuration.tsx
 copy packages/web-util/src/forms/fields/{InputInteger.stories.tsx => 
InputSecret.stories.tsx} (86%)
 copy packages/web-util/src/forms/fields/{InputText.tsx => InputSecret.tsx} 
(59%)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx 
b/packages/aml-backoffice-ui/src/Routing.tsx
index ae9d14991..148ece344 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -164,6 +164,7 @@ function PrivateRouting(): VNode {
     case "decideNew": {
       return (
         <AmlDecisionRequestWizard
+          account={location.values.cid}
           onMove={(step) => {
             if (!step) {
               navigateTo(privatePages.profile.url({}));
@@ -182,6 +183,7 @@ function PrivateRouting(): VNode {
     case "decide": {
       return (
         <AmlDecisionRequestWizard
+          account={location.values.cid}
           onMove={(step) => {
             if (!step) {
               navigateTo(privatePages.profile.url({}));
@@ -200,6 +202,7 @@ function PrivateRouting(): VNode {
     case "decideWithStep": {
       return (
         <AmlDecisionRequestWizard
+          account={location.values.cid}
           step={location.values.step as WizardSteps}
           onMove={(step) => {
             if (!step) {
diff --git a/packages/aml-backoffice-ui/src/forms.json 
b/packages/aml-backoffice-ui/src/forms.json
index 94dcda317..6aff29275 100644
--- a/packages/aml-backoffice-ui/src/forms.json
+++ b/packages/aml-backoffice-ui/src/forms.json
@@ -1,529 +1,3 @@
 {
-  "forms": [
-    {
-      "label": "Information on customer",
-      "id": "902_1e",
-      "version": 1,
-      "config": {
-        "type": "double-column",
-        "design": [
-          {
-            "title": "Information on customer",
-            "description": "The customer is the person with whom the member 
concludes the contract with regard to the financial service provided (civil 
law). Does the member act as director of a domiciliary company, this 
domiciliary company is the customer.",
-            "fields": [
-              {
-                "type": "choiceStacked",
-
-                "name": "customerType",
-                "id": ".customerType",
-                "label": "Type of customer",
-                "help": "Select one and complete the next form",
-                "required": true,
-                "choices": [
-                  {
-                    "label": "Natural person",
-                    "value": "natural"
-                  },
-                  {
-                    "label": "Legal entity",
-                    "value": "legal"
-                  }
-                ]
-              },
-              {
-                "type": "group",
-
-                "label": "Natural customer form",
-                "name": "algo",
-                "id": "algo",
-                "before": "a) Country risk (nationality)",
-                "after": "a) Country risk (nationality)",
-                "fields": [
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.fullName",
-                    "id": ".naturalCustomer.fullName",
-                    "label": "Full name",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.address",
-                    "id": ".naturalCustomer.address",
-                    "label": "Residential address",
-                    "required": true
-                  },
-                  {
-                    "type": "integer",
-
-                    "name": "naturalCustomer.telephone",
-                    "id": ".naturalCustomer.telephone",
-                    "label": "Telephone"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.email",
-                    "id": ".naturalCustomer.email",
-                    "label": "E-mail"
-                  },
-                  {
-                    "type": "absoluteTimeText",
-
-                    "pattern": "dd/MM/yyyy",
-                    "name": "naturalCustomer.dateOfBirth",
-                    "id": ".naturalCustomer.dateOfBirth",
-                    "label": "Date of birth",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.nationality",
-                    "id": ".naturalCustomer.nationality",
-                    "label": "Nationality",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.document",
-                    "id": ".naturalCustomer.document",
-                    "label": "Identification document",
-                    "required": true
-                  },
-                  {
-                    "type": "file",
-
-                    "name": "naturalCustomer.documentAttachment",
-                    "id": ".naturalCustomer.documentAttachment",
-                    "label": "Document attachment",
-                    "required": true,
-                    "maxBites": 2097152,
-                    "accept": ".pdf",
-                    "help": "PDF file with max size of 2 mega bytes"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.companyName",
-                    "id": ".naturalCustomer.companyName",
-                    "label": "Company name"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.office",
-                    "id": ".naturalCustomer.office",
-                    "label": "Registered office"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "naturalCustomer.companyDocument",
-                    "id": ".naturalCustomer.companyDocument",
-                    "label": "Company identification document"
-                  },
-                  {
-                    "type": "file",
-
-                    "name": "naturalCustomer.companyDocumentAttachment",
-                    "id": ".naturalCustomer.companyDocumentAttachment",
-                    "label": "Document attachment",
-                    "required": true,
-                    "maxBites": 2097152,
-                    "accept": ".png",
-                    "help": "PNG file with max size of 2 mega bytes"
-                  }
-                ]
-              },
-
-              {
-                "type": "group",
-
-                "label": "Natural customer form",
-                "name": "algo",
-                "id": "algo",
-                "before": "a) Country risk (nationality)",
-                "after": "a) Country risk (nationality)",
-                "fields": [
-                  {
-                    "type": "text",
-
-                    "name": "legalCustomer.companyName",
-                    "id": ".legalCustomer.companyName",
-                    "label": "Company name",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "legalCustomer.domicile",
-                    "id": ".legalCustomer.domicile",
-                    "label": "Domicile",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "legalCustomer.contactPerson",
-                    "id": ".legalCustomer.contactPerson",
-                    "label": "Contact person"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "legalCustomer.telephone",
-                    "id": ".legalCustomer.telephone",
-                    "label": "Telephone"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "legalCustomer.email",
-                    "id": ".legalCustomer.email",
-                    "label": "E-mail"
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "legalCustomer.document",
-                    "id": ".legalCustomer.document",
-                    "label": "Identification document",
-                    "help": "Not older than 12 month"
-                  },
-                  {
-                    "type": "file",
-
-                    "name": "legalCustomer.documentAttachment",
-                    "id": ".legalCustomer.documentAttachment",
-                    "label": "Document attachment",
-                    "required": true,
-                    "maxBites": 2097152,
-                    "accept": ".png",
-                    "help": "PNG file with max size of 2 mega bytes"
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "title": "Information on the natural persons who establish the 
business relationship for legal entities and partnerships",
-            "description": "For legal entities and partnerships the identity 
of the natural persons who establish the business relationship must be 
verified.",
-            "fields": [
-              {
-                "type": "array",
-
-                "name": "businessEstablisher",
-                "id": ".businessEstablisher",
-                "label": "Persons",
-                "required": true,
-                "labelFieldId": "fullName",
-                "placeholder": "this is the placeholder",
-                "fields": [
-                  {
-                    "type": "text",
-
-                    "name": "fullName",
-                    "id": ".fullName",
-                    "label": "Full name",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "address",
-                    "id": ".address",
-                    "label": "Residential address",
-                    "required": true
-                  },
-                  {
-                    "type": "absoluteTimeText",
-
-                    "pattern": "dd/MM/yyyy",
-                    "name": "dateOfBirth",
-                    "id": ".dateOfBirth",
-                    "label": "Date of birth",
-                    "required": true
-                  },
-
-                  {
-                    "type": "text",
-
-                    "name": "nationality",
-                    "id": ".nationality",
-                    "label": "Nationality",
-                    "required": true
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "typeOfAuthorization",
-                    "id": ".typeOfAuthorization",
-                    "label": "Type of authorization (signatory of 
representation)",
-                    "required": true
-                  },
-                  {
-                    "type": "file",
-
-                    "name": "documentAttachment",
-                    "id": ".documentAttachment",
-                    "label": "Identification document attachment",
-                    "required": true,
-                    "maxBites": 2097152,
-                    "accept": ".pdf",
-                    "help": "PDF file with max size of 2 mega bytes"
-                  },
-                  {
-                    "type": "choiceStacked",
-
-                    "name": "powerOfAttorneyArrangements",
-                    "id": ".powerOfAttorneyArrangements",
-                    "label": "Power of attorney arrangements",
-                    "required": true,
-                    "choices": [
-                      {
-                        "label": "CR extract",
-                        "value": "cr"
-                      },
-                      {
-                        "label": "Mandate",
-                        "value": "mandate"
-                      },
-                      {
-                        "label": "Other",
-                        "value": "other"
-                      }
-                    ]
-                  },
-                  {
-                    "type": "text",
-
-                    "name": "powerOfAttorneyArrangementsOther",
-                    "id": ".powerOfAttorneyArrangementsOther",
-                    "label": "Power of attorney arrangements",
-                    "required": true
-                  }
-                ],
-                "labelField": "fullName"
-              }
-            ]
-          },
-          {
-            "title": "Acceptance of business relationship",
-            "fields": [
-              {
-                "type": "absoluteTimeText",
-
-                "name": "acceptance.when",
-                "id": ".acceptance.when",
-                "pattern": "dd/MM/yyyy",
-                "converterId": "Taler.AbsoluteTime",
-                "label": "Date (conclusion of contract)"
-              },
-              {
-                "type": "choiceStacked",
-
-                "name": "acceptance.acceptedBy",
-                "id": ".acceptance.acceptedBy",
-                "label": "Accepted by",
-                "required": true,
-                "choices": [
-                  {
-                    "label": "Face-to-face meeting with customer",
-                    "value": "face-to-face"
-                  },
-                  {
-                    "label": "Correspondence: authenticated copy of 
identification document obtained",
-                    "value": "correspondence-document"
-                  },
-                  {
-                    "label": "Correspondence: residential address validated",
-                    "value": "correspondence-address"
-                  }
-                ]
-              },
-              {
-                "type": "choiceStacked",
-
-                "name": "acceptance.typeOfCorrespondence",
-                "id": ".acceptance.typeOfCorrespondence",
-                "label": "Type of correspondence service",
-                "choices": [
-                  {
-                    "label": "to the customer",
-                    "value": "customer"
-                  },
-                  {
-                    "label": "hold at bank",
-                    "value": "bank"
-                  },
-                  {
-                    "label": "to the member",
-                    "value": "member"
-                  },
-                  {
-                    "label": "to a third party",
-                    "value": "third-party"
-                  }
-                ]
-              },
-              {
-                "type": "text",
-
-                "name": "acceptance.thirdPartyFullName",
-                "id": ".acceptance.thirdPartyFullName",
-                "label": "Third party full name",
-                "required": true
-              },
-              {
-                "type": "text",
-
-                "name": "acceptance.thirdPartyAddress",
-                "id": ".acceptance.thirdPartyAddress",
-                "label": "Third party  address",
-                "required": true
-              },
-              {
-                "type": "selectMultiple",
-
-                "name": "acceptance.language",
-                "id": ".acceptance.language",
-                "label": "Languages",
-                "choices": [
-                  {
-                    "label": "Espanol",
-                    "value": "es"
-                  }
-                ],
-                "unique": true
-              },
-              {
-                "type": "textArea",
-
-                "name": "acceptance.furtherInformation",
-                "id": ".acceptance.furtherInformation",
-                "label": "Further information"
-              }
-            ]
-          },
-          {
-            "title": "Information on the beneficial owner of the assets and/or 
controlling person",
-            "description": "Establishment of the beneficial owner of the 
assets and/or controlling person",
-            "fields": [
-              {
-                "type": "choiceStacked",
-
-                "name": "establishment",
-                "id": ".establishment",
-                "label": "The customer is",
-                "required": true,
-                "choices": [
-                  {
-                    "label": "a natural person and there are no doubts that 
this person is the sole beneficial owner of the assets",
-                    "value": "natural"
-                  },
-                  {
-                    "label": "a foundation (or a similar construct; incl. 
underlying companies)",
-                    "value": "foundation"
-                  },
-                  {
-                    "label": "a trust (incl. underlying companies)",
-                    "value": "trust"
-                  },
-                  {
-                    "label": "a life insurance policy with separately managed 
accounts/securities accounts",
-                    "value": "insurance-wrapper"
-                  },
-                  {
-                    "label": "all other cases",
-                    "value": "other"
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "title": "Evaluation with regard to embargo procedures/terrorism 
lists on establishing the business relationship",
-            "description": "Verification whether the customer, beneficial 
owners of the assets, controlling persons, authorized representatives or other 
involved persons are listed on an embargo/terrorism list (date of 
verification/result)",
-            "fields": [
-              {
-                "type": "textArea",
-
-                "name": "embargoEvaluation",
-                "id": ".embargoEvaluation",
-                "help": "The evaluation must be made at the beginning of the 
business relationship and has to be repeated in the case of permanent business 
relationship every time the according lists are updated.",
-                "label": "Evaluation"
-              }
-            ]
-          },
-          {
-            "title": "In the case of cash transactions/occasional customers: 
Information on type and purpose of business relationship",
-            "description": "These details are only necessary for occasional 
customers, i.e. money exchange, money and asset transfer or other cash 
transactions provided that no customer profile (VQF doc. No. 902.5) is created",
-            "fields": [
-              {
-                "type": "choiceStacked",
-
-                "name": "cashTransactions.typeOfBusiness",
-                "id": ".cashTransactions.typeOfBusiness",
-                "label": "Type of business relationship",
-                "choices": [
-                  {
-                    "label": "Money exchange",
-                    "value": "money-exchange"
-                  },
-                  {
-                    "label": "Money and asset transfer",
-                    "value": "money-and-asset-transfer"
-                  },
-                  {
-                    "label": "Other cash transactions. Specify below",
-                    "value": "other"
-                  }
-                ]
-              },
-              {
-                "type": "text",
-
-                "name": "cashTransactions.otherTypeOfBusiness",
-                "id": ".cashTransactions.otherTypeOfBusiness",
-                "required": true,
-                "label": "Specify other cash transactions:"
-              },
-              {
-                "type": "textArea",
-                "name": "cashTransactions.purpose",
-                "id": ".cashTransactions.purpose",
-                "label": "Purpose of the business relationship (purpose of 
service requested)"
-              }
-            ]
-          }
-        ]
-      }
-    },
-    {
-      "label": "Example form",
-      "id": "example",
-      "version": 1,
-      "config": {
-        "type": "double-column",
-        "design": [
-          {
-            "title": "Boolean inputs",
-            "fields": [
-              {
-                "type": "toggle",
-                "name": "yes",
-                "id": ".yes",
-                "label": "Yes or no?"
-              }
-            ]
-          }
-        ]
-      }
-    }
-  ],
-  "not_yet_supported": []
+  "forms": []
 }
diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts 
b/packages/aml-backoffice-ui/src/hooks/account.ts
index 9c2049129..a4fbcb803 100644
--- a/packages/aml-backoffice-ui/src/hooks/account.ts
+++ b/packages/aml-backoffice-ui/src/hooks/account.ts
@@ -34,9 +34,12 @@ export function revalidateAccountInformation() {
     { revalidate: true },
   );
 }
-export function useAccountInformation(paytoHash: string) {
+export function useAccountInformation(paytoHash?: string) {
   const officer = useOfficer();
-  const session = officer.state === "ready" ? officer.account : undefined;
+  const session =
+    officer.state === "ready" && paytoHash !== undefined
+      ? officer.account
+      : undefined;
 
   const {
     lib: { exchange: api },
@@ -97,4 +100,3 @@ export function useServerMeasures() {
   if (error) return error;
   return undefined;
 }
-
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts 
b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
index 4298ad6f3..132af8b1c 100644
--- a/packages/aml-backoffice-ui/src/hooks/decision-request.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -16,36 +16,53 @@
 
 import {
   AbsoluteTime,
+  AmountJson,
   Codec,
-  KycRule,
+  Duration,
   MeasureInformation,
-  PaytoHash,
-  Timestamp,
-  TranslatedString,
   buildCodecForObject,
   codecForAbsoluteTime,
+  codecForAmountJson,
   codecForAny,
   codecForBoolean,
-  codecForKycRules,
+  codecForDurationMs,
   codecForList,
   codecForMap,
   codecForMeasureInformation,
   codecForString,
   codecOptional,
+  codecOptionalDefault,
 } from "@gnu-taler/taler-util";
-import {
-  buildStorageKey,
-  useLocalStorage,
-  useTranslationContext,
-} from "@gnu-taler/web-util/browser";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+
+export type BalanceForm = {
+  balance: AmountJson | undefined;
+  balanceUnlimited: boolean;
+  withdrawalAmount: AmountJson | undefined;
+  withdrawalTimeframe: Duration | undefined;
+  withdrawalUnlimited: boolean;
+  depositAmount: AmountJson | undefined;
+  depositTimeframe: Duration | undefined;
+  depositUnlimited: boolean;
+  closeAmount: AmountJson | undefined;
+  closeTimeframe: Duration | undefined;
+  closeUnlimited: boolean;
+  mergeAmount: AmountJson | undefined;
+  mergeTimeframe: Duration | undefined;
+  mergeUnlimited: boolean;
+  aggregateAmount: AmountJson | undefined;
+  aggregateTimeframe: Duration | undefined;
+  aggregateUnlimited: boolean;
+};
 
 export interface DecisionRequest {
-  rules: KycRule[] | undefined;
+  rules: BalanceForm | undefined;
   deadline: AbsoluteTime | undefined;
   properties: object | undefined;
+  custom_properties: object | undefined;
   custom_events: string[] | undefined;
   inhibit_events: string[] | undefined;
-  keep_investigating: boolean | undefined;
+  keep_investigating: boolean;
   justification: string | undefined;
   next_measure: string[][] | undefined;
   custom_measures:
@@ -55,15 +72,52 @@ export interface DecisionRequest {
     | undefined;
 }
 
+export const codecForBalanceForm = (): Codec<BalanceForm> =>
+  buildCodecForObject<BalanceForm>()
+    .property("balance", codecOptional(codecForAmountJson()))
+    .property(
+      "balanceUnlimited",
+      codecOptionalDefault(codecForBoolean(), false),
+    )
+    .property("withdrawalAmount", codecOptional(codecForAmountJson()))
+    .property("withdrawalTimeframe", codecOptional(codecForDurationMs))
+    .property(
+      "withdrawalUnlimited",
+      codecOptionalDefault(codecForBoolean(), false),
+    )
+    .property("depositAmount", codecOptional(codecForAmountJson()))
+    .property("depositTimeframe", codecOptional(codecForDurationMs))
+    .property(
+      "depositUnlimited",
+      codecOptionalDefault(codecForBoolean(), false),
+    )
+    .property("closeAmount", codecOptional(codecForAmountJson()))
+    .property("closeTimeframe", codecOptional(codecForDurationMs))
+    .property("closeUnlimited", codecOptionalDefault(codecForBoolean(), false))
+    .property("mergeAmount", codecOptional(codecForAmountJson()))
+    .property("mergeTimeframe", codecOptional(codecForDurationMs))
+    .property("mergeUnlimited", codecOptionalDefault(codecForBoolean(), false))
+    .property("aggregateAmount", codecOptional(codecForAmountJson()))
+    .property("aggregateTimeframe", codecOptional(codecForDurationMs))
+    .property(
+      "aggregateUnlimited",
+      codecOptionalDefault(codecForBoolean(), false),
+    )
+    .build("BalanceForm");
+
 export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
   buildCodecForObject<DecisionRequest>()
-    .property("rules", codecOptional(codecForList(codecForKycRules())))
+    .property("rules", codecOptional(codecForBalanceForm()))
     .property("deadline", codecOptional(codecForAbsoluteTime))
     .property("properties", codecForAny())
+    .property("custom_properties", codecForAny())
     .property("justification", codecOptional(codecForString()))
     .property("custom_events", codecOptional(codecForList(codecForString())))
     .property("inhibit_events", codecOptional(codecForList(codecForString())))
-    .property("keep_investigating", codecOptional(codecForBoolean()))
+    .property(
+      "keep_investigating",
+      codecOptionalDefault(codecForBoolean(), false),
+    )
     .property(
       "next_measure",
       codecOptional(codecForList(codecForList(codecForString()))),
@@ -80,9 +134,10 @@ const defaultDecisionRequest: DecisionRequest = {
   custom_events: undefined,
   inhibit_events: undefined,
   justification: undefined,
-  keep_investigating: undefined,
+  keep_investigating: false,
   next_measure: undefined,
   properties: undefined,
+  custom_properties: undefined,
   rules: undefined,
 };
 
@@ -110,6 +165,7 @@ export function useCurrentDecisionRequest(): [
     v: DecisionRequest[T],
   ) {
     const newValue = { ...value, [k]: v };
+    console.log("===", v, k);
     update(newValue);
   }
   return [value, updateField, update];
diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts 
b/packages/aml-backoffice-ui/src/hooks/decisions.ts
index 24941b29e..a6c95098d 100644
--- a/packages/aml-backoffice-ui/src/hooks/decisions.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decisions.ts
@@ -19,6 +19,8 @@ import { useState } from "preact/hooks";
 import {
   OfficerAccount,
   OperationOk,
+  opFixedSuccess,
+  opSuccessFromHttp,
   TalerExchangeResultByMethod,
   TalerHttpError,
 } from "@gnu-taler/taler-util";
@@ -63,12 +65,7 @@ export function useCurrentDecisionsUnderInvestigation() {
   const { data, error } = useSWR<
     TalerExchangeResultByMethod<"getAmlDecisions">,
     TalerHttpError
-  >(
-    !session
-      ? undefined
-      : [session, offset, "getAmlDecisions"],
-    fetcher,
-  );
+  >(!session ? undefined : [session, offset, "getAmlDecisions"], fetcher);
 
   if (error) return error;
   if (data === undefined) return undefined;
@@ -109,12 +106,7 @@ export function useCurrentDecisions() {
   const { data, error } = useSWR<
     TalerExchangeResultByMethod<"getAmlDecisions">,
     TalerHttpError
-  >(
-    !session
-      ? undefined
-      : [session, offset, "getAmlDecisions"],
-    fetcher,
-  );
+  >(!session ? undefined : [session, offset, "getAmlDecisions"], fetcher);
 
   if (error) return error;
   if (data === undefined) return undefined;
@@ -176,6 +168,53 @@ export function useAccountDecisions(accountStr: string) {
   );
 }
 
+/**
+ * @param account
+ * @param args
+ * @returns
+ */
+export function useAccountActiveDecision(accountStr?: string) {
+  const officer = useOfficer();
+  const session =
+    accountStr !== undefined && officer.state === "ready"
+      ? officer.account
+      : undefined;
+  const {
+    lib: { exchange: api },
+  } = useExchangeApiContext();
+
+  const [offset, setOffset] = useState<string>();
+
+  async function fetcher([officer, account, offset]: [
+    OfficerAccount,
+    string,
+    string | undefined,
+  ]) {
+    return await api.getAmlDecisions(officer, {
+      order: "dec",
+      offset,
+      account,
+      active: true,
+      limit: PAGINATED_LIST_REQUEST,
+    });
+  }
+
+  const { data, error } = useSWR<
+    TalerExchangeResultByMethod<"getAmlDecisions">,
+    TalerHttpError
+  >(
+    !session ? undefined : [session, accountStr, offset, "getAmlDecisions"],
+    fetcher,
+  );
+
+  if (error) return error;
+  if (data === undefined) return undefined;
+  if (data.type !== "ok") return data;
+
+  if (!data.body.records.length) return opFixedSuccess(undefined);
+  return opFixedSuccess(data.body.records[0]);
+}
+
 type PaginatedResult<T> = OperationOk<T> & {
   isLastPage: boolean;
   isFirstPage: boolean;
diff --git a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx 
b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
index 8491abe0b..045c8f681 100644
--- a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
+++ b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
@@ -16,15 +16,31 @@
 import {
   AbsoluteTime,
   assertUnreachable,
+  MeasureInformation,
+  TalerError,
   TranslatedString,
 } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
 import {
+  FormDesign,
+  FormUI,
+  InternationalizationAPI,
+  UIFormElementConfig,
+  UIHandlerId,
+  useExchangeApiContext,
+  useForm,
+  TalerFormAttributes,
+  useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import {
+  BalanceForm,
   DecisionRequest,
   useCurrentDecisionRequest,
 } from "../hooks/decision-request.js";
-import { ShowDecisionLimitInfo } from "./CaseDetails.js";
+import { useAccountActiveDecision } from "../hooks/decisions.js";
+import { ShowDecisionLimitInfo, ShowMeasuresToSelect } from "./CaseDetails.js";
+import { useEffect, useRef } from "preact/hooks";
+import { useServerMeasures } from "../hooks/account.js";
 
 export type WizardSteps =
   | "rules" // define the limits
@@ -58,15 +74,7 @@ const STEPS_ORDER_MAP = STEPS_ORDER.reduce(
 );
 
 function isRulesCompleted(request: DecisionRequest): boolean {
-  return (
-    request.rules !== undefined &&
-    request.rules.findIndex((r) => r.operation_type === "AGGREGATE") !== -1 &&
-    request.rules.findIndex((r) => r.operation_type === "BALANCE") !== -1 &&
-    request.rules.findIndex((r) => r.operation_type === "CLOSE") !== -1 &&
-    request.rules.findIndex((r) => r.operation_type === "DEPOSIT") !== -1 &&
-    request.rules.findIndex((r) => r.operation_type === "MERGE") !== -1 &&
-    request.rules.findIndex((r) => r.operation_type === "WITHDRAW") !== -1
-  );
+  return request.rules !== undefined;
 }
 function isPropertiesCompleted(request: DecisionRequest): boolean {
   return request.properties !== undefined;
@@ -87,9 +95,11 @@ function isJustificationCompleted(request: DecisionRequest): 
boolean {
 }
 
 export function AmlDecisionRequestWizard({
+  account,
   step,
   onMove,
 }: {
+  account?: string;
   step?: WizardSteps;
   onMove: (n: WizardSteps | undefined) => void;
 }): VNode {
@@ -98,7 +108,7 @@ export function AmlDecisionRequestWizard({
   const content = (function () {
     switch (stepOrDefault) {
       case "rules":
-        return <Rules />;
+        return <Rules account={account} />;
       case "properties":
         return <Properties />;
       case "events":
@@ -132,41 +142,324 @@ export function AmlDecisionRequestWizard({
       >
         <i18n.Translate>Next</i18n.Translate>
       </button>
-      {/* <button
-        onClick={() => {
-          onMove(undefined);
-        }}
-        class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
-      >
-        <i18n.Translate>Exit</i18n.Translate>
-      </button> */}
       {content}
     </div>
   );
 }
 
+const rulesForm = (
+  i18n: InternationalizationAPI,
+  currency: string,
+): FormDesign<BalanceForm> => ({
+  type: "double-column",
+  sections: [
+    {
+      title: i18n.str`Wallet`,
+      description: i18n.str`Limit the state of the wallet.`,
+      fields: [
+        {
+          id: "balance" as UIHandlerId,
+          type: "amount",
+          label: i18n.str`Balance`,
+          currency,
+        },
+        {
+          id: "balanceUnlimited" as UIHandlerId,
+          type: "toggle",
+          label: i18n.str`Unlimited`,
+        },
+      ],
+    },
+    {
+      title: i18n.str`Operations`,
+      description: i18n.str`Limit the operation rate.`,
+      fields: [
+        {
+          type: "group",
+          label: i18n.str`Withdrawal`,
+          fields: [
+            {
+              id: "withdrawalAmount" as UIHandlerId,
+              type: "amount",
+              label: i18n.str`Amount`,
+              currency,
+            },
+            {
+              id: "withdrawalTimeframe" as UIHandlerId,
+              type: "duration",
+              label: i18n.str`Timeframe`,
+            },
+            {
+              id: "withdrawalUnlimited" as UIHandlerId,
+              type: "toggle",
+              label: i18n.str`Unlimited`,
+            },
+          ],
+        },
+        {
+          type: "group",
+          label: i18n.str`Deposit`,
+          fields: [
+            {
+              id: "depositAmount" as UIHandlerId,
+              type: "amount",
+              label: i18n.str`Amount`,
+
+              currency,
+            },
+            {
+              id: "depositTimeframe" as UIHandlerId,
+              type: "duration",
+              label: i18n.str`Timeframe`,
+            },
+          ],
+        },
+        {
+          type: "group",
+          label: i18n.str`Aggregate`,
+          fields: [
+            {
+              id: "aggregateAmount" as UIHandlerId,
+              type: "amount",
+              label: i18n.str`Amount`,
+
+              currency,
+            },
+            {
+              id: "aggregateTimeframe" as UIHandlerId,
+              type: "duration",
+              label: i18n.str`Timeframe`,
+            },
+          ],
+        },
+        {
+          type: "group",
+          label: i18n.str`Merge`,
+          fields: [
+            {
+              id: "mergeAmount" as UIHandlerId,
+              type: "amount",
+              label: i18n.str`Amount`,
+
+              currency,
+            },
+            {
+              id: "mergeTimeframe" as UIHandlerId,
+              type: "duration",
+              label: i18n.str`Timeframe`,
+            },
+          ],
+        },
+        {
+          type: "group",
+          label: i18n.str`Close`,
+          fields: [
+            {
+              id: "closeAmount" as UIHandlerId,
+              type: "amount",
+              label: i18n.str`Amount`,
+
+              currency,
+            },
+            {
+              id: "closeTimeframe" as UIHandlerId,
+              type: "duration",
+              label: i18n.str`Timeframe`,
+            },
+          ],
+        },
+      ],
+    },
+  ],
+});
+
+function onComponentUnload(callback: () => void) {
+  /**
+   * we use a ref to avoid evaluating the effect function
+   * on every render and so the unload is called only once
+   */
+  const ref = useRef<typeof callback>();
+  ref.current = callback;
+
+  useEffect(() => {
+    return () => {
+      ref.current!();
+    };
+  }, []);
+}
+
 /**
  * Defined new limits for the account
  * @param param0
  * @returns
  */
-function Rules({}: {}): VNode {
-  const [request] = useCurrentDecisionRequest();
-  if (request.rules) {
-    return (
-      <ShowDecisionLimitInfo
-        fixed
-        since={AbsoluteTime.now()}
-        until={request.deadline ?? AbsoluteTime.never()}
-        rules={request.rules}
-        startOpen
-      />
-    );
-  }
+function Rules({ account }: { account?: string }): VNode {
+  const activeDecision = useAccountActiveDecision(account);
+
+  const { i18n } = useTranslationContext();
+  const { config } = useExchangeApiContext();
+  const [request, updateRequest] = useCurrentDecisionRequest();
+  const currency = config.config.currency;
+  const design = rulesForm(i18n, currency);
+  const form = useForm<BalanceForm>(design, request.rules ?? {});
+
+  onComponentUnload(() => {
+    updateRequest("rules", form.status.result as any);
+  });
 
-  return <div></div>;
+  const info =
+    !activeDecision ||
+    activeDecision instanceof TalerError ||
+    activeDecision.type === "fail"
+      ? undefined
+      : activeDecision.body;
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+      {!info ? undefined : (
+        <div>
+          <h2 class="mt-4 mb-2">
+            <i18n.Translate>Current limits</i18n.Translate>
+          </h2>
+          <ShowDecisionLimitInfo
+            fixed
+            since={AbsoluteTime.fromProtocolTimestamp(info.decision_time)}
+            until={AbsoluteTime.fromProtocolTimestamp(
+              info.limits.expiration_time,
+            )}
+            rules={info.limits.rules}
+            startOpen
+          />
+        </div>
+      )}
+    </div>
+  );
 }
 
+type PropertiesForm = {
+  defined: { [name: string]: boolean };
+  custom: { [name: string]: boolean };
+};
+
+const propertiesForm = (
+  i18n: InternationalizationAPI,
+  fields: UIFormElementConfig[],
+): FormDesign<PropertiesForm> => ({
+  type: "double-column",
+  sections: [
+    {
+      title: i18n.str`Properties`,
+      description: i18n.str`props.`,
+      fields: fields.map((f) =>
+        "id" in f ? { ...f, id: ("defined." + f.id) as UIHandlerId } : f,
+      ),
+    },
+    {
+      title: i18n.str`Custom properties`,
+      description: i18n.str`add properties not listed above`,
+      fields: [
+        {
+          id: "custom" as UIHandlerId,
+          type: "array",
+          label: i18n.str`Fields`,
+          labelFieldId: "name" as UIHandlerId,
+          fields: [
+            {
+              type: "text",
+              label: i18n.str`Name`,
+              id: "name" as UIHandlerId,
+            },
+            {
+              type: "text",
+              label: i18n.str`Value`,
+              id: "value" as UIHandlerId,
+            },
+          ],
+        },
+      ],
+    },
+  ],
+});
+
+function fieldsByDialect(
+  i18n: InternationalizationAPI,
+  dialect: string | undefined,
+): UIFormElementConfig[] {
+  if (!dialect) return [];
+  switch (dialect) {
+    case "testing": {
+      return [
+        {
+          id: "ACCOUNT_PEP" satisfies keyof 
TalerFormAttributes.AccountProperties_Testing as UIHandlerId,
+          label: i18n.str`Public exposed person?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+      ];
+    }
+    case "gls": {
+      return [
+        {
+          id: "ACCOUNT_REPORTED" satisfies keyof 
TalerFormAttributes.AccountProperties_GLS as UIHandlerId,
+          label: i18n.str`Is PEP`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+      ];
+    }
+    case "tops": {
+      return [
+        {
+          id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Frozen?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+        {
+          id: "ACCOUNT_HIGH_RISK" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`High risk?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+        {
+          id: "ACCOUNT_PEP" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Public exposed person?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+        {
+          id: "ACCOUNT_REPORTED" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is reported to authorities?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+        {
+          id: "ACCOUNT_SANCTIONED" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is PEP`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        },
+        {
+          id: "ACCOUNT_BUSINESS_DOMAIN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Business domain`,
+          // gana_type: "Boolean",
+          type: "text",
+        },
+      ];
+    }
+    default: {
+      return [];
+    }
+  }
+}
 /**
  * Update account properites
  * @param param0
@@ -174,10 +467,53 @@ function Rules({}: {}): VNode {
  */
 function Properties({}: {}): VNode {
   const { i18n } = useTranslationContext();
-  const [request] = useCurrentDecisionRequest();
-  return <div> not yet impltemented: props</div>;
+  const [request, updateRequest] = useCurrentDecisionRequest();
+  const { config } = useExchangeApiContext();
+  const design = propertiesForm(
+    i18n,
+    fieldsByDialect(i18n, config.config.aml_spa_dialect),
+  );
+
+  const form = useForm<PropertiesForm>(design, {
+    defined: request.properties,
+    custom: request.custom_properties,
+  });
+
+  onComponentUnload(() => {
+    updateRequest("properties", form.status.result.defined);
+    updateRequest("custom_properties", form.status.result.custom);
+  });
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+    </div>
+  );
 }
 
+type EventsForm = {
+  trigger: { [name: string]: boolean };
+  inhibit: { [name: string]: boolean };
+};
+
+const eventsForm = (
+  i18n: InternationalizationAPI,
+  props: string[],
+): FormDesign<MeasureForm> => ({
+  type: "double-column",
+  sections: [
+    {
+      title: i18n.str`Custom events`,
+      description: i18n.str`This events will be triggered by default unless 
you marked to skip it.`,
+      fields: [],
+    },
+    {
+      title: i18n.str`Inhibit`,
+      description: i18n.str`This events will be triggered by default unless 
you marked to skip it.`,
+      fields: [],
+    },
+  ],
+});
 /**
  * Trigger additional events
  * @param param0
@@ -189,6 +525,40 @@ function Events({}: {}): VNode {
   return <div> not yet impltemented: events</div>;
 }
 
+type MeasureForm = {
+  paths: { steps: Array<string> }[];
+};
+
+const measureForm = (
+  i18n: InternationalizationAPI,
+  mi: (MeasureInformation & { id: string })[],
+): FormDesign<MeasureForm> => ({
+  type: "single-column",
+  fields: [
+    {
+      type: "array",
+      id: "paths" as UIHandlerId,
+      label: i18n.str`Paths`,
+      help: i18n.str`For every entry the customer will have a different path 
to satify checks.`,
+      labelFieldId: "steps" as UIHandlerId,
+      fields: [
+        {
+          type: "selectMultiple",
+          choices: mi.map((m) => {
+            return {
+              value: m.id,
+              label: m.id,
+            };
+          }),
+          id: "steps" as UIHandlerId,
+          label: i18n.str`Steps`,
+          help: i18n.str`The checks that the customer will need to satisfy for 
this path.`,
+        },
+      ],
+    },
+  ],
+});
+
 /**
  * Ask for more information, define new paths to proceed
  * @param param0
@@ -196,8 +566,36 @@ function Events({}: {}): VNode {
  */
 function Measures({}: {}): VNode {
   const { i18n } = useTranslationContext();
-  const [request] = useCurrentDecisionRequest();
-  return <div> not yet impltemented: measures</div>;
+  const [request, updateRequest] = useCurrentDecisionRequest();
+  const measures = useServerMeasures();
+  const measureList =
+    !measures || measures instanceof TalerError || measures.type === "fail"
+      ? []
+      : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
+
+  const initValue: MeasureForm = !request.next_measure
+    ? { paths: [] }
+    : { paths: request.next_measure.map((steps) => ({ steps })) };
+
+  const design = measureForm(i18n, measureList);
+  const form = useForm<MeasureForm>(design, initValue);
+
+  onComponentUnload(() => {
+    const r = !form.status.result.paths
+      ? []
+      : (form.status.result.paths.map(
+          (path) => path?.steps ?? [],
+        ) as string[][]);
+    updateRequest("next_measure", r);
+    updateRequest("custom_measures", {});
+  });
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+      <ShowMeasuresToSelect />
+    </div>
+  );
 }
 
 /**
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx 
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 48e9a1e82..8cde09808 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -256,7 +256,7 @@ export function CaseDetails({
               custom_events: undefined,
               inhibit_events: undefined,
               justification: undefined,
-              keep_investigating: undefined,
+              keep_investigating: false,
               next_measure: undefined,
               properties: undefined,
               rules: undefined,
@@ -1554,7 +1554,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
   },
 ];
 
-function ShowMeasuresToSelect({
+export function ShowMeasuresToSelect({
   onSelect,
 }: {
   onSelect?: (m: MeasureInfo) => void;
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx 
b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index d1bafb0a4..bf4beb62a 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -72,13 +72,13 @@ const createAccountForm = (i18n: InternationalizationAPI): 
FormDesign => ({
   fields: [
     {
       id: "password" as UIHandlerId,
-      type: "text",
+      type: "secret",
       label: i18n.str`Password`,
       required: true,
     },
     {
       id: "repeat" as UIHandlerId,
-      type: "text",
+      type: "secret",
       label: i18n.str`Repeat password`,
       required: true,
     },
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
index 95b4911a0..93e34bbaf 100644
--- a/packages/taler-util/src/time.ts
+++ b/packages/taler-util/src/time.ts
@@ -298,6 +298,34 @@ export namespace Duration {
     return { d_ms };
   }
 
+  export function toSpec({ d_ms }: Duration):
+    | {
+        seconds: number;
+        minutes: number;
+        hours: number;
+        days: number;
+      }
+    | undefined {
+    if (d_ms === "forever") return undefined;
+    let time = d_ms;
+    const millis = d_ms % SECONDS;
+    time -= millis;
+    const s = time % MINUTES;
+    time -= s;
+    const m = time % HOURS;
+    time -= m;
+    const h = time % DAYS;
+    time -= h;
+    const d = time;
+
+    return {
+      seconds: s / SECONDS,
+      minutes: m / MINUTES,
+      hours: h / HOURS,
+      days: d / DAYS,
+    };
+  }
+
   export function getForever(): Duration {
     return { d_ms: "forever" };
   }
@@ -605,7 +633,9 @@ export function durationAdd(d1: Duration, d2: Duration): 
Duration {
 export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
   decode(x: any, c?: Context): AbsoluteTime {
     if (x === undefined) {
-      throw Error(`got undefined and expected absolute time at 
${renderContext(c)}`);
+      throw Error(
+        `got undefined and expected absolute time at ${renderContext(c)}`,
+      );
     }
     const t_ms = x.t_ms;
     if (typeof t_ms === "string") {
@@ -623,7 +653,9 @@ export const codecForTimestamp: 
Codec<TalerProtocolTimestamp> = {
   decode(x: any, c?: Context): TalerProtocolTimestamp {
     // Compatibility, should be removed soon.
     if (x === undefined) {
-      throw Error(`got undefined and expected timestamp at 
${renderContext(c)}`);
+      throw Error(
+        `got undefined and expected timestamp at ${renderContext(c)}`,
+      );
     }
     const t_ms = x.t_ms;
     if (typeof t_ms === "string") {
@@ -676,3 +708,19 @@ export const codecForDuration: 
Codec<TalerProtocolDuration> = {
     throw Error(`expected duration at ${renderContext(c)}`);
   },
 };
+
+export const codecForDurationMs: Codec<Duration> = {
+  decode(x: any, c?: Context): Duration {
+    const d_ms = x.d_ms;
+    if (typeof d_ms === "string") {
+      if (d_ms === "forever") {
+        return { d_ms: "forever" };
+      }
+      throw Error(`expected duration at ${renderContext(c)}`);
+    }
+    if (typeof d_ms === "number") {
+      return { d_ms };
+    }
+    throw Error(`expected duration at ${renderContext(c)}`);
+  },
+};
diff --git a/packages/taler-util/src/types-taler-common.ts 
b/packages/taler-util/src/types-taler-common.ts
index 174626b02..ee7be5176 100644
--- a/packages/taler-util/src/types-taler-common.ts
+++ b/packages/taler-util/src/types-taler-common.ts
@@ -229,6 +229,7 @@ export interface TalerCommonConfigResponse {
 export const codecForTalerCommonConfigResponse =
   (): Codec<TalerCommonConfigResponse> =>
     buildCodecForObject<TalerCommonConfigResponse>()
+      .allowExtra()
       .property("name", codecForString())
       .property("version", codecForString())
       .build("TalerCommonConfigResponse");
diff --git a/packages/taler-util/src/types-taler-exchange.ts 
b/packages/taler-util/src/types-taler-exchange.ts
index be61218ba..409e33ee7 100644
--- a/packages/taler-util/src/types-taler-exchange.ts
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -1632,6 +1632,13 @@ export interface ExchangeVersionResponse {
 
   // Names of supported KYC requirements.
   supported_kyc_requirements: string[];
+
+  // Bank-specific dialect for the AML SPA. Determines
+  // which set of forms is available as well as statistics
+  // to show and sets of properties/events to trigger in
+  // AML decisions.
+  // @since protocol **v24**.
+  aml_spa_dialect?: string;
 }
 
 export interface WireAccount {
@@ -2208,6 +2215,28 @@ export interface ExchangeKeysResponse {
   // normalized JSON-object of field extensions, if it was set.
   // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS.
   extensions_sig?: EddsaSignature;
+
+  // Set to true if this exchange has KYC enabled and thus
+  // requires KYC auth wire transfers prior to a first deposit.
+  // @since in protocol **v24**.
+  kyc_enabled: boolean;
+
+  // Set to true if this exchange allows the use
+  // of reserves for rewards.
+  // @deprecated in protocol **v18**.
+  // rewards_allowed: false;
+
+  // Shopping URL where users may find shops that accept
+  // digital cash issued by this exchange.
+  // @since protocol **v21**.
+  shopping_url?: string;
+
+  // Small(est?) amount that can likely be transferred to
+  // the exchange. Should be the default amount for KYC
+  // authentication wire transfers to this exchange.
+  // Optional, not present if not known or not configured.
+  // @since protocol **v21**.
+  tiny_amount?: AmountString;
 }
 
 export interface ZeroLimitedOperation {
@@ -2422,6 +2451,9 @@ export const codecForExchangeConfig = (): 
Codec<ExchangeVersionResponse> =>
     .property("currency", codecForString())
     .property("currency_specification", codecForCurrencySpecificiation())
     .property("supported_kyc_requirements", codecForList(codecForString()))
+    .property("aml_spa_dialect", codecOptional(codecForString()))
+    .deprecatedProperty("shopping_url")
+    .deprecatedProperty("wallet_balance_limit_without_kyc")
     .build("TalerExchangeApi.ExchangeVersionResponse");
 
 // FIXME: complete the codec to check for valid exchange response
@@ -2453,6 +2485,10 @@ export const codecForExchangeKeys = (): 
Codec<ExchangeKeysResponse> =>
     .property("wads", codecForAny())
     .property("wallet_balance_limit_without_kyc", codecForAny())
     .property("wire_fees", codecForAny())
+    .property("kyc_enabled", codecForBoolean())
+    .property("shopping_url", codecOptional(codecForString()))
+    .property("tiny_amount", codecOptional(codecForAmountString()))
+    .deprecatedProperty("rewards_allowed")
     .build("TalerExchangeApi.ExchangeKeysResponse");
 
 export const codecForEventCounter = (): Codec<EventCounter> =>
diff --git a/packages/web-util/src/forms/field-types.ts 
b/packages/web-util/src/forms/field-types.ts
index 91c4b2539..34696b430 100644
--- a/packages/web-util/src/forms/field-types.ts
+++ b/packages/web-util/src/forms/field-types.ts
@@ -9,6 +9,8 @@ import { InputChoiceStacked } from 
"./fields/InputChoiceStacked.js";
 import { InputFile } from "./fields/InputFile.js";
 import { InputInteger } from "./fields/InputInteger.js";
 import { InputSelectMultiple } from "./fields/InputSelectMultiple.js";
+import { InputDuration } from "./fields/InputDuration.js";
+import { InputSecret } from "./fields/InputSecret.js";
 import { InputSelectOne } from "./fields/InputSelectOne.js";
 import { InputText } from "./fields/InputText.js";
 import { InputTextArea } from "./fields/InputTextArea.js";
@@ -33,8 +35,10 @@ type FieldType<T extends object = any, K extends keyof T = 
any> = {
   choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
   absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0];
   integer: Parameters<typeof InputInteger<T, K>>[0];
+  secret: Parameters<typeof InputSecret<T, K>>[0];
   toggle: Parameters<typeof InputToggle<T, K>>[0];
   amount: Parameters<typeof InputAmount<T, K>>[0];
+  duration: Parameters<typeof InputDuration<T, K>>[0];
 };
 
 /**
@@ -64,10 +68,15 @@ export type UIFormField =
       properties: FieldType["choiceHorizontal"];
     }
   | { type: "integer"; properties: FieldType["integer"] }
+  | { type: "secret"; properties: FieldType["secret"] }
   | { type: "toggle"; properties: FieldType["toggle"] }
   | {
       type: "absoluteTimeText";
       properties: FieldType["absoluteTimeText"];
+    }
+  | {
+      type: "duration";
+      properties: FieldType["duration"];
     };
 
 export type FieldComponentFunction<key extends keyof FieldType> = (
@@ -99,6 +108,7 @@ export const UIFormConfiguration: UIFormFieldMap = {
   //@ts-ignore
   choiceHorizontal: InputChoiceHorizontal,
   integer: InputInteger,
+  secret: InputSecret,
   //@ts-ignore
   selectOne: InputSelectOne,
   //@ts-ignore
@@ -107,4 +117,5 @@ export const UIFormConfiguration: UIFormFieldMap = {
   toggle: InputToggle,
   //@ts-ignore
   amount: InputAmount,
+  duration: InputDuration,
 };
diff --git a/packages/web-util/src/forms/fields/InputArray.stories.tsx 
b/packages/web-util/src/forms/fields/InputArray.stories.tsx
index a20c98756..a4dfaf417 100644
--- a/packages/web-util/src/forms/fields/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputArray.stories.tsx
@@ -135,3 +135,39 @@ export const NonMixingProperties = 
tests.createExample(TestedComponent, {
   initial: initial2,
   design: design2,
 });
+
+const initial3: any = {
+  list: [{ steps: ["asd"] }],
+};
+
+const design3: FormDesign = {
+  type: "single-column",
+  fields: [
+    {
+      type: "array",
+      id: "list" as UIHandlerId,
+      label: `Paths`,
+      help: `For every entry the customer will have a different path to satify 
checks.`,
+      labelFieldId: "steps" as UIHandlerId,
+      fields: [
+        {
+          type: "selectMultiple",
+          choices: ["asd", "qwe", "zxc"].map((m) => {
+            return {
+              value: m,
+              label: m,
+            };
+          }),
+          id: "steps" as UIHandlerId,
+          label: `Steps`,
+          help: `The checks that the customer will need to satisfy for this 
path.`,
+        },
+      ],
+    },
+  ],
+};
+
+export const ArrayOfSelect = tests.createExample(TestedComponent, {
+  initial: initial3,
+  design: design3,
+});
diff --git a/packages/web-util/src/forms/fields/InputArray.tsx 
b/packages/web-util/src/forms/fields/InputArray.tsx
index b0cfd80bc..abb495aa5 100644
--- a/packages/web-util/src/forms/fields/InputArray.tsx
+++ b/packages/web-util/src/forms/fields/InputArray.tsx
@@ -1,7 +1,11 @@
 import { TranslatedString } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
-import { getValueFromPath, useForm } from "../../hooks/useForm.js";
+import {
+  getValueFromPath,
+  RecursivePartial,
+  useForm,
+} from "../../hooks/useForm.js";
 import {
   SingleColumnFormSectionUI,
   useTranslationContext,
@@ -83,88 +87,38 @@ export function noHandlerPropsAndNoContextForField(
   );
 }
 
-// function getRequiredFields(fields: UIFormField[]): Array<UIHandlerId> {
-//   const shape: Array<UIHandlerId> = [];
-//   fields.forEach((field) => {
-//     if ("name" in field.properties) {
-//       // FIXME: this should be a validation when loading the form
-//       // consistency check
-//       if (shape.indexOf(field.properties.name) !== -1) {
-//         throw Error(`already present: ${field.properties.name}`);
-//       }
-//       if (!field.properties.required) {
-//         return;
-//       }
-//       shape.push(field.properties.name);
-//     } else if (field.type === "group") {
-//       Array.prototype.push.apply(
-//         shape,
-//         getRequiredFields(field.properties.fields),
-//       );
-//     }
-//   });
-//   return shape;
-// }
+type FormType = {};
 
-function getRequiredFields(fields: UIFormElementConfig[]): Array<UIHandlerId> {
-  const shape: Array<UIHandlerId> = [];
-  fields.forEach((field) => {
-    if ("id" in field) {
-      // FIXME: this should be a validation when loading the form
-      // consistency check
-      if (shape.indexOf(field.id) !== -1) {
-        throw Error(`already present: ${field.id}`);
-      }
-      if (!field.required) {
-        return;
-      }
-      shape.push(field.id);
-    } else if (field.type === "group") {
-      Array.prototype.push.apply(shape, getRequiredFields(field.fields));
-    }
-  });
-  return shape;
-}
+function ArrayForm({
+  fields,
+  selected,
+  onChange,
+}: {
+  fields: UIFormElementConfig[];
+  selected: Record<string, string | undefined> | undefined;
+  onChange: (r: RecursivePartial<FormType>) => void;
+}): VNode {
+  const form = useForm<FormType>(
+    {
+      type: "single-column",
+      fields,
+    },
+    selected ?? {},
+  );
 
-// function getShapeFromFields(fields: UIFormField[]): Array<UIHandlerId> {
-//   const shape: Array<UIHandlerId> = [];
-//   fields.forEach((field) => {
-//     if ("name" in field.properties) {
-//       // FIXME: this should be a validation when loading the form
-//       // consistency check
-//       if (shape.indexOf(field.properties.name) !== -1) {
-//         throw Error(`already present: ${field.properties.name}`);
-//       }
-//       shape.push(field.properties.name);
-//     } else if (field.type === "group") {
-//       Array.prototype.push.apply(
-//         shape,
-//         getShapeFromFields(field.properties.fields),
-//       );
-//     }
-//   });
-//   return shape;
-// }
+  useEffect(() => {
+    onChange(form.status.result);
+  }, [form.status.result]);
 
-function getShapeFromFields(fields: UIFormElementConfig[]): Array<UIHandlerId> 
{
-  const shape: Array<UIHandlerId> = [];
-  fields.forEach((field) => {
-    if ("id" in field) {
-      // FIXME: this should be a validation when loading the form
-      // consistency check
-      if (shape.indexOf(field.id) !== -1) {
-        throw Error(`already present: ${field.id}`);
-      }
-      shape.push(field.id);
-    } else if (field.type === "group") {
-      Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
-    }
-  });
-  return shape;
+  return (
+    <div class="px-4 py-6">
+      <div class="grid grid-cols-1 gap-y-8 ">
+        <SingleColumnFormSectionUI fields={fields} handler={form.handler} />
+      </div>
+    </div>
+  );
 }
 
-type FormType = {};
-
 export function InputArray<T extends object, K extends keyof T>(
   props: {
     fields: UIFormElementConfig[];
@@ -183,20 +137,6 @@ export function InputArray<T extends object, K extends 
keyof T>(
   const selected =
     selectedIndex === undefined ? undefined : list[selectedIndex];
 
-  const form = useForm<FormType>(
-    {
-      type: "single-column",
-      fields,
-    },
-    selected ?? {},
-  );
-
-  useEffect(() => {
-    if (selectedIndex === undefined) return;
-    const newValue = [...list];
-    newValue.splice(selectedIndex, 1, form.status.result);
-    onChange(newValue as any);
-  }, [form.status.result, selectedIndex]);
   const { i18n } = useTranslationContext();
 
   return (
@@ -210,8 +150,11 @@ export function InputArray<T extends object, K extends 
keyof T>(
       <div class="overflow-hidden ring-1 ring-gray-900/5 rounded-xl p-4">
         <div class="-space-y-px rounded-md bg-white ">
           {list.map((v, idx) => {
-            const label =
+            const labelValue =
               getValueFromPath(v, labelField.split(".")) ?? "<<incomplete>>";
+            const label = Array.isArray(labelValue)
+              ? labelValue.join(", ")
+              : labelValue;
             return (
               <Option
                 label={label as TranslatedString}
@@ -246,51 +189,15 @@ export function InputArray<T extends object, K extends 
keyof T>(
           )}
         </div>
         {selectedIndex !== undefined && (
-          /**
-           * This form provider act as a substate of the parent form
-           * Consider creating an InnerFormProvider since not every feature is 
expected
-           */
-          // <FormProvider
-          //   initial={selected ?? {}}
-          //   readOnly={state.disabled}
-          //   computeFormState={(v) => {
-          //     // current state is ignored
-          //     // the state is defined by the parent form
-
-          //     // elements should be present in the state object since this 
is expected to be an array
-          //     //@ts-ignore
-          //     // return state.elements[selectedIndex];
-          //     return {};
-          //   }}
-          //   onSubmit={(v) => {
-          //     const newValue = [...list];
-          //     newValue.splice(selectedIndex, 1, v);
-          //     onChange(newValue as any);
-          //     setSelectedIndex(undefined);
-          //   }}
-          //   onUpdate={(v) => {
-          //     const newValue = [...list];
-          //     newValue.splice(selectedIndex, 1, v);
-          //     onChange(newValue as any);
-          //   }}
-          // >
-          <div class="px-4 py-6">
-            <div class="grid grid-cols-1 gap-y-8 ">
-              <SingleColumnFormSectionUI
-                fields={fields}
-                handler={form.handler}
-              />
-              {/* <RenderAllFieldsByUiConfig
-                fields={convertUiField(
-                  i18n,
-                  fields,
-                  form.handler,
-                  getConverterById,
-                )}
-              /> */}
-            </div>
-          </div>
-          // </FormProvider>
+          <ArrayForm
+            fields={fields}
+            onChange={(result) => {
+              const newValue = [...list];
+              newValue.splice(selectedIndex, 1, result);
+              onChange(newValue as any);
+            }}
+            selected={selected}
+          />
         )}
         {selectedIndex !== undefined && (
           <div class="flex items-center justify-end gap-x-6">
diff --git a/packages/web-util/src/forms/fields/InputInteger.stories.tsx 
b/packages/web-util/src/forms/fields/InputDuration.stories.tsx
similarity index 80%
copy from packages/web-util/src/forms/fields/InputInteger.stories.tsx
copy to packages/web-util/src/forms/fields/InputDuration.stories.tsx
index 0a2bcaca0..8c0983287 100644
--- a/packages/web-util/src/forms/fields/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputDuration.stories.tsx
@@ -19,20 +19,25 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { TranslatedString } from "@gnu-taler/taler-util";
+import { Duration, TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../../tests/hook.js";
-import { DefaultForm as TestedComponent } from "../forms-ui.js";
 import { FormDesign, UIHandlerId } from "../forms-types.js";
+import { DefaultForm as TestedComponent } from "../forms-ui.js";
 
 export default {
-  title: "Input Integer",
+  title: "Input duration",
 };
 
 type TargetObject = {
-  age: number;
+  time: Duration;
 };
 const initial: TargetObject = {
-  age: 5,
+  time: Duration.fromSpec({
+    days: 1,
+    hours: 2,
+    minutes: 3,
+    seconds: 4,
+  }),
 };
 
 const design: FormDesign = {
@@ -42,10 +47,9 @@ const design: FormDesign = {
       title: "this is a simple form" as TranslatedString,
       fields: [
         {
-          type: "integer",
-          label: "Age" as TranslatedString,
-          id: "age" as UIHandlerId,
-          tooltip: "just numbers" as TranslatedString,
+          type: "duration",
+          label: "How long?" as TranslatedString,
+          id: "time" as UIHandlerId,
         },
       ],
     },
diff --git a/packages/web-util/src/forms/fields/InputDuration.tsx 
b/packages/web-util/src/forms/fields/InputDuration.tsx
new file mode 100644
index 000000000..9c368bdf3
--- /dev/null
+++ b/packages/web-util/src/forms/fields/InputDuration.tsx
@@ -0,0 +1,183 @@
+import { Fragment, VNode, h } from "preact";
+import { useTranslationContext } from "../../index.browser.js";
+import { UIFormProps } from "../FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
+import { InputWrapper } from "./InputLine.js";
+import { Duration } from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+
+export function InputDuration<T extends object, K extends keyof T>(
+  props: UIFormProps<T, K>,
+): VNode {
+  const { name, placeholder, before, after, converter, disabled } = props;
+  const { i18n } = useTranslationContext();
+  const { value, onChange, state, error } =
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
+
+  const sd = !value ? undefined : Duration.toSpec(value as Duration);
+  const [days, setDays] = useState(sd?.days ?? 0);
+  const [hours, setHours] = useState(sd?.hours ?? 0);
+  const [minutes, setMinutes] = useState(sd?.minutes ?? 0);
+  const [seconds, setSeconds] = useState(sd?.seconds ?? 0);
+
+  useEffect(() => {
+    onChange(
+      Duration.fromSpec({
+        days,
+        hours,
+        minutes,
+        seconds,
+      }),
+    );
+  }, [days, hours, minutes, seconds]);
+  const fromString: (s: string) => any =
+    converter?.fromStringUI ?? defaultFromString;
+  const toString: (s: any) => string = converter?.toStringUI ?? 
defaultToString;
+
+  if (state.hidden) return <div />;
+
+  let clazz =
+    "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset 
focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 
disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 
disabled:ring-gray-200";
+  // if (before) {
+  //   switch (before.type) {
+  //     case "icon": {
+  //       clazz += " pl-10";
+  //       break;
+  //     }
+  //     case "button": {
+  //       clazz += " rounded-none rounded-r-md ";
+  //       break;
+  //     }
+  //     case "text": {
+  clazz += " min-w-0 flex-1 rounded-r-md rounded-none ";
+  //       break;
+  //     }
+  //   }
+  // }
+  if (after) {
+    switch (after.type) {
+      case "icon": {
+        clazz += " pr-10";
+        break;
+      }
+      case "button": {
+        clazz += " rounded-none rounded-l-md";
+        break;
+      }
+      case "text": {
+        clazz += " min-w-0 flex-1 rounded-l-md rounded-none ";
+        break;
+      }
+    }
+  }
+  const showError = value !== undefined && error;
+  if (showError) {
+    clazz +=
+      " text-red-900 ring-red-300  placeholder:text-red-300 
focus:ring-red-500";
+  } else {
+    clazz +=
+      " text-gray-900 ring-gray-300 placeholder:text-gray-400 
focus:ring-indigo-600";
+  }
+  return (
+    <InputWrapper<T, K>
+      {...props}
+      help={props.help ?? state.help}
+      disabled={disabled ?? false}
+      error={showError ? error : undefined}
+    >
+      <div class="flex flex-col gap-1">
+        <div class="flex">
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>days</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setDays(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.days) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>hours</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setHours(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.hours) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+        </div>
+        <div class="flex">
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>minutes</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setMinutes(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.minutes) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>seconds</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setSeconds(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.seconds) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+        </div>
+      </div>
+    </InputWrapper>
+  );
+}
+
+function defaultToString(v: unknown) {
+  return v === undefined ? "" : typeof v !== "object" ? String(v) : "";
+}
+function defaultFromString(v: string) {
+  return v;
+}
diff --git a/packages/web-util/src/forms/fields/InputLine.tsx 
b/packages/web-util/src/forms/fields/InputLine.tsx
index 982fd4670..bbbc871e0 100644
--- a/packages/web-util/src/forms/fields/InputLine.tsx
+++ b/packages/web-util/src/forms/fields/InputLine.tsx
@@ -104,7 +104,7 @@ export function RenderAddon({
   }
 }
 
-function InputWrapper<T extends object, K extends keyof T>({
+export function InputWrapper<T extends object, K extends keyof T>({
   children,
   label,
   tooltip,
diff --git a/packages/web-util/src/forms/fields/InputInteger.stories.tsx 
b/packages/web-util/src/forms/fields/InputSecret.stories.tsx
similarity index 86%
copy from packages/web-util/src/forms/fields/InputInteger.stories.tsx
copy to packages/web-util/src/forms/fields/InputSecret.stories.tsx
index 0a2bcaca0..5179b6fbb 100644
--- a/packages/web-util/src/forms/fields/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputSecret.stories.tsx
@@ -25,14 +25,14 @@ import { DefaultForm as TestedComponent } from 
"../forms-ui.js";
 import { FormDesign, UIHandlerId } from "../forms-types.js";
 
 export default {
-  title: "Input Integer",
+  title: "Input secret",
 };
 
 type TargetObject = {
-  age: number;
+  pwd: string;
 };
 const initial: TargetObject = {
-  age: 5,
+  pwd: "5",
 };
 
 const design: FormDesign = {
@@ -42,10 +42,9 @@ const design: FormDesign = {
       title: "this is a simple form" as TranslatedString,
       fields: [
         {
-          type: "integer",
-          label: "Age" as TranslatedString,
-          id: "age" as UIHandlerId,
-          tooltip: "just numbers" as TranslatedString,
+          type: "secret",
+          label: "Password" as TranslatedString,
+          id: "pwd" as UIHandlerId,
         },
       ],
     },
diff --git a/packages/web-util/src/forms/fields/InputText.tsx 
b/packages/web-util/src/forms/fields/InputSecret.tsx
similarity index 59%
copy from packages/web-util/src/forms/fields/InputText.tsx
copy to packages/web-util/src/forms/fields/InputSecret.tsx
index 80c9b844e..730cb0880 100644
--- a/packages/web-util/src/forms/fields/InputText.tsx
+++ b/packages/web-util/src/forms/fields/InputSecret.tsx
@@ -1,9 +1,9 @@
 import { VNode, h } from "preact";
-import { UIFormProps } from "../FormProvider.js";
 import { InputLine } from "./InputLine.js";
+import { UIFormProps } from "../FormProvider.js";
 
-export function InputText<T extends object, K extends keyof T>(
+export function InputSecret<T extends object, K extends keyof T>(
   props: UIFormProps<T, K>,
 ): VNode {
-  return <InputLine type="text" {...props} />;
+  return <InputLine type="password" {...props} />;
 }
diff --git a/packages/web-util/src/forms/fields/InputSelectMultiple.tsx 
b/packages/web-util/src/forms/fields/InputSelectMultiple.tsx
index ef74e28c2..d7e8f9032 100644
--- a/packages/web-util/src/forms/fields/InputSelectMultiple.tsx
+++ b/packages/web-util/src/forms/fields/InputSelectMultiple.tsx
@@ -4,6 +4,7 @@ import { UIFormProps } from "../FormProvider.js";
 import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
 import { ChoiceS } from "./InputChoiceStacked.js";
 import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { useTranslationContext } from "../../index.browser.js";
 
 export function InputSelectMultiple<T extends object, K extends keyof T>(
   props: {
@@ -22,6 +23,7 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
     unique,
     max,
   } = props;
+  const { i18n } = useTranslationContext();
   const { value, onChange, state } =
     props.handler ?? noHandlerPropsAndNoContextForField(props.name);
 
@@ -39,7 +41,9 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
     filter === undefined
       ? undefined
       : choices.filter((v) => {
-          return regex.test(v.label);
+          const match = regex.test(v.label);
+          if (!unique) return match;
+          return match && list.indexOf(v.value as string) === -1;
         });
   return (
     <div class="sm:col-span-6">
@@ -116,7 +120,20 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
             </svg>
           </button>
 
-          {filteredChoices !== undefined && (
+          {!filter ? undefined : filteredChoices === undefined ||
+            !filteredChoices.length ? (
+            <ul
+              class="absolute z-10 mt-1 max-h-60 w-full overflow-auto 
rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 
focus:outline-none sm:text-sm"
+              id="options"
+              role="listbox"
+            >
+              <li class="relative cursor-pointer select-none py-2 pl-3 pr-9 
text-gray-900 hover:text-white hover:bg-indigo-600">
+                <span class="block truncate">
+                  <i18n.Translate>No element found</i18n.Translate>
+                </span>
+              </li>
+            </ul>
+          ) : (
             <ul
               class="absolute z-10 mt-1 max-h-60 w-full overflow-auto 
rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 
focus:outline-none sm:text-sm"
               id="options"
@@ -138,31 +155,15 @@ export function InputSelectMultiple<T extends object, K 
extends keyof T>(
                         return;
                       }
                       const newValue = [...list];
-                      newValue.splice(0, 0, v.value as string);
+                      newValue.push(v.value as string);
+                      // newValue.splice(0, 0, v.value as string);
                       onChange(newValue as any);
                     }}
-
-                    // tabindex="-1"
                   >
-                    {/* <!-- Selected: "font-semibold" --> */}
                     <span class="block truncate">{v.label}</span>
-
-                    {/* <!--
-          Checkmark, only display for selected option.
-
-          Active: "text-white", Not Active: "text-indigo-600"
-        --> */}
                   </li>
                 );
               })}
-
-              {/* <!--
-        Combobox option, manage highlight styles based on 
mouseenter/mouseleave and keyboard navigation.
-
-        Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
-      --> */}
-
-              {/* <!-- More items... --> */}
             </ul>
           )}
         </div>
diff --git a/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx 
b/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx
index 5c9dfe04f..b36889d2f 100644
--- a/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx
@@ -76,3 +76,40 @@ export const SimpleComment = 
tests.createExample(TestedComponent, {
   initial,
   design,
 });
+
+const design2: FormDesign = {
+  type: "double-column",
+  sections: [
+    {
+      title: "this is a simple form" as TranslatedString,
+      fields: [
+        {
+          type: "selectOne",
+          label: "label of the field" as TranslatedString,
+          id: "things" as UIHandlerId,
+          required: true,
+          placeholder: "search..." as TranslatedString,
+          choices: [
+            {
+              label: "one label" as TranslatedString,
+              value: "one",
+            },
+            {
+              label: "two label" as TranslatedString,
+              value: "two",
+            },
+            {
+              label: "five label" as TranslatedString,
+              value: "five",
+            },
+          ],
+        },
+      ],
+    },
+  ],
+};
+
+export const SimpleRequired = tests.createExample(TestedComponent, {
+  initial,
+  design: design2,
+});
diff --git a/packages/web-util/src/forms/fields/InputText.stories.tsx 
b/packages/web-util/src/forms/fields/InputText.stories.tsx
index 092e8fa81..1eb7275e7 100644
--- a/packages/web-util/src/forms/fields/InputText.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputText.stories.tsx
@@ -51,6 +51,7 @@ const design: FormDesign = {
           type: "text",
           label: "label of the field" as TranslatedString,
           id: "comment" as UIHandlerId,
+          required: true,
         },
       ],
     },
diff --git a/packages/web-util/src/forms/fields/InputToggle.stories.tsx 
b/packages/web-util/src/forms/fields/InputToggle.stories.tsx
index 6a2f30c80..7e856e583 100644
--- a/packages/web-util/src/forms/fields/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputToggle.stories.tsx
@@ -64,6 +64,7 @@ export const WithThreeState = 
tests.createExample(TestedComponent, {
         type: "toggle",
         label: "do you accept?" as TranslatedString,
         threeState: true,
+        required: true,
         id: "accept" as UIHandlerId,
       },
     ],
diff --git a/packages/web-util/src/forms/fields/InputToggle.tsx 
b/packages/web-util/src/forms/fields/InputToggle.tsx
index f22ffe1e7..ee20ec9f6 100644
--- a/packages/web-util/src/forms/fields/InputToggle.tsx
+++ b/packages/web-util/src/forms/fields/InputToggle.tsx
@@ -24,7 +24,6 @@ export function InputToggle<T extends object, K extends keyof 
T>(
   const isOn = !!value;
   return (
     <div class="sm:col-span-6">
-      v = {JSON.stringify({ value, isOn })}
       <div class="flex items-center justify-between">
         <LabelWithTooltipMaybeRequired
           label={label}
@@ -33,7 +32,13 @@ export function InputToggle<T extends object, K extends 
keyof T>(
         />
         <button
           type="button"
-          data-state={isOn ? "on" : value === undefined ? "undefined" : "off"}
+          data-state={
+            isOn
+              ? "on"
+              : threeState && value === undefined
+                ? "undefined"
+                : "off"
+          }
           class="bg-indigo-600 data-[state=off]:bg-gray-200 
data-[state=undefined]:bg-gray-200  relative inline-flex h-6 w-12 flex-shrink-0 
cursor-pointer rounded-full border-2 border-transparent transition-colors 
duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 
focus:ring-offset-2"
           role="switch"
           aria-labelledby="availability-label"
@@ -47,7 +52,13 @@ export function InputToggle<T extends object, K extends 
keyof T>(
           }}
         >
           <span
-            data-state={isOn ? "on" : value === undefined ? "undefined" : 
"off"}
+            data-state={
+              isOn
+                ? "on"
+                : threeState && value === undefined
+                  ? "undefined"
+                  : "off"
+            }
             class="translate-x-6 data-[state=off]:translate-x-0 
data-[state=undefined]:translate-x-3 pointer-events-none inline-block h-5 w-5 
transform rounded-full bg-white shadow ring-0 transition duration-200 
ease-in-out"
           ></span>
         </button>
diff --git a/packages/web-util/src/forms/forms-types.ts 
b/packages/web-util/src/forms/forms-types.ts
index e604ce274..0da107e34 100644
--- a/packages/web-util/src/forms/forms-types.ts
+++ b/packages/web-util/src/forms/forms-types.ts
@@ -17,7 +17,9 @@ import {
   TranslatedString,
 } from "@gnu-taler/taler-util";
 
-export type FormDesign = DoubleColumnFormDesign | SingleColumnFormDesign;
+export type FormDesign<T = unknown> =
+  | DoubleColumnFormDesign
+  | SingleColumnFormDesign;
 
 /**
  * form with composed by multiple sections
@@ -54,7 +56,9 @@ export type UIFormElementConfig =
   | UIFormFieldChoiseStacked
   | UIFormFieldFile
   | UIFormFieldInteger
+  | UIFormFieldSecret
   | UIFormFieldSelectMultiple
+  | UIFormFieldDuration
   | UIFormFieldSelectOne
   | UIFormFieldText
   | UIFormFieldTextArea
@@ -124,6 +128,10 @@ type UIFormFieldInteger = {
   min?: Integer;
 } & UIFormFieldBaseConfig;
 
+type UIFormFieldSecret = {
+  type: "secret";
+} & UIFormFieldBaseConfig;
+
 export interface SelectUiChoice {
   label: string;
   description?: string;
@@ -139,6 +147,10 @@ type UIFormFieldSelectMultiple = {
   allowFreeForm?: boolean;
 } & UIFormFieldBaseConfig;
 
+type UIFormFieldDuration = {
+  type: "duration";
+} & UIFormFieldBaseConfig;
+
 type UIFormFieldSelectOne = {
   type: "selectOne";
   choices: Array<SelectUiChoice>;
@@ -310,6 +322,16 @@ const codecForUiFormFieldInteger = (): 
Codec<UIFormFieldInteger> =>
     .property("min", codecOptional(codecForNumber()))
     .build("UIFormFieldInteger");
 
+const codecForUiFormFieldSecret = (): Codec<UIFormFieldSecret> =>
+  codecForUIFormFieldBaseConfigTemplate<UIFormFieldSecret>()
+    .property("type", codecForConstString("secret"))
+    .build("UIFormFieldSecret");
+
+const codecForUiFormFieldDuration = (): Codec<UIFormFieldDuration> =>
+  codecForUIFormFieldBaseConfigTemplate<UIFormFieldDuration>()
+    .property("type", codecForConstString("duration"))
+    .build("UiFormFieldDuration");
+
 const codecForUiFormFieldSelectMultiple =
   (): Codec<UIFormFieldSelectMultiple> =>
     codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>()
@@ -358,7 +380,9 @@ const codecForUiFormField = (): Codec<UIFormElementConfig> 
=>
     .alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
     .alternative("file", codecForUiFormFieldFile())
     .alternative("integer", codecForUiFormFieldInteger())
+    .alternative("secret", codecForUiFormFieldSecret())
     .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
+    .alternative("duration", codecForUiFormFieldDuration())
     .alternative("selectOne", codecForUiFormFieldSelectOne())
     .alternative("text", codecForUiFormFieldText())
     .alternative("textArea", codecForUiFormFieldTextArea())
diff --git a/packages/web-util/src/forms/forms-ui.tsx 
b/packages/web-util/src/forms/forms-ui.tsx
index 55ee29cb3..3db07ea92 100644
--- a/packages/web-util/src/forms/forms-ui.tsx
+++ b/packages/web-util/src/forms/forms-ui.tsx
@@ -26,9 +26,18 @@ export function DefaultForm<T>({
   return (
     <div>
       <FormUI design={design} handler={handler} />
-      <pre class="break-all whitespace-pre-wrap">
-        {JSON.stringify({ status }, undefined, 2)}
-      </pre>
+      {status.status === "ok" ? (
+        <pre class="break-all whitespace-pre-wrap">
+          {JSON.stringify(status.result ?? {}, undefined, 2)}
+        </pre>
+      ) : (
+        <Fragment>
+          <h1>form validation </h1>
+          <pre class="break-all whitespace-pre-wrap bg-red-200 border 
border-red-500 w-max p-4">
+            {JSON.stringify(status.errors, undefined, 2)}
+          </pre>
+        </Fragment>
+      )}
     </div>
   );
 }
diff --git a/packages/web-util/src/forms/forms-utils.ts 
b/packages/web-util/src/forms/forms-utils.ts
index 5322913cf..dc7d7d22f 100644
--- a/packages/web-util/src/forms/forms-utils.ts
+++ b/packages/web-util/src/forms/forms-utils.ts
@@ -177,6 +177,19 @@ export function convertFormConfigToUiField(
           },
         } as UIFormField;
       }
+      case "secret": {
+        return {
+          type: "secret",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
       case "selectMultiple": {
         return {
           type: "selectMultiple",
@@ -232,6 +245,19 @@ export function convertFormConfigToUiField(
           },
         } as UIFormField;
       }
+      case "duration": {
+        return {
+          type: "duration",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
       case "toggle": {
         return {
           type: "toggle",
diff --git a/packages/web-util/src/forms/gana/taler_form_attributes.ts 
b/packages/web-util/src/forms/gana/taler_form_attributes.ts
index e634ab702..7fcd63975 100644
--- a/packages/web-util/src/forms/gana/taler_form_attributes.ts
+++ b/packages/web-util/src/forms/gana/taler_form_attributes.ts
@@ -1319,4 +1319,55 @@ export namespace TalerFormAttributes {
     TAX_IS_USA_LAW?: Boolean;
   }
   export interface GLS_BusinessRepresentative {}
+  export interface AccountProperties_Testing {
+    /**
+     * Business domain of the account owner.
+     * Required: false
+     */
+    ACCOUNT_BUSINESS_DOMAIN?: String;
+    /**
+     * True if this is a politically exposed account.
+     * Required: false
+     */
+    ACCOUNT_PEP?: Boolean;
+  }
+  export interface AccountProperties_TOPS {
+    /**
+     * Business domain of the account owner.
+     * Required: false
+     */
+    ACCOUNT_BUSINESS_DOMAIN?: String;
+    /**
+     * Is the client's account currently frozen?
+     * Required: false
+     */
+    ACCOUNT_FROZEN?: Boolean;
+    /**
+     * True if this is a high-risk account.
+     * Required: false
+     */
+    ACCOUNT_HIGH_RISK?: Boolean;
+    /**
+     * True if this is a politically exposed account.
+     * Required: false
+     */
+    ACCOUNT_PEP?: Boolean;
+    /**
+     * Was the client's account reported to the authorities?
+     * Required: false
+     */
+    ACCOUNT_REPORTED?: Boolean;
+    /**
+     * True if this is a sanctioned account.
+     * Required: false
+     */
+    ACCOUNT_SANCTIONED?: Boolean;
+  }
+  export interface AccountProperties_GLS {
+    /**
+     * Was the client's account reported to the authorities?
+     * Required: false
+     */
+    ACCOUNT_REPORTED?: Boolean;
+  }
 }
diff --git a/packages/web-util/src/forms/index.stories.ts 
b/packages/web-util/src/forms/index.stories.ts
index cdccff050..5b62c512a 100644
--- a/packages/web-util/src/forms/index.stories.ts
+++ b/packages/web-util/src/forms/index.stories.ts
@@ -10,3 +10,5 @@ export * as a10 from "./fields/InputSelectOne.stories.js";
 export * as a11 from "./fields/InputText.stories.js";
 export * as a12 from "./fields/InputTextArea.stories.js";
 export * as a13 from "./fields/InputToggle.stories.js";
+export * as a14 from "./fields/InputSecret.stories.js";
+export * as a15 from "./fields/InputDuration.stories.js";
diff --git a/packages/web-util/src/forms/index.ts 
b/packages/web-util/src/forms/index.ts
index 1ae882ab9..8908166a9 100644
--- a/packages/web-util/src/forms/index.ts
+++ b/packages/web-util/src/forms/index.ts
@@ -25,6 +25,7 @@ export * from "./fields/InputLine.js";
 export * from "./fields/InputSelectMultiple.js";
 export * from "./fields/InputSelectOne.js";
 export * from "./fields/InputText.js";
+export * from "./fields/InputSecret.js";
 export * from "./fields/InputTextArea.js";
 export * from "./fields/InputToggle.js";
 export * from "./TimePicker.js";
diff --git a/packages/web-util/src/hooks/useForm.ts 
b/packages/web-util/src/hooks/useForm.ts
index a669f3007..b96387b6b 100644
--- a/packages/web-util/src/hooks/useForm.ts
+++ b/packages/web-util/src/hooks/useForm.ts
@@ -27,7 +27,7 @@ import {
   UIFormElementConfig,
   UIHandlerId,
 } from "@gnu-taler/web-util/browser";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
 
 export type FormHandler<T> = {
   [k in keyof T]?: T[k] extends string
@@ -110,7 +110,7 @@ function checkAllRequirements<T>(
  * @returns
  */
 export function useForm<T>(
-  design: FormDesign,
+  design: FormDesign<T>,
   initialValue: RecursivePartial<FormValues<T>>,
   check?: (f: RecursivePartial<FormValues<T>>) => FormErrors<T> | undefined,
 ): FormState<T> {

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