diff options
Diffstat (limited to 'packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx')
-rw-r--r-- | packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx | 392 |
1 files changed, 0 insertions, 392 deletions
diff --git a/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx deleted file mode 100644 index 9cfef07..0000000 --- a/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx +++ /dev/null @@ -1,392 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { h, VNode, Fragment } from "preact"; -import { useCallback, useState } from "preact/hooks"; -import { Translate, Translator, useTranslator } from "../../i18n"; -import { COUNTRY_TABLE } from "../../utils/constants"; -import { FormErrors, FormProvider } from "./FormProvider"; -import { Input } from "./Input"; -import { InputGroup } from "./InputGroup"; -import { InputSelector } from "./InputSelector"; -import { InputProps, useField } from "./useField"; - -export interface Props<T> extends InputProps<T> { - isValid?: (e: any) => boolean; -} - -// 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; - // path2 if the second field to be used, optional - path2?: string; - // options of the payto uri - options: { - "receiver-name"?: string; - sender?: string; - message?: string; - amount?: string; - instruction?: string; - [name: string]: string | undefined; - }; -}; - -function isEthereumAddress(address: string) { - if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) { - return false; - } else if ( - /^(0x|0X)?[0-9a-f]{40}$/.test(address) || - /^(0x|0X)?[0-9A-F]{40}$/.test(address) - ) { - return true; - } - return checkAddressChecksum(address); -} - -function checkAddressChecksum(address: string) { - //TODO implement ethereum checksum - return true; -} - -function validateBitcoin(addr: string, i18n: Translator): 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.`; -} - -function validateEthereum(addr: string, i18n: Translator): 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.`; -} - -/** - * An IBAN is validated by converting it into an integer and performing a - * basic mod-97 operation (as described in ISO 7064) on it. - * If the IBAN is valid, the remainder equals 1. - * - * The algorithm of IBAN validation is as follows: - * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid - * 2.- Move the four initial characters to the end of the string - * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35 - * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97 - * - * 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 { - // Check total length - if (iban.length < 4) - return i18n`IBAN numbers usually have more that 4 digits`; - if (iban.length > 34) - return i18n`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`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`IBAN number is not valid, checksum is wrong`; - return undefined; -} - -// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank'] -const targets = [ - "Choose one...", - "iban", - "x-taler-bank", - "bitcoin", - "ethereum", -]; -const noTargetValue = targets[0]; -const defaultTarget = { target: noTargetValue, options: {} }; - -function undefinedIfEmpty<T>(obj: T): T | undefined { - return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) - ? obj - : undefined; -} - -export function InputPaytoForm<T>({ - name, - readonly, - label, - tooltip, -}: Props<keyof T>): VNode { - const { value: paytos, onChange } = useField<T>(name); - - const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget); - - 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 errors: FormErrors<Entity> = { - target: value.target === noTargetValue ? i18n`required` : undefined, - path1: !value.path1 - ? i18n`required` - : value.target === "iban" - ? validateIBAN(value.path1, i18n) - : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) - : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, - path2: - value.target === "x-taler-bank" - ? !value.path2 - ? i18n`required` - : undefined - : undefined, - options: undefinedIfEmpty({ - "receiver-name": !value.options?.["receiver-name"] - ? i18n`required` - : undefined, - }), - }; - - const hasErrors = Object.keys(errors).some( - (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]); - - //FIXME: translating plural singular - return ( - <InputGroup name="payto" label={label} fixed tooltip={tooltip}> - <FormProvider<Entity> - name="tax" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <InputSelector<Entity> - name="target" - label={i18n`Target type`} - tooltip={i18n`Method to use for wire transfer`} - values={targets} - toStr={(v) => (v === noTargetValue ? i18n`Choose one...` : v)} - /> - - {value.target === "ach" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Routing`} - tooltip={i18n`Routing number.`} - /> - <Input<Entity> - name="path2" - label={i18n`Account`} - tooltip={i18n`Account number.`} - /> - </Fragment> - )} - {value.target === "bic" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Code`} - tooltip={i18n`Business Identifier Code.`} - /> - </Fragment> - )} - {value.target === "iban" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Account`} - tooltip={i18n`Bank Account Number.`} - inputExtra={{ style: { textTransform: "uppercase" } }} - /> - </Fragment> - )} - {value.target === "upi" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Account`} - tooltip={i18n`Unified Payment Interface.`} - /> - </Fragment> - )} - {value.target === "bitcoin" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Address`} - tooltip={i18n`Bitcoin protocol.`} - /> - </Fragment> - )} - {value.target === "ethereum" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Address`} - tooltip={i18n`Ethereum protocol.`} - /> - </Fragment> - )} - {value.target === "ilp" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Address`} - tooltip={i18n`Interledger protocol.`} - /> - </Fragment> - )} - {value.target === "void" && <Fragment />} - {value.target === "x-taler-bank" && ( - <Fragment> - <Input<Entity> - name="path1" - label={i18n`Host`} - tooltip={i18n`Bank host.`} - /> - <Input<Entity> - name="path2" - label={i18n`Account`} - tooltip={i18n`Bank account.`} - /> - </Fragment> - )} - - {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> - )} - </FormProvider> - </InputGroup> - ); -} - -function tryUrl(s: string): URL | undefined { - try { - return new URL(s); - } catch (e) { - return undefined; - } -} |