From c2efc802b2789cb47a6b0a54fc1672b98ee37db2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Sep 2024 13:54:33 -0300 Subject: search account in aml form and remove name from dynamic forms --- .../aml-backoffice-ui/src/pages/CaseUpdate.tsx | 6 +- packages/aml-backoffice-ui/src/pages/Cases.tsx | 151 ++++++----- .../aml-backoffice-ui/src/pages/CreateAccount.tsx | 12 +- packages/aml-backoffice-ui/src/pages/Search.tsx | 298 +++++++++++++++++++++ .../src/pages/ShowConsolidated.tsx | 16 +- .../aml-backoffice-ui/src/pages/UnlockAccount.tsx | 10 +- 6 files changed, 409 insertions(+), 84 deletions(-) create mode 100644 packages/aml-backoffice-ui/src/pages/Search.tsx (limited to 'packages/aml-backoffice-ui/src/pages') diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx index 2708dba4c..87f1aed5f 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -117,7 +117,7 @@ export function CaseUpdate({ ); }); - const [form, state] = useFormState(shape, initial, (st) => { + const { handler, status } = useFormState(shape, initial, (st) => { const partialErrors = undefinedIfEmpty>({ state: st.state === undefined ? i18n.str`required` : undefined, threshold: !st.threshold ? i18n.str`required` : undefined, @@ -143,7 +143,7 @@ export function CaseUpdate({ }; }); - const validatedForm = state.status !== "ok" ? undefined : state.result; + const validatedForm = status.status !== "ok" ? undefined : status.result; const submitHandler = validatedForm === undefined @@ -224,7 +224,7 @@ export function CaseUpdate({ fields={convertUiField( i18n, section.fields, - form, + handler, getConverterById, )} /> diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index c229850b1..c7191332a 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -21,22 +21,18 @@ import { } from "@gnu-taler/taler-util"; import { Attention, - ErrorLoading, - InputChoiceHorizontal, Loading, - UIHandlerId, - amlStateConverter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useCurrentDecisions, useCurrentDecisionsUnderInvestigation } from "../hooks/decisions.js"; +import { + useCurrentDecisions, + useCurrentDecisionsUnderInvestigation, +} from "../hooks/decisions.js"; import { privatePages } from "../Routing.js"; -import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js"; -import { undefinedIfEmpty } from "./CreateAccount.js"; -import { Officer } from "./Officer.js"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { useState } from "preact/hooks"; +import { Officer } from "./Officer.js"; type FormType = { // state: TalerExchangeApi.AmlState; @@ -48,7 +44,7 @@ export function CasesUI({ onNext, filtered, }: { - filtered: boolean, + filtered: boolean; onFirstPage?: () => void; onNext?: () => void; records: TalerExchangeApi.AmlDecision[]; @@ -93,17 +89,20 @@ export function CasesUI({ return (
- {filtered ? + {filtered ? (

Cases under investigation

- A list of all the accounts which are waiting for a deicison to be made. + A list of all the accounts which are waiting for a deicison to + be made.

-
:
+
+ ) : ( +

Cases

@@ -113,7 +112,7 @@ export function CasesUI({

- } + )}
@@ -155,7 +154,11 @@ export function CasesUI({
- {r.to_investigate ? : undefined} + {r.to_investigate ? ( + + + + ) : undefined} ); @@ -189,7 +192,8 @@ export function Cases() { - This account signature is wrong, contact administrator or create a new one. + This account signature is wrong, contact administrator or create + a new one. @@ -200,27 +204,26 @@ export function Cases() { return ( - - This account is not known. - - - - - ); - } - case HttpStatusCode.Conflict: { - return ( - - - - This account doesn't have access. Request account activation - sending your public key. - + This account is not known. ); } + case HttpStatusCode.Conflict: + { + return ( + + + + This account doesn't have access. Request account activation + sending your public key. + + + + + ); + } return ; default: assertUnreachable(list); @@ -233,10 +236,10 @@ export function Cases() { records={list.body} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} - // filter={stateFilter} - // onChangeFilter={(d) => { - // setStateFilter(d); - // }} + // filter={stateFilter} + // onChangeFilter={(d) => { + // setStateFilter(d); + // }} /> ); } @@ -258,7 +261,8 @@ export function CasesUnderInvestigation() { - This account signature is wrong, contact administrator or create a new one. + This account signature is wrong, contact administrator or create + a new one. @@ -269,27 +273,26 @@ export function CasesUnderInvestigation() { return ( - - This account is not known. - - - - - ); - } - case HttpStatusCode.Conflict: { - return ( - - - - This account doesn't have access. Request account activation - sending your public key. - + This account is not known. ); } + case HttpStatusCode.Conflict: + { + return ( + + + + This account doesn't have access. Request account activation + sending your public key. + + + + + ); + } return ; default: assertUnreachable(list); @@ -302,10 +305,10 @@ export function CasesUnderInvestigation() { records={list.body} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} - // filter={stateFilter} - // onChangeFilter={(d) => { - // setStateFilter(d); - // }} + // filter={stateFilter} + // onChangeFilter={(d) => { + // setStateFilter(d); + // }} /> ); } @@ -316,12 +319,23 @@ export function CasesUnderInvestigation() { // // } export const ToInvestigateIcon = () => ( - - + + ); - export const PeopleIcon = () => ( ( ); +export const SearchIcon = () => ( + + + +); + function Pagination({ onFirstPage, onNext, diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx index 87310aa27..328d8459b 100644 --- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -89,7 +89,9 @@ function createFormValidator( }; } -export function undefinedIfEmpty(obj: T): T | undefined { +export function undefinedIfEmpty( + obj: T, +): T | undefined { if (obj === undefined) return undefined; return Object.keys(obj).some( (k) => (obj as Record)[k] !== undefined, @@ -105,7 +107,7 @@ export function CreateAccount(): VNode { const [notification, withErrorHandler] = useLocalNotificationHandler(); - const [form, status] = useFormState( + const { handler, status } = useFormState( [".password", ".repeat"] as Array, { password: undefined, @@ -118,7 +120,7 @@ export function CreateAccount(): VNode { status.status === "fail" || officer.state !== "not-found" ? undefined : withErrorHandler( - async () => officer.create(form.password!.value!), + async () => officer.create(handler.password!.value!), () => {}, ); return ( @@ -148,7 +150,7 @@ export function CreateAccount(): VNode { name="password" type="password" required - handler={form.password} + handler={handler.password} />
@@ -158,7 +160,7 @@ export function CreateAccount(): VNode { name="repeat" type="password" required - handler={form.repeat} + handler={handler.repeat} />
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx new file mode 100644 index 000000000..047e56180 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Search.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 + */ +import { + convertUiField, + getConverterById, + InternationalizationAPI, + RenderAllFieldsByUiConfig, + UIFormElementConfig, + UIHandlerId, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h } from "preact"; +import { + FormErrors, + FormStatus, + FormValues, + getShapeFromFields, + RecursivePartial, + useFormState, +} from "../hooks/form.js"; +import { useOfficer } from "../hooks/officer.js"; +import { undefinedIfEmpty } from "./CreateAccount.js"; +import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +interface FormType { + paytoType: "generic" | "iban" | "x-taler-bank"; +} + +export function Search() { + const officer = useOfficer(); + const { i18n } = useTranslationContext(); + + const paytoForm = useFormState( + getShapeFromFields(paytoTypeField(i18n)), + { paytoType: "generic" }, + createFormValidator(i18n), + ); + + const secondFieldSet = + paytoForm.status.status !== "ok" + ? [] + : paytoForm.status.result.paytoType === "iban" + ? ibanFields(i18n) + : paytoForm.status.result.paytoType === "x-taler-bank" + ? talerBankFields(i18n) + : genericFields(i18n); + + const secondForm = useFormState( + getShapeFromFields(secondFieldSet), + {}, + createFormValidator(i18n), + ); + + if (officer.state !== "ready") { + return ; + } + + return ( +
+

+ Search account +

+
{ + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > +
+ +
+
+ +
{ + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > +
+ +
+
+
+ ); +} + +function createFormValidator(i18n: InternationalizationAPI) { + return function check( + state: RecursivePartial>, + ): FormStatus { + const errors = undefinedIfEmpty>({ + paytoType: !state?.paytoType ? i18n.str`required` : undefined, + }); + + if (errors === undefined) { + const result: FormType = { + paytoType: state.paytoType! as any, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial = { + paytoType: state?.paytoType, + }; + return { + status: "fail", + result, + errors, + }; + }; +} + +const paytoTypeField: ( + i18n: InternationalizationAPI, +) => UIFormElementConfig[] = (i18n) => [ + { + id: "paytoType" as UIHandlerId, + type: "choiceHorizontal", + required: true, + choices: [ + { + value: "generic", + label: i18n.str`Generic Payto:// URI`, + }, + { + value: "iban", + label: i18n.str`IBAN`, + }, + { + value: "x-taler-bank", + label: i18n.str`Taler Bank`, + }, + ], + label: `Account type`, + }, +]; + +const receiverName: (i18n: InternationalizationAPI) => UIFormElementConfig = ( + i18n, +) => ({ + id: "receiverName" as UIHandlerId, + type: "text", + required: true, + label: `Owner's name`, + help: i18n.str`It should match the bank account name.`, + placeholder: i18n.str`John Doe`, +}); + +const genericFields: ( + i18n: InternationalizationAPI, +) => UIFormElementConfig[] = (i18n) => [ + { + id: "paytoText" as UIHandlerId, + type: "textArea", + required: true, + label: `Payto URI`, + help: i18n.str`As defined by RFC 8905`, + placeholder: i18n.str`payto://`, + }, + receiverName(i18n), +]; +const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( + i18n, +) => [ + { + id: "account" as UIHandlerId, + type: "text", + required: true, + label: `Account`, + help: i18n.str`International Bank Account Number`, + placeholder: i18n.str`DE1231231231`, + validator: (value) => validateIBAN(value, i18n), + }, + receiverName(i18n), +]; + +const talerBankFields: ( + i18n: InternationalizationAPI, +) => UIFormElementConfig[] = (i18n) => [ + { + id: "account" as UIHandlerId, + type: "text", + required: true, + label: `Bank account`, + help: i18n.str`Bank account id`, + placeholder: i18n.str`DE123123123`, + }, + { + id: "hostname" as UIHandlerId, + type: "text", + required: true, + label: `Hostname`, + validator: (value) => validateTalerBank(value, i18n), + help: i18n.str`Without the scheme, may include subpath: bank.com, bank.com/path/`, + placeholder: i18n.str`bank.demo.taler.net`, + }, + receiverName(i18n), +]; + +function validateIBAN( + iban: string, + i18n: ReturnType["i18n"], +): TranslatedString | undefined { + // Check total length + if (iban.length < 4) + return i18n.str`IBAN numbers usually have more that 4 digits`; + if (iban.length > 34) + return i18n.str`IBAN numbers usually have less that 34 digits`; + + const A_code = "A".charCodeAt(0); + const Z_code = "Z".charCodeAt(0); + const IBAN = iban.toUpperCase(); + + // check supported country + // const code = IBAN.substr(0, 2); + // const found = code in COUNTRY_TABLE; + // if (!found) return i18n.str`IBAN country code not found`; + + // 2.- Move the four initial characters to the end of the string + const step2 = IBAN.substr(4) + iban.substr(0, 4); + const step3 = Array.from(step2) + .map((letter) => { + const code = letter.charCodeAt(0); + if (code < A_code || code > Z_code) return letter; + return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`; + }) + .join(""); + + function calculate_iban_checksum(str: string): number { + const numberStr = str.substr(0, 5); + const rest = str.substr(5); + const number = parseInt(numberStr, 10); + const result = number % 97; + if (rest.length > 0) { + return calculate_iban_checksum(`${result}${rest}`); + } + return result; + } + + const checksum = calculate_iban_checksum(step3); + if (checksum !== 1) + return i18n.str`IBAN number is invalid, checksum is wrong`; + return undefined; +} + +const DOMAIN_REGEX = + /^[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9-_](?:\.[a-zA-Z0-9-_]{2,})+(:[0-9]+)?(\/[a-zA-Z0-9-.]+)*\/?$/; + +function validateTalerBank( + addr: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { + try { + const valid = DOMAIN_REGEX.test(addr); + if (valid) return undefined; + } catch (e) { + console.log(e); + } + return i18n.str`This is not a valid host.`; +} diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx index 21c14fee3..7374125b0 100644 --- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -20,7 +20,6 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { - DefaultForm, FormConfiguration, RenderAllFieldsByUiConfig, UIFormElementConfig, @@ -31,8 +30,8 @@ import { } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; -import { AmlEvent } from "./CaseDetails.js"; import { getShapeFromFields, useFormState } from "../hooks/form.js"; +import { AmlEvent } from "./CaseDetails.js"; /** * the exchange doesn't hava a consistent api @@ -78,7 +77,6 @@ export function ShowConsolidated({ type: "text", label: key as TranslatedString, id: `${key}.value` as UIHandlerId, - name: `${key}.value`, disabled: true, help: `At ${ field.since.t_ms === "never" @@ -92,13 +90,11 @@ export function ShowConsolidated({ : undefined!, ], }; - const shape: Array = []; - - formConfig.design.forEach((section) => { - Array.prototype.push.apply(shape, getShapeFromFields(section.fields)); - }); + const shape: Array = formConfig.design.flatMap((field) => + getShapeFromFields(field.fields), + ); - const [form, state] = useFormState<{}>(shape, fixed, (result) => { + const { handler } = useFormState<{}>(shape, fixed, (result) => { return { status: "ok", errors: undefined, result }; }); @@ -130,7 +126,7 @@ export function ShowConsolidated({ fields={convertUiField( i18n, section.fields, - form, + handler, getConverterById, )} /> diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index 084e639bf..72656bb98 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -19,7 +19,7 @@ import { LocalNotificationBanner, UIHandlerId, useLocalNotificationHandler, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { FormErrors, useFormState } from "../hooks/form.js"; @@ -36,7 +36,7 @@ export function UnlockAccount(): VNode { const officer = useOfficer(); const [notification, withErrorHandler] = useLocalNotificationHandler(); - const [form, status] = useFormState( + const { handler, status } = useFormState( [".password"] as Array, { password: undefined, @@ -64,7 +64,7 @@ export function UnlockAccount(): VNode { status.status === "fail" || officer.state !== "locked" ? undefined : withErrorHandler( - async () => officer.tryUnlock(form.password!.value!), + async () => officer.tryUnlock(handler.password!.value!), () => {}, ); @@ -94,14 +94,13 @@ export function UnlockAccount(): VNode {
-
label={i18n.str`Password`} name="password" type="password" required - handler={form.password} + handler={handler.password} />
@@ -115,7 +114,6 @@ export function UnlockAccount(): VNode { Unlock
-