[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] 02/02: skeletons for order, products, transf
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] 02/02: skeletons for order, products, transfers and tips |
Date: |
Mon, 08 Mar 2021 13:19:58 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository merchant-backoffice.
commit e76acb3878c13104210631e429ca9cbbb3a6af8d
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Mar 8 09:19:27 2021 -0300
skeletons for order, products, transfers and tips
---
CHANGELOG.md | 22 +-
packages/frontend/src/AdminRoutes.tsx | 89 ++-
packages/frontend/src/InstanceRoutes.tsx | 86 ++-
.../frontend/src/components/menu/NavigationBar.tsx | 2 +-
packages/frontend/src/components/menu/SideBar.tsx | 30 +-
packages/frontend/src/context/backend.ts | 2 +
packages/frontend/src/declaration.d.ts | 614 ++++++++++++++++++++-
packages/frontend/src/hooks/backend.ts | 288 +++++++++-
packages/frontend/src/hooks/index.ts | 22 +-
packages/frontend/src/index.tsx | 32 +-
packages/frontend/src/messages/en.po | 30 +
packages/frontend/src/routes/admin/list/Table.tsx | 15 +-
packages/frontend/src/routes/admin/list/View.tsx | 1 -
.../frontend/src/routes/instance/details/index.tsx | 14 +-
.../src/routes/instance/orders/create/index.tsx | 5 +
.../{admin => instance/orders}/list/Table.tsx | 47 +-
.../src/routes/instance/orders/list/index.tsx | 35 ++
.../src/routes/instance/orders/update/index.tsx | 5 +
.../src/routes/instance/products/create/index.tsx | 5 +
.../{admin => instance/products}/list/Table.tsx | 47 +-
.../src/routes/instance/products/list/index.tsx | 44 ++
.../src/routes/instance/products/update/index.tsx | 5 +
.../src/routes/instance/tips/create/index.tsx | 5 +
.../routes/{admin => instance/tips}/list/Table.tsx | 45 +-
.../src/routes/instance/tips/list/index.tsx | 37 ++
.../src/routes/instance/tips/update/index.tsx | 5 +
.../src/routes/instance/transfers/create/index.tsx | 5 +
.../{admin => instance/transfers}/list/Table.tsx | 37 +-
.../src/routes/instance/transfers/list/index.tsx | 36 ++
.../src/routes/instance/transfers/update/index.tsx | 5 +
.../frontend/src/routes/instance/update/index.tsx | 4 +-
packages/frontend/src/utils/functions.ts | 13 +-
packages/frontend/src/utils/table.ts | 20 +
33 files changed, 1488 insertions(+), 164 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 228bfbc..b0c25c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,7 @@ and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0
- red color when input is invalid (onchange)
- validate everything using onChange
- feature: input as date format
- - bug: there is missing a mutate call when updating to remove the instance
from cache
- - add order section
- - add product section
- - add tips section
- implement better error handling (improve creation of duplicated instances)
- replace Yup and type definition with a taler-library for the purpose (first
wait Florian to refactor wallet core)
- add more doc style comments
@@ -24,8 +20,25 @@ and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0
- configure prettier
- prune scss styles to reduce size
- create a loading page to be use when the data is not ready
+ - some way to copy the url of a created instance
+ - change the admin title to "instances" if we are listing the instances and
"settings: $ID" on updating instances
+ - fix mobile: some things are still on the left
+ - update title with: Taler Backoffice: $PAGE_TITLE
+ - instance id in instance list should be clickable
+ - edit button to go to instance settings
+ - notifications should tale place between title and content, and not disapear
+ - confirmation page when creating instances
+ - if there is enough space for tables in mobile, make the scrollables
## [Unreleased]
+ - 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
+
+## [0.0.3] - 2021-03-04
- submit form on key press == enter
- version of backoffice in sidebar
- fixed login dialog on mobile
@@ -39,6 +52,7 @@ and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0
- 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
diff --git a/packages/frontend/src/AdminRoutes.tsx
b/packages/frontend/src/AdminRoutes.tsx
index 3abc688..6b63239 100644
--- a/packages/frontend/src/AdminRoutes.tsx
+++ b/packages/frontend/src/AdminRoutes.tsx
@@ -15,7 +15,7 @@
*/
import { h, VNode } from "preact";
import Router, { route, Route } from "preact-router";
-import { RootPaths, Redirect } from "./index";
+import { RootPaths, Redirect, InstancePaths } from "./index";
import { MerchantBackend } from "./declaration";
import { useMessageTemplate } from "preact-messages";
import { Notification } from "./utils/types";
@@ -25,6 +25,23 @@ import InstanceListPage from './routes/admin/list';
import InstanceCreatePage from "./routes/admin/create";
import NotFoundPage from './routes/notfound';
+import ProductListPage from './routes/instance/products/list'
+import ProductCreatePage from './routes/instance/products/create'
+import ProductUpdatePage from './routes/instance/products/update'
+
+import OrderListPage from './routes/instance/orders/list'
+import OrderCreatePage from './routes/instance/orders/create'
+import OrderUpdatePage from './routes/instance/orders/update'
+
+import TipListPage from './routes/instance/tips/list'
+import TipCreatePage from './routes/instance/tips/create'
+import TipUpdatePage from './routes/instance/tips/update'
+
+import TransferListPage from './routes/instance/transfers/list'
+import TransferCreatePage from './routes/instance/transfers/create'
+import LoginPage from "./routes/login";
+import { SwrError } from "./hooks/backend";
+
interface Props {
pushNotification: (n: Notification) => void;
instances: MerchantBackend.Instances.Instance[]
@@ -32,6 +49,10 @@ interface Props {
}
export function AdminRoutes({ instances, pushNotification, addTokenCleaner }:
Props): VNode {
const i18n = useMessageTemplate();
+
+ // const [token, updateToken] = useBackendInstanceToken(id);
+ // const { changeBackend } = useBackendContext();
+ const updateLoginStatus = () => null;
return <Router>
<Route path={RootPaths.root} component={Redirect}
to={RootPaths.list_instances} />
@@ -78,6 +99,72 @@ export function AdminRoutes({ instances, pushNotification,
addTokenCleaner }: Pr
parent="/instance/:id"
/>
+ <Route path={InstancePaths.product_list}
+ component={ProductListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={InstancePaths.product_update}
+ component={ProductUpdatePage}
+ />
+ <Route path={InstancePaths.product_new}
+ component={ProductCreatePage}
+ />
+
+ <Route path={InstancePaths.order_list}
+ component={OrderListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={InstancePaths.order_update}
+ component={OrderUpdatePage}
+ />
+ <Route path={InstancePaths.order_new}
+ component={OrderCreatePage}
+ />
+
+ <Route path={InstancePaths.tips_list}
+ component={TipListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={InstancePaths.tips_update}
+ component={TipUpdatePage}
+ />
+ <Route path={InstancePaths.tips_new}
+ component={TipCreatePage}
+ />
+
+ <Route path={InstancePaths.transfers_list}
+ component={TransferListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={InstancePaths.transfers_new}
+ component={TransferCreatePage}
+ />
+
+
<Route default component={NotFoundPage} />
</Router>
diff --git a/packages/frontend/src/InstanceRoutes.tsx
b/packages/frontend/src/InstanceRoutes.tsx
index 9bc5f06..175b465 100644
--- a/packages/frontend/src/InstanceRoutes.tsx
+++ b/packages/frontend/src/InstanceRoutes.tsx
@@ -34,6 +34,21 @@ import InstanceUpdatePage from "./routes/instance/update";
import DetailPage from './routes/instance/details';
import NotFoundPage from './routes/notfound';
+import ProductListPage from './routes/instance/products/list'
+import ProductCreatePage from './routes/instance/products/create'
+import ProductUpdatePage from './routes/instance/products/update'
+
+import OrderListPage from './routes/instance/orders/list'
+import OrderCreatePage from './routes/instance/orders/create'
+import OrderUpdatePage from './routes/instance/orders/update'
+
+import TipListPage from './routes/instance/tips/list'
+import TipCreatePage from './routes/instance/tips/create'
+import TipUpdatePage from './routes/instance/tips/update'
+
+import TransferListPage from './routes/instance/transfers/list'
+import TransferCreatePage from './routes/instance/transfers/create'
+
export interface Props {
id: string;
pushNotification: (n: Notification) => void;
@@ -61,7 +76,7 @@ export function InstanceRoutes({ id, pushNotification,
addTokenCleaner, parent }
return <InstanceContextProvider value={value}>
<Router>
- <Route path={(!parent? "" : parent) + InstancePaths.details}
+ <Route path={(!parent ? "" : parent) + InstancePaths.details}
component={DetailPage}
onUnauthorized={() => <LoginPage
@@ -71,9 +86,9 @@ export function InstanceRoutes({ id, pushNotification,
addTokenCleaner, parent }
onLoadError={(error: SwrError) => <LoginPage
withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
onConfirm={updateLoginStatus} />}
- />
+ />
- <Route path={(!parent? "" : parent) + InstancePaths.update}
+ <Route path={(!parent ? "" : parent) + InstancePaths.update}
component={InstanceUpdatePage}
onUnauthorized={() => <LoginPage
@@ -98,6 +113,71 @@ export function InstanceRoutes({ id, pushNotification,
addTokenCleaner, parent }
}}
/>
+ <Route path={(!parent ? "" : parent) + InstancePaths.product_list}
+ component={ProductListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.product_update}
+ component={ProductUpdatePage}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.product_new}
+ component={ProductCreatePage}
+ />
+
+ <Route path={(!parent ? "" : parent) + InstancePaths.order_list}
+ component={OrderListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.order_update}
+ component={OrderUpdatePage}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.order_new}
+ component={OrderCreatePage}
+ />
+
+ <Route path={(!parent ? "" : parent) + InstancePaths.tips_list}
+ component={TipListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.tips_update}
+ component={TipUpdatePage}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.tips_new}
+ component={TipCreatePage}
+ />
+
+ <Route path={(!parent ? "" : parent) + InstancePaths.transfers_list}
+ component={TransferListPage}
+ onUnauthorized={() => <LoginPage
+ withMessage={{ message: i18n`Access denied`, description: i18n`Check
your token is valid`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+
+ onLoadError={(error: SwrError) => <LoginPage
+ withMessage={{ message: i18n`Problem reaching the server`,
description: i18n`Got message: ${error.message} from: ${error.backend}
(hasToken: ${error.hasToken})`, type: 'ERROR', }}
+ onConfirm={updateLoginStatus} />}
+ />
+ <Route path={(!parent ? "" : parent) + InstancePaths.transfers_new}
+ component={TransferCreatePage}
+ />
+
<Route default component={NotFoundPage} />
</Router>
</InstanceContextProvider>;
diff --git a/packages/frontend/src/components/menu/NavigationBar.tsx
b/packages/frontend/src/components/menu/NavigationBar.tsx
index 22d432e..e1bb4c7 100644
--- a/packages/frontend/src/components/menu/NavigationBar.tsx
+++ b/packages/frontend/src/components/menu/NavigationBar.tsx
@@ -48,7 +48,7 @@ export function NavigationBar({ onMobileMenu, title }:
Props): VNode {
<img src={logo} style={{ height: 50, maxHeight: 50 }} />
</a>
<div class="navbar-end">
- <div class="navbar-item">
+ <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
<LangSelector />
</div>
</div>
diff --git a/packages/frontend/src/components/menu/SideBar.tsx
b/packages/frontend/src/components/menu/SideBar.tsx
index 5967117..e534c77 100644
--- a/packages/frontend/src/components/menu/SideBar.tsx
+++ b/packages/frontend/src/components/menu/SideBar.tsx
@@ -60,28 +60,36 @@ export function Sidebar({ mobile, instance, onLogout }:
Props): VNode {
</Fragment>}
<p class="menu-label">Instance</p>
<ul class="menu-list">
- { instance && <li>
- <a href="/update" class="has-icon">
- <span class="icon"><i class="mdi mdi-square-edit-outline"
/></span>
- <span class="menu-item-label">Settings</span>
- </a>
- </li> }
+ {instance && <Fragment>
+ <li>
+ <a href="/update" class="has-icon">
+ <span class="icon"><i class="mdi mdi-square-edit-outline"
/></span>
+ <span class="menu-item-label">Settings</span>
+ </a>
+ </li>
+ </Fragment>}
<li>
- <a href="/forms" class="has-icon">
+ <a href="/o" class="has-icon">
<span class="icon"><i class="mdi mdi-square-edit-outline"
/></span>
<span class="menu-item-label">Orders</span>
</a>
</li>
<li>
- <a href="/profile" class="has-icon">
+ <a href="/p" class="has-icon">
+ <span class="icon"><i class="mdi mdi-account-circle" /></span>
+ <span class="menu-item-label">Products</span>
+ </a>
+ </li>
+ <li>
+ <a href="/t" class="has-icon">
<span class="icon"><i class="mdi mdi-account-circle" /></span>
- <span class="menu-item-label">Inventory</span>
+ <span class="menu-item-label">Transfers</span>
</a>
</li>
<li>
- <a href="/profile" class="has-icon">
+ <a href="/r" class="has-icon">
<span class="icon"><i class="mdi mdi-account-circle" /></span>
- <span class="menu-item-label">Tipping</span>
+ <span class="menu-item-label">Tips</span>
</a>
</li>
</ul>
diff --git a/packages/frontend/src/context/backend.ts
b/packages/frontend/src/context/backend.ts
index 5b9b648..55eba5d 100644
--- a/packages/frontend/src/context/backend.ts
+++ b/packages/frontend/src/context/backend.ts
@@ -19,6 +19,7 @@ import { StateUpdater, useContext } from 'preact/hooks'
export interface BackendContextType {
url: string;
token?: string;
+ triedToLog: boolean;
changeBackend: (url: string) => void;
resetBackend: () => void;
// clearTokens: () => void;
@@ -43,6 +44,7 @@ const BackendContext = createContext<BackendContextType>({
url: '',
lang: 'en',
token: undefined,
+ triedToLog: false,
changeBackend: () => null,
resetBackend: () => null,
// clearTokens: () => null,
diff --git a/packages/frontend/src/declaration.d.ts
b/packages/frontend/src/declaration.d.ts
index 6a08212..c0e541f 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -21,8 +21,13 @@
+type HashCode = string;
type EddsaPublicKey = string;
+type EddsaSignature = string;
+type WireTransferIdentifierRawP = string;
type RelativeTime = Duration;
+type ImageDataUrl = string;
+
interface Timestamp {
// Milliseconds since epoch, or the special
// value "forever" to represent an event that will
@@ -54,6 +59,64 @@ export namespace MerchantBackend {
tax: Amount;
}
+ interface Auditor {
+ // official name
+ name: string;
+
+ // Auditor's public key
+ auditor_pub: EddsaPublicKey;
+
+ // Base URL of the auditor
+ url: string;
+ }
+ interface Exchange {
+ // the exchange's base URL
+ url: string;
+
+ // master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+
+ interface Product {
+ // merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity?: Integer;
+
+ // The unit in which the product is measured (liters, kilograms,
packages, etc.)
+ unit?: string;
+
+ // The price of the product; this is the total price for quantity
times unit of this product.
+ price?: Amount;
+
+ // An optional base64-encoded product image
+ image?: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for this product. Can be empty.
+ taxes?: Tax[];
+
+ // time indicating when this product should be delivered
+ delivery_date?: Timestamp;
+ }
+ interface Merchant {
+ // label for a location with the business address of the merchant
+ address: Location;
+
+ // the merchant's legal name of business
+ name: string;
+
+ // label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street
address) may be absent.
+ jurisdiction: Location;
+ }
+
interface VersionResponse {
// libtool-style representation of the Merchant protocol version, see
//
https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
@@ -305,7 +368,7 @@ export namespace MerchantBackend {
}
- namespace Inventory {
+ namespace Products {
// POST /private/products
interface ProductAddDetail {
@@ -464,4 +527,553 @@ export namespace MerchantBackend {
// DELETE /private/products/$PRODUCT_ID
}
+
+ namespace Orders {
+ interface OrderHistory {
+ // timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+ }
+ interface OrderHistoryEntry {
+
+ // order ID of the transaction related to this entry.
+ order_id: string;
+
+ // row ID of the order in the database
+ row_id: number;
+
+ // when the order was created
+ timestamp: Timestamp;
+
+ // the amount of money the order is for
+ amount: Amount;
+
+ // the summary of the order
+ summary: string;
+
+ // whether some part of the order is refundable,
+ // that is the refund deadline has not yet expired
+ // and the total amount refunded so far is below
+ // the value of the original transaction.
+ refundable: boolean;
+
+ // whether the order has been paid or not
+ paid: boolean;
+ }
+
+ interface PostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all
+ order: Order;
+
+ // if set, the backend will then set the refund deadline to the
current
+ // time plus the specified delay. If it's not set, refunds will
not be
+ // possible.
+ refund_delay?: RelativeTime;
+
+ // specifies the payment target preferred by the client. Can be
used
+ // to select among the various (active) wire methods supported by
the instance.
+ payment_target?: string;
+
+ // specifies that some products are to be included in the
+ // order from the inventory. For these inventory management
+ // is performed (so the products must be in stock) and
+ // details are completed from the product data of the backend.
+ inventory_products?: MinimalInventoryProduct[];
+
+ // Specifies a lock identifier that was used to
+ // lock a product in the inventory. Only useful if
+ // manage_inventory is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping card was being built. Multiple UUIDs can
+ // be used in case different UUIDs were used for different
+ // products (i.e. in case the user started with multiple
+ // shopping sessions that were combined during checkout).
+ lock_uuids?: UUID[];
+
+ // Should a token for claiming the order be generated?
+ // False can make sense if the ORDER_ID is sufficiently
+ // high entropy to prevent adversarial claims (like it is
+ // if the backend auto-generates one). Default is 'true'.
+ create_token?: boolean;
+
+ }
+ type Order = MinimalOrderDetail | ContractTerms;
+
+ interface MinimalOrderDetail {
+ // Amount to be paid by the customer
+ amount: Amount;
+
+ // Short summary of the order
+ summary: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional. When POSTing to the
+ // merchant, the placeholder "${ORDER_ID}" will be
+ // replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL).
+ fulfillment_url?: string;
+ }
+
+ // FIXME: Where is this being used?
+ // type ProductSpecification = (MinimalInventoryProduct | Product);
+
+ interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!)
+ product_id: string;
+
+ // How many units of the product are requested
+ quantity: Integer;
+ }
+ interface PostOrderResponse {
+ // Order ID of the response that was just created
+ order_id: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // Provided only if "create_token" was set to 'true'
+ // in the request.
+ token?: ClaimToken;
+ }
+ interface OutOfStockResponse {
+
+ // Product ID of an out-of-stock item
+ product_id: string;
+
+ // Requested quantity
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quanitity)
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+ }
+
+ interface ForgetRequest {
+
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+ }
+ interface RefundRequest {
+ // Amount to be refunded
+ refund: Amount;
+
+ // Human-readable refund justification
+ reason: string;
+ }
+ interface MerchantRefundResponse {
+
+ // URL (handled by the backend) that the wallet should access to
+ // trigger refund processing.
+ // taler://refund/...
+ taler_refund_uri: string;
+
+ // Contract hash that a client may need to authenticate an
+ // HTTP request to obtain the above URI in a wallet-friendly way.
+ h_contract: HashCode;
+ }
+
+ }
+
+ namespace Tips {
+
+ // GET /private/reserves
+ interface TippingReserveStatus {
+ // Array of all known reserves (possibly empty!)
+ reserves: ReserveStatusEntry[];
+ }
+ interface ReserveStatusEntry {
+ // Public key of the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Timestamp when it was established
+ creation_time: Timestamp;
+
+ // Timestamp when it expires
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for tips that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Is this reserve active (false if it was deleted but not purged)
+ active: boolean;
+ }
+
+ interface ReserveCreateRequest {
+ // Amount that the merchant promises to put into the reserve
+ initial_balance: Amount;
+
+ // Exchange the merchant intends to use for tipping
+ exchange_url: string;
+
+ // Desired wire method, for example "iban" or "x-taler-bank"
+ wire_method: string;
+ }
+ interface ReserveCreateConfirmation {
+ // Public key identifying the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Wire account of the exchange where to transfer the funds
+ payto_uri: string;
+ }
+ interface TipCreateRequest {
+ // Amount that the customer should be tipped
+ amount: Amount;
+
+ // Justification for giving the tip
+ justification: string;
+
+ // URL that the user should be directed to after tipping,
+ // will be included in the tip_token.
+ next_url: string;
+ }
+ interface TipCreateConfirmation {
+ // Unique tip identifier for the tip that was created.
+ tip_id: HashCode;
+
+ // taler://tip URI for the tip
+ taler_tip_uri: string;
+
+ // URL that will directly trigger processing
+ // the tip when the browser is redirected to it
+ tip_status_url: string;
+
+ // when does the tip expire
+ tip_expiration: Timestamp;
+ }
+
+ }
+
+ namespace Transfers {
+
+ interface TransferList {
+ // list of all the transfers that fit the filter that we know
+ transfers: TransferDetails[];
+ }
+ interface TransferDetails {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a
base32-encoded value)
+ wtid: string;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filgering via offset.
+ transfer_serial_id: number;
+
+ // Time of the execution of the wire transfer by the exchange,
according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with it.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ verified?: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ confirmed?: boolean;
+ }
+
+ interface TransferInformation {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a
base32-encoded value)
+ wtid: WireTransferIdentifierRawP;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+ }
+ interface MerchantTrackTransferResponse {
+ // Total amount transferred
+ total: Amount;
+
+ // Applicable wire fee that was charged
+ wire_fee: Amount;
+
+ // Time of the execution of the wire transfer by the exchange,
according to the exchange
+ execution_time: Timestamp;
+
+ // details about the deposits
+ deposits_sums: MerchantTrackTransferDetail[];
+ }
+ interface MerchantTrackTransferDetail {
+ // Business activity associated with the wire transferred amount
+ // deposit_value.
+ order_id: string;
+
+ // The total amount the exchange paid back for order_id.
+ deposit_value: Amount;
+
+ // applicable fees for the deposit
+ deposit_fee: Amount;
+ }
+
+ type ExchangeConflictDetails = WireFeeConflictDetails |
TrackTransferConflictDetails
+ // Note: this is not the full 'proof' of missbehavior, as
+ // the bogus message from the exchange with a signature
+ // over the 'different' wire fee is missing.
+ //
+ // This information is NOT provided by the current implementation,
+ // because this would be quite expensive to generate and is
+ // hardly needed _here_. Once we add automated reports for
+ // the Taler auditor, we need to generate this data anyway
+ // and should probably return it here as well.
+ interface WireFeeConflictDetails {
+ // Numerical error code:
+ code: "TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE";
+
+ // Text describing the issue for humans.
+ hint: string;
+
+
+ // Wire fee (wrongly) charged by the exchange, breaking the
+ // contract affirmed by the exchange_sig.
+ wire_fee: Amount;
+
+ // Timestamp of the wire transfer
+ execution_time: Timestamp;
+
+ // The expected wire fee (as signed by the exchange)
+ expected_wire_fee: Amount;
+
+ // Expected closing fee (needed to verify signature)
+ expected_closing_fee: Amount;
+
+ // Start date of the expected fee structure
+ start_date: Timestamp;
+
+ // End date of the expected fee structure
+ end_date: Timestamp;
+
+ // Signature of the exchange affirming the expected fee structure
+ master_sig: EddsaSignature;
+
+ // Master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+ interface TrackTransferConflictDetails {
+ // Numerical error code
+ code:
"TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS";
+
+ // Text describing the issue for humans.
+ hint: string;
+
+ // Offset in the exchange_transfer where the
+ // exchange's response fails to match the exchange_deposit_proof.
+ conflict_offset: number;
+
+ // The response from the exchange which tells us when the
+ // coin was returned to us, except that it does not match
+ // the expected value of the coin.
+ //
+ // This field is NOT provided by the current implementation,
+ // because this would be quite expensive to generate and is
+ // hardly needed _here_. Once we add automated reports for
+ // the Taler auditor, we need to generate this data anyway
+ // and should probably return it here as well.
+ // exchange_transfer?: TrackTransferResponse;
+
+ // Public key of the exchange used to sign the response to
+ // our deposit request.
+ deposit_exchange_pub: EddsaPublicKey;
+
+ // Signature of the exchange signing the (conflicting) response.
+ // Signs over a struct TALER_DepositConfirmationPS.
+ deposit_exchange_sig: EddsaSignature;
+
+ // Hash of the merchant's bank account the wire transfer went to
+ h_wire: HashCode;
+
+ // Hash of the contract terms with the conflicting deposit.
+ h_contract_terms: HashCode;
+
+ // At what time the exchange received the deposit. Needed
+ // to verify the \exchange_sig\.
+ deposit_timestamp: Timestamp;
+
+ // At what time the refund possibility expired (needed to verify
exchange_sig).
+ refund_deadline: Timestamp;
+
+ // Public key of the coin for which we have conflicting
information.
+ coin_pub: EddsaPublicKey;
+
+ // Amount the exchange counted the coin for in the transfer.
+ amount_with_fee: Amount;
+
+ // Expected value of the coin.
+ coin_value: Amount;
+
+ // Expected deposit fee of the coin.
+ coin_fee: Amount;
+
+ // Expected deposit fee of the coin.
+ deposit_fee: Amount;
+
+ }
+
+ // interface TrackTransferProof {
+ // // signature from the exchange made with purpose
+ // // TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT
+ // exchange_sig: EddsaSignature;
+
+ // // public EdDSA key of the exchange that was used to generate
the signature.
+ // // Should match one of the exchange's signing keys from /keys.
Again given
+ // // explicitly as the client might otherwise be confused by
clock skew as to
+ // // which signing key was used.
+ // exchange_pub: EddsaSignature;
+
+ // // hash of the wire details (identical for all deposits)
+ // // Needed to check the exchange_sig
+ // h_wire: HashCode;
+ // }
+
+ }
+
+
+ interface ContractTerms {
+ // Human-readable description of the whole purchase
+ summary: string;
+
+ // Map from IETF BCP 47 language tags to localized summaries
+ summary_i18n?: { [lang_tag: string]: string };
+
+ // Unique, free-form identifier for the proposal.
+ // Must be unique within a merchant instance.
+ // For merchants that do not store proposals in their DB
+ // before the customer paid for them, the order_id can be used
+ // by the frontend to restore a proposal from the information
+ // encoded in it (such as a short product identifier and timestamp).
+ order_id: string;
+
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
+ amount: Amount;
+
+ // The URL for this purchase. Every time is is visited, the merchant
+ // will send back to the customer the same proposal. Clearly, this URL
+ // can be bookmarked and shared by users.
+ fulfillment_url?: string;
+
+ // Maximum total deposit fee accepted by the merchant for this contract
+ max_fee: Amount;
+
+ // Maximum wire fee accepted by the merchant (customer share to be
+ // divided by the 'wire_fee_amortization' factor, and further reduced
+ // if deposit fees are below 'max_fee'). Default if missing is zero.
+ max_wire_fee: Amount;
+
+ // Over how many customer transactions does the merchant expect to
+ // amortize wire fees on average? If the exchange's wire fee is
+ // above 'max_wire_fee', the difference is divided by this number
+ // to compute the expected customer's contribution to the wire fee.
+ // The customer's contribution may further be reduced by the difference
+ // between the 'max_fee' and the sum of the actual deposit fees.
+ // Optional, default value if missing is 1. 0 and negative values are
+ // invalid and also interpreted as 1.
+ wire_fee_amortization: number;
+
+ // List of products that are part of the purchase (see Product).
+ products: Product[];
+
+ // Time when this contract was generated
+ timestamp: Timestamp;
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_deadline: Timestamp;
+
+ // After this deadline, the merchant won't accept payments for the
contact
+ pay_deadline: Timestamp;
+
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
+ wire_transfer_deadline: Timestamp;
+
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend Note that this can be an
ephemeral key.
+ merchant_pub: EddsaPublicKey;
+
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
+ merchant_base_url: string;
+
+ // More info about the merchant, see below
+ merchant: Merchant;
+
+ // The hash of the merchant instance's wire details.
+ h_wire: HashCode;
+
+ // Wire transfer method identifier for the wire method associated with
h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer
method.
+ wire_method: string;
+
+ // Any exchanges audited by these auditors are accepted by the
merchant.
+ auditors: Auditor[];
+
+ // Exchanges that the merchant accepts even if it does not accept any
auditors that audit them.
+ exchanges: Exchange[];
+
+ // Delivery location for (all!) products.
+ delivery_location?: Location;
+
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
+ delivery_date?: Timestamp;
+
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
+
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
+ auto_refund?: RelativeTime;
+
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ extra?: any;
+ }
+
}
diff --git a/packages/frontend/src/hooks/backend.ts
b/packages/frontend/src/hooks/backend.ts
index 56c85e0..f5ed418 100644
--- a/packages/frontend/src/hooks/backend.ts
+++ b/packages/frontend/src/hooks/backend.ts
@@ -52,6 +52,7 @@ interface RequestOptions {
method?: Methods;
token?: string;
data?: any;
+ params?: any;
}
@@ -69,11 +70,12 @@ async function request(url: string, options: RequestOptions
= {}): Promise<any>
const res = await axios({
- method: options.method || 'get',
url,
responseType: 'json',
headers,
- data: options.data
+ method: options.method || 'get',
+ data: options.data,
+ params: options.params
})
return res.data
} catch (e) {
@@ -88,6 +90,10 @@ function fetcher(url: string, token: string, backend:
string) {
return request(`${backend}${url}`, { token })
}
+function transferFetcher(url: string, token: string, backend: string) {
+ return request(`${backend}${url}`, { token, params: { payto_uri: '' } })
+}
+
interface AdminMutateAPI {
createInstance: (data:
MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>;
deleteInstance: (id: string) => Promise<void>;
@@ -117,6 +123,218 @@ export function useAdminMutateAPI(): AdminMutateAPI {
return { createInstance, deleteInstance }
}
+interface ProductMutateAPI {
+ createProduct: (data: MerchantBackend.Products.ProductAddDetail) =>
Promise<void>;
+ updateProduct: (id: string, data:
MerchantBackend.Products.ProductPatchDetail) => Promise<void>;
+ deleteProduct: (id: string) => Promise<void>;
+ lockProduct: (id: string, data: MerchantBackend.Products.LockRequest) =>
Promise<void>;
+}
+
+
+export function useProductMutateAPI(): ProductMutateAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext()
+ const { token: instanceToken, id, admin } = useInstanceContext()
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+
+ const createProduct = async (data:
MerchantBackend.Products.ProductAddDetail): Promise<void> => {
+ await request(`${url}/private/products`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/products', adminToken, baseUrl], null)
+ mutate([`/private/products`, token, url], null)
+ }
+
+ const updateProduct = async (productId: string, data:
MerchantBackend.Products.ProductPatchDetail): Promise<void> => {
+ await request(`${url}/private/products/${productId}`, {
+ method: 'patch',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/products', adminToken, baseUrl], null)
+ mutate([`/private/products`, token, url], null)
+ }
+
+ const deleteProduct = async (productId: string): Promise<void> => {
+ await request(`${url}/private/products/${productId}`, {
+ method: 'delete',
+ token,
+ })
+
+ if (adminToken) mutate(['/private/products', adminToken, baseUrl], null)
+ mutate([`/private/products`, token, url], null)
+ }
+
+ const lockProduct = async (productId: string, data:
MerchantBackend.Products.LockRequest): Promise<void> => {
+ await request(`${url}/private/products/${productId}/lock`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/products', adminToken, baseUrl], null)
+ mutate([`/private/products`, token, url], null)
+ }
+
+ return { createProduct, updateProduct, deleteProduct, lockProduct }
+}
+
+interface OrderMutateAPI {
+ //FIXME: add OutOfStockResponse on 410
+ createOrder: (data: MerchantBackend.Orders.PostOrderRequest) =>
Promise<MerchantBackend.Orders.PostOrderResponse>;
+ forgetOrder: (id: string, data: MerchantBackend.Orders.ForgetRequest) =>
Promise<void>;
+ deleteOrder: (id: string) => Promise<void>;
+}
+
+export function useOrderMutateAPI(): OrderMutateAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext()
+ const { token: instanceToken, id, admin } = useInstanceContext()
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const createOrder = async (data: MerchantBackend.Orders.PostOrderRequest):
Promise<MerchantBackend.Orders.PostOrderResponse> => {
+ const res = await request(`${url}/private/orders`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/orders', adminToken, baseUrl], null)
+ mutate([`/private/orders`, token, url], null)
+ return res
+ }
+ const forgetOrder = async (orderId: string, data:
MerchantBackend.Orders.ForgetRequest): Promise<void> => {
+ await request(`${url}/private/orders/${orderId}/forget`, {
+ method: 'patch',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/orders', adminToken, baseUrl], null)
+ mutate([`/private/orders`, token, url], null)
+ }
+ const deleteOrder = async (orderId: string): Promise<void> => {
+ await request(`${url}/private/orders/${orderId}`, {
+ method: 'delete',
+ token
+ })
+
+ if (adminToken) mutate(['/private/orders', adminToken, baseUrl], null)
+ mutate([`/private/orders`, token, url], null)
+ }
+ return { createOrder, forgetOrder, deleteOrder }
+}
+
+interface TransferMutateAPI {
+ informTransfer: (data: MerchantBackend.Transfers.TransferInformation) =>
Promise<MerchantBackend.Transfers.MerchantTrackTransferResponse>;
+}
+
+export function useTransferMutateAPI(): TransferMutateAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext()
+ const { token: instanceToken, id, admin } = useInstanceContext()
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const informTransfer = async (data:
MerchantBackend.Transfers.TransferInformation):
Promise<MerchantBackend.Transfers.MerchantTrackTransferResponse> => {
+ const res = await request(`${url}/private/transfers`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/transfers', adminToken, baseUrl], null)
+ mutate([`/private/transfers`, token, url], null)
+ return res
+ }
+
+ return { informTransfer }
+}
+
+interface TipsMutateAPI {
+ createReserve: (data: MerchantBackend.Tips.ReserveCreateRequest) =>
Promise<MerchantBackend.Tips.ReserveCreateConfirmation>;
+ authorizeTipReserve: (id: string, data:
MerchantBackend.Tips.TipCreateRequest) =>
Promise<MerchantBackend.Tips.TipCreateConfirmation>;
+ authorizeTip: (data: MerchantBackend.Tips.TipCreateRequest) =>
Promise<MerchantBackend.Tips.TipCreateConfirmation>;
+ deleteReserve: (id: string) => Promise<void>;
+}
+
+export function useTipsMutateAPI(): TipsMutateAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext()
+ const { token: instanceToken, id, admin } = useInstanceContext()
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ //reserves
+ const createReserve = async (data:
MerchantBackend.Tips.ReserveCreateRequest):
Promise<MerchantBackend.Tips.ReserveCreateConfirmation> => {
+ const res = await request(`${url}/private/reserves`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/reserves', adminToken, baseUrl], null)
+ mutate([`/private/reserves`, token, url], null)
+ return res
+ }
+
+ const authorizeTipReserve = async (pub: string, data:
MerchantBackend.Tips.TipCreateRequest):
Promise<MerchantBackend.Tips.TipCreateConfirmation> => {
+ const res = await request(`${url}/private/reserves/${pub}/authorize-tip`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/reserves', adminToken, baseUrl], null)
+ mutate([`/private/reserves`, token, url], null)
+ return res
+ }
+
+ const authorizeTip = async (data: MerchantBackend.Tips.TipCreateRequest):
Promise<MerchantBackend.Tips.TipCreateConfirmation> => {
+ const res = await request(`${url}/private/tips`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ if (adminToken) mutate(['/private/reserves', adminToken, baseUrl], null)
+ mutate([`/private/reserves`, token, url], null)
+ return res
+ }
+
+ const deleteReserve = async (pub: string): Promise<void> => {
+ await request(`${url}/private/reserves/${pub}`, {
+ method: 'delete',
+ token,
+ })
+
+ if (adminToken) mutate(['/private/reserves', adminToken, baseUrl], null)
+ mutate([`/private/reserves`, token, url], null)
+ }
+
+
+ return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve }
+}
+
interface InstaceMutateAPI {
updateInstance: (data:
MerchantBackend.Instances.InstanceReconfigurationMessage, a?:
MerchantBackend.Instances.InstanceAuthConfigurationMessage) => Promise<void>;
deleteInstance: () => Promise<void>;
@@ -128,7 +346,7 @@ export function useInstanceMutateAPI(): InstaceMutateAPI {
const { url: baseUrl, token: adminToken } = useBackendContext()
const { token, id, admin } = useInstanceContext()
- const url = !admin ? baseUrl: `${baseUrl}/instances/${id}`
+ const url = !admin ? baseUrl : `${baseUrl}/instances/${id}`
const updateInstance = async (instance:
MerchantBackend.Instances.InstanceReconfigurationMessage, auth?:
MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => {
await request(`${url}/private/`, {
@@ -187,17 +405,77 @@ export function useBackendInstances():
HttpResponse<MerchantBackend.Instances.In
return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
}
-export function useBackendInstance():
HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
+export function useInstanceDetails():
HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
const { url: baseUrl } = useBackendContext();
const { token, id, admin } = useInstanceContext();
- const url = !admin ? baseUrl: `${baseUrl}/instances/${id}`
+ const url = !admin ? baseUrl : `${baseUrl}/instances/${id}`
const { data, error } =
useSWR<MerchantBackend.Instances.QueryInstancesResponse,
SwrError>([`/private/`, token, url], fetcher)
return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
}
+export function useInstanceProducts():
HttpResponse<MerchantBackend.Products.InventorySummaryResponse> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const { data, error } =
useSWR<MerchantBackend.Products.InventorySummaryResponse,
SwrError>([`/private/products`, token, url], fetcher)
+
+ return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
+}
+
+export function useInstanceOrders():
HttpResponse<MerchantBackend.Orders.OrderHistory> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const { data, error } = useSWR<MerchantBackend.Orders.OrderHistory,
SwrError>([`/private/orders`, token, url], fetcher)
+
+ return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
+}
+
+export function useInstanceTips():
HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const { data, error } = useSWR<MerchantBackend.Tips.TippingReserveStatus,
SwrError>([`/private/reserves`, token, url], fetcher)
+
+ return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
+}
+
+export function useInstanceTransfers():
HttpResponse<MerchantBackend.Transfers.TransferList> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const { data, error } = useSWR<MerchantBackend.Transfers.TransferList,
SwrError>([`/private/transfers`, token, url], transferFetcher)
+
+ return { data, unauthorized: error?.status === 401, notfound: error?.status
=== 404, error }
+}
+
export function useBackendConfig():
HttpResponse<MerchantBackend.VersionResponse> {
const { url, token } = useBackendContext()
const { data, error } = useSWR<MerchantBackend.VersionResponse,
SwrError>(['/config', token, url], fetcher)
diff --git a/packages/frontend/src/hooks/index.ts
b/packages/frontend/src/hooks/index.ts
index 514a458..68a3c5b 100644
--- a/packages/frontend/src/hooks/index.ts
+++ b/packages/frontend/src/hooks/index.ts
@@ -25,18 +25,28 @@ import { ValueOrFunction } from '../utils/types';
export function useBackendContextState() {
const [lang, setLang] = useLang()
- const [url, changeBackend, resetBackend] = useBackendURL();
+ const [url, triedToLog, changeBackend, resetBackend] = useBackendURL();
const [token, updateToken] = useBackendDefaultToken();
- return { url, token, changeBackend, updateToken, lang, setLang, resetBackend
}
+
+ return { url, token, triedToLog, changeBackend, updateToken, lang, setLang,
resetBackend }
}
-export function useBackendURL(): [string, StateUpdater<string>, () => void] {
+export function useBackendURL(): [string, boolean, StateUpdater<string>, () =>
void] {
const [value, setter] = useNotNullLocalStorage('backend-url', typeof window
!== 'undefined' ? window.location.origin : '')
- const checkedSetter = (v: ValueOrFunction<string>) => setter(p => (v
instanceof Function ? v(p) : v).replace(/\/$/, ''))
- const reset = () => checkedSetter(typeof window !== 'undefined' ?
window.location.origin : '')
- return [value, checkedSetter, reset]
+ const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
+
+ const checkedSetter = (v: ValueOrFunction<string>) => {
+ setTriedToLog('yes')
+ return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
+ }
+
+ const resetBackend = () => {
+ setTriedToLog(undefined)
+ }
+ return [value, !!triedToLog, checkedSetter, resetBackend]
}
+
export function useBackendDefaultToken(): [string | undefined,
StateUpdater<string | undefined>] {
return useLocalStorage('backend-token')
}
diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx
index f2f50b3..29aec14 100644
--- a/packages/frontend/src/index.tsx
+++ b/packages/frontend/src/index.tsx
@@ -48,6 +48,21 @@ export enum RootPaths {
export enum InstancePaths {
details = '/',
update = '/update',
+
+ product_list = '/p',
+ product_update = '/p/:pid/update',
+ product_new = '/p/new',
+
+ order_list = '/o',
+ order_update = '/p/:oid/update',
+ order_new = '/o/new',
+
+ tips_list = '/r',
+ tips_update = '/r/:rid/update',
+ tips_new = '/r/new',
+
+ transfers_list = '/t',
+ transfers_new = '/t/new',
}
export function Redirect({ to }: { to: string }): null {
@@ -71,7 +86,7 @@ export default function Application(): VNode {
function ApplicationStatusRoutes(): VNode {
const { notifications, pushNotification, removeNotification } =
useNotifications()
- const { changeBackend, updateToken, resetBackend } = useBackendContext()
+ const { changeBackend, triedToLog, updateToken, resetBackend } =
useBackendContext()
const backendConfig = useBackendConfig();
const i18n = useMessageTemplate()
@@ -88,6 +103,19 @@ function ApplicationStatusRoutes(): VNode {
const v = `${backendConfig.data?.currency} ${backendConfig.data?.version}`
const ctx = useMemo(() => ({ currency: backendConfig.data?.currency || '',
version: backendConfig.data?.version || '' }), [v])
+ if (!triedToLog) {
+ return <div id="app">
+ <Menu />
+ <LoginPage
+ onConfirm={(url: string, token?: string) => {
+ changeBackend(url)
+ if (token) updateToken(token)
+ route(RootPaths.list_instances)
+ }}
+ />
+ </div>
+ }
+
if (!backendConfig.data) {
if (!backendConfig.error) return <div class="is-loading" />
@@ -125,7 +153,7 @@ function ApplicationStatusRoutes(): VNode {
return <div id="app" class="has-navbar-fixed-top">
<ConfigContextProvider value={ctx}>
<Notifications notifications={notifications}
removeNotification={removeNotification} />
- <Route default component={ApplicationReadyRoutes}
pushNotification={pushNotification} addTokenCleaner={addTokenCleanerMemo}
clearAllTokens={clearAllTokens} /> :
+ <Route default component={ApplicationReadyRoutes}
pushNotification={pushNotification} addTokenCleaner={addTokenCleanerMemo}
clearAllTokens={clearAllTokens} />
</ConfigContextProvider>
</div>
}
diff --git a/packages/frontend/src/messages/en.po
b/packages/frontend/src/messages/en.po
index 717e4f0..d12bbaf 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -218,5 +218,35 @@ msgstr "Login required"
msgid "Please enter your auth token. Token should have \"secret-token:\" and
start with Bearer or ApiKey"
msgstr "Please enter your auth token. Token should have \"secret-token:\" and
start with Bearer or ApiKey"
+msgid "Orders"
+msgstr "Orders"
+msgid "fields.order.amount.label"
+msgstr "Amount"
+msgid "fields.order.summary.label"
+msgstr "Summary"
+
+msgid "fields.order.paid.label"
+msgstr "Paid"
+
+msgid "Products"
+msgstr "Products"
+
+msgid "fields.product.id.label"
+msgstr "Id"
+
+msgid "Transfers"
+msgstr "Transfers"
+
+msgid "Tips"
+msgstr "Tips"
+
+msgid "fields.tips.committed_amount.label"
+msgstr "Commited Amount"
+
+msgid "fields.tips.exchange_initial_amount.label"
+msgstr "Exchange Initial Amount"
+
+msgid "fields.tips.merchant_initial_amount.label"
+msgstr "Merchant Initial Amount"
diff --git a/packages/frontend/src/routes/admin/list/Table.tsx
b/packages/frontend/src/routes/admin/list/Table.tsx
index 2c00668..4831ed3 100644
--- a/packages/frontend/src/routes/admin/list/Table.tsx
+++ b/packages/frontend/src/routes/admin/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 { h, VNode } from "preact"
import { Message } from "preact-messages"
@@ -118,10 +118,13 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.id}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.name}</td>
+ <td><a onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.id}</a></td>
+ <td >{i.name}</td>
<td class="is-actions-cell">
<div class="buttons is-right">
+ <button class="button is-small is-success jb-modal"
type="button" onClick={(): void => onUpdate(i.id)}>
+ <span class="icon"><i class="mdi mdi-pen" /></span>
+ </button>
<button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onDelete(i)}>
<span class="icon"><i class="mdi mdi-trash-can" /></span>
</button>
diff --git a/packages/frontend/src/routes/admin/list/View.tsx
b/packages/frontend/src/routes/admin/list/View.tsx
index a66fb5e..26c2eca 100644
--- a/packages/frontend/src/routes/admin/list/View.tsx
+++ b/packages/frontend/src/routes/admin/list/View.tsx
@@ -22,7 +22,6 @@
import { h, VNode } from "preact";
import { MerchantBackend } from "../../../declaration";
import { CardTable } from './Table';
-import { Message } from "preact-messages";
interface Props {
instances: MerchantBackend.Instances.Instance[];
diff --git a/packages/frontend/src/routes/instance/details/index.tsx
b/packages/frontend/src/routes/instance/details/index.tsx
index e0a3248..492878c 100644
--- a/packages/frontend/src/routes/instance/details/index.tsx
+++ b/packages/frontend/src/routes/instance/details/index.tsx
@@ -17,7 +17,7 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useInstanceContext } from "../../../context/backend";
import { Notification } from "../../../utils/types";
-import { useBackendInstance, useInstanceMutateAPI, SwrError } from
"../../../hooks/backend";
+import { useInstanceDetails, useInstanceMutateAPI, SwrError } from
"../../../hooks/backend";
import { DetailPage } from "./DetailPage";
import { DeleteModal } from "../../../components/modal";
@@ -31,14 +31,14 @@ interface Props {
export default function Detail({ onUpdate, onLoadError, onUnauthorized,
pushNotification, onDelete }: Props): VNode {
const { id } = useInstanceContext()
- const details = useBackendInstance()
+ const result = useInstanceDetails()
const [deleting, setDeleting] = useState<boolean>(false)
const { deleteInstance } = useInstanceMutateAPI()
- if (!details.data) {
- if (details.unauthorized) return onUnauthorized()
- if (details.error) return onLoadError(details.error)
+ if (!result.data) {
+ if (result.unauthorized) return onUnauthorized()
+ if (result.error) return onLoadError(result.error)
return <div>
loading ....
</div>
@@ -46,12 +46,12 @@ export default function Detail({ onUpdate, onLoadError,
onUnauthorized, pushNoti
return <Fragment>
<DetailPage
- selected={details.data}
+ selected={result.data}
onUpdate={onUpdate}
onDelete={() => setDeleting(true)}
/>
{deleting && <DeleteModal
- element={{ name: details.data.name, id }}
+ element={{ name: result.data.name, id }}
onCancel={() => setDeleting(false)}
onConfirm={async (): Promise<void> => {
try {
diff --git a/packages/frontend/src/routes/instance/orders/create/index.tsx
b/packages/frontend/src/routes/instance/orders/create/index.tsx
new file mode 100644
index 0000000..2d84be3
--- /dev/null
+++ b/packages/frontend/src/routes/instance/orders/create/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>order create page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/admin/list/Table.tsx
b/packages/frontend/src/routes/instance/orders/list/Table.tsx
similarity index 80%
copy from packages/frontend/src/routes/admin/list/Table.tsx
copy to packages/frontend/src/routes/instance/orders/list/Table.tsx
index 2c00668..ff2f50e 100644
--- a/packages/frontend/src/routes/admin/list/Table.tsx
+++ b/packages/frontend/src/routes/instance/orders/list/Table.tsx
@@ -22,18 +22,21 @@
import { h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend } from "../../../declaration"
+import { MerchantBackend, WidthId } from "../../../../declaration"
+import { Actions, buildActions } from "../../../../utils/table";
+
+type Entity = MerchantBackend.Orders.OrderHistoryEntry & {id: string}
interface Props {
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
onCreate: () => void;
selected?: boolean;
}
export function CardTable({ instances, onCreate, onUpdate, onDelete, selected
}: Props): VNode {
- const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
const [rowSelection, rowSelectionHandler] = useState<string[]>([])
useEffect(() => {
@@ -53,12 +56,14 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
return <div class="card has-table">
<header class="card-header">
- <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Instances" /></p>
+ <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Orders" /></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'))} >
+ type="button"
+ onClick={(): void => actionQueueHandler(buildActions(instances,
rowSelection, 'DELETE'))}
+ >
<span class="icon"><i class="mdi mdi-trash-can" /></span>
</button>
</div>
@@ -83,9 +88,9 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
}
interface TableProps {
rowSelection: string[];
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
}
@@ -104,8 +109,9 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</th>
- <th><Message id="fields.instance.id.label" /></th>
- <th><Message id="fields.instance.name.label" /></th>
+ <th><Message id="fields.order.amount.label" /></th>
+ <th><Message id="fields.order.summary.label" /></th>
+ <th><Message id="fields.order.paid.label" /></th>
<th />
</tr>
</thead>
@@ -118,8 +124,9 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.id}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.name}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.amount}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.summary}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.paid}</td>
<td class="is-actions-cell">
<div class="buttons is-right">
<button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onDelete(i)}>
@@ -144,19 +151,3 @@ function EmptyTable(): VNode {
}
-interface Actions {
- element: MerchantBackend.Instances.Instance;
- type: 'DELETE' | 'UPDATE';
-}
-
-function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
- return value !== null && value !== undefined;
-}
-
-function buildActions(intances: MerchantBackend.Instances.Instance[],
selected: string[], action: 'DELETE'): Actions[] {
- return selected.map(id => intances.find(i => i.id === id))
- .filter(notEmpty)
- .map(id => ({ element: id, type: action }))
-}
-
-
diff --git a/packages/frontend/src/routes/instance/orders/list/index.tsx
b/packages/frontend/src/routes/instance/orders/list/index.tsx
new file mode 100644
index 0000000..d1cff4c
--- /dev/null
+++ b/packages/frontend/src/routes/instance/orders/list/index.tsx
@@ -0,0 +1,35 @@
+import { h, VNode } from 'preact';
+import { useConfigContext } from '../../../../context/backend';
+import { MerchantBackend } from '../../../../declaration';
+import { SwrError, useInstanceOrders, useOrderMutateAPI, useProductMutateAPI }
from '../../../../hooks/backend';
+import { CardTable } from './Table';
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: SwrError) => VNode;
+ onCreate: () => void;
+}
+export default function ({ onUnauthorized, onLoadError, onCreate }: Props):
VNode {
+ const result = useInstanceOrders()
+ const { createOrder, deleteOrder } = useOrderMutateAPI()
+ const { currency } = useConfigContext()
+ if (!result.data) {
+ if (result.unauthorized) return onUnauthorized()
+ if (result.error) return onLoadError(result.error)
+ return <div>
+ loading ....
+ </div>
+ }
+ return <section class="section is-main-section">
+ <CardTable instances={result.data.orders.map(o => ({ ...o, id: o.order_id
}))}
+ onCreate={() => createOrder({
+ order: {
+ amount: `${currency}:${Math.floor(Math.random() * 20 + 1)}`,
+ summary: `some summary with a random number
${Math.floor(Math.random() * 20 + 1)}`,
+ }
+ })}
+ onDelete={(order: MerchantBackend.Orders.OrderHistoryEntry) =>
deleteOrder(order.order_id)}
+ onUpdate={() => null}
+ />
+ </section>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/orders/update/index.tsx
b/packages/frontend/src/routes/instance/orders/update/index.tsx
new file mode 100644
index 0000000..a1f58d3
--- /dev/null
+++ b/packages/frontend/src/routes/instance/orders/update/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>order update page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/products/create/index.tsx
b/packages/frontend/src/routes/instance/products/create/index.tsx
new file mode 100644
index 0000000..cb2a82c
--- /dev/null
+++ b/packages/frontend/src/routes/instance/products/create/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>product list page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/admin/list/Table.tsx
b/packages/frontend/src/routes/instance/products/list/Table.tsx
similarity index 78%
copy from packages/frontend/src/routes/admin/list/Table.tsx
copy to packages/frontend/src/routes/instance/products/list/Table.tsx
index 2c00668..ab3795b 100644
--- a/packages/frontend/src/routes/admin/list/Table.tsx
+++ b/packages/frontend/src/routes/instance/products/list/Table.tsx
@@ -14,26 +14,29 @@
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 { h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend } from "../../../declaration"
+import { MerchantBackend } from "../../../../declaration"
+import { Actions, buildActions } from "../../../../utils/table"
+
+type Entity = MerchantBackend.Products.InventoryEntry & { id: string }
interface Props {
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
onCreate: () => void;
selected?: boolean;
}
export function CardTable({ instances, onCreate, onUpdate, onDelete, selected
}: Props): VNode {
- const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
const [rowSelection, rowSelectionHandler] = useState<string[]>([])
useEffect(() => {
@@ -53,7 +56,7 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
return <div class="card has-table">
<header class="card-header">
- <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Instances" /></p>
+ <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Products" /></p>
<div class="card-header-icon" aria-label="more options">
@@ -83,9 +86,9 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
}
interface TableProps {
rowSelection: string[];
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
}
@@ -104,8 +107,7 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</th>
- <th><Message id="fields.instance.id.label" /></th>
- <th><Message id="fields.instance.name.label" /></th>
+ <th><Message id="fields.product.id.label" /></th>
<th />
</tr>
</thead>
@@ -118,8 +120,7 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.id}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.name}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{ cursor:
'pointer' }} >{i.id}</td>
<td class="is-actions-cell">
<div class="buttons is-right">
<button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onDelete(i)}>
@@ -144,19 +145,3 @@ function EmptyTable(): VNode {
}
-interface Actions {
- element: MerchantBackend.Instances.Instance;
- type: 'DELETE' | 'UPDATE';
-}
-
-function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
- return value !== null && value !== undefined;
-}
-
-function buildActions(intances: MerchantBackend.Instances.Instance[],
selected: string[], action: 'DELETE'): Actions[] {
- return selected.map(id => intances.find(i => i.id === id))
- .filter(notEmpty)
- .map(id => ({ element: id, type: action }))
-}
-
-
diff --git a/packages/frontend/src/routes/instance/products/list/index.tsx
b/packages/frontend/src/routes/instance/products/list/index.tsx
new file mode 100644
index 0000000..a7f271e
--- /dev/null
+++ b/packages/frontend/src/routes/instance/products/list/index.tsx
@@ -0,0 +1,44 @@
+import { h, VNode } from 'preact';
+import { create } from 'yup/lib/Reference';
+import { SwrError, useInstanceProducts, useProductMutateAPI } from
'../../../../hooks/backend';
+import { CardTable } from './Table';
+import logo from '../../../../assets/logo.jpeg';
+import { useConfigContext } from '../../../../context/backend';
+import { MerchantBackend } from '../../../../declaration';
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: SwrError) => VNode;
+}
+export default function ({ onUnauthorized, onLoadError }: Props): VNode {
+ const result = useInstanceProducts()
+ const { createProduct, deleteProduct } = useProductMutateAPI()
+ const { currency } = useConfigContext()
+ if (!result.data) {
+ if (result.unauthorized) return onUnauthorized()
+ if (result.error) return onLoadError(result.error)
+ return <div>
+ loading ....
+ </div>
+ }
+ return <section class="section is-main-section">
+ <CardTable instances={result.data.products.map(o => ({ ...o, id:
o.product_id }))}
+ onCreate={() => createProduct({
+ product_id: `${Math.floor(Math.random() * 999999 + 1)}`,
+ address: {},
+ description: '',
+ description_i18n: {
+ en: '', es: ''
+ },
+ image: {} as string, //WTF?
+ price: `${currency}:${Math.floor(Math.random() * 20 + 1)}`,
+ taxes: [],
+ total_stock: Math.floor(Math.random() * 20 + 1),
+ unit: 'units',
+ next_restock: { t_ms: 'never' }, //WTF? should not be required
+ })}
+ onDelete={(prod: MerchantBackend.Products.InventoryEntry) =>
deleteProduct(prod.product_id)}
+ onUpdate={() => null}
+ />
+ </section>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/products/update/index.tsx
b/packages/frontend/src/routes/instance/products/update/index.tsx
new file mode 100644
index 0000000..f91bf13
--- /dev/null
+++ b/packages/frontend/src/routes/instance/products/update/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>product update page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/tips/create/index.tsx
b/packages/frontend/src/routes/instance/tips/create/index.tsx
new file mode 100644
index 0000000..39608c3
--- /dev/null
+++ b/packages/frontend/src/routes/instance/tips/create/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>tip create page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/admin/list/Table.tsx
b/packages/frontend/src/routes/instance/tips/list/Table.tsx
similarity index 81%
copy from packages/frontend/src/routes/admin/list/Table.tsx
copy to packages/frontend/src/routes/instance/tips/list/Table.tsx
index 2c00668..bd818fd 100644
--- a/packages/frontend/src/routes/admin/list/Table.tsx
+++ b/packages/frontend/src/routes/instance/tips/list/Table.tsx
@@ -22,18 +22,21 @@
import { h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend } from "../../../declaration"
+import { MerchantBackend } from "../../../../declaration"
+import { Actions, buildActions } from "../../../../utils/table"
+
+type Entity = MerchantBackend.Tips.ReserveStatusEntry & { id: string }
interface Props {
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
onCreate: () => void;
selected?: boolean;
}
export function CardTable({ instances, onCreate, onUpdate, onDelete, selected
}: Props): VNode {
- const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
const [rowSelection, rowSelectionHandler] = useState<string[]>([])
useEffect(() => {
@@ -53,7 +56,7 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
return <div class="card has-table">
<header class="card-header">
- <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Instances" /></p>
+ <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Tips" /></p>
<div class="card-header-icon" aria-label="more options">
@@ -83,9 +86,9 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
}
interface TableProps {
rowSelection: string[];
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
}
@@ -104,8 +107,9 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</th>
- <th><Message id="fields.instance.id.label" /></th>
- <th><Message id="fields.instance.name.label" /></th>
+ <th><Message id="fields.tips.committed_amount.label" /></th>
+ <th><Message id="fields.tips.exchange_initial_amount.label" /></th>
+ <th><Message id="fields.tips.merchant_initial_amount.label" /></th>
<th />
</tr>
</thead>
@@ -118,8 +122,9 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.id}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.name}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.committed_amount}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.exchange_initial_amount}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.merchant_initial_amount}</td>
<td class="is-actions-cell">
<div class="buttons is-right">
<button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onDelete(i)}>
@@ -142,21 +147,3 @@ function EmptyTable(): VNode {
<p><Message id="There is no instances yet, add more pressing the + sign"
/></p>
</div>
}
-
-
-interface Actions {
- element: MerchantBackend.Instances.Instance;
- type: 'DELETE' | 'UPDATE';
-}
-
-function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
- return value !== null && value !== undefined;
-}
-
-function buildActions(intances: MerchantBackend.Instances.Instance[],
selected: string[], action: 'DELETE'): Actions[] {
- return selected.map(id => intances.find(i => i.id === id))
- .filter(notEmpty)
- .map(id => ({ element: id, type: action }))
-}
-
-
diff --git a/packages/frontend/src/routes/instance/tips/list/index.tsx
b/packages/frontend/src/routes/instance/tips/list/index.tsx
new file mode 100644
index 0000000..9c7ea6d
--- /dev/null
+++ b/packages/frontend/src/routes/instance/tips/list/index.tsx
@@ -0,0 +1,37 @@
+import { h, VNode } from 'preact';
+import { useConfigContext } from '../../../../context/backend';
+import { MerchantBackend } from '../../../../declaration';
+import { SwrError, useInstanceMutateAPI, useInstanceTips, useTipsMutateAPI }
from '../../../../hooks/backend';
+import { CardTable } from './Table';
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: SwrError) => VNode;
+}
+export default function ({ onUnauthorized, onLoadError }: Props): VNode {
+ const result = useInstanceTips()
+ const { createReserve, deleteReserve } = useTipsMutateAPI()
+ const { currency } = useConfigContext()
+ if (!result.data) {
+ if (result.unauthorized) return onUnauthorized()
+ if (result.error) return onLoadError(result.error)
+ return <div>
+ loading ....
+ </div>
+ }
+ return <section class="section is-main-section">
+ <CardTable instances={result.data.reserves.filter(r => r.active).map(o =>
({ ...o, id: o.reserve_pub }))}
+ onCreate={() => createReserve({
+ // explode with basic
+ wire_method: 'x-taler-bank',
+ initial_balance: `${currency}:${Math.floor(Math.random() * 20 + 1)}`,
+ //explode with 1
+ // hangs with /asd/asd/
+ // http://localhost:8081/
+ exchange_url: 'http://exchange.taler:8081',
+ })}
+ onDelete={(reserve: MerchantBackend.Tips.ReserveStatusEntry) =>
deleteReserve(reserve.reserve_pub)}
+ onUpdate={() => null}
+ />
+ </section>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/tips/update/index.tsx
b/packages/frontend/src/routes/instance/tips/update/index.tsx
new file mode 100644
index 0000000..dc4f045
--- /dev/null
+++ b/packages/frontend/src/routes/instance/tips/update/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>tip update page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/transfers/create/index.tsx
b/packages/frontend/src/routes/instance/transfers/create/index.tsx
new file mode 100644
index 0000000..797ac19
--- /dev/null
+++ b/packages/frontend/src/routes/instance/transfers/create/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>transfer create page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/admin/list/Table.tsx
b/packages/frontend/src/routes/instance/transfers/list/Table.tsx
similarity index 84%
copy from packages/frontend/src/routes/admin/list/Table.tsx
copy to packages/frontend/src/routes/instance/transfers/list/Table.tsx
index 2c00668..b6586ba 100644
--- a/packages/frontend/src/routes/admin/list/Table.tsx
+++ b/packages/frontend/src/routes/instance/transfers/list/Table.tsx
@@ -22,18 +22,21 @@
import { h, VNode } from "preact"
import { Message } from "preact-messages"
import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend } from "../../../declaration"
+import { MerchantBackend } from "../../../../declaration"
+import { Actions, buildActions } from "../../../../utils/table"
+
+type Entity = MerchantBackend.Transfers.TransferDetails & { id: string }
interface Props {
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
onCreate: () => void;
selected?: boolean;
}
export function CardTable({ instances, onCreate, onUpdate, onDelete, selected
}: Props): VNode {
- const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
const [rowSelection, rowSelectionHandler] = useState<string[]>([])
useEffect(() => {
@@ -53,7 +56,7 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
return <div class="card has-table">
<header class="card-header">
- <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Instances" /></p>
+ <p class="card-header-title"><span class="icon"><i class="mdi
mdi-account-multiple" /></span><Message id="Transfers" /></p>
<div class="card-header-icon" aria-label="more options">
@@ -83,9 +86,9 @@ export function CardTable({ instances, onCreate, onUpdate,
onDelete, selected }:
}
interface TableProps {
rowSelection: string[];
- instances: MerchantBackend.Instances.Instance[];
+ instances: Entity[];
onUpdate: (id: string) => void;
- onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
}
@@ -118,8 +121,8 @@ function Table({ rowSelection, rowSelectionHandler,
instances, onUpdate, onDelet
<span class="check" />
</label>
</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.id}</td>
- <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.name}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.credit_amount}</td>
+ <td onClick={(): void => onUpdate(i.id)} style={{cursor:
'pointer'}} >{i.exchange_url}</td>
<td class="is-actions-cell">
<div class="buttons is-right">
<button class="button is-small is-danger jb-modal"
type="button" onClick={(): void => onDelete(i)}>
@@ -144,19 +147,3 @@ function EmptyTable(): VNode {
}
-interface Actions {
- element: MerchantBackend.Instances.Instance;
- type: 'DELETE' | 'UPDATE';
-}
-
-function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
- return value !== null && value !== undefined;
-}
-
-function buildActions(intances: MerchantBackend.Instances.Instance[],
selected: string[], action: 'DELETE'): Actions[] {
- return selected.map(id => intances.find(i => i.id === id))
- .filter(notEmpty)
- .map(id => ({ element: id, type: action }))
-}
-
-
diff --git a/packages/frontend/src/routes/instance/transfers/list/index.tsx
b/packages/frontend/src/routes/instance/transfers/list/index.tsx
new file mode 100644
index 0000000..488130c
--- /dev/null
+++ b/packages/frontend/src/routes/instance/transfers/list/index.tsx
@@ -0,0 +1,36 @@
+import { h, VNode } from 'preact';
+import { useConfigContext } from '../../../../context/backend';
+import { SwrError, useInstanceTransfers, useTransferMutateAPI } from
'../../../../hooks/backend';
+import { CardTable } from './Table';
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: SwrError) => VNode;
+}
+export default function ({ onUnauthorized, onLoadError }: Props): VNode {
+ const result = useInstanceTransfers()
+ const { informTransfer } = useTransferMutateAPI()
+ const { currency } = useConfigContext()
+ if (!result.data) {
+ if (result.unauthorized) return onUnauthorized()
+ if (result.error) return onLoadError(result.error)
+ return <div>
+ loading ....
+ </div>
+ }
+ return <section class="section is-main-section">
+ <CardTable instances={result.data.transfers.map(o => ({ ...o, id:
String(o.transfer_serial_id) }))}
+ onCreate={() => informTransfer({
+ wtid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+
+ // exchange: payto://x-taler-bank/bank.taler:5882/exchangeminator
+ // payto://x-taler-bank/bank.taler:5882/9?subject=qwe&amount=COL:10
+ payto_uri: 'payto://x-taler-bank/bank.taler:5882/blogger',
+ exchange_url: 'http://exchange.taler:8081/',
+ credit_amount: 'COL:2'
+ })}
+ onDelete={() => null}
+ onUpdate={() => null}
+ />
+ </section>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/transfers/update/index.tsx
b/packages/frontend/src/routes/instance/transfers/update/index.tsx
new file mode 100644
index 0000000..a1f58d3
--- /dev/null
+++ b/packages/frontend/src/routes/instance/transfers/update/index.tsx
@@ -0,0 +1,5 @@
+import { h, VNode } from 'preact';
+
+export default function ():VNode {
+ return <div>order update page</div>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/routes/instance/update/index.tsx
b/packages/frontend/src/routes/instance/update/index.tsx
index 2f75258..51bb6a5 100644
--- a/packages/frontend/src/routes/instance/update/index.tsx
+++ b/packages/frontend/src/routes/instance/update/index.tsx
@@ -18,7 +18,7 @@ import { useState } from "preact/hooks";
import { UpdateTokenModal } from "../../../components/modal";
import { useInstanceContext } from "../../../context/backend";
import { MerchantBackend } from "../../../declaration";
-import { SwrError, useBackendInstance, useInstanceMutateAPI } from
"../../../hooks/backend";
+import { SwrError, useInstanceDetails, useInstanceMutateAPI } from
"../../../hooks/backend";
import { UpdatePage } from "./UpdatePage";
interface Props {
@@ -35,7 +35,7 @@ interface Props {
export default function Update({ onBack, onConfirm, onLoadError,
onUpdateError, onUnauthorized }: Props): VNode {
const { updateInstance, setNewToken, clearToken } = useInstanceMutateAPI();
const [updatingToken, setUpdatingToken] = useState<boolean>(false)
- const details = useBackendInstance()
+ const details = useInstanceDetails()
const { id, token } = useInstanceContext()
if (!details.data) {
diff --git a/packages/frontend/src/utils/functions.ts
b/packages/frontend/src/utils/functions.ts
index f51aacf..7550e68 100644
--- a/packages/frontend/src/utils/functions.ts
+++ b/packages/frontend/src/utils/functions.ts
@@ -20,7 +20,18 @@ export function hasKey<O>(obj: O, key: string | number |
symbol): key is keyof O
return key in obj
}
+declare global {
+ interface Window { MerchantBackoffice: any; }
+}
+
+if (typeof window !== "undefined") {
+ window.MerchantBackoffice = window.MerchantBackoffice || {
+ missing_locales: [],
+ getMissingTranslation: () => Array.from(new
Set(window.MerchantBackoffice.missing_locales)).filter(i => i).map(i => `msgid
"${i}"\nmsgstr ""\n`).join('\n')
+ };
+}
+
export function onTranslationError(error: MessageError) {
if (typeof window === "undefined") return;
- (window as any)['missing_locale'] = ([] as string[]).concat((window as
any)['missing_locale']).concat(error.path.join())
+ window.MerchantBackoffice.missing_locales =
window.MerchantBackoffice.missing_locales.concat(error.path.join())
}
diff --git a/packages/frontend/src/utils/table.ts
b/packages/frontend/src/utils/table.ts
new file mode 100644
index 0000000..d9e3d53
--- /dev/null
+++ b/packages/frontend/src/utils/table.ts
@@ -0,0 +1,20 @@
+
+
+export interface Actions<T extends WithId> {
+ element: T;
+ type: 'DELETE' | 'UPDATE';
+}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
+
+interface WithId {
+ id: string
+}
+
+export function buildActions<T extends WithId>(intances: T[], selected:
string[], action: 'DELETE'): Actions<T>[] {
+ return selected.map(id => intances.find(i => i.id === id))
+ .filter(notEmpty)
+ .map(id => ({ element: id, type: action }))
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.