gnunet-svn
[Top][All Lists]
Advanced

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

[taler-typescript-core] branch master updated (ab605e408 -> 422aee9e9)


From: gnunet
Subject: [taler-typescript-core] branch master updated (ab605e408 -> 422aee9e9)
Date: Mon, 20 Jan 2025 06:20:31 +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 ab605e408 fix broken build
     new a85af3425 events impl
     new 231eeef55 new rules wizard
     new 4f1f0d96f limit operation type as enum
     new 422aee9e9 on component unload should be in web utils

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


Summary of changes:
 .../aml-backoffice-ui/src/ExchangeAmlFrame.tsx     |  23 +-
 packages/aml-backoffice-ui/src/Routing.tsx         |   2 +-
 .../src/hooks/decision-request.ts                  |  83 +--
 .../aml-backoffice-ui/src/hooks/preferences.ts     |  23 +-
 .../src/pages/AmlDecisionRequestWizard.tsx         | 770 ---------------------
 .../aml-backoffice-ui/src/pages/CaseDetails.tsx    | 287 +++++---
 .../pages/decision/AmlDecisionRequestWizard.tsx    | 298 ++++++++
 .../src/pages/decision/Events.tsx                  | 208 ++++++
 .../src/pages/decision/Justification.tsx           |  14 +
 .../src/pages/decision/Measures.tsx                | 121 ++++
 .../src/pages/decision/Properties.tsx              | 189 +++++
 .../aml-backoffice-ui/src/pages/decision/Rules.tsx | 206 ++++++
 packages/taler-util/src/types-taler-exchange.ts    |  57 +-
 packages/taler-util/src/types-taler-kyc-aml.ts     |   7 +-
 packages/web-util/src/components/ToastBanner.tsx   |  79 ++-
 packages/web-util/src/components/index.ts          |   1 +
 packages/web-util/src/components/utils.ts          |  26 +-
 packages/web-util/src/forms/fields/InputArray.tsx  |   1 -
 18 files changed, 1363 insertions(+), 1032 deletions(-)
 delete mode 100644 
packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
 create mode 100644 
packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
 create mode 100644 packages/aml-backoffice-ui/src/pages/decision/Events.tsx
 create mode 100644 
packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
 create mode 100644 packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
 create mode 100644 packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
 create mode 100644 packages/aml-backoffice-ui/src/pages/decision/Rules.tsx

diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx 
b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
index 78b9c2722..8b5911641 100644
--- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
+++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -112,15 +112,8 @@ const VERSION = typeof __VERSION__ !== "undefined" ? 
__VERSION__ : undefined;
  * ui props and state
  */
 
