summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-09 14:37:30 -0300
committerSebastian <sebasjm@gmail.com>2021-08-09 14:37:30 -0300
commitf4cbd85008d14a78433b9495cce48903192e2e0d (patch)
tree541e79045ce28ba68e8278c407191d0c37668649
parente10811b1e2610d8b82221ec11d5300425777bec7 (diff)
downloadmerchant-backoffice-f4cbd85008d14a78433b9495cce48903192e2e0d.tar.gz
merchant-backoffice-f4cbd85008d14a78433b9495cce48903192e2e0d.tar.bz2
merchant-backoffice-f4cbd85008d14a78433b9495cce48903192e2e0d.zip
payto uri form
-rw-r--r--packages/frontend/src/components/form/InputGroup.tsx9
-rw-r--r--packages/frontend/src/components/form/InputPaytoForm.tsx167
-rw-r--r--packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx17
-rw-r--r--packages/frontend/src/paths/admin/create/CreatePage.tsx27
-rw-r--r--packages/frontend/src/paths/admin/list/View.tsx6
-rw-r--r--packages/frontend/src/paths/instance/update/UpdatePage.tsx30
-rw-r--r--packages/frontend/src/scss/main.scss1
7 files changed, 190 insertions, 67 deletions
diff --git a/packages/frontend/src/components/form/InputGroup.tsx b/packages/frontend/src/components/form/InputGroup.tsx
index a4252f0..8af9c7d 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -28,11 +28,12 @@ export interface Props<T> {
label: ComponentChildren;
tooltip?: ComponentChildren;
alternative?: ComponentChildren;
+ fixed?: boolean;
initialActive?: boolean;
}
-export function InputGroup<T>({ name, label, children, tooltip, alternative, initialActive }: Props<keyof T>): VNode {
- const [active, setActive] = useState(initialActive);
+export function InputGroup<T>({ name, label, children, tooltip, alternative, fixed, initialActive }: Props<keyof T>): VNode {
+ const [active, setActive] = useState(initialActive || fixed);
const group = useGroupField<T>(name);
return <div class="card">
@@ -46,13 +47,13 @@ export function InputGroup<T>({ name, label, children, tooltip, alternative, ini
<i class="mdi mdi-alert" />
</span>}
</p>
- <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}>
+ { !fixed && <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}>
<span class="icon">
{active ?
<i class="mdi mdi-arrow-up" /> :
<i class="mdi mdi-arrow-down" />}
</span>
- </button>
+ </button> }
</header>
{active ? <div class="card-content">
{children}
diff --git a/packages/frontend/src/components/form/InputPaytoForm.tsx b/packages/frontend/src/components/form/InputPaytoForm.tsx
new file mode 100644
index 0000000..c52dc33
--- /dev/null
+++ b/packages/frontend/src/components/form/InputPaytoForm.tsx
@@ -0,0 +1,167 @@
+/*
+ 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, useTranslator } from "../../i18n";
+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 = {
+ target: string,
+ path: string,
+ path1: string,
+ path2: string,
+ host: string,
+ account: string,
+ options: {
+ 'receiver-name'?: string,
+ sender?: string,
+ message?: string,
+ amount?: string,
+ instruction?: string,
+ [name: string]: string | undefined,
+ },
+}
+
+// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank']
+const targets = ['iban', 'x-taler-bank']
+const defaultTarget = { target: 'iban', 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)
+
+ if (value.path1) {
+ if (value.path2) {
+ value.path = `/${value.path1}/${value.path2}`
+ } else {
+ value.path = `/${value.path1}`
+ }
+ }
+ const i18n = useTranslator()
+
+ const url = new URL(`payto://${value.target}${value.path}`)
+ const ops = value.options!
+ 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.toString()
+
+ const errors: FormErrors<Entity> = {
+ target: !value.target ? i18n`required` : undefined,
+ path1: !value.path1 ? i18n`required` : (
+ value.target === 'iban' ? (
+ value.path1.length < 4 ? i18n`IBAN numbers usually have more that 4 digits` : (
+ value.path1.length > 34 ? i18n`IBAN numbers usually have less that 34 digits` :
+ undefined
+ )
+ ): 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} />
+
+ {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.`} />
+ </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 === 'ilp' && <Fragment>
+ <Input<Entity> name="path1" label={i18n`Address`} tooltip={i18n`Interledger protocol.`} />
+ </Fragment>}
+ {value.target === 'void' && <Fragment>
+ </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>}
+
+ <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>
+
+ <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>
+ )
+}
diff --git a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
index 873ee80..fae8a35 100644
--- a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
@@ -20,16 +20,16 @@
*/
import { Fragment, h } from "preact";
+import { useBackendContext } from "../../context/backend";
+import { useTranslator } from "../../i18n";
+import { Entity } from "../../paths/admin/create/CreatePage";
import { Input } from "../form/Input";
import { InputCurrency } from "../form/InputCurrency";
import { InputDuration } from "../form/InputDuration";
import { InputGroup } from "../form/InputGroup";
import { InputLocation } from "../form/InputLocation";
-import { InputPayto } from "../form/InputPayto";
+import { InputPaytoForm } from "../form/InputPaytoForm";
import { InputWithAddon } from "../form/InputWithAddon";
-import { useBackendContext } from "../../context/backend";
-import { useTranslator } from "../../i18n";
-import { Entity } from "../../paths/admin/create/CreatePage";
export function DefaultInstanceFormFields({ readonlyId, showId }: { readonlyId?: boolean; showId: boolean }) {
const i18n = useTranslator();
@@ -45,13 +45,8 @@ export function DefaultInstanceFormFields({ readonlyId, showId }: { readonlyId?:
label={i18n`Business name`}
tooltip={i18n`Legal name of the business represented by this instance.`} />
- <Input<Entity> name="creditor_name"
- label={i18n`Creditor Name`}
- tooltip={i18n`name of who receive the money`}
- />
-
- <InputPayto<Entity> name="payto_uris_base"
- label={i18n`Bank account URI`} help="x-taler-bank/bank.taler:5882/blogger"
+ <InputPaytoForm<Entity> name="payto_uris"
+ label={i18n`Bank account`}
tooltip={i18n`URI specifying bank account for crediting revenue.`} />
<InputCurrency<Entity> name="default_max_deposit_fee"
diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx
index 76f02e1..f5fa7c9 100644
--- a/packages/frontend/src/paths/admin/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx
@@ -32,8 +32,6 @@ import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants";
import { Amounts } from "@gnu-taler/taler-util";
export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {
- payto_uris_base: string[], // field to construct final payto URI
- creditor_name: string, // name of the receiver for the payto URI
auth_token?: string
}
@@ -47,6 +45,7 @@ interface Props {
function with_defaults(id?: string): Partial<Entity> {
return {
id,
+ payto_uris: [],
default_pay_delay: { d_ms: 1000 * 60 * 60 }, // one hour
default_wire_fee_amortization: 1,
default_wire_transfer_delay: { d_ms: 1000 * 2 * 60 * 60 * 24 }, // one day
@@ -64,23 +63,12 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
const i18n = useTranslator()
- if (value.payto_uris && value.payto_uris.length > 0) {
- const payto = new URL(value.payto_uris[0])
- value.creditor_name = payto.searchParams.get('receiver-name') || undefined
- value.payto_uris_base = value.payto_uris.map( p => {
- const payto = new URL(p)
- payto.searchParams.delete('receiver-name')
- return payto.toString()
- })
- }
-
const errors: FormErrors<Entity> = {
id: !value.id ? i18n`required` : (!INSTANCE_ID_REGEX.test(value.id) ? i18n`is not valid` : undefined),
name: !value.name ? i18n`required` : undefined,
- creditor_name: !value.creditor_name ? i18n`required` : undefined,
- payto_uris_base:
- !value.payto_uris_base || !value.payto_uris_base.length ? i18n`required` : (
- undefinedIfEmpty(value.payto_uris_base.map(p => {
+ payto_uris:
+ !value.payto_uris || !value.payto_uris.length ? i18n`required` : (
+ undefinedIfEmpty(value.payto_uris.map(p => {
return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined
}))
),
@@ -126,13 +114,6 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
if (!value.address) value.address = {}
if (!value.jurisdiction) value.jurisdiction = {}
// remove above use conversion
- const receiverName = value.creditor_name!
- value.payto_uris = value.payto_uris_base?.map(p => {
- const payto = new URL(p)
- payto.searchParams.set('receiver-name', receiverName)
- return payto.toString()
- }) || []
- value.payto_uris_base = undefined
// schema.validateSync(value, { abortEarly: false })
return onCreate(value as Entity);
}
diff --git a/packages/frontend/src/paths/admin/list/View.tsx b/packages/frontend/src/paths/admin/list/View.tsx
index 35096cd..a77a5a1 100644
--- a/packages/frontend/src/paths/admin/list/View.tsx
+++ b/packages/frontend/src/paths/admin/list/View.tsx
@@ -55,17 +55,17 @@ export function View({ instances, onCreate, onDelete, onPurge, onUpdate, setInst
<div class="tabs" style={{ overflow: 'inherit' }}>
<ul>
<li class={showIsActive}>
- <div class="has-tooltip-right" data-tooltip={i18n`only show active instances`}>
+ <div class="has-tooltip-right" data-tooltip={i18n`Only show active instances`}>
<a onClick={() => setShow("active")}><Translate>Active</Translate></a>
</div>
</li>
<li class={showIsDeleted}>
- <div class="has-tooltip-right" data-tooltip={i18n`only show deleted instances`}>
+ <div class="has-tooltip-right" data-tooltip={i18n`Only show deleted instances`}>
<a onClick={() => setShow("deleted")}><Translate>Deleted</Translate></a>
</div>
</li>
<li class={showAll}>
- <div class="has-tooltip-right" data-tooltip={i18n`show all instances`}>
+ <div class="has-tooltip-right" data-tooltip={i18n`Show all instances`}>
<a onClick={() => setShow(null)}><Translate>All</Translate></a>
</div>
</li>
diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
index 4a965b6..0fa96ed 100644
--- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
@@ -35,8 +35,6 @@ import { Amounts } from "@gnu-taler/taler-util";
type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
- payto_uris_base: string[], // field to construct final payto URI
- creditor_name: string, // name of the receiver for the payto URI
auth_token?: string
}
@@ -56,19 +54,8 @@ function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity
default_wire_fee_amortization: 1,
default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour
default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours
- creditor_name: '',
- payto_uris_base: new Array<string>()
}
- if (payto_uris && payto_uris.length > 0) {
- const payto = new URL(payto_uris[0])
- defaults.creditor_name = payto.searchParams.get('receiver-name') || ''
- defaults.payto_uris_base = payto_uris.map( p => {
- const payto = new URL(p)
- payto.searchParams.delete('receiver-name')
- return payto.toString()
- })
- }
- return { ...defaults, ...rest, payto_uris: [] };
+ return { ...defaults, ...rest, payto_uris };
}
function getTokenValuePart(t?: string): string | undefined {
@@ -103,10 +90,9 @@ export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props):
const errors: FormErrors<Entity> = {
name: !value.name ? i18n`required` : undefined,
- creditor_name: !value.creditor_name ? i18n`required` : undefined,
- payto_uris_base:
- !value.payto_uris_base || !value.payto_uris_base.length ? i18n`required` : (
- undefinedIfEmpty(value.payto_uris_base.map(p => {
+ payto_uris:
+ !value.payto_uris || !value.payto_uris.length ? i18n`required` : (
+ undefinedIfEmpty(value.payto_uris.map(p => {
return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined
}))
),
@@ -144,14 +130,6 @@ export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props):
const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined)
const submit = async (): Promise<void> => {
- const receiverName = value.creditor_name!
- value.payto_uris = value.payto_uris_base?.map(p => {
- const payto = new URL(p)
- payto.searchParams.set('receiver-name', receiverName)
- return payto.toString()
- }) || []
- value.payto_uris_base = undefined
-
await onUpdate(schema.cast(value));
await onBack()
return Promise.resolve()
diff --git a/packages/frontend/src/scss/main.scss b/packages/frontend/src/scss/main.scss
index f9ae0ef..b523566 100644
--- a/packages/frontend/src/scss/main.scss
+++ b/packages/frontend/src/scss/main.scss
@@ -170,6 +170,7 @@ input:read-only {
.icon[data-tooltip]:before {
transition: none;
+ z-index: 5;
}
span[data-tooltip] {