[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.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-merchant-backoffice] branch master updated: reserve and tips,
gnunet <=