merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 7fc5f0aa0b6e738521ee7339d1f0f0b20db9915d
parent 6d040035819e5c90a1bbe7940f4d204b2de5c1f4
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue,  4 May 2021 15:15:28 -0300

transfer list and notification

Diffstat:
MCHANGELOG.md | 6+-----
MDESIGN.md | 30++++++++++++++++++++++++++++++
Acrock.ts | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/InstanceRoutes.tsx | 8+++++++-
Apackages/frontend/src/components/form/InputLocation.tsx | 44++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/components/form/InputStock.tsx | 19++-----------------
Mpackages/frontend/src/components/form/useField.tsx | 4++--
Mpackages/frontend/src/hooks/transfer.ts | 4++--
Mpackages/frontend/src/paths/admin/create/CreatePage.tsx | 33++++-----------------------------
Mpackages/frontend/src/paths/instance/orders/create/CreatePage.tsx | 16++--------------
Apackages/frontend/src/paths/instance/transfers/create/CreatePage.tsx | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/paths/instance/transfers/create/index.tsx | 36+++++++++++++++++++++++++++++++++---
Mpackages/frontend/src/paths/instance/transfers/list/Table.tsx | 76+++++++++++++++++++++++++++++++++-------------------------------------------
Mpackages/frontend/src/paths/instance/transfers/list/index.tsx | 23++++++++++-------------
Mpackages/frontend/src/paths/instance/update/UpdatePage.tsx | 33++++-----------------------------
15 files changed, 375 insertions(+), 158 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -5,13 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Future work] - - gettext templates should be generated from the source code (#6791) - date format (error handling) - - allow point separator for amounts - red color when input is invalid (onchange) - validate everything using onChange - - feature: input as date format - replace Yup and type definition with a taler-library for the purpose (first wait Florian to refactor wallet core) - add more doc style comments @@ -21,12 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - fix mobile: some things are still on the left - edit button to go to instance settings - check if there is a way to remove auto async for /routes /components/{async,routes} so it can be turned on when building non-single-bundle - - product detail: we could have some button that brings us to the detailed screen for the product - navigation to another instance should not do full refresh - cleanup instance and token management, because code is a mess and can be refactored - unlock a product when is locked - - check that there is no place where the taxes are suming up + - check that there is no place where the taxes are summing up - translation missing: yup (check for some other dynamic message) ## [Unreleased] - fixed bug when updating token and not admin diff --git a/DESIGN.md b/DESIGN.md @@ -68,6 +68,36 @@ The core concepts are: * <Input /> an others: defines UI, create <input /> DOM controls and access the form with useField() +To use it you will need a state somewhere with the object holding all the form +information. + +``` +const [state, setState] = useState({ name: '', age: 11 }) +``` + +Optionally an error object an be built with the error messages + +``` +const errors = { + field1: undefined, + field2: 'should be greater than 18', +} +``` + +This 3 elements are use to setup the FormProvider + +``` +<FormProvider errors={errors} object={state} valueHandler={setState}> +...inputs +</FormProvider> +``` + +Inputs should handle UI rendering and use `useField(name)` to get: + + * error: the error message if there is one + * value: the current value of the object + * onChange: function to update the current field + # Custom Hooks diff --git a/crock.ts b/crock.ts @@ -0,0 +1,95 @@ +function getValue(chr: string): number { + let a = chr; + switch (chr) { + case "O": + case "o": + a = "0;"; + break; + case "i": + case "I": + case "l": + case "L": + a = "1"; + break; + case "u": + case "U": + a = "V"; + } + + if (a >= "0" && a <= "9") { + return a.charCodeAt(0) - "0".charCodeAt(0); + } + + if (a >= "a" && a <= "z") a = a.toUpperCase(); + let dec = 0; + if (a >= "A" && a <= "Z") { + if ("I" < a) dec++; + if ("L" < a) dec++; + if ("O" < a) dec++; + if ("U" < a) dec++; + return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec; + } + throw new Error('encoding'); +} + +const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +export function encodeCrock(data: ArrayBuffer): string { + const dataBytes = new Uint8Array(data); + let sb = ""; + const size = data.byteLength; + let bitBuf = 0; + let numBits = 0; + let pos = 0; + while (pos < size || numBits > 0) { + if (pos < size && numBits < 5) { + const d = dataBytes[pos++]; + bitBuf = (bitBuf << 8) | d; + numBits += 8; + } + if (numBits < 5) { + // zero-padding + bitBuf = bitBuf << (5 - numBits); + numBits = 5; + } + const v = (bitBuf >>> (numBits - 5)) & 31; + sb += encTable[v]; + numBits -= 5; + } + return sb; +} + +export function decodeCrock(encoded: string): Uint8Array { + const size = encoded.length; + let bitpos = 0; + let bitbuf = 0; + let readPosition = 0; + const outLen = Math.floor((size * 5) / 8); + const out = new Uint8Array(outLen); + let outPos = 0; + + while (readPosition < size || bitpos > 0) { + if (readPosition < size) { + const v = getValue(encoded[readPosition++]); + bitbuf = (bitbuf << 5) | v; + bitpos += 5; + } + while (bitpos >= 8) { + const d = (bitbuf >>> (bitpos - 8)) & 0xff; + out[outPos++] = d; + bitpos -= 8; + } + if (readPosition == size && bitpos > 0) { + bitbuf = (bitbuf << (8 - bitpos)) & 0xff; + bitpos = bitbuf == 0 ? 0 : 8; + } + } + return out; +} + +const bin = decodeCrock("PV2D1G85528VDSZGED8KE98FEXT3PWQ99WV590TNVKJKM3GM3GRG") +const base64String = Buffer + .from(String.fromCharCode.apply(null, bin)) + .toString('base64'); + +console.log(base64String) + diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -39,6 +39,7 @@ import ProductCreatePage from './paths/instance/products/create'; import ProductListPage from './paths/instance/products/list'; import ProductUpdatePage from './paths/instance/products/update'; import TransferListPage from './paths/instance/transfers/list'; +import TransferCreatePage from './paths/instance/transfers/create'; import InstanceUpdatePage, { Props as InstanceUpdatePageProps } from "./paths/instance/update"; import LoginPage from './paths/login'; import NotFoundPage from './paths/notfound'; @@ -62,7 +63,7 @@ export enum InstancePaths { // tips_new = '/tip/new', transfers_list = '/transfers', - // transfers_new = '/transfer/new', + transfers_new = '/transfer/new', } // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -245,8 +246,13 @@ export function InstanceRoutes({ id, admin }: Props): VNode { onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.update)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onCreate={() => { route(InstancePaths.transfers_new) }} /> + <Route path={InstancePaths.transfers_new} component={TransferCreatePage} + onConfirm={() => { route(InstancePaths.transfers_list) }} + onBack={() => { route(InstancePaths.transfers_list) }} + /> {/** * Example pages */} diff --git a/packages/frontend/src/components/form/InputLocation.tsx b/packages/frontend/src/components/form/InputLocation.tsx @@ -0,0 +1,43 @@ +/* + 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 { Fragment, h } from "preact"; +import { useTranslator } from "../../i18n"; +import { Input } from "./Input"; + +export function InputLocation({name}:{name:string}) { + const i18n = useTranslator() + return <> + <Input name={`${name}.country`} label={i18n`Country`} /> + <Input name={`${name}.address_lines`} inputType="multiline" + label={i18n`Address`} + toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} + fromStr={(v: string) => v.split('\n')} + /> + <Input name={`${name}.building_number`} label={i18n`Building number`} /> + <Input name={`${name}.building_name`} label={i18n`Building name`} /> + <Input name={`${name}.street`} label={i18n`Street`} /> + <Input name={`${name}.post_code`} label={i18n`Post code`} /> + <Input name={`${name}.town_location`} label={i18n`Town location`} /> + <Input name={`${name}.town`} label={i18n`Town`} /> + <Input name={`${name}.district`} label={i18n`District`} /> + <Input name={`${name}.country_subdivision`} label={i18n`Country subdivision`} /> + </> +} +\ No newline at end of file diff --git a/packages/frontend/src/components/form/InputStock.tsx b/packages/frontend/src/components/form/InputStock.tsx @@ -28,6 +28,7 @@ import { InputGroup } from "./InputGroup"; import { InputNumber } from "./InputNumber"; import { InputDate } from "./InputDate"; import { Translate, useTranslator } from "../../i18n"; +import { InputLocation } from "./InputLocation"; export interface Props<T> extends InputProps<T> { alreadyExist?: boolean; @@ -149,23 +150,7 @@ export function InputStock<T>({ name, readonly, placeholder, tooltip, label, hel <InputDate<Entity> name="nextRestock" label={i18n`Next restock`} withTimestampSupport /> <InputGroup<Entity> name="address" label={i18n`Delivery address`}> - - <Input name="address.country" label={i18n`Country`} /> - - <Input name="address.address_lines" inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - - <Input name="address.building_number" label={i18n`Building number`} /> - <Input name="address.building_name" label={i18n`Building name`} /> - <Input name="address.street" label={i18n`Street`} /> - <Input name="address.post_code" label={i18n`Post code`} /> - <Input name="address.town_location" label={i18n`Town location`} /> - <Input name="address.town" label={i18n`Town`} /> - <Input name="address.district" label={i18n`District`} /> - <Input name="address.country_subdivision" label={i18n`Country subdivision`} /> + <InputLocation name="address" /> </InputGroup> </FormProvider> </div> diff --git a/packages/frontend/src/components/form/useField.tsx b/packages/frontend/src/components/form/useField.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { VNode } from "preact"; +import { ComponentChildren, VNode } from "preact"; import { useFormContext } from "./FormProvider"; interface Use<V> { @@ -79,5 +79,5 @@ export interface InputProps<T> { placeholder?: string; tooltip?: string; readonly?: boolean; - help?:VNode; + help?: ComponentChildren; } \ No newline at end of file diff --git a/packages/frontend/src/hooks/transfer.ts b/packages/frontend/src/hooks/transfer.ts @@ -20,10 +20,10 @@ import useSWR from 'swr'; import { useInstanceContext } from '../context/instance'; async function transferFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> { - return request<T>(`${backend}${url}`, { token, params: { payto_uri: '' } }) + return request<T>(`${backend}${url}`, { token, params: { } }) } -export function useTransferMutateAPI(): TransferMutateAPI { +export function useTransferAPI(): TransferMutateAPI { const { url: baseUrl, token: adminToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx @@ -27,6 +27,7 @@ import { Input } from "../../../components/form/Input"; import { InputCurrency } from "../../../components/form/InputCurrency"; import { InputDuration } from "../../../components/form/InputDuration"; import { InputGroup } from "../../../components/form/InputGroup"; +import { InputLocation } from "../../../components/form/InputLocation"; import { InputPayto } from "../../../components/form/InputPayto"; import { InputSecured } from "../../../components/form/InputSecured"; import { InputWithAddon } from "../../../components/form/InputWithAddon"; @@ -87,7 +88,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { <InputSecured<Entity> name="auth_token" label={i18n`Auth token`} /> - <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} /> + <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} help="payto://x-taler-bank/bank.taler:5882/blogger" /> <InputCurrency<Entity> name="default_max_deposit_fee" label={i18n`Default max deposit fee`} /> @@ -96,37 +97,11 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { <Input<Entity> name="default_wire_fee_amortization" label={i18n`Default wire fee amortization`} /> <InputGroup name="address" label={i18n`Address`}> - <Input name="address.country" label={i18n`Country`} /> - <Input name="address.address_lines" inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name="address.building_number" label={i18n`Building number`} /> - <Input name="address.building_name" label={i18n`Building name`} /> - <Input name="address.street" label={i18n`Street`} /> - <Input name="address.post_code" label={i18n`Post code`} /> - <Input name="address.town_location" label={i18n`Town location`} /> - <Input name="address.town" label={i18n`Town`} /> - <Input name="address.district" label={i18n`District`} /> - <Input name="address.country_subdivision" label={i18n`Country subdivision`} /> + <InputLocation name="address" /> </InputGroup> <InputGroup name="jurisdiction" label={i18n`Jurisdiction`}> - <Input name="jurisdiction.country" label={i18n`Country`} /> - <Input name="jurisdiction.address_lines" inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name="jurisdiction.building_number" label={i18n`Building number`} /> - <Input name="jurisdiction.building_name" label={i18n`Building name`} /> - <Input name="jurisdiction.street" label={i18n`Street`} /> - <Input name="jurisdiction.post_code" label={i18n`Post code`} /> - <Input name="jurisdiction.town_location" label={i18n`Town location`} /> - <Input name="jurisdiction.town" label={i18n`Town`} /> - <Input name="jurisdiction.district" label={i18n`District`} /> - <Input name="jurisdiction.country_subdivision" label={i18n`Country subdivision`} /> + <InputLocation name="jurisdiction" /> </InputGroup> <InputDuration<Entity> name="default_pay_delay" label={i18n`Default pay delay`} /> diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -28,6 +28,7 @@ import { Input } from "../../../../components/form/Input"; import { InputCurrency } from "../../../../components/form/InputCurrency"; import { InputDate } from "../../../../components/form/InputDate"; import { InputGroup } from "../../../../components/form/InputGroup"; +import { InputLocation } from "../../../../components/form/InputLocation"; import { ProductList } from "../../../../components/product/ProductList"; import { useConfigContext } from "../../../../context/config"; import { MerchantBackend, WithId } from "../../../../declaration"; @@ -305,20 +306,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <InputDate name="payments.delivery_date" label={i18n`Delivery date`} /> {value.payments.delivery_date && <InputGroup name="payments.delivery_location" label={i18n`Location`} > - <Input name="payments.delivery_location.country" label={i18n`Country`} /> - <Input name="payments.delivery_location.address_lines" inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name="payments.delivery_location.building_number" label={i18n`Building number`} /> - <Input name="payments.delivery_location.building_name" label={i18n`Building name`} /> - <Input name="payments.delivery_location.street" label={i18n`Street`} /> - <Input name="payments.delivery_location.post_code" label={i18n`Post code`} /> - <Input name="payments.delivery_location.town_location" label={i18n`Town location`} /> - <Input name="payments.delivery_location.town" label={i18n`Town`} /> - <Input name="payments.delivery_location.district" label={i18n`District`} /> - <Input name="payments.delivery_location.country_subdivision" label={i18n`Country subdivision`} /> + <InputLocation name="payments.delivery_location" /> </InputGroup>} <InputCurrency name="payments.max_fee" label={i18n`Max fee`} /> diff --git a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx @@ -0,0 +1,105 @@ +/* + 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 } from "preact"; +import { useState } from "preact/hooks"; +import { FormErrors, FormProvider } from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon"; +import { MerchantBackend } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; + +type Entity = MerchantBackend.Transfers.TransferInformation + +interface Props { + onCreate: (d: Entity) => void; + onBack?: () => void; +} + +// # The encoded symbol space does not include I, L, O or U +// symbols = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' +// # These five symbols are exclusively for checksum values +// check_symbols = '*~$=U' + + +const CROCKFORD_BASE32_REGEX = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/ +const URL_REGEX = /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/ + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const i18n = useTranslator() + + const [state, setState] = useState<Partial<Entity>>({ + wtid: 'DCMGEM7F0DPW930M06C2AVNC6CFXT6HBQ2YVQH7EC8ZQ0W8SS9TG', + payto_uri: 'payto://x-taler-bank/bank.taler:5882/blogger', + exchange_url: 'http://exchange.taler:8081/', + credit_amount: 'COL:22.80' + }) + + const errors: FormErrors<Entity> = { + wtid: !state.wtid ? i18n`cannot be empty` : + (!CROCKFORD_BASE32_REGEX.test(state.wtid) ? i18n`check the id, doest look valid` : + (state.wtid.length !== 52 ? i18n`should have 52 characters, current ${state.wtid.length}` : + undefined)), + payto_uri: !state.payto_uri ? i18n`cannot be empty` : undefined, + credit_amount: !state.credit_amount ? i18n`cannot be empty` : undefined, + exchange_url: !state.exchange_url ? i18n`cannot be empty` : + (!URL_REGEX.test(state.exchange_url) ? i18n`URL doesn't have the right format` : undefined), + } + + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + + const submitForm = () => { + if (hasErrors) return + onCreate({ ...state, payto_uri: 'payto://x-taler-bank/bank.taler:5882/blogger' } as any) + } + + return <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + + <FormProvider object={state} valueHandler={setState} errors={errors}> + <Input<Entity> name="wtid" label={i18n`Transfer ID`} help="" /> + <InputWithAddon<Entity> name="payto_uri" + label={i18n`Account Address`} + addonBefore="payto://" + toStr={(v?: string) => v ? v.substring("payto://".length) : ''} + fromStr={(v: string) => !v ? '' : `payto://${v}`} + help="x-taler-bank/bank.taler:5882/blogger" /> + <Input<Entity> name="exchange_url" + label={i18n`Exchange URL`} + help="http://exchange.taler:8081/" /> + <InputCurrency<Entity> name="credit_amount" label={i18n`Amount`} /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} + <button class="button is-success" disabled={hasErrors} onClick={submitForm} ><Translate>Confirm</Translate></button> + </div> + + </div> + <div class="column" /> + </div> + </section> + </div> +} +\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/transfers/create/index.tsx b/packages/frontend/src/paths/instance/transfers/create/index.tsx @@ -19,8 +19,38 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode } from 'preact'; +import { Fragment, h, VNode } from 'preact'; +import { useState } from 'preact/hooks'; +import { NotificationCard } from '../../../../components/menu'; +import { MerchantBackend } from '../../../../declaration'; +import { useTransferAPI } from '../../../../hooks/transfer'; +import { useTranslator } from '../../../../i18n'; +import { Notification } from '../../../../utils/types'; +import { CreatePage } from './CreatePage'; -export default function CreateTransfer():VNode { - return <div>transfer create page</div> +export type Entity = MerchantBackend.Transfers.TransferInformation +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateTransfer({onConfirm, onBack}:Props): VNode { + const { informTransfer } = useTransferAPI() + const [notif, setNotif] = useState<Notification | undefined>(undefined) + const i18n = useTranslator() + + return <> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Transfers.TransferInformation) => { + informTransfer(request).then(() => onConfirm()).catch((error) => { + setNotif({ + message: i18n`could not inform transfer`, + type: "ERROR", + description: error.message + }) + }) + }} /> + </> } \ No newline at end of file diff --git a/packages/frontend/src/paths/instance/transfers/list/Table.tsx b/packages/frontend/src/paths/instance/transfers/list/Table.tsx @@ -14,15 +14,16 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** - * - * @author Sebastian Javier Marchano (sebasjm) - */ +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { format } from "date-fns" import { h, VNode } from "preact" import { StateUpdater, useEffect, useState } from "preact/hooks" import { MerchantBackend, WithId } from "../../../../declaration" -import { Translate } from "../../../../i18n" +import { Translate, useTranslator } from "../../../../i18n" import { Actions, buildActions } from "../../../../utils/table" type Entity = MerchantBackend.Transfers.TransferDetails & WithId @@ -32,6 +33,7 @@ interface Props { onUpdate: (id: string) => void; onDelete: (id: Entity) => void; onCreate: () => void; + accounts: string[]; selected?: boolean; } @@ -62,7 +64,7 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > - Delete + <Translate>Delete</Translate> </button> </div> <div class="card-header-icon" aria-label="more options"> @@ -96,46 +98,34 @@ function toggleSelected<T>(id: T): (prev: T[]) => T[] { return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) } -function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete }: TableProps): VNode { +function Table({ instances }: TableProps): VNode { + const i18n = useTranslator() return ( <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.length === instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> - <span class="check" /> - </label> - </th> - <th><Translate>ID</Translate></th> - <th><Translate>Name</Translate></th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} /> - <span class="check" /> - </label> - </td> - <td onClick={(): void => onUpdate(i.id)} style={{cursor: 'pointer'}} >{i.credit_amount}</td> - <td onClick={(): void => onUpdate(i.id)} style={{cursor: 'pointer'}} >{i.exchange_url}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> - Delete - </button> - </div> - </td> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th><Translate>Credit</Translate></th> + <th><Translate>Address</Translate></th> + <th><Translate>Exchange URL</Translate></th> + <th><Translate>Confirmed</Translate></th> + <th><Translate>Verified</Translate></th> + <th><Translate>Executed at</Translate></th> </tr> - })} - - </tbody> - </table> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td>{i.credit_amount}</td> + <td>{i.payto_uri}</td> + <td>{i.exchange_url}</td> + <td>{i.confirmed ? i18n`yes` : i18n`no`}</td> + <td>{i.verified ? i18n`yes` : i18n`no`}</td> + <td>{i.execution_time ? (i.execution_time.t_ms == 'never' ? i18n`never` : format(i.execution_time.t_ms, 'yyyy/MM/dd HH:mm:ss')) : i18n`unknown`}</td> + </tr> + })} + </tbody> + </table> </div>) } diff --git a/packages/frontend/src/paths/instance/transfers/list/index.tsx b/packages/frontend/src/paths/instance/transfers/list/index.tsx @@ -22,34 +22,31 @@ import { h, VNode } from 'preact'; import { Loading } from '../../../../components/exception/loading'; import { HttpError } from '../../../../hooks/backend'; -import { useInstanceTransfers, useTransferMutateAPI } from "../../../../hooks/transfer"; +import { useInstanceDetails } from '../../../../hooks/instance'; +import { useInstanceTransfers, useTransferAPI } from "../../../../hooks/transfer"; import { CardTable } from './Table'; interface Props { onUnauthorized: () => VNode; onLoadError: (error: HttpError) => VNode; onNotFound: () => VNode; + onCreate: () => void; } -export default function ListTransfer({ onUnauthorized, onLoadError, onNotFound }: Props): VNode { +export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, onNotFound }: Props): VNode { const result = useInstanceTransfers() - const { informTransfer } = useTransferMutateAPI() - + const instance = useInstanceDetails() + if (result.clientError && result.isUnauthorized) return onUnauthorized() if (result.clientError && result.isNotfound) return onNotFound() if (result.loading) return <Loading /> if (!result.ok) return onLoadError(result) + const accounts = !instance.ok? [] : instance.data.accounts.map(a => a.payto_uri) + return <section class="section is-main-section"> <CardTable instances={result.data.transfers.map(o => ({ ...o, id: String(o.transfer_serial_id) }))} - onCreate={() => informTransfer({ - wtid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - - // exchange: payto://x-taler-bank/bank.taler:5882/exchangeminator - // payto://x-taler-bank/bank.taler:5882/9?subject=qwe&amount=COL:10 - payto_uri: 'payto://x-taler-bank/bank.taler:5882/blogger', - exchange_url: 'http://exchange.taler:8081/', - credit_amount: 'COL:2' - })} + accounts={accounts} + onCreate={onCreate} onDelete={() => null} onUpdate={() => null} /> diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/update/UpdatePage.tsx @@ -27,6 +27,7 @@ import { Input } from "../../../components/form/Input"; import { InputCurrency } from "../../../components/form/InputCurrency"; import { InputDuration } from "../../../components/form/InputDuration"; import { InputGroup } from "../../../components/form/InputGroup"; +import { InputLocation } from "../../../components/form/InputLocation"; import { InputPayto } from "../../../components/form/InputPayto"; import { InputSecured } from "../../../components/form/InputSecured"; import { useInstanceContext } from "../../../context/instance"; @@ -106,7 +107,7 @@ export function UpdatePage({ onUpdate, selected, onBack }: Props): VNode { <InputSecured<Entity> name="auth_token" label={i18n`Auth token`} /> - <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} /> + <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} help="payto://x-taler-bank/bank.taler:5882/blogger" /> <InputCurrency<Entity> name="default_max_deposit_fee" label={i18n`Default max deposit fee`} /> @@ -115,37 +116,11 @@ export function UpdatePage({ onUpdate, selected, onBack }: Props): VNode { <Input<Entity> name="default_wire_fee_amortization" inputType="number" label={i18n`Default wire fee amortization`} /> <InputGroup name="address" label={i18n`Address`}> - <Input name="address.country" label={i18n`Country`} /> - <Input name="address.address_lines" inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name="address.building_number" label={i18n`Building number`} /> - <Input name="address.building_name" label={i18n`Building name`} /> - <Input name="address.street" label={i18n`Street`} /> - <Input name="address.post_code" label={i18n`Post code`} /> - <Input name="address.town_location" label={i18n`Town location`} /> - <Input name="address.town" label={i18n`Town`} /> - <Input name="address.district" label={i18n`District`} /> - <Input name="address.country_subdivision" label={i18n`Country subdivision`} /> + <InputLocation name="address" /> </InputGroup> <InputGroup name="jurisdiction" label={i18n`Jurisdiction`}> - <Input name="jurisdiction.country" label={i18n`Country`} /> - <Input name="jurisdiction.address_lines" inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name="jurisdiction.building_number" label={i18n`Building number`} /> - <Input name="jurisdiction.building_name" label={i18n`Building name`} /> - <Input name="jurisdiction.street" label={i18n`Street`} /> - <Input name="jurisdiction.post_code" label={i18n`Post code`} /> - <Input name="jurisdiction.town_location" label={i18n`Town location`} /> - <Input name="jurisdiction.town" label={i18n`Town`} /> - <Input name="jurisdiction.district" label={i18n`District`} /> - <Input name="jurisdiction.country_subdivision" label={i18n`Country subdivision`} /> + <InputLocation name="jurisdiction" /> </InputGroup> <InputDuration<Entity> name="default_pay_delay" label={i18n`Default pay delay`} />