summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-05-27 10:36:56 -0300
committerSebastian <sebasjm@gmail.com>2021-05-27 11:02:35 -0300
commit8e91c94d4733e035d5a078461205e86641190e70 (patch)
tree79aa1148988481d74445b9314c26bdd45110874f
parent1409f599d5c3ba5cf75b597fb38757fcd8e9f02f (diff)
downloadmerchant-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
-rw-r--r--CHANGELOG.md136
-rw-r--r--packages/frontend/src/InstanceRoutes.tsx17
-rw-r--r--packages/frontend/src/components/exception/loading.tsx10
-rw-r--r--packages/frontend/src/components/exception/login.tsx11
-rw-r--r--packages/frontend/src/components/form/InputSecured.tsx74
-rw-r--r--packages/frontend/src/components/menu/index.tsx10
-rw-r--r--packages/frontend/src/components/modal/index.tsx14
-rw-r--r--packages/frontend/src/components/notifications/CreatedSuccessfully.tsx4
-rw-r--r--packages/frontend/src/components/product/ProductForm.tsx2
-rw-r--r--packages/frontend/src/context/backend.ts6
-rw-r--r--packages/frontend/src/context/instance.ts1
-rw-r--r--packages/frontend/src/declaration.d.ts45
-rw-r--r--packages/frontend/src/hooks/instance.ts10
-rw-r--r--packages/frontend/src/paths/admin/create/CreatePage.tsx3
-rw-r--r--packages/frontend/src/paths/instance/orders/create/CreatePage.tsx66
-rw-r--r--packages/frontend/src/paths/instance/orders/create/index.tsx5
-rw-r--r--packages/frontend/src/paths/instance/products/list/Table.tsx2
-rw-r--r--packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx128
-rw-r--r--packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx39
-rw-r--r--packages/frontend/src/paths/instance/reserves/create/index.tsx8
-rw-r--r--packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx22
-rw-r--r--packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx23
-rw-r--r--packages/frontend/src/paths/instance/reserves/list/Table.tsx227
-rw-r--r--packages/frontend/src/paths/instance/transfers/list/index.tsx37
-rw-r--r--packages/frontend/src/paths/instance/update/UpdatePage.tsx90
-rw-r--r--packages/frontend/src/paths/instance/update/index.tsx15
-rw-r--r--packages/frontend/src/utils/amount.ts41
-rw-r--r--packages/frontend/src/utils/constants.ts1
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,]*$/