commit 07d0ae31b43db5afdd2f3997c284e6594342bb84 parent 0c95d00a5c9c5deabc33a826b39bf411606b1438 Author: Sebastian <sebasjm@gmail.com> Date: Mon, 9 Jun 2025 11:48:48 -0300 fix #9810 Diffstat:
18 files changed, 313 insertions(+), 328 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -148,10 +148,7 @@ export function ExchangeAmlFrame({ const settings = useUiSettingsContext(); return ( - <div - class="min-h-full flex flex-col m-0 bg-slate-200" - style="min-height: 100vh;" - > + <div class="min-h-full flex flex-col m-0 bg-slate-200 min-h-screen"> <div class="bg-indigo-600 pb-32"> <Header title="Exchange" @@ -219,10 +216,7 @@ export function ExchangeAmlFrame({ <div class="-mt-32 flex grow "> {officer?.state !== "ready" ? undefined : <Navigation />} <div class="flex mx-auto my-4 min-w-80"> - <main - class="block rounded-lg bg-white px-5 py-6 shadow " - style={{ minWidth: 600 }} - > + <main class="block rounded-lg bg-white px-5 py-6 shadow min-w-xl"> {children} </main> </div> diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts @@ -115,7 +115,7 @@ export interface DecisionRequest { export const codecForMeasure = (): Codec<MeasureInformation> => buildCodecForObject<MeasureInformation>() .property("check_name", codecOptionalDefault(codecForString(), "SKIP")) - .property("prog_name", codecForString()) + .property("prog_name", codecOptional(codecForString())) .property("context", codecOptional(codecForMap(codecForAny()))) .build("MeasureInformation"); diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -1255,10 +1255,7 @@ export function ShowMeasuresToSelect({ return ( <CurrentMeasureTable - measures={computeAvailableMesaures( - measures.body, - // , cm - )} + measures={computeAvailableMesaures(measures.body)} onSelect={onSelect} /> ); @@ -1269,26 +1266,45 @@ export function computeAvailableMesaures( // customMeasures?: Readonly<CustomMeasures>, skpiFilter?: (m: MeasureInfo) => boolean, ): Mesaures { - const init: Mesaures = { forms: [], procedures: [] }; + const init: Mesaures = { forms: [], procedures: [], info: [] }; if (!serverMeasures) { return init; } const server = Object.entries(serverMeasures.roots).reduce( (prev, [key, value]) => { if (value.check_name !== "SKIP") { - const r: MeasureInfo = { - type: "form", - name: key, - context: value.context, - programName: value.prog_name, - program: serverMeasures.programs[value.prog_name], - checkName: value.check_name, - check: serverMeasures.checks[value.check_name], - custom: false, - }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.forms.push(r); + if (!value.prog_name) { + const r: MeasureInfo = { + type: "info", + name: key, + context: value.context, + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], + custom: true, + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.info.push(r); + } else { + const r: MeasureInfo = { + type: "form", + name: key, + context: value.context, + programName: value.prog_name, + program: serverMeasures.programs[value.prog_name], + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], + custom: false, + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.forms.push(r); + } } else { + if (!value.prog_name) { + console.error( + `ERROR: program name can't be empty for measure "${key}"`, + ); + return prev; + } const r: MeasureInfo = { type: "procedure", name: key, @@ -1306,37 +1322,4 @@ export function computeAvailableMesaures( ); 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; } diff --git a/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx b/packages/aml-backoffice-ui/src/pages/MeasuresTable.tsx @@ -33,9 +33,9 @@ export type ProcedureMeasure = { export type FormMeasure = { type: "form"; name: string; - programName: string; + programName?: string; + program?: AmlProgramRequirement; checkName: string; - program: AmlProgramRequirement; check: KycCheckInformation; context?: object; custom: boolean; @@ -43,12 +43,15 @@ export type FormMeasure = { export type InfoMeasure = { type: "info"; name: string; + checkName: string; + check: KycCheckInformation; context?: object; custom: boolean; }; export type Mesaures = { procedures: ProcedureMeasure[]; forms: FormMeasure[]; + info: InfoMeasure[]; }; export function CurrentMeasureTable({ measures, @@ -149,6 +152,79 @@ export function CurrentMeasureTable({ </div> )} + {!measures.info.length ? undefined : ( + <div class="mt-4 flow-root"> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-base font-semibold text-gray-900"> + <i18n.Translate>Info</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700"> + <i18n.Translate> + Will show a label or information to the user until a new state. + </i18n.Translate> + </p> + </div> + </div> + + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <div class="overflow-hidden shadow ring-1 ring-black/5 sm:rounded-lg"> + <table class="min-w-full divide-y divide-gray-300"> + <thead class="bg-gray-50"> + <tr> + {onSelect ? ( + <th scope="col" class="relative p-2 "> + <span class="sr-only">Select</span> + </th> + ) : ( + <Fragment /> + )} + <th + scope="col" + class="p-2 text-left text-sm font-semibold text-gray-900 sm:pl-6" + > + <i18n.Translate>Name</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> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {measures.info.map((m) => { + return ( + <tr> + {onSelect ? ( + <td class="relative whitespace-nowrap p-2 text-right text-sm font-medium "> + <button + onClick={() => onSelect(m)} + class="rounded-md w-fit border-0 p-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Modify</i18n.Translate> + </button> + </td> + ) : ( + <Fragment /> + )} + <td class="whitespace-nowrap p-2 text-sm font-medium text-gray-900 sm:pl-6"> + {m.name} + </td> + <td class="whitespace-nowrap p-2 text-sm text-gray-500"> + {Object.keys(m.context ?? {}).join(", ")} + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + </div> + </div> + )} + {!measures.procedures.length ? undefined : ( <div class="mt-4 flow-root"> <div class="sm:flex sm:items-center"> @@ -204,13 +280,6 @@ export function CurrentMeasureTable({ </thead> <tbody class="divide-y divide-gray-200 bg-white"> {measures.procedures.map((m) => { - // if ( - // m.context && - // "internal" in m.context && - // m.context.internal - // ) { - // return <Fragment />; - // } return ( <tr> {onSelect ? ( diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx @@ -7,6 +7,7 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { + ErrorsSummary, FormDesign, FormUI, InternationalizationAPI, @@ -217,7 +218,7 @@ export function MeasureForm({ custom_measures: CURRENT_MEASURES, }); if (onChanged) { - onChanged(newMeasure.name) + onChanged(newMeasure.name); } }} 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" @@ -232,7 +233,7 @@ export function MeasureForm({ custom_measures: currentMeasures, }); if (onRemoved) { - onRemoved(name!) + onRemoved(name!); } }} 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" @@ -387,7 +388,6 @@ const formDesign = ( { type: "selectOne", id: "program", - required: true, label: i18n.str`Program`, choices: programs.map((m) => { return { @@ -395,9 +395,12 @@ const formDesign = ( label: m.key, }; }), + help: i18n.str`Only required when no check is specified`, validator(value, form) { return !value - ? i18n.str`required` + ? !form.check + ? i18n.str`Missing check or program` + : undefined : programAndCheckMatch(i18n, summary, value, form.check) ?? programAndContextMatch(i18n, summary, value, form.context); }, diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx @@ -260,7 +260,7 @@ export function Transfers({ /> </span> ) : ( - <span style={{ color: "grey" }}> + <span class="text-grey-500"> <{i18n.str`Invalid value`}> </span> )} @@ -299,7 +299,7 @@ export function Transfers({ spec={config.config.currency_specification} /> ) : ( - <span style={{ color: "grey" }}> + <span class="text-grey-500"> < {i18n.str`Invalid value`}> </span> diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx @@ -1,4 +1,5 @@ import { + assertUnreachable, MeasureInformation, TalerError, TalerExchangeApi, @@ -95,13 +96,51 @@ function ActiveMeasureForm(): VNode { ? undefined : measures.body; - const measureList = !measureBody ? [] : Object.keys(measureBody.roots); - const design = formDesign(i18n, [ + const measureList = (!measureBody ? [] : Object.keys(measureBody.roots)).map( + (m) => + ({ + type: "normal", + name: m, + }) satisfies NormalMeasure, + ); + + const requestCustomMeasures = request?.custom_measures ?? {} + const customMeasures = Object.keys(requestCustomMeasures).map( + (m) => + ({ + type: "normal", + name: m, + }) satisfies NormalMeasure, + ); + + const checkList = !measureBody ? [] : Object.entries(measureBody.checks); + const simpleChecks = checkList + .filter( + ([, check]) => check.outputs.length > 0 && check.requires.length > 0, + ) + .map( + ([key]) => + ({ + type: "simple-check-form", + checkName: key, + }) satisfies SimpleCheckMeasure, + ); + + const allMeasures: MeasureType[] = [ ...measureList, - ...Object.keys(request?.custom_measures ?? {}), - ]); + ...customMeasures, + ...simpleChecks, + ]; - const nm = !request.new_measures ? [] : request.new_measures; + const design = formDesign(i18n, allMeasures); + + const nm = (!request.new_measures ? [] : request.new_measures).map( + (m) => + ({ + type: "normal", + name: m, + }) satisfies NormalMeasure, + ); const initValue = useMemo<FormType>( () => ({ measures: nm }), @@ -110,8 +149,30 @@ function ActiveMeasureForm(): VNode { const form = useForm<FormType>(design, initValue); onComponentUnload(() => { + const newMeasures: string[] = []; + const formMeasures = form.status.result.measures ?? []; + for (const m of formMeasures) { + switch (m.type) { + case "normal": { + newMeasures.push(m.name) + break; + } + case "simple-check-form": { + const generatedId = `check-${m.checkName}` + requestCustomMeasures[generatedId] = { + check_name: m.checkName, + } + newMeasures.push(generatedId) + break; + } + default: { + assertUnreachable(m); + } + } + } updateRequest("unload active measure", { - new_measures: (form.status.result.measures ?? []) as string[], + new_measures: newMeasures, + custom_measures: requestCustomMeasures, }); }); @@ -208,13 +269,35 @@ function ShowAllMeasures({ ); } +type MeasureType = NormalMeasure | SimpleCheckMeasure; + +/** + * Normal measures are custom measures or server defined measure. + * The name reference some measure already defined + */ +type NormalMeasure = { + type: "normal"; + name: string; +}; + +/** + * Simple check form measure are not yet defined but it should be automatically + * added on submission. + * This checks requires no context and output no information so it doesn't need + * any program. + */ +type SimpleCheckMeasure = { + type: "simple-check-form"; + checkName: string; +}; + type FormType = { - measures: string[]; + measures: MeasureType[]; }; function formDesign( i18n: InternationalizationAPI, - measureNames: string[], + measureNames: MeasureType[], ): FormDesign<FormType> { return { type: "single-column", @@ -222,12 +305,24 @@ function formDesign( { type: "selectMultiple", unique: true, - choices: measureNames.map((name) => { - return { - value: name, - label: name, - }; - }), + choices: measureNames.map((me) => { + switch (me.type) { + case "normal": { + return { + label: me.name, + value: me, + }; + } + case "simple-check-form": { + return { + label: `CHECK: ${me.checkName}`, + value: me, + }; + } + } + // FIXME: choises should allow value to be any type + // check: why do we require value to be string? + }) as any, id: "measures", label: i18n.str`Active measures`, help: i18n.str`Measures that the customer will need to satisfy while the rules are active.`, @@ -241,7 +336,7 @@ function computeAvailableMesauresCustom( serverMeasures: TalerExchangeApi.AvailableMeasureSummary | undefined, skpiFilter?: (m: MeasureInfo) => boolean, ): Mesaures { - const init: Mesaures = { forms: [], procedures: [] }; + const init: Mesaures = { forms: [], procedures: [], info: [] }; if (!customMeasures || !serverMeasures) { return init; @@ -249,19 +344,40 @@ function computeAvailableMesauresCustom( const custom = Object.entries(customMeasures).reduce((prev, [key, value]) => { if (value.check_name !== "SKIP") { - const r: MeasureInfo = { - type: "form", - name: key, - context: value.context, - programName: value.prog_name, - program: serverMeasures.programs[value.prog_name], - checkName: value.check_name, - check: serverMeasures.checks[value.check_name], - custom: true, - }; - if (skpiFilter && skpiFilter(r)) return prev; // skip - prev.forms.push(r); + if (!value.prog_name) { + const r: MeasureInfo = { + type: "info", + name: key, + context: value.context, + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], + custom: true, + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.info.push(r); + } else { + const r: MeasureInfo = { + type: "form", + name: key, + context: value.context, + programName: value.prog_name, + program: value.prog_name + ? serverMeasures.programs[value.prog_name] + : undefined, + checkName: value.check_name, + check: serverMeasures.checks[value.check_name], + custom: true, + }; + if (skpiFilter && skpiFilter(r)) return prev; // skip + prev.forms.push(r); + } } else { + if (!value.prog_name) { + console.error( + `ERROR: program name can't be empty for measure "${key}"`, + ); + return prev; + } const r: MeasureInfo = { type: "procedure", name: key, diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -3,12 +3,13 @@ import { AmlDecisionRequest, assertUnreachable, HttpStatusCode, + MeasureInformation, opFixedSuccess, parsePaytoUri, PaytoString, stringifyPaytoUri, TalerError, - TOPS_AmlEventsName + TOPS_AmlEventsName, } from "@gnu-taler/taler-util"; import { Attention, @@ -20,13 +21,9 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { - useCurrentDecisionRequest -} from "../../hooks/decision-request.js"; +import { useCurrentDecisionRequest } from "../../hooks/decision-request.js"; import { useOfficer } from "../../hooks/officer.js"; -import { - useServerMeasures -} from "../../hooks/server-info.js"; +import { useServerMeasures } from "../../hooks/server-info.js"; import { computeAvailableMesaures, ShowDecisionLimitInfo, @@ -73,7 +70,9 @@ export function Summary({ const activeMeasureInfo: Mesaures = { forms: allMeasures.forms.filter((m) => d.indexOf(m.name) !== -1), procedures: allMeasures.procedures.filter((m) => d.indexOf(m.name) !== -1), + info: allMeasures.info.filter((m) => d.indexOf(m.name) !== -1), }; + // preserve-investigate const { lib } = useExchangeApiContext(); @@ -135,7 +134,9 @@ export function Summary({ ), rules: decision.rules!, successor_measure: decision.onExpire_measure, - custom_measures: decision.custom_measures ?? {}, + custom_measures: workaround_defaultProgramName( + decision.custom_measures ?? {}, + ), }, attributes_expiration: decision.attributes?.expiration ? AbsoluteTime.toProtocolTimestamp( @@ -192,10 +193,7 @@ export function Summary({ if (submitConfirmation) { return ( <div class="flex"> - <div - class="overflow-hidden rounded-lg bg-white shadow-lg" - style={{ width: 600, marign: "auto" }} - > + <div class="overflow-hidden rounded-lg bg-white shadow-lg w-500 w-64 m-auto"> <div class="px-4 py-5 sm:p-6"> <Attention type="warning" title={i18n.str`Confirmation required`}> <i18n.Translate> @@ -230,7 +228,7 @@ export function Summary({ return ( <Fragment> - <LocalNotificationBanner notification={notification} /> + {/* <LocalNotificationBanner notification={notification} /> */} {INVALID_RULES ? ( <Fragment> @@ -367,3 +365,26 @@ export function Summary({ </Fragment> ); } +/** + * FIXME: this should be removed when the server allows null programs + * or we define a no-operation program + * + * https://bugs.gnunet.org/view.php?id=9810 + * https://bugs.gnunet.org/view.php?id=9874 + * + * @param measures + * @returns + */ +export function workaround_defaultProgramName( + measures: Record<string, MeasureInformation>, +) { + const ms = Object.keys(measures); + for (const name of ms) { + const m = measures[name]; + if (!m.prog_name) { + measures[name].prog_name = "preserve-investigate"; + } + } + return measures; +} + diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx @@ -1,54 +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 { h, VNode } from "preact"; -import { useEffect, useRef } from "preact/hooks"; -// import qrcode from "qrcode-generator"; - -export function QR({ text }: { text: string }): VNode { - const divRef = useRef<HTMLDivElement>(null); - useEffect(() => { - // const qr = qrcode(0, "L"); - // qr.addData(text); - // qr.make(); - // if (divRef.current) - // divRef.current.innerHTML = qr.createSvgTag({ - // scalable: true, - // }); - }); - - return ( - <div - style={{ - width: "100%", - display: "flex", - flexDirection: "column", - alignItems: "left", - }} - > - <div - style={{ - width: "50%", - minWidth: 200, - maxWidth: 300, - marginRight: "auto", - marginLeft: "auto", - }} - ref={divRef} - /> - </div> - ); -} diff --git a/packages/taler-util/src/aml/events.ts b/packages/taler-util/src/aml/events.ts @@ -1,6 +1,5 @@ import { Amounts } from "../amounts.js"; -import { LimitOperationType } from "../types-taler-exchange.js"; -import { AccountProperties, KycRule } from "../types-taler-kyc-aml.js"; +import { AccountProperties, KycRule, LimitOperationType } from "../types-taler-exchange.js"; import { TalerAmlProperties } from "../taler-account-properties.js"; import { isOneOf } from "./properties.js"; diff --git a/packages/taler-util/src/aml/properties.ts b/packages/taler-util/src/aml/properties.ts @@ -1,9 +1,6 @@ import { TalerAmlProperties } from "../taler-account-properties.js"; import { TalerFormAttributes } from "../taler-form-attributes.js"; -import { - AccountProperties, - LegitimizationRuleSet, -} from "../types-taler-kyc-aml.js"; +import { AccountProperties, LegitimizationRuleSet } from "../types-taler-exchange.js"; /** * List of account properties required by TOPS diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -1688,7 +1688,7 @@ export interface MeasureInformation { check_name: string; // Name of an AML program. - prog_name: string; + prog_name?: string; // Context for the check. Optional. context?: Object; diff --git a/packages/taler-util/src/types-taler-kyc-aml.ts b/packages/taler-util/src/types-taler-kyc-aml.ts @@ -15,19 +15,17 @@ */ import { + AccountProperties, buildCodecForObject, Codec, codecForAccountProperties, codecForAny, codecForList, codecOptional, - LimitOperationType, + LegitimizationRuleSet } from "./index.js"; import { - AmountString, - Integer, - RelativeTime, - Timestamp, + Timestamp } from "./types-taler-common.js"; // https://docs.taler.net/taler-kyc-manual.html#implementing-your-own-aml-programs @@ -215,146 +213,6 @@ export interface AmlOutcome { new_measures?: string; } -// All fields in this object are optional. The actual -// properties collected depend fully on the discretion -// of the exchange operator; -// however, some common fields are standardized -// and thus described here. -export interface AccountProperties { - // True if this is a politically exposed account. - // Rules for classifying accounts as politically - // exposed are country-dependent. - pep?: boolean; - - // True if this is a sanctioned account. - // Rules for classifying accounts as sanctioned - // are country-dependent. - sanctioned?: boolean; - - // True if this is a high-risk account. - // Rules for classifying accounts as at-risk - // are exchange operator-dependent. - high_risk?: boolean; - - // Business domain of the account owner. - // The list of possible business domains is - // operator- or country-dependent. - business_domain?: string; - - // Is the client's account currently frozen? - is_frozen?: boolean; - - // Was the client's account reported to the authorities? - was_reported?: boolean; - - [key: string]: string | boolean | number | undefined; -} - -export interface LegitimizationRuleSet { - // When does this set of rules expire and - // we automatically transition to the successor - // measure? - expiration_time: Timestamp; - - // Name of the measure to apply when the expiration time is - // reached. If not set, we refer to the default - // set of rules (and the default account state). - successor_measure?: string; - - // Legitimization rules that are to be applied - // to this account. - rules: KycRule[]; - - // Custom measures that KYC rules and the - // successor_measure may refer to. - custom_measures: { [measure: string]: MeasureInformation }; -} - -export interface KycRule { - // Type of operation to which the rule applies. - // - // Must be one of "WITHDRAW", "DEPOSIT", - // (p2p) "MERGE", (wallet) "BALANCE", - // (reserve) "CLOSE", "AGGREGATE", - // "TRANSACTION" or "REFUND". - operation_type: LimitOperationType; - - // Name of the configuration section this rule - // originates from. Not available for all rules. - // Primarily informational, but also useful to - // explicitly manipulate rules by-name in AML programs. - rule_name?: string; - - // The measures will be taken if the given - // threshold is crossed over the given timeframe. - threshold: AmountString; - - // Over which duration should the threshold be - // computed. All amounts of the respective - // operation_type will be added up for this - // duration and the sum compared to the threshold. - timeframe: RelativeTime; - - // Array of names of measures to apply. - // Names listed can be original measures or - // custom measures from the AmlOutcome. - // A special measure "verboten" is used if the - // threshold may never be crossed. - measures: string[]; - - // If multiple rules apply to the same account - // at the same time, the number with the highest - // rule determines which set of measures will - // be activated and thus become visible for the - // user. - display_priority: Integer; - - // True if the rule (specifically, operation_type, - // threshold, timeframe) and the general nature of - // the measures (verboten or approval required) - // should be exposed to the client. - // Defaults to "false" if not set. - exposed?: boolean; - - // True if all the measures will eventually need to - // be satisfied, false if any of the measures should - // do. Primarily used by the SPA to indicate how - // the measures apply when showing them to the user; - // in the end, AML programs will decide after each - // measure what to do next. - // Default (if missing) is false. - is_and_combinator?: boolean; -} - -export interface MeasureInformation { - // Name of a KYC check. - check_name: string; - - // Name of an AML program. - prog_name: string; - - // Context for the check. Optional. - context?: Object; - - // Operation that this measure relates to. - // NULL if unknown. Useful as a hint to the - // user if there are many (voluntary) measures - // and some related to unlocking certain operations. - // (and due to zero-amount thresholds, no measure - // was actually specifically triggered). - // - // Must be one of "WITHDRAW", "DEPOSIT", - // (p2p) "MERGE", (wallet) "BALANCE", - // (reserve) "CLOSE", "AGGREGATE", - // "TRANSACTION" or "REFUND". - // New in protocol **v21**. - operation_type?: LimitOperationType; - - // Can this measure be undertaken voluntarily? - // Optional, default is false. - // Since protocol **vATTEST**. - voluntary?: boolean; -} export const codecForAmlProgramInput = (): Codec<AmlProgramInput> => buildCodecForObject<AmlProgramInput>() diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx @@ -11,7 +11,7 @@ interface Props { export function Attention({ type = "info", title, children, onClose, timeout = Duration.getForever() }: Props): VNode { return <div class={`group attention-${type} mt-2 shadow-lg`}> - {timeout.d_ms === "forever" ? undefined : <style>{` + {/* {timeout.d_ms === "forever" ? undefined : <style>{` .progress { animation: notificationTimeoutBar ${Math.round(timeout.d_ms / 1000)}s ease-in-out; animation-fill-mode:both; @@ -22,7 +22,7 @@ export function Attention({ type = "info", title, children, onClose, timeout = D 100% { width: 100%; } } `}</style> - } + } */} <div data-timed={timeout.d_ms !== "forever"} class="rounded-md data-[timed=true]:rounded-b-none group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> <div class="flex"> diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx @@ -88,7 +88,7 @@ export function Button<T extends OperationResult<A, B>, A, B>({ function Wait(): VNode { return ( <Fragment> - <style> + {/* <style> {` #l1 { width: 120px; height: 20px; @@ -99,7 +99,7 @@ function Wait(): VNode { @keyframes l17 { 100% {background-size:120% 100%} `} - </style> + </style> */} <div id="l1" /> </Fragment> ); diff --git a/packages/web-util/src/components/Header.tsx b/packages/web-util/src/components/Header.tsx @@ -38,10 +38,9 @@ export function Header({ <div class="flex-shrink-0 bg-white rounded-lg"> <a href={iconLinkURL ?? "#"} name="logo"> <img - class="h-8 w-auto" + class="h-8 w-auto m-1" src={logo} alt="GNU Taler" - style={{ height: "1.5rem", margin: ".5rem" }} /> </a> </div> diff --git a/packages/web-util/src/components/Loading.tsx b/packages/web-util/src/components/Loading.tsx @@ -35,7 +35,7 @@ export function Loading(): VNode { function Spinner(): VNode { return ( - <div class="lds-ring" style={{ margin: "auto" }}> + <div class="lds-ring m-auto" > <div /> <div /> <div /> diff --git a/packages/web-util/src/components/ShowInputErrorLabel.tsx b/packages/web-util/src/components/ShowInputErrorLabel.tsx @@ -25,5 +25,5 @@ export function ShowInputErrorLabel({ }): VNode { if (message && isDirty) return <div class="text-base" style={{ color: "red" }}>{message}</div>; - return <div class="text-base" style={{ }}> </div>; + return <div class="text-base" > </div>; }