diff options
author | Sebastian <sebasjm@gmail.com> | 2021-05-27 10:36:56 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-05-27 11:02:35 -0300 |
commit | 8e91c94d4733e035d5a078461205e86641190e70 (patch) | |
tree | 79aa1148988481d74445b9314c26bdd45110874f | |
parent | 1409f599d5c3ba5cf75b597fb38757fcd8e9f02f (diff) | |
download | merchant-backoffice-8e91c94d4733e035d5a078461205e86641190e70.tar.gz merchant-backoffice-8e91c94d4733e035d5a078461205e86641190e70.tar.bz2 merchant-backoffice-8e91c94d4733e035d5a078461205e86641190e70.zip |
some fixes
- api key remove from login, and bearer
- 'secret-token:' add missing, remove when the user set
- change the password and stay there logged in
- auth token section
- header with the instance id
- create a section with the auth token
- bug after clicking change, cycliing over change the password
- reserve created suffcefluy
- message => wire transfer subject
- reservers
- exchange https:// pattern (wallet util base url helper)
- valid wire method
- add missing titles in navbar
- loading spinner on slow backend
28 files changed, 611 insertions, 436 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bfb40..ad3634d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - prune scss styles to reduce size - 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 - - 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 @@ -26,132 +24,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - translation missing: yup (check for some ot her dynamic message) - contract terms - fulfillment url should check absolute url or relative to the merchant domain - - duplicate order button - - simplify order - - react routing refactor to use query parameters from history - create taler ui - contract terms in the wallet - - when backoffice get a response of the merchant that have info about the reponsse of the exchange, the error is not readed correctly... see wire transfer + - when backoffice get a response of the merchant that have info about the reponse of the exchange, the error is not readed correctly... see wire transfer - when creating the first default instance, the page keeps reloading preventing for filling the form - - - payto auto remove (add if is missing) - - back cancel in all dialog (order) - - transfer use the tabs instead of three state - - spining while working, - - disabled button grey out - - add timeout to the request - - hash - - delete button, without - - delete transaction if verified is missing + - api key remove from login, and bearer + - 'secret-token:' add missing, remove when the user set + - change the password and stay there logged in + - auth token section + - header with the instance id + - create a section with the auth token + - bug after clicking change, cycliing over change the password + - reserve created suffcefluy + - message => wire transfer subject + - reservers + - exchange https:// pattern (wallet util base url helper) + - valid wire method - - update instance - - auth token: status => external/managed + - create instance with auth token + - add more information about reserve when is not founded + - replace manipulation of amount with taler util libraries + - check, not running the backend, load the SPA, then start running the backend. after this click the login button should enter the app but doesnt do anything - - price not required: product page has required field that should not be required - - animation on load + +wallet + + - show transaction with error state + - add developer mode in settings, this will show debug tab + - add transaction details, and delete button ## [Unreleased] - - fixed bug when updating token and not admin - - showing a yellow bar on non-default instance navigation (admin) - -## [0.0.6] - 2021-03-25 - - complete order list information (#6793) - - complete product list information (#6792) - - missing fields in the instance update - - https://bugs.gnunet.org/view.php?id=6815 - - -## [0.0.5] - 2021-03-18 - - change the admin title to "instances" if we are listing the instances and "settings: $ID" on updating instances (#6790) - - update title with: Taler Backoffice: $PAGE_TITLE (#6790) - - paths should be /orders instead of /o (same others) - - if there is enough space for tables in mobile, make the scrollables (#6789) - - show create default instance if it is not already - - notifications should tale place between title and content, and not disapear (#6788) - - create a loading page to be use when the data is not ready (test at #/loading) - - confirmation page when creating instances - -## [0.0.4] - 2021-03-11 - - prevent letters to be input in numbers - - instance id in instance list should be clickable - - edit button to go to instance settings - - add order section - - add product section - - add tips section - - add transfers section - - initial state before login - - logout takes you to a initial state, not showing error messages - - change the admin title to "instances" if we are listing the instances and "settings: $ID" on updating instances - - update title with: Taler Backoffice: $PAGE_TITLE - -## [0.0.3] - 2021-03-04 - - submit form on key press == enter - - version of backoffice in sidebar - - fixed login dialog on mobile - - LangSelector ascomponent - - refactored Navigation and Sidebar - - do not display Logout when there is no token - - fix: Login Page should show on unauthorized - - fix: row clicking on card table was overriding checkbox onClick - - remove headers of the page - - clear all tokens now remove backend-url - - remove checkbox from auth token, use button (manage auth) - - auth token config as popup with 3 actions (clear (sure?), cancel, set token) - - new password enpoint - - bug: there is missing a mutate call when updating to remove the instance from cache - - -## [0.0.2] - 2021-02-25 - - REFACTOR: remove react-i18n and implement messageformat - - REFACTOR: routes definitions to allow nested routes and tokens - - REFACTOR: remove yup from input form defitions - - added PORT environment variable for `make dev` and `make serve` - - added `make dist` and `make install` - - remove last '/' on the backend url - - what happend if cannot access the config - - reorder the fields from the address/juriction section (take example) - - save every auth token of different instances - - remove footer - - show the connection state (url, currency, version) in the sidebar - - add backend url without slash - - added linter rule for source header - - bug: set text int the intpu date (seconds) - - row in the list instance are now clickable - - re implemented the language selector, remove the current lang from the dropdown - - remove payment adress and public key from instance listing - - fix bug on CORS error - - moved the login button to the sidebar - - remove the details page, go directly to the update page - - login modal: url before token, and removed the checkbox - - added payto:// to the field - - validate payto_uris on add ## [0.0.1] - 2021-02-18 ### Changed - - button of the form to the right - - add supported currency for Amount fields (like taler bank) - - rename name to business name - - change auth field to have a checkbox that activate the validation and show an input to set the token - - rename PayTo URI to bank account - - change id input to reflect that is going to be use in the url (prepend the backend url as a non editable and put the input after) - - refactor: change create popup to create page - - add the information popup into the fields to help with the description (like https://b2b.dab-bank.de/smartbroker/) - - take default lang from the browser for localization - - refactor update page - - Login button should be centered - - replace default exports for named exports ### Added - - implement taler built system (bootstrap, configure, makefile) - - implement pnpm - - take the url where the spa was loaded as a default backend url - - let the user change the backend url when ask for the auth token - - take the currency from merchant-backend - - change the input PayTO URI to a string field with a + button to add more - - format duration as human readable - - add copyright headers to every source file ### Deprecated diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx index 402c760..a7e4227 100644 --- a/packages/frontend/src/InstanceRoutes.tsx +++ b/packages/frontend/src/InstanceRoutes.tsx @@ -95,17 +95,20 @@ export function InstanceRoutes({ id, admin }: Props): VNode { addTokenCleaner(cleaner); }, [addTokenCleaner, cleaner]); - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url); - if (!token) return + const changeToken = (token?:string) => { if (admin) { updateToken(token); } else { updateDefaultToken(token) } + } + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url); + if (!token) return + changeToken(token) }; - const value = useMemo(() => ({ id, token, admin }), [id, token, admin]) + const value = useMemo(() => ({ id, token, admin, changeToken }), [id, token, admin]) const ServerErrorRedirectTo = (to: InstancePaths | AdminPaths) => (error: HttpError) => { setGlobalNotification({ @@ -297,14 +300,14 @@ export function Redirect({ to }: { to: string }): null { } function AdminInstanceUpdatePage({ id, ...rest }: { id: string } & InstanceUpdatePageProps) { - const [token, updateToken] = useBackendInstanceToken(id); - const value = useMemo(() => ({ id, token, admin: true }), [id, token]) + const [token, changeToken] = useBackendInstanceToken(id); const { changeBackend } = useBackendContext(); const updateLoginStatus = (url: string, token?: string) => { changeBackend(url); if (token) - updateToken(token); + changeToken(token); }; + const value = useMemo(() => ({ id, token, admin: true, changeToken }), [id, token]) const i18n = useTranslator(); return <InstanceContextProvider value={value}> <InstanceUpdatePage {...rest} diff --git a/packages/frontend/src/components/exception/loading.tsx b/packages/frontend/src/components/exception/loading.tsx index 40c7c7b..f2139a1 100644 --- a/packages/frontend/src/components/exception/loading.tsx +++ b/packages/frontend/src/components/exception/loading.tsx @@ -23,10 +23,10 @@ import { h, VNode } from "preact"; export function Loading(): VNode { return <div class="columns is-centered is-vcentered" style={{ height: 'calc(100% - 3rem)', position: 'absolute', width: '100%' }}> - <div class="column is-one-fifth"> - <div class="lds-ring"> - <div /><div /><div /><div /> - </div> - </div> + <Spinner /> </div> +} + +export function Spinner(): VNode { + return <div class="lds-ring"><div /><div /><div /><div /></div> }
\ No newline at end of file diff --git a/packages/frontend/src/components/exception/login.tsx b/packages/frontend/src/components/exception/login.tsx index 1132bfe..611af1a 100644 --- a/packages/frontend/src/components/exception/login.tsx +++ b/packages/frontend/src/components/exception/login.tsx @@ -36,6 +36,13 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { const { admin, token: instanceToken } = useInstanceContext() const [token, setToken] = useState(!admin ? baseToken : instanceToken || '') + function updateToken(token:string) { + const value = token && token.startsWith('secret-token:')? + token.substring('secret-token:'.length) : token + + setToken(`secret-token:${value}`) + } + const [url, setURL] = useState(backendUrl) const i18n = useTranslator() @@ -46,7 +53,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { <p class="modal-card-title">{i18n`Login required`}</p> </header> <section class="modal-card-body" style={{ border: '1px solid', borderTop: 0, borderBottom: 0 }}> - {i18n`Please enter your auth token. Token should have "secret-token:" and start with Bearer or ApiKey`} + {i18n`Please enter your auth token.`} <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label">URL</label> @@ -73,7 +80,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { <input class="input" type="text" placeholder={"set new token"} name="token" onKeyPress={e => e.keyCode === 13 ? onConfirm(url, token ? token : undefined) : null} value={token} - onInput={(e): void => setToken(e?.currentTarget.value)} + onInput={(e): void => updateToken(e?.currentTarget.value)} /> </p> </div> diff --git a/packages/frontend/src/components/form/InputSecured.tsx b/packages/frontend/src/components/form/InputSecured.tsx index 6e3059b..64737e3 100644 --- a/packages/frontend/src/components/form/InputSecured.tsx +++ b/packages/frontend/src/components/form/InputSecured.tsx @@ -21,7 +21,6 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Translate } from "../../i18n"; -import { UpdateTokenModal } from "../modal"; import { InputProps, useField } from "./useField"; export type Props<T> = InputProps<T>; @@ -40,7 +39,7 @@ export function InputSecured<T>({ name, readonly, placeholder, tooltip, label, h const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name); const [active, setActive] = useState(false); - const [newValue, setNuewValue] = useState(""); + const [newValue, setNuewValue] = useState("") return <Fragment> <div class="field is-horizontal"> @@ -53,22 +52,65 @@ export function InputSecured<T>({ name, readonly, placeholder, tooltip, label, h </label> </div> <div class="field-body is-flex-grow-3"> - <div class="field has-addons"> - <button class="button" onClick={(): void => { setActive(!active); }} > - <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> - <span><Translate>Manage token</Translate></span> - </button> - <TokenStatus prev={initial} post={value} /> - </div> + {!active ? + <Fragment> + <div class="field has-addons"> + <button class="button" onClick={(): void => { setActive(!active); }} > + <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> + <span><Translate>Manage token</Translate></span> + </button> + <TokenStatus prev={initial} post={value} /> + </div> + </Fragment> : + <Fragment> + <div class="field has-addons"> + <div class="control"> + <a class="button is-static">secret-token:</a> + </div> + <div class="control is-expanded"> + <input class="input" type="text" + placeholder={placeholder} readonly={readonly || !active} + disabled={readonly || !active} + name={String(name)} value={newValue} + onInput={(e): void => { + setNuewValue(e.currentTarget.value) + }} /> + {help} + </div> + <div class="control"> + <button class="button is-info" disabled={fromStr(newValue) === value} onClick={(): void => { onChange(fromStr(newValue)); setActive(!active); setNuewValue(""); }} > + <div class="icon is-left"><i class="mdi mdi-lock-outline" /></div> + <span><Translate>Update</Translate></span> + </button> + </div> + </div> + </Fragment> + } {error ? <p class="help is-danger">{error}</p> : null} </div> </div> - {active && <UpdateTokenModal oldToken={initial} - onCancel={() => { onChange(initial!); setActive(false); }} - onClear={() => { onChange(null!); setActive(false); }} - onConfirm={(newToken) => { - onChange(newToken as any); setActive(false) - }} - />} + {active && + <div class="field is-horizontal"> + <div class="field-body is-flex-grow-3"> + <div class="level" style={{ width: '100%' }}> + <div class="level-right is-flex-grow-1"> + <div class="level-item"> + <button class="button is-danger" disabled={null === value || undefined === value} onClick={(): void => { onChange(null!); setActive(!active); setNuewValue(""); }} > + <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div> + <span><Translate>Remove</Translate></span> + </button> + </div> + <div class="level-item"> + <button class="button " onClick={(): void => { onChange(initial!); setActive(!active); setNuewValue(""); }} > + <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div> + <span><Translate>Cancel</Translate></span> + </button> + </div> + </div> + + </div> + </div> + </div> + } </Fragment >; } diff --git a/packages/frontend/src/components/menu/index.tsx b/packages/frontend/src/components/menu/index.tsx index 5140eb0..31826ed 100644 --- a/packages/frontend/src/components/menu/index.tsx +++ b/packages/frontend/src/components/menu/index.tsx @@ -30,16 +30,16 @@ function getInstanceTitle(path: string, id: string): string { // case InstancePaths.details: return `${id}` case InstancePaths.update: return `${id}: Settings` case InstancePaths.order_list: return `${id}: Orders` - // case InstancePaths.order_new: return `${id}: New order` + case InstancePaths.order_new: return `${id}: New order` case InstancePaths.order_details: return `${id}: Detail of the order` case InstancePaths.product_list: return `${id}: Products` case InstancePaths.product_new: return `${id}: New product` case InstancePaths.product_update: return `${id}: Update product` - // case InstancePaths.tips_list: return `${id}: Tips` - // case InstancePaths.tips_new: return `${id}: New tip` - // case InstancePaths.tips_update: return `${id}: Update tip` + case InstancePaths.reserves_details: return `${id}: Detail of a reserve` + case InstancePaths.reserves_new: return `${id}: New reserve` + case InstancePaths.reserves_list: return `${id}: Reserves` case InstancePaths.transfers_list: return `${id}: Transfers` - // case InstancePaths.transfers_new: return `${id}: New Transfer` + case InstancePaths.transfers_new: return `${id}: New transfer` default: return ''; } } diff --git a/packages/frontend/src/components/modal/index.tsx b/packages/frontend/src/components/modal/index.tsx index 747019c..cba1ce8 100644 --- a/packages/frontend/src/components/modal/index.tsx +++ b/packages/frontend/src/components/modal/index.tsx @@ -25,6 +25,7 @@ import { useState } from "preact/hooks"; import { useInstanceContext } from "../../context/instance"; import { Translate, useTranslator } from "../../i18n"; import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants"; +import { Loading, Spinner } from "../exception/loading"; import { FormProvider } from "../form/FormProvider"; import { Input } from "../form/Input"; @@ -167,20 +168,23 @@ export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: Upd export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode { const i18n = useTranslator() - return <div class={"modal is-active"}> + return <div class="modal is-active"> <div class="modal-background " onClick={onCancel} /> <div class="modal-card"> <header class="modal-card-head"> - <p class="modal-card-title"><Translate>Operation is taking to much time</Translate></p> + <p class="modal-card-title"><Translate>Operation in progress...</Translate></p> </header> <section class="modal-card-body"> - <p><Translate>You can wait a little longer or abort the request to the backend. If the problem persist - contact the administrator.</Translate></p> + <div class="columns"> + <div class="column" /> + <Spinner /> + <div class="column" /> + </div> <p>{i18n`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p> </section> <footer class="modal-card-foot"> <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={onCancel} ><Translate>Abort</Translate></button> + <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button> </div> </footer> </div> diff --git a/packages/frontend/src/components/notifications/CreatedSuccessfully.tsx b/packages/frontend/src/components/notifications/CreatedSuccessfully.tsx index 8e2eee2..5f10e4b 100644 --- a/packages/frontend/src/components/notifications/CreatedSuccessfully.tsx +++ b/packages/frontend/src/components/notifications/CreatedSuccessfully.tsx @@ -26,9 +26,9 @@ interface Props { } export function CreatedSuccessfully({ children, onConfirm, onCreateAnother }: Props): VNode { - return <div class="columns is-fullwidth is-vcentered content-full-size"> + return <div class="columns is-fullwidth is-vcentered mt-3"> <div class="column" /> - <div class="column is-three-quarters"> + <div class="column is-four-fifths"> <div class="card"> <header class="card-header has-background-success"> <p class="card-header-title has-text-white-ter"> diff --git a/packages/frontend/src/components/product/ProductForm.tsx b/packages/frontend/src/components/product/ProductForm.tsx index 2729247..f6d52a7 100644 --- a/packages/frontend/src/components/product/ProductForm.tsx +++ b/packages/frontend/src/components/product/ProductForm.tsx @@ -95,7 +95,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist, }: Props) { return <div> <FormProvider<Entity> name="product" errors={errors} object={value} valueHandler={valueHandler} > - {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" addonBefore={`${backend.url}/product/`} label={i18n`ID`} tooltip={i18n`unique name identification`} />} + {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" addonBefore={`${backend.url}/product/`} label={i18n`ID`} tooltip={i18n`display name identification`} />} <InputImage<Entity> name="image" label={i18n`Image`} tooltip={i18n`photo of the product`} /> <Input<Entity> name="description" inputType="multiline" label={i18n`Description`} tooltip={i18n`full-length description`} /> diff --git a/packages/frontend/src/context/backend.ts b/packages/frontend/src/context/backend.ts index e33cdd5..c1b2c14 100644 --- a/packages/frontend/src/context/backend.ts +++ b/packages/frontend/src/context/backend.ts @@ -47,7 +47,11 @@ const BackendContext = createContext<BackendContextType>({ export function useBackendContextState(): BackendContextType { const [url, triedToLog, changeBackend, resetBackend] = useBackendURL(); - const [token, updateToken] = useBackendDefaultToken(); + const [token, _updateToken] = useBackendDefaultToken(); + const updateToken = (t?:string) => { + // console.log("update token", t) + _updateToken(t) + } const tokenCleaner = useCallback(() => { updateToken(undefined) }, []) const [cleaners, setCleaners] = useState([tokenCleaner]) diff --git a/packages/frontend/src/context/instance.ts b/packages/frontend/src/context/instance.ts index 0edfbfe..fecf364 100644 --- a/packages/frontend/src/context/instance.ts +++ b/packages/frontend/src/context/instance.ts @@ -26,6 +26,7 @@ interface Type { id: string; token?: string; admin?: boolean; + changeToken: (t?:string) => void; } const Context = createContext<Type>({} as any) diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts index 04a2e2d..6717566 100644 --- a/packages/frontend/src/declaration.d.ts +++ b/packages/frontend/src/declaration.d.ts @@ -51,6 +51,51 @@ type Amount = string; type UUID = string; type Integer = number; +export namespace ExchangeBackend { + interface WireResponse { + + // Master public key of the exchange, must match the key returned in /keys. + master_public_key: EddsaPublicKey; + + // Array of wire accounts operated by the exchange for + // incoming wire transfers. + accounts: WireAccount[]; + + // Object mapping names of wire methods (i.e. "sepa" or "x-taler-bank") + // to wire fees. + fees: { method : AggregateTransferFee }; + } + interface WireAccount { + // payto:// URI identifying the account and wire method + payto_uri: string; + + // Signature using the exchange's offline key + // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS. + master_sig: EddsaSignature; + } + interface AggregateTransferFee { + // Per transfer wire transfer fee. + wire_fee: Amount; + + // Per transfer closing fee. + closing_fee: Amount; + + // What date (inclusive) does this fee go into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + start_date: Timestamp; + + // What date (exclusive) does this fee stop going into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + end_date: Timestamp; + + // Signature of TALER_MasterWireFeePS with + // purpose TALER_SIGNATURE_MASTER_WIRE_FEES. + sig: EddsaSignature; + } + +} export namespace MerchantBackend { interface ErrorDetail { diff --git a/packages/frontend/src/hooks/instance.ts b/packages/frontend/src/hooks/instance.ts index 4bb6410..2e5c6c2 100644 --- a/packages/frontend/src/hooks/instance.ts +++ b/packages/frontend/src/hooks/instance.ts @@ -21,7 +21,7 @@ import { useInstanceContext } from '../context/instance'; interface InstanceAPI { - updateInstance: (data: MerchantBackend.Instances.InstanceReconfigurationMessage, a?: MerchantBackend.Instances.InstanceAuthConfigurationMessage) => Promise<void>; + updateInstance: (data: MerchantBackend.Instances.InstanceReconfigurationMessage) => Promise<void>; deleteInstance: () => Promise<void>; clearToken: () => Promise<void>; setNewToken: (token: string) => Promise<void>; @@ -33,19 +33,13 @@ export function useInstanceAPI(): InstanceAPI { const url = !admin ? baseUrl : `${baseUrl}/instances/${id}` - const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage, auth?: MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => { + const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { await request(`${url}/private/`, { method: 'patch', token, data: instance }) - if (auth) await request(`${url}/private/auth`, { - method: 'post', - token, - data: auth - }) - if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null) mutate([`/private/`, token, url], null) }; diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx index 4cf4b3c..aa46f2f 100644 --- a/packages/frontend/src/paths/admin/create/CreatePage.tsx +++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx @@ -56,7 +56,6 @@ function with_defaults(id?: string): Partial<Entity> { export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { const [value, valueHandler] = useState(with_defaults(forceId)) - // const [errors, setErrors] = useState<FormErrors<Entity>>({}) let errors: FormErrors<Entity> = {} try { @@ -86,7 +85,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { <div class="column is-two-thirds"> <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - <InputWithAddon<Entity> name="id" label={i18n`ID`} addonBefore={`${backend.url}/private/instances/`} readonly={!!forceId} tooltip={i18n`unique name identification`} /> + <InputWithAddon<Entity> name="id" label={i18n`ID`} addonBefore={`${backend.url}/private/instances/`} readonly={!!forceId} tooltip={i18n`display name identification`} /> <Input<Entity> name="name" label={i18n`Name`} tooltip={i18n`descriptive name`} /> diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx index 140d0c5..9d22fc8 100644 --- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -88,43 +88,45 @@ interface Entity { export function CreatePage({ onCreate, onBack }: Props): VNode { const [value, valueHandler] = useState(with_defaults()) - const [errors, setErrors] = useState<FormErrors<Entity>>({}) + // const [errors, setErrors] = useState<FormErrors<Entity>>({}) const inventoryList = Object.values(value.inventoryProducts) const productList = Object.values(value.products) + let errors: FormErrors<Entity> = {} + try { + schema.validateSync(value, { abortEarly: false }) + } catch (err) { + const yupErrors = err.inner as yup.ValidationError[] + errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) + } + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + const submit = (): void => { - try { - schema.validateSync(value, { abortEarly: false }) - const order = schema.cast(value) - - const request: MerchantBackend.Orders.PostOrderRequest = { - order: { - amount: order.pricing.order_price, - summary: order.pricing.summary, - products: productList, - extra: value.extra, - pay_deadline: value.payments.pay_deadline ? { t_ms: Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000 } : undefined, - wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000 } : undefined, - refund_deadline: value.payments.refund_deadline ? { t_ms: Math.floor(value.payments.refund_deadline.getTime() / 1000) * 1000 } : undefined, - max_fee: value.payments.max_fee, - max_wire_fee: value.payments.max_wire_fee, - delivery_date: value.payments.delivery_date ? { t_ms: value.payments.delivery_date.getTime() } : undefined, - delivery_location: value.payments.delivery_location, - fulfillment_url: value.payments.fullfilment_url, - }, - inventory_products: inventoryList.map(p => ({ - product_id: p.product.id, - quantity: p.quantity - })), - } - - onCreate(request); - } catch (err) { - const errors = err.inner as yup.ValidationError[] - const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) - setErrors(pathMessages) + const order = schema.cast(value) + + const request: MerchantBackend.Orders.PostOrderRequest = { + order: { + amount: order.pricing.order_price, + summary: order.pricing.summary, + products: productList, + extra: value.extra, + pay_deadline: value.payments.pay_deadline ? { t_ms: Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000 } : undefined, + wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000 } : undefined, + refund_deadline: value.payments.refund_deadline ? { t_ms: Math.floor(value.payments.refund_deadline.getTime() / 1000) * 1000 } : undefined, + max_fee: value.payments.max_fee, + max_wire_fee: value.payments.max_wire_fee, + delivery_date: value.payments.delivery_date ? { t_ms: value.payments.delivery_date.getTime() } : undefined, + delivery_location: value.payments.delivery_location, + fulfillment_url: value.payments.fullfilment_url, + }, + inventory_products: inventoryList.map(p => ({ + product_id: p.product.id, + quantity: p.quantity + })), } + + onCreate(request); } const config = useConfigContext() @@ -313,7 +315,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <div class="buttons is-right mt-5"> {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} - <button class="button is-success" onClick={submit} ><Translate>Confirm</Translate></button> + <button class="button is-success" onClick={submit} disabled={hasErrors} ><Translate>Confirm</Translate></button> </div> </div> diff --git a/packages/frontend/src/paths/instance/orders/create/index.tsx b/packages/frontend/src/paths/instance/orders/create/index.tsx index 01d2e6c..ee0577a 100644 --- a/packages/frontend/src/paths/instance/orders/create/index.tsx +++ b/packages/frontend/src/paths/instance/orders/create/index.tsx @@ -42,11 +42,6 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { return <Fragment> - <NotificationCard notification={{ - message: 'DEMO', - type: 'WARN', - description: 'this can be created as a popup or be expanded with more options' - }} /> <NotificationCard notification={notif} /> diff --git a/packages/frontend/src/paths/instance/products/list/Table.tsx b/packages/frontend/src/paths/instance/products/list/Table.tsx index 85e5ce4..878506d 100644 --- a/packages/frontend/src/paths/instance/products/list/Table.tsx +++ b/packages/frontend/src/paths/instance/products/list/Table.tsx @@ -242,7 +242,7 @@ function EmptyTable(): VNode { <p> <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> </p> - <p><Translate>There is no instances yet, add more pressing the + sign</Translate></p> + <p><Translate>There is no products yet, add more pressing the + sign</Translate></p> </div> } diff --git a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx index 6c079e6..a98e665 100644 --- a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx @@ -19,14 +19,18 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; import { FormErrors, FormProvider } from "../../../../components/form/FormProvider"; import { Input } from "../../../../components/form/Input"; import { InputCurrency } from "../../../../components/form/InputCurrency"; -import { MerchantBackend } from "../../../../declaration"; +import { ExchangeBackend, MerchantBackend } from "../../../../declaration"; import { Translate, useTranslator } from "../../../../i18n"; import { AsyncButton } from "../../../../components/exception/AsyncButton"; +import { canonicalizeBaseUrl, ExchangeKeysJson } from "@gnu-taler/taler-util" +import { PAYTO_WIRE_METHOD_LOOKUP, URL_REGEX } from "../../../../utils/constants"; +import { request } from "../../../../hooks/backend"; +import { InputSelector } from "../../../../components/form/InputSelector"; type Entity = MerchantBackend.Tips.ReserveCreateRequest @@ -36,38 +40,122 @@ interface Props { } -export function CreatePage({ onCreate, onBack }: Props): VNode { - const [reserve, setReserve] = useState<Partial<Entity>>({}) +enum Steps { + EXCHANGE, + WIRE_METHOD, +} + +interface ViewProps { + step : Steps, + setCurrentStep: (s:Steps) => void; + reserve: Partial<Entity>; + onBack?: () => void; + submitForm: () => Promise<void>; + setReserve: StateUpdater<Partial<Entity>>; +} +function ViewStep({ step, setCurrentStep, reserve, onBack, submitForm, setReserve }: ViewProps): VNode { const i18n = useTranslator() + const [wireMethods, setWireMethods] = useState<Array<string>>([]) + const [exchangeQueryError, setExchangeQueryError] = useState<string|undefined>(undefined) + + useEffect(() => { + setExchangeQueryError(undefined) + }, [reserve.exchange_url]) + + switch (step) { + case Steps.EXCHANGE: { + const errors: FormErrors<Entity> = { + initial_balance: !reserve.initial_balance ? 'cannot be empty' : !(parseInt(reserve.initial_balance.split(':')[1], 10) > 0) ? i18n`it should be greater than 0` : undefined, + exchange_url: !reserve.exchange_url ? i18n`cannot be empty` : !URL_REGEX.test(reserve.exchange_url) ? i18n`must be a valid URL` : !!exchangeQueryError ? exchangeQueryError : undefined, + } + + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + + return <Fragment> + <FormProvider<Entity> object={reserve} errors={errors} valueHandler={setReserve}> + <InputCurrency<Entity> name="initial_balance" label={i18n`Initial balance`} /> + <Input<Entity> name="exchange_url" label={i18n`Exchange URL`} /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} + <AsyncButton onClick={() => { + return request<ExchangeBackend.WireResponse>(`${reserve.exchange_url}wire`).then(r => { + const wireMethods = r.data.accounts.map(a => { + const match = PAYTO_WIRE_METHOD_LOOKUP.exec(a.payto_uri) + return match && match[1] || '' + }) + setWireMethods(wireMethods) + setCurrentStep(Steps.WIRE_METHOD) + return + }).catch((r: any) => { + setExchangeQueryError(r.message) + }) + }} disabled={hasErrors} ><Translate>Next</Translate></AsyncButton> + </div> + </Fragment> + } - const errors: FormErrors<Entity> = { - initial_balance: !reserve.initial_balance ? 'cannot be empty' : !(parseInt(reserve.initial_balance.split(':')[1], 10) > 0) ? i18n`it should be greater than 0` : undefined, - exchange_url: !reserve.exchange_url ? i18n`cannot be empty` : undefined, - wire_method: !reserve.wire_method ? i18n`cannot be empty` : undefined, + case Steps.WIRE_METHOD: { + const errors: FormErrors<Entity> = { + wire_method: !reserve.wire_method ? i18n`cannot be empty` : undefined, + } + + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + return <Fragment> + <FormProvider<Entity> object={reserve} errors={errors} valueHandler={setReserve}> + <InputCurrency<Entity> name="initial_balance" label={i18n`Initial balance`} readonly /> + <Input<Entity> name="exchange_url" label={i18n`Exchange URL`} readonly /> + <InputSelector<Entity> name="wire_method" label={i18n`Wire method`} values={wireMethods} placeholder={i18n`Select one wire method`}/> + </FormProvider> + <div class="buttons is-right mt-5"> + {onBack && <button class="button" onClick={() => setCurrentStep(Steps.EXCHANGE)} ><Translate>Back</Translate></button>} + <AsyncButton onClick={submitForm} disabled={hasErrors} ><Translate>Confirm</Translate></AsyncButton> + </div> + </Fragment> + + } } +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [reserve, setReserve] = useState<Partial<Entity>>({}) - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) const submitForm = () => { - if (hasErrors) return Promise.reject() return onCreate(reserve as Entity) } + const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE) + + return <div> <section class="section is-main-section"> <div class="columns"> <div class="column" /> <div class="column is-two-thirds"> - <FormProvider<Entity> object={reserve} errors={errors} valueHandler={setReserve}> - <InputCurrency<Entity> name="initial_balance" label={i18n`Initial balance`} /> - <Input<Entity> name="exchange_url" label={i18n`Exchange`} /> - <Input<Entity> name="wire_method" label={i18n`Wire method`} /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} - <AsyncButton onClick={submitForm} disabled={hasErrors} ><Translate>Confirm</Translate></AsyncButton> + + <div class="tabs is-toggle is-fullwidth is-small"> + <ul> + <li class={currentStep === Steps.EXCHANGE?"is-active":""}> + <a style={{ cursor: 'initial' }}> + <span>Set exchange</span> + </a> + </li> + <li class={currentStep === Steps.WIRE_METHOD?"is-active":""}> + <a style={{ cursor: 'initial' }}> + <span>Set wire method</span> + </a> + </li> + </ul> </div> + + <ViewStep step={currentStep} reserve={reserve} + setCurrentStep={setCurrentStep} + setReserve={setReserve} + submitForm={submitForm} + onBack={onBack} + /> </div> <div class="column" /> </div> diff --git a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx index af27c5b..2deae14 100644 --- a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx +++ b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx @@ -16,8 +16,9 @@ import { h, VNode } from "preact"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully"; import { MerchantBackend } from "../../../../declaration"; +import { Translate } from "../../../../i18n"; -type Entity = MerchantBackend.Tips.ReserveCreateConfirmation; +type Entity = {request: MerchantBackend.Tips.ReserveCreateRequest, response: MerchantBackend.Tips.ReserveCreateConfirmation}; interface Props { entity: Entity; @@ -30,27 +31,57 @@ export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Prop return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> <div class="field is-horizontal"> <div class="field-label is-normal"> + <label class="label">Exchange</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={entity.request.exchange_url} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={entity.request.initial_balance} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> <label class="label">Account address</label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input readonly class="input" value={entity.payto_uri} /> + <input readonly class="input" value={entity.response.payto_uri} /> </p> </div> </div> </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Message</label> + <label class="label">Subject</label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input class="input" readonly value={entity.reserve_pub} /> + <input class="input" readonly value={entity.response.reserve_pub} /> </p> </div> </div> </div> + <p class="is-size-5"><Translate>Now you should transfer to the exchange into the account address indicated above and the transaction must carry the subject message.</Translate></p> + + <p class="is-size-5"><Translate>For example:</Translate></p> + <pre> + {entity.response.payto_uri}?message={entity.response.reserve_pub}&amount={entity.request.initial_balance} + </pre> </Template>; } diff --git a/packages/frontend/src/paths/instance/reserves/create/index.tsx b/packages/frontend/src/paths/instance/reserves/create/index.tsx index ad990e9..eb16f85 100644 --- a/packages/frontend/src/paths/instance/reserves/create/index.tsx +++ b/packages/frontend/src/paths/instance/reserves/create/index.tsx @@ -28,7 +28,6 @@ import { useTranslator } from '../../../../i18n'; import { Notification } from '../../../../utils/types'; import { CreatedSuccessfully } from './CreatedSuccessfully'; import { CreatePage } from './CreatePage'; - interface Props { onBack: () => void; onConfirm: () => void; @@ -38,7 +37,10 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode { const [notif, setNotif] = useState<Notification | undefined>(undefined) const i18n = useTranslator() - const [createdOk, setCreatedOk] = useState<MerchantBackend.Tips.ReserveCreateConfirmation | undefined>(undefined); + const [createdOk, setCreatedOk] = useState<{ + request: MerchantBackend.Tips.ReserveCreateRequest, + response: MerchantBackend.Tips.ReserveCreateConfirmation + } | undefined>(undefined); if (createdOk) { return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} /> @@ -49,7 +51,7 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode { <CreatePage onBack={onBack} onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => { - return createReserve(request).then((r) => setCreatedOk(r.data)).catch((error) => { + return createReserve(request).then((r) => setCreatedOk({ request, response: r.data })).catch((error) => { setNotif({ message: i18n`could not create reserve`, type: "ERROR", diff --git a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx index 3771337..08b463a 100644 --- a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx +++ b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx @@ -44,9 +44,9 @@ interface Props { selected: Entity; } -export function DetailPage({ selected }: Props): VNode { +export function DetailPage({ selected, onBack }: Props): VNode { const i18n = useTranslator() - return <Fragment> + return <div class="section main-section"> <FormProvider object={selected} valueHandler={null} > <InputDate<Entity> name="creation_time" label={i18n`Created at`} readonly /> <InputDate<Entity> name="expiration_time" label={i18n`Valid until`} readonly /> @@ -58,7 +58,17 @@ export function DetailPage({ selected }: Props): VNode { {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} /> : <div> no tips for this reserve </div>} - </Fragment> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + <div class="buttons is-right mt-5"> + <button class="button" onClick={onBack}><Translate>Back</Translate></button> + </div> + </div> + <div class="column" /> + </div> + + </div> } async function copyToClipboard(text: string) { @@ -101,10 +111,10 @@ function TipRow({ id, entry }: { id: string, entry: MerchantBackend.Tips.TipStat } if (!result.ok) { return <tr> - <td>...</td> - <td>{entry.total_amount}</td> + <td>...</td> {/* authorized */} <td>{entry.total_amount}</td> - <td>expired</td> + <td>{entry.reason}</td> + <td>...</td> {/* expired */} </tr> } const info = result.data diff --git a/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx b/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx index 42847ff..6f97815 100644 --- a/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx +++ b/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx @@ -44,19 +44,20 @@ export function AuthorizeTipModal({ onCancel, onConfirm, tipAuthorized }: Author type State = MerchantBackend.Tips.TipCreateRequest const [form, setValue] = useState<Partial<State>>({}) const i18n = useTranslator(); - const [errors, setErrors] = useState<FormErrors<State>>({}) - const validateAndConfirm = () => { - try { - AuthorizeTipSchema.validateSync(form, { abortEarly: false }) - onConfirm(form as State) - } catch (err) { - const errors = err.inner as any[] - const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) - setErrors(pathMessages) - } + // const [errors, setErrors] = useState<FormErrors<State>>({}) + let errors: FormErrors<State> = {} + try { + AuthorizeTipSchema.validateSync(form, { abortEarly: false }) + } catch (err) { + const yupErrors = err.inner as any[] + errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) } + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + const validateAndConfirm = () => { + onConfirm(form as State) + } if (tipAuthorized) { return <ContinueModal description="tip" active onConfirm={onCancel}> <CreatedSuccessfully @@ -67,7 +68,7 @@ export function AuthorizeTipModal({ onCancel, onConfirm, tipAuthorized }: Author </ContinueModal> } - return <ConfirmModal description="tip" active onCancel={onCancel} onConfirm={validateAndConfirm}> + return <ConfirmModal description="tip" active onCancel={onCancel} disabled={hasErrors} onConfirm={validateAndConfirm}> <FormProvider<State> errors={errors} object={form} valueHandler={setValue} > <InputCurrency<State> name="amount" label={i18n`Amount`} tooltip={i18n`amount of tip`}/> diff --git a/packages/frontend/src/paths/instance/reserves/list/Table.tsx b/packages/frontend/src/paths/instance/reserves/list/Table.tsx index 58a2df5..a21de64 100644 --- a/packages/frontend/src/paths/instance/reserves/list/Table.tsx +++ b/packages/frontend/src/paths/instance/reserves/list/Table.tsx @@ -14,10 +14,10 @@ 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 { Fragment, h, VNode } from "preact" @@ -37,23 +37,7 @@ interface Props { selected?: boolean; } -export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete, selected }: Props): VNode { - const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]); - const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { - onDelete(actionQueue[0].element) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onDelete]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { - onNewTip(actionQueue[0].element) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onNewTip]) +export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete }: Props): VNode { const [withoutFunds, withFunds] = instances.reduce((prev, current) => { const amount = current.exchange_initial_amount @@ -63,118 +47,91 @@ export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete, s prev[1] = prev[1].concat(current) } return prev - }, new Array<Array<Entity>>([],[])) + }, new Array<Array<Entity>>([], [])) return <Fragment> {withoutFunds.length > 0 && <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves not yet funded</Translate></p> - - <div class="card-header-icon" aria-label="more options"> - - <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} - type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > - Delete - </button> - </div> - <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> - </div> - - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - <TableWithoutFund instances={withoutFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves not yet funded</Translate></p> + <div class="card-header-icon" aria-label="more options"> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + <TableWithoutFund instances={withoutFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} /> + </div> </div> </div> - </div> - </div> } - - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves ready</Translate></p> - - <div class="card-header-icon" aria-label="more options"> - - <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} - type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > - Delete - </button> - </div> - <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> - </div> + </div>} - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {withFunds.length > 0 ? - <Table instances={withFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : - <EmptyTable /> - } + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves ready</Translate></p> + <div class="card-header-icon" aria-label="more options"> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {withFunds.length > 0 ? + <Table instances={withFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} /> : + <EmptyTable /> + } + </div> </div> </div> </div> - </div> </Fragment> } interface TableProps { - rowSelection: string[]; instances: Entity[]; onNewTip: (id: Entity) => void; onDelete: (id: Entity) => void; onSelect: (id: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; } -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, onNewTip, onSelect, onDelete }: TableProps): VNode { +function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode { return ( <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th><Translate>Created at</Translate></th> - <th><Translate>Expires at</Translate></th> - <th><Translate>Initial</Translate></th> - <th><Translate>Picked up</Translate></th> - <th><Translate>Committed</Translate></th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.exchange_initial_amount}</td> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.pickup_amount}</td> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.committed_amount}</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 + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th><Translate>Created at</Translate></th> + <th><Translate>Expires at</Translate></th> + <th><Translate>Initial</Translate></th> + <th><Translate>Picked up</Translate></th> + <th><Translate>Committed</Translate></th> + <th /> + </tr> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.exchange_initial_amount}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.pickup_amount}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.committed_amount}</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> - <button class="button is-small is-info jb-modal" type="button" onClick={(): void => onNewTip(i)}> - New Tip + <button class="button is-small is-info jb-modal" type="button" onClick={(): void => onNewTip(i)}> + New Tip </button> - </div> - </td> - </tr> - })} + </div> + </td> + </tr> + })} - </tbody> - </table></div>) + </tbody> + </table></div>) } function EmptyTable(): VNode { @@ -186,34 +143,34 @@ function EmptyTable(): VNode { </div> } -function TableWithoutFund({ rowSelection, rowSelectionHandler, instances, onNewTip, onSelect, onDelete }: TableProps): VNode { +function TableWithoutFund({ instances, onSelect, onDelete }: TableProps): VNode { return ( <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th><Translate>Created at</Translate></th> - <th><Translate>Expires at</Translate></th> - <th><Translate>Expected Balance</Translate></th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> - <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.merchant_initial_amount}</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>Created at</Translate></th> + <th><Translate>Expires at</Translate></th> + <th><Translate>Expected Balance</Translate></th> + <th /> </tr> - })} + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.merchant_initial_amount}</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> + </tr> + })} - </tbody> - </table></div>) + </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 index d141011..5486451 100644 --- a/packages/frontend/src/paths/instance/transfers/list/index.tsx +++ b/packages/frontend/src/paths/instance/transfers/list/index.tsx @@ -28,6 +28,7 @@ import { Input } from '../../../../components/form/Input'; import { InputBoolean } from '../../../../components/form/InputBoolean'; import { InputSearchProduct } from '../../../../components/form/InputSearchProduct'; import { InputSelector } from '../../../../components/form/InputSelector'; +import { MerchantBackend } from '../../../../declaration'; import { HttpError } from '../../../../hooks/backend'; import { useInstanceDetails } from '../../../../hooks/instance'; import { useInstanceTransfers, useTransferAPI } from "../../../../hooks/transfer"; @@ -59,6 +60,17 @@ export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, on const isNonVerifiedTransfers = form.verified === 'no' ? "is-active" : '' const isAllTransfers = form.verified === undefined ? 'is-active' : '' + const result = useInstanceTransfers({ + position, + payto_uri: form.payto_uri === '' ? undefined : form.payto_uri, + verified: form.verified, + }, (id) => setPosition(id)) + + 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) + return <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -82,6 +94,9 @@ export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, on </div> <View accounts={accounts} + transfers={result.data.transfers} + onLoadMoreBefore={result.isReachingStart ? result.loadMorePrev : undefined} + onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} form={form} onCreate={onCreate} onLoadError={onLoadError} onNotFound={onNotFound} onUnauthorized={onUnauthorized} position={position} setPosition={setPosition} /> @@ -89,31 +104,23 @@ export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, on } interface ViewProps extends Props { + transfers: MerchantBackend.Transfers.TransferDetails[]; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; position?: string; setPosition: (s: string) => void; form: Form; accounts: string[]; } -function View({ onUnauthorized, onLoadError, onCreate, onNotFound, position, form, setPosition, accounts }: ViewProps) { - const result = useInstanceTransfers({ - position, - payto_uri: form.payto_uri === '' ? undefined : form.payto_uri, - verified: form.verified, - }, (id) => setPosition(id)) - - 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) - - return <CardTable instances={result.data.transfers.map(o => ({ ...o, id: String(o.transfer_serial_id) }))} +function View({ transfers, onCreate, accounts, onLoadMoreBefore, onLoadMoreAfter }: ViewProps) { + return <CardTable instances={transfers.map(o => ({ ...o, id: String(o.transfer_serial_id) }))} accounts={accounts} onCreate={onCreate} onDelete={() => null} onUpdate={() => null} - onLoadMoreBefore={result.loadMorePrev} hasMoreBefore={!result.isReachingStart} - onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd} + onLoadMoreBefore={onLoadMoreBefore} hasMoreBefore={!onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} hasMoreAfter={!onLoadMoreAfter} /> } diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/update/UpdatePage.tsx index 9d6777f..e12e09f 100644 --- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx +++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx @@ -31,6 +31,7 @@ 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 { UpdateTokenModal } from "../../../components/modal"; import { useInstanceContext } from "../../../context/instance"; import { MerchantBackend } from "../../../declaration"; import { Translate, useTranslator } from "../../../i18n"; @@ -38,15 +39,16 @@ import { InstanceUpdateSchema as schema } from '../../../schemas'; type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & { auth_token?: string } - +//MerchantBackend.Instances.InstanceAuthConfigurationMessage interface Props { - onUpdate: (d: Entity, auth?: MerchantBackend.Instances.InstanceAuthConfigurationMessage) => void; + onUpdate: (d: Entity) => void; + onChangeAuth: (d: MerchantBackend.Instances.InstanceAuthConfigurationMessage) => Promise<void>; selected: MerchantBackend.Instances.QueryInstancesResponse; isLoading: boolean; onBack: () => void; } -function convert(from: MerchantBackend.Instances.QueryInstancesResponse, token?: string): Entity { +function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity { const { accounts, ...rest } = from const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) const defaults = { @@ -54,7 +56,7 @@ function convert(from: MerchantBackend.Instances.QueryInstancesResponse, token?: default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours } - return { ...defaults, ...rest, payto_uris, auth_token: from.auth.method === "external" ? undefined : token }; + return { ...defaults, ...rest, payto_uris }; } function getTokenValuePart(t?: string): string | undefined { @@ -64,10 +66,24 @@ function getTokenValuePart(t?: string): string | undefined { return match[1] } -export function UpdatePage({ onUpdate, selected, onBack }: Props): VNode { - const { token } = useInstanceContext() + + +export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props): VNode { + const { id, token } = useInstanceContext() const currentTokenValue = getTokenValuePart(token) - const [value, valueHandler] = useState<Partial<Entity>>(convert(selected, currentTokenValue)) + + function updateToken(token: string | undefined | null) { + const value = token && token.startsWith('secret-token:') ? + token.substring('secret-token:'.length) : token + + if (!token) { + onChangeAuth({ method: 'external' }) + } else { + onChangeAuth({ method: 'token', token: `secret-token:${value}` }) + } + } + + const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)) let errors: FormErrors<Entity> = {} try { @@ -79,36 +95,72 @@ export function UpdatePage({ onUpdate, selected, onBack }: Props): VNode { const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) const submit = async (): Promise<void> => { // use conversion instead of this - const newToken = value.auth_token; - value.auth_token = undefined; + // const newToken = value.auth_token; + // value.auth_token = undefined; //if new token was not set or has been set to the actual current token //it is not needed to send a change //otherwise, checked where we are setting a new token or removing it - const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage | undefined = - newToken === undefined || newToken === currentTokenValue ? undefined : (newToken === null ? - { method: "external" } : - { method: "token", token: `secret-token:${newToken}` }); + // const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage | undefined = + // newToken === undefined || newToken === currentTokenValue ? undefined : (newToken === null ? + // { method: "external" } : + // { method: "token", token: `secret-token:${newToken}` }); // remove above use conversion schema.validateSync(value, { abortEarly: false }) - await onUpdate(schema.cast(value), auth); + await onUpdate(schema.cast(value)); await onBack() return Promise.resolve() } + const [active, setActive] = useState(false); const i18n = useTranslator() return <div> - <section class="section is-main-section"> + <section class="section "> + + <section class="hero is-hero-bar"> + <div class="hero-body"> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4">Instance id: <b>{id}</b></span> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <h1 class="title"> + <button class="button is-danger" onClick={(): void => { setActive(!active); }} > + <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> + <span><Translate>Manage token</Translate></span> + </button> + </h1> + </div> + </div> + </div> + </div></section> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + {active && <UpdateTokenModal oldToken={currentTokenValue} + onCancel={() => { setActive(false); }} + onClear={() => { updateToken(null); setActive(false); }} + onConfirm={(newToken) => { + updateToken(newToken); setActive(false) + }} + />} + </div> + <div class="column" /> + </div> + <hr /> <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - <Input<Entity> name="name" label={i18n`Name`} tooltip={i18n`unique name of this instance`} /> - - <InputSecured<Entity> name="auth_token" label={i18n`Auth token`} /> + <Input<Entity> name="name" label={i18n`Name`} tooltip={i18n`display name of this instance`} /> <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} help="x-taler-bank/bank.taler:5882/blogger" /> @@ -141,6 +193,6 @@ export function UpdatePage({ onUpdate, selected, onBack }: Props): VNode { </div> </section> - </div> + </div > } diff --git a/packages/frontend/src/paths/instance/update/index.tsx b/packages/frontend/src/paths/instance/update/index.tsx index 81c9a06..b226af0 100644 --- a/packages/frontend/src/paths/instance/update/index.tsx +++ b/packages/frontend/src/paths/instance/update/index.tsx @@ -15,6 +15,7 @@ */ import { Fragment, h, VNode } from "preact"; import { Loading } from "../../../components/exception/loading"; +import { useInstanceContext } from "../../../context/instance"; import { MerchantBackend } from "../../../declaration"; import { HttpError } from "../../../hooks/backend"; import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance"; @@ -32,8 +33,9 @@ export interface Props { } export default function Update({ onBack, onConfirm, onLoadError, onNotFound, onUpdateError, onUnauthorized }: Props): VNode { - const { updateInstance } = useInstanceAPI(); + const { updateInstance, clearToken, setNewToken } = useInstanceAPI(); const result = useInstanceDetails() + const { changeToken } = useInstanceContext() if (result.clientError && result.isUnauthorized) return onUnauthorized() if (result.clientError && result.isNotfound) return onNotFound() @@ -45,8 +47,13 @@ export default function Update({ onBack, onConfirm, onLoadError, onNotFound, onU onBack={onBack} isLoading={false} selected={result.data} - onUpdate={(d: MerchantBackend.Instances.InstanceReconfigurationMessage, t?: MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => { - return updateInstance(d, t).then(onConfirm).catch(onUpdateError) - }} /> + onUpdate={(d: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { + return updateInstance(d).then(onConfirm).catch(onUpdateError) + }} + onChangeAuth={(d: MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => { + const apiCall = d.method === 'external' ? clearToken() : setNewToken(d.token!); + return apiCall.then(() => changeToken(d.token)).then(onConfirm).catch(onUpdateError) + }} + /> </Fragment> }
\ No newline at end of file diff --git a/packages/frontend/src/utils/amount.ts b/packages/frontend/src/utils/amount.ts index c799d97..731dc76 100644 --- a/packages/frontend/src/utils/amount.ts +++ b/packages/frontend/src/utils/amount.ts @@ -13,6 +13,7 @@ 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/> */ +import { Amounts } from "@gnu-taler/taler-util"; import { MerchantBackend } from "../declaration"; /** @@ -55,24 +56,36 @@ export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[], cur: } export const multiplyPrice = (price: string, q: number) => { - const [currency, value] = price.split(':') - const total = parseInt(value, 10) * q - return `${currency}:${total}` + const a = Amounts.parseOrThrow(price) + const r = Amounts.mult(a, q) + return Amounts.stringify(r.amount) + // const [currency, value] = price.split(':') + // const total = parseInt(value, 10) * q + // return `${currency}:${total}` } export const subtractPrices = (one: string, two: string) => { - const [currency, valueOne] = one.split(':') - const [, valueTwo] = two.split(':') - return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}` + const a = Amounts.parseOrThrow(one) + const b = Amounts.parseOrThrow(two) + const r = Amounts.sub(a, b) + return Amounts.stringify(r.amount) + // const [currency, valueOne] = one.split(':') + // const [, valueTwo] = two.split(':') + // return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}` } -export const rate = (one?: string, two?: string) => { - const [, valueOne] = (one || '').split(':') - const [, valueTwo] = (two || '').split(':') - const intOne = parseInt(valueOne, 10) - const intTwo = parseInt(valueTwo, 10) - if (!intTwo) return intOne - if (!intOne) return 0 - return intOne / intTwo +export const rate = (one: string, two: string) => { + const a = Amounts.parseOrThrow(one) + const b = Amounts.parseOrThrow(two) + const af = Amounts.toFloat(a) + const bf = Amounts.toFloat(b) + return af / bf + // const [, valueOne] = (one || '').split(':') + // const [, valueTwo] = (two || '').split(':') + // const intOne = parseInt(valueOne, 10) + // const intTwo = parseInt(valueTwo, 10) + // if (!intTwo) return intOne + // if (!intOne) return 0 + // return intOne / intTwo } diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts index a8ab9ca..1f654c0 100644 --- a/packages/frontend/src/utils/constants.ts +++ b/packages/frontend/src/utils/constants.ts @@ -21,6 +21,7 @@ //https://tools.ietf.org/html/rfc8905 export const PAYTO_REGEX = /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/ +export const PAYTO_WIRE_METHOD_LOOKUP = /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/ export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/ |