summaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx')
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx334
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;
- }
-}