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:
Sebastian 2022-08-08 14:09:28 -03:00
parent 4409d8384b
commit 7a600514c6
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
17 changed files with 1128 additions and 307 deletions

View 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>
);
}

View File

@ -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, {});

View File

@ -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>
&nbsp;
</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>
&nbsp;
</Fragment>
))}
</td>
</tr>
</ContractTermsTable>
</div>
</Modal>
);
}

View File

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

View File

@ -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;

View File

@ -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,
}; };

View File

@ -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
}; };
} }

View File

@ -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,
}, },
}); });

View File

@ -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;
} }

View File

@ -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 &nbsp;
<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 &nbsp;
<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>

View File

@ -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: {},
}); });

View File

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

View File

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

View File

@ -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 (

View File

@ -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: {

View File

@ -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>
); );
} }

View File

@ -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.
*/ */