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)
|
||||
*/
|
||||
|
||||
import * as a1 from "./Banner.stories.js";
|
||||
import * as a1 from "./Banner.stories.js";
|
||||
import * as a2 from "./PendingTransactions.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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
& > * {
|
||||
width: 600px;
|
||||
}
|
||||
section {
|
||||
margin-bottom: 2em;
|
||||
table td {
|
||||
padding: 5px 5px;
|
||||
}
|
||||
table tr {
|
||||
border-bottom: 1px solid black;
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
button {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
|
@ -14,7 +14,7 @@
|
||||
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 { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||
import { ButtonHandler } from "../../mui/handlers.js";
|
||||
@ -37,6 +37,7 @@ export type State =
|
||||
| State.Ready
|
||||
| State.NoEnoughBalance
|
||||
| State.NoBalanceForCurrency
|
||||
| State.Completed
|
||||
| State.Confirmed;
|
||||
|
||||
export namespace State {
|
||||
@ -52,8 +53,6 @@ export namespace State {
|
||||
|
||||
interface BaseInfo {
|
||||
amount: AmountJson;
|
||||
totalFees: AmountJson;
|
||||
payStatus: PreparePayResult;
|
||||
uri: string;
|
||||
error: undefined;
|
||||
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
|
||||
@ -61,20 +60,30 @@ export namespace State {
|
||||
}
|
||||
export interface NoBalanceForCurrency extends BaseInfo {
|
||||
status: "no-balance-for-currency"
|
||||
payStatus: PreparePayResult;
|
||||
balance: undefined;
|
||||
}
|
||||
export interface NoEnoughBalance extends BaseInfo {
|
||||
status: "no-enough-balance"
|
||||
payStatus: PreparePayResult;
|
||||
balance: AmountJson;
|
||||
}
|
||||
export interface Ready extends BaseInfo {
|
||||
status: "ready";
|
||||
payStatus: PreparePayResultPaymentPossible;
|
||||
payHandler: ButtonHandler;
|
||||
balance: AmountJson;
|
||||
}
|
||||
|
||||
export interface Confirmed extends BaseInfo {
|
||||
status: "confirmed";
|
||||
payStatus: PreparePayResultAlreadyConfirmed;
|
||||
balance: AmountJson;
|
||||
}
|
||||
|
||||
export interface Completed extends BaseInfo {
|
||||
status: "completed";
|
||||
payStatus: PreparePayResult;
|
||||
payResult: ConfirmPayResult;
|
||||
payHandler: ButtonHandler;
|
||||
balance: AmountJson;
|
||||
@ -87,6 +96,7 @@ const viewMapping: StateViewMap<State> = {
|
||||
"no-balance-for-currency": BaseView,
|
||||
"no-enough-balance": BaseView,
|
||||
confirmed: BaseView,
|
||||
completed: BaseView,
|
||||
ready: BaseView,
|
||||
};
|
||||
|
||||
|
@ -78,20 +78,9 @@ export function useComponentState(
|
||||
(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 = {
|
||||
uri: hook.response.uri,
|
||||
amount,
|
||||
totalFees,
|
||||
payStatus,
|
||||
error: undefined,
|
||||
goBack, goToWalletManualWithdraw
|
||||
}
|
||||
@ -100,12 +89,45 @@ export function useComponentState(
|
||||
return {
|
||||
status: "no-balance-for-currency",
|
||||
balance: undefined,
|
||||
payStatus,
|
||||
...baseResult,
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
try {
|
||||
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 = {
|
||||
onClick: payErrMsg ? undefined : doPayment,
|
||||
error: payErrMsg,
|
||||
};
|
||||
|
||||
if (!payResult) {
|
||||
return {
|
||||
status: "ready",
|
||||
payHandler,
|
||||
...baseResult,
|
||||
balance: foundAmount
|
||||
};
|
||||
}
|
||||
|
||||
// (payStatus.status === PreparePayResultType.PaymentPossible)
|
||||
return {
|
||||
status: "confirmed",
|
||||
balance: foundAmount,
|
||||
payResult,
|
||||
payHandler: {},
|
||||
status: "ready",
|
||||
payHandler,
|
||||
payStatus,
|
||||
...baseResult,
|
||||
balance: foundAmount
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
@ -21,11 +21,14 @@
|
||||
|
||||
import {
|
||||
Amounts,
|
||||
ConfirmPayResultType,
|
||||
ContractTerms,
|
||||
PreparePayResultType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import merchantIcon from "../../../static-dev/merchant-icon.jpeg";
|
||||
import { createExample } from "../../test-utils.js";
|
||||
import { BaseView } from "./views.js";
|
||||
import beer from "../../../static-dev/beer.png";
|
||||
|
||||
export default {
|
||||
title: "cta/payment",
|
||||
@ -34,25 +37,22 @@ export default {
|
||||
};
|
||||
|
||||
export const NoBalance = createExample(BaseView, {
|
||||
status: "ready",
|
||||
status: "no-balance-for-currency",
|
||||
error: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:10"),
|
||||
balance: undefined,
|
||||
payHandler: {
|
||||
onClick: async () => {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||
|
||||
uri: "",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
noncePriv: "",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
contractTerms: {
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
summary: "some beers",
|
||||
amount: "USD:10",
|
||||
@ -62,7 +62,7 @@ export const NoBalance = createExample(BaseView, {
|
||||
});
|
||||
|
||||
export const NoEnoughBalance = createExample(BaseView, {
|
||||
status: "ready",
|
||||
status: "no-enough-balance",
|
||||
error: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:10"),
|
||||
balance: {
|
||||
@ -70,21 +70,18 @@ export const NoEnoughBalance = createExample(BaseView, {
|
||||
fraction: 40000000,
|
||||
value: 9,
|
||||
},
|
||||
payHandler: {
|
||||
onClick: async () => {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||
|
||||
uri: "",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
noncePriv: "",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
contractTerms: {
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
summary: "some beers",
|
||||
amount: "USD:10",
|
||||
@ -94,7 +91,7 @@ export const NoEnoughBalance = createExample(BaseView, {
|
||||
});
|
||||
|
||||
export const EnoughBalanceButRestricted = createExample(BaseView, {
|
||||
status: "ready",
|
||||
status: "no-enough-balance",
|
||||
error: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:10"),
|
||||
balance: {
|
||||
@ -102,21 +99,18 @@ export const EnoughBalanceButRestricted = createExample(BaseView, {
|
||||
fraction: 40000000,
|
||||
value: 19,
|
||||
},
|
||||
payHandler: {
|
||||
onClick: async () => {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||
|
||||
uri: "",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
noncePriv: "",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
contractTerms: {
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
summary: "some beers",
|
||||
amount: "USD:10",
|
||||
@ -139,7 +133,6 @@ export const PaymentPossible = createExample(BaseView, {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
@ -150,13 +143,19 @@ export const PaymentPossible = createExample(BaseView, {
|
||||
contractTerms: {
|
||||
nonce: "123213123",
|
||||
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",
|
||||
summary: "some beers",
|
||||
} as Partial<ContractTerms> as any,
|
||||
contractTermsHash: "123456",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
},
|
||||
});
|
||||
|
||||
@ -174,7 +173,6 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
@ -185,18 +183,19 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
|
||||
contractTerms: {
|
||||
nonce: "123213123",
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
amount: "USD:10",
|
||||
summary: "some beers",
|
||||
} as Partial<ContractTerms> as any,
|
||||
contractTermsHash: "123456",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
},
|
||||
});
|
||||
|
||||
import beer from "../../../static-dev/beer.png";
|
||||
|
||||
export const TicketWithAProductList = createExample(BaseView, {
|
||||
status: "ready",
|
||||
error: undefined,
|
||||
@ -211,7 +210,6 @@ export const TicketWithAProductList = createExample(BaseView, {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
@ -222,7 +220,10 @@ export const TicketWithAProductList = createExample(BaseView, {
|
||||
contractTerms: {
|
||||
nonce: "123213123",
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
amount: "USD:10",
|
||||
summary: "some beers",
|
||||
@ -247,11 +248,11 @@ export const TicketWithAProductList = createExample(BaseView, {
|
||||
],
|
||||
} as Partial<ContractTerms> as any,
|
||||
contractTermsHash: "123456",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
},
|
||||
});
|
||||
|
||||
export const AlreadyConfirmedByOther = createExample(BaseView, {
|
||||
export const TicketWithShipping = createExample(BaseView, {
|
||||
status: "ready",
|
||||
error: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:10"),
|
||||
@ -265,7 +266,52 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
|
||||
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",
|
||||
payStatus: {
|
||||
@ -274,19 +320,22 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
|
||||
amountRaw: "USD:10",
|
||||
contractTerms: {
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
summary: "some beers",
|
||||
amount: "USD:10",
|
||||
} as Partial<ContractTerms> as any,
|
||||
contractTermsHash: "123456",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
paid: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
|
||||
status: "ready",
|
||||
status: "completed",
|
||||
error: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:10"),
|
||||
balance: {
|
||||
@ -294,33 +343,34 @@ export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
|
||||
fraction: 40000000,
|
||||
value: 11,
|
||||
},
|
||||
payHandler: {
|
||||
onClick: async () => {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payResult: {
|
||||
type: ConfirmPayResultType.Done,
|
||||
contractTerms: {} as any,
|
||||
},
|
||||
payStatus: {
|
||||
status: PreparePayResultType.AlreadyConfirmed,
|
||||
amountEffective: "USD:10",
|
||||
amountRaw: "USD:10",
|
||||
contractTerms: {
|
||||
merchant: {
|
||||
name: "someone",
|
||||
name: "the merchant",
|
||||
logo: merchantIcon,
|
||||
website: "https://www.themerchant.taler",
|
||||
email: "contact@merchant.taler",
|
||||
},
|
||||
summary: "some beers",
|
||||
amount: "USD:10",
|
||||
} as Partial<ContractTerms> as any,
|
||||
contractTermsHash: "123456",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
paid: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const AlreadyPaidWithFulfillment = createExample(BaseView, {
|
||||
status: "ready",
|
||||
status: "completed",
|
||||
error: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:10"),
|
||||
balance: {
|
||||
@ -328,29 +378,34 @@ export const AlreadyPaidWithFulfillment = createExample(BaseView, {
|
||||
fraction: 40000000,
|
||||
value: 11,
|
||||
},
|
||||
payHandler: {
|
||||
onClick: async () => {
|
||||
null;
|
||||
},
|
||||
},
|
||||
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||
|
||||
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: {
|
||||
status: PreparePayResultType.AlreadyConfirmed,
|
||||
amountEffective: "USD:10",
|
||||
amountRaw: "USD:10",
|
||||
contractTerms: {
|
||||
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:
|
||||
"congratulations! you are looking at the fulfillment message! ",
|
||||
summary: "some beers",
|
||||
amount: "USD:10",
|
||||
} as Partial<ContractTerms> as any,
|
||||
contractTermsHash: "123456",
|
||||
proposalId: "proposal1234",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
paid: true,
|
||||
},
|
||||
});
|
||||
|
@ -204,7 +204,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||
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;
|
||||
}
|
||||
|
||||
@ -246,7 +246,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||
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;
|
||||
}
|
||||
|
||||
@ -293,7 +293,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||
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();
|
||||
r.payHandler.onClick();
|
||||
}
|
||||
@ -302,13 +302,13 @@ describe("Payment CTA states", () => {
|
||||
|
||||
{
|
||||
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.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
|
||||
expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||
if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
|
||||
expect(r.payResult.contractTerms).not.undefined;
|
||||
expect(r.payHandler.onClick).undefined;
|
||||
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
|
||||
// if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
|
||||
// expect(r.payResult.contractTerms).not.undefined;
|
||||
// expect(r.payHandler.onClick).undefined;
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate();
|
||||
@ -354,7 +354,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||
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();
|
||||
r.payHandler.onClick();
|
||||
}
|
||||
@ -366,7 +366,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||
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;
|
||||
if (r.payHandler.error === undefined) expect.fail();
|
||||
//FIXME: error message here is bad
|
||||
@ -425,7 +425,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
|
||||
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;
|
||||
|
||||
notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5"));
|
||||
@ -438,7 +438,7 @@ describe("Payment CTA states", () => {
|
||||
if (r.status !== "ready") expect.fail();
|
||||
expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AbsoluteTime,
|
||||
Amounts,
|
||||
ConfirmPayResultType,
|
||||
ContractTerms,
|
||||
@ -38,8 +39,10 @@ import {
|
||||
WalletAction,
|
||||
WarningBox,
|
||||
} from "../../components/styled/index.js";
|
||||
import { Time } from "../../components/Time.js";
|
||||
import { useTranslationContext } from "../../context/translation.js";
|
||||
import { Button } from "../../mui/Button.js";
|
||||
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
|
||||
import { State } from "./index.js";
|
||||
|
||||
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||
@ -56,6 +59,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||
type SupportedStates =
|
||||
| State.Ready
|
||||
| State.Confirmed
|
||||
| State.Completed
|
||||
| State.NoBalanceForCurrency
|
||||
| State.NoEnoughBalance;
|
||||
|
||||
@ -63,6 +67,15 @@ export function BaseView(state: SupportedStates): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
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 (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
@ -73,9 +86,9 @@ export function BaseView(state: SupportedStates): VNode {
|
||||
|
||||
<ShowImportantMessage state={state} />
|
||||
|
||||
<section>
|
||||
{state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
|
||||
Amounts.isNonZero(state.totalFees) && (
|
||||
<section style={{ textAlign: "left" }}>
|
||||
{/* {state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
|
||||
Amounts.isNonZero(totalFees) && (
|
||||
<Part
|
||||
big
|
||||
title={<i18n.Translate>Total to pay</i18n.Translate>}
|
||||
@ -89,26 +102,45 @@ export function BaseView(state: SupportedStates): VNode {
|
||||
text={<Amount value={state.payStatus.amountRaw} />}
|
||||
kind="neutral"
|
||||
/>
|
||||
{Amounts.isNonZero(state.totalFees) && (
|
||||
{Amounts.isNonZero(totalFees) && (
|
||||
<Fragment>
|
||||
<Part
|
||||
big
|
||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||
text={<Amount value={state.totalFees} />}
|
||||
text={<Amount value={totalFees} />}
|
||||
kind="negative"
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<Part
|
||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||
text={contractTerms.merchant.name}
|
||||
kind="neutral"
|
||||
/>
|
||||
)} */}
|
||||
<Part
|
||||
title={<i18n.Translate>Purchase</i18n.Translate>}
|
||||
text={contractTerms.summary}
|
||||
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 && (
|
||||
<Part
|
||||
title={<i18n.Translate>Receipt</i18n.Translate>}
|
||||
@ -116,8 +148,19 @@ export function BaseView(state: SupportedStates): VNode {
|
||||
kind="neutral"
|
||||
/>
|
||||
)}
|
||||
{contractTerms.products && contractTerms.products.length > 0 && (
|
||||
<ProductList products={contractTerms.products} />
|
||||
{contractTerms.pay_deadline && (
|
||||
<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>
|
||||
<ButtonsSection
|
||||
@ -232,7 +275,7 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status == "confirmed") {
|
||||
if (state.status == "completed") {
|
||||
const { payResult, payHandler } = state;
|
||||
if (payHandler.error) {
|
||||
return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
|
||||
@ -264,7 +307,7 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
function PayWithMobile({ state }: { state: State.Ready }): VNode {
|
||||
function PayWithMobile({ state }: { state: SupportedStates }): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const [showQR, setShowQR] = useState<boolean>(false);
|
||||
@ -286,7 +329,7 @@ function PayWithMobile({ state }: { state: State.Ready }): VNode {
|
||||
<div>
|
||||
<QR text={privateUri} />
|
||||
<i18n.Translate>
|
||||
Scan the QR code or
|
||||
Scan the QR code or
|
||||
<a href={privateUri}>
|
||||
<i18n.Translate>click here</i18n.Translate>
|
||||
</a>
|
||||
@ -306,61 +349,66 @@ function ButtonsSection({
|
||||
}): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
if (state.status === "ready") {
|
||||
const { payStatus } = state;
|
||||
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
||||
return (
|
||||
<Fragment>
|
||||
<section>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={state.payHandler.onClick}
|
||||
>
|
||||
<i18n.Translate>
|
||||
Pay {<Amount value={payStatus.amountEffective} />}
|
||||
</i18n.Translate>
|
||||
</Button>
|
||||
</section>
|
||||
<PayWithMobile state={state} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
|
||||
let BalanceMessage = "";
|
||||
if (!state.balance) {
|
||||
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
|
||||
return (
|
||||
<Fragment>
|
||||
<section>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={state.payHandler.onClick}
|
||||
>
|
||||
<i18n.Translate>
|
||||
Pay
|
||||
{<Amount value={state.payStatus.amountEffective} />}
|
||||
</i18n.Translate>
|
||||
</Button>
|
||||
</section>
|
||||
<PayWithMobile state={state} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if (
|
||||
state.status === "no-enough-balance" ||
|
||||
state.status === "no-balance-for-currency"
|
||||
) {
|
||||
// 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 {
|
||||
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 {
|
||||
BalanceMessage = i18n.str`Your current balance is not enough for this order.`;
|
||||
}
|
||||
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 (
|
||||
<Fragment>
|
||||
<section>
|
||||
{payStatus.paid &&
|
||||
{state.payStatus.paid &&
|
||||
state.payStatus.contractTerms.fulfillment_message && (
|
||||
<Part
|
||||
title={<i18n.Translate>Merchant message</i18n.Translate>}
|
||||
@ -369,13 +417,13 @@ function ButtonsSection({
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
{!payStatus.paid && <PayWithMobile state={state} />}
|
||||
{!state.payStatus.paid && <PayWithMobile state={state} />}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.status === "confirmed") {
|
||||
if (state.status === "completed") {
|
||||
if (state.payResult.type === ConfirmPayResultType.Pending) {
|
||||
return (
|
||||
<section>
|
||||
|
@ -30,6 +30,7 @@ export default {
|
||||
|
||||
export const EmptyBalance = createExample(TestedComponent, {
|
||||
balances: [],
|
||||
goToWalletManualWithdraw: {},
|
||||
});
|
||||
|
||||
export const SomeCoins = createExample(TestedComponent, {
|
||||
@ -42,6 +43,8 @@ export const SomeCoins = createExample(TestedComponent, {
|
||||
requiresUserInput: false,
|
||||
},
|
||||
],
|
||||
addAction: {},
|
||||
goToWalletManualWithdraw: {},
|
||||
});
|
||||
|
||||
export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||
@ -68,6 +71,8 @@ export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||
requiresUserInput: false,
|
||||
},
|
||||
],
|
||||
goToWalletManualWithdraw: {},
|
||||
addAction: {},
|
||||
});
|
||||
|
||||
export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||
@ -94,6 +99,8 @@ export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||
requiresUserInput: false,
|
||||
},
|
||||
],
|
||||
goToWalletManualWithdraw: {},
|
||||
addAction: {},
|
||||
});
|
||||
|
||||
export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
|
||||
@ -148,4 +155,6 @@ export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
|
||||
requiresUserInput: false,
|
||||
},
|
||||
],
|
||||
goToWalletManualWithdraw: {},
|
||||
addAction: {},
|
||||
});
|
||||
|
@ -23,8 +23,10 @@ import { Loading } from "../components/Loading.js";
|
||||
import { LoadingError } from "../components/LoadingError.js";
|
||||
import { MultiActionButton } from "../components/MultiActionButton.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 { ButtonHandler } from "../mui/handlers.js";
|
||||
import { compose, StateViewMap } from "../utils/index.js";
|
||||
import { AddNewActionView } from "../wallet/AddNewActionView.js";
|
||||
import * as wxApi from "../wxApi.js";
|
||||
import { NoBalanceHelp } from "./NoBalanceHelp.js";
|
||||
@ -34,17 +36,46 @@ export interface Props {
|
||||
goToWalletHistory: (currency: string) => Promise<void>;
|
||||
goToWalletManualWithdraw: () => Promise<void>;
|
||||
}
|
||||
export function BalancePage({
|
||||
goToWalletManualWithdraw,
|
||||
goToWalletDeposit,
|
||||
goToWalletHistory,
|
||||
}: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
export type State = State.Loading | State.Error | State.Action | State.Balances;
|
||||
|
||||
export namespace State {
|
||||
export interface Loading {
|
||||
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 state = useAsyncAsHook(wxApi.getBalance);
|
||||
const state = useAsyncAsHook(api.getBalance);
|
||||
|
||||
useEffect(() => {
|
||||
return wxApi.onUpdateNotification(
|
||||
return api.onUpdateNotification(
|
||||
[NotificationType.WithdrawGroupFinished],
|
||||
() => {
|
||||
state?.retry();
|
||||
@ -52,58 +83,80 @@ export function BalancePage({
|
||||
);
|
||||
});
|
||||
|
||||
const balances = !state || state.hasError ? [] : state.response.balances;
|
||||
|
||||
if (!state) {
|
||||
return <Loading />;
|
||||
return {
|
||||
status: "loading",
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (state.hasError) {
|
||||
return (
|
||||
<LoadingError
|
||||
title={<i18n.Translate>Could not load balance page</i18n.Translate>}
|
||||
error={state}
|
||||
/>
|
||||
);
|
||||
return {
|
||||
status: "error",
|
||||
error: state,
|
||||
};
|
||||
}
|
||||
|
||||
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 (
|
||||
<BalanceView
|
||||
balances={balances}
|
||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||
goToWalletDeposit={goToWalletDeposit}
|
||||
goToWalletHistory={goToWalletHistory}
|
||||
goToAddAction={async () => setAddingAction(true)}
|
||||
<LoadingError
|
||||
title={<i18n.Translate>Could not load balance page</i18n.Translate>}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export interface BalanceViewProps {
|
||||
balances: Balance[];
|
||||
goToWalletManualWithdraw: () => Promise<void>;
|
||||
goToAddAction: () => Promise<void>;
|
||||
goToWalletDeposit: (currency: string) => Promise<void>;
|
||||
goToWalletHistory: (currency: string) => Promise<void>;
|
||||
|
||||
function ActionView({ cancel }: State.Action): VNode {
|
||||
return <AddNewActionView onCancel={cancel.onClick!} />;
|
||||
}
|
||||
|
||||
export function BalanceView({
|
||||
balances,
|
||||
goToWalletManualWithdraw,
|
||||
goToWalletDeposit,
|
||||
goToWalletHistory,
|
||||
goToAddAction,
|
||||
}: BalanceViewProps): VNode {
|
||||
export function BalanceView(state: State.Balances): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const currencyWithNonZeroAmount = balances
|
||||
const currencyWithNonZeroAmount = state.balances
|
||||
.filter((b) => !Amounts.isZero(b.available))
|
||||
.map((b) => b.available.split(":")[0]);
|
||||
|
||||
if (balances.length === 0) {
|
||||
if (state.balances.length === 0) {
|
||||
return (
|
||||
<NoBalanceHelp goToWalletManualWithdraw={goToWalletManualWithdraw} />
|
||||
<NoBalanceHelp
|
||||
goToWalletManualWithdraw={state.goToWalletManualWithdraw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,23 +164,26 @@ export function BalanceView({
|
||||
<Fragment>
|
||||
<section>
|
||||
<BalanceTable
|
||||
balances={balances}
|
||||
goToWalletHistory={goToWalletHistory}
|
||||
balances={state.balances}
|
||||
goToWalletHistory={state.goToWalletHistory}
|
||||
/>
|
||||
</section>
|
||||
<footer style={{ justifyContent: "space-between" }}>
|
||||
<Button variant="contained" onClick={goToWalletManualWithdraw}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={state.goToWalletManualWithdraw.onClick}
|
||||
>
|
||||
<i18n.Translate>Withdraw</i18n.Translate>
|
||||
</Button>
|
||||
{currencyWithNonZeroAmount.length > 0 && (
|
||||
<MultiActionButton
|
||||
label={(s) => <i18n.Translate>Deposit {s}</i18n.Translate>}
|
||||
actions={currencyWithNonZeroAmount}
|
||||
onClick={(c) => goToWalletDeposit(c)}
|
||||
onClick={(c) => state.goToWalletDeposit(c)}
|
||||
/>
|
||||
)}
|
||||
<JustInDevMode>
|
||||
<Button onClick={goToAddAction}>
|
||||
<Button onClick={state.addAction.onClick}>
|
||||
<i18n.Translate>Enter URI</i18n.Translate>
|
||||
</Button>
|
||||
</JustInDevMode>
|
||||
|
@ -17,13 +17,14 @@ import { css } from "@linaria/core";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Alert } from "../mui/Alert.js";
|
||||
import { Button } from "../mui/Button.js";
|
||||
import { ButtonHandler } from "../mui/handlers.js";
|
||||
import { Paper } from "../mui/Paper.js";
|
||||
import { Typography } from "../mui/Typography.js";
|
||||
|
||||
export function NoBalanceHelp({
|
||||
goToWalletManualWithdraw,
|
||||
}: {
|
||||
goToWalletManualWithdraw: () => Promise<void>;
|
||||
goToWalletManualWithdraw: ButtonHandler;
|
||||
}): VNode {
|
||||
return (
|
||||
<Paper
|
||||
@ -37,7 +38,7 @@ export function NoBalanceHelp({
|
||||
fullWidth
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
onClick={goToWalletManualWithdraw}
|
||||
onClick={goToWalletManualWithdraw.onClick}
|
||||
>
|
||||
<Typography>Withdraw</Typography>
|
||||
</Button>
|
||||
|
@ -143,7 +143,11 @@ export function HistoryView({
|
||||
|
||||
if (balances.length === 0 || !selectedCurrency) {
|
||||
return (
|
||||
<NoBalanceHelp goToWalletManualWithdraw={goToWalletManualWithdraw} />
|
||||
<NoBalanceHelp
|
||||
goToWalletManualWithdraw={{
|
||||
onClick: goToWalletManualWithdraw,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -114,6 +114,12 @@ const exampleData = {
|
||||
tip: {
|
||||
...commonTransaction,
|
||||
type: TransactionType.Tip,
|
||||
// merchant: {
|
||||
// name: "the merchant",
|
||||
// logo: merchantIcon,
|
||||
// website: "https://www.themerchant.taler",
|
||||
// email: "contact@merchant.taler",
|
||||
// },
|
||||
merchantBaseUrl: "http://merchant.taler",
|
||||
} as TransactionTip,
|
||||
refund: {
|
||||
|
@ -16,18 +16,18 @@
|
||||
|
||||
import {
|
||||
AbsoluteTime,
|
||||
amountFractionalLength,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
Location,
|
||||
MerchantInfo,
|
||||
NotificationType,
|
||||
OrderShortInfo,
|
||||
parsePaytoUri,
|
||||
PaytoUri,
|
||||
stringifyPaytoUri,
|
||||
TalerProtocolTimestamp,
|
||||
Transaction,
|
||||
TransactionDeposit,
|
||||
TransactionPayment,
|
||||
TransactionRefresh,
|
||||
TransactionRefund,
|
||||
TransactionTip,
|
||||
@ -46,6 +46,7 @@ import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
|
||||
import { Loading } from "../components/Loading.js";
|
||||
import { LoadingError } from "../components/LoadingError.js";
|
||||
import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
|
||||
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
|
||||
import {
|
||||
CenteredDialog,
|
||||
InfoBox,
|
||||
@ -319,10 +320,15 @@ export function TransactionView({
|
||||
? undefined
|
||||
: Amounts.parseOrThrow(transaction.refundPending);
|
||||
|
||||
const total = Amounts.sub(
|
||||
Amounts.parseOrThrow(transaction.amountEffective),
|
||||
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
||||
).amount;
|
||||
const price = {
|
||||
raw: Amounts.parseOrThrow(transaction.amountRaw),
|
||||
effective: Amounts.parseOrThrow(transaction.amountEffective),
|
||||
};
|
||||
const refund = {
|
||||
raw: Amounts.parseOrThrow(transaction.totalRefundRaw),
|
||||
effective: Amounts.parseOrThrow(transaction.totalRefundEffective),
|
||||
};
|
||||
const total = Amounts.sub(price.effective, refund.effective).amount;
|
||||
|
||||
return (
|
||||
<TransactionTemplate>
|
||||
@ -404,45 +410,7 @@ export function TransactionView({
|
||||
)}
|
||||
<Part
|
||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||
text={
|
||||
<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>
|
||||
}
|
||||
text={<MerchantDetails merchant={transaction.info.merchant} />}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
@ -452,7 +420,14 @@ export function TransactionView({
|
||||
/>
|
||||
<Part
|
||||
title={<i18n.Translate>Details</i18n.Translate>}
|
||||
text={<PurchaseDetails transaction={transaction} />}
|
||||
text={
|
||||
<PurchaseDetails
|
||||
price={price}
|
||||
refund={refund}
|
||||
info={transaction.info}
|
||||
proposalId={transaction.proposalId}
|
||||
/>
|
||||
}
|
||||
kind="neutral"
|
||||
/>
|
||||
</TransactionTemplate>
|
||||
@ -521,12 +496,7 @@ export function TransactionView({
|
||||
</Header>
|
||||
{/* <Part
|
||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||
text={transaction.info.merchant.name}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
title={<i18n.Translate>Invoice ID</i18n.Translate>}
|
||||
text={transaction.info.orderId}
|
||||
text={<MerchantDetails merchant={transaction.merchant} />}
|
||||
kind="neutral"
|
||||
/> */}
|
||||
<Part
|
||||
@ -584,6 +554,46 @@ export function TransactionView({
|
||||
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({
|
||||
date,
|
||||
location,
|
||||
@ -703,57 +713,58 @@ function DeliveryDetails({
|
||||
);
|
||||
}
|
||||
|
||||
function PurchaseDetails({
|
||||
transaction,
|
||||
export interface AmountWithFee {
|
||||
effective: AmountJson;
|
||||
raw: AmountJson;
|
||||
}
|
||||
export function PurchaseDetails({
|
||||
price,
|
||||
refund,
|
||||
info,
|
||||
proposalId,
|
||||
}: {
|
||||
transaction: TransactionPayment;
|
||||
price: AmountWithFee;
|
||||
refund?: AmountWithFee;
|
||||
info: OrderShortInfo;
|
||||
proposalId: string;
|
||||
}): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const partialFee = Amounts.sub(
|
||||
Amounts.parseOrThrow(transaction.amountEffective),
|
||||
Amounts.parseOrThrow(transaction.amountRaw),
|
||||
).amount;
|
||||
const partialFee = Amounts.sub(price.effective, price.raw).amount;
|
||||
|
||||
const refundRaw = Amounts.parseOrThrow(transaction.totalRefundRaw);
|
||||
|
||||
const refundFee = Amounts.sub(
|
||||
refundRaw,
|
||||
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
||||
).amount;
|
||||
const refundFee = !refund
|
||||
? Amounts.getZero(price.effective.currency)
|
||||
: Amounts.sub(refund.raw, refund.effective).amount;
|
||||
|
||||
const fee = Amounts.sum([partialFee, refundFee]).amount;
|
||||
|
||||
const hasProducts =
|
||||
transaction.info.products && transaction.info.products.length > 0;
|
||||
const hasProducts = info.products && info.products.length > 0;
|
||||
|
||||
const hasShipping =
|
||||
transaction.info.delivery_date !== undefined ||
|
||||
transaction.info.delivery_location !== undefined;
|
||||
info.delivery_date !== undefined || info.delivery_location !== undefined;
|
||||
|
||||
const showLargePic = (): void => {
|
||||
return;
|
||||
};
|
||||
|
||||
const total = Amounts.sub(
|
||||
Amounts.parseOrThrow(transaction.amountEffective),
|
||||
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
||||
).amount;
|
||||
const total = !refund
|
||||
? price.effective
|
||||
: Amounts.sub(price.effective, refund.effective).amount;
|
||||
|
||||
return (
|
||||
<PurchaseDetailsTable>
|
||||
<tr>
|
||||
<td>Price</td>
|
||||
<td>
|
||||
<Amount value={transaction.amountRaw} />
|
||||
<Amount value={price.raw} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{Amounts.isNonZero(refundRaw) && (
|
||||
{refund && Amounts.isNonZero(refund.raw) && (
|
||||
<tr>
|
||||
<td>Refunded</td>
|
||||
<td>
|
||||
<Amount value={transaction.totalRefundRaw} negative />
|
||||
<Amount value={refund.raw} negative />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -784,7 +795,7 @@ function PurchaseDetails({
|
||||
title={<i18n.Translate>Products</i18n.Translate>}
|
||||
text={
|
||||
<ListOfProducts>
|
||||
{transaction.info.products?.map((p, k) => (
|
||||
{info.products?.map((p, k) => (
|
||||
<Row key={k}>
|
||||
<a href="#" onClick={showLargePic}>
|
||||
<img src={p.image ? p.image : emptyImg} />
|
||||
@ -813,14 +824,19 @@ function PurchaseDetails({
|
||||
title={<i18n.Translate>Delivery</i18n.Translate>}
|
||||
text={
|
||||
<DeliveryDetails
|
||||
date={transaction.info.delivery_date}
|
||||
location={transaction.info.delivery_location}
|
||||
date={info.delivery_date}
|
||||
location={info.delivery_location}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>
|
||||
<ShowFullContractTermPopup proposalId={proposalId} />
|
||||
</td>
|
||||
</tr>
|
||||
</PurchaseDetailsTable>
|
||||
);
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ import {
|
||||
PendingOperationsResponse,
|
||||
RemoveBackupProviderRequest,
|
||||
TalerError,
|
||||
WalletContractData,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
||||
@ -190,6 +191,11 @@ export function getBalance(): Promise<BalancesResponse> {
|
||||
return callBackend("getBalances", {});
|
||||
}
|
||||
|
||||
|
||||
export function getContractTermsDetails(proposalId: string): Promise<WalletContractData> {
|
||||
return callBackend("getContractTermsDetails", { proposalId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the full event history for this wallet.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user