taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 9529d1b1b0b4f8ae0f45de90de221f6ac9a4a180
parent 6a6fed5646a3ac085e223a0e112f94be0147885d
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 24 Apr 2025 23:13:41 -0300

wip #9788

Diffstat:
Mpackages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx | 7-------
Mpackages/aml-backoffice-ui/src/Routing.tsx | 16++++++++--------
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/aml-backoffice-ui/src/pages/MeasureList.tsx | 8+++++---
Mpackages/aml-backoffice-ui/src/pages/MeasuresTable.tsx | 9+++++++++
Mpackages/aml-backoffice-ui/src/pages/NewMeasure.tsx | 183+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/aml-backoffice-ui/src/pages/Search.tsx | 6+++++-
Mpackages/aml-backoffice-ui/src/pages/decision/Measures.tsx | 57+++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/aml-backoffice-ui/src/pages/decision/Properties.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 18++++++++++++------
Mpackages/taler-util/src/taleruri.ts | 6+++++-
Mpackages/web-util/src/forms/forms-types.ts | 4++--
Mpackages/web-util/src/hooks/useForm.ts | 5+++--
13 files changed, 253 insertions(+), 130 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -258,13 +258,6 @@ function Navigation(): VNode { label: i18n.str`Search`, }, { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile` }, - showDebugInfo - ? { - route: privatePages.measures, - Icon: FormIcon, - label: i18n.str`Measures`, - } - : undefined, ]; const { path } = useNavigationContext(); return ( diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -133,8 +133,8 @@ export const privatePages = { /\/show-collected\/(?<cid>[a-zA-Z0-9]+)\/(?<rowId>[0-9]+)/, ({ cid, rowId }) => `#/show-collected/${cid}/${rowId}`, ), - measuresNew: urlPattern(/\/measures\/new/, () => "#/measures/new"), - measures: urlPattern(/\/measures/, () => "#/measures"), + // measuresNew: urlPattern(/\/measures\/new/, () => "#/measures/new"), + // measures: urlPattern(/\/measures/, () => "#/measures"), search: urlPattern(/\/search/, () => "#/search"), transfers: urlPattern(/\/transfers/, () => "#/transfers"), accounts: urlPattern(/\/accounts/, () => "#/accounts"), @@ -165,12 +165,12 @@ function PrivateRouting(): VNode { case "profile": { return <Officer />; } - case "measures": { - return <MeasureList routeToNew={privatePages.measuresNew} />; - } - case "measuresNew": { - return <NewMeasure />; - } + // case "measures": { + // return <MeasureList routeToNew={privatePages.measuresNew} />; + // } + // case "measuresNew": { + // return <NewMeasure />; + // } case "decide": { return ( <AmlDecisionRequestWizard diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -545,7 +545,7 @@ function ShowMesaureInfo({ nextMeasures }: { nextMeasures: string[] }): VNode { const filteredMeasures = nextMeasures.filter((n) => !!n && !!n.trim()); const allMeasures = computeAvailableMesaures( measures.body, - cm, + // cm, (m) => filteredMeasures.indexOf(m.name) === -1, ); @@ -1256,7 +1256,10 @@ export function ShowMeasuresToSelect({ return ( <CurrentMeasureTable - measures={computeAvailableMesaures(measures.body, cm)} + measures={computeAvailableMesaures( + measures.body, + // , cm + )} onSelect={onSelect} /> ); @@ -1264,7 +1267,7 @@ export function ShowMeasuresToSelect({ export function computeAvailableMesaures( serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, - customMeasures?: Readonly<CustomMeasures>, + // customMeasures?: Readonly<CustomMeasures>, skpiFilter?: (m: MeasureInfo) => boolean, ): Mesaures { const init: Mesaures = { forms: [], procedures: [] }; @@ -1303,11 +1306,54 @@ export function computeAvailableMesaures( init, ); - if (!customMeasures) { - return server; + return server; + // if (!customMeasures) { + // } + + // const serverAndCustom = customMeasures.measures.reduce((prev, value) => { + // if (value.check_name !== "SKIP") { + // const r: MeasureInfo = { + // type: "form", + // name: value.name, + // context: value.context, + // programName: value.program, + // program: serverMeasures.programs[value.program], + // checkName: value.check_name, + // check: serverMeasures.checks[value.check_name], + // custom: true, + // }; + // if (skpiFilter && skpiFilter(r)) return prev; // skip + // prev.forms.push(r); + // } else { + // const r: MeasureInfo = { + // type: "procedure", + // name: value.name, + // context: value.context, + // programName: value.program, + // program: serverMeasures.programs[value.program], + // custom: true, + // }; + // if (skpiFilter && skpiFilter(r)) return prev; // skip + // prev.procedures.push(r); + // } + // return prev; + // }, server); + + // return serverAndCustom; +} + +export function computeAvailableMesauresCustom( + customMeasures: Readonly<CustomMeasures>, + serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, + skpiFilter?: (m: MeasureInfo) => boolean, +): Mesaures { + const init: Mesaures = { forms: [], procedures: [] }; + + if (!customMeasures || !serverMeasures) { + return init; } - const serverAndCustom = customMeasures.measures.reduce((prev, value) => { + const custom = customMeasures.measures.reduce((prev, value) => { if (value.check_name !== "SKIP") { const r: MeasureInfo = { type: "form", @@ -1334,7 +1380,7 @@ export function computeAvailableMesaures( prev.procedures.push(r); } return prev; - }, server); + }, init); - return serverAndCustom; + return custom; } diff --git a/packages/aml-backoffice-ui/src/pages/MeasureList.tsx b/packages/aml-backoffice-ui/src/pages/MeasureList.tsx @@ -26,7 +26,6 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, h } from "preact"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { useCustomMeasures } from "../hooks/custom-measures.js"; import { useServerMeasures } from "../hooks/server-info.js"; import { computeAvailableMesaures } from "./CaseDetails.js"; import { CurrentMeasureTable } from "./MeasuresTable.js"; @@ -36,7 +35,7 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { const { i18n } = useTranslationContext(); const measures = useServerMeasures(); - const [custom] = useCustomMeasures(); + // const [custom] = useCustomMeasures(); if (!measures) { return <Loading />; @@ -88,7 +87,10 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { } } - const ms = computeAvailableMesaures(measures.body, custom); + const ms = computeAvailableMesaures( + measures.body, + // , custom + ); return ( <div> diff --git a/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx @@ -196,6 +196,12 @@ export function CurrentMeasureTable({ scope="col" class="p-2 text-left text-sm font-semibold text-gray-900" > + <i18n.Translate>Input requirement</i18n.Translate> + </th> + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900" + > <i18n.Translate>Context</i18n.Translate> </th> </tr> @@ -230,6 +236,9 @@ export function CurrentMeasureTable({ {m.program.description} </td> <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {m.program.inputs.join(",")} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> {Object.keys(m.context ?? {}).join(", ")} </td> </tr> diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx @@ -8,62 +8,71 @@ import { } from "@gnu-taler/taler-util"; import { FormDesign, - FormErrors, FormUI, InternationalizationAPI, - UIHandlerId, - undefinedIfEmpty, useForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { useCustomMeasures } from "../hooks/custom-measures.js"; import { useServerMeasures } from "../hooks/server-info.js"; import { computeAvailableMesaures } from "./CaseDetails.js"; import { CurrentMeasureTable } from "./MeasuresTable.js"; +export type MeasureDefinition = { + name: string; + program: string; + check: string; + context: { key: string; value: string }[]; +}; + /** * Defined new limits for the account * @param param0 * @returns */ -export function NewMeasure({}: {}): VNode { +export function NewMeasure({ + initial, + onCancel, + onNewMeasure, +}: { + initial?: MeasureDefinition; + onCancel: () => void; + onNewMeasure: (m: MeasureDefinition) => void; +}): VNode { const { i18n } = useTranslationContext(); - const [custom, updateCustom] = useCustomMeasures(); const measures = useServerMeasures(); // const [rules, setRules] = useState<KycRule[]>([]); - const names = - !measures || measures instanceof TalerError || measures.type === "fail" - ? { measures: [], programs: [], checks: [] } - : { - // measures: Object.entries(measures.body.roots).map(([key,value]) => ({ - // key, value: value. - // })), - programs: Object.entries(measures.body.programs).map( - ([key, value]) => ({ - key, - value, - }), - ), - checks: Object.entries(measures.body.checks).map(([key, value]) => ({ - key, - value, - })), - }; const summary = !measures || measures instanceof TalerError || measures.type === "fail" ? undefined : measures.body; + const names = !summary + ? { measures: [], programs: [], checks: [] } + : { + measures: Object.entries(summary.roots).map(([key, value]) => ({ + key, + value, + })), + programs: Object.entries(summary.programs).map(([key, value]) => ({ + key, + value, + })), + checks: Object.entries(summary.checks).map(([key, value]) => ({ + key, + value, + })), + }; + const design = formDesign(i18n, names.programs, names.checks, summary); - const form = useForm<FormType>( + const form = useForm<MeasureDefinition>( design, - { + initial ?? { program: "check-tos", // check: "form-accept-tos", - check: "askEmail", + check: "askEmail", // testing invalid context: [ { key: "domain", @@ -105,22 +114,6 @@ export function NewMeasure({}: {}): VNode { // }, ); - function addNewRule(nr: FormType) { - const measures = !custom.measures ? [] : [...custom.measures]; - const idx = measures.findIndex((m) => m.name === nr.name); - if (idx !== -1) { - measures.splice(idx, 1); - } - measures.push({ - check_name: nr.check, - context: nr.context, - name: nr.name, - program: nr.program, - type: !nr.check ? "procedure" : "form", - }); - updateCustom("measures", measures); - } - if (!summary) { return <div>loading...</div>; } @@ -148,20 +141,24 @@ export function NewMeasure({}: {}): VNode { ? [] : (form.status.result.context as { key: string; value: string }[]); - const related = computeAvailableMesaures(summary, custom, (m) => { - if (name && m.name === name) { - return false; - } - if (program && m.programName === program.name) { - return false; - } - if (m.type === "form" && check && m.checkName === check.name) { - return false; - } - return true; - }); + // const related = computeAvailableMesaures( + // summary, + // // custom, + // (m) => { + // if (name && m.name === name) { + // return false; + // } + // if (program && m.programName === program.name) { + // return false; + // } + // if (m.type === "form" && check && m.checkName === check.name) { + // return false; + // } + // return true; + // }, + // ); - const haveRelated = related.forms.length > 0 || related.procedures.length > 0; + // const haveRelated = related.forms.length > 0 || related.procedures.length > 0; return ( <div> @@ -172,9 +169,18 @@ export function NewMeasure({}: {}): VNode { <FormUI design={design} model={form.model} /> <button + onClick={() => { + onCancel(); + }} + class="m-4 rounded-md w-fit border-1 px-3 py-2 text-center text-sm shadow-sm " + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <button disabled={form.status.status === "fail"} onClick={() => { - addNewRule(form.status.result as FormType); + onNewMeasure(form.status.result as MeasureDefinition); }} 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" > @@ -297,7 +303,7 @@ export function NewMeasure({}: {}): VNode { </div> )} - {!haveRelated ? undefined : ( + {/* {!haveRelated ? undefined : ( <div class="px-4 mt-4"> <div class="sm:flex sm:items-center"> <div class="sm:flex-auto"> @@ -314,18 +320,11 @@ export function NewMeasure({}: {}): VNode { <CurrentMeasureTable measures={related} /> </div> - )} + )} */} </div> ); } -type FormType = { - name: string; - program: string; - check: string; - context: { key: string; value: string }[]; -}; - const formDesign = ( i18n: InternationalizationAPI, programs: { key: string; value: AmlProgramRequirement }[], @@ -343,13 +342,14 @@ const formDesign = ( return !value ? i18n.str`required` : summary && summary.roots[value] - ? i18n.str`already exist` + ? i18n.str`There is already a measure with that name` : undefined; }, }, { type: "selectOne", id: "program", + required: true, label: i18n.str`Program`, choices: programs.map((m) => { return { @@ -357,36 +357,36 @@ const formDesign = ( label: m.key, }; }), - validator(value) { - // FIXME: cross validation + validator(value, form) { return !value ? i18n.str`required` - : // : !summary ? undefined : programAndCheckMatch(i18n, summary, value, f.check) ?? - undefined; + : !summary + ? undefined + : programAndCheckMatch(i18n, summary, value, form.check) ?? + programAndContextMatch(i18n, summary, value, form.context); }, }, { type: "selectOne", id: "check", label: i18n.str`Check`, + help: i18n.str`Without a check the program will run automatically`, choices: checks.map((m) => { return { value: m.key, label: m.key, }; }), - validator(value) { - // FIXME: cross validation - return undefined; - // return checkAndcontextMatch( - // i18n, - // summary!, - // f.check, - // (f.context ?? []) as { - // key: string; - // value: string; - // }[], - // ) + validator(value, form) { + return checkAndcontextMatch( + i18n, + summary!, + value, + (form.context ?? []) as { + key: string; + value: string; + }[], + ); }, }, { @@ -454,3 +454,20 @@ function checkAndcontextMatch( } return; } + +function programAndContextMatch( + i18n: InternationalizationAPI, + summary: AvailableMeasureSummary, + program: string, + context: { key: string; value: string }[], +): TranslatedString | undefined { + const check = summary.programs[program]; + const output = context.map((d) => d.key); + const missing = check.context.filter((d) => { + return output.indexOf(d) === -1; + }); + if (missing.length > 0) { + return i18n.str`There are missing requirements: ${missing.join(", ")}`; + } + return; +} diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -591,7 +591,11 @@ const walletFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( help: i18n.str`Exchange hostname`, placeholder: i18n.str`exchange.taler.net`, validator(value) { - return !DOMAIN_REGEX.test(value) ? i18n.str`Invalid hostname` : undefined; + return !DOMAIN_REGEX.test(value) + ? i18n.str`Invalid hostname` + : value.endsWith("/") + ? i18n.str`remove last '/'` + : undefined; }, }, { diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx @@ -4,19 +4,23 @@ import { FormUI, InternationalizationAPI, onComponentUnload, - UIHandlerId, useForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useMemo } from "preact/hooks"; +import { Fragment, h, VNode } from "preact"; +import { useMemo, useState } from "preact/hooks"; import { CustomMeasures, useCustomMeasures, } from "../../hooks/custom-measures.js"; import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { useServerMeasures } from "../../hooks/server-info.js"; -import { ShowMeasuresToSelect } from "../CaseDetails.js"; +import { + computeAvailableMesaures, + computeAvailableMesauresCustom, +} from "../CaseDetails.js"; +import { CurrentMeasureTable } from "../MeasuresTable.js"; +import { NewMeasure } from "../NewMeasure.js"; /** * Ask for more information, define new paths to proceed @@ -28,10 +32,15 @@ export function Measures({}: {}): VNode { const [request, _, updateRequest] = useCurrentDecisionRequest(); const measures = useServerMeasures(); const [custom] = useCustomMeasures(); - const measureList = + + const measureBody = !measures || measures instanceof TalerError || measures.type === "fail" - ? [] - : Object.entries(measures.body.roots).map(([id, mi]) => ({ id, ...mi })); + ? undefined + : measures.body; + + const measureList = !measureBody + ? [] + : Object.entries(measureBody.roots).map(([id, mi]) => ({ id, ...mi })); const nm = !request.new_measures ? [] : request.new_measures; @@ -50,10 +59,42 @@ export function Measures({}: {}): VNode { }); }); + const haveCustomMeasures = Object.keys(custom.measures).length > 0; + + const [addMesaure, setAddMeasure] = useState<boolean>(true)//test; + if (addMesaure) { + return ( + <NewMeasure + onCancel={() => { + setAddMeasure(false); + }} + onNewMeasure={() => { + + }} + /> + ); + } return ( <div> <FormUI design={design} model={form.model} /> - <ShowMeasuresToSelect /> + <button + onClick={() => { + setAddMeasure(true); + }} + 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 custom measure</i18n.Translate> + </button> + {!haveCustomMeasures ? undefined : ( + <Fragment> + <h1>Custom measures</h1> + <CurrentMeasureTable + measures={computeAvailableMesauresCustom(custom, measureBody)} + /> + </Fragment> + )} + <h1>Server measures</h1> + <CurrentMeasureTable measures={computeAvailableMesaures(measureBody)} /> </div> ); } diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -109,7 +109,7 @@ function ReloadForm({ merged }: { merged: any }): VNode { onComponentUnload(() => { updateRequest({ ...request, - properties: form.status.result.defined as Record<string, boolean>, + properties: (form.status.result.defined ?? {}) as Record<string, boolean>, custom_properties: (form.status.result.custom ?? []).reduce( (prev, cur) => { if (!cur || !cur.name || !cur.value) return prev; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -59,12 +59,11 @@ export function Summary({ const [notification, withErrorHandler] = useLocalNotificationHandler(); const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; - const [custom] = useCustomMeasures(); const allMeasures = computeAvailableMesaures( !measures || measures instanceof TalerError || measures.type === "fail" ? undefined : measures.body, - custom, + // custom, ); const d = decision.new_measures === undefined ? [] : decision.new_measures; @@ -85,8 +84,6 @@ export function Summary({ ? !isJustificationCompletedForNewACcount(decision) : !isJustificationCompleted(decision); const INVALID_ATTRIBUTES = !isAttributesCompleted(decision); - // decision.attributes !== undefined && - // decision.attributes.errors !== undefined; const CANT_SUBMIT = INVALID_ACCOUNT || @@ -126,7 +123,16 @@ export function Summary({ ), rules: decision.rules!, successor_measure: decision.onExpire_measure, - custom_measures: {}, // TODO: compute custom measures + custom_measures: { + "asd": { + check_name: "form-accept-tos", + prog_name: "check-tos", + context: { + tos_url: "taler.net", + provider_name: "asd", + } + } + }, }, attributes_expiration: decision.attributes?.expiration ? AbsoluteTime.toProtocolTimestamp( @@ -135,7 +141,7 @@ export function Summary({ : undefined, events: decision.triggering_events, attributes: decision.attributes?.data, - properties: decision.properties!, // TODO: compute properties + properties: decision.properties!, new_measures: decision.new_measures!.join(" "), }; return lib.exchange.makeAmlDesicion(session, request); diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts @@ -696,7 +696,11 @@ export function stringifyWithdrawUri({ } export function getURLHostnamePortPath(baseUrl: string) { - return getUrlInfo(baseUrl).path; + const path = getUrlInfo(baseUrl).path + if (path.endsWith("/")){ + return path.substring(0, path.length -1) + } + return path; } /** diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -138,7 +138,7 @@ type UIFormFieldArray = { * @returns an error message if the value is not valid, * undefined if there is no error. */ - validator?: (array: Array<object>) => TranslatedString | undefined; + validator?: (array: Array<object>, form: any) => TranslatedString | undefined; fields: UIFormElementConfig[]; } & UIFormFieldBaseConfig; @@ -305,7 +305,7 @@ export type UIFormFieldBaseConfig = UIFieldElementDescription & { should returns un undefined if there is no error. this function is called before conversion */ - validator?: (text: string) => TranslatedString | undefined; + validator?: (text: string, form: any) => TranslatedString | undefined; /* property id of the form */ id: UIHandlerId; diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -225,6 +225,7 @@ function checkFormFieldIsValid( currentValue: string | undefined, i18n: InternationalizationAPI, secitonTitle: string | undefined, + form: any, ): ErrorAndLabel | undefined { if (!("id" in formElement)) { return undefined; @@ -238,7 +239,7 @@ function checkFormFieldIsValid( }; } else if (formElement.validator) { try { - const message = formElement.validator(currentValue as any); + const message = formElement.validator(currentValue as any, form); if (message !== undefined) { return { label: formElement.label as TranslatedString, @@ -302,7 +303,7 @@ function constructFormHandler<T>( (formElement.hide && formElement.hide(currentValue, result)); const currentError: ErrorAndLabel | undefined = !hidden - ? checkFormFieldIsValid(formElement, currentValue, i18n, secitonTitle) + ? checkFormFieldIsValid(formElement, currentValue, i18n, secitonTitle, formValue) : undefined; if (currentError !== undefined) {