
if handler do not trap error then fail at compile time, all safe handlers push alert on error errors are typed so they render good information
1566 lines
42 KiB
TypeScript
1566 lines
42 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,
|
|
TranslatedString,
|
|
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 { CopyButton } from "../components/CopyButton.js";
|
|
import { ErrorAlertView } from "../components/CurrentAlerts.js";
|
|
import { Loading } from "../components/Loading.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 { alertFromError } from "../context/alert.js";
|
|
import { useBackendContext } from "../context/backend.js";
|
|
import { useTranslationContext } from "../context/translation.js";
|
|
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
|
import { Button } from "../mui/Button.js";
|
|
import { Pages } from "../NavigationBar.js";
|
|
|
|
interface Props {
|
|
tid: string;
|
|
goToWalletHistory: (currency?: string) => Promise<void>;
|
|
}
|
|
|
|
export function TransactionPage({
|
|
tid: transactionId,
|
|
goToWalletHistory,
|
|
}: Props): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
const api = useBackendContext();
|
|
const state = useAsyncAsHook(
|
|
() =>
|
|
api.wallet.call(WalletApiOperation.GetTransactionById, {
|
|
transactionId,
|
|
}),
|
|
[transactionId],
|
|
);
|
|
|
|
useEffect(() =>
|
|
api.listener.onUpdateNotification(
|
|
[NotificationType.WithdrawGroupFinished],
|
|
state?.retry,
|
|
),
|
|
);
|
|
|
|
if (!state) {
|
|
return <Loading />;
|
|
}
|
|
|
|
if (state.hasError) {
|
|
return (
|
|
<ErrorAlertView
|
|
error={alertFromError(
|
|
i18n.str`Could not load transaction information`,
|
|
state,
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const currency = Amounts.parse(state.response.amountRaw)?.currency;
|
|
|
|
return (
|
|
<TransactionView
|
|
transaction={state.response}
|
|
onSend={async () => {
|
|
null;
|
|
}}
|
|
onDelete={async () => {
|
|
await api.wallet.call(WalletApiOperation.DeleteTransaction, {
|
|
transactionId,
|
|
});
|
|
goToWalletHistory(currency);
|
|
}}
|
|
onRetry={async () => {
|
|
await api.wallet.call(WalletApiOperation.RetryTransaction, {
|
|
transactionId,
|
|
});
|
|
goToWalletHistory(currency);
|
|
}}
|
|
onRefund={async (purchaseId) => {
|
|
await api.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" }}>
|
|
{transaction?.error ? (
|
|
<ErrorAlertView
|
|
error={alertFromError(
|
|
i18n.str`There was an error trying to complete the transaction`,
|
|
transaction.error,
|
|
)}
|
|
/>
|
|
) : undefined}
|
|
{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}
|
|
/>
|
|
<table>
|
|
<tbody>
|
|
<tr>
|
|
<td>
|
|
<pre>
|
|
<b>
|
|
<a
|
|
target="_bank"
|
|
rel="noreferrer"
|
|
title="RFC 8905 for designating targets for payments"
|
|
href="https://tools.ietf.org/html/rfc8905"
|
|
>
|
|
Payto URI
|
|
</a>
|
|
</b>
|
|
</pre>
|
|
</td>
|
|
<td width="100%" style={{ wordBreak: "break-all" }}>
|
|
{transaction.withdrawalDetails.exchangePaytoUris[0]}
|
|
</td>
|
|
<td>
|
|
<CopyButton
|
|
getContent={() =>
|
|
transaction.withdrawalDetails.type ===
|
|
WithdrawalType.ManualTransfer
|
|
? transaction.withdrawalDetails.exchangePaytoUris[0]
|
|
: ""
|
|
}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<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>
|
|
Wire transfer need a confirmation. Go to the
|
|
<a
|
|
href={transaction.withdrawalDetails.bankConfirmationUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
style={{ display: "inline" }}
|
|
>
|
|
<i18n.Translate>bank site</i18n.Translate>
|
|
</a>{" "}
|
|
and check wire transfer operation to exchange account is
|
|
complete.
|
|
</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.str`Details`}
|
|
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.str`Refunds`}
|
|
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.str`Offer`}
|
|
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.str`Merchant`}
|
|
text={<MerchantDetails merchant={transaction.info.merchant} />}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Invoice ID`}
|
|
text={transaction.info.orderId as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Details`}
|
|
text={<DepositDetails transaction={transaction} />}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Wire transfer deadline`}
|
|
text={
|
|
<Time
|
|
timestamp={AbsoluteTime.fromTimestamp(
|
|
transaction.wireTransferDeadline,
|
|
)}
|
|
format="dd MMMM yyyy 'at' HH:mm"
|
|
/>
|
|
}
|
|
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.str`Details`}
|
|
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.str`Merchant`}
|
|
text={<MerchantDetails merchant={transaction.merchant} />}
|
|
kind="neutral"
|
|
/> */}
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Merchant`}
|
|
text={transaction.info.merchant.name as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Original order ID`}
|
|
text={
|
|
<a
|
|
href={Pages.balanceTransaction({
|
|
tid: transaction.refundedTransactionId,
|
|
})}
|
|
>
|
|
{transaction.info.orderId}
|
|
</a>
|
|
}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Purchase summary`}
|
|
text={transaction.info.summary as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Subject`}
|
|
text={transaction.info.summary as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
) : undefined}
|
|
<Part
|
|
title={i18n.str`Exchange`}
|
|
text={transaction.exchangeBaseUrl as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
{transaction.pending /** pending is not-pay */ && (
|
|
<Part
|
|
title={i18n.str`URI`}
|
|
text={<ShowQrWithCopy text={transaction.talerUri} />}
|
|
kind="neutral"
|
|
/>
|
|
)}
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Subject`}
|
|
text={transaction.info.summary as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
) : undefined}
|
|
<Part
|
|
title={i18n.str`Exchange`}
|
|
text={transaction.exchangeBaseUrl as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Subject`}
|
|
text={transaction.info.summary as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
) : undefined}
|
|
<Part
|
|
title={i18n.str`Exchange`}
|
|
text={transaction.exchangeBaseUrl as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
{/* {transaction.pending && ( //pending is not-received
|
|
)} */}
|
|
<Part
|
|
title={i18n.str`URI`}
|
|
text={<ShowQrWithCopy text={transaction.talerUri} />}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Subject`}
|
|
text={transaction.info.summary as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
) : undefined}
|
|
<Part
|
|
title={i18n.str`Exchange`}
|
|
text={transaction.exchangeBaseUrl as TranslatedString}
|
|
kind="neutral"
|
|
/>
|
|
<Part
|
|
title={i18n.str`Details`}
|
|
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.str`Products`}
|
|
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.str`Delivery`}
|
|
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(e, r).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} 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: TranslatedString;
|
|
}): 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>;
|
|
}
|