From 20353eda268efa962959bead466b59823bfb9b29 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 3 May 2024 08:43:53 -0300 Subject: form hook now takes the shape of the form (do not rely on initial value) --- packages/aml-backoffice-ui/src/context/ui-forms.ts | 36 ++-- packages/aml-backoffice-ui/src/forms/simplest.ts | 1 + packages/aml-backoffice-ui/src/hooks/form.ts | 89 ++++++-- .../aml-backoffice-ui/src/pages/CaseDetails.tsx | 4 +- .../aml-backoffice-ui/src/pages/CaseUpdate.tsx | 239 ++++++++++++++------- packages/aml-backoffice-ui/src/pages/Cases.tsx | 14 +- .../aml-backoffice-ui/src/pages/CreateAccount.tsx | 2 + .../src/pages/ShowConsolidated.tsx | 7 +- .../aml-backoffice-ui/src/pages/UnlockAccount.tsx | 2 + packages/aml-backoffice-ui/src/utils/converter.ts | 25 ++- 10 files changed, 290 insertions(+), 129 deletions(-) (limited to 'packages/aml-backoffice-ui/src') diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts index 2e0b8a76d..9cf6125c9 100644 --- a/packages/aml-backoffice-ui/src/context/ui-forms.ts +++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts @@ -39,7 +39,7 @@ import { useContext } from "preact/hooks"; export type Type = UiForms; const defaultForms: UiForms = { - forms: [] + forms: [], }; const Context = createContext(defaultForms); @@ -142,7 +142,7 @@ type UIFormFieldConfigCaption = { type UIFormFieldConfigGroup = { type: "group"; - properties: UIFormFieldBaseConfig & { + properties: UIFieldBaseDescription & { fields: UIFormFieldConfig[]; }; }; @@ -213,7 +213,7 @@ type UIFormFieldConfigToggle = { properties: UIFormFieldBaseConfig; }; -type UIFieldBaseDescription = { +export type UIFieldBaseDescription = { /* label if the field, visible for the user */ label: string; /* long text to be shown on user demand */ @@ -222,6 +222,9 @@ type UIFieldBaseDescription = { /* short text to be shown close to the field */ help?: string; + /* name of the field, useful for a11y */ + name: string; + /* if the field should be initialy hidden */ hidden?: boolean; /* ui element to show before */ @@ -230,7 +233,7 @@ type UIFieldBaseDescription = { addonAfterId?: string; }; -type UIFormFieldBaseConfig = UIFieldBaseDescription & { +export type UIFormFieldBaseConfig = UIFieldBaseDescription & { /* example to be shown inside the field */ placeholder?: string; @@ -240,9 +243,6 @@ type UIFormFieldBaseConfig = UIFieldBaseDescription & { /* readonly and dim */ disabled?: boolean; - /* name of the field, useful for a11y */ - name: string; - /* conversion id to conver the string into the value type the id should be known to the ui impl */ @@ -258,23 +258,27 @@ export type UIHandlerId = string & { [__handlerId]: true }; // FIXME: validate well formed ui field id const codecForUiFieldId = codecForString as () => Codec; -const codecForUIFormFieldBaseConfigTemplate = < - T extends UIFormFieldBaseConfig, +const codecForUIFormFieldBaseDescriptionTemplate = < + T extends UIFieldBaseDescription, >() => buildCodecForObject() - .property("id", codecForUiFieldId()) .property("addonAfterId", codecOptional(codecForString())) .property("addonBeforeId", codecOptional(codecForString())) - .property("converterId", codecOptional(codecForString())) - .property("disabled", codecOptional(codecForBoolean())) .property("hidden", codecOptional(codecForBoolean())) - .property("required", codecOptional(codecForBoolean())) .property("help", codecOptional(codecForString())) .property("label", codecForString()) .property("name", codecForString()) - .property("placeholder", codecOptional(codecForString())) .property("tooltip", codecOptional(codecForString())); +const codecForUIFormFieldBaseConfigTemplate = < + T extends UIFormFieldBaseConfig, +>() => + codecForUIFormFieldBaseDescriptionTemplate() + .property("id", codecForUiFieldId()) + .property("converterId", codecOptional(codecForString())) + .property("disabled", codecOptional(codecForBoolean())) + .property("required", codecOptional(codecForBoolean())) + .property("placeholder", codecOptional(codecForString())); const codecForUIFormFieldBaseConfig = (): Codec => codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties"); @@ -370,7 +374,9 @@ const codecForUiFormFieldFile = (): Codec => const codecForUIFormFieldWithFieldsConfig = (): Codec< UIFormFieldConfigGroup["properties"] > => - codecForUIFormFieldBaseConfigTemplate() + codecForUIFormFieldBaseDescriptionTemplate< + UIFormFieldConfigGroup["properties"] + >() .property("fields", codecForList(codecForUiFormField())) .build("UIFormFieldConfigGroup.properties"); diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts index c7ba95462..b52f2bf74 100644 --- a/packages/aml-backoffice-ui/src/forms/simplest.ts +++ b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -86,6 +86,7 @@ export function resolutionSection( id: ".threshold" as UIHandlerId, currency: "NETZBON", name: "threshold", + converterId: "Taler.Amount", label: i18n.str`New threshold`, }, }, diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts index edeae6085..752444bd2 100644 --- a/packages/aml-backoffice-ui/src/hooks/form.ts +++ b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -15,12 +15,14 @@ */ import { + AbsoluteTime, AmountJson, TalerExchangeApi, TranslatedString, } from "@gnu-taler/taler-util"; import { UIFieldHandler } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; +import { UIFormFieldConfig, UIHandlerId } from "../context/ui-forms.js"; // export type UIField = { // value: string | undefined; @@ -56,6 +58,8 @@ export type FormErrors = { [k in keyof T]?: T[k] extends string ? TranslatedString : T[k] extends AmountJson + ? TranslatedString + : T[k] extends AbsoluteTime ? TranslatedString : T[k] extends TalerExchangeApi.AmlState ? TranslatedString @@ -75,31 +79,22 @@ export type FormStatus = }; function constructFormHandler( + shape: Array, form: RecursivePartial>, updateForm: (d: RecursivePartial>) => void, errors: FormErrors | undefined, ): FormHandler { - const keys = Object.keys(form) as Array; - const handler = keys.reduce((prev, fieldName) => { - const currentValue: unknown = form[fieldName]; - const currentError: unknown = - errors !== undefined ? errors[fieldName] : undefined; + const handler = shape.reduce((handleForm, fieldId) => { + + const path = fieldId.split(".") + function updater(newValue: unknown) { - updateForm({ ...form, [fieldName]: newValue }); - } - /** - * There is no clear way to know if this object is a custom field - * or a group of fields - */ - if (typeof currentValue === "object") { - // @ts-expect-error FIXME better typing - const group = constructFormHandler(currentValue, updater, currentError); - // @ts-expect-error FIXME better typing - prev[fieldName] = group; - return prev; + updateForm(setValueDeeper(form, path, newValue)); } + const currentValue: unknown = getValueDeeper(form, path) + const currentError: unknown = errors === undefined ? undefined : getValueDeeper(errors, path) const field: UIFieldHandler = { // @ts-expect-error FIXME better typing error: currentError, @@ -108,14 +103,37 @@ function constructFormHandler( onChange: updater, state: {}, }; - // @ts-expect-error FIXME better typing - prev[fieldName] = field; - return prev; + + return setValueDeeper(handleForm, path, field) + + /** + * There is no clear way to know if this object is a custom field + * or a group of fields + */ + // if (typeof currentValue === "object") { + // // @ts-expect-error FIXME better typing + // const group = constructFormHandler(currentValue, updater, currentError); + // // @ts-expect-error FIXME better typing + // prev[fieldName] = group; + // return prev; + // } + + // const field: UIFieldHandler = { + // // @ts-expect-error FIXME better typing + // error: currentError, + // // @ts-expect-error FIXME better typing + // value: currentValue, + // onChange: updater, + // state: {}, + // }; + // handleForm[fieldName] = field; + // return handleForm; }, {} as FormHandler); return handler; } + /** * FIXME: Consider sending this to web-utils * @@ -125,6 +143,7 @@ function constructFormHandler( * @returns */ export function useFormState( + shape: Array, defaultValue: RecursivePartial>, check: (f: RecursivePartial>) => FormStatus, ): [FormHandler, FormStatus] { @@ -132,7 +151,35 @@ export function useFormState( useState>>(defaultValue); const status = check(form); - const handler = constructFormHandler(form, updateForm, status.errors); + const handler = constructFormHandler(shape, form, updateForm, status.errors); return [handler, status]; } + + +function getValueDeeper( + object: Record, + names: string[], +): UIFieldHandler { + if (names.length === 0) return object as UIFieldHandler; + const [head, ...rest] = names; + if (!head) { + return getValueDeeper(object, rest); + } + if (object === undefined) { + throw Error("handler not found"); + } + return getValueDeeper(object[head], rest); +} + +function setValueDeeper(object: any, names: string[], value: any): any { + if (names.length === 0) return value; + const [head, ...rest] = names; + if (!head) { + return setValueDeeper(object, rest, value); + } + if (object === undefined) { + return { [head]: setValueDeeper({}, rest, value) }; + } + return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; +} diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index 62c54d222..11b6d053e 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -45,6 +45,7 @@ import { privatePages } from "../Routing.js"; import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; +import { preloadedForms } from "../forms/index.js"; export type AmlEvent = | AmlFormEvent @@ -164,6 +165,7 @@ export function CaseDetails({ account }: { account: string }) { const details = useCaseDetails(account); const {forms} = useUiFormsContext() + const allForms = [...forms, ...preloadedForms(i18n)] if (!details) { return ; } @@ -183,7 +185,7 @@ export function CaseDetails({ account }: { account: string }) { } const { aml_history, kyc_attributes } = details.body; - const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, forms); + const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, allForms); if (showForm !== undefined) { return ( diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx index f04d404d0..bbfa65ca7 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -20,13 +20,15 @@ import { HttpStatusCode, TalerExchangeApi, TalerProtocolTimestamp, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { + Addon, Button, InternationalizationAPI, LocalNotificationBanner, RenderAllFieldsByUiConfig, + StringConverter, UIFieldHandler, UIFormField, useExchangeApiContext, @@ -37,14 +39,19 @@ import { Fragment, VNode, h } from "preact"; import { privatePages } from "../Routing.js"; import { FormMetadata, + UIFieldBaseDescription, + UIFormFieldBaseConfig, UIFormFieldConfig, + UIHandlerId, useUiFormsContext, } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; -import { FormHandler, useFormState } from "../hooks/form.js"; +import { FormErrors, FormHandler, useFormState } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { Justification } from "./CaseDetails.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { undefinedIfEmpty } from "./CreateAccount.js"; +import { getConverterById } from "../utils/converter.js"; function searchForm( i18n: InternationalizationAPI, @@ -67,6 +74,7 @@ type FormType = { when: AbsoluteTime; state: TalerExchangeApi.AmlState; threshold: AmountJson; + comment: string; }; export function CaseUpdate({ @@ -89,6 +97,7 @@ export function CaseUpdate({ when: AbsoluteTime.now(), state: TalerExchangeApi.AmlState.pending, threshold: Amounts.zeroOfCurrency(config.currency), + comment: "", }; if (officer.state !== "ready") { @@ -99,27 +108,41 @@ export function CaseUpdate({ return
form with id {formId} not found
; } - let defaultValue = initial + const shape: Array = []; theForm.config.design.forEach((section) => { section.fields.forEach((field) => { if ("id" in field.properties) { - const path = field.properties.id.split("."); - defaultValue = setValueDeeper(defaultValue, path, undefined); + shape.push(field.properties.id); + // const path = field.properties.id.split("."); + // defaultValue = setValueDeeper(defaultValue, path, undefined); } }); }); - - const [form, state] = useFormState(defaultValue, (st) => { + const [form, state] = useFormState(shape, initial, (st) => { + const errors = undefinedIfEmpty>({ + state: !st.state ? i18n.str`required` : undefined, + threshold: !st.threshold ? i18n.str`required` : undefined, + when: !st.when ? i18n.str`required` : undefined, + comment: !st.comment ? i18n.str`required` : undefined, + }); + if (errors === undefined) { + return { + status: "ok", + result: st as any, + errors: undefined, + }; + } return { - status: "ok", + status: "fail", result: st as any, - errors: undefined, + errors, }; }); - - console.log("FORM", form) - const validatedForm = state.status === "fail" ? undefined : state.result; + + console.log("NOW FORM", form); + + const validatedForm = state.status !== "ok" ? undefined : state.result; const submitHandler = validatedForm === undefined @@ -166,75 +189,6 @@ export function CaseUpdate({ }, ); - function convertUiField( - fieldConfig: UIFormFieldConfig[], - form: FormHandler, - ): UIFormField[] { - return fieldConfig.map((config) => { - switch (config.type) { - case "absoluteTime": { - return undefined!; - } - case "amount": { - return { - type: "amount", - properties: { - ...(config.properties as any), - handler: getValueDeeper(form, config.properties.id.split(".")), - }, - } as UIFormField; - } - case "array": { - return undefined!; - } - case "caption": { - return undefined!; - } - case "choiceHorizontal": { - return { - type: "choiceHorizontal", - properties: { - handler: getValueDeeper(form, config.properties.id.split(".")), - choices: config.properties.choices, - }, - } as UIFormField; - } - case "choiceStacked": - case "file": - case "group": - case "integer": - case "selectMultiple": - case "selectOne": { - return undefined!; - } - case "text": { - return { - type: "text", - properties: { - ...(config.properties as any), - handler: getValueDeeper(form, config.properties.id.split(".")), - }, - } as UIFormField; - } - case "textArea": { - return { - type: "text", - properties: { - ...(config.properties as any), - handler: getValueDeeper(form, config.properties.id.split(".")), - }, - } as UIFormField; - } - case "toggle": { - return undefined!; - } - default: { - assertUnreachable(config); - } - } - }); - } - return ( @@ -261,7 +215,7 @@ export function CaseUpdate({
@@ -281,7 +235,8 @@ export function CaseUpdate({ @@ -350,3 +305,121 @@ function setValueDeeper(object: any, names: string[], value: any): any { } return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; } + +function getAddonById(_id: string | undefined): Addon { + return undefined!; +} + + +function converInputFieldsProps( + form: FormHandler, + p: UIFormFieldBaseConfig, +) { + return { + converter: getConverterById(p.converterId), + handler: getValueDeeper(form, p.id.split(".")), + }; +} + +function converBaseFieldsProps( + i18n_: InternationalizationAPI, + p: UIFieldBaseDescription, +) { + return { + after: getAddonById(p.addonAfterId), + before: getAddonById(p.addonBeforeId), + hidden: p.hidden, + name: p.name, + help: i18n_.str`${p.help}`, + label: i18n_.str`${p.label}`, + tooltip: i18n_.str`${p.tooltip}`, + }; +} + +function convertUiField( + i18n_: InternationalizationAPI, + fieldConfig: UIFormFieldConfig[], + form: FormHandler, +): UIFormField[] { + return fieldConfig.map((config) => { + // NON input fields + switch (config.type) { + case "caption": { + const resp: UIFormField = { + type: config.type, + properties: converBaseFieldsProps(i18n_, config.properties), + }; + return resp; + } + case "group": { + const resp: UIFormField = { + type: config.type, + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + fields: convertUiField(i18n_, config.properties.fields, form), + }, + }; + return resp; + } + } + // Input Fields + switch (config.type) { + case "absoluteTime": { + return undefined!; + } + case "amount": { + return { + type: "amount", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + }, + } as UIFormField; + } + case "array": { + return undefined!; + } + case "choiceHorizontal": { + return { + type: "choiceHorizontal", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "choiceStacked": + case "file": + case "integer": + case "selectMultiple": + case "selectOne": { + return undefined!; + } + case "text": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + }, + } as UIFormField; + } + case "textArea": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + }, + } as UIFormField; + } + case "toggle": { + return undefined!; + } + default: { + assertUnreachable(config); + } + } + }); +} diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 2e92c111e..3860bcd98 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -24,7 +24,7 @@ import { ErrorLoading, InputChoiceHorizontal, Loading, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -34,6 +34,8 @@ import { privatePages } from "../Routing.js"; import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; import { Officer } from "./Officer.js"; +import { UIHandlerId } from "../context/ui-forms.js"; +import { amlStateConverter } from "../utils/converter.js"; type FormType = { state: TalerExchangeApi.AmlState; @@ -55,6 +57,7 @@ export function CasesUI({ const { i18n } = useTranslationContext(); const [form, status] = useFormState( + [".state"] as Array, { state: filter, }, @@ -106,18 +109,19 @@ export function CasesUI({ name="state" label={i18n.str`Filter`} handler={form.state} + converter={amlStateConverter} choices={[ { label: i18n.str`Pending`, - value: TalerExchangeApi.AmlState.pending, + value: "pending", }, { label: i18n.str`Frozen`, - value: TalerExchangeApi.AmlState.frozen, + value: "frozen", }, { label: i18n.str`Normal`, - value: TalerExchangeApi.AmlState.normal, + value: "normal", }, ]} /> @@ -269,7 +273,7 @@ export function Cases() { onNext={list.isLastPage ? undefined : list.loadNext} filter={stateFilter} onChangeFilter={(d) => { - setStateFilter(d) + setStateFilter(d); }} /> ); diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx index abcaaa2a6..98160fb3a 100644 --- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -31,6 +31,7 @@ import { } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { usePreferences } from "../hooks/preferences.js"; +import { UIHandlerId } from "../context/ui-forms.js"; type FormType = { password: string; @@ -104,6 +105,7 @@ export function CreateAccount(): VNode { const [notification, withErrorHandler] = useLocalNotificationHandler(); const [form, status] = useFormState( + [".password", ".repeat"] as Array, { password: undefined, repeat: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx index 0169572bf..0978d8bcc 100644 --- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -72,18 +72,19 @@ export function ShowConsolidated({ properties: { label: i18n.str`State`, name: "aml.state", + choices: [ { label: i18n.str`Frozen`, - value: TalerExchangeApi.AmlState.frozen, + value: "frozen", }, { label: i18n.str`Pending`, - value: TalerExchangeApi.AmlState.pending, + value: "pending", }, { label: i18n.str`Normal`, - value: TalerExchangeApi.AmlState.normal, + value: "normal", }, ], }, diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index 9552f2b0c..b81e66468 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -24,6 +24,7 @@ import { VNode, h } from "preact"; import { FormErrors, useFormState } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; +import { UIHandlerId } from "../context/ui-forms.js"; type FormType = { password: string; @@ -36,6 +37,7 @@ export function UnlockAccount(): VNode { const [notification, withErrorHandler] = useLocalNotificationHandler(); const [form, status] = useFormState( + [".password"] as Array, { password: undefined, }, diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts index cca764a81..25a824697 100644 --- a/packages/aml-backoffice-ui/src/utils/converter.ts +++ b/packages/aml-backoffice-ui/src/utils/converter.ts @@ -14,7 +14,8 @@ GNU Taler; see the file COPYING. If not, see */ -import { TalerExchangeApi } from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, TalerExchangeApi } from "@gnu-taler/taler-util"; +import { StringConverter } from "@gnu-taler/web-util/browser"; export const amlStateConverter = { toStringUI: stringifyAmlState, @@ -45,3 +46,25 @@ function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState { throw Error(`unknown AML state: ${s}`); } } + +const amountConverter: StringConverter = { + fromStringUI(v: string | undefined): AmountJson { + // FIXME: requires currency + return Amounts.parse(`NETZBON:${v}`) ?? Amounts.zeroOfCurrency("NETZBON"); + }, + toStringUI(v: unknown): string { + return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson); + }, +}; + +export function getConverterById(id: string | undefined): StringConverter { + if (id === "Taler.Amount") { + // @ts-expect-error check this + return amountConverter; + } + if (id === "TalerExchangeApi.AmlState") { + // @ts-expect-error check this + return amlStateConverter; + } + return undefined!; +} -- cgit v1.2.3