-export function ExchangeAmlFrame({
-  children,
-  officer,
-}: {
-  officer?: OfficerState;
-  children?: ComponentChildren;
-}): VNode {
+function useErrorReport() {
   const { i18n } = useTranslationContext();
-
   const [error] = useErrorBoundary();
 
   useEffect(() => {
@@ -137,6 +130,18 @@ export function ExchangeAmlFrame({
       // resetError()
     }
   }, [error]);
+}
+
+export function ExchangeAmlFrame({
+  children,
+  officer,
+}: {
+  officer?: OfficerState;
+  children?: ComponentChildren;
+}): VNode {
+  const { i18n } = useTranslationContext();
+
+  useErrorReport();
 
   const [preferences, updatePreferences] = usePreferences();
   const settings = useUiSettingsContext();
@@ -207,7 +212,7 @@ export function ExchangeAmlFrame({
 
       <div class="fixed z-20 w-full">
         <div class="mx-auto w-4/5">
-          <ToastBanner />
+          <ToastBanner debug={preferences.showDebugInfo} />
         </div>
       </div>
 
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx 
b/packages/aml-backoffice-ui/src/Routing.tsx
index 148ece344..1358eda50 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -41,7 +41,7 @@ import { Measures } from "./pages/Measures.js";
 import {
   AmlDecisionRequestWizard,
   WizardSteps,
-} from "./pages/AmlDecisionRequestWizard.js";
+} from "./pages/decision/AmlDecisionRequestWizard.js";
 import { useCurrentDecisionRequest } from "./hooks/decision-request.js";
 
 export function Routing(): VNode {
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts 
b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
index 132af8b1c..9b67b5654 100644
--- a/packages/aml-backoffice-ui/src/hooks/decision-request.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -16,16 +16,14 @@
 
 import {
   AbsoluteTime,
-  AmountJson,
   Codec,
-  Duration,
+  KycRule,
   MeasureInformation,
   buildCodecForObject,
   codecForAbsoluteTime,
-  codecForAmountJson,
   codecForAny,
   codecForBoolean,
-  codecForDurationMs,
+  codecForKycRules,
   codecForList,
   codecForMap,
   codecForMeasureInformation,
@@ -35,28 +33,8 @@ import {
 } from "@gnu-taler/taler-util";
 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: BalanceForm | undefined;
+  rules: KycRule[] | undefined;
   deadline: AbsoluteTime | undefined;
   properties: object | undefined;
   custom_properties: object | undefined;
@@ -64,50 +42,12 @@ export interface DecisionRequest {
   inhibit_events: string[] | undefined;
   keep_investigating: boolean;
   justification: string | undefined;
-  next_measure: string[][] | undefined;
-  custom_measures:
-    | {
-        [measure_name: string]: MeasureInformation;
-      }
-    | undefined;
+  new_measures: string | 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(codecForBalanceForm()))
+    .property("rules", codecOptional(codecForList(codecForKycRules())))
     .property("deadline", codecOptional(codecForAbsoluteTime))
     .property("properties", codecForAny())
     .property("custom_properties", codecForAny())
@@ -118,24 +58,16 @@ export const codecForDecisionRequest = (): 
Codec<DecisionRequest> =>
       "keep_investigating",
       codecOptionalDefault(codecForBoolean(), false),
     )
-    .property(
-      "next_measure",
-      codecOptional(codecForList(codecForList(codecForString()))),
-    )
-    .property(
-      "custom_measures",
-      codecOptional(codecForMap(codecForMeasureInformation())),
-    )
+    .property("new_measures", codecOptional(codecForString()))
     .build("DecisionRequest");
 
 const defaultDecisionRequest: DecisionRequest = {
-  custom_measures: undefined,
   deadline: undefined,
   custom_events: undefined,
   inhibit_events: undefined,
   justification: undefined,
   keep_investigating: false,
-  next_measure: undefined,
+  new_measures: undefined,
   properties: undefined,
   custom_properties: undefined,
   rules: undefined,
@@ -165,7 +97,6 @@ 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/preferences.ts 
b/packages/aml-backoffice-ui/src/hooks/preferences.ts
index d329cdbb2..76c5cf3d4 100644
--- a/packages/aml-backoffice-ui/src/hooks/preferences.ts
+++ b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -18,7 +18,7 @@ import {
   Codec,
   TranslatedString,
   buildCodecForObject,
-  codecForBoolean
+  codecForBoolean,
 } from "@gnu-taler/taler-util";
 import {
   buildStorageKey,
@@ -30,18 +30,21 @@ interface Preferences {
   showDebugInfo: boolean;
   allowInsecurePassword: boolean;
   keepSessionAfterReload: boolean;
+  testingDialect: boolean;
 }
 
 export const codecForPreferences = (): Codec<Preferences> =>
   buildCodecForObject<Preferences>()
-  .property("allowInsecurePassword", (codecForBoolean()))
-  .property("showDebugInfo", codecForBoolean())
-  .property("keepSessionAfterReload", (codecForBoolean()))
+    .property("allowInsecurePassword", codecForBoolean())
+    .property("showDebugInfo", codecForBoolean())
+    .property("testingDialect", codecForBoolean())
+    .property("keepSessionAfterReload", codecForBoolean())
     .build("Preferences");
 
 const defaultPreferences: Preferences = {
   allowInsecurePassword: false,
   showDebugInfo: false,
+  testingDialect: false,
   keepSessionAfterReload: false,
 };
 
@@ -75,6 +78,7 @@ export function getAllBooleanPreferences(): Array<keyof 
Preferences> {
     "showDebugInfo",
     "allowInsecurePassword",
     "keepSessionAfterReload",
+    "testingDialect",
   ];
 }
 
@@ -83,8 +87,13 @@ export function getLabelForPreferences(
   i18n: ReturnType<typeof useTranslationContext>["i18n"],
 ): TranslatedString {
   switch (k) {
-    case "showDebugInfo": return i18n.str`Show debug info`
-    case "allowInsecurePassword": return i18n.str`Allow Insecure password`
-    case "keepSessionAfterReload": return i18n.str`Keep session after reload`
+    case "showDebugInfo":
+      return i18n.str`Show debug info`;
+    case "testingDialect":
+      return i18n.str`Use testing dialect`;
+    case "allowInsecurePassword":
+      return i18n.str`Allow Insecure password`;
+    case "keepSessionAfterReload":
+      return i18n.str`Keep session after reload`;
   }
 }
diff --git a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx 
b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
deleted file mode 100644
index 045c8f681..000000000
--- a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-import {
-  AbsoluteTime,
-  assertUnreachable,
-  MeasureInformation,
-  TalerError,
-  TranslatedString,
-} from "@gnu-taler/taler-util";
-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 { 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
-  | "measures" // define a new form/challenge
-  | "properties" // define account information
-  | "events" // define events to trigger
-  | "justification"; // finalize, investigate?;
-
-const STEPS_ORDER: WizardSteps[] = [
-  "rules",
-  "measures",
-  "properties",
-  "events",
-  "justification",
-];
-
-const STEPS_ORDER_MAP = STEPS_ORDER.reduce(
-  (map, cur, idx, steps) => {
-    map[cur] = {
-      prev: idx === 0 ? undefined : steps[idx - 1],
-      next: idx === steps.length ? undefined : steps[idx + 1],
-    };
-    return map;
-  },
-  {} as {
-    [s in WizardSteps]: {
-      next: WizardSteps | undefined;
-      prev: WizardSteps | undefined;
-    };
-  },
-);
-
-function isRulesCompleted(request: DecisionRequest): boolean {
-  return request.rules !== undefined;
-}
-function isPropertiesCompleted(request: DecisionRequest): boolean {
-  return request.properties !== undefined;
-}
-function isEventsCompleted(request: DecisionRequest): boolean {
-  return request.custom_events !== undefined;
-}
-function isMeasuresCompleted(request: DecisionRequest): boolean {
-  return (
-    request.next_measure !== undefined && request.custom_measures !== undefined
-  );
-}
-function isJustificationCompleted(request: DecisionRequest): boolean {
-  return (
-    request.keep_investigating !== undefined &&
-    request.justification !== undefined
-  );
-}
-
-export function AmlDecisionRequestWizard({
-  account,
-  step,
-  onMove,
-}: {
-  account?: string;
-  step?: WizardSteps;
-  onMove: (n: WizardSteps | undefined) => void;
-}): VNode {
-  const { i18n } = useTranslationContext();
-  const stepOrDefault = step ?? "rules";
-  const content = (function () {
-    switch (stepOrDefault) {
-      case "rules":
-        return <Rules account={account} />;
-      case "properties":
-        return <Properties />;
-      case "events":
-        return <Events />;
-      case "measures":
-        return <Measures />;
-      case "justification":
-        return <Justification />;
-    }
-    assertUnreachable(stepOrDefault);
-  })();
-
-  return (
-    <div>
-      <WizardSteps step={stepOrDefault} onMove={onMove} />
-      <button
-        disabled={!STEPS_ORDER_MAP[stepOrDefault].prev}
-        onClick={() => {
-          onMove(STEPS_ORDER_MAP[stepOrDefault].prev);
-        }}
-        class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
disabled:bg-gray-500 bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
-      >
-        <i18n.Translate>Prev</i18n.Translate>
-      </button>
-      <button
-        disabled={!STEPS_ORDER_MAP[stepOrDefault].next}
-        onClick={() => {
-          onMove(STEPS_ORDER_MAP[stepOrDefault].next);
-        }}
-        class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
disabled:bg-gray-500 bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
-      >
-        <i18n.Translate>Next</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({ 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);
-  });
-
-  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
- * @returns
- */
-function Properties({}: {}): VNode {
-  const { i18n } = useTranslationContext();
-  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
- * @returns
- */
-function Events({}: {}): VNode {
-  const { i18n } = useTranslationContext();
-  const [request] = useCurrentDecisionRequest();
-  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
- * @returns
- */
-function Measures({}: {}): VNode {
-  const { i18n } = useTranslationContext();
-  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>
-  );
-}
-
-/**
- * Mark for further investigation and explain decision
- * @param param0
- * @returns
- */
-function Justification({}: {}): VNode {
-  const { i18n } = useTranslationContext();
-  const [request] = useCurrentDecisionRequest();
-  return <div> not yet impltemented: justification and investigation</div>;
-}
-
-function WizardSteps({
-  step: currentStep,
-  onMove,
-}: {
-  step: WizardSteps;
-  onMove: (n: WizardSteps | undefined) => void;
-}): VNode {
-  const [request] = useCurrentDecisionRequest();
-  const { i18n } = useTranslationContext();
-  const STEP_INFO: {
-    [s in WizardSteps]: {
-      label: TranslatedString;
-      description: TranslatedString;
-      isCompleted: (r: DecisionRequest) => boolean;
-    };
-  } = {
-    rules: {
-      label: i18n.str`Rules`,
-      description: i18n.str`Set the limit of the operations`,
-      isCompleted: isRulesCompleted,
-    },
-    events: {
-      label: i18n.str`Events`,
-      description: i18n.str`Trigger notifications about the account.`,
-      isCompleted: isEventsCompleted,
-    },
-    measures: {
-      label: i18n.str`Measures`,
-      description: i18n.str`Ask the customer to take action.`,
-      isCompleted: isMeasuresCompleted,
-    },
-    justification: {
-      label: i18n.str`Justification`,
-      description: i18n.str`Describe the decision.`,
-      isCompleted: isJustificationCompleted,
-    },
-    properties: {
-      label: i18n.str`Properties`,
-      description: i18n.str`Add information about the account.`,
-      isCompleted: isPropertiesCompleted,
-    },
-  };
-  return (
-    <div class="lg:border-b lg:border-t lg:border-gray-200">
-      <nav class="mx-auto max-w-7xl " aria-label="Progress">
-        <ol
-          role="list"
-          class="overflow-hidden rounded-md lg:flex lg:rounded-none 
lg:border-l lg:border-r lg:border-gray-200"
-        >
-          {STEPS_ORDER.map((stepLabel) => {
-            const info = STEP_INFO[stepLabel];
-            const st = info.isCompleted(request)
-              ? "completed"
-              : currentStep === stepLabel
-                ? "current"
-                : "incomplete";
-
-            const pos = !STEPS_ORDER_MAP[stepLabel].prev
-              ? "first"
-              : !STEPS_ORDER_MAP[stepLabel].next
-                ? "last"
-                : "middle";
-
-            return (
-              <li class="relative lg:flex-1">
-                <div
-                  data-pos={pos}
-                  class="overflow-hidden data-[pos=first]:rounded-t-md border 
data-[pos=first]:border-b-0 border-gray-200 lg:border-0"
-                >
-                  {currentStep === stepLabel ? (
-                    <span
-                      class="absolute left-0 top-0 h-full w-1 bg-indigo-600 
lg:bottom-0 lg:top-auto lg:h-1 lg:w-full"
-                      aria-hidden="true"
-                    ></span>
-                  ) : undefined}
-                  <button
-                    aria-current="step"
-                    class="group"
-                    onClick={() => {
-                      onMove(stepLabel);
-                    }}
-                  >
-                    <span
-                      data-status={st}
-                      class="absolute left-0 top-0 h-full w-1 
data-[status=current]:bg-indigo-600 data-[status=current]:bg-transparent 
group-hover:bg-gray-200  lg:bottom-0 lg:top-auto lg:h-1 lg:w-full"
-                      aria-hidden="true"
-                    ></span>
-                    <div>
-                      <span class="flex items-start px-4 pt-4 text-sm 
font-medium">
-                        <span class="shrink-0">
-                          <span
-                            data-status={st}
-                            class="flex size-6 items-center justify-center 
rounded-full data-[status=completed]:bg-indigo-600 border-2 
data-[status=current]:border-indigo-600 
data-[status=incomplete]:border-gray-300"
-                          >
-                            <svg
-                              class="size-4 text-white "
-                              viewBox="0 0 24 24"
-                              fill="currentColor"
-                              aria-hidden="true"
-                              data-slot="icon"
-                            >
-                              <path
-                                fill-rule="evenodd"
-                                d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 
13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 
8.493-12.74a.75.75 0 0 1 1.04-.207Z"
-                                clip-rule="evenodd"
-                              />
-                            </svg>
-                          </span>
-                        </span>
-                        <span
-                          data-status={st}
-                          class="ml-4 data-[status=current]:text-indigo-600"
-                        >
-                          {info.label}
-                        </span>
-                      </span>
-                    </div>
-                    <div class="p-2 text-start">
-                      <span class="ml-4 mt-0.5 flex min-w-0 flex-col">
-                        <span
-                          data-current={currentStep === stepLabel}
-                          class="text-sm font-medium 
data-[current=true]:text-indigo-600"
-                        ></span>
-                        <span class="text-sm font-medium text-gray-500">
-                          {info.description}
-                        </span>
-                      </span>
-                    </div>
-                  </button>
-                  {pos === "first" ? undefined : (
-                    <div
-                      data-pos={pos}
-                      class="absolute inset-0 left-0 top-0 hidden w-2 lg:block"
-                      aria-hidden="true"
-                    >
-                      <svg
-                        data-pos={pos}
-                        class="size-full text-gray-300 
data-[pos=middle]:h-full data-[pos=middle]:w-full"
-                        viewBox="0 0 12 82"
-                        fill="none"
-                        preserveAspectRatio="none"
-                      >
-                        <path
-                          d="M0.5 0V31L10.5 41L0.5 51V82"
-                          stroke="currentcolor"
-                          vector-effect="non-scaling-stroke"
-                        />
-                      </svg>
-                    </div>
-                  )}
-                </div>
-              </li>
-            );
-          })}
-        </ol>
-      </nav>
-    </div>
-  );
-}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx 
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index cc20f36e3..dd5aea232 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -27,6 +27,7 @@ import {
   CurrencySpecification,
   HttpStatusCode,
   KycRule,
+  LimitOperationType,
   OperationFail,
   OperationOk,
   TalerError,
@@ -251,14 +252,13 @@ export function CaseDetails({
         <button
           onClick={async () => {
             onNewDecision({
-              custom_measures: undefined,
               deadline: undefined,
               custom_properties: undefined,
               custom_events: undefined,
               inhibit_events: undefined,
               justification: undefined,
               keep_investigating: false,
-              next_measure: undefined,
+              new_measures: undefined,
               properties: undefined,
               rules: undefined,
             });
@@ -795,6 +795,169 @@ function ShowMesaureInfo({
   );
 }
 
+export function RulesInfo({
+  rules,
+  onEdit,
+  onRemove,
+}: {
+  rules: KycRule[];
+  onEdit?: (k: KycRule, idx: number) => void;
+  onRemove?: (k: KycRule, idx: number) => void;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const { config } = useExchangeApiContext();
+
+  if (!rules.length) {
+    return (
+      <Attention
+        title={i18n.str`There are no rules for operations`}
+        type="warning"
+      />
+    );
+  }
+
+  const balanceLimitIdx = rules.findIndex(
+    (r) => r.operation_type === "BALANCE",
+  );
+  const balanceLimit = rules[balanceLimitIdx];
+
+  const hasActions = !!onEdit || !!onRemove;
+
+  return (
+    <Fragment>
+      <div class="">
+        <div class="flex mt-2 rounded-md w-fit  shadow-sm border-0 ring-1 
ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+          <div class="whitespace-nowrap pointer-events-none bg-gray-200 
inset-y-0 items-center px-3 flex">
+            <i18n.Translate>Max balance</i18n.Translate>
+          </div>
+          <div class="p-2  disabled:bg-gray-200 text-right rounded-md 
rounded-l-none data-[left=true]:text-left py-1.5 pl-3 text-gray-900  
placeholder:text-gray-400  sm:text-sm sm:leading-6">
+            {!balanceLimit ? (
+              <i18n.Translate>Unlimited</i18n.Translate>
+            ) : (
+              <RenderAmount
+                value={Amounts.parseOrThrow(balanceLimit.threshold)}
+                spec={config.config.currency_specification}
+              />
+            )}
+          </div>
+        </div>
+      </div>
+      <div class="">
+        <table class="min-w-full divide-y divide-gray-300">
+          <thead class="bg-gray-50">
+            <tr>
+              <th
+                scope="col"
+                class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold 
text-gray-900 sm:pl-6"
+              >
+                <i18n.Translate>Operation</i18n.Translate>
+              </th>
+              <th
+                scope="col"
+                class="px-3 py-3.5 text-left text-sm font-semibold 
text-gray-900"
+              >
+                <i18n.Translate>Timeframe</i18n.Translate>
+              </th>
+              <th
+                scope="col"
+                class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
+              >
+                <i18n.Translate>Amount</i18n.Translate>
+              </th>
+              <th
+                scope="col"
+                class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
+              >
+                <i18n.Translate>Measures</i18n.Translate>
+              </th>
+              {!hasActions ? undefined : (
+                <th
+                  scope="col"
+                  class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
+                >
+                  <i18n.Translate>Actions</i18n.Translate>
+                </th>
+              )}
+            </tr>
+          </thead>
+          <tbody class="divide-y divide-gray-200">
+            {rules.map((r, idx) => {
+              if (r.operation_type === "BALANCE") return;
+              return (
+                <tr>
+                  <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm 
font-medium text-gray-900 sm:pl-6 text-left">
+                    {r.operation_type}
+                  </td>
+                  <td class="whitespace-nowrap px-3 py-4 text-sm 
text-gray-500">
+                    {r.timeframe.d_us === "forever" ? (
+                      <i18n.Translate>Forever</i18n.Translate>
+                    ) : (
+                      formatDuration(
+                        intervalToDuration({
+                          start: 0,
+                          end: r.timeframe.d_us / 1000,
+                        }),
+                      )
+                    )}
+                  </td>
+                  <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 
text-sm font-medium sm:pr-6 text-right">
+                    <RenderAmount
+                      value={Amounts.parseOrThrow(r.threshold)}
+                      spec={config.config.currency_specification}
+                    />
+                  </td>
+                  <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 
text-sm font-medium sm:pr-6 text-right">
+                    {r.measures}
+                  </td>
+                  {!hasActions ? undefined : (
+                    <td class="relative flex justify-end whitespace-nowrap 
py-4 pl-3 pr-4 text-sm font-medium sm:pr-6">
+                      {!onEdit ? undefined : (
+                        <button onClick={() => onEdit(r, idx)}>
+                          <svg
+                            xmlns="http://www.w3.org/2000/svg";
+                            fill="none"
+                            viewBox="0 0 24 24"
+                            stroke-width="1.5"
+                            stroke="currentColor"
+                            class="size-6 text-green-700"
+                          >
+                            <path
+                              stroke-linecap="round"
+                              stroke-linejoin="round"
+                              d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 
2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 
1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 
21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
+                            />
+                          </svg>
+                        </button>
+                      )}
+                      {!onRemove ? undefined : (
+                        <button onClick={() => onRemove(r, idx)}>
+                          <svg
+                            xmlns="http://www.w3.org/2000/svg";
+                            fill="none"
+                            viewBox="0 0 24 24"
+                            stroke-width="1.5"
+                            stroke="currentColor"
+                            class="size-6 text-red-700"
+                          >
+                            <path
+                              stroke-linecap="round"
+                              stroke-linejoin="round"
+                              d="m14.74 9-.346 9m-4.788 0L9.26 
9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 
1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 
48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 
0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 
0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+                            />
+                          </svg>
+                        </button>
+                      )}
+                    </td>
+                  )}
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
+    </Fragment>
+  );
+}
 export function ShowDecisionLimitInfo({
   rules,
   since,
@@ -811,7 +974,6 @@ export function ShowDecisionLimitInfo({
   fixed?: boolean;
 }): VNode {
   const { i18n } = useTranslationContext();
-  const { config } = useExchangeApiContext();
   const [opened, setOpened] = useState(startOpen ?? false);
 
   function Header() {
@@ -897,7 +1059,6 @@ export function ShowDecisionLimitInfo({
       </div>
     );
   }
-  const balanceLimit = rules.find((r) => r.operation_type === "BALANCE");
 
   return (
     <div class="overflow-hidden border border-gray-800 rounded-xl">
@@ -925,87 +1086,7 @@ export function ShowDecisionLimitInfo({
           </div>
         )}
 
-        <div class="">
-          <div class="flex mt-2 rounded-md w-fit  shadow-sm border-0 ring-1 
ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
-            <div class="whitespace-nowrap pointer-events-none bg-gray-200 
inset-y-0 items-center px-3 flex">
-              <i18n.Translate>Max balance</i18n.Translate>
-            </div>
-            <div class="p-2  disabled:bg-gray-200 text-right rounded-md 
rounded-l-none data-[left=true]:text-left py-1.5 pl-3 text-gray-900  
placeholder:text-gray-400  sm:text-sm sm:leading-6">
-              {!balanceLimit ? (
-                <i18n.Translate>Unlimited</i18n.Translate>
-              ) : (
-                <RenderAmount
-                  value={Amounts.parseOrThrow(balanceLimit.threshold)}
-                  spec={config.config.currency_specification}
-                />
-              )}
-            </div>
-          </div>
-        </div>
-
-        {!rules.length ? (
-          <Attention
-            title={i18n.str`There are no rules for operations`}
-            type="warning"
-          />
-        ) : (
-          <div class="">
-            <table class="min-w-full divide-y divide-gray-300">
-              <thead class="bg-gray-50">
-                <tr>
-                  <th
-                    scope="col"
-                    class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold 
text-gray-900 sm:pl-6"
-                  >
-                    <i18n.Translate>Operation</i18n.Translate>
-                  </th>
-                  <th
-                    scope="col"
-                    class="px-3 py-3.5 text-left text-sm font-semibold 
text-gray-900"
-                  >
-                    <i18n.Translate>Timeframe</i18n.Translate>
-                  </th>
-                  <th
-                    scope="col"
-                    class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right"
-                  >
-                    <i18n.Translate>Amount</i18n.Translate>
-                  </th>
-                </tr>
-              </thead>
-              <tbody class="divide-y divide-gray-200">
-                {rules.map((r) => {
-                  if (r.operation_type === "BALANCE") return;
-                  return (
-                    <tr>
-                      <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm 
font-medium text-gray-900 sm:pl-6 text-left">
-                        {r.operation_type}
-                      </td>
-                      <td class="whitespace-nowrap px-3 py-4 text-sm 
text-gray-500">
-                        {r.timeframe.d_us === "forever" ? (
-                          <i18n.Translate>Forever</i18n.Translate>
-                        ) : (
-                          formatDuration(
-                            intervalToDuration({
-                              start: 0,
-                              end: r.timeframe.d_us / 1000,
-                            }),
-                          )
-                        )}
-                      </td>
-                      <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 
text-right text-sm font-medium sm:pr-6 text-right">
-                        <RenderAmount
-                          value={Amounts.parseOrThrow(r.threshold)}
-                          spec={config.config.currency_specification}
-                        />
-                      </td>
-                    </tr>
-                  );
-                })}
-              </tbody>
-            </table>
-          </div>
-        )}
+        <RulesInfo rules={rules} />
       </div>
     </div>
   );
@@ -1346,7 +1427,7 @@ const THRESHOLD_2000_WEEK: (currency: string) => 
TalerExchangeApi.KycRule[] = (
   currency,
 ) => [
   {
-    operation_type: "WITHDRAW",
+    operation_type: LimitOperationType.withdraw,
     threshold: `${currency}:2000`,
     timeframe: {
       d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
@@ -1357,7 +1438,7 @@ const THRESHOLD_2000_WEEK: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "DEPOSIT",
+    operation_type: LimitOperationType.deposit,
     threshold: `${currency}:2000`,
     timeframe: {
       d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
@@ -1368,7 +1449,7 @@ const THRESHOLD_2000_WEEK: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "AGGREGATE",
+    operation_type: LimitOperationType.aggregate,
     threshold: `${currency}:2000`,
     timeframe: {
       d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
@@ -1379,7 +1460,7 @@ const THRESHOLD_2000_WEEK: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "MERGE",
+    operation_type: LimitOperationType.merge,
     threshold: `${currency}:2000`,
     timeframe: {
       d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
@@ -1390,7 +1471,7 @@ const THRESHOLD_2000_WEEK: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "BALANCE",
+    operation_type: LimitOperationType.balance,
     threshold: `${currency}:2000`,
     timeframe: {
       d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
@@ -1401,7 +1482,7 @@ const THRESHOLD_2000_WEEK: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "CLOSE",
+    operation_type: LimitOperationType.close,
     threshold: `${currency}:2000`,
     timeframe: {
       d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
@@ -1417,7 +1498,7 @@ const THRESHOLD_100_HOUR: (currency: string) => 
TalerExchangeApi.KycRule[] = (
   currency,
 ) => [
   {
-    operation_type: "WITHDRAW",
+    operation_type: LimitOperationType.withdraw,
     threshold: `${currency}:100`,
     timeframe: {
       d_us: 1 * 60 * 60 * 1000 * 1000,
@@ -1428,7 +1509,7 @@ const THRESHOLD_100_HOUR: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "DEPOSIT",
+    operation_type: LimitOperationType.deposit,
     threshold: `${currency}:100`,
     timeframe: {
       d_us: 1 * 60 * 60 * 1000 * 1000,
@@ -1439,7 +1520,7 @@ const THRESHOLD_100_HOUR: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "AGGREGATE",
+    operation_type: LimitOperationType.aggregate,
     threshold: `${currency}:100`,
     timeframe: {
       d_us: 1 * 60 * 60 * 1000 * 1000,
@@ -1450,7 +1531,7 @@ const THRESHOLD_100_HOUR: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "MERGE",
+    operation_type: LimitOperationType.merge,
     threshold: `${currency}:100`,
     timeframe: {
       d_us: 1 * 60 * 60 * 1000 * 1000,
@@ -1461,7 +1542,7 @@ const THRESHOLD_100_HOUR: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "BALANCE",
+    operation_type: LimitOperationType.balance,
     threshold: `${currency}:100`,
     timeframe: {
       d_us: 1 * 60 * 60 * 1000 * 1000,
@@ -1472,7 +1553,7 @@ const THRESHOLD_100_HOUR: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "CLOSE",
+    operation_type: LimitOperationType.close,
     threshold: `${currency}:100`,
     timeframe: {
       d_us: 1 * 60 * 60 * 1000 * 1000,
@@ -1488,7 +1569,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
   currency,
 ) => [
   {
-    operation_type: "WITHDRAW",
+    operation_type: LimitOperationType.withdraw,
     threshold: `${currency}:0`,
     timeframe: {
       d_us: "forever",
@@ -1499,7 +1580,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "DEPOSIT",
+    operation_type: LimitOperationType.deposit,
     threshold: `${currency}:0`,
     timeframe: {
       d_us: "forever",
@@ -1510,7 +1591,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "AGGREGATE",
+    operation_type: LimitOperationType.aggregate,
     threshold: `${currency}:0`,
     timeframe: {
       d_us: "forever",
@@ -1521,7 +1602,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "MERGE",
+    operation_type: LimitOperationType.merge,
     threshold: `${currency}:0`,
     timeframe: {
       d_us: "forever",
@@ -1532,7 +1613,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "BALANCE",
+    operation_type: LimitOperationType.balance,
     threshold: `${currency}:0`,
     timeframe: {
       d_us: "forever",
@@ -1543,7 +1624,7 @@ const FREEZE_RULES: (currency: string) => 
TalerExchangeApi.KycRule[] = (
     is_and_combinator: true,
   },
   {
-    operation_type: "CLOSE",
+    operation_type: LimitOperationType.close,
     threshold: `${currency}:0`,
     timeframe: {
       d_us: "forever",
diff --git 
a/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
new file mode 100644
index 000000000..3d6f8a1b4
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/AmlDecisionRequestWizard.tsx
@@ -0,0 +1,298 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+  assertUnreachable,
+  MeasureInformation,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+  FormDesign,
+  InternationalizationAPI,
+  UIHandlerId,
+  useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import {
+  DecisionRequest,
+  useCurrentDecisionRequest,
+} from "../../hooks/decision-request.js";
+import { Events } from "./Events.js";
+import { Properties } from "./Properties.js";
+import { Rules } from "./Rules.js";
+import { Measures } from "./Measures.js";
+import { Justification } from "./Justification.js";
+
+export type WizardSteps =
+  | "rules" // define the limits
+  | "measures" // define a new form/challenge
+  | "properties" // define account information
+  | "events" // define events to trigger
+  | "justification"; // finalize, investigate?;
+
+const STEPS_ORDER: WizardSteps[] = [
+  "rules",
+  "measures",
+  "properties",
+  "events",
+  "justification",
+];
+
+const STEPS_ORDER_MAP = STEPS_ORDER.reduce(
+  (map, cur, idx, steps) => {
+    map[cur] = {
+      prev: idx === 0 ? undefined : steps[idx - 1],
+      next: idx === steps.length ? undefined : steps[idx + 1],
+    };
+    return map;
+  },
+  {} as {
+    [s in WizardSteps]: {
+      next: WizardSteps | undefined;
+      prev: WizardSteps | undefined;
+    };
+  },
+);
+
+function isRulesCompleted(request: DecisionRequest): boolean {
+  return request.rules !== undefined;
+}
+function isPropertiesCompleted(request: DecisionRequest): boolean {
+  return request.properties !== undefined;
+}
+function isEventsCompleted(request: DecisionRequest): boolean {
+  return request.custom_events !== undefined;
+}
+function isMeasuresCompleted(request: DecisionRequest): boolean {
+  return request.new_measures !== undefined;
+}
+function isJustificationCompleted(request: DecisionRequest): boolean {
+  return (
+    request.keep_investigating !== undefined &&
+    request.justification !== undefined
+  );
+}
+
+export function AmlDecisionRequestWizard({
+  account,
+  step,
+  onMove,
+}: {
+  account?: string;
+  step?: WizardSteps;
+  onMove: (n: WizardSteps | undefined) => void;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const stepOrDefault = step ?? "rules";
+  const content = (function () {
+    switch (stepOrDefault) {
+      case "rules":
+        return <Rules account={account} />;
+      case "properties":
+        return <Properties />;
+      case "events":
+        return <Events />;
+      case "measures":
+        return <Measures />;
+      case "justification":
+        return <Justification />;
+    }
+    assertUnreachable(stepOrDefault);
+  })();
+
+  return (
+    <div>
+      <WizardSteps step={stepOrDefault} onMove={onMove} />
+      <button
+        disabled={!STEPS_ORDER_MAP[stepOrDefault].prev}
+        onClick={() => {
+          onMove(STEPS_ORDER_MAP[stepOrDefault].prev);
+        }}
+        class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
disabled:bg-gray-500 bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+      >
+        <i18n.Translate>Prev</i18n.Translate>
+      </button>
+      <button
+        disabled={!STEPS_ORDER_MAP[stepOrDefault].next}
+        onClick={() => {
+          onMove(STEPS_ORDER_MAP[stepOrDefault].next);
+        }}
+        class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm 
disabled:bg-gray-500 bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+      >
+        <i18n.Translate>Next</i18n.Translate>
+      </button>
+      {content}
+    </div>
+  );
+}
+function WizardSteps({
+  step: currentStep,
+  onMove,
+}: {
+  step: WizardSteps;
+  onMove: (n: WizardSteps | undefined) => void;
+}): VNode {
+  const [request] = useCurrentDecisionRequest();
+  const { i18n } = useTranslationContext();
+  const STEP_INFO: {
+    [s in WizardSteps]: {
+      label: TranslatedString;
+      description: TranslatedString;
+      isCompleted: (r: DecisionRequest) => boolean;
+    };
+  } = {
+    rules: {
+      label: i18n.str`Rules`,
+      description: i18n.str`Set the limit of the operations`,
+      isCompleted: isRulesCompleted,
+    },
+    events: {
+      label: i18n.str`Events`,
+      description: i18n.str`Trigger notifications about the account.`,
+      isCompleted: isEventsCompleted,
+    },
+    measures: {
+      label: i18n.str`Measures`,
+      description: i18n.str`Ask the customer to take action.`,
+      isCompleted: isMeasuresCompleted,
+    },
+    justification: {
+      label: i18n.str`Justification`,
+      description: i18n.str`Describe the decision.`,
+      isCompleted: isJustificationCompleted,
+    },
+    properties: {
+      label: i18n.str`Properties`,
+      description: i18n.str`Add information about the account.`,
+      isCompleted: isPropertiesCompleted,
+    },
+  };
+  return (
+    <div class="lg:border-b lg:border-t lg:border-gray-200">
+      <nav class="mx-auto max-w-7xl " aria-label="Progress">
+        <ol
+          role="list"
+          class="overflow-hidden rounded-md lg:flex lg:rounded-none 
lg:border-l lg:border-r lg:border-gray-200"
+        >
+          {STEPS_ORDER.map((stepLabel) => {
+            const info = STEP_INFO[stepLabel];
+            const st = info.isCompleted(request)
+              ? "completed"
+              : currentStep === stepLabel
+                ? "current"
+                : "incomplete";
+
+            const pos = !STEPS_ORDER_MAP[stepLabel].prev
+              ? "first"
+              : !STEPS_ORDER_MAP[stepLabel].next
+                ? "last"
+                : "middle";
+
+            return (
+              <li class="relative lg:flex-1">
+                <div
+                  data-pos={pos}
+                  class="overflow-hidden data-[pos=first]:rounded-t-md border 
data-[pos=first]:border-b-0 border-gray-200 lg:border-0"
+                >
+                  {currentStep === stepLabel ? (
+                    <span
+                      class="absolute left-0 top-0 h-full w-1 bg-indigo-600 
lg:bottom-0 lg:top-auto lg:h-1 lg:w-full"
+                      aria-hidden="true"
+                    ></span>
+                  ) : undefined}
+                  <button
+                    aria-current="step"
+                    class="group"
+                    onClick={() => {
+                      onMove(stepLabel);
+                    }}
+                  >
+                    <span
+                      data-status={st}
+                      class="absolute left-0 top-0 h-full w-1 
data-[status=current]:bg-indigo-600 data-[status=current]:bg-transparent 
group-hover:bg-gray-200  lg:bottom-0 lg:top-auto lg:h-1 lg:w-full"
+                      aria-hidden="true"
+                    ></span>
+                    <div>
+                      <span class="flex items-start px-4 pt-4 text-sm 
font-medium">
+                        <span class="shrink-0">
+                          <span
+                            data-status={st}
+                            class="flex size-6 items-center justify-center 
rounded-full data-[status=completed]:bg-indigo-600 border-2 
data-[status=current]:border-indigo-600 
data-[status=incomplete]:border-gray-300"
+                          >
+                            <svg
+                              class="size-4 text-white "
+                              viewBox="0 0 24 24"
+                              fill="currentColor"
+                              aria-hidden="true"
+                              data-slot="icon"
+                            >
+                              <path
+                                fill-rule="evenodd"
+                                d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 
13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 
8.493-12.74a.75.75 0 0 1 1.04-.207Z"
+                                clip-rule="evenodd"
+                              />
+                            </svg>
+                          </span>
+                        </span>
+                        <span
+                          data-status={st}
+                          class="ml-4 data-[status=current]:text-indigo-600"
+                        >
+                          {info.label}
+                        </span>
+                      </span>
+                    </div>
+                    <div class="p-2 text-start">
+                      <span class="ml-4 mt-0.5 flex min-w-0 flex-col">
+                        <span
+                          data-current={currentStep === stepLabel}
+                          class="text-sm font-medium 
data-[current=true]:text-indigo-600"
+                        ></span>
+                        <span class="text-sm font-medium text-gray-500">
+                          {info.description}
+                        </span>
+                      </span>
+                    </div>
+                  </button>
+                  {pos === "first" ? undefined : (
+                    <div
+                      data-pos={pos}
+                      class="absolute inset-0 left-0 top-0 hidden w-2 lg:block"
+                      aria-hidden="true"
+                    >
+                      <svg
+                        data-pos={pos}
+                        class="size-full text-gray-300 
data-[pos=middle]:h-full data-[pos=middle]:w-full"
+                        viewBox="0 0 12 82"
+                        fill="none"
+                        preserveAspectRatio="none"
+                      >
+                        <path
+                          d="M0.5 0V31L10.5 41L0.5 51V82"
+                          stroke="currentcolor"
+                          vector-effect="non-scaling-stroke"
+                        />
+                      </svg>
+                    </div>
+                  )}
+                </div>
+              </li>
+            );
+          })}
+        </ol>
+      </nav>
+    </div>
+  );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Events.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
new file mode 100644
index 000000000..e65586a77
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/Events.tsx
@@ -0,0 +1,208 @@
+import {
+  useTranslationContext,
+  useExchangeApiContext,
+  useForm,
+  FormUI,
+  TalerFormAttributes,
+  UIHandlerId,
+  InternationalizationAPI,
+  UIFormElementConfig,
+  FormDesign,
+  onComponentUnload,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import { usePreferences } from "../../hooks/preferences.js";
+import { MeasureInformation } from "@gnu-taler/taler-util";
+
+/**
+ * Trigger additional events
+ * @param param0
+ * @returns
+ */
+export function Events({}: {}): VNode {
+  const { i18n } = useTranslationContext();
+  const [request, _, updateRequest] = useCurrentDecisionRequest();
+  const [pref] = usePreferences();
+  const { config } = useExchangeApiContext();
+
+  const calculatedProps = {
+    ...(request.properties ?? {}),
+    ...(request.custom_properties ?? {}),
+  };
+
+  const calculatedEvents = eventsByDialect(
+    i18n,
+    pref.testingDialect ? "testing" : config.config.aml_spa_dialect,
+    calculatedProps,
+  );
+
+  const design = eventsForm(i18n, calculatedEvents);
+
+  const form = useForm<EventsForm>(design, {
+    inhibit: calculatedEvents.reduce(
+      (prev, cur) => {
+        if (cur.type !== "toggle") return prev;
+        const isInhibit =
+          request.inhibit_events !== undefined &&
+          request.inhibit_events.indexOf(cur.id) !== -1;
+        prev[cur.id] = isInhibit;
+        return prev;
+      },
+      {} as EventsForm["inhibit"],
+    ),
+    trigger: !request.custom_events
+      ? []
+      : request.custom_events.map((name) => ({ name })),
+  });
+
+  onComponentUnload(() => {
+    updateRequest({
+      ...request,
+      custom_events: !form.status.result.trigger
+        ? []
+        : form.status.result.trigger.map((t) => t?.name!),
+      inhibit_events: Object.entries(form.status.result.inhibit ?? {})
+        .filter(([key, inhibit]) => !!inhibit)
+        .map(([key]) => key),
+    });
+  });
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+    </div>
+  );
+}
+
+export type EventsForm = {
+  trigger: { name: string }[];
+  inhibit: { [name: string]: boolean };
+};
+
+export const eventsForm = (
+  i18n: InternationalizationAPI,
+  defaultEvents: UIFormElementConfig[],
+): FormDesign<MeasureInformation> => ({
+  type: "double-column",
+  sections: [
+    {
+      title: i18n.str`Inhibit default events`,
+      description: i18n.str`Use this form to prevent events to be triggered by 
the current status.`,
+      fields: !defaultEvents.length
+        ? [
+            {
+              type: "caption",
+              label: i18n.str`No default events calculated.`,
+            },
+          ]
+        : defaultEvents.map((f) =>
+            "id" in f ? { ...f, id: ("inhibit." + f.id) as UIHandlerId } : f,
+          ),
+    },
+    {
+      title: i18n.str`Custom event`,
+      description: i18n.str`Add more events to be triggered by this request.`,
+      fields: [
+        {
+          id: "trigger" as UIHandlerId,
+          type: "array",
+          label: i18n.str`Event list`,
+          labelFieldId: "name" as UIHandlerId,
+          fields: [
+            {
+              type: "text",
+              label: i18n.str`Name`,
+              id: "name" as UIHandlerId,
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  // fields: [
+  //   {
+  //     id: "trigger" as UIHandlerId,
+  //     type: "array",
+  //     labelFieldId: "name" as UIHandlerId,
+  //     label: i18n.str`Trigger`,
+  //     fields: [],
+  //   },
+  //   {
+  //     id: "inhibit" as UIHandlerId,
+  //     type: "array",
+  //     labelFieldId: "name" as UIHandlerId,
+  //     label: i18n.str`Inhibit`,
+  //     fields: [],
+  //   },
+  // ],
+});
+
+export function eventsByDialect(
+  i18n: InternationalizationAPI,
+  dialect: string | undefined,
+  properties: object,
+): UIFormElementConfig[] {
+  if (!dialect) return [];
+  const result: UIFormElementConfig[] = [];
+  switch (dialect) {
+    case "testing": {
+      const props = properties as TalerFormAttributes.AccountProperties_TOPS;
+      if (props.ACCOUNT_FROZEN) {
+        result.push({
+          id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is froozen?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        });
+      }
+      if (props.ACCOUNT_SANCTIONED) {
+        result.push({
+          id: "ACCOUNT_SANCTIONED" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is sacntioned?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        });
+      }
+      if (props.ACCOUNT_HIGH_RISK) {
+        result.push({
+          id: "ACCOUNT_HIGH_RISK" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is high risk?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        });
+      }
+      break;
+    }
+    case "gls": {
+      const props = properties as TalerFormAttributes.AccountProperties_TOPS;
+      if (props.ACCOUNT_FROZEN) {
+        result.push({
+          id: "ACCOUNT_FROZEN" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is frozen?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        });
+      }
+      break;
+    }
+    case "tops": {
+      const props = properties as TalerFormAttributes.AccountProperties_TOPS;
+      if (props.ACCOUNT_HIGH_RISK) {
+        result.push({
+          id: "ACCOUNT_HIGH_RISK" satisfies keyof 
TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+          label: i18n.str`Is high risk?`,
+          // gana_type: "Boolean",
+          type: "toggle",
+          required: true,
+        });
+      }
+      break;
+    }
+  }
+  return result;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
new file mode 100644
index 000000000..32c7110b2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/Justification.tsx
@@ -0,0 +1,14 @@
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+
+/**
+ * Mark for further investigation and explain decision
+ * @param param0
+ * @returns
+ */
+export function Justification({}: {}): VNode {
+  const { i18n } = useTranslationContext();
+  const [request] = useCurrentDecisionRequest();
+  return <div> not yet impltemented: justification and investigation</div>;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
new file mode 100644
index 000000000..1ef4d8859
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -0,0 +1,121 @@
+import { MeasureInformation, TalerError } from "@gnu-taler/taler-util";
+import {
+  useTranslationContext,
+  useForm,
+  onComponentUnload,
+  FormUI,
+  InternationalizationAPI,
+  FormDesign,
+  UIHandlerId,
+  UIFormElementConfig,
+  RecursivePartial,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useServerMeasures } from "../../hooks/account.js";
+import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import { ShowMeasuresToSelect } from "../CaseDetails.js";
+
+export function serializeMeasures(
+  paths?: RecursivePartial<MeasurePath[]>,
+): string {
+  if (!paths) return "";
+  return paths
+    .map((p) => {
+      if (!p?.steps) return "";
+      return p.steps.join("+");
+    })
+    .join(" ");
+}
+export function deserializeMeasures(
+  measures: string | undefined,
+): MeasurePath[] {
+  if (!measures) return [];
+  return measures.split(" ").map((path) => ({ steps: path.split("+") }));
+}
+
+/**
+ * Ask for more information, define new paths to proceed
+ * @param param0
+ * @returns
+ */
+export function Measures({}: {}): VNode {
+  const { i18n } = useTranslationContext();
+  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.new_measures
+    ? { paths: [] }
+    : { paths: deserializeMeasures(request.new_measures) };
+
+  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({
+      ...request,
+      new_measures: serializeMeasures(
+        (form.status.result.paths ?? []) as MeasurePath[],
+      ),
+    });
+  });
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+      <ShowMeasuresToSelect />
+    </div>
+  );
+}
+
+type MeasurePath = { steps: string[] };
+
+type MeasureForm = {
+  paths: MeasurePath[];
+};
+
+export function measureArrayField(
+  i18n: InternationalizationAPI,
+  mi: (MeasureInformation & { id: string })[],
+): UIFormElementConfig {
+  return {
+    type: "array",
+    id: "paths" as UIHandlerId,
+    label: i18n.str`Measures`,
+    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.`,
+      },
+    ],
+  };
+}
+
+function measureForm(
+  i18n: InternationalizationAPI,
+  mi: (MeasureInformation & { id: string })[],
+): FormDesign<MeasureForm> {
+  return {
+    type: "single-column",
+    fields: [measureArrayField(i18n, mi)],
+  };
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
new file mode 100644
index 000000000..ef685e857
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx
@@ -0,0 +1,189 @@
+import {
+  useTranslationContext,
+  useExchangeApiContext,
+  useForm,
+  onComponentUnload,
+  FormUI,
+  InternationalizationAPI,
+  UIFormElementConfig,
+  UIHandlerId,
+  TalerFormAttributes,
+  FormDesign,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import { usePreferences } from "../../hooks/preferences.js";
+
+/**
+ * Update account properites
+ * @param param0
+ * @returns
+ */
+export function Properties({}: {}): VNode {
+  const { i18n } = useTranslationContext();
+  const [request, _, updateRequest] = useCurrentDecisionRequest();
+  const { config } = useExchangeApiContext();
+  const [pref] = usePreferences();
+  const design = propertiesForm(
+    i18n,
+    propertiesByDialect(
+      i18n,
+      pref.testingDialect ? "testing" : config.config.aml_spa_dialect,
+    ),
+  );
+
+  const form = useForm<PropertiesForm>(design, {
+    defined: request.properties,
+    custom: Object.entries(request.custom_properties ?? {}).map(
+      ([name, value]) => {
+        return { name, value };
+      },
+    ),
+  });
+
+  onComponentUnload(() => {
+    updateRequest({
+      ...request,
+      properties: form.status.result.defined ?? {},
+      custom_properties: (form.status.result.custom ?? []).reduce(
+        (prev, cur) => {
+          if (!cur || !cur.name || !cur.value) return prev;
+          console.log(cur);
+          prev[cur.name] = cur.value;
+          return prev;
+        },
+        {} as Record<string, string>,
+      ),
+    });
+  });
+
+  return (
+    <div>
+      <FormUI design={design} handler={form.handler} />
+    </div>
+  );
+}
+
+export type PropertiesForm = {
+  defined: { [name: string]: string };
+  custom: { name: string; value: string }[];
+};
+
+export const propertiesForm = (
+  i18n: InternationalizationAPI,
+  props: UIFormElementConfig[],
+): FormDesign<PropertiesForm> => ({
+  type: "double-column",
+  sections: [
+    {
+      title: i18n.str`Properties`,
+      description: i18n.str`Default properties are defined by the server 
dialect`,
+      fields: props.map((f) =>
+        "id" in f ? { ...f, id: ("defined." + f.id) as UIHandlerId } : f,
+      ),
+    },
+    {
+      title: i18n.str`Custom properties`,
+      description: i18n.str`Add more properties that not listed above.`,
+      fields: [
+        {
+          id: "custom" as UIHandlerId,
+          type: "array",
+          label: i18n.str`New properties`,
+          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,
+            },
+          ],
+        },
+      ],
+    },
+  ],
+});
+
+export function propertiesByDialect(
+  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 [];
+    }
+  }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx 
b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
new file mode 100644
index 000000000..db1653e02
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/decision/Rules.tsx
@@ -0,0 +1,206 @@
+import {
+  AbsoluteTime,
+  AmountJson,
+  Amounts,
+  Duration,
+  KycRule,
+  LimitOperationType,
+  MeasureInformation,
+  TalerError,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+  FormDesign,
+  FormUI,
+  InternationalizationAPI,
+  onComponentUnload,
+  UIHandlerId,
+  useExchangeApiContext,
+  useForm,
+  useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useServerMeasures } from "../../hooks/account.js";
+import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
+import { useAccountActiveDecision } from "../../hooks/decisions.js";
+import { RulesInfo, ShowDecisionLimitInfo } from "../CaseDetails.js";
+import { measureArrayField, serializeMeasures } from "./Measures.js";
+
+/**
+ * Defined new limits for the account
+ * @param param0
+ * @returns
+ */
+export function Rules({ account }: { account?: string }): VNode {
+  const activeDecision = useAccountActiveDecision(account);
+
+  const { i18n } = useTranslationContext();
+  const { config } = useExchangeApiContext();
+  const [request, updateRequest] = useCurrentDecisionRequest();
+  const measures = useServerMeasures();
+  // const [rules, setRules] = useState<KycRule[]>([]);
+  const measureList =
+    !measures || measures instanceof TalerError || measures.type === "fail"
+      ? []
+      : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi }));
+  const design = formDesign(i18n, config.config.currency, measureList);
+
+  const form = useForm<FormType>(design, {});
+
+  const info =
+    !activeDecision ||
+    activeDecision instanceof TalerError ||
+    activeDecision.type === "fail"
+      ? undefined
+      : activeDecision.body;
+
+  function addNewRule(nr: FormType) {
+    const result = !request.rules ? [] : [...request.rules];
+    result.push({
+      timeframe: Duration.toTalerProtocolDuration(nr.timeframe),
+      threshold: Amounts.stringify(nr.threshold),
+      operation_type: nr.operation_type,
+      display_priority: 1,
+      measures: [serializeMeasures(nr.paths)], // FIXME: change how server 
expect new measures
+    });
+    updateRequest("rules", result);
+  }
+
+  return (
+    <div>
+      <h2 class="mt-4 mb-2">
+        <i18n.Translate>Add a new rule</i18n.Translate>
+      </h2>
+
+      <FormUI design={design} handler={form.handler} />
+
+      <button
+        disabled={form.status.status === "fail"}
+        onClick={() => {
+          console.log(form);
+          addNewRule(form.status.result as FormType);
+        }}
+        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 disabled:bg-gray-600"
+      >
+        <i18n.Translate>Add</i18n.Translate>
+      </button>
+
+      <h2 class="mt-4 mb-2">
+        <i18n.Translate>New rules</i18n.Translate>
+      </h2>
+
+      <RulesInfo
+        rules={request.rules ?? []}
+        // onEdit={(r, idx) => {
+        //   const nr = !request.rules ? [] : [...request.rules];
+        //   nr.splice(idx, 1);
+        //   updateRequest("rules", nr);
+        // }}
+        onRemove={(r, idx) => {
+          const nr = !request.rules ? [] : [...request.rules];
+          nr.splice(idx, 1);
+          updateRequest("rules", nr);
+        }}
+      />
+
+      {!info ? undefined : (
+        <div>
+          <h2 class="mt-4 mb-2">
+            <i18n.Translate>Current rules</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 FormType = {
+  operation_type: LimitOperationType;
+  threshold: AmountJson;
+  timeframe: Duration;
+  exposed: boolean;
+  paths: { steps: Array<string> }[];
+};
+
+// operation_type: LimitOperationType;
+// threshold: AmountString;
+// timeframe: RelativeTime;
+// measures: string[];
+// display_priority: Integer;
+// exposed?: boolean;
+// is_and_combinator?: boolean;
+function labelForOperationType(
+  op: LimitOperationType,
+  i18n: InternationalizationAPI,
+): TranslatedString {
+  switch (op) {
+    case LimitOperationType.withdraw:
+      return i18n.ctx("operation type")`Withdraw`;
+    case LimitOperationType.deposit:
+      return i18n.ctx("operation type")`Deposit`;
+    case LimitOperationType.merge:
+      return i18n.ctx("operation type")`Merge`;
+    case LimitOperationType.aggregate:
+      return i18n.ctx("operation type")`Aggregate`;
+    case LimitOperationType.balance:
+      return i18n.ctx("operation type")`Balance`;
+    case LimitOperationType.refund:
+      return i18n.ctx("operation type")`Refund`;
+    case LimitOperationType.close:
+      return i18n.ctx("operation type")`Close`;
+    case LimitOperationType.transaction:
+      return i18n.ctx("operation type")`Transaction`;
+  }
+}
+
+const formDesign = (
+  i18n: InternationalizationAPI,
+  currency: string,
+  mi: (MeasureInformation & { id: string })[],
+): FormDesign<KycRule> => ({
+  type: "single-column",
+  fields: [
+    {
+      id: "operation_type" as UIHandlerId,
+      type: "choiceHorizontal",
+      label: i18n.str`Operation type`,
+      required: true,
+      choices: Object.values(LimitOperationType).map((op) => {
+        return {
+          value: op,
+          label: labelForOperationType(op, i18n),
+        };
+      }),
+    },
+    {
+      id: "threshold" as UIHandlerId,
+      type: "amount",
+      required: true,
+      label: i18n.str`Threshold`,
+      currency,
+    },
+    {
+      id: "timeframe" as UIHandlerId,
+      type: "duration",
+      required: true,
+      label: i18n.str`Timeframe`,
+    },
+    {
+      id: "exposed" as UIHandlerId,
+      type: "toggle",
+      label: i18n.str`Exposed`,
+      help: i18n.str`Is the customer aware of this limit?`,
+    },
+    measureArrayField(i18n, mi),
+  ],
+});
diff --git a/packages/taler-util/src/types-taler-exchange.ts 
b/packages/taler-util/src/types-taler-exchange.ts
index 409e33ee7..ee3519655 100644
--- a/packages/taler-util/src/types-taler-exchange.ts
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -1510,7 +1510,7 @@ export interface MeasureInformation {
   // (reserve) "CLOSE", "AGGREGATE",
   // "TRANSACTION" or "REFUND".
   // New in protocol **v21**.
-  operation_type?: string;
+  operation_type?: LimitOperationType;
 
   // Can this measure be undertaken voluntarily?
   // Optional, default is false.
@@ -1780,7 +1780,7 @@ export interface AccountKycStatus {
   limits?: AccountLimit[];
 }
 
-export type LimitOperationType =
+export type LimitOperationType2 =
   | "WITHDRAW"
   | "DEPOSIT"
   | "MERGE"
@@ -1789,7 +1789,16 @@ export type LimitOperationType =
   | "REFUND"
   | "CLOSE"
   | "TRANSACTION";
-
+export enum LimitOperationType {
+  withdraw = "WITHDRAW",
+  deposit = "DEPOSIT",
+  merge = "MERGE",
+  aggregate = "AGGREGATE",
+  balance = "BALANCE",
+  refund = "REFUND",
+  close = "CLOSE",
+  transaction = "TRANSACTION",
+}
 export interface AccountLimit {
   // Operation that is limited.
   operation_type: LimitOperationType;
@@ -2016,17 +2025,9 @@ export interface AmlDecisionRequest {
   decision_time: Timestamp;
 }
 
-export type KycRuleType =
-  | "WITHDRAW"
-  | "DEPOSIT"
-  | "AGGREGATE"
-  | "MERGE"
-  | "BALANCE"
-  | "CLOSE";
-
 export interface KycRule {
   // Type of operation to which the rule applies.
-  operation_type: KycRuleType;
+  operation_type: LimitOperationType;
 
   // The measures will be taken if the given
   // threshold is crossed over the given timeframe.
@@ -2246,7 +2247,7 @@ export interface ZeroLimitedOperation {
   // (p2p) "MERGE", (wallet) "BALANCE",
   // (reserve) "CLOSE", "AGGREGATE",
   // "TRANSACTION" or "REFUND".
-  operation_type: string;
+  operation_type: LimitOperationType;
 }
 
 interface ExtensionManifest {
@@ -2533,7 +2534,7 @@ export const codecForMeasureInformation = (): 
Codec<MeasureInformation> =>
     .property("prog_name", codecForString())
     .property("check_name", codecForString())
     .property("context", codecForAny())
-    .property("operation_type", codecOptional(codecForString()))
+    .property("operation_type", codecOptional(codecForOperationType))
     .property("voluntary", codecOptional(codecForBoolean()))
     .build("TalerExchangeApi.MeasureInformation");
 
@@ -2593,17 +2594,7 @@ export const codecForLegitimizationRuleSet = (): 
Codec<LegitimizationRuleSet> =>
 
 export const codecForKycRules = (): Codec<KycRule> =>
   buildCodecForObject<KycRule>()
-    .property(
-      "operation_type",
-      codecForEither(
-        codecForConstString("WITHDRAW"),
-        codecForConstString("DEPOSIT"),
-        codecForConstString("MERGE"),
-        codecForConstString("BALANCE"),
-        codecForConstString("CLOSE"),
-        codecForConstString("AGGREGATE"),
-      ),
-    )
+    .property("operation_type", codecForOperationType)
     .property("threshold", codecForAmountString())
     .property("timeframe", codecForDuration)
     .property("measures", codecForList(codecForString()))
@@ -2652,14 +2643,14 @@ export const codecForAccountKycStatus = (): 
Codec<AccountKycStatus> =>
     .build("TalerExchangeApi.AccountKycStatus");
 
 export const codecForOperationType = codecForEither(
-  codecForConstString("WITHDRAW"),
-  codecForConstString("DEPOSIT"),
-  codecForConstString("MERGE"),
-  codecForConstString("BALANCE"),
-  codecForConstString("CLOSE"),
-  codecForConstString("AGGREGATE"),
-  codecForConstString("TRANSACTION"),
-  codecForConstString("REFUND"),
+  codecForConstString(LimitOperationType.withdraw),
+  codecForConstString(LimitOperationType.deposit),
+  codecForConstString(LimitOperationType.merge),
+  codecForConstString(LimitOperationType.balance),
+  codecForConstString(LimitOperationType.close),
+  codecForConstString(LimitOperationType.aggregate),
+  codecForConstString(LimitOperationType.transaction),
+  codecForConstString(LimitOperationType.refund),
 );
 
 export const codecForAccountLimit = (): Codec<AccountLimit> =>
diff --git a/packages/taler-util/src/types-taler-kyc-aml.ts 
b/packages/taler-util/src/types-taler-kyc-aml.ts
index 4d5528325..11a8cf5a9 100644
--- a/packages/taler-util/src/types-taler-kyc-aml.ts
+++ b/packages/taler-util/src/types-taler-kyc-aml.ts
@@ -21,6 +21,7 @@ import {
   codecForAny,
   codecForList,
   codecOptional,
+  LimitOperationType,
 } from "./index.js";
 import {
   AmountString,
@@ -266,7 +267,7 @@ export interface KycRule {
   // (p2p) "MERGE", (wallet) "BALANCE",
   // (reserve) "CLOSE", "AGGREGATE",
   // "TRANSACTION" or "REFUND".
-  operation_type: string;
+  operation_type: LimitOperationType;
 
   // The measures will be taken if the given
   // threshold is crossed over the given timeframe.
@@ -331,7 +332,7 @@ export interface MeasureInformation {
   // (reserve) "CLOSE", "AGGREGATE",
   // "TRANSACTION" or "REFUND".
   // New in protocol **v21**.
-  operation_type?: string;
+  operation_type?: LimitOperationType;
 
   // Can this measure be undertaken voluntarily?
   // Optional, default is false.
@@ -341,7 +342,7 @@ export interface MeasureInformation {
 
 export const codecForAmlProgramInput = (): Codec<AmlProgramInput> =>
   buildCodecForObject<AmlProgramInput>()
-    .property("aml_history", codecOptional( codecForList(codecForAny())))
+    .property("aml_history", codecOptional(codecForList(codecForAny())))
     .property("kyc_history", codecOptional(codecForList(codecForAny())))
     .property("attributes", codecOptional(codecForAccountProperties()))
     .property("context", codecOptional(codecForAny()))
diff --git a/packages/web-util/src/components/ToastBanner.tsx 
b/packages/web-util/src/components/ToastBanner.tsx
index ece26285f..163e9af3a 100644
--- a/packages/web-util/src/components/ToastBanner.tsx
+++ b/packages/web-util/src/components/ToastBanner.tsx
@@ -13,49 +13,76 @@
  You should have received a copy of the GNU General Public License along with
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
-import { Fragment, VNode, h } from "preact"
-import { Attention, GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT, 
Notification, useNotifications } from "../index.browser.js"
+import { Fragment, VNode, h } from "preact";
+import {
+  Attention,
+  GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT,
+  Notification,
+  useNotifications,
+} from "../index.browser.js";
+import { Duration } from "@gnu-taler/taler-util";
 
 /**
  * Toasts should be considered when displaying these types of information to 
the user:
- * 
+ *
  * Low attention messages that do not require user action
  * Singular status updates
  * Confirmations
  * Information that does not need to be followed up
- * 
+ *
  * Do not use toasts if the information contains the following:
- * 
+ *
  * High attention and crtitical information
  * Time-sensitive information
  * Requires user action or input
  * Batch updates
- * 
- * @returns 
+ *
+ * @returns
  */
-export function ToastBanner(): VNode {
-  const notifs = useNotifications()
-  if (notifs.length === 0) return <Fragment />
-  const show = notifs.filter(e => !e.message.ack && !e.message.timeout)
-  if (show.length === 0) return <Fragment />
-  return <AttentionByType msg={show[0]} />
+export function ToastBanner({ debug }: { debug?: boolean }): VNode {
+  const notifs = useNotifications();
+  if (notifs.length === 0) return <Fragment />;
+  const show = notifs.filter((e) => !e.message.ack && !e.message.timeout);
+  if (show.length === 0) return <Fragment />;
+  return <AttentionByType msg={show[0]} debug={debug} />;
 }
 
-function AttentionByType({ msg }: { msg: Notification }) {
+function AttentionByType({
+  msg,
+  debug,
+}: {
+  debug?: boolean;
+  msg: Notification;
+}) {
   switch (msg.message.type) {
     case "error":
-      return <Attention type="danger" title={msg.message.title} onClose={() => 
{
-        msg.acknowledge()
-      }} timeout={GLOBAL_TOAST_TIMEOUT}>
-        {msg.message.description &&
-          <div class="mt-2 text-sm text-red-700">
-            {msg.message.description}
-          </div>
-        }
-      </Attention>
+      return (
+        <Attention
+          type="danger"
+          title={msg.message.title}
+          onClose={() => {
+            msg.acknowledge();
+          }}
+          timeout={debug ? Duration.getForever() : GLOBAL_TOAST_TIMEOUT}
+        >
+          {msg.message.description && (
+            <div class="mt-2 text-sm text-red-700">
+              {msg.message.description}
+            </div>
+          )}
+          {!debug ? undefined : <pre>{msg.message.debug}</pre>}
+        </Attention>
+      );
     case "info":
-      return <Attention type="success" title={msg.message.title} onClose={() 
=> {
-        msg.acknowledge();
-      }} timeout={GLOBAL_TOAST_TIMEOUT} />
+      return (
+        <Attention
+          type="success"
+          title={msg.message.title}
+          onClose={() => {
+            msg.acknowledge();
+          }}
+          timeout={GLOBAL_TOAST_TIMEOUT}
+        />
+      );
   }
 }
diff --git a/packages/web-util/src/components/index.ts 
b/packages/web-util/src/components/index.ts
index 63231f8a2..944bed269 100644
--- a/packages/web-util/src/components/index.ts
+++ b/packages/web-util/src/components/index.ts
@@ -1,4 +1,5 @@
 export * as utils from "./utils.js";
+export { onComponentUnload } from "./utils.js";
 export * from "./Attention.js";
 export * from "./CopyButton.js";
 export * from "./ErrorLoading.js";
diff --git a/packages/web-util/src/components/utils.ts 
b/packages/web-util/src/components/utils.ts
index 75c3fc0fe..a9871ce85 100644
--- a/packages/web-util/src/components/utils.ts
+++ b/packages/web-util/src/components/utils.ts
@@ -1,4 +1,5 @@
 import { createElement, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
 
 export type StateFunc<S> = (p: S) => VNode;
 
@@ -12,7 +13,6 @@ export function compose<SType extends { status: string }, 
PType>(
   hook: (p: PType) => RecursiveState<SType>,
   viewMap: StateViewMap<SType>,
 ): (p: PType) => VNode {
-
   function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
     function ComposedComponent(): VNode {
       const state = stateHook();
@@ -39,7 +39,6 @@ export function compose<SType extends { status: string }, 
PType>(
 export function recursive<PType>(
   hook: (p: PType) => RecursiveState<VNode>,
 ): (p: PType) => VNode {
-
   function withHook(stateHook: () => RecursiveState<VNode>): () => VNode {
     function ComposedComponent(): VNode {
       const state = stateHook();
@@ -61,7 +60,28 @@ export function recursive<PType>(
   };
 }
 
-
+/**
+ * Call `callback` only once.
+ *
+ * Callback can be a closure with binding to the current caller context. This 
helper
+ * will always take the latest `callback`
+ *
+ * @param callback
+ */
+export 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!();
+    };
+  }, []);
+}
 
 /**
  *
diff --git a/packages/web-util/src/forms/fields/InputArray.tsx 
b/packages/web-util/src/forms/fields/InputArray.tsx
index abb495aa5..dc5186e10 100644
--- a/packages/web-util/src/forms/fields/InputArray.tsx
+++ b/packages/web-util/src/forms/fields/InputArray.tsx
@@ -213,7 +213,6 @@ export function InputArray<T extends object, K extends 
keyof T>(
 
             <button
               type="button"
-              disabled={selected !== undefined}
               onClick={() => {
                 const newValue = [...list];
                 newValue.splice(selectedIndex, 1);

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