diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx')
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx | 334 |
1 files changed, 197 insertions, 137 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx index e8022ca15..a0c15c77c 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021 Taler Systems S.A. + (C) 2021-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 @@ -18,9 +18,13 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, Fragment } from "preact"; -import { useCallback, useState } from "preact/hooks"; -import { Translate, Translator, useTranslator } from "../../i18n/index.js"; +import { + parsePaytoUri, + PaytoUriGeneric, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; import { COUNTRY_TABLE } from "../../utils/constants.js"; import { undefinedIfEmpty } from "../../utils/table.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; @@ -28,21 +32,23 @@ import { Input } from "./Input.js"; import { InputGroup } from "./InputGroup.js"; import { InputSelector } from "./InputSelector.js"; import { InputProps, useField } from "./useField.js"; +import { useEffect, useState } from "preact/hooks"; export interface Props<T> extends InputProps<T> { isValid?: (e: any) => boolean; } +// type Entity = PaytoUriGeneric // https://datatracker.ietf.org/doc/html/rfc8905 type Entity = { // iban, bitcoin, x-taler-bank. it defined the format target: string; // path1 if the first field to be used - path1: string; + path1?: string; // path2 if the second field to be used, optional path2?: string; - // options of the payto uri - options: { + // params of the payto uri + params: { "receiver-name"?: string; sender?: string; message?: string; @@ -69,24 +75,53 @@ function checkAddressChecksum(address: string) { return true; } -function validateBitcoin(addr: string, i18n: Translator): string | undefined { +function validateBitcoin_path1( + addr: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { try { const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr); if (valid) return undefined; } catch (e) { console.log(e); } - return i18n`This is not a valid bitcoin address.`; + return i18n.str`This is not a valid bitcoin address.`; } -function validateEthereum(addr: string, i18n: Translator): string | undefined { +function validateEthereum_path1( + addr: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { try { const valid = isEthereumAddress(addr); if (valid) return undefined; } catch (e) { console.log(e); } - return i18n`This is not a valid Ethereum address.`; + return i18n.str`This is not a valid Ethereum address.`; +} + +/** + * validates + * bank.com/ + * bank.com + * bank.com/path + * bank.com/path/subpath/ + */ +const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+(\/[a-zA-Z0-9-.]+)*\/?$/ + +function validateTalerBank_path1( + addr: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { + console.log(addr, DOMAIN_REGEX.test(addr)) + 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.`; } /** @@ -103,12 +138,15 @@ function validateEthereum(addr: string, i18n: Translator): string | undefined { * If the remainder is 1, the check digit test is passed and the IBAN might be valid. * */ -function validateIBAN(iban: string, i18n: Translator): string | undefined { +function validateIBAN_path1( + iban: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { // Check total length if (iban.length < 4) - return i18n`IBAN numbers usually have more that 4 digits`; + return i18n.str`IBAN numbers usually have more that 4 digits`; if (iban.length > 34) - return i18n`IBAN numbers usually have less that 34 digits`; + return i18n.str`IBAN numbers usually have less that 34 digits`; const A_code = "A".charCodeAt(0); const Z_code = "Z".charCodeAt(0); @@ -116,7 +154,7 @@ function validateIBAN(iban: string, i18n: Translator): string | undefined { // check supported country const code = IBAN.substr(0, 2); const found = code in COUNTRY_TABLE; - if (!found) return i18n`IBAN country code not found`; + 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); @@ -140,7 +178,8 @@ function validateIBAN(iban: string, i18n: Translator): string | undefined { } const checksum = calculate_iban_checksum(step3); - if (checksum !== 1) return i18n`IBAN number is not valid, checksum is wrong`; + if (checksum !== 1) + return i18n.str`IBAN number is not valid, checksum is wrong`; return undefined; } @@ -153,7 +192,10 @@ const targets = [ "ethereum", ]; const noTargetValue = targets[0]; -const defaultTarget = { target: noTargetValue, options: {} }; +const defaultTarget: Entity = { + target: noTargetValue, + params: {}, +}; export function InputPaytoForm<T>({ name, @@ -161,52 +203,47 @@ export function InputPaytoForm<T>({ label, tooltip, }: Props<keyof T>): VNode { - const { value: paytos, onChange } = useField<T>(name); + const { value: initialValueStr, onChange } = useField<T>(name); - const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget); + const initialPayto = parsePaytoUri(initialValueStr ?? ""); + const paths = !initialPayto ? [] : initialPayto.targetPath.split("/"); + const initialPath1 = paths.length >= 1 ? paths[0] : undefined; + const initialPath2 = paths.length >= 2 ? paths[1] : undefined; + const initial: Entity = + initialPayto === undefined + ? defaultTarget + : { + target: initialPayto.targetType, + params: initialPayto.params, + path1: initialPath1, + path2: initialPath2, + }; + const [value, setValue] = useState<Partial<Entity>>(initial); - let payToPath; - if (value.target === "iban" && value.path1) { - payToPath = `/${value.path1.toUpperCase()}`; - } else if (value.path1) { - if (value.path2) { - payToPath = `/${value.path1}/${value.path2}`; - } else { - payToPath = `/${value.path1}`; - } - } - const i18n = useTranslator(); - - const ops = value.options!; - const url = tryUrl(`payto://${value.target}${payToPath}`); - if (url) { - Object.keys(ops).forEach((opt_key) => { - const opt_value = ops[opt_key]; - if (opt_value) url.searchParams.set(opt_key, opt_value); - }); - } - const paytoURL = !url ? "" : url.toString(); + const { i18n } = useTranslationContext(); const errors: FormErrors<Entity> = { - target: value.target === noTargetValue ? i18n`required` : undefined, + target: value.target === noTargetValue ? i18n.str`required` : undefined, path1: !value.path1 - ? i18n`required` + ? i18n.str`required` : value.target === "iban" - ? validateIBAN(value.path1, i18n) - : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) - : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, + ? validateIBAN_path1(value.path1, i18n) + : value.target === "bitcoin" + ? validateBitcoin_path1(value.path1, i18n) + : value.target === "ethereum" + ? validateEthereum_path1(value.path1, i18n) + : value.target === "x-taler-bank" + ? validateTalerBank_path1(value.path1, i18n) + : undefined, path2: value.target === "x-taler-bank" ? !value.path2 - ? i18n`required` + ? i18n.str`required` : undefined : undefined, - options: undefinedIfEmpty({ - "receiver-name": !value.options?.["receiver-name"] - ? i18n`required` + params: undefinedIfEmpty({ + "receiver-name": !value.params?.["receiver-name"] + ? i18n.str`required` : undefined, }), }; @@ -215,14 +252,51 @@ export function InputPaytoForm<T>({ (k) => (errors as any)[k] !== undefined, ); - const submit = useCallback((): void => { - const alreadyExists = - paytos.findIndex((x: string) => x === paytoURL) !== -1; - if (!alreadyExists) { - onChange([paytoURL, ...paytos] as any); - } - valueHandler(defaultTarget); - }, [value]); + const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1 + const str = + hasErrors || !value.target + ? undefined + : stringifyPaytoUri({ + targetType: value.target, + targetPath: value.path2 + ? `${path1WithSlash}${value.path2}` + : value.path1 ?? "", + params: value.params ?? ({} as any), + isKnown: false, + }); + useEffect(() => { + onChange(str as any); + }, [str]); + + // const submit = useCallback((): void => { + // // const accounts: TalerMerchantApi.AccountAddDetails[] = paytos; + // // const alreadyExists = + // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; + // // if (!alreadyExists) { + // const newValue: TalerMerchantApi.AccountAddDetails = { + // payto_uri: paytoURL, + // }; + // if (value.auth) { + // if (value.auth.url) { + // newValue.credit_facade_url = value.auth.url; + // } + // if (value.auth.type === "none") { + // newValue.credit_facade_credentials = { + // type: "none", + // }; + // } + // if (value.auth.type === "basic") { + // newValue.credit_facade_credentials = { + // type: "basic", + // username: value.auth.username ?? "", + // password: value.auth.password ?? "", + // }; + // } + // } + // onChange(newValue as any); + // // } + // // valueHandler(defaultTarget); + // }, [value]); //FIXME: translating plural singular return ( @@ -231,27 +305,30 @@ export function InputPaytoForm<T>({ name="tax" errors={errors} object={value} - valueHandler={valueHandler} + valueHandler={setValue} > <InputSelector<Entity> name="target" - label={i18n`Target type`} - tooltip={i18n`Method to use for wire transfer`} + label={i18n.str`Account type`} + tooltip={i18n.str`Method to use for wire transfer`} values={targets} - toStr={(v) => (v === noTargetValue ? i18n`Choose one...` : v)} + readonly={readonly} + toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} /> {value.target === "ach" && ( <Fragment> <Input<Entity> name="path1" - label={i18n`Routing`} - tooltip={i18n`Routing number.`} + label={i18n.str`Routing`} + readonly={readonly} + tooltip={i18n.str`Routing number.`} /> <Input<Entity> name="path2" - label={i18n`Account`} - tooltip={i18n`Account number.`} + label={i18n.str`Account`} + readonly={readonly} + tooltip={i18n.str`Account number.`} /> </Fragment> )} @@ -259,8 +336,9 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Code`} - tooltip={i18n`Business Identifier Code.`} + label={i18n.str`Code`} + readonly={readonly} + tooltip={i18n.str`Business Identifier Code.`} /> </Fragment> )} @@ -268,8 +346,10 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Account`} - tooltip={i18n`Bank Account Number.`} + label={i18n.str`IBAN`} + tooltip={i18n.str`International Bank Account Number.`} + readonly={readonly} + placeholder="DE1231231231" inputExtra={{ style: { textTransform: "uppercase" } }} /> </Fragment> @@ -278,8 +358,9 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Account`} - tooltip={i18n`Unified Payment Interface.`} + readonly={readonly} + label={i18n.str`Account`} + tooltip={i18n.str`Unified Payment Interface.`} /> </Fragment> )} @@ -287,8 +368,9 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Address`} - tooltip={i18n`Bitcoin protocol.`} + readonly={readonly} + label={i18n.str`Address`} + tooltip={i18n.str`Bitcoin protocol.`} /> </Fragment> )} @@ -296,8 +378,9 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Address`} - tooltip={i18n`Ethereum protocol.`} + readonly={readonly} + label={i18n.str`Address`} + tooltip={i18n.str`Ethereum protocol.`} /> </Fragment> )} @@ -305,8 +388,9 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Address`} - tooltip={i18n`Interledger protocol.`} + readonly={readonly} + label={i18n.str`Address`} + tooltip={i18n.str`Interledger protocol.`} /> </Fragment> )} @@ -315,73 +399,49 @@ export function InputPaytoForm<T>({ <Fragment> <Input<Entity> name="path1" - label={i18n`Host`} - tooltip={i18n`Bank host.`} + readonly={readonly} + label={i18n.str`Host`} + fromStr={(v) => { + if (v.startsWith("http")) { + try { + const url = new URL(v); + return url.host + url.pathname; + } catch { + return v; + } + } + return v; + }} + tooltip={i18n.str`Bank host.`} + help={<Fragment> + <div><i18n.Translate>Without scheme and may include subpath:</i18n.Translate></div> + <div>bank.com/</div> + <div>bank.com/path/subpath/</div> + </Fragment>} /> <Input<Entity> name="path2" - label={i18n`Account`} - tooltip={i18n`Bank account.`} + readonly={readonly} + label={i18n.str`Account`} + tooltip={i18n.str`Bank account.`} /> </Fragment> )} + {/** + * Show additional fields apart from the payto + */} {value.target !== noTargetValue && ( - <Input - name="options.receiver-name" - label={i18n`Name`} - tooltip={i18n`Bank account owner's name.`} - /> - )} - - <div class="field is-horizontal"> - <div class="field-label is-normal" /> - <div class="field-body" style={{ display: "block" }}> - {paytos.map((v: any, i: number) => ( - <div - key={i} - class="tags has-addons mt-3 mb-0 mr-3" - style={{ flexWrap: "nowrap" }} - > - <span - class="tag is-medium is-info mb-0" - style={{ maxWidth: "90%" }} - > - {v} - </span> - <a - class="tag is-medium is-danger is-delete mb-0" - onClick={() => { - onChange(paytos.filter((f: any) => f !== v) as any); - }} - /> - </div> - ))} - {!paytos.length && i18n`No accounts yet.`} - </div> - </div> - - {value.target !== noTargetValue && ( - <div class="buttons is-right mt-5"> - <button - class="button is-info" - data-tooltip={i18n`add tax to the tax list`} - disabled={hasErrors} - onClick={submit} - > - <Translate>Add</Translate> - </button> - </div> + <Fragment> + <Input + name="params.receiver-name" + readonly={readonly} + label={i18n.str`Owner's name`} + tooltip={i18n.str`Legal name of the person holding the account.`} + /> + </Fragment> )} </FormProvider> </InputGroup> ); } - -function tryUrl(s: string): URL | undefined { - try { - return new URL(s); - } catch (e) { - return undefined; - } -} |