/* This file is part of GNU Taler (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 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 */ /** * * @author Sebastian Javier Marchano (sebasjm) */ 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"; 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 extends InputProps { 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; // path2 if the second field to be used, optional path2?: string; // params of the payto uri params: { "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_path1( addr: string, i18n: ReturnType["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.str`This is not a valid bitcoin address.`; } function validateEthereum_path1( addr: string, i18n: ReturnType["i18n"], ): string | undefined { try { const valid = isEthereumAddress(addr); if (valid) return undefined; } catch (e) { console.log(e); } 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["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.`; } /** * 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_path1( iban: string, i18n: ReturnType["i18n"], ): string | 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 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: Entity = { target: noTargetValue, params: {}, }; export function InputPaytoForm({ name, readonly, label, tooltip, }: Props): VNode { const { value: initialValueStr, onChange } = useField(name); 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>(initial); const { i18n } = useTranslationContext(); const errors: FormErrors = { target: value.target === noTargetValue ? i18n.str`required` : undefined, path1: !value.path1 ? i18n.str`required` : value.target === "iban" ? 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.str`required` : undefined : undefined, params: undefinedIfEmpty({ "receiver-name": !value.params?.["receiver-name"] ? i18n.str`required` : undefined, }), }; const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, ); 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 ( name="tax" errors={errors} object={value} valueHandler={setValue} > name="target" label={i18n.str`Account type`} tooltip={i18n.str`Method to use for wire transfer`} values={targets} readonly={readonly} toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} /> {value.target === "ach" && ( name="path1" label={i18n.str`Routing`} readonly={readonly} tooltip={i18n.str`Routing number.`} /> name="path2" label={i18n.str`Account`} readonly={readonly} tooltip={i18n.str`Account number.`} /> )} {value.target === "bic" && ( name="path1" label={i18n.str`Code`} readonly={readonly} tooltip={i18n.str`Business Identifier Code.`} /> )} {value.target === "iban" && ( name="path1" label={i18n.str`IBAN`} tooltip={i18n.str`International Bank Account Number.`} readonly={readonly} placeholder="DE1231231231" inputExtra={{ style: { textTransform: "uppercase" } }} /> )} {value.target === "upi" && ( name="path1" readonly={readonly} label={i18n.str`Account`} tooltip={i18n.str`Unified Payment Interface.`} /> )} {value.target === "bitcoin" && ( name="path1" readonly={readonly} label={i18n.str`Address`} tooltip={i18n.str`Bitcoin protocol.`} /> )} {value.target === "ethereum" && ( name="path1" readonly={readonly} label={i18n.str`Address`} tooltip={i18n.str`Ethereum protocol.`} /> )} {value.target === "ilp" && ( name="path1" readonly={readonly} label={i18n.str`Address`} tooltip={i18n.str`Interledger protocol.`} /> )} {value.target === "void" && } {value.target === "x-taler-bank" && ( name="path1" 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={
Without scheme and may include subpath:
bank.com/
bank.com/path/subpath/
} /> name="path2" readonly={readonly} label={i18n.str`Account`} tooltip={i18n.str`Bank account.`} />
)} {/** * Show additional fields apart from the payto */} {value.target !== noTargetValue && ( )}
); }