wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx

1521 lines
40 KiB
TypeScript

/*
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,
AmountJson,
Amounts,
Location,
MerchantInfo,
NotificationType,
OrderShortInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
TalerProtocolTimestamp,
Transaction,
TransactionDeposit,
TransactionRefresh,
TransactionRefund,
TransactionTip,
TransactionType,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { styled } from "@linaria/react";
import { differenceInSeconds } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import emptyImg from "../../static/img/empty.png";
import { Amount } from "../components/Amount.js";
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
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 { QR } from "../components/QR.js";
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
import {
CenteredDialog,
InfoBox,
ListOfProducts,
Overlay,
Row,
SmallLightText,
SubTitle,
WarningBox,
} from "../components/styled/index.js";
import { Time } from "../components/Time.js";
import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
import { Pages } from "../NavigationBar.js";
import { wxApi } from "../wxApi.js";
interface Props {
tid: string;
goToWalletHistory: (currency?: string) => Promise<void>;
}
export function TransactionPage({
tid: transactionId,
goToWalletHistory,
}: Props): VNode {
const { i18n } = useTranslationContext();
const state = useAsyncAsHook(
() =>
wxApi.wallet.call(WalletApiOperation.GetTransactionById, {
transactionId,
}),
[transactionId],
);
useEffect(() =>
wxApi.listener.onUpdateNotification(
[NotificationType.WithdrawGroupFinished],
state?.retry,
),
);
if (!state) {
return <Loading />;
}
if (state.hasError) {
return (
<LoadingError
title={
<i18n.Translate>
Could not load the transaction information
</i18n.Translate>
}
error={state}
/>
);
}
const currency = Amounts.parse(state.response.amountRaw)?.currency;
return (
<TransactionView
transaction={state.response}
onSend={async () => {
null;
}}
onDelete={async () => {
await wxApi.wallet.call(WalletApiOperation.DeleteTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onRetry={async () => {
await wxApi.wallet.call(WalletApiOperation.RetryTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onRefund={async (purchaseId) => {
await wxApi.wallet.call(WalletApiOperation.ApplyRefundFromPurchaseId, {
purchaseId,
});
}}
onBack={() => goToWalletHistory(currency)}
/>
);
}
export interface WalletTransactionProps {
transaction: Transaction;
onSend: () => Promise<void>;
onDelete: () => Promise<void>;
onRetry: () => Promise<void>;
onRefund: (id: string) => Promise<void>;
onBack: () => Promise<void>;
}
const PurchaseDetailsTable = styled.table`
width: 100%;
& > tr > td:nth-child(2n) {
text-align: right;
}
`;
export function TransactionView({
transaction,
onDelete,
onRetry,
onSend,
onRefund,
}: WalletTransactionProps): VNode {
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
async function doCheckBeforeForget(): Promise<void> {
if (
transaction.pending &&
transaction.type === TransactionType.Withdrawal
) {
setConfirmBeforeForget(true);
} else {
onDelete();
}
}
const SHOWING_RETRY_THRESHOLD_SECS = 30;
const { i18n } = useTranslationContext();
function TransactionTemplate({
children,
}: {
children: ComponentChildren;
}): VNode {
const showSend = false;
// (transaction.type === TransactionType.PeerPullCredit ||
// transaction.type === TransactionType.PeerPushDebit) &&
// !transaction.info.completed;
const showRetry =
transaction.error !== undefined ||
transaction.timestamp.t_s === "never" ||
(transaction.pending &&
differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) >
SHOWING_RETRY_THRESHOLD_SECS);
return (
<Fragment>
<section style={{ padding: 8, textAlign: "center" }}>
<ErrorTalerOperation
title={
<i18n.Translate>
There was an error trying to complete the transaction
</i18n.Translate>
}
error={transaction?.error}
/>
{transaction.pending && (
<WarningBox>
<i18n.Translate>This transaction is not completed</i18n.Translate>
</WarningBox>
)}
</section>
<section>{children}</section>
<footer>
<div>
{showSend ? (
<Button variant="contained" onClick={onSend}>
<i18n.Translate>Send</i18n.Translate>
</Button>
) : null}
</div>
<div>
{showRetry ? (
<Button variant="contained" onClick={onRetry}>
<i18n.Translate>Retry</i18n.Translate>
</Button>
) : null}
<Button
variant="contained"
color="error"
onClick={doCheckBeforeForget}
>
<i18n.Translate>Forget</i18n.Translate>
</Button>
</div>
</footer>
</Fragment>
);
}
if (transaction.type === TransactionType.Withdrawal) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
const chosen = Amounts.parseOrThrow(transaction.amountRaw);
return (
<TransactionTemplate>
{confirmBeforeForget ? (
<Overlay>
<CenteredDialog>
<header>
<i18n.Translate>Caution!</i18n.Translate>
</header>
<section>
<i18n.Translate>
If you have already wired money to the exchange you will loose
the chance to get the coins form it.
</i18n.Translate>
</section>
<footer>
<Button
variant="contained"
color="secondary"
onClick={async () => setConfirmBeforeForget(false)}
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
<Button variant="contained" color="error" onClick={onDelete}>
<i18n.Translate>Confirm</i18n.Translate>
</Button>
</footer>
</CenteredDialog>
</Overlay>
) : undefined}
<Header
timestamp={transaction.timestamp}
type={i18n.str`Withdrawal`}
total={total}
kind="positive"
>
{transaction.exchangeBaseUrl}
</Header>
{!transaction.pending ? undefined : transaction.withdrawalDetails
.type === WithdrawalType.ManualTransfer ? (
<Fragment>
<BankDetailsByPaytoType
amount={chosen}
exchangeBaseUrl={transaction.exchangeBaseUrl}
payto={parsePaytoUri(
transaction.withdrawalDetails.exchangePaytoUris[0],
)}
subject={transaction.withdrawalDetails.reservePub}
/>
<WarningBox>
<i18n.Translate>
Make sure to use the correct subject, otherwise the money will
not arrive in this wallet.
</i18n.Translate>
</WarningBox>
</Fragment>
) : (
<Fragment>
{!transaction.withdrawalDetails.confirmed &&
transaction.withdrawalDetails.bankConfirmationUrl ? (
<InfoBox>
<div style={{ display: "block" }}>
<i18n.Translate>
The bank did not yet confirmed the wire transfer. Go to the
{` `}
<a
href={transaction.withdrawalDetails.bankConfirmationUrl}
target="_blank"
rel="noreferrer"
style={{ display: "inline" }}
>
<i18n.Translate>bank site</i18n.Translate>
</a>{" "}
and check there is no pending step.
</i18n.Translate>
</div>
</InfoBox>
) : undefined}
{transaction.withdrawalDetails.confirmed && (
<InfoBox>
<i18n.Translate>
Bank has confirmed the wire transfer. Waiting for the exchange
to send the coins
</i18n.Translate>
</InfoBox>
)}
</Fragment>
)}
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<WithdrawDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Payment) {
const pendingRefund =
transaction.refundPending === undefined
? undefined
: Amounts.parseOrThrow(transaction.refundPending);
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>
<Header
timestamp={transaction.timestamp}
total={total}
type={i18n.str`Payment`}
kind="negative"
>
{transaction.info.fulfillmentUrl ? (
<a
href={transaction.info.fulfillmentUrl}
target="_bank"
rel="noreferrer"
>
{transaction.info.summary}
</a>
) : (
transaction.info.summary
)}
</Header>
<br />
{transaction.refunds.length > 0 ? (
<Part
title={<i18n.Translate>Refunds</i18n.Translate>}
text={
<table>
{transaction.refunds.map((r, i) => {
return (
<tr key={i}>
<td>
<i18n.Translate>
{<Amount value={r.amountEffective} />}{" "}
<a
href={Pages.balanceTransaction({
tid: r.transactionId,
})}
>
was refunded
</a>{" "}
on{" "}
{
<Time
timestamp={AbsoluteTime.fromTimestamp(
r.timestamp,
)}
format="dd MMMM yyyy"
/>
}
</i18n.Translate>
</td>
</tr>
);
})}
</table>
}
kind="neutral"
/>
) : undefined}
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
<InfoBox>
<i18n.Translate>
Merchant created a refund for this order but was not automatically
picked up.
</i18n.Translate>
<Part
title={<i18n.Translate>Offer</i18n.Translate>}
text={<Amount value={pendingRefund} />}
kind="positive"
/>
<div>
<div />
<div>
<Button
variant="contained"
onClick={() => onRefund(transaction.proposalId)}
>
<i18n.Translate>Accept</i18n.Translate>
</Button>
</div>
</div>
</InfoBox>
)}
<Part
title={<i18n.Translate>Merchant</i18n.Translate>}
text={<MerchantDetails merchant={transaction.info.merchant} />}
kind="neutral"
/>
<Part
title={<i18n.Translate>Invoice ID</i18n.Translate>}
text={transaction.info.orderId}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<PurchaseDetails
price={price}
refund={refund}
info={transaction.info}
proposalId={transaction.proposalId}
/>
}
kind="neutral"
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Deposit) {
const total = Amounts.parseOrThrow(transaction.amountRaw);
const payto = parsePaytoUri(transaction.targetPaytoUri);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Deposit`}
total={total}
kind="negative"
>
{!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />}
</Header>
{payto && <PartPayto payto={payto} kind="neutral" />}
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={<DepositDetails transaction={transaction} />}
kind="neutral"
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refresh) {
const total = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount;
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refresh`}
total={total}
kind="negative"
>
{transaction.exchangeBaseUrl}
</Header>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={<RefreshDetails transaction={transaction} />}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Tip) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Tip`}
total={total}
kind="positive"
>
{transaction.merchantBaseUrl}
</Header>
{/* <Part
title={<i18n.Translate>Merchant</i18n.Translate>}
text={<MerchantDetails merchant={transaction.merchant} />}
kind="neutral"
/> */}
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={<TipDetails transaction={transaction} />}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refund) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refund`}
total={total}
kind="positive"
>
{transaction.info.summary}
</Header>
<Part
title={<i18n.Translate>Merchant</i18n.Translate>}
text={transaction.info.merchant.name}
kind="neutral"
/>
<Part
title={<i18n.Translate>Original order ID</i18n.Translate>}
text={
<a
href={Pages.balanceTransaction({
tid: transaction.refundedTransactionId,
})}
>
{transaction.info.orderId}
</a>
}
kind="neutral"
/>
<Part
title={<i18n.Translate>Purchase summary</i18n.Translate>}
text={transaction.info.summary}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={<RefundDetails transaction={transaction} />}
/>
</TransactionTemplate>
);
}
function ShowQrWithCopy({ text }: { text: string }): VNode {
const [showing, setShowing] = useState(false);
async function copy(): Promise<void> {
navigator.clipboard.writeText(text);
}
async function toggle(): Promise<void> {
setShowing((s) => !s);
}
if (showing) {
return (
<div>
<QR text={text} />
<Button onClick={copy}>
<i18n.Translate>copy</i18n.Translate>
</Button>
<Button onClick={toggle}>
<i18n.Translate>hide qr</i18n.Translate>
</Button>
</div>
);
}
return (
<div>
<div>{text.substring(0, 64)}...</div>
<Button onClick={copy}>
<i18n.Translate>copy</i18n.Translate>
</Button>
<Button onClick={toggle}>
<i18n.Translate>show qr</i18n.Translate>
</Button>
</div>
);
}
if (transaction.type === TransactionType.PeerPullCredit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
total={total}
kind="positive"
>
<i18n.Translate>Invoice</i18n.Translate>
</Header>
{transaction.info.summary ? (
<Part
title={<i18n.Translate>Subject</i18n.Translate>}
text={transaction.info.summary}
kind="neutral"
/>
) : undefined}
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
{transaction.pending /** pending is not-pay */ && (
<Part
title={<i18n.Translate>URI</i18n.Translate>}
text={<ShowQrWithCopy text={transaction.talerUri} />}
kind="neutral"
/>
)}
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<InvoiceDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPullDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
total={total}
kind="negative"
>
<i18n.Translate>Invoice</i18n.Translate>
</Header>
{transaction.info.summary ? (
<Part
title={<i18n.Translate>Subject</i18n.Translate>}
text={transaction.info.summary}
kind="neutral"
/>
) : undefined}
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<InvoiceDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPushDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
total={total}
kind="negative"
>
<i18n.Translate>Transfer</i18n.Translate>
</Header>
{transaction.info.summary ? (
<Part
title={<i18n.Translate>Subject</i18n.Translate>}
text={transaction.info.summary}
kind="neutral"
/>
) : undefined}
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
{/* {transaction.pending && ( //pending is not-received
)} */}
<Part
title={<i18n.Translate>URI</i18n.Translate>}
text={<ShowQrWithCopy text={transaction.talerUri} />}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<TransferDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPushCredit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
total={total}
kind="positive"
>
<i18n.Translate>Transfer</i18n.Translate>
</Header>
{transaction.info.summary ? (
<Part
title={<i18n.Translate>Subject</i18n.Translate>}
text={transaction.info.summary}
kind="neutral"
/>
) : undefined}
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<TransferDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
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,
}: {
date: TalerProtocolTimestamp | undefined;
location: Location | undefined;
}): VNode {
const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
{location && (
<Fragment>
{location.country && (
<tr>
<td>
<i18n.Translate>Country</i18n.Translate>
</td>
<td>{location.country}</td>
</tr>
)}
{location.address_lines && (
<tr>
<td>
<i18n.Translate>Address lines</i18n.Translate>
</td>
<td>{location.address_lines}</td>
</tr>
)}
{location.building_number && (
<tr>
<td>
<i18n.Translate>Building number</i18n.Translate>
</td>
<td>{location.building_number}</td>
</tr>
)}
{location.building_name && (
<tr>
<td>
<i18n.Translate>Building name</i18n.Translate>
</td>
<td>{location.building_name}</td>
</tr>
)}
{location.street && (
<tr>
<td>
<i18n.Translate>Street</i18n.Translate>
</td>
<td>{location.street}</td>
</tr>
)}
{location.post_code && (
<tr>
<td>
<i18n.Translate>Post code</i18n.Translate>
</td>
<td>{location.post_code}</td>
</tr>
)}
{location.town_location && (
<tr>
<td>
<i18n.Translate>Town location</i18n.Translate>
</td>
<td>{location.town_location}</td>
</tr>
)}
{location.town && (
<tr>
<td>
<i18n.Translate>Town</i18n.Translate>
</td>
<td>{location.town}</td>
</tr>
)}
{location.district && (
<tr>
<td>
<i18n.Translate>District</i18n.Translate>
</td>
<td>{location.district}</td>
</tr>
)}
{location.country_subdivision && (
<tr>
<td>
<i18n.Translate>Country subdivision</i18n.Translate>
</td>
<td>{location.country_subdivision}</td>
</tr>
)}
</Fragment>
)}
{!location || !date ? undefined : (
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
)}
{date && (
<Fragment>
<tr>
<td>
<i18n.Translate>Date</i18n.Translate>
</td>
<td>
<Time
timestamp={AbsoluteTime.fromTimestamp(date)}
format="dd MMMM yyyy, HH:mm"
/>
</td>
</tr>
</Fragment>
)}
</PurchaseDetailsTable>
);
}
export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
return (
<div>
<p style={{ marginTop: 0 }}>
<a rel="noreferrer" target="_blank" href={exchange}>
{exchange}
</a>
</p>
</div>
);
}
export interface AmountWithFee {
effective: AmountJson;
raw: AmountJson;
}
export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.raw, amount.effective).amount;
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Invoice</i18n.Translate>
</td>
<td>
<Amount value={amount.raw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={amount.effective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.raw, amount.effective).amount;
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Transfer</i18n.Translate>
</td>
<td>
<Amount value={amount.raw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={amount.effective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.raw, amount.effective).amount;
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Withdraw</i18n.Translate>
</td>
<td>
<Amount value={amount.raw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={amount.effective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function PurchaseDetails({
price,
refund,
info,
proposalId,
}: {
price: AmountWithFee;
refund?: AmountWithFee;
info: OrderShortInfo;
proposalId: string;
}): VNode {
const { i18n } = useTranslationContext();
const partialFee = Amounts.sub(price.effective, price.raw).amount;
const refundFee = !refund
? Amounts.zeroOfCurrency(price.effective.currency)
: Amounts.sub(refund.raw, refund.effective).amount;
const fee = Amounts.sum([partialFee, refundFee]).amount;
const hasProducts = info.products && info.products.length > 0;
const hasShipping =
info.delivery_date !== undefined || info.delivery_location !== undefined;
const showLargePic = (): void => {
return;
};
const total = !refund
? price.effective
: Amounts.sub(price.effective, refund.effective).amount;
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Price</i18n.Translate>
</td>
<td>
<Amount value={price.raw} />
</td>
</tr>
{refund && Amounts.isNonZero(refund.raw) && (
<tr>
<td>
<i18n.Translate>Refunded</i18n.Translate>
</td>
<td>
<Amount value={refund.raw} negative />
</td>
</tr>
)}
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={total} />
</td>
</tr>
{hasProducts && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
title={<i18n.Translate>Products</i18n.Translate>}
text={
<ListOfProducts>
{info.products?.map((p, k) => (
<Row key={k}>
<a href="#" onClick={showLargePic}>
<img src={p.image ? p.image : emptyImg} />
</a>
<div>
{p.quantity && p.quantity > 0 && (
<SmallLightText>
x {p.quantity} {p.unit}
</SmallLightText>
)}
<div>{p.description}</div>
</div>
</Row>
))}
</ListOfProducts>
}
/>
</td>
</tr>
)}
{hasShipping && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
title={<i18n.Translate>Delivery</i18n.Translate>}
text={
<DeliveryDetails
date={info.delivery_date}
location={info.delivery_location}
/>
}
/>
</td>
</tr>
)}
<tr>
<td>
<ShowFullContractTermPopup proposalId={proposalId} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
function RefundDetails({
transaction,
}: {
transaction: TransactionRefund;
}): VNode {
const { i18n } = useTranslationContext();
const r = Amounts.parseOrThrow(transaction.amountRaw);
const e = Amounts.parseOrThrow(transaction.amountEffective);
const fee = Amounts.sub(r, e).amount;
const maxFrac = [r, e, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Amount</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
function DepositDetails({
transaction,
}: {
transaction: TransactionDeposit;
}): VNode {
const { i18n } = useTranslationContext();
const r = Amounts.parseOrThrow(transaction.amountRaw);
const e = Amounts.parseOrThrow(transaction.amountEffective);
const fee = Amounts.sub(r, e).amount;
const maxFrac = [r, e, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Amount</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total transfer</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
function RefreshDetails({
transaction,
}: {
transaction: TransactionRefresh;
}): VNode {
const { i18n } = useTranslationContext();
const r = Amounts.parseOrThrow(transaction.amountRaw);
const e = Amounts.parseOrThrow(transaction.amountEffective);
const fee = Amounts.sub(r, e).amount;
const maxFrac = [r, e, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Amount</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
const { i18n } = useTranslationContext();
const r = Amounts.parseOrThrow(transaction.amountRaw);
const e = Amounts.parseOrThrow(transaction.amountEffective);
const fee = Amounts.sub(r, e).amount;
const maxFrac = [r, e, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Amount</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
function Header({
timestamp,
total,
children,
kind,
type,
}: {
timestamp: TalerProtocolTimestamp;
total: AmountJson;
children: ComponentChildren;
kind: Kind;
type: string;
}): VNode {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "row",
}}
>
<div>
<SubTitle>{children}</SubTitle>
<Time
timestamp={AbsoluteTime.fromTimestamp(timestamp)}
format="dd MMMM yyyy, HH:mm"
/>
</div>
<div>
<SubTitle>
<Part
title={type}
text={<Amount value={total} negative={kind === "negative"} />}
kind={kind}
/>
</SubTitle>
</div>
</div>
);
}
function NicePayto({ payto }: { payto: PaytoUri }): VNode {
if (payto.isKnown) {
switch (payto.targetType) {
case "bitcoin": {
return <div>{payto.targetPath.substring(0, 20)}...</div>;
}
case "x-taler-bank": {
const url = new URL("/", `https://${payto.host}`);
return (
<Fragment>
<div>{payto.account}</div>
<SmallLightText>
<a href={url.href} target="_bank" rel="noreferrer">
{url.toString()}
</a>
</SmallLightText>
</Fragment>
);
}
case "iban": {
return <div>{payto.targetPath.substring(0, 20)}</div>;
}
}
}
return <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
}