fixing #6096
merchant details and contract terms details factored out, to be used by other components tests and stories updated payment completed != confirmed (confirmed if paid by someone else)
This commit is contained in:
parent
4409d8384b
commit
7a600514c6
91
packages/taler-wallet-webextension/src/components/Modal.tsx
Normal file
91
packages/taler-wallet-webextension/src/components/Modal.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 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 { styled } from "@linaria/react";
|
||||||
|
import { ComponentChildren, h, VNode } from "preact";
|
||||||
|
import { ButtonHandler } from "../mui/handlers.js";
|
||||||
|
import closeIcon from "../svg/close_24px.svg";
|
||||||
|
import { Link, LinkPrimary, LinkWarning } from "./styled/index.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ComponentChildren;
|
||||||
|
onClose: ButtonHandler;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullSize = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 5%;
|
||||||
|
vertical-align: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Body = styled.div`
|
||||||
|
height: 95%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function Modal({ title, children, onClose }: Props): VNode {
|
||||||
|
return (
|
||||||
|
<FullSize onClick={onClose?.onClick}>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
background: "white",
|
||||||
|
width: 600,
|
||||||
|
height: "80%",
|
||||||
|
margin: "auto",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 8,
|
||||||
|
// overflow: "scroll",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Header>
|
||||||
|
<div>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
</div>
|
||||||
|
<Link onClick={onClose?.onClick}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
marginLeft: 4,
|
||||||
|
marginRight: 4,
|
||||||
|
// fill: "white",
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: closeIcon }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Header>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
|
||||||
|
</div>
|
||||||
|
</FullSize>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 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 { WalletContractData } from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { createExample } from "../test-utils.js";
|
||||||
|
import {
|
||||||
|
ErrorView,
|
||||||
|
HiddenView,
|
||||||
|
LoadingView,
|
||||||
|
ShowView,
|
||||||
|
} from "./ShowFullContractTermPopup.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "component/ShowFullContractTermPopup",
|
||||||
|
};
|
||||||
|
|
||||||
|
const cd: WalletContractData = {
|
||||||
|
amount: {
|
||||||
|
currency: "ARS",
|
||||||
|
fraction: 0,
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
contractTermsHash:
|
||||||
|
"92X0KSJPZ8XS2XECCGFWTCGW8XMFCXTT2S6WHZDP6H9Y3TSKMTHY94WXEWDERTNN5XWCYGW4VN5CF2D4846HXTW7P06J4CZMHCWKC9G",
|
||||||
|
fulfillmentUrl: "",
|
||||||
|
merchantBaseUrl: "https://merchant-backend.taler.ar/",
|
||||||
|
merchantPub: "JZYHJ13M91GMSQMT75J8Q6ZN0QP8XF8CRHR7K5MMWYE8JQB6AAPG",
|
||||||
|
merchantSig:
|
||||||
|
"0YA1WETV15R6K8QKS79QA3QMT16010F42Q49VSKYQ71HVQKAG0A4ZJCA4YTKHE9EA5SP156TJSKZEJJJ87305N6PS80PC48RNKYZE08",
|
||||||
|
orderId: "2022.220-0281XKKB8W7YE",
|
||||||
|
summary: "w",
|
||||||
|
maxWireFee: {
|
||||||
|
currency: "ARS",
|
||||||
|
fraction: 0,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
payDeadline: {
|
||||||
|
t_s: 1660002673,
|
||||||
|
},
|
||||||
|
refundDeadline: {
|
||||||
|
t_s: 1660002673,
|
||||||
|
},
|
||||||
|
wireFeeAmortization: 1,
|
||||||
|
allowedAuditors: [
|
||||||
|
{
|
||||||
|
auditorBaseUrl: "https://auditor.taler.ar/",
|
||||||
|
auditorPub: "0000000000000000000000000000000000000000000000000000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allowedExchanges: [
|
||||||
|
{
|
||||||
|
exchangeBaseUrl: "https://exchange.taler.ar/",
|
||||||
|
exchangePub: "1C2EYE90PYDNVRTQ25A3PA0KW5W4WPAJNNQHVHV49PT6W5CERFV0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: {
|
||||||
|
t_s: 1659972710,
|
||||||
|
},
|
||||||
|
wireMethod: "x-taler-bank",
|
||||||
|
wireInfoHash:
|
||||||
|
"QDT28374ZHYJ59WQFZ3TW1D5WKJVDYHQT86VHED3TNMB15ANJSKXDYPPNX01348KDYCX6T4WXA5A8FJJ8YWNEB1JW726C1JPKHM89DR",
|
||||||
|
maxDepositFee: {
|
||||||
|
currency: "ARS",
|
||||||
|
fraction: 0,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
merchant: {
|
||||||
|
name: "Default",
|
||||||
|
address: {
|
||||||
|
country: "ar",
|
||||||
|
},
|
||||||
|
jurisdiction: {
|
||||||
|
country: "ar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
products: [],
|
||||||
|
autoRefund: undefined,
|
||||||
|
summaryI18n: undefined,
|
||||||
|
deliveryDate: undefined,
|
||||||
|
deliveryLocation: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowingSimpleOrder = createExample(ShowView, {
|
||||||
|
contractTerms: cd,
|
||||||
|
});
|
||||||
|
export const Error = createExample(ErrorView, {
|
||||||
|
proposalId: "asd",
|
||||||
|
error: {
|
||||||
|
hasError: true,
|
||||||
|
message: "message",
|
||||||
|
operational: false,
|
||||||
|
// details: {
|
||||||
|
// code: 123,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const Loading = createExample(LoadingView, {});
|
||||||
|
export const Hidden = createExample(HiddenView, {});
|
@ -0,0 +1,385 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 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 { AbsoluteTime, Duration, Location } from "@gnu-taler/taler-util";
|
||||||
|
import { WalletContractData } from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { styled } from "@linaria/react";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Loading } from "../components/Loading.js";
|
||||||
|
import { LoadingError } from "../components/LoadingError.js";
|
||||||
|
import { Modal } from "../components/Modal.js";
|
||||||
|
import { Time } from "../components/Time.js";
|
||||||
|
import { useTranslationContext } from "../context/translation.js";
|
||||||
|
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||||
|
import { ButtonHandler } from "../mui/handlers.js";
|
||||||
|
import { compose, StateViewMap } from "../utils/index.js";
|
||||||
|
import * as wxApi from "../wxApi.js";
|
||||||
|
import { Amount } from "./Amount.js";
|
||||||
|
import { Link, LinkPrimary } from "./styled/index.js";
|
||||||
|
|
||||||
|
const ContractTermsTable = styled.table`
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 0px;
|
||||||
|
& > tr > td {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
& > tr > td:nth-child(2n) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
& > tr:nth-child(2n) {
|
||||||
|
background: #ebebeb;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function locationAsText(l: Location | undefined): VNode {
|
||||||
|
if (!l) return <span />;
|
||||||
|
const lines = [
|
||||||
|
...(l.address_lines || []).map((e) => [e]),
|
||||||
|
[l.town_location, l.town, l.street],
|
||||||
|
[l.building_name, l.building_number],
|
||||||
|
[l.country, l.country_subdivision],
|
||||||
|
[l.district, l.post_code],
|
||||||
|
];
|
||||||
|
//remove all missing value
|
||||||
|
//then remove all empty lines
|
||||||
|
const curated = lines
|
||||||
|
.map((l) => l.filter((v) => !!v))
|
||||||
|
.filter((l) => l.length > 0);
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{curated.map((c, i) => (
|
||||||
|
<div key={i}>{c.join(",")}</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = States.Loading | States.Error | States.Hidden | States.Show;
|
||||||
|
|
||||||
|
namespace States {
|
||||||
|
export interface Loading {
|
||||||
|
status: "loading";
|
||||||
|
hideHandler: ButtonHandler;
|
||||||
|
}
|
||||||
|
export interface Error {
|
||||||
|
status: "error";
|
||||||
|
proposalId: string;
|
||||||
|
error: HookError;
|
||||||
|
hideHandler: ButtonHandler;
|
||||||
|
}
|
||||||
|
export interface Hidden {
|
||||||
|
status: "hidden";
|
||||||
|
showHandler: ButtonHandler;
|
||||||
|
}
|
||||||
|
export interface Show {
|
||||||
|
status: "show";
|
||||||
|
hideHandler: ButtonHandler;
|
||||||
|
contractTerms: WalletContractData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useComponentState({ proposalId }: Props, api: typeof wxApi): State {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const hook = useAsyncAsHook(async () => {
|
||||||
|
if (!show) return undefined;
|
||||||
|
return await api.getContractTermsDetails(proposalId);
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
const hideHandler = {
|
||||||
|
onClick: async () => setShow(false),
|
||||||
|
};
|
||||||
|
const showHandler = {
|
||||||
|
onClick: async () => setShow(true),
|
||||||
|
};
|
||||||
|
if (!show) {
|
||||||
|
return {
|
||||||
|
status: "hidden",
|
||||||
|
showHandler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!hook) return { status: "loading", hideHandler };
|
||||||
|
if (hook.hasError)
|
||||||
|
return { status: "error", proposalId, error: hook, hideHandler };
|
||||||
|
if (!hook.response) return { status: "loading", hideHandler };
|
||||||
|
return {
|
||||||
|
status: "show",
|
||||||
|
contractTerms: hook.response,
|
||||||
|
hideHandler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewMapping: StateViewMap<State> = {
|
||||||
|
loading: LoadingView,
|
||||||
|
error: ErrorView,
|
||||||
|
show: ShowView,
|
||||||
|
hidden: HiddenView,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowFullContractTermPopup = compose(
|
||||||
|
"ShowFullContractTermPopup",
|
||||||
|
(p: Props) => useComponentState(p, wxApi),
|
||||||
|
viewMapping,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function LoadingView({ hideHandler }: States.Loading): VNode {
|
||||||
|
return (
|
||||||
|
<Modal title="Full detail" onClose={hideHandler}>
|
||||||
|
<Loading />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorView({
|
||||||
|
hideHandler,
|
||||||
|
error,
|
||||||
|
proposalId,
|
||||||
|
}: States.Error): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
return (
|
||||||
|
<Modal title="Full detail" onClose={hideHandler}>
|
||||||
|
<LoadingError
|
||||||
|
title={
|
||||||
|
<i18n.Translate>
|
||||||
|
Could not load purchase proposal details
|
||||||
|
</i18n.Translate>
|
||||||
|
}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HiddenView({ showHandler }: States.Hidden): VNode {
|
||||||
|
return <Link onClick={showHandler?.onClick}>Show full details</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
|
||||||
|
const createdAt = AbsoluteTime.fromTimestamp(contractTerms.timestamp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="Full detail" onClose={hideHandler}>
|
||||||
|
<div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
|
||||||
|
<ContractTermsTable>
|
||||||
|
<tr>
|
||||||
|
<td>Order Id</td>
|
||||||
|
<td>{contractTerms.orderId}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Summary</td>
|
||||||
|
<td>{contractTerms.summary}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Amount</td>
|
||||||
|
<td>
|
||||||
|
<Amount value={contractTerms.amount} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant name</td>
|
||||||
|
<td>{contractTerms.merchant.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant jurisdiction</td>
|
||||||
|
<td>{locationAsText(contractTerms.merchant.jurisdiction)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant address</td>
|
||||||
|
<td>{locationAsText(contractTerms.merchant.address)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant logo</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={contractTerms.merchant.logo}
|
||||||
|
style={{ width: 64, height: 64, margin: 4 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant website</td>
|
||||||
|
<td>{contractTerms.merchant.website}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant email</td>
|
||||||
|
<td>{contractTerms.merchant.email}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Merchant public key</td>
|
||||||
|
<td>
|
||||||
|
<span title={contractTerms.merchantPub}>
|
||||||
|
{contractTerms.merchantPub.substring(0, 6)}...
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Delivery date</td>
|
||||||
|
<td>
|
||||||
|
{contractTerms.deliveryDate && (
|
||||||
|
<Time
|
||||||
|
timestamp={AbsoluteTime.fromTimestamp(
|
||||||
|
contractTerms.deliveryDate,
|
||||||
|
)}
|
||||||
|
format="dd MMMM yyyy, HH:mm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Delivery location</td>
|
||||||
|
<td>{locationAsText(contractTerms.deliveryLocation)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Products</td>
|
||||||
|
<td>
|
||||||
|
{!contractTerms.products || contractTerms.products.length === 0
|
||||||
|
? "none"
|
||||||
|
: contractTerms.products
|
||||||
|
.map((p) => `${p.description} x ${p.quantity}`)
|
||||||
|
.join(", ")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Created at</td>
|
||||||
|
<td>
|
||||||
|
{contractTerms.timestamp && (
|
||||||
|
<Time
|
||||||
|
timestamp={AbsoluteTime.fromTimestamp(
|
||||||
|
contractTerms.timestamp,
|
||||||
|
)}
|
||||||
|
format="dd MMMM yyyy, HH:mm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Refund deadline</td>
|
||||||
|
<td>
|
||||||
|
{
|
||||||
|
<Time
|
||||||
|
timestamp={AbsoluteTime.fromTimestamp(
|
||||||
|
contractTerms.refundDeadline,
|
||||||
|
)}
|
||||||
|
format="dd MMMM yyyy, HH:mm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Auto refund</td>
|
||||||
|
<td>
|
||||||
|
{
|
||||||
|
<Time
|
||||||
|
timestamp={AbsoluteTime.addDuration(
|
||||||
|
createdAt,
|
||||||
|
!contractTerms.autoRefund
|
||||||
|
? Duration.getZero()
|
||||||
|
: Duration.fromTalerProtocolDuration(
|
||||||
|
contractTerms.autoRefund,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
format="dd MMMM yyyy, HH:mm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Pay deadline</td>
|
||||||
|
<td>
|
||||||
|
{
|
||||||
|
<Time
|
||||||
|
timestamp={AbsoluteTime.fromTimestamp(
|
||||||
|
contractTerms.payDeadline,
|
||||||
|
)}
|
||||||
|
format="dd MMMM yyyy, HH:mm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Fulfillment URL</td>
|
||||||
|
<td>{contractTerms.fulfillmentUrl}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Fulfillment message</td>
|
||||||
|
<td>{contractTerms.fulfillmentMessage}</td>
|
||||||
|
</tr>
|
||||||
|
{/* <tr>
|
||||||
|
<td>Public reorder URL</td>
|
||||||
|
<td>{contractTerms.public_reorder_url}</td>
|
||||||
|
</tr> */}
|
||||||
|
<tr>
|
||||||
|
<td>Max deposit fee</td>
|
||||||
|
<td>
|
||||||
|
<Amount value={contractTerms.maxDepositFee} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Max fee</td>
|
||||||
|
<td>
|
||||||
|
<Amount value={contractTerms.maxWireFee} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Minimum age</td>
|
||||||
|
<td>{contractTerms.minimumAge}</td>
|
||||||
|
</tr>
|
||||||
|
{/* <tr>
|
||||||
|
<td>Extra</td>
|
||||||
|
<td>
|
||||||
|
<pre>{contractTerms.}</pre>
|
||||||
|
</td>
|
||||||
|
</tr> */}
|
||||||
|
<tr>
|
||||||
|
<td>Wire fee amortization</td>
|
||||||
|
<td>{contractTerms.wireFeeAmortization}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Auditors</td>
|
||||||
|
<td>
|
||||||
|
{(contractTerms.allowedAuditors || []).map((e) => (
|
||||||
|
<Fragment key={e.auditorPub}>
|
||||||
|
<a href={e.auditorBaseUrl} title={e.auditorPub}>
|
||||||
|
{e.auditorPub.substring(0, 6)}...
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Exchanges</td>
|
||||||
|
<td>
|
||||||
|
{(contractTerms.allowedExchanges || []).map((e) => (
|
||||||
|
<Fragment key={e.exchangePub}>
|
||||||
|
<a href={e.exchangeBaseUrl} title={e.exchangePub}>
|
||||||
|
{e.exchangePub.substring(0, 6)}...
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ContractTermsTable>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -19,8 +19,9 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as a1 from "./Banner.stories.js";
|
import * as a1 from "./Banner.stories.js";
|
||||||
import * as a2 from "./PendingTransactions.stories.js";
|
import * as a2 from "./PendingTransactions.stories.js";
|
||||||
import * as a3 from "./Amount.stories.js";
|
import * as a3 from "./Amount.stories.js";
|
||||||
|
import * as a4 from "./ShowFullContractTermPopup.stories.js";
|
||||||
|
|
||||||
export default [a1, a2, a3];
|
export default [a1, a2, a3, a4];
|
||||||
|
@ -40,8 +40,18 @@ export const WalletAction = styled.div`
|
|||||||
& h1:first-child {
|
& h1:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
& > * {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
section {
|
section {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
|
table td {
|
||||||
|
padding: 5px 5px;
|
||||||
|
}
|
||||||
|
table tr {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AmountJson, ConfirmPayResult, PreparePayResult } from "@gnu-taler/taler-util";
|
import { AmountJson, ConfirmPayResult, PreparePayResult, PreparePayResultAlreadyConfirmed, PreparePayResultInsufficientBalance, PreparePayResultPaymentPossible } from "@gnu-taler/taler-util";
|
||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||||
import { ButtonHandler } from "../../mui/handlers.js";
|
import { ButtonHandler } from "../../mui/handlers.js";
|
||||||
@ -37,6 +37,7 @@ export type State =
|
|||||||
| State.Ready
|
| State.Ready
|
||||||
| State.NoEnoughBalance
|
| State.NoEnoughBalance
|
||||||
| State.NoBalanceForCurrency
|
| State.NoBalanceForCurrency
|
||||||
|
| State.Completed
|
||||||
| State.Confirmed;
|
| State.Confirmed;
|
||||||
|
|
||||||
export namespace State {
|
export namespace State {
|
||||||
@ -52,8 +53,6 @@ export namespace State {
|
|||||||
|
|
||||||
interface BaseInfo {
|
interface BaseInfo {
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
totalFees: AmountJson;
|
|
||||||
payStatus: PreparePayResult;
|
|
||||||
uri: string;
|
uri: string;
|
||||||
error: undefined;
|
error: undefined;
|
||||||
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
|
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
|
||||||
@ -61,20 +60,30 @@ export namespace State {
|
|||||||
}
|
}
|
||||||
export interface NoBalanceForCurrency extends BaseInfo {
|
export interface NoBalanceForCurrency extends BaseInfo {
|
||||||
status: "no-balance-for-currency"
|
status: "no-balance-for-currency"
|
||||||
|
payStatus: PreparePayResult;
|
||||||
balance: undefined;
|
balance: undefined;
|
||||||
}
|
}
|
||||||
export interface NoEnoughBalance extends BaseInfo {
|
export interface NoEnoughBalance extends BaseInfo {
|
||||||
status: "no-enough-balance"
|
status: "no-enough-balance"
|
||||||
|
payStatus: PreparePayResult;
|
||||||
balance: AmountJson;
|
balance: AmountJson;
|
||||||
}
|
}
|
||||||
export interface Ready extends BaseInfo {
|
export interface Ready extends BaseInfo {
|
||||||
status: "ready";
|
status: "ready";
|
||||||
|
payStatus: PreparePayResultPaymentPossible;
|
||||||
payHandler: ButtonHandler;
|
payHandler: ButtonHandler;
|
||||||
balance: AmountJson;
|
balance: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Confirmed extends BaseInfo {
|
export interface Confirmed extends BaseInfo {
|
||||||
status: "confirmed";
|
status: "confirmed";
|
||||||
|
payStatus: PreparePayResultAlreadyConfirmed;
|
||||||
|
balance: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Completed extends BaseInfo {
|
||||||
|
status: "completed";
|
||||||
|
payStatus: PreparePayResult;
|
||||||
payResult: ConfirmPayResult;
|
payResult: ConfirmPayResult;
|
||||||
payHandler: ButtonHandler;
|
payHandler: ButtonHandler;
|
||||||
balance: AmountJson;
|
balance: AmountJson;
|
||||||
@ -87,6 +96,7 @@ const viewMapping: StateViewMap<State> = {
|
|||||||
"no-balance-for-currency": BaseView,
|
"no-balance-for-currency": BaseView,
|
||||||
"no-enough-balance": BaseView,
|
"no-enough-balance": BaseView,
|
||||||
confirmed: BaseView,
|
confirmed: BaseView,
|
||||||
|
completed: BaseView,
|
||||||
ready: BaseView,
|
ready: BaseView,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,20 +78,9 @@ export function useComponentState(
|
|||||||
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
|
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
let totalFees = Amounts.getZero(amount.currency);
|
|
||||||
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
|
||||||
const amountEffective: AmountJson = Amounts.parseOrThrow(
|
|
||||||
payStatus.amountEffective,
|
|
||||||
);
|
|
||||||
totalFees = Amounts.sub(amountEffective, amount).amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseResult = {
|
const baseResult = {
|
||||||
uri: hook.response.uri,
|
uri: hook.response.uri,
|
||||||
amount,
|
amount,
|
||||||
totalFees,
|
|
||||||
payStatus,
|
|
||||||
error: undefined,
|
error: undefined,
|
||||||
goBack, goToWalletManualWithdraw
|
goBack, goToWalletManualWithdraw
|
||||||
}
|
}
|
||||||
@ -100,12 +89,45 @@ export function useComponentState(
|
|||||||
return {
|
return {
|
||||||
status: "no-balance-for-currency",
|
status: "no-balance-for-currency",
|
||||||
balance: undefined,
|
balance: undefined,
|
||||||
|
payStatus,
|
||||||
...baseResult,
|
...baseResult,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundAmount = Amounts.parseOrThrow(foundBalance.available);
|
const foundAmount = Amounts.parseOrThrow(foundBalance.available);
|
||||||
|
|
||||||
|
if (payResult) {
|
||||||
|
return {
|
||||||
|
status: "completed",
|
||||||
|
balance: foundAmount,
|
||||||
|
payStatus,
|
||||||
|
payHandler: {
|
||||||
|
error: payErrMsg,
|
||||||
|
},
|
||||||
|
payResult,
|
||||||
|
...baseResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
|
||||||
|
return {
|
||||||
|
status: 'no-enough-balance',
|
||||||
|
balance: foundAmount,
|
||||||
|
payStatus,
|
||||||
|
...baseResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
|
||||||
|
return {
|
||||||
|
status: "confirmed",
|
||||||
|
balance: foundAmount,
|
||||||
|
payStatus,
|
||||||
|
...baseResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function doPayment(): Promise<void> {
|
async function doPayment(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (payStatus.status !== "payment-possible") {
|
if (payStatus.status !== "payment-possible") {
|
||||||
@ -138,34 +160,19 @@ export function useComponentState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
|
|
||||||
return {
|
|
||||||
status: 'no-enough-balance',
|
|
||||||
balance: foundAmount,
|
|
||||||
...baseResult,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payHandler: ButtonHandler = {
|
const payHandler: ButtonHandler = {
|
||||||
onClick: payErrMsg ? undefined : doPayment,
|
onClick: payErrMsg ? undefined : doPayment,
|
||||||
error: payErrMsg,
|
error: payErrMsg,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!payResult) {
|
// (payStatus.status === PreparePayResultType.PaymentPossible)
|
||||||
return {
|
|
||||||
status: "ready",
|
|
||||||
payHandler,
|
|
||||||
...baseResult,
|
|
||||||
balance: foundAmount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "confirmed",
|
status: "ready",
|
||||||
balance: foundAmount,
|
payHandler,
|
||||||
payResult,
|
payStatus,
|
||||||
payHandler: {},
|
|
||||||
...baseResult,
|
...baseResult,
|
||||||
|
balance: foundAmount
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,11 +21,14 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Amounts,
|
Amounts,
|
||||||
|
ConfirmPayResultType,
|
||||||
ContractTerms,
|
ContractTerms,
|
||||||
PreparePayResultType,
|
PreparePayResultType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
import merchantIcon from "../../../static-dev/merchant-icon.jpeg";
|
||||||
import { createExample } from "../../test-utils.js";
|
import { createExample } from "../../test-utils.js";
|
||||||
import { BaseView } from "./views.js";
|
import { BaseView } from "./views.js";
|
||||||
|
import beer from "../../../static-dev/beer.png";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "cta/payment",
|
title: "cta/payment",
|
||||||
@ -34,25 +37,22 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NoBalance = createExample(BaseView, {
|
export const NoBalance = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "no-balance-for-currency",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:10"),
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
balance: undefined,
|
balance: undefined,
|
||||||
payHandler: {
|
|
||||||
onClick: async () => {
|
|
||||||
null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
|
||||||
|
|
||||||
uri: "",
|
uri: "",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
@ -62,7 +62,7 @@ export const NoBalance = createExample(BaseView, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const NoEnoughBalance = createExample(BaseView, {
|
export const NoEnoughBalance = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "no-enough-balance",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:10"),
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
balance: {
|
balance: {
|
||||||
@ -70,21 +70,18 @@ export const NoEnoughBalance = createExample(BaseView, {
|
|||||||
fraction: 40000000,
|
fraction: 40000000,
|
||||||
value: 9,
|
value: 9,
|
||||||
},
|
},
|
||||||
payHandler: {
|
|
||||||
onClick: async () => {
|
|
||||||
null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
|
||||||
|
|
||||||
uri: "",
|
uri: "",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
@ -94,7 +91,7 @@ export const NoEnoughBalance = createExample(BaseView, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const EnoughBalanceButRestricted = createExample(BaseView, {
|
export const EnoughBalanceButRestricted = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "no-enough-balance",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:10"),
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
balance: {
|
balance: {
|
||||||
@ -102,21 +99,18 @@ export const EnoughBalanceButRestricted = createExample(BaseView, {
|
|||||||
fraction: 40000000,
|
fraction: 40000000,
|
||||||
value: 19,
|
value: 19,
|
||||||
},
|
},
|
||||||
payHandler: {
|
|
||||||
onClick: async () => {
|
|
||||||
null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
|
||||||
|
|
||||||
uri: "",
|
uri: "",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
@ -139,7 +133,6 @@ export const PaymentPossible = createExample(BaseView, {
|
|||||||
null;
|
null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
|
||||||
|
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
@ -150,13 +143,19 @@ export const PaymentPossible = createExample(BaseView, {
|
|||||||
contractTerms: {
|
contractTerms: {
|
||||||
nonce: "123213123",
|
nonce: "123213123",
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
|
},
|
||||||
|
pay_deadline: {
|
||||||
|
t_s: new Date().getTime() / 1000 + 60 * 60 * 3,
|
||||||
},
|
},
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
contractTermsHash: "123456",
|
contractTermsHash: "123456",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,7 +173,6 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
|
|||||||
null;
|
null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
|
||||||
|
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
@ -185,18 +183,19 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
|
|||||||
contractTerms: {
|
contractTerms: {
|
||||||
nonce: "123213123",
|
nonce: "123213123",
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
contractTermsHash: "123456",
|
contractTermsHash: "123456",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
import beer from "../../../static-dev/beer.png";
|
|
||||||
|
|
||||||
export const TicketWithAProductList = createExample(BaseView, {
|
export const TicketWithAProductList = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
@ -211,7 +210,6 @@ export const TicketWithAProductList = createExample(BaseView, {
|
|||||||
null;
|
null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
|
||||||
|
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
@ -222,7 +220,10 @@ export const TicketWithAProductList = createExample(BaseView, {
|
|||||||
contractTerms: {
|
contractTerms: {
|
||||||
nonce: "123213123",
|
nonce: "123213123",
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
@ -247,11 +248,11 @@ export const TicketWithAProductList = createExample(BaseView, {
|
|||||||
],
|
],
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
contractTermsHash: "123456",
|
contractTermsHash: "123456",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlreadyConfirmedByOther = createExample(BaseView, {
|
export const TicketWithShipping = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:10"),
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
@ -265,7 +266,52 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
|
|||||||
null;
|
null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
|
||||||
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
|
payStatus: {
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
amountEffective: "USD:10.20",
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
noncePriv: "",
|
||||||
|
contractTerms: {
|
||||||
|
nonce: "123213123",
|
||||||
|
merchant: {
|
||||||
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
|
},
|
||||||
|
amount: "USD:10",
|
||||||
|
summary: "banana pi set",
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
description: "banana pi",
|
||||||
|
price: "USD:2",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
delivery_date: {
|
||||||
|
t_s: new Date().getTime() / 1000 + 30 * 24 * 60 * 60,
|
||||||
|
},
|
||||||
|
delivery_location: {
|
||||||
|
town: "Liverpool",
|
||||||
|
street: "Down st 1234",
|
||||||
|
},
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AlreadyConfirmedByOther = createExample(BaseView, {
|
||||||
|
status: "confirmed",
|
||||||
|
error: undefined,
|
||||||
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
|
balance: {
|
||||||
|
currency: "USD",
|
||||||
|
fraction: 40000000,
|
||||||
|
value: 11,
|
||||||
|
},
|
||||||
|
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
@ -274,19 +320,22 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
|
|||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
contractTermsHash: "123456",
|
contractTermsHash: "123456",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
paid: false,
|
paid: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
|
export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "completed",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:10"),
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
balance: {
|
balance: {
|
||||||
@ -294,33 +343,34 @@ export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
|
|||||||
fraction: 40000000,
|
fraction: 40000000,
|
||||||
value: 11,
|
value: 11,
|
||||||
},
|
},
|
||||||
payHandler: {
|
|
||||||
onClick: async () => {
|
|
||||||
null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
|
||||||
|
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
|
payResult: {
|
||||||
|
type: ConfirmPayResultType.Done,
|
||||||
|
contractTerms: {} as any,
|
||||||
|
},
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
amountEffective: "USD:10",
|
amountEffective: "USD:10",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
contractTermsHash: "123456",
|
contractTermsHash: "123456",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
paid: true,
|
paid: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlreadyPaidWithFulfillment = createExample(BaseView, {
|
export const AlreadyPaidWithFulfillment = createExample(BaseView, {
|
||||||
status: "ready",
|
status: "completed",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:10"),
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
balance: {
|
balance: {
|
||||||
@ -328,29 +378,34 @@ export const AlreadyPaidWithFulfillment = createExample(BaseView, {
|
|||||||
fraction: 40000000,
|
fraction: 40000000,
|
||||||
value: 11,
|
value: 11,
|
||||||
},
|
},
|
||||||
payHandler: {
|
|
||||||
onClick: async () => {
|
|
||||||
null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
|
||||||
|
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
|
payResult: {
|
||||||
|
type: ConfirmPayResultType.Done,
|
||||||
|
contractTerms: {
|
||||||
|
fulfillment_message: "thanks for buying!",
|
||||||
|
fulfillment_url: "https://demo.taler.net",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
},
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
amountEffective: "USD:10",
|
amountEffective: "USD:10",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
merchant: {
|
merchant: {
|
||||||
name: "someone",
|
name: "the merchant",
|
||||||
|
logo: merchantIcon,
|
||||||
|
website: "https://www.themerchant.taler",
|
||||||
|
email: "contact@merchant.taler",
|
||||||
},
|
},
|
||||||
|
fulfillment_url: "https://demo.taler.net",
|
||||||
fulfillment_message:
|
fulfillment_message:
|
||||||
"congratulations! you are looking at the fulfillment message! ",
|
"congratulations! you are looking at the fulfillment message! ",
|
||||||
summary: "some beers",
|
summary: "some beers",
|
||||||
amount: "USD:10",
|
amount: "USD:10",
|
||||||
} as Partial<ContractTerms> as any,
|
} as Partial<ContractTerms> as any,
|
||||||
contractTermsHash: "123456",
|
contractTermsHash: "123456",
|
||||||
proposalId: "proposal1234",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
paid: true,
|
paid: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -204,7 +204,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:0"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:0"));
|
||||||
expect(r.payHandler.onClick).not.undefined;
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +246,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
expect(r.payHandler.onClick).not.undefined;
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +293,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
if (r.payHandler.onClick === undefined) expect.fail();
|
if (r.payHandler.onClick === undefined) expect.fail();
|
||||||
r.payHandler.onClick();
|
r.payHandler.onClick();
|
||||||
}
|
}
|
||||||
@ -302,13 +302,13 @@ describe("Payment CTA states", () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const r = getLastResultOrThrow();
|
const r = getLastResultOrThrow();
|
||||||
if (r.status !== "confirmed") expect.fail();
|
if (r.status !== "completed") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
|
// if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
|
||||||
expect(r.payResult.contractTerms).not.undefined;
|
// expect(r.payResult.contractTerms).not.undefined;
|
||||||
expect(r.payHandler.onClick).undefined;
|
// expect(r.payHandler.onClick).undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertNoPendingUpdate();
|
await assertNoPendingUpdate();
|
||||||
@ -354,7 +354,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
if (r.payHandler.onClick === undefined) expect.fail();
|
if (r.payHandler.onClick === undefined) expect.fail();
|
||||||
r.payHandler.onClick();
|
r.payHandler.onClick();
|
||||||
}
|
}
|
||||||
@ -366,7 +366,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
expect(r.payHandler.onClick).undefined;
|
expect(r.payHandler.onClick).undefined;
|
||||||
if (r.payHandler.error === undefined) expect.fail();
|
if (r.payHandler.error === undefined) expect.fail();
|
||||||
//FIXME: error message here is bad
|
//FIXME: error message here is bad
|
||||||
@ -425,7 +425,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
expect(r.payHandler.onClick).not.undefined;
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
|
|
||||||
notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5"));
|
notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5"));
|
||||||
@ -438,7 +438,7 @@ describe("Payment CTA states", () => {
|
|||||||
if (r.status !== "ready") expect.fail();
|
if (r.status !== "ready") expect.fail();
|
||||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||||
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||||
expect(r.payHandler.onClick).not.undefined;
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
Amounts,
|
Amounts,
|
||||||
ConfirmPayResultType,
|
ConfirmPayResultType,
|
||||||
ContractTerms,
|
ContractTerms,
|
||||||
@ -38,8 +39,10 @@ import {
|
|||||||
WalletAction,
|
WalletAction,
|
||||||
WarningBox,
|
WarningBox,
|
||||||
} from "../../components/styled/index.js";
|
} from "../../components/styled/index.js";
|
||||||
|
import { Time } from "../../components/Time.js";
|
||||||
import { useTranslationContext } from "../../context/translation.js";
|
import { useTranslationContext } from "../../context/translation.js";
|
||||||
import { Button } from "../../mui/Button.js";
|
import { Button } from "../../mui/Button.js";
|
||||||
|
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
|
||||||
import { State } from "./index.js";
|
import { State } from "./index.js";
|
||||||
|
|
||||||
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||||
@ -56,6 +59,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
|||||||
type SupportedStates =
|
type SupportedStates =
|
||||||
| State.Ready
|
| State.Ready
|
||||||
| State.Confirmed
|
| State.Confirmed
|
||||||
|
| State.Completed
|
||||||
| State.NoBalanceForCurrency
|
| State.NoBalanceForCurrency
|
||||||
| State.NoEnoughBalance;
|
| State.NoEnoughBalance;
|
||||||
|
|
||||||
@ -63,6 +67,15 @@ export function BaseView(state: SupportedStates): VNode {
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const contractTerms: ContractTerms = state.payStatus.contractTerms;
|
const contractTerms: ContractTerms = state.payStatus.contractTerms;
|
||||||
|
|
||||||
|
const price = {
|
||||||
|
raw: state.amount,
|
||||||
|
effective:
|
||||||
|
"amountEffective" in state.payStatus
|
||||||
|
? Amounts.parseOrThrow(state.payStatus.amountEffective)
|
||||||
|
: state.amount,
|
||||||
|
};
|
||||||
|
const totalFees = Amounts.sub(price.effective, price.raw).amount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletAction>
|
<WalletAction>
|
||||||
<LogoHeader />
|
<LogoHeader />
|
||||||
@ -73,9 +86,9 @@ export function BaseView(state: SupportedStates): VNode {
|
|||||||
|
|
||||||
<ShowImportantMessage state={state} />
|
<ShowImportantMessage state={state} />
|
||||||
|
|
||||||
<section>
|
<section style={{ textAlign: "left" }}>
|
||||||
{state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
|
{/* {state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
|
||||||
Amounts.isNonZero(state.totalFees) && (
|
Amounts.isNonZero(totalFees) && (
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Total to pay</i18n.Translate>}
|
title={<i18n.Translate>Total to pay</i18n.Translate>}
|
||||||
@ -89,26 +102,45 @@ export function BaseView(state: SupportedStates): VNode {
|
|||||||
text={<Amount value={state.payStatus.amountRaw} />}
|
text={<Amount value={state.payStatus.amountRaw} />}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
{Amounts.isNonZero(state.totalFees) && (
|
{Amounts.isNonZero(totalFees) && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||||
text={<Amount value={state.totalFees} />}
|
text={<Amount value={totalFees} />}
|
||||||
kind="negative"
|
kind="negative"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)} */}
|
||||||
<Part
|
|
||||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
|
||||||
text={contractTerms.merchant.name}
|
|
||||||
kind="neutral"
|
|
||||||
/>
|
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Purchase</i18n.Translate>}
|
title={<i18n.Translate>Purchase</i18n.Translate>}
|
||||||
text={contractTerms.summary}
|
text={contractTerms.summary}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
|
<Part
|
||||||
|
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||||
|
text={<MerchantDetails merchant={contractTerms.merchant} />}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
|
{/* <pre>{JSON.stringify(price)}</pre>
|
||||||
|
<hr />
|
||||||
|
<pre>{JSON.stringify(state.payStatus, undefined, 2)}</pre> */}
|
||||||
|
<Part
|
||||||
|
title={<i18n.Translate>Details</i18n.Translate>}
|
||||||
|
text={
|
||||||
|
<PurchaseDetails
|
||||||
|
price={price}
|
||||||
|
info={{
|
||||||
|
...contractTerms,
|
||||||
|
orderId: contractTerms.order_id,
|
||||||
|
contractTermsHash: "",
|
||||||
|
products: contractTerms.products!,
|
||||||
|
}}
|
||||||
|
proposalId={state.payStatus.proposalId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
{contractTerms.order_id && (
|
{contractTerms.order_id && (
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Receipt</i18n.Translate>}
|
title={<i18n.Translate>Receipt</i18n.Translate>}
|
||||||
@ -116,8 +148,19 @@ export function BaseView(state: SupportedStates): VNode {
|
|||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{contractTerms.products && contractTerms.products.length > 0 && (
|
{contractTerms.pay_deadline && (
|
||||||
<ProductList products={contractTerms.products} />
|
<Part
|
||||||
|
title={<i18n.Translate>Valid until</i18n.Translate>}
|
||||||
|
text={
|
||||||
|
<Time
|
||||||
|
timestamp={AbsoluteTime.fromTimestamp(
|
||||||
|
contractTerms.pay_deadline,
|
||||||
|
)}
|
||||||
|
format="dd MMMM yyyy, HH:mm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<ButtonsSection
|
<ButtonsSection
|
||||||
@ -232,7 +275,7 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == "confirmed") {
|
if (state.status == "completed") {
|
||||||
const { payResult, payHandler } = state;
|
const { payResult, payHandler } = state;
|
||||||
if (payHandler.error) {
|
if (payHandler.error) {
|
||||||
return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
|
return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
|
||||||
@ -264,7 +307,7 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
|
|||||||
return <Fragment />;
|
return <Fragment />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PayWithMobile({ state }: { state: State.Ready }): VNode {
|
function PayWithMobile({ state }: { state: SupportedStates }): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const [showQR, setShowQR] = useState<boolean>(false);
|
const [showQR, setShowQR] = useState<boolean>(false);
|
||||||
@ -286,7 +329,7 @@ function PayWithMobile({ state }: { state: State.Ready }): VNode {
|
|||||||
<div>
|
<div>
|
||||||
<QR text={privateUri} />
|
<QR text={privateUri} />
|
||||||
<i18n.Translate>
|
<i18n.Translate>
|
||||||
Scan the QR code or
|
Scan the QR code or
|
||||||
<a href={privateUri}>
|
<a href={privateUri}>
|
||||||
<i18n.Translate>click here</i18n.Translate>
|
<i18n.Translate>click here</i18n.Translate>
|
||||||
</a>
|
</a>
|
||||||
@ -306,61 +349,66 @@ function ButtonsSection({
|
|||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
if (state.status === "ready") {
|
if (state.status === "ready") {
|
||||||
const { payStatus } = state;
|
return (
|
||||||
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
<Fragment>
|
||||||
return (
|
<section>
|
||||||
<Fragment>
|
<Button
|
||||||
<section>
|
variant="contained"
|
||||||
<Button
|
color="success"
|
||||||
variant="contained"
|
onClick={state.payHandler.onClick}
|
||||||
color="success"
|
>
|
||||||
onClick={state.payHandler.onClick}
|
<i18n.Translate>
|
||||||
>
|
Pay
|
||||||
<i18n.Translate>
|
{<Amount value={state.payStatus.amountEffective} />}
|
||||||
Pay {<Amount value={payStatus.amountEffective} />}
|
</i18n.Translate>
|
||||||
</i18n.Translate>
|
</Button>
|
||||||
</Button>
|
</section>
|
||||||
</section>
|
<PayWithMobile state={state} />
|
||||||
<PayWithMobile state={state} />
|
</Fragment>
|
||||||
</Fragment>
|
);
|
||||||
);
|
}
|
||||||
}
|
if (
|
||||||
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
|
state.status === "no-enough-balance" ||
|
||||||
let BalanceMessage = "";
|
state.status === "no-balance-for-currency"
|
||||||
if (!state.balance) {
|
) {
|
||||||
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
|
// if (state.payStatus.status === PreparePayResultType.InsufficientBalance) {
|
||||||
|
let BalanceMessage = "";
|
||||||
|
if (!state.balance) {
|
||||||
|
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
|
||||||
|
} else {
|
||||||
|
const balanceShouldBeEnough =
|
||||||
|
Amounts.cmp(state.balance, state.amount) !== -1;
|
||||||
|
if (balanceShouldBeEnough) {
|
||||||
|
BalanceMessage = i18n.str`Could not find enough coins to pay this order. Even if you have enough ${state.balance.currency} some restriction may apply.`;
|
||||||
} else {
|
} else {
|
||||||
const balanceShouldBeEnough =
|
BalanceMessage = i18n.str`Your current balance is not enough for this order.`;
|
||||||
Amounts.cmp(state.balance, state.amount) !== -1;
|
|
||||||
if (balanceShouldBeEnough) {
|
|
||||||
BalanceMessage = i18n.str`Could not find enough coins to pay this order. Even if you have enough ${state.balance.currency} some restriction may apply.`;
|
|
||||||
} else {
|
|
||||||
BalanceMessage = i18n.str`Your current balance is not enough for this order.`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<section>
|
|
||||||
<WarningBox>{BalanceMessage}</WarningBox>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="success"
|
|
||||||
onClick={() => goToWalletManualWithdraw(state.amount.currency)}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Withdraw digital cash</i18n.Translate>
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
<PayWithMobile state={state} />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section>
|
||||||
|
<WarningBox>{BalanceMessage}</WarningBox>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
onClick={() => goToWalletManualWithdraw(state.amount.currency)}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Withdraw digital cash</i18n.Translate>
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
<PayWithMobile state={state} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
if (state.status === "confirmed") {
|
||||||
|
if (state.payStatus.status === PreparePayResultType.AlreadyConfirmed) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<section>
|
<section>
|
||||||
{payStatus.paid &&
|
{state.payStatus.paid &&
|
||||||
state.payStatus.contractTerms.fulfillment_message && (
|
state.payStatus.contractTerms.fulfillment_message && (
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Merchant message</i18n.Translate>}
|
title={<i18n.Translate>Merchant message</i18n.Translate>}
|
||||||
@ -369,13 +417,13 @@ function ButtonsSection({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{!payStatus.paid && <PayWithMobile state={state} />}
|
{!state.payStatus.paid && <PayWithMobile state={state} />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status === "confirmed") {
|
if (state.status === "completed") {
|
||||||
if (state.payResult.type === ConfirmPayResultType.Pending) {
|
if (state.payResult.type === ConfirmPayResultType.Pending) {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
@ -30,6 +30,7 @@ export default {
|
|||||||
|
|
||||||
export const EmptyBalance = createExample(TestedComponent, {
|
export const EmptyBalance = createExample(TestedComponent, {
|
||||||
balances: [],
|
balances: [],
|
||||||
|
goToWalletManualWithdraw: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SomeCoins = createExample(TestedComponent, {
|
export const SomeCoins = createExample(TestedComponent, {
|
||||||
@ -42,6 +43,8 @@ export const SomeCoins = createExample(TestedComponent, {
|
|||||||
requiresUserInput: false,
|
requiresUserInput: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
addAction: {},
|
||||||
|
goToWalletManualWithdraw: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
|
export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||||
@ -68,6 +71,8 @@ export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
|
|||||||
requiresUserInput: false,
|
requiresUserInput: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
goToWalletManualWithdraw: {},
|
||||||
|
addAction: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
|
export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||||
@ -94,6 +99,8 @@ export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
|
|||||||
requiresUserInput: false,
|
requiresUserInput: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
goToWalletManualWithdraw: {},
|
||||||
|
addAction: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
|
export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
|
||||||
@ -148,4 +155,6 @@ export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
|
|||||||
requiresUserInput: false,
|
requiresUserInput: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
goToWalletManualWithdraw: {},
|
||||||
|
addAction: {},
|
||||||
});
|
});
|
||||||
|
@ -23,8 +23,10 @@ import { Loading } from "../components/Loading.js";
|
|||||||
import { LoadingError } from "../components/LoadingError.js";
|
import { LoadingError } from "../components/LoadingError.js";
|
||||||
import { MultiActionButton } from "../components/MultiActionButton.js";
|
import { MultiActionButton } from "../components/MultiActionButton.js";
|
||||||
import { useTranslationContext } from "../context/translation.js";
|
import { useTranslationContext } from "../context/translation.js";
|
||||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||||
import { Button } from "../mui/Button.js";
|
import { Button } from "../mui/Button.js";
|
||||||
|
import { ButtonHandler } from "../mui/handlers.js";
|
||||||
|
import { compose, StateViewMap } from "../utils/index.js";
|
||||||
import { AddNewActionView } from "../wallet/AddNewActionView.js";
|
import { AddNewActionView } from "../wallet/AddNewActionView.js";
|
||||||
import * as wxApi from "../wxApi.js";
|
import * as wxApi from "../wxApi.js";
|
||||||
import { NoBalanceHelp } from "./NoBalanceHelp.js";
|
import { NoBalanceHelp } from "./NoBalanceHelp.js";
|
||||||
@ -34,17 +36,46 @@ export interface Props {
|
|||||||
goToWalletHistory: (currency: string) => Promise<void>;
|
goToWalletHistory: (currency: string) => Promise<void>;
|
||||||
goToWalletManualWithdraw: () => Promise<void>;
|
goToWalletManualWithdraw: () => Promise<void>;
|
||||||
}
|
}
|
||||||
export function BalancePage({
|
|
||||||
goToWalletManualWithdraw,
|
export type State = State.Loading | State.Error | State.Action | State.Balances;
|
||||||
goToWalletDeposit,
|
|
||||||
goToWalletHistory,
|
export namespace State {
|
||||||
}: Props): VNode {
|
export interface Loading {
|
||||||
const { i18n } = useTranslationContext();
|
status: "loading";
|
||||||
|
error: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Error {
|
||||||
|
status: "error";
|
||||||
|
error: HookError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
status: "action";
|
||||||
|
error: undefined;
|
||||||
|
cancel: ButtonHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Balances {
|
||||||
|
status: "balance";
|
||||||
|
error: undefined;
|
||||||
|
balances: Balance[];
|
||||||
|
addAction: ButtonHandler;
|
||||||
|
goToWalletDeposit: (currency: string) => Promise<void>;
|
||||||
|
goToWalletHistory: (currency: string) => Promise<void>;
|
||||||
|
goToWalletManualWithdraw: ButtonHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useComponentState(
|
||||||
|
{ goToWalletDeposit, goToWalletHistory, goToWalletManualWithdraw }: Props,
|
||||||
|
api: typeof wxApi,
|
||||||
|
): State {
|
||||||
const [addingAction, setAddingAction] = useState(false);
|
const [addingAction, setAddingAction] = useState(false);
|
||||||
const state = useAsyncAsHook(wxApi.getBalance);
|
const state = useAsyncAsHook(api.getBalance);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return wxApi.onUpdateNotification(
|
return api.onUpdateNotification(
|
||||||
[NotificationType.WithdrawGroupFinished],
|
[NotificationType.WithdrawGroupFinished],
|
||||||
() => {
|
() => {
|
||||||
state?.retry();
|
state?.retry();
|
||||||
@ -52,58 +83,80 @@ export function BalancePage({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const balances = !state || state.hasError ? [] : state.response.balances;
|
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return <Loading />;
|
return {
|
||||||
|
status: "loading",
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.hasError) {
|
if (state.hasError) {
|
||||||
return (
|
return {
|
||||||
<LoadingError
|
status: "error",
|
||||||
title={<i18n.Translate>Could not load balance page</i18n.Translate>}
|
error: state,
|
||||||
error={state}
|
};
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addingAction) {
|
if (addingAction) {
|
||||||
return <AddNewActionView onCancel={async () => setAddingAction(false)} />;
|
return {
|
||||||
|
status: "action",
|
||||||
|
error: undefined,
|
||||||
|
cancel: {
|
||||||
|
onClick: async () => setAddingAction(false),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
status: "balance",
|
||||||
|
error: undefined,
|
||||||
|
balances: state.response.balances,
|
||||||
|
addAction: {
|
||||||
|
onClick: async () => setAddingAction(true),
|
||||||
|
},
|
||||||
|
goToWalletManualWithdraw: {
|
||||||
|
onClick: goToWalletManualWithdraw,
|
||||||
|
},
|
||||||
|
goToWalletDeposit,
|
||||||
|
goToWalletHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewMapping: StateViewMap<State> = {
|
||||||
|
loading: Loading,
|
||||||
|
error: ErrorView,
|
||||||
|
action: ActionView,
|
||||||
|
balance: BalanceView,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BalancePage = compose(
|
||||||
|
"BalancePage",
|
||||||
|
(p: Props) => useComponentState(p, wxApi),
|
||||||
|
viewMapping,
|
||||||
|
);
|
||||||
|
|
||||||
|
function ErrorView({ error }: State.Error): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
return (
|
return (
|
||||||
<BalanceView
|
<LoadingError
|
||||||
balances={balances}
|
title={<i18n.Translate>Could not load balance page</i18n.Translate>}
|
||||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
error={error}
|
||||||
goToWalletDeposit={goToWalletDeposit}
|
|
||||||
goToWalletHistory={goToWalletHistory}
|
|
||||||
goToAddAction={async () => setAddingAction(true)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export interface BalanceViewProps {
|
|
||||||
balances: Balance[];
|
function ActionView({ cancel }: State.Action): VNode {
|
||||||
goToWalletManualWithdraw: () => Promise<void>;
|
return <AddNewActionView onCancel={cancel.onClick!} />;
|
||||||
goToAddAction: () => Promise<void>;
|
|
||||||
goToWalletDeposit: (currency: string) => Promise<void>;
|
|
||||||
goToWalletHistory: (currency: string) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BalanceView({
|
export function BalanceView(state: State.Balances): VNode {
|
||||||
balances,
|
|
||||||
goToWalletManualWithdraw,
|
|
||||||
goToWalletDeposit,
|
|
||||||
goToWalletHistory,
|
|
||||||
goToAddAction,
|
|
||||||
}: BalanceViewProps): VNode {
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const currencyWithNonZeroAmount = balances
|
const currencyWithNonZeroAmount = state.balances
|
||||||
.filter((b) => !Amounts.isZero(b.available))
|
.filter((b) => !Amounts.isZero(b.available))
|
||||||
.map((b) => b.available.split(":")[0]);
|
.map((b) => b.available.split(":")[0]);
|
||||||
|
|
||||||
if (balances.length === 0) {
|
if (state.balances.length === 0) {
|
||||||
return (
|
return (
|
||||||
<NoBalanceHelp goToWalletManualWithdraw={goToWalletManualWithdraw} />
|
<NoBalanceHelp
|
||||||
|
goToWalletManualWithdraw={state.goToWalletManualWithdraw}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,23 +164,26 @@ export function BalanceView({
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<section>
|
<section>
|
||||||
<BalanceTable
|
<BalanceTable
|
||||||
balances={balances}
|
balances={state.balances}
|
||||||
goToWalletHistory={goToWalletHistory}
|
goToWalletHistory={state.goToWalletHistory}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<footer style={{ justifyContent: "space-between" }}>
|
<footer style={{ justifyContent: "space-between" }}>
|
||||||
<Button variant="contained" onClick={goToWalletManualWithdraw}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={state.goToWalletManualWithdraw.onClick}
|
||||||
|
>
|
||||||
<i18n.Translate>Withdraw</i18n.Translate>
|
<i18n.Translate>Withdraw</i18n.Translate>
|
||||||
</Button>
|
</Button>
|
||||||
{currencyWithNonZeroAmount.length > 0 && (
|
{currencyWithNonZeroAmount.length > 0 && (
|
||||||
<MultiActionButton
|
<MultiActionButton
|
||||||
label={(s) => <i18n.Translate>Deposit {s}</i18n.Translate>}
|
label={(s) => <i18n.Translate>Deposit {s}</i18n.Translate>}
|
||||||
actions={currencyWithNonZeroAmount}
|
actions={currencyWithNonZeroAmount}
|
||||||
onClick={(c) => goToWalletDeposit(c)}
|
onClick={(c) => state.goToWalletDeposit(c)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<JustInDevMode>
|
<JustInDevMode>
|
||||||
<Button onClick={goToAddAction}>
|
<Button onClick={state.addAction.onClick}>
|
||||||
<i18n.Translate>Enter URI</i18n.Translate>
|
<i18n.Translate>Enter URI</i18n.Translate>
|
||||||
</Button>
|
</Button>
|
||||||
</JustInDevMode>
|
</JustInDevMode>
|
||||||
|
@ -17,13 +17,14 @@ import { css } from "@linaria/core";
|
|||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { Alert } from "../mui/Alert.js";
|
import { Alert } from "../mui/Alert.js";
|
||||||
import { Button } from "../mui/Button.js";
|
import { Button } from "../mui/Button.js";
|
||||||
|
import { ButtonHandler } from "../mui/handlers.js";
|
||||||
import { Paper } from "../mui/Paper.js";
|
import { Paper } from "../mui/Paper.js";
|
||||||
import { Typography } from "../mui/Typography.js";
|
import { Typography } from "../mui/Typography.js";
|
||||||
|
|
||||||
export function NoBalanceHelp({
|
export function NoBalanceHelp({
|
||||||
goToWalletManualWithdraw,
|
goToWalletManualWithdraw,
|
||||||
}: {
|
}: {
|
||||||
goToWalletManualWithdraw: () => Promise<void>;
|
goToWalletManualWithdraw: ButtonHandler;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@ -37,7 +38,7 @@ export function NoBalanceHelp({
|
|||||||
fullWidth
|
fullWidth
|
||||||
color="warning"
|
color="warning"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={goToWalletManualWithdraw}
|
onClick={goToWalletManualWithdraw.onClick}
|
||||||
>
|
>
|
||||||
<Typography>Withdraw</Typography>
|
<Typography>Withdraw</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -143,7 +143,11 @@ export function HistoryView({
|
|||||||
|
|
||||||
if (balances.length === 0 || !selectedCurrency) {
|
if (balances.length === 0 || !selectedCurrency) {
|
||||||
return (
|
return (
|
||||||
<NoBalanceHelp goToWalletManualWithdraw={goToWalletManualWithdraw} />
|
<NoBalanceHelp
|
||||||
|
goToWalletManualWithdraw={{
|
||||||
|
onClick: goToWalletManualWithdraw,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -114,6 +114,12 @@ const exampleData = {
|
|||||||
tip: {
|
tip: {
|
||||||
...commonTransaction,
|
...commonTransaction,
|
||||||
type: TransactionType.Tip,
|
type: TransactionType.Tip,
|
||||||
|
// merchant: {
|
||||||
|
// name: "the merchant",
|
||||||
|
// logo: merchantIcon,
|
||||||
|
// website: "https://www.themerchant.taler",
|
||||||
|
// email: "contact@merchant.taler",
|
||||||
|
// },
|
||||||
merchantBaseUrl: "http://merchant.taler",
|
merchantBaseUrl: "http://merchant.taler",
|
||||||
} as TransactionTip,
|
} as TransactionTip,
|
||||||
refund: {
|
refund: {
|
||||||
|
@ -16,18 +16,18 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
amountFractionalLength,
|
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
Location,
|
Location,
|
||||||
|
MerchantInfo,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
|
OrderShortInfo,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
PaytoUri,
|
PaytoUri,
|
||||||
stringifyPaytoUri,
|
stringifyPaytoUri,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
Transaction,
|
Transaction,
|
||||||
TransactionDeposit,
|
TransactionDeposit,
|
||||||
TransactionPayment,
|
|
||||||
TransactionRefresh,
|
TransactionRefresh,
|
||||||
TransactionRefund,
|
TransactionRefund,
|
||||||
TransactionTip,
|
TransactionTip,
|
||||||
@ -46,6 +46,7 @@ import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
|
|||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { LoadingError } from "../components/LoadingError.js";
|
import { LoadingError } from "../components/LoadingError.js";
|
||||||
import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
|
import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
|
||||||
|
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
|
||||||
import {
|
import {
|
||||||
CenteredDialog,
|
CenteredDialog,
|
||||||
InfoBox,
|
InfoBox,
|
||||||
@ -319,10 +320,15 @@ export function TransactionView({
|
|||||||
? undefined
|
? undefined
|
||||||
: Amounts.parseOrThrow(transaction.refundPending);
|
: Amounts.parseOrThrow(transaction.refundPending);
|
||||||
|
|
||||||
const total = Amounts.sub(
|
const price = {
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
raw: Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
effective: Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
).amount;
|
};
|
||||||
|
const refund = {
|
||||||
|
raw: Amounts.parseOrThrow(transaction.totalRefundRaw),
|
||||||
|
effective: Amounts.parseOrThrow(transaction.totalRefundEffective),
|
||||||
|
};
|
||||||
|
const total = Amounts.sub(price.effective, refund.effective).amount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransactionTemplate>
|
<TransactionTemplate>
|
||||||
@ -404,45 +410,7 @@ export function TransactionView({
|
|||||||
)}
|
)}
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||||
text={
|
text={<MerchantDetails merchant={transaction.info.merchant} />}
|
||||||
<Fragment>
|
|
||||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
|
||||||
{transaction.info.merchant.logo && (
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
src={transaction.info.merchant.logo}
|
|
||||||
style={{ width: 64, height: 64, margin: 4 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<p>{transaction.info.merchant.name}</p>
|
|
||||||
{transaction.info.merchant.website && (
|
|
||||||
<a
|
|
||||||
href={transaction.info.merchant.website}
|
|
||||||
target="_blank"
|
|
||||||
style={{ textDecorationColor: "gray" }}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<SmallLightText>
|
|
||||||
{transaction.info.merchant.website}
|
|
||||||
</SmallLightText>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{transaction.info.merchant.email && (
|
|
||||||
<a
|
|
||||||
href={`mailto:${transaction.info.merchant.email}`}
|
|
||||||
style={{ textDecorationColor: "gray" }}
|
|
||||||
>
|
|
||||||
<SmallLightText>
|
|
||||||
{transaction.info.merchant.email}
|
|
||||||
</SmallLightText>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
<Part
|
<Part
|
||||||
@ -452,7 +420,14 @@ export function TransactionView({
|
|||||||
/>
|
/>
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Details</i18n.Translate>}
|
title={<i18n.Translate>Details</i18n.Translate>}
|
||||||
text={<PurchaseDetails transaction={transaction} />}
|
text={
|
||||||
|
<PurchaseDetails
|
||||||
|
price={price}
|
||||||
|
refund={refund}
|
||||||
|
info={transaction.info}
|
||||||
|
proposalId={transaction.proposalId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
</TransactionTemplate>
|
</TransactionTemplate>
|
||||||
@ -521,12 +496,7 @@ export function TransactionView({
|
|||||||
</Header>
|
</Header>
|
||||||
{/* <Part
|
{/* <Part
|
||||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||||
text={transaction.info.merchant.name}
|
text={<MerchantDetails merchant={transaction.merchant} />}
|
||||||
kind="neutral"
|
|
||||||
/>
|
|
||||||
<Part
|
|
||||||
title={<i18n.Translate>Invoice ID</i18n.Translate>}
|
|
||||||
text={transaction.info.orderId}
|
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/> */}
|
/> */}
|
||||||
<Part
|
<Part
|
||||||
@ -584,6 +554,46 @@ export function TransactionView({
|
|||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MerchantDetails({
|
||||||
|
merchant,
|
||||||
|
}: {
|
||||||
|
merchant: MerchantInfo;
|
||||||
|
}): VNode {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||||
|
{merchant.logo && (
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={merchant.logo}
|
||||||
|
style={{ width: 64, height: 64, margin: 4 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p style={{ marginTop: 0 }}>{merchant.name}</p>
|
||||||
|
{merchant.website && (
|
||||||
|
<a
|
||||||
|
href={merchant.website}
|
||||||
|
target="_blank"
|
||||||
|
style={{ textDecorationColor: "gray" }}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<SmallLightText>{merchant.website}</SmallLightText>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{merchant.email && (
|
||||||
|
<a
|
||||||
|
href={`mailto:${merchant.email}`}
|
||||||
|
style={{ textDecorationColor: "gray" }}
|
||||||
|
>
|
||||||
|
<SmallLightText>{merchant.email}</SmallLightText>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DeliveryDetails({
|
function DeliveryDetails({
|
||||||
date,
|
date,
|
||||||
location,
|
location,
|
||||||
@ -703,57 +713,58 @@ function DeliveryDetails({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PurchaseDetails({
|
export interface AmountWithFee {
|
||||||
transaction,
|
effective: AmountJson;
|
||||||
|
raw: AmountJson;
|
||||||
|
}
|
||||||
|
export function PurchaseDetails({
|
||||||
|
price,
|
||||||
|
refund,
|
||||||
|
info,
|
||||||
|
proposalId,
|
||||||
}: {
|
}: {
|
||||||
transaction: TransactionPayment;
|
price: AmountWithFee;
|
||||||
|
refund?: AmountWithFee;
|
||||||
|
info: OrderShortInfo;
|
||||||
|
proposalId: string;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const partialFee = Amounts.sub(
|
const partialFee = Amounts.sub(price.effective, price.raw).amount;
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
).amount;
|
|
||||||
|
|
||||||
const refundRaw = Amounts.parseOrThrow(transaction.totalRefundRaw);
|
const refundFee = !refund
|
||||||
|
? Amounts.getZero(price.effective.currency)
|
||||||
const refundFee = Amounts.sub(
|
: Amounts.sub(refund.raw, refund.effective).amount;
|
||||||
refundRaw,
|
|
||||||
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
|
||||||
).amount;
|
|
||||||
|
|
||||||
const fee = Amounts.sum([partialFee, refundFee]).amount;
|
const fee = Amounts.sum([partialFee, refundFee]).amount;
|
||||||
|
|
||||||
const hasProducts =
|
const hasProducts = info.products && info.products.length > 0;
|
||||||
transaction.info.products && transaction.info.products.length > 0;
|
|
||||||
|
|
||||||
const hasShipping =
|
const hasShipping =
|
||||||
transaction.info.delivery_date !== undefined ||
|
info.delivery_date !== undefined || info.delivery_location !== undefined;
|
||||||
transaction.info.delivery_location !== undefined;
|
|
||||||
|
|
||||||
const showLargePic = (): void => {
|
const showLargePic = (): void => {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const total = Amounts.sub(
|
const total = !refund
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
? price.effective
|
||||||
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
: Amounts.sub(price.effective, refund.effective).amount;
|
||||||
).amount;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PurchaseDetailsTable>
|
<PurchaseDetailsTable>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Price</td>
|
<td>Price</td>
|
||||||
<td>
|
<td>
|
||||||
<Amount value={transaction.amountRaw} />
|
<Amount value={price.raw} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{Amounts.isNonZero(refundRaw) && (
|
{refund && Amounts.isNonZero(refund.raw) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Refunded</td>
|
<td>Refunded</td>
|
||||||
<td>
|
<td>
|
||||||
<Amount value={transaction.totalRefundRaw} negative />
|
<Amount value={refund.raw} negative />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
@ -784,7 +795,7 @@ function PurchaseDetails({
|
|||||||
title={<i18n.Translate>Products</i18n.Translate>}
|
title={<i18n.Translate>Products</i18n.Translate>}
|
||||||
text={
|
text={
|
||||||
<ListOfProducts>
|
<ListOfProducts>
|
||||||
{transaction.info.products?.map((p, k) => (
|
{info.products?.map((p, k) => (
|
||||||
<Row key={k}>
|
<Row key={k}>
|
||||||
<a href="#" onClick={showLargePic}>
|
<a href="#" onClick={showLargePic}>
|
||||||
<img src={p.image ? p.image : emptyImg} />
|
<img src={p.image ? p.image : emptyImg} />
|
||||||
@ -813,14 +824,19 @@ function PurchaseDetails({
|
|||||||
title={<i18n.Translate>Delivery</i18n.Translate>}
|
title={<i18n.Translate>Delivery</i18n.Translate>}
|
||||||
text={
|
text={
|
||||||
<DeliveryDetails
|
<DeliveryDetails
|
||||||
date={transaction.info.delivery_date}
|
date={info.delivery_date}
|
||||||
location={transaction.info.delivery_location}
|
location={info.delivery_location}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<ShowFullContractTermPopup proposalId={proposalId} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</PurchaseDetailsTable>
|
</PurchaseDetailsTable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ import {
|
|||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
RemoveBackupProviderRequest,
|
RemoveBackupProviderRequest,
|
||||||
TalerError,
|
TalerError,
|
||||||
|
WalletContractData,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
||||||
@ -190,6 +191,11 @@ export function getBalance(): Promise<BalancesResponse> {
|
|||||||
return callBackend("getBalances", {});
|
return callBackend("getBalances", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getContractTermsDetails(proposalId: string): Promise<WalletContractData> {
|
||||||
|
return callBackend("getContractTermsDetails", { proposalId });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the full event history for this wallet.
|
* Retrieve the full event history for this wallet.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user