gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: reserve and tips


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: reserve and tips
Date: Fri, 14 May 2021 23:19:02 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new bed0ebb  reserve and tips
bed0ebb is described below

commit bed0ebb449a3427b58a007cb1cf0e9f03baf2736
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri May 14 18:18:34 2021 -0300

    reserve and tips
---
 CHANGELOG.md                                       |  13 +-
 packages/frontend/src/InstanceRoutes.tsx           |  32 ++-
 packages/frontend/src/components/menu/SideBar.tsx  |   8 +-
 packages/frontend/src/components/modal/index.tsx   |  23 ++-
 packages/frontend/src/declaration.d.ts             |  70 +++++++
 packages/frontend/src/hooks/tips.ts                |  96 +++++++--
 .../frontend/src/paths/instance/details/index.tsx  |   2 -
 .../src/paths/instance/orders/list/Table.tsx       |   4 +-
 .../paths/instance/reserves/create/CreatePage.tsx  |  77 ++++++++
 .../reserves/create/CreatedSuccessfully.tsx        |  56 ++++++
 .../src/paths/instance/reserves/create/index.tsx   |  61 ++++++
 .../paths/instance/reserves/details/DetailPage.tsx | 117 +++++++++++
 .../src/paths/instance/reserves/details/index.tsx  |  47 +++++
 .../instance/reserves/list/AutorizeTipModal.tsx    |  81 ++++++++
 .../instance/reserves/list/CreatedSuccessfully.tsx |  81 ++++++++
 .../src/paths/instance/reserves/list/Table.tsx     | 219 +++++++++++++++++++++
 .../src/paths/instance/reserves/list/index.tsx     |  97 +++++++++
 .../src/paths/instance/tips/create/index.tsx       |  26 ---
 .../src/paths/instance/tips/list/Table.tsx         | 150 --------------
 .../src/paths/instance/tips/list/index.tsx         |  60 ------
 .../src/paths/instance/tips/update/index.tsx       |  26 ---
 packages/frontend/src/schemas/index.ts             |  11 +-
 22 files changed, 1061 insertions(+), 296 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ec01db..e6aa6b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,11 +23,18 @@ and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0
 
  - unlock a product when is locked
  - check that there is no place where the taxes are summing up 
- - translation missing: yup (check for some other dynamic message)
+ - translation missing: yup (check for some ot  her dynamic message)
  - contract terms
  - fulfillment url should check absolute url or relative to the merchant domain
- - duplicate order
- - order 
+ - duplicate order button
+ - simplify order 
+ - react routing refactor to use query parameters from history
+ - create taler ui
+ - contract terms in the wallet
+ - when backoffice get a response of the merchant that have info about the 
reponse of the exchange, the error is not readed correctly... see wire transfer
+ - when creating the first default instance, the page keeps reloading 
preventing for filling the form
+ - 
+
 ## [Unreleased]
  - fixed bug when updating token and not admin
  - showing a yellow bar on non-default instance navigation (admin)
diff --git a/packages/frontend/src/InstanceRoutes.tsx 
b/packages/frontend/src/InstanceRoutes.tsx
index 6da3e78..993d5b9 100644
--- a/packages/frontend/src/InstanceRoutes.tsx
+++ b/packages/frontend/src/InstanceRoutes.tsx
@@ -40,6 +40,9 @@ import ProductListPage from './paths/instance/products/list';
 import ProductUpdatePage from './paths/instance/products/update';
 import TransferListPage from './paths/instance/transfers/list';
 import TransferCreatePage from './paths/instance/transfers/create';
+import ReservesCreatePage from './paths/instance/reserves/create';
+import ReservesDetailsPage from './paths/instance/reserves/details';
+import ReservesListPage from './paths/instance/reserves/list';
 import InstanceUpdatePage, { Props as InstanceUpdatePageProps } from 
"./paths/instance/update";
 import LoginPage from './paths/login';
 import NotFoundPage from './paths/notfound';
@@ -58,9 +61,9 @@ export enum InstancePaths {
   order_new = '/order/new',
   order_details = '/order/:oid/details',
 
-  // tips_list = '/tips',
-  // tips_update = '/tip/:rid/update',
-  // tips_new = '/tip/new',
+  reserves_list = '/reserves',
+  reserves_details = '/reserves/:rid/details',
+  reserves_new = '/reserves/new',
 
   transfers_list = '/transfers',
   transfers_new = '/transfer/new',
@@ -253,6 +256,29 @@ export function InstanceRoutes({ id, admin }: Props): 
VNode {
         onConfirm={() => { route(InstancePaths.transfers_list) }}
         onBack={() => { route(InstancePaths.transfers_list) }}
       />
+
+      {/**
+       * reserves pages
+       */}
+      <Route path={InstancePaths.reserves_list} component={ReservesListPage}
+        onUnauthorized={LoginPageAccessDenied}
+        onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+        onSelect={(id: string) => { 
route(InstancePaths.reserves_details.replace(':rid', id)) }}
+        onCreate={() => { route(InstancePaths.reserves_new) }}
+      />
+
+      <Route path={InstancePaths.reserves_details} 
component={ReservesDetailsPage}
+        onUnauthorized={LoginPageAccessDenied}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)}
+        onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+        onBack={() => { route(InstancePaths.reserves_list) }}
+      />
+
+      <Route path={InstancePaths.reserves_new} component={ReservesCreatePage}
+        onConfirm={() => { route(InstancePaths.reserves_list) }}
+        onBack={() => { route(InstancePaths.reserves_list) }}
+      />
       {/**
        * Example pages
        */}
diff --git a/packages/frontend/src/components/menu/SideBar.tsx 
b/packages/frontend/src/components/menu/SideBar.tsx
index c46b75c..2562802 100644
--- a/packages/frontend/src/components/menu/SideBar.tsx
+++ b/packages/frontend/src/components/menu/SideBar.tsx
@@ -77,12 +77,12 @@ export function Sidebar({ mobile, instance, onLogout, admin 
}: Props): VNode {
               <span 
class="menu-item-label"><Translate>Transfers</Translate></span>
             </a>
           </li>
-          {/* <li>
-            <a href="/tips" class="has-icon">
+          <li>
+            <a href="/reserves" class="has-icon">
               <span class="icon"><i class="mdi mdi-cash" /></span>
-              <span class="menu-item-label">Tips</span>
+              <span class="menu-item-label">Reserves</span>
             </a>
-          </li> */}
+          </li>
         </ul>
         <p class="menu-label"><Translate>Connection</Translate></p>
         <ul class="menu-list">
diff --git a/packages/frontend/src/components/modal/index.tsx 
b/packages/frontend/src/components/modal/index.tsx
index 0038703..f4445f0 100644
--- a/packages/frontend/src/components/modal/index.tsx
+++ b/packages/frontend/src/components/modal/index.tsx
@@ -41,7 +41,7 @@ export function ConfirmModal({ active, description, onCancel, 
onConfirm, childre
     <div class="modal-background " onClick={onCancel} />
     <div class="modal-card">
       <header class="modal-card-head">
-        {!description ? null : <p class="modal-card-title">{description}</p>}
+        {!description ? null : <p class="modal-card-title  
has-text-white-ter">{description}</p>}
         <button class="delete " aria-label="close" onClick={onCancel} />
       </header>
       <section class="modal-card-body">
@@ -58,6 +58,27 @@ export function ConfirmModal({ active, description, 
onCancel, onConfirm, childre
   </div>
 }
 
+export function ContinueModal({ active, description, onCancel, onConfirm, 
children, disabled }: Props): VNode {
+  return <div class={active ? "modal is-active" : "modal"}>
+    <div class="modal-background " onClick={onCancel} />
+    <div class="modal-card">
+      <header class="modal-card-head has-background-success">
+        {!description ? null : <p class="modal-card-title">{description}</p>}
+        <button class="delete " aria-label="close" onClick={onCancel} />
+      </header>
+      <section class="modal-card-body">
+        {children}
+      </section>
+      <footer class="modal-card-foot">
+        <div class="buttons is-right" style={{ width: '100%' }}>
+          <button class="button is-success " disabled={disabled} 
onClick={onConfirm} ><Translate>Continue</Translate></button>
+        </div>
+      </footer>
+    </div>
+    <button class="modal-close is-large " aria-label="close" 
onClick={onCancel} />
+  </div>
+}
+
 export function ClearConfirmModal({ description, onCancel, onClear, onConfirm, 
children, disabled }: Props & { onClear?: () => void }): VNode {
   return <div class="modal is-active">
     <div class="modal-background " onClick={onCancel} />
diff --git a/packages/frontend/src/declaration.d.ts 
b/packages/frontend/src/declaration.d.ts
index 0586cd7..04a2e2d 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -929,6 +929,76 @@ export namespace MerchantBackend {
             tip_expiration: Timestamp;
         }
 
+        interface ReserveDetail {
+            // 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;
+
+            // Array of all tips created by this reserves (possibly empty!).
+            // Only present if asked for explicitly.
+            tips?: TipStatusEntry[];
+
+            // Is this reserve active (false if it was deleted but not purged)?
+            active: boolean;
+        }
+        interface TipStatusEntry {
+
+            // Unique identifier for the tip.
+            tip_id: HashCode;
+
+            // Total amount of the tip that can be withdrawn.
+            total_amount: Amount;
+
+            // Human-readable reason for why the tip was granted.
+            reason: string;
+        }
+
+        interface TipDetails {
+            // Amount that we authorized for this tip.
+            total_authorized: Amount;
+
+            // Amount that was picked up by the user already.
+            total_picked_up: Amount;
+
+            // Human-readable reason given when authorizing the tip.
+            reason: string;
+
+            // Timestamp indicating when the tip is set to expire (may be in 
the past).
+            expiration: Timestamp;
+
+            // Reserve public key from which the tip is funded.
+            reserve_pub: EddsaPublicKey;
+
+            // Array showing the pickup operations of the wallet (possibly 
empty!).
+            // Only present if asked for explicitly.
+            pickups?: PickupDetail[];
+        }
+        interface PickupDetail {
+            // Unique identifier for the pickup operation.
+            pickup_id: HashCode;
+
+            // Number of planchets involved.
+            num_planchets: Integer;
+
+            // Total amount requested for this pickup_id.
+            requested_amount: Amount;
+        }
+
     }
 
     namespace Transfers {
diff --git a/packages/frontend/src/hooks/tips.ts 
b/packages/frontend/src/hooks/tips.ts
index 52d3762..345e1fa 100644
--- a/packages/frontend/src/hooks/tips.ts
+++ b/packages/frontend/src/hooks/tips.ts
@@ -20,7 +20,7 @@ import { MerchantBackend } from '../declaration';
 import { fetcher, HttpError, HttpResponse, HttpResponseOk, mutateAll, request 
} from './backend';
 
 
-export function useTipsMutateAPI(): TipsMutateAPI {
+export function useReservesAPI(): ReserveMutateAPI {
   const { url: baseUrl, token: adminToken } = useBackendContext();
   const { token: instanceToken, id, admin } = useInstanceContext();
 
@@ -31,48 +31,56 @@ export function useTipsMutateAPI(): TipsMutateAPI {
   };
 
   const createReserve = async (data: 
MerchantBackend.Tips.ReserveCreateRequest): 
Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>> => {
-    mutateAll(/@"\/private\/reserves"@/);
-
-    return 
request<MerchantBackend.Tips.ReserveCreateConfirmation>(`${url}/private/reserves`,
 {
+    const res = await 
request<MerchantBackend.Tips.ReserveCreateConfirmation>(`${url}/private/reserves`,
 {
       method: 'post',
       token,
       data
     });
-  };
 
-  const authorizeTipReserve = (pub: string, data: 
MerchantBackend.Tips.TipCreateRequest): 
Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
-    mutateAll(/@"\/private\/reserves"@/);
+    await mutateAll(/@"\/private\/reserves"@/);
+
+    return res
+  };
 
-    return 
request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/reserves/${pub}/authorize-tip`,
 {
+  const authorizeTipReserve = async (pub: string, data: 
MerchantBackend.Tips.TipCreateRequest): 
Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
+    const res = await 
request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/reserves/${pub}/authorize-tip`,
 {
       method: 'post',
       token,
       data
     });
-  };
+    await mutateAll(/@"\/private\/reserves"@/);
 
-  const authorizeTip = (data: MerchantBackend.Tips.TipCreateRequest): 
Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
-    mutateAll(/@"\/private\/reserves"@/);
+    return res
+  };
 
-    return 
request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/tips`, {
+  const authorizeTip = async (data: MerchantBackend.Tips.TipCreateRequest): 
Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
+    const res = await 
request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/tips`, {
       method: 'post',
       token,
       data
     });
+
+    await mutateAll(/@"\/private\/reserves"@/);
+
+    return res
   };
 
-  const deleteReserve = (pub: string): Promise<HttpResponse<void>> => {
-    mutateAll(/@"\/private\/reserves"@/);
-    return request(`${url}/private/reserves/${pub}`, {
+  const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => {
+    const res = await request<void>(`${url}/private/reserves/${pub}`, {
       method: 'delete',
       token,
     });
+
+    await mutateAll(/@"\/private\/reserves"@/);
+
+    return res
   };
 
 
   return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve };
 }
 
-export interface TipsMutateAPI {
+export interface ReserveMutateAPI {
   createReserve: (data: MerchantBackend.Tips.ReserveCreateRequest) => 
Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>;
   authorizeTipReserve: (id: string, data: 
MerchantBackend.Tips.TipCreateRequest) => 
Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
   authorizeTip: (data: MerchantBackend.Tips.TipCreateRequest) => 
Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
@@ -91,9 +99,61 @@ export function useInstanceTips(): 
HttpResponse<MerchantBackend.Tips.TippingRese
 
   const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, 
HttpError>([`/private/reserves`, token, url], fetcher)
 
-  if (isValidating) return {loading:true, data: data?.data}
+  if (isValidating) return { loading: true, data: data?.data }
+  if (data) return data
+  if (error) return error
+  return { loading: true }
+}
+
+
+export function useReserveDetails(reserveId: string): 
HttpResponse<MerchantBackend.Tips.ReserveDetail> {
+  const { url: baseUrl } = useBackendContext();
+  const { token, id: instanceId, admin } = useInstanceContext();
+
+  const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
+
+  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, 
HttpError>([`/private/reserves/${reserveId}`, token, url], 
reserveDetailFetcher, {
+    refreshInterval:0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+  })
+
+  if (isValidating) return { loading: true, data: data?.data }
+  if (data) return data
+  if (error) return error
+  return { loading: true }
+}
+
+export function useTipDetails(tipId: string): 
HttpResponse<MerchantBackend.Tips.TipDetails> {
+  const { url: baseUrl } = useBackendContext();
+  const { token, id: instanceId, admin } = useInstanceContext();
+
+  const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
+
+  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Tips.TipDetails>, 
HttpError>([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, {
+    refreshInterval:0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+  })
+
+  if (isValidating) return { loading: true, data: data?.data }
   if (data) return data
   if (error) return error
-  return {loading: true}
+  return { loading: true }
 }
 
+export function reserveDetailFetcher<T>(url: string, token: string, backend: 
string): Promise<HttpResponseOk<T>> {
+  return request<T>(`${backend}${url}`, { token, params: {
+    tips: 'yes'
+  } })
+}
+
+export function tipsDetailFetcher<T>(url: string, token: string, backend: 
string): Promise<HttpResponseOk<T>> {
+  return request<T>(`${backend}${url}`, { token, params: {
+    pickups: 'yes'
+  } })
+}
diff --git a/packages/frontend/src/paths/instance/details/index.tsx 
b/packages/frontend/src/paths/instance/details/index.tsx
index cda56a4..1567589 100644
--- a/packages/frontend/src/paths/instance/details/index.tsx
+++ b/packages/frontend/src/paths/instance/details/index.tsx
@@ -54,10 +54,8 @@ export default function Detail({ onUpdate, onLoadError, 
onUnauthorized, onDelete
       onConfirm={async (): Promise<void> => {
         try {
           await deleteInstance()
-          // pushNotification({ message: 'delete_success', type: 'SUCCESS' })
           onDelete()
         } catch (error) {
-          // pushNotification({ message: 'delete_error', type: 'ERROR' })
         }
         setDeleting(false)
       }}
diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx 
b/packages/frontend/src/paths/instance/orders/list/Table.tsx
index ef8efd1..fb97346 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -31,7 +31,7 @@ import { ConfirmModal } from "../../../../components/modal";
 import { MerchantBackend, WithId } from "../../../../declaration";
 import { useOrderDetails } from "../../../../hooks/order";
 import { Translate, useTranslator } from "../../../../i18n";
-import { RefoundSchema } from "../../../../schemas";
+import { AuthorizeTipSchema, RefundSchema as RefundSchema } from 
"../../../../schemas";
 import { mergeRefunds, subtractPrices, sumPrices } from 
"../../../../utils/amount";
 import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants";
 
@@ -170,7 +170,7 @@ export function RefundModal({ id, onCancel, onConfirm }: 
RefundModalProps): VNod
 
   const validateAndConfirm = () => {
     try {
-      RefoundSchema.validateSync(form, { abortEarly: false })
+      RefundSchema.validateSync(form, { abortEarly: false })
       if (!form.refund) return;
       onConfirm({
         refund: form.refund,
diff --git 
a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
new file mode 100644
index 0000000..3531aae
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
@@ -0,0 +1,77 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormErrors, FormProvider } from 
"../../../../components/form/FormProvider";
+import { Input } from "../../../../components/form/Input";
+import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { MerchantBackend } from "../../../../declaration";
+import { Translate, useTranslator } from "../../../../i18n";
+
+type Entity = MerchantBackend.Tips.ReserveCreateRequest
+
+interface Props {
+  onCreate: (d: Entity) => void;
+  onBack?: () => void;
+}
+
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+  const [reserve, setReserve] = useState<Partial<Entity>>({
+    initial_balance: 'COL:2',
+    exchange_url: 'http://exchange.taler:8081/',
+    wire_method: 'x-taler-bank',
+  })
+  const i18n = useTranslator()
+
+  const errors : FormErrors<Entity> = {
+
+  }
+
+  const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
+
+  const submitForm = () => {
+    if (hasErrors) return
+    onCreate(reserve as Entity)
+  }
+
+  return <div>
+    <section class="section is-main-section">
+      <div class="columns">
+        <div class="column" />
+        <div class="column is-two-thirds">
+          <FormProvider<Entity> object={reserve} valueHandler={setReserve}>
+            <InputCurrency<Entity> name="initial_balance" label={i18n`Initial 
balance`} />
+            <Input<Entity> name="exchange_url" label={i18n`Exchange`} />
+            <Input<Entity> name="wire_method" label={i18n`Wire method`} />
+          </FormProvider>
+
+          <div class="buttons is-right mt-5">
+            {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
+            <button class="button is-success" onClick={submitForm} 
><Translate>Confirm</Translate></button>
+          </div>
+        </div>
+        <div class="column" />
+      </div>
+    </section>
+  </div>
+}
\ No newline at end of file
diff --git 
a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx 
b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
new file mode 100644
index 0000000..af27c5b
--- /dev/null
+++ 
b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
@@ -0,0 +1,56 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from 
"../../../../components/notifications/CreatedSuccessfully";
+import { MerchantBackend } from "../../../../declaration";
+
+type Entity = MerchantBackend.Tips.ReserveCreateConfirmation;
+
+interface Props {
+  entity: Entity;
+  onConfirm: () => void;
+  onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: 
Props): VNode {
+
+  return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+    <div class="field is-horizontal">
+      <div class="field-label is-normal">
+        <label class="label">Account address</label>
+      </div>
+      <div class="field-body is-flex-grow-3">
+        <div class="field">
+          <p class="control">
+            <input readonly class="input" value={entity.payto_uri} />
+          </p>
+        </div>
+      </div>
+    </div>
+    <div class="field is-horizontal">
+      <div class="field-label is-normal">
+        <label class="label">Message</label>
+      </div>
+      <div class="field-body is-flex-grow-3">
+        <div class="field">
+          <p class="control">
+            <input class="input" readonly value={entity.reserve_pub} />
+          </p>
+        </div>
+      </div>
+    </div>
+  </Template>;
+}
diff --git a/packages/frontend/src/paths/instance/reserves/create/index.tsx 
b/packages/frontend/src/paths/instance/reserves/create/index.tsx
new file mode 100644
index 0000000..d7de65b
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { Fragment, h, VNode } from 'preact';
+import { useState } from 'preact/hooks';
+import { NotificationCard } from '../../../../components/menu';
+import { MerchantBackend } from '../../../../declaration';
+import { useReservesAPI } from '../../../../hooks/tips';
+import { useTranslator } from '../../../../i18n';
+import { Notification } from '../../../../utils/types';
+import { CreatedSuccessfully } from './CreatedSuccessfully';
+import { CreatePage } from './CreatePage';
+
+interface Props {
+  onBack: () => void;
+  onConfirm: () => void;
+}
+export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
+  const { createReserve } = useReservesAPI()
+  const [notif, setNotif] = useState<Notification | undefined>(undefined)
+  const i18n = useTranslator()
+
+  const [createdOk, setCreatedOk] = 
useState<MerchantBackend.Tips.ReserveCreateConfirmation | undefined>(undefined);
+
+  if (createdOk) {
+    return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />
+  }
+
+  return <Fragment>
+    <NotificationCard notification={notif} />
+    <CreatePage
+      onBack={onBack}
+      onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => {
+        createReserve(request).then((r) => setCreatedOk(r.data)).catch((error) 
=> {
+          setNotif({
+            message: i18n`could not create reserve`,
+            type: "ERROR",
+            description: error.message
+          })
+        })
+      }} />
+  </Fragment>
+}
diff --git 
a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx 
b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx
new file mode 100644
index 0000000..3771337
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx
@@ -0,0 +1,117 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { format, isAfter } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../../components/form/FormProvider";
+import { Input } from "../../../../components/form/Input";
+import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { InputDate } from "../../../../components/form/InputDate";
+import { InputDuration } from "../../../../components/form/InputDuration";
+import { InputGroup } from "../../../../components/form/InputGroup";
+import { InputLocation } from "../../../../components/form/InputLocation";
+import { NotificationCard } from "../../../../components/menu";
+import { ProductList } from "../../../../components/product/ProductList";
+import { MerchantBackend } from "../../../../declaration";
+import { useTipDetails } from "../../../../hooks/tips";
+import { Translate, useTranslator } from "../../../../i18n";
+import { mergeRefunds } from "../../../../utils/amount";
+
+type Entity = MerchantBackend.Tips.ReserveDetail;
+type CT = MerchantBackend.ContractTerms
+
+interface Props {
+  onBack: () => void;
+  selected: Entity;
+}
+
+export function DetailPage({ selected }: Props): VNode {
+  const i18n = useTranslator()
+  return <Fragment>
+    <FormProvider object={selected} valueHandler={null} >
+      <InputDate<Entity> name="creation_time" label={i18n`Created at`} 
readonly />
+      <InputDate<Entity> name="expiration_time" label={i18n`Valid until`} 
readonly />
+      <InputCurrency<Entity> name="merchant_initial_amount" 
label={i18n`Created balance`} readonly />
+      <InputCurrency<Entity> name="exchange_initial_amount" 
label={i18n`Exchange balance`} readonly />
+      <InputCurrency<Entity> name="pickup_amount" label={i18n`Picked up`} 
readonly />
+      <InputCurrency<Entity> name="committed_amount" label={i18n`Committed`} 
readonly />
+    </FormProvider>
+    {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} 
/> : <div>
+      no tips for this reserve
+    </div>}
+  </Fragment>
+}
+
+async function copyToClipboard(text: string) {
+  return navigator.clipboard.writeText(text)
+}
+
+interface TableProps {
+  tips: MerchantBackend.Tips.TipStatusEntry[];
+}
+
+function Table({ tips }: TableProps): VNode {
+  return (
+    <div class="table-container">
+      <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+        <thead>
+          <tr>
+            <th><Translate>Authorized</Translate></th>
+            <th><Translate>Picked up</Translate></th>
+            <th><Translate>Reason</Translate></th>
+            <th><Translate>Expiration</Translate></th>
+          </tr>
+        </thead>
+        <tbody>
+          {tips.map((t, i) => {
+            return <TipRow id={t.tip_id} key={i} entry={t} />
+          })}
+        </tbody>
+      </table></div>)
+}
+
+function TipRow({ id, entry }: { id: string, entry: 
MerchantBackend.Tips.TipStatusEntry }) {
+  const result = useTipDetails(id)
+  if (result.loading) {
+    return <tr>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+  }
+  if (!result.ok) {
+    return <tr>
+      <td>...</td>
+      <td>{entry.total_amount}</td>
+      <td>{entry.total_amount}</td>
+      <td>expired</td>
+    </tr>
+  }
+  const info = result.data
+  return <tr>
+    <td>{info.total_authorized}</td>
+    <td>{info.total_picked_up}</td>
+    <td>{info.reason}</td>
+    <td>{info.expiration.t_ms === "never" ? "never" : 
format(info.expiration.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td>
+  </tr>
+}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/reserves/details/index.tsx 
b/packages/frontend/src/paths/instance/reserves/details/index.tsx
new file mode 100644
index 0000000..569ef5b
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/details/index.tsx
@@ -0,0 +1,47 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { Fragment, h, VNode } from 'preact';
+import { Loading } from '../../../../components/exception/loading';
+import { HttpError } from '../../../../hooks/backend';
+import { useReserveDetails } from '../../../../hooks/tips';
+import { DetailPage } from './DetailPage';
+
+interface Props {
+  rid: string;
+
+  onUnauthorized: () => VNode;
+  onLoadError: (error: HttpError) => VNode;
+  onNotFound: () => VNode;
+  onDelete: () => void;
+  onBack: () => void;
+}
+export default function DetailReserve({rid, onUnauthorized, onLoadError, 
onNotFound, onBack, onDelete}: Props):VNode {
+  const result = useReserveDetails(rid)
+
+  if (result.clientError && result.isUnauthorized) return onUnauthorized()
+  if (result.clientError && result.isNotfound) return onNotFound()
+  if (result.loading) return <Loading />
+  if (!result.ok) return onLoadError(result)
+  return <Fragment>
+    <DetailPage selected={result.data} onBack={onBack} />
+  </Fragment>
+}
diff --git 
a/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx 
b/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx
new file mode 100644
index 0000000..72215fb
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormErrors, FormProvider } from 
"../../../../components/form/FormProvider";
+import { Input } from "../../../../components/form/Input";
+import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { ConfirmModal, ContinueModal } from "../../../../components/modal";
+import { MerchantBackend } from "../../../../declaration";
+import { useTranslator } from "../../../../i18n";
+import { AuthorizeTipSchema } from "../../../../schemas";
+import { CreatedSuccessfully } from "./CreatedSuccessfully";
+
+interface AutorizaTipModalProps {
+  onCancel: () => void;
+  onConfirm: (value: MerchantBackend.Tips.TipCreateRequest) => void;
+  tipAuthorized?: {
+    response: MerchantBackend.Tips.TipCreateConfirmation;
+    request: MerchantBackend.Tips.TipCreateRequest;
+  };
+}
+
+export function AuthorizeTipModal({ onCancel, onConfirm, tipAuthorized }: 
AutorizaTipModalProps): VNode {
+  // const result = useOrderDetails(id)
+  type State = MerchantBackend.Tips.TipCreateRequest
+  const [form, setValue] = useState<Partial<State>>({})
+  const i18n = useTranslator();
+  const [errors, setErrors] = useState<FormErrors<State>>({})
+
+  const validateAndConfirm = () => {
+    try {
+      AuthorizeTipSchema.validateSync(form, { abortEarly: false })
+      onConfirm(form as State)
+    } catch (err) {
+      const errors = err.inner as any[]
+      const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ 
...prev, [cur.path]: cur.message }), {})
+      setErrors(pathMessages)
+    }
+  }
+
+  if (tipAuthorized) {
+    return <ContinueModal description="tip" active onConfirm={onCancel}>
+      <CreatedSuccessfully
+        entity={tipAuthorized.response}
+        request={tipAuthorized.request}
+        onConfirm={onCancel}
+      />
+    </ContinueModal>
+  }
+
+  return <ConfirmModal description="tip" active onCancel={onCancel} 
onConfirm={validateAndConfirm}>
+
+    <FormProvider<State> errors={errors} object={form} valueHandler={setValue} 
>
+      <InputCurrency<State> name="amount" label={i18n`Amount`} />
+      <Input<State> name="justification" label={i18n`Justification`} 
inputType="multiline" />
+      <Input<State> name="next_url" label={i18n`URL after tip`} />
+    </FormProvider>
+
+  </ConfirmModal>
+}
+
+
diff --git 
a/packages/frontend/src/paths/instance/reserves/list/CreatedSuccessfully.tsx 
b/packages/frontend/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
new file mode 100644
index 0000000..7e8dd2c
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from 
"../../../../components/notifications/CreatedSuccessfully";
+import { MerchantBackend } from "../../../../declaration";
+
+type Entity = MerchantBackend.Tips.TipCreateConfirmation;
+
+interface Props {
+  entity: Entity;
+  request: MerchantBackend.Tips.TipCreateRequest,
+  onConfirm: () => void;
+  onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({ request, entity, onConfirm, 
onCreateAnother }: Props): VNode {
+  return <Fragment>
+    <div class="field is-horizontal">
+      <div class="field-label is-normal">
+        <label class="label">Amount</label>
+      </div>
+      <div class="field-body is-flex-grow-3">
+        <div class="field">
+          <p class="control">
+            <input readonly class="input" value={request.amount} />
+          </p>
+        </div>
+      </div>
+    </div>
+    <div class="field is-horizontal">
+      <div class="field-label is-normal">
+        <label class="label">Justification</label>
+      </div>
+      <div class="field-body is-flex-grow-3">
+        <div class="field">
+          <p class="control">
+            <input readonly class="input" value={request.justification} />
+          </p>
+        </div>
+      </div>
+    </div>
+    <div class="field is-horizontal">
+      <div class="field-label is-normal">
+        <label class="label">URL</label>
+      </div>
+      <div class="field-body is-flex-grow-3">
+        <div class="field">
+          <p class="control">
+            <input readonly class="input" value={entity.tip_status_url} />
+          </p>
+        </div>
+      </div>
+    </div>
+    <div class="field is-horizontal">
+      <div class="field-label is-normal">
+        <label class="label">Valid until</label>
+      </div>
+      <div class="field-body is-flex-grow-3">
+        <div class="field">
+          <p class="control">
+            <input class="input" readonly value={!entity.tip_expiration || 
entity.tip_expiration.t_ms === "never" ? "never" : 
format(entity.tip_expiration.t_ms, 'yyyy/MM/dd HH:mm:ss')} />
+          </p>
+        </div>
+      </div>
+    </div>
+  </Fragment>;
+}
diff --git a/packages/frontend/src/paths/instance/reserves/list/Table.tsx 
b/packages/frontend/src/paths/instance/reserves/list/Table.tsx
new file mode 100644
index 0000000..58a2df5
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/list/Table.tsx
@@ -0,0 +1,219 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+ /**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { format } from "date-fns"
+import { Fragment, h, VNode } from "preact"
+import { StateUpdater, useEffect, useState } from "preact/hooks"
+import { MerchantBackend, WithId } from "../../../../declaration"
+import { Translate } from "../../../../i18n"
+import { Actions, buildActions } from "../../../../utils/table"
+
+type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId
+
+interface Props {
+  instances: Entity[];
+  onNewTip: (id: Entity) => void;
+  onSelect: (id: Entity) => void;
+  onDelete: (id: Entity) => void;
+  onCreate: () => void;
+  selected?: boolean;
+}
+
+export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete, 
selected }: Props): VNode {
+  const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
+  const [rowSelection, rowSelectionHandler] = useState<string[]>([])
+
+  useEffect(() => {
+    if (actionQueue.length > 0 && !selected && actionQueue[0].type == 
'DELETE') {
+      onDelete(actionQueue[0].element)
+      actionQueueHandler(actionQueue.slice(1))
+    }
+  }, [actionQueue, selected, onDelete])
+
+  useEffect(() => {
+    if (actionQueue.length > 0 && !selected && actionQueue[0].type == 
'UPDATE') {
+      onNewTip(actionQueue[0].element)
+      actionQueueHandler(actionQueue.slice(1))
+    }
+  }, [actionQueue, selected, onNewTip])
+
+  const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
+    const amount = current.exchange_initial_amount
+    if (amount.endsWith(':0')) {
+      prev[0] = prev[0].concat(current)
+    } else {
+      prev[1] = prev[1].concat(current)
+    }
+    return prev
+  }, new Array<Array<Entity>>([],[]))
+
+
+  return <Fragment>
+    {withoutFunds.length > 0 && <div class="card has-table">
+    <header class="card-header">
+      <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" 
/></span><Translate>Reserves not yet funded</Translate></p>
+
+      <div class="card-header-icon" aria-label="more options">
+
+        <button class={rowSelection.length > 0 ? "button is-danger" : 
"is-hidden"}
+          type="button" onClick={(): void => 
actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
+          Delete
+        </button>
+      </div>
+      <div class="card-header-icon" aria-label="more options">
+        <button class="button is-info" type="button" onClick={onCreate}>
+          <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" 
/></span>
+        </button>
+      </div>
+
+    </header>
+    <div class="card-content">
+      <div class="b-table has-pagination">
+        <div class="table-wrapper has-mobile-cards">
+          <TableWithoutFund instances={withoutFunds} onNewTip={onNewTip} 
onSelect={onSelect} onDelete={onDelete} rowSelection={rowSelection} 
rowSelectionHandler={rowSelectionHandler} />
+        </div>
+      </div>
+    </div>
+  </div> }
+
+  <div class="card has-table">
+    <header class="card-header">
+      <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" 
/></span><Translate>Reserves ready</Translate></p>
+
+      <div class="card-header-icon" aria-label="more options">
+
+        <button class={rowSelection.length > 0 ? "button is-danger" : 
"is-hidden"}
+          type="button" onClick={(): void => 
actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
+          Delete
+        </button>
+      </div>
+      <div class="card-header-icon" aria-label="more options">
+        <button class="button is-info" type="button" onClick={onCreate}>
+          <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" 
/></span>
+        </button>
+      </div>
+
+    </header>
+    <div class="card-content">
+      <div class="b-table has-pagination">
+        <div class="table-wrapper has-mobile-cards">
+          {withFunds.length > 0 ?
+            <Table instances={withFunds} onNewTip={onNewTip} 
onSelect={onSelect} onDelete={onDelete} rowSelection={rowSelection} 
rowSelectionHandler={rowSelectionHandler} /> :
+            <EmptyTable />
+          }
+        </div>
+      </div>
+    </div>
+  </div>
+  </Fragment>
+}
+interface TableProps {
+  rowSelection: string[];
+  instances: Entity[];
+  onNewTip: (id: Entity) => void;
+  onDelete: (id: Entity) => void;
+  onSelect: (id: Entity) => void;
+  rowSelectionHandler: StateUpdater<string[]>;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+  return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : 
prev.filter(e => e != id)
+}
+
+function Table({ rowSelection, rowSelectionHandler, instances, onNewTip, 
onSelect, onDelete }: TableProps): VNode {
+  return (
+    <div class="table-container">
+    <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+      <thead>
+        <tr>
+          <th><Translate>Created at</Translate></th>
+          <th><Translate>Expires at</Translate></th>
+          <th><Translate>Initial</Translate></th>
+          <th><Translate>Picked up</Translate></th>
+          <th><Translate>Committed</Translate></th>
+          <th />
+        </tr>
+      </thead>
+      <tbody>
+        {instances.map(i => {
+          return <tr key={i.id}>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 
'yyyy/MM/dd HH:mm:ss')}</td>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 
'yyyy/MM/dd HH:mm:ss')}</td>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.exchange_initial_amount}</td>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.pickup_amount}</td>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.committed_amount}</td>
+            <td class="is-actions-cell right-sticky">
+              <div class="buttons is-right">
+                <button class="button is-small is-danger jb-modal" 
type="button" onClick={(): void => onDelete(i)}>
+                  Delete
+                </button>
+                <button class="button is-small is-info jb-modal" type="button" 
onClick={(): void => onNewTip(i)}>
+                  New Tip
+                </button>
+              </div>
+            </td>
+          </tr>
+        })}
+
+      </tbody>
+    </table></div>)
+}
+
+function EmptyTable(): VNode {
+  return <div class="content has-text-grey has-text-centered">
+    <p>
+      <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" 
/></span>
+    </p>
+    <p><Translate>There is no ready reserves yet, add more pressing the + sign 
or fund them</Translate></p>
+  </div>
+}
+
+function TableWithoutFund({ rowSelection, rowSelectionHandler, instances, 
onNewTip, onSelect, onDelete }: TableProps): VNode {
+  return (
+    <div class="table-container">
+    <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+      <thead>
+        <tr>
+          <th><Translate>Created at</Translate></th>
+          <th><Translate>Expires at</Translate></th>
+          <th><Translate>Expected Balance</Translate></th>
+          <th />
+        </tr>
+      </thead>
+      <tbody>
+        {instances.map(i => {
+          return <tr key={i.id}>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 
'yyyy/MM/dd HH:mm:ss')}</td>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 
'yyyy/MM/dd HH:mm:ss')}</td>
+            <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} 
>{i.merchant_initial_amount}</td>
+            <td class="is-actions-cell right-sticky">
+              <div class="buttons is-right">
+                <button class="button is-small is-danger jb-modal" 
type="button" onClick={(): void => onDelete(i)}>
+                  Delete
+                </button>
+              </div>
+            </td>
+          </tr>
+        })}
+
+      </tbody>
+    </table></div>)
+}
diff --git a/packages/frontend/src/paths/instance/reserves/list/index.tsx 
b/packages/frontend/src/paths/instance/reserves/list/index.tsx
new file mode 100644
index 0000000..4c9a7df
--- /dev/null
+++ b/packages/frontend/src/paths/instance/reserves/list/index.tsx
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from 'preact';
+import { useState } from 'preact/hooks';
+import { Loading } from '../../../../components/exception/loading';
+import { NotificationCard } from '../../../../components/menu';
+import { MerchantBackend } from '../../../../declaration';
+import { HttpError } from '../../../../hooks/backend';
+import { useInstanceTips, useReservesAPI } from "../../../../hooks/tips";
+import { useTranslator } from '../../../../i18n';
+import { Notification } from '../../../../utils/types';
+import { CardTable } from './Table';
+import { AuthorizeTipModal } from './AutorizeTipModal';
+
+interface Props {
+  onUnauthorized: () => VNode;
+  onLoadError: (e: HttpError) => VNode;
+  onSelect: (id: string) => void;
+  onNotFound: () => VNode;
+  onCreate: () => void;
+}
+
+interface TipConfirmation {
+  response: MerchantBackend.Tips.TipCreateConfirmation;
+  request: MerchantBackend.Tips.TipCreateRequest;
+}
+
+export default function ListTips({ onUnauthorized, onLoadError, onNotFound, 
onSelect, onCreate }: Props): VNode {
+  const result = useInstanceTips()
+  const { deleteReserve, authorizeTipReserve } = useReservesAPI()
+  const [notif, setNotif] = useState<Notification | undefined>(undefined)
+  const i18n = useTranslator()
+  const [reserveForTip, setReserveForTip] = useState<string | 
undefined>(undefined);
+  const [tipAuthorized, setTipAuthorized] = useState<TipConfirmation | 
undefined>(undefined);
+
+  if (result.clientError && result.isUnauthorized) return onUnauthorized()
+  if (result.clientError && result.isNotfound) return onNotFound()
+  if (result.loading) return <Loading />
+  if (!result.ok) return onLoadError(result)
+
+
+  return <section class="section is-main-section">
+    <NotificationCard notification={notif} />
+
+    {reserveForTip && (
+      <AuthorizeTipModal
+        onCancel={() => {
+          setReserveForTip(undefined)
+          setTipAuthorized(undefined)
+        }}
+        tipAuthorized={tipAuthorized}
+        onConfirm={async (request) => {
+          try {
+            const response = await authorizeTipReserve(reserveForTip, request)
+            setTipAuthorized({
+              request, response: response.data
+            })
+          } catch (error) {
+            setNotif({
+              message: i18n`could not create the tip`,
+              type: "ERROR",
+              description: error.message
+            })
+            setReserveForTip(undefined)
+          }
+        }
+        }
+      />
+    )}
+
+    <CardTable instances={result.data.reserves.filter(r => r.active).map(o => 
({ ...o, id: o.reserve_pub }))}
+      onCreate={onCreate}
+      onDelete={(reserve) => deleteReserve(reserve.reserve_pub)}
+      onSelect={(reserve) => onSelect(reserve.id)}
+      onNewTip={(reserve) => setReserveForTip(reserve.id)}
+    />
+  </section>
+}
diff --git a/packages/frontend/src/paths/instance/tips/create/index.tsx 
b/packages/frontend/src/paths/instance/tips/create/index.tsx
deleted file mode 100644
index 677a209..0000000
--- a/packages/frontend/src/paths/instance/tips/create/index.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode } from 'preact';
-
-export default function CreateTips():VNode {
-  return <div>tip create page</div>
-}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/tips/list/Table.tsx 
b/packages/frontend/src/paths/instance/tips/list/Table.tsx
deleted file mode 100644
index 722b013..0000000
--- a/packages/frontend/src/paths/instance/tips/list/Table.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode } from "preact"
-import { StateUpdater, useEffect, useState } from "preact/hooks"
-import { MerchantBackend, WithId } from "../../../../declaration"
-import { Translate } from "../../../../i18n"
-import { Actions, buildActions } from "../../../../utils/table"
-
-type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId
-
-interface Props {
-  instances: Entity[];
-  onUpdate: (id: string) => void;
-  onDelete: (id: Entity) => void;
-  onCreate: () => void;
-  selected?: boolean;
-}
-
-export function CardTable({ instances, onCreate, onUpdate, onDelete, selected 
}: Props): VNode {
-  const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]);
-  const [rowSelection, rowSelectionHandler] = useState<string[]>([])
-
-  useEffect(() => {
-    if (actionQueue.length > 0 && !selected && actionQueue[0].type == 
'DELETE') {
-      onDelete(actionQueue[0].element)
-      actionQueueHandler(actionQueue.slice(1))
-    }
-  }, [actionQueue, selected, onDelete])
-
-  useEffect(() => {
-    if (actionQueue.length > 0 && !selected && actionQueue[0].type == 
'UPDATE') {
-      onUpdate(actionQueue[0].element.id)
-      actionQueueHandler(actionQueue.slice(1))
-    }
-  }, [actionQueue, selected, onUpdate])
-
-
-  return <div class="card has-table">
-    <header class="card-header">
-      <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" 
/></span><Translate>Tips</Translate></p>
-
-      <div class="card-header-icon" aria-label="more options">
-
-        <button class={rowSelection.length > 0 ? "button is-danger" : 
"is-hidden"}
-          type="button" onClick={(): void => 
actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
-          Delete
-        </button>
-      </div>
-      <div class="card-header-icon" aria-label="more options">
-        <button class="button is-info" type="button" onClick={onCreate}>
-          <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" 
/></span>
-        </button>
-      </div>
-
-    </header>
-    <div class="card-content">
-      <div class="b-table has-pagination">
-        <div class="table-wrapper has-mobile-cards">
-          {instances.length > 0 ?
-            <Table instances={instances} onUpdate={onUpdate} 
onDelete={onDelete} rowSelection={rowSelection} 
rowSelectionHandler={rowSelectionHandler} /> :
-            <EmptyTable />
-          }
-        </div>
-      </div>
-    </div>
-  </div>
-}
-interface TableProps {
-  rowSelection: string[];
-  instances: Entity[];
-  onUpdate: (id: string) => void;
-  onDelete: (id: Entity) => void;
-  rowSelectionHandler: StateUpdater<string[]>;
-}
-
-function toggleSelected<T>(id: T): (prev: T[]) => T[] {
-  return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : 
prev.filter(e => e != id)
-}
-
-function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, 
onDelete }: TableProps): VNode {
-  return (
-    <div class="table-container">
-    <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
-      <thead>
-        <tr>
-          <th class="is-checkbox-cell">
-            <label class="b-checkbox checkbox">
-              <input type="checkbox" checked={rowSelection.length === 
instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length 
=== instances.length ? [] : instances.map(i => i.id))} />
-              <span class="check" />
-            </label>
-          </th>
-          <th><Translate>Committed amount</Translate></th>
-          <th><Translate>Exchange initial amount</Translate></th>
-          <th><Translate>Merchant initial amount</Translate></th>
-          <th />
-        </tr>
-      </thead>
-      <tbody>
-        {instances.map(i => {
-          return <tr key={i.id}>
-            <td class="is-checkbox-cell">
-              <label class="b-checkbox checkbox">
-                <input type="checkbox" checked={rowSelection.indexOf(i.id) != 
-1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} />
-                <span class="check" />
-              </label>
-            </td>
-            <td onClick={(): void => onUpdate(i.id)} style={{cursor: 
'pointer'}} >{i.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 right-sticky">
-              <div class="buttons is-right">
-                <button class="button is-small is-danger jb-modal" 
type="button" onClick={(): void => onDelete(i)}>
-                  Delete
-                </button>
-              </div>
-            </td>
-          </tr>
-        })}
-
-      </tbody>
-    </table></div>)
-}
-
-function EmptyTable(): VNode {
-  return <div class="content has-text-grey has-text-centered">
-    <p>
-      <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" 
/></span>
-    </p>
-    <p><Translate>There is no tips yet, add more pressing the + 
sign</Translate></p>
-  </div>
-}
diff --git a/packages/frontend/src/paths/instance/tips/list/index.tsx 
b/packages/frontend/src/paths/instance/tips/list/index.tsx
deleted file mode 100644
index e1afcca..0000000
--- a/packages/frontend/src/paths/instance/tips/list/index.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode } from 'preact';
-import { Loading } from '../../../../components/exception/loading';
-import { useConfigContext } from '../../../../context/config';
-import { MerchantBackend } from '../../../../declaration';
-import { HttpError } from '../../../../hooks/backend';
-import { useInstanceTips, useTipsMutateAPI } from "../../../../hooks/tips";
-import { CardTable } from './Table';
-
-interface Props {
-  onUnauthorized: () => VNode;
-  onLoadError: (e: HttpError) => VNode;
-  onNotFound: () => VNode;
-}
-export default function ListTips({ onUnauthorized, onLoadError, onNotFound }: 
Props): VNode {
-  const result = useInstanceTips()
-  const { createReserve, deleteReserve } = useTipsMutateAPI()
-  const { currency } = useConfigContext()
-
-  if (result.clientError && result.isUnauthorized) return onUnauthorized()
-  if (result.clientError && result.isNotfound) return onNotFound()
-  if (result.loading) return <Loading />
-  if (!result.ok) return onLoadError(result)
-
-  return <section class="section is-main-section">
-    <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-ban',
-        initial_balance: `${currency}:${Math.floor(Math.random() * 20 + 1)}`,
-        //explode with 1
-        // hangs with /asd/asd/
-        // http://localhost:8081/
-        exchange_url: 'https://exchange-demo.rigel.ar/',
-      })}
-      onDelete={(reserve: MerchantBackend.Tips.ReserveStatusEntry) => 
deleteReserve(reserve.reserve_pub)}
-      onUpdate={() => null}
-    />
-  </section>
-}
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/tips/update/index.tsx 
b/packages/frontend/src/paths/instance/tips/update/index.tsx
deleted file mode 100644
index f7d4c78..0000000
--- a/packages/frontend/src/paths/instance/tips/update/index.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, VNode } from 'preact';
-
-export default function UpdateTips():VNode {
-  return <div>tip update page</div>
-}
\ No newline at end of file
diff --git a/packages/frontend/src/schemas/index.ts 
b/packages/frontend/src/schemas/index.ts
index 4e0e1c8..1c58016 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -113,7 +113,7 @@ export const InstanceSchema = yup.object().shape({
 export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']);
 export const InstanceCreateSchema = InstanceSchema.clone();
 
-export const RefoundSchema = yup.object().shape({
+export const RefundSchema = yup.object().shape({
   mainReason: yup.string().required(),
   description: yup.string().required(),
   refund: yup.string()
@@ -122,6 +122,15 @@ export const RefoundSchema = yup.object().shape({
     .test('amount_positive', 'the amount is not valid', currencyGreaterThan0),
 })
 
+export const AuthorizeTipSchema = yup.object().shape({
+  justification: yup.string().required(),
+  amount: yup.string()
+    .required()
+    .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
+    .test('amount_positive', 'the amount is not valid', currencyGreaterThan0),
+  next_url: yup.string().required(),
+})
+
 const stringIsValidJSON = (value?: string) => {
   const p = value?.trim()
   if (!p) return true;

-- 
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]