diff options
author | Sebastian <sebasjm@gmail.com> | 2024-09-06 13:54:33 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-09-06 13:54:58 -0300 |
commit | c2efc802b2789cb47a6b0a54fc1672b98ee37db2 (patch) | |
tree | de5535184952ba20b77dd481ae152a077be8b547 /packages/aml-backoffice-ui/src/pages | |
parent | d671ef0f4cfa7d17b43b265501ae595882549f17 (diff) |
search account in aml form and remove name from dynamic forms
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 6 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 151 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 12 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Search.tsx | 298 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 16 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx | 10 |
6 files changed, 409 insertions, 84 deletions
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<FormType>(shape, initial, (st) => { + const { handler, status } = useFormState<FormType>(shape, initial, (st) => { const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({ 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 ( <div> <div class="sm:flex sm:items-center"> - {filtered ? + {filtered ? ( <div class="px-2 sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> <i18n.Translate>Cases under investigation</i18n.Translate> </h1> <p class="mt-2 text-sm text-gray-700 w-80"> <i18n.Translate> - 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. </i18n.Translate> </p> - </div> : <div class="px-2 sm:flex-auto"> + </div> + ) : ( + <div class="px-2 sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> <i18n.Translate>Cases</i18n.Translate> </h1> @@ -113,7 +112,7 @@ export function CasesUI({ </i18n.Translate> </p> </div> - } + )} </div> <div class="mt-8 flow-root"> <div class="overflow-x-auto"> @@ -155,7 +154,11 @@ export function CasesUI({ </div> </td> <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> - {r.to_investigate ? <span title="require investigation"><ToInvestigateIcon /></span> : undefined} + {r.to_investigate ? ( + <span title="require investigation"> + <ToInvestigateIcon /> + </span> + ) : undefined} </td> </tr> ); @@ -189,7 +192,8 @@ export function Cases() { <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create a new one. + This account signature is wrong, contact administrator or create + a new one. </i18n.Translate> </Attention> <Officer /> @@ -200,27 +204,26 @@ export function Cases() { return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account is not known. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - } - case HttpStatusCode.Conflict: { - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account doesn't have access. Request account activation - sending your public key. - </i18n.Translate> + <i18n.Translate>This account is not known.</i18n.Translate> </Attention> <Officer /> </Fragment> ); } + case HttpStatusCode.Conflict: + { + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account doesn't have access. Request account activation + sending your public key. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + } return <Officer />; 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() { <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is wrong, contact administrator or create a new one. + This account signature is wrong, contact administrator or create + a new one. </i18n.Translate> </Attention> <Officer /> @@ -269,27 +273,26 @@ export function CasesUnderInvestigation() { return ( <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account is not known. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - } - case HttpStatusCode.Conflict: { - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account doesn't have access. Request account activation - sending your public key. - </i18n.Translate> + <i18n.Translate>This account is not known.</i18n.Translate> </Attention> <Officer /> </Fragment> ); } + case HttpStatusCode.Conflict: + { + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account doesn't have access. Request account activation + sending your public key. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + } return <Officer />; 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() { // </svg> // } export const ToInvestigateIcon = () => ( - <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /> + <svg + title="requires investigation" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" + /> </svg> ); - export const PeopleIcon = () => ( <svg xmlns="http://www.w3.org/2000/svg" @@ -356,6 +370,23 @@ export const HomeIcon = () => ( </svg> ); +export const SearchIcon = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" + /> + </svg> +); + 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<T extends object | undefined>(obj: T): T | undefined { +export function undefinedIfEmpty<T extends object | undefined>( + obj: T, +): T | undefined { if (obj === undefined) return undefined; return Object.keys(obj).some( (k) => (obj as Record<string, T>)[k] !== undefined, @@ -105,7 +107,7 @@ export function CreateAccount(): VNode { const [notification, withErrorHandler] = useLocalNotificationHandler(); - const [form, status] = useFormState<FormType>( + const { handler, status } = useFormState<FormType>( [".password", ".repeat"] as Array<UIHandlerId>, { 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} /> </div> @@ -158,7 +160,7 @@ export function CreateAccount(): VNode { name="repeat" type="password" required - handler={form.repeat} + handler={handler.repeat} /> </div> 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 <http://www.gnu.org/licenses/> + */ +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 <HandleAccountNotReady officer={officer} />; + } + + return ( + <div> + <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 "> + <i18n.Translate>Search account</i18n.Translate> + </h1> + <form + class="space-y-6" + noValidate + onSubmit={(e) => { + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"> + <RenderAllFieldsByUiConfig + fields={convertUiField( + i18n, + paytoTypeField(i18n), + paytoForm.handler, + getConverterById, + )} + /> + </div> + </form> + + <form + class="space-y-6" + noValidate + onSubmit={(e) => { + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"> + <RenderAllFieldsByUiConfig + fields={convertUiField( + i18n, + secondFieldSet, + secondForm.handler, + getConverterById, + )} + /> + </div> + </form> + </div> + ); +} + +function createFormValidator(i18n: InternationalizationAPI) { + return function check( + state: RecursivePartial<FormValues<FormType>>, + ): FormStatus<FormType> { + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + 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<FormType> = { + 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<typeof useTranslationContext>["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<UIHandlerId> = []; - - formConfig.design.forEach((section) => { - Array.prototype.push.apply(shape, getShapeFromFields(section.fields)); - }); + const shape: Array<UIHandlerId> = 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<FormType>( + const { handler, status } = useFormState<FormType>( [".password"] as Array<UIHandlerId>, { 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 { <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> - <div class="mb-4"> <InputLine<FormType, "password"> label={i18n.str`Password`} name="password" type="password" required - handler={form.password} + handler={handler.password} /> </div> @@ -115,7 +114,6 @@ export function UnlockAccount(): VNode { <i18n.Translate>Unlock</i18n.Translate> </Button> </div> - </div> <Button type="button" |