diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-09 14:37:30 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-09 14:37:30 -0300 |
commit | f4cbd85008d14a78433b9495cce48903192e2e0d (patch) | |
tree | 541e79045ce28ba68e8278c407191d0c37668649 | |
parent | e10811b1e2610d8b82221ec11d5300425777bec7 (diff) | |
download | merchant-backoffice-f4cbd85008d14a78433b9495cce48903192e2e0d.tar.gz merchant-backoffice-f4cbd85008d14a78433b9495cce48903192e2e0d.tar.bz2 merchant-backoffice-f4cbd85008d14a78433b9495cce48903192e2e0d.zip |
payto uri form
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] { |