gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] 02/02: payto uri form


From: gnunet
Subject: [taler-merchant-backoffice] 02/02: payto uri form
Date: Mon, 09 Aug 2021 19:37:42 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

commit f4cbd85008d14a78433b9495cce48903192e2e0d
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Aug 9 14:37:30 2021 -0300

    payto uri form
---
 .../frontend/src/components/form/InputGroup.tsx    |   9 +-
 .../src/components/form/InputPaytoForm.tsx         | 167 +++++++++++++++++++++
 .../instance/DefaultInstanceFormFields.tsx         |  17 +--
 .../frontend/src/paths/admin/create/CreatePage.tsx |  27 +---
 packages/frontend/src/paths/admin/list/View.tsx    |   6 +-
 .../src/paths/instance/update/UpdatePage.tsx       |  30 +---
 packages/frontend/src/scss/main.scss               |   1 +
 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] {

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]