diff options
author | Sebastian <sebasjm@gmail.com> | 2024-09-08 18:10:26 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-09-08 18:10:40 -0300 |
commit | 19c75d414fe8427b4dc72f3021e23cf48017f249 (patch) | |
tree | 3131cf6d9a95a21bb4010f3ae8cd2fafe97cb7b3 | |
parent | f9502ad5a529e7b5d011dd0cc948473c25c3aff5 (diff) |
payto form
-rw-r--r-- | packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx | 2 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 4 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Search.tsx | 357 | ||||
-rw-r--r-- | packages/taler-util/src/payto.ts | 10 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputLine.tsx | 6 | ||||
-rw-r--r-- | packages/web-util/src/forms/forms.ts | 2 |
6 files changed, 309 insertions, 72 deletions
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx index b7741d4c7..a74cd09b9 100644 --- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx +++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -215,7 +215,7 @@ export function ExchangeAmlFrame({ <div class="flex mx-auto my-4"> <main class="block rounded-lg bg-white px-5 py-6 shadow " - style={{ minWidth: 300 }} + style={{ minWidth: 600 }} > {children} </main> diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index c7191332a..c274cbcb0 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -232,7 +232,7 @@ export function Cases() { return ( <CasesUI - filtered={true} + filtered={false} records={list.body} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} @@ -301,7 +301,7 @@ export function CasesUnderInvestigation() { return ( <CasesUI - filtered={false} + filtered={true} records={list.body} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx index 047e56180..bcdca0243 100644 --- a/packages/aml-backoffice-ui/src/pages/Search.tsx +++ b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -14,6 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { + buildPayto, + encodeCrock, + hashPaytoUri, + parsePaytoUri, + PaytoUri, + stringifyPaytoUri, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { convertUiField, getConverterById, InternationalizationAPI, @@ -22,7 +31,8 @@ import { UIHandlerId, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { h } from "preact"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { FormErrors, FormStatus, @@ -34,34 +44,16 @@ import { 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 [paytoUri, setPayto] = useState<PaytoUri | undefined>(undefined); + 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), - {}, + { paytoType: "iban" }, createFormValidator(i18n), ); @@ -95,40 +87,169 @@ export function Search() { </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> + {paytoForm.status.status !== "ok" ? undefined : paytoForm.status.result + .paytoType === "x-taler-bank" ? ( + <XTalerBankForm onSearch={setPayto} /> + ) : paytoForm.status.result.paytoType === "iban" ? ( + <IbanForm onSearch={setPayto} /> + ) : ( + <GenericForm onSearch={setPayto} /> + )} + <pre>{!paytoUri ? undefined : stringifyPaytoUri(paytoUri)}</pre> + <pre>{!paytoUri ? undefined : encodeCrock(hashPaytoUri(paytoUri))}</pre> </div> ); } +function XTalerBankForm({ + onSearch, +}: { + onSearch: (p: PaytoUri | undefined) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const fields = talerBankFields(i18n); + const form = useFormState( + getShapeFromFields(fields), + {}, + createTalerBankPaytoValidator(i18n), + ); + const paytoUri = + form.status.status === "fail" + ? undefined + : buildPayto( + "x-taler-bank", + form.status.result.hostname, + form.status.result.account, + { + "receiver-name": form.status.result.name, + }, + ); + + return ( + <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, fields, form.handler, getConverterById)} + /> + </div> + <button + disabled={form.status.status === "fail"} + class="disabled:bg-gray-100 disabled:text-gray-500 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" + onClick={() => onSearch(paytoUri)} + > + Search + </button> + </form> + ); +} +function IbanForm({ + onSearch, +}: { + onSearch: (p: PaytoUri | undefined) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const fields = ibanFields(i18n); + const form = useFormState( + getShapeFromFields(fields), + {}, + createIbanPaytoValidator(i18n), + ); + const paytoUri = + form.status.status === "fail" + ? undefined + : buildPayto("iban", form.status.result.account, undefined, { + "receiver-name": form.status.result.name, + }); + + return ( + <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, fields, form.handler, getConverterById)} + /> + </div> + <button + disabled={form.status.status === "fail"} + class="disabled:bg-gray-100 disabled:text-gray-500 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" + onClick={() => onSearch(paytoUri)} + > + Search + </button> + </form> + ); +} +function GenericForm({ + onSearch, +}: { + onSearch: (p: PaytoUri | undefined) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const fields = genericFields(i18n); + const form = useFormState( + getShapeFromFields(fields), + {}, + createGenericPaytoValidator(i18n), + ); + const paytoUri = + form.status.status === "fail" + ? undefined + : parsePaytoUri(form.status.result.payto); + return ( + <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, fields, form.handler, getConverterById)} + /> + </div> + <button + disabled={form.status.status === "fail"} + class="disabled:bg-gray-100 disabled:text-gray-500 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" + onClick={() => onSearch(paytoUri)} + > + Search + </button> + </form> + ); +} + +interface FormPayto { + paytoType: "generic" | "iban" | "x-taler-bank"; +} + function createFormValidator(i18n: InternationalizationAPI) { return function check( - state: RecursivePartial<FormValues<FormType>>, - ): FormStatus<FormType> { - const errors = undefinedIfEmpty<FormErrors<FormType>>({ + state: RecursivePartial<FormValues<FormPayto>>, + ): FormStatus<FormPayto> { + const errors = undefinedIfEmpty<FormErrors<FormPayto>>({ paytoType: !state?.paytoType ? i18n.str`required` : undefined, }); if (errors === undefined) { - const result: FormType = { + const result: FormPayto = { paytoType: state.paytoType! as any, }; return { @@ -137,7 +258,7 @@ function createFormValidator(i18n: InternationalizationAPI) { errors, }; } - const result: RecursivePartial<FormType> = { + const result: RecursivePartial<FormPayto> = { paytoType: state?.paytoType, }; return { @@ -148,6 +269,119 @@ function createFormValidator(i18n: InternationalizationAPI) { }; } +interface PaytoUriGenericForm { + payto: string; +} + +function createGenericPaytoValidator(i18n: InternationalizationAPI) { + return function check( + state: RecursivePartial<FormValues<PaytoUriGenericForm>>, + ): FormStatus<PaytoUriGenericForm> { + const errors = undefinedIfEmpty<FormErrors<PaytoUriGenericForm>>({ + payto: !state.payto + ? i18n.str`required` + : parsePaytoUri(state.payto) === undefined + ? i18n.str`invalid` + : undefined, + }); + + if (errors === undefined) { + const result: PaytoUriGenericForm = { + payto: state.payto! as any, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial<PaytoUriGenericForm> = { + // targetType: state.iban + }; + return { + status: "fail", + result, + errors, + }; + }; +} + +interface PaytoUriIBANForm { + account: string; + name: string; +} + +function createIbanPaytoValidator(i18n: InternationalizationAPI) { + return function check( + state: RecursivePartial<FormValues<PaytoUriIBANForm>>, + ): FormStatus<PaytoUriIBANForm> { + const errors = undefinedIfEmpty<FormErrors<PaytoUriIBANForm>>({ + account: !state.account ? i18n.str`required` : undefined, + name: !state.name ? i18n.str`required` : undefined, + }); + + if (errors === undefined) { + const result: PaytoUriIBANForm = { + account: state.account!, + name: state.name!, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial<PaytoUriIBANForm> = { + account: state.account, + name: state.name, + }; + return { + status: "fail", + result, + errors, + }; + }; +} +interface PaytoUriTalerBankForm { + hostname: string; + account: string; + name: string; +} +function createTalerBankPaytoValidator(i18n: InternationalizationAPI) { + return function check( + state: RecursivePartial<FormValues<PaytoUriTalerBankForm>>, + ): FormStatus<PaytoUriTalerBankForm> { + const errors = undefinedIfEmpty<FormErrors<PaytoUriTalerBankForm>>({ + account: !state.account ? i18n.str`required` : undefined, + hostname: !state.hostname ? i18n.str`required` : undefined, + name: !state.name ? i18n.str`required` : undefined, + }); + + if (errors === undefined) { + const result: PaytoUriTalerBankForm = { + account: state.account!, + hostname: state.hostname!, + name: state.name!, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial<PaytoUriTalerBankForm> = { + account: state.account, + hostname: state.hostname, + name: state.name, + }; + return { + status: "fail", + result, + errors, + }; + }; +} + const paytoTypeField: ( i18n: InternationalizationAPI, ) => UIFormElementConfig[] = (i18n) => [ @@ -157,10 +391,6 @@ const paytoTypeField: ( required: true, choices: [ { - value: "generic", - label: i18n.str`Generic Payto:// URI`, - }, - { value: "iban", label: i18n.str`IBAN`, }, @@ -168,18 +398,22 @@ const paytoTypeField: ( value: "x-taler-bank", label: i18n.str`Taler Bank`, }, + { + value: "generic", + label: i18n.str`Generic Payto:// URI`, + }, ], - label: `Account type`, + label: i18n.str`Account type`, }, ]; const receiverName: (i18n: InternationalizationAPI) => UIFormElementConfig = ( i18n, ) => ({ - id: "receiverName" as UIHandlerId, + id: "name" as UIHandlerId, type: "text", required: true, - label: `Owner's name`, + label: i18n.str`Owner's name`, help: i18n.str`It should match the bank account name.`, placeholder: i18n.str`John Doe`, }); @@ -188,14 +422,13 @@ const genericFields: ( i18n: InternationalizationAPI, ) => UIFormElementConfig[] = (i18n) => [ { - id: "paytoText" as UIHandlerId, + id: "payto" as UIHandlerId, type: "textArea", required: true, - label: `Payto URI`, + label: i18n.str`Payto URI`, help: i18n.str`As defined by RFC 8905`, placeholder: i18n.str`payto://`, }, - receiverName(i18n), ]; const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( i18n, @@ -204,10 +437,10 @@ const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( id: "account" as UIHandlerId, type: "text", required: true, - label: `Account`, + label: i18n.str`Account`, help: i18n.str`International Bank Account Number`, placeholder: i18n.str`DE1231231231`, - validator: (value) => validateIBAN(value, i18n), + // validator: (value) => validateIBAN(value, i18n), }, receiverName(i18n), ]; @@ -219,7 +452,7 @@ const talerBankFields: ( id: "account" as UIHandlerId, type: "text", required: true, - label: `Bank account`, + label: i18n.str`Bank account`, help: i18n.str`Bank account id`, placeholder: i18n.str`DE123123123`, }, @@ -227,10 +460,10 @@ const talerBankFields: ( id: "hostname" as UIHandlerId, type: "text", required: true, - label: `Hostname`, - validator: (value) => validateTalerBank(value, i18n), + label: i18n.str`Hostname`, help: i18n.str`Without the scheme, may include subpath: bank.com, bank.com/path/`, placeholder: i18n.str`bank.demo.taler.net`, + // validator: (value) => validateTalerBank(value, i18n), }, receiverName(i18n), ]; diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index 76b5ff0cf..3c28a9ad0 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -99,21 +99,25 @@ export function buildPayto( type: "iban", iban: string, bic: string | undefined, + params?: Record<string, string>, ): PaytoUriIBAN; export function buildPayto( type: "bitcoin", address: string, reserve: string | undefined, + params?: Record<string, string>, ): PaytoUriBitcoin; export function buildPayto( type: "x-taler-bank", host: string, account: string, + params?: Record<string, string>, ): PaytoUriTalerBank; export function buildPayto( type: PaytoType, first: string, second?: string, + params: Record<string, string> = {}, ): PaytoUriGeneric { switch (type) { case "bitcoin": { @@ -123,7 +127,7 @@ export function buildPayto( targetType: "bitcoin", targetPath: first, address: uppercased, - params: {}, + params, segwitAddrs: !second ? [] : generateFakeSegwitAddress(second, first), }; return result; @@ -134,7 +138,7 @@ export function buildPayto( isKnown: true, targetType: "iban", iban: uppercased, - params: {}, + params, targetPath: !second ? uppercased : `${second}/${uppercased}`, }; return result; @@ -146,7 +150,7 @@ export function buildPayto( targetType: "x-taler-bank", host: first, account: second, - params: {}, + params, targetPath: `${first}/${second}`, }; return result; diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx index a89d2e24e..4c0176195 100644 --- a/packages/web-util/src/forms/InputLine.tsx +++ b/packages/web-util/src/forms/InputLine.tsx @@ -59,9 +59,9 @@ export function LabelWithTooltipMaybeRequired({ ); if (required) { return ( - <div class="flex justify-between"> + <div class="flex justify-between w-fit"> {WithTooltip} - <span class="text-sm leading-6 text-red-600">*</span> + <span class="text-sm leading-6 text-red-600 pl-2">*</span> </div> ); } @@ -121,7 +121,7 @@ function InputWrapper<T extends object, K extends keyof T>({ children: ComponentChildren; } & UIFormProps<T, K>): VNode { return ( - <div class="sm:col-span-6"> + <div class="sm:col-span-6 "> <LabelWithTooltipMaybeRequired label={label} required={required} diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts index 3b56081ef..2c789b9a3 100644 --- a/packages/web-util/src/forms/forms.ts +++ b/packages/web-util/src/forms/forms.ts @@ -297,7 +297,7 @@ export function convertUiField( } case "textArea": { return { - type: "text", + type: "textArea", properties: { ...converBaseFieldsProps(i18n_, config), ...converInputFieldsProps(form, config, getConverterById), |