gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

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