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

1940 lines
52 KiB
TypeScript
Raw Normal View History

/*
2022-06-06 17:05:26 +02:00
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
2022-06-06 17:05:26 +02:00
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.
2022-06-06 17:05:26 +02:00
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
2022-06-06 17:05:26 +02:00
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
2021-11-15 15:18:58 +01:00
import {
2022-03-18 15:32:41 +01:00
AbsoluteTime,
AmountJson,
2021-11-15 15:18:58 +01:00
Amounts,
Location,
MerchantInfo,
NotificationType,
OrderShortInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
2021-11-15 15:18:58 +01:00
Transaction,
TransactionDeposit,
TransactionIdStr,
2023-05-10 18:35:18 +02:00
TransactionMajorState,
2021-11-15 15:18:58 +01:00
TransactionType,
2023-01-09 12:38:48 +01:00
TranslatedString,
WithdrawalType,
2021-11-15 15:18:58 +01:00
} from "@gnu-taler/taler-util";
2022-10-25 17:23:08 +02:00
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
2023-05-05 13:47:00 +02:00
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
2023-01-20 19:44:53 +01:00
import { differenceInSeconds, isPast } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
2022-04-26 03:37:41 +02:00
import { useEffect, useState } from "preact/hooks";
2021-10-27 20:13:35 +02:00
import emptyImg from "../../static/img/empty.png";
2022-04-11 16:33:55 +02:00
import { Amount } from "../components/Amount.js";
2022-03-29 04:41:07 +02:00
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
2022-11-07 19:11:45 +01:00
import { CopyButton } from "../components/CopyButton.js";
2023-01-11 19:26:07 +01:00
import { AlertView, ErrorAlertView } from "../components/CurrentAlerts.js";
2022-03-29 04:41:07 +02:00
import { Loading } from "../components/Loading.js";
import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
2022-08-31 05:20:35 +02:00
import { QR } from "../components/QR.js";
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
2021-11-15 15:18:58 +01:00
import {
CenteredDialog,
2023-06-01 19:26:28 +02:00
ErrorBox,
InfoBox,
2021-11-15 15:18:58 +01:00
ListOfProducts,
Overlay,
Row,
2021-11-15 15:18:58 +01:00
SmallLightText,
SubTitle,
2023-04-24 19:53:01 +02:00
SuccessBox,
2021-11-15 15:18:58 +01:00
WarningBox,
2022-03-29 04:41:07 +02:00
} from "../components/styled/index.js";
import { Time } from "../components/Time.js";
2023-01-13 20:09:33 +01:00
import { alertFromError, useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
2022-04-26 04:07:31 +02:00
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
2022-06-01 20:47:47 +02:00
import { Button } from "../mui/Button.js";
2023-01-13 20:09:33 +01:00
import { SafeHandler } from "../mui/handlers.js";
2022-05-14 23:09:33 +02:00
import { Pages } from "../NavigationBar.js";
2023-01-20 19:44:53 +01:00
import { assertUnreachable } from "../utils/index.js";
2023-04-19 17:42:47 +02:00
import { EnabledBySettings } from "../components/EnabledBySettings.js";
2023-06-02 19:32:52 +02:00
import { useSettings } from "../hooks/useSettings.js";
2022-01-25 14:29:29 +01:00
interface Props {
tid: string;
2022-06-01 20:47:47 +02:00
goToWalletHistory: (currency?: string) => Promise<void>;
2022-01-25 14:29:29 +01:00
}
2022-04-26 03:37:41 +02:00
export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
const transactionId = tid as TransactionIdStr; //FIXME: validate
const { i18n } = useTranslationContext();
const api = useBackendContext();
2022-10-25 17:23:08 +02:00
const state = useAsyncAsHook(
() =>
api.wallet.call(WalletApiOperation.GetTransactionById, {
2022-10-25 17:23:08 +02:00
transactionId,
}),
[transactionId],
);
2022-04-26 03:37:41 +02:00
2022-10-25 17:23:08 +02:00
useEffect(() =>
api.listener.onUpdateNotification(
2023-03-31 17:15:46 +02:00
[NotificationType.WithdrawGroupFinished, NotificationType.KycRequested],
2022-10-25 17:23:08 +02:00
state?.retry,
),
);
if (!state) {
2022-01-25 14:29:29 +01:00
return <Loading />;
2021-07-13 20:33:28 +02:00
}
if (state.hasError) {
return (
<ErrorAlertView
error={alertFromError(
2023-01-09 12:38:48 +01:00
i18n.str`Could not load transaction information`,
state,
)}
2022-01-25 14:29:29 +01:00
/>
);
}
2022-01-25 14:29:29 +01:00
const currency = Amounts.parse(state.response.amountRaw)?.currency;
2021-11-15 15:18:58 +01:00
return (
<TransactionView
transaction={state.response}
2023-01-13 20:09:33 +01:00
onCancel={async () => {
2023-05-30 04:47:21 +02:00
await api.wallet.call(WalletApiOperation.CancelAbortingTransaction, {
2023-01-13 20:09:33 +01:00
transactionId,
});
goToWalletHistory(currency);
}}
2023-05-30 04:47:21 +02:00
onSuspend={async () => {
await api.wallet.call(WalletApiOperation.SuspendTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onResume={async () => {
2023-06-01 19:26:28 +02:00
await api.wallet.call(WalletApiOperation.ResumeTransaction, {
2023-05-30 04:47:21 +02:00
transactionId,
});
goToWalletHistory(currency);
}}
onAbort={async () => {
await api.wallet.call(WalletApiOperation.AbortTransaction, {
2022-10-25 17:23:08 +02:00
transactionId,
});
goToWalletHistory(currency);
}}
onRetry={async () => {
await api.wallet.call(WalletApiOperation.RetryTransaction, {
2022-10-25 17:23:08 +02:00
transactionId,
});
goToWalletHistory(currency);
}}
2023-05-30 04:47:21 +02:00
onDelete={async () => {
await api.wallet.call(WalletApiOperation.DeleteTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
2023-05-10 18:35:18 +02:00
onRefund={async (transactionId) => {
await api.wallet.call(WalletApiOperation.StartRefundQuery, {
transactionId,
2022-10-25 17:23:08 +02:00
});
}}
2022-01-25 14:29:29 +01:00
onBack={() => goToWalletHistory(currency)}
2021-11-15 15:18:58 +01:00
/>
);
}
export interface WalletTransactionProps {
2021-10-15 00:37:18 +02:00
transaction: Transaction;
2023-01-13 20:09:33 +01:00
onCancel: () => Promise<void>;
2023-05-30 04:47:21 +02:00
onSuspend: () => Promise<void>;
onResume: () => Promise<void>;
onAbort: () => Promise<void>;
2022-06-01 20:47:47 +02:00
onDelete: () => Promise<void>;
onRetry: () => Promise<void>;
onRefund: (id: TransactionIdStr) => Promise<void>;
2022-06-01 20:47:47 +02:00
onBack: () => Promise<void>;
}
const PurchaseDetailsTable = styled.table`
width: 100%;
& > tr > td:nth-child(2n) {
text-align: right;
}
`;
2023-01-13 20:09:33 +01:00
type TransactionTemplateProps = Omit<
Omit<WalletTransactionProps, "onRefund">,
"onBack"
> & {
children: ComponentChildren;
};
function TransactionTemplate({
2021-11-15 15:18:58 +01:00
transaction,
onDelete,
onRetry,
2023-05-30 04:47:21 +02:00
onAbort,
onResume,
onSuspend,
2023-01-13 20:09:33 +01:00
onCancel,
children,
}: TransactionTemplateProps): VNode {
const { i18n } = useTranslationContext();
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
2023-01-13 20:09:33 +01:00
const [confirmBeforeCancel, setConfirmBeforeCancel] = useState(false);
const { safely } = useAlertContext();
2023-06-02 19:32:52 +02:00
const [settings] = useSettings();
2021-12-06 14:31:19 +01:00
2022-06-01 20:47:47 +02:00
async function doCheckBeforeForget(): Promise<void> {
if (
2023-05-10 18:35:18 +02:00
transaction.txState.major === TransactionMajorState.Pending &&
transaction.type === TransactionType.Withdrawal
) {
setConfirmBeforeForget(true);
} else {
onDelete();
}
}
2021-12-06 14:31:19 +01:00
2023-01-13 20:09:33 +01:00
async function doCheckBeforeCancel(): Promise<void> {
setConfirmBeforeCancel(true);
2021-07-13 20:33:28 +02:00
}
2021-08-24 20:16:11 +02:00
const hasCancelTransactionImplemented =
transaction.type === TransactionType.Payment;
2023-05-30 04:47:21 +02:00
const hasAbortTransactionImplemented =
transaction.type === TransactionType.Withdrawal ||
transaction.type === TransactionType.Deposit ||
transaction.type === TransactionType.Payment;
2023-01-13 20:09:33 +01:00
2023-05-30 04:47:21 +02:00
const isFinalState =
transaction.txState.major === TransactionMajorState.Aborted ||
transaction.txState.major === TransactionMajorState.Done ||
transaction.txState.major === TransactionMajorState.Failed;
const showAbort =
hasAbortTransactionImplemented &&
transaction.txState.major === TransactionMajorState.Pending;
const showCancel =
hasCancelTransactionImplemented &&
transaction.txState.major === TransactionMajorState.Aborting;
const showRetry =
2023-06-01 19:26:28 +02:00
!isFinalState &&
2023-05-30 04:47:21 +02:00
transaction.txState.major !== TransactionMajorState.Pending &&
transaction.txState.major !== TransactionMajorState.Aborting;
const showDelete = isFinalState;
const showResume =
transaction.txState.major === TransactionMajorState.Suspended ||
transaction.txState.major === TransactionMajorState.SuspendedAborting;
const showSuspend =
transaction.txState.major === TransactionMajorState.Pending ||
transaction.txState.major === TransactionMajorState.Aborting;
2023-01-13 20:09:33 +01:00
return (
<Fragment>
<section style={{ padding: 8, textAlign: "center" }}>
{transaction?.error ? (
transaction.error.code === 7025 ? (
<AlertView
alert={{
type: "warning",
message: i18n.str`KYC check required for the transaction to complete`,
description:
transaction.error.kycUrl &&
typeof transaction.error.kycUrl === "string" ? (
<div>
<i18n.Translate>
Follow this link to the{` `}
<a href={transaction.error.kycUrl}>KYC verifier</a>
</i18n.Translate>
</div>
) : (
i18n.str`No more information has been provided`
),
}}
/>
) : (
<ErrorAlertView
error={alertFromError(
i18n.str`There was an error trying to complete the transaction`,
transaction.error,
)}
/>
)
) : undefined}
2023-05-10 18:35:18 +02:00
{transaction.txState.major === TransactionMajorState.Pending && (
2023-01-13 20:09:33 +01:00
<WarningBox>
<i18n.Translate>This transaction is not completed</i18n.Translate>
</WarningBox>
)}
2023-06-01 19:26:28 +02:00
{transaction.txState.major === TransactionMajorState.Aborted && (
<InfoBox>
<i18n.Translate>This transaction was aborted</i18n.Translate>
</InfoBox>
)}
{transaction.txState.major === TransactionMajorState.Failed && (
<ErrorBox>
<i18n.Translate>This transaction failed</i18n.Translate>
</ErrorBox>
)}
{confirmBeforeForget ? (
<Overlay>
<CenteredDialog>
2022-02-23 19:18:37 +01:00
<header>
<i18n.Translate>Caution!</i18n.Translate>
2022-02-23 19:18:37 +01:00
</header>
<section>
<i18n.Translate>
2022-02-23 19:18:37 +01:00
If you have already wired money to the exchange you will loose
the chance to get the coins form it.
</i18n.Translate>
</section>
<footer>
2022-06-01 20:47:47 +02:00
<Button
variant="contained"
color="secondary"
2023-01-13 20:09:33 +01:00
onClick={
(async () =>
setConfirmBeforeForget(false)) as SafeHandler<void>
}
2022-06-01 20:47:47 +02:00
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
2023-01-13 20:09:33 +01:00
<Button
variant="contained"
color="error"
onClick={safely("delete transaction", onDelete)}
2023-01-13 20:09:33 +01:00
>
<i18n.Translate>Confirm</i18n.Translate>
2022-06-01 20:47:47 +02:00
</Button>
</footer>
</CenteredDialog>
</Overlay>
) : undefined}
2023-01-13 20:09:33 +01:00
{confirmBeforeCancel ? (
<Overlay>
<CenteredDialog>
<header>
<i18n.Translate>Caution!</i18n.Translate>
</header>
<section>
<i18n.Translate>
2023-01-16 17:50:34 +01:00
Doing a cancellation while the transaction still active might
2023-01-13 20:09:33 +01:00
result in lost coins. Do you still want to cancel the
transaction?
</i18n.Translate>
</section>
<footer>
<Button
variant="contained"
color="secondary"
onClick={
(async () =>
setConfirmBeforeCancel(false)) as SafeHandler<void>
}
>
<i18n.Translate>No</i18n.Translate>
</Button>
<Button
variant="contained"
color="error"
onClick={safely("cancel active transaction", onCancel)}
2023-01-13 20:09:33 +01:00
>
<i18n.Translate>Yes</i18n.Translate>
</Button>
</footer>
</CenteredDialog>
</Overlay>
) : undefined}
</section>
<section>{children}</section>
<footer>
2023-05-30 04:47:21 +02:00
<div />
2023-01-13 20:09:33 +01:00
<div>
2023-05-30 04:47:21 +02:00
{showRetry && (
<Button
variant="contained"
onClick={safely("retry transaction", onRetry)}
>
<i18n.Translate>Retry</i18n.Translate>
</Button>
)}
{showAbort && (
2023-01-13 20:09:33 +01:00
<Button
variant="contained"
2023-05-30 04:47:21 +02:00
onClick={safely("abort transaction", onAbort)}
2023-01-13 20:09:33 +01:00
>
2023-05-30 04:47:21 +02:00
<i18n.Translate>Abort</i18n.Translate>
2023-01-13 20:09:33 +01:00
</Button>
2023-05-30 04:47:21 +02:00
)}
2023-06-02 19:32:52 +02:00
{showResume && settings.suspendIndividualTransaction && (
2023-01-13 20:09:33 +01:00
<Button
variant="contained"
2023-05-30 04:47:21 +02:00
onClick={safely("resume transaction", onResume)}
2023-01-13 20:09:33 +01:00
>
2023-05-30 04:47:21 +02:00
<i18n.Translate>Resume</i18n.Translate>
2023-01-13 20:09:33 +01:00
</Button>
2023-05-30 04:47:21 +02:00
)}
2023-06-02 19:32:52 +02:00
{showSuspend && settings.suspendIndividualTransaction && (
2023-05-30 04:47:21 +02:00
<Button
variant="contained"
onClick={safely("suspend transaction", onSuspend)}
>
<i18n.Translate>Suspend</i18n.Translate>
</Button>
)}
{showCancel && (
<Button
variant="contained"
color="error"
onClick={doCheckBeforeCancel as SafeHandler<void>}
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
)}
{showDelete && (
2023-01-13 20:09:33 +01:00
<Button
variant="contained"
color="error"
onClick={doCheckBeforeForget as SafeHandler<void>}
>
2023-04-19 17:42:47 +02:00
<i18n.Translate>Delete</i18n.Translate>
2023-01-13 20:09:33 +01:00
</Button>
)}
</div>
</footer>
</Fragment>
);
}
export function TransactionView({
transaction,
onDelete,
2023-05-30 04:47:21 +02:00
onAbort,
onBack,
onResume,
onSuspend,
2023-01-13 20:09:33 +01:00
onRetry,
onRefund,
onCancel,
}: WalletTransactionProps): VNode {
const { i18n } = useTranslationContext();
const { safely } = useAlertContext();
2023-01-20 19:44:53 +01:00
const raw = Amounts.parseOrThrow(transaction.amountRaw);
const effective = Amounts.parseOrThrow(transaction.amountEffective);
if (
transaction.type === TransactionType.Withdrawal ||
transaction.type === TransactionType.InternalWithdrawal
) {
2023-01-13 20:09:33 +01:00
return (
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Withdrawal`}
2023-01-20 19:44:53 +01:00
total={effective}
kind="positive"
>
{transaction.exchangeBaseUrl}
</Header>
2023-05-10 18:35:18 +02:00
{/**FIXME: DD37 check if this holds */}
{transaction.txState.major !==
2023-05-10 18:35:18 +02:00
TransactionMajorState.Pending ? undefined : transaction
.withdrawalDetails.type === WithdrawalType.ManualTransfer ? (
2023-02-13 13:28:42 +01:00
//manual withdrawal
<Fragment>
<BankDetailsByPaytoType
2023-01-20 19:44:53 +01:00
amount={raw}
exchangeBaseUrl={transaction.exchangeBaseUrl}
payto={parsePaytoUri(
transaction.withdrawalDetails.exchangePaytoUris[0],
)}
subject={transaction.withdrawalDetails.reservePub}
/>
2022-11-07 19:11:45 +01:00
<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>
2022-11-07 19:14:40 +01:00
<td width="100%" style={{ wordBreak: "break-all" }}>
2022-11-07 19:11:45 +01:00
{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>
) : (
2023-02-13 13:28:42 +01:00
//integrated bank withdrawal
<Fragment>
{!transaction.withdrawalDetails.confirmed &&
transaction.withdrawalDetails.bankConfirmationUrl ? (
<InfoBox>
<div style={{ display: "block" }}>
<i18n.Translate>
Wire transfer need a confirmation. Go to the{" "}
2022-02-23 19:18:37 +01:00
<a
href={transaction.withdrawalDetails.bankConfirmationUrl}
target="_blank"
rel="noreferrer"
style={{ display: "inline" }}
2022-02-23 19:18:37 +01:00
>
<i18n.Translate>bank site</i18n.Translate>
</a>{" "}
2022-11-18 17:26:48 +01:00
and check wire transfer operation to exchange account is
complete.
</i18n.Translate>
</div>
</InfoBox>
) : undefined}
2023-02-13 13:28:42 +01:00
{transaction.withdrawalDetails.confirmed &&
!transaction.withdrawalDetails.reserveIsReady && (
<InfoBox>
<i18n.Translate>
Bank has confirmed the wire transfer. Waiting for the
exchange to send the coins
</i18n.Translate>
</InfoBox>
)}
{transaction.withdrawalDetails.confirmed &&
transaction.withdrawalDetails.reserveIsReady && (
<InfoBox>
<i18n.Translate>
Exchange is ready to send the coins, withdrawal in progress.
</i18n.Translate>
</InfoBox>
)}
</Fragment>
)}
2021-11-15 15:18:58 +01:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2022-08-10 16:50:46 +02:00
text={
<WithdrawDetails
2023-01-20 19:44:53 +01:00
amount={getAmountWithFee(effective, raw, "credit")}
2022-08-10 16:50:46 +02:00
/>
}
2021-11-15 15:18:58 +01:00
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Payment) {
2022-05-14 23:09:33 +02:00
const pendingRefund =
transaction.refundPending === undefined
? undefined
: Amounts.parseOrThrow(transaction.refundPending);
2023-01-20 19:44:53 +01:00
const effectiveRefund = Amounts.parseOrThrow(
transaction.totalRefundEffective,
);
2021-11-15 15:18:58 +01:00
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onRetry={onRetry}
onCancel={onCancel}
>
<Header
timestamp={transaction.timestamp}
2023-01-20 19:44:53 +01:00
total={effective}
type={i18n.str`Payment`}
2021-11-15 15:18:58 +01:00
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
2023-01-09 12:38:48 +01:00
title={i18n.str`Refunds`}
text={
<table>
{transaction.refunds.map((r, i) => {
return (
<tr key={i}>
<td>
2022-09-13 16:07:39 +02:00
<i18n.Translate>
{<Amount value={r.amountEffective} />}{" "}
<a
href={Pages.balanceTransaction({
tid: r.transactionId,
})}
>
was refunded
</a>{" "}
on{" "}
{
<Time
timestamp={AbsoluteTime.fromProtocolTimestamp(
2022-09-13 16:07:39 +02:00
r.timestamp,
)}
format="dd MMMM yyyy"
/>
}
</i18n.Translate>
</td>
</tr>
);
})}
</table>
}
kind="neutral"
/>
) : undefined}
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
<InfoBox>
2023-02-20 17:24:24 +01:00
{transaction.refundQueryActive ? (
<i18n.Translate>Refund is in progress.</i18n.Translate>
) : (
<i18n.Translate>
Merchant created a refund for this order but was not
automatically picked up.
</i18n.Translate>
)}
2022-05-14 23:09:33 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Offer`}
text={<Amount value={pendingRefund} />}
2022-05-14 23:09:33 +02:00
kind="positive"
/>
2023-02-20 17:24:24 +01:00
{transaction.refundQueryActive ? undefined : (
<div>
2023-02-20 17:24:24 +01:00
<div />
<div>
<Button
variant="contained"
onClick={safely("refund transaction", () =>
onRefund(transaction.transactionId),
2023-02-20 17:24:24 +01:00
)}
>
<i18n.Translate>Accept</i18n.Translate>
</Button>
</div>
</div>
2023-02-20 17:24:24 +01:00
)}
</InfoBox>
2022-05-14 23:09:33 +02:00
)}
2023-04-24 19:53:01 +02:00
{transaction.posConfirmation ? (
<AlertView
alert={{
type: "info",
message: i18n.str`Confirmation code`,
description: <pre>{transaction.posConfirmation}</pre>,
2023-04-24 19:53:01 +02:00
}}
/>
) : undefined}
2022-02-23 19:18:37 +01:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Merchant`}
text={<MerchantDetails merchant={transaction.info.merchant} />}
2021-11-15 15:18:58 +01:00
kind="neutral"
/>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Invoice ID`}
text={transaction.info.orderId as TranslatedString}
2022-02-23 19:18:37 +01:00
kind="neutral"
/>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
text={
<PurchaseDetails
2023-01-20 19:44:53 +01:00
price={getAmountWithFee(effective, raw, "debit")}
effectiveRefund={effectiveRefund}
info={transaction.info}
proposalId={transaction.proposalId}
/>
}
2021-11-15 15:18:58 +01:00
kind="neutral"
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Deposit) {
2022-05-03 05:16:03 +02:00
const payto = parsePaytoUri(transaction.targetPaytoUri);
2023-01-15 21:49:57 +01:00
const wireTime = AbsoluteTime.fromProtocolTimestamp(
2023-01-15 21:49:57 +01:00
transaction.wireTransferDeadline,
);
const shouldBeWired = wireTime.t_ms !== "never" && isPast(wireTime.t_ms);
2021-11-15 15:18:58 +01:00
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Deposit`}
2023-01-20 19:44:53 +01:00
total={effective}
kind="negative"
>
{!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />}
</Header>
{payto && <PartPayto payto={payto} kind="neutral" />}
2021-11-15 15:18:58 +01:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2023-01-20 19:44:53 +01:00
text={
<DepositDetails
amount={getAmountWithFee(effective, raw, "debit")}
/>
}
2021-12-23 19:17:36 +01:00
kind="neutral"
2021-11-15 15:18:58 +01:00
/>
2023-01-15 21:49:57 +01:00
{!shouldBeWired ? (
<Part
title={i18n.str`Wire transfer deadline`}
text={
<Time timestamp={wireTime} format="dd MMMM yyyy 'at' HH:mm" />
}
kind="neutral"
/>
) : transaction.wireTransferProgress === 0 ? (
<AlertView
alert={{
type: "warning",
message: i18n.str`Wire transfer is not initiated`,
description: i18n.str` `,
}}
/>
) : transaction.wireTransferProgress === 100 ? (
<Fragment>
<AlertView
alert={{
type: "success",
message: i18n.str`Wire transfer completed`,
description: i18n.str` `,
}}
/>
<Part
title={i18n.str`Transfer details`}
text={
<TrackingDepositDetails
trackingState={transaction.trackingState}
/>
}
kind="neutral"
/>
</Fragment>
2023-01-15 21:49:57 +01:00
) : (
<AlertView
alert={{
type: "info",
message: i18n.str`Wire transfer in progress`,
description: i18n.str` `,
}}
/>
)}
2021-11-15 15:18:58 +01:00
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refresh) {
2021-11-15 15:18:58 +01:00
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refresh`}
2023-01-20 19:44:53 +01:00
total={effective}
2021-11-15 15:18:58 +01:00
kind="negative"
>
{"Refresh"}
</Header>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2023-01-20 19:44:53 +01:00
text={
<RefreshDetails
amount={getAmountWithFee(effective, raw, "debit")}
/>
}
2021-11-15 15:18:58 +01:00
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Tip) {
2021-11-15 15:18:58 +01:00
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Tip`}
2023-01-20 19:44:53 +01:00
total={effective}
kind="positive"
>
{transaction.merchantBaseUrl}
</Header>
{/* <Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Merchant`}
text={<MerchantDetails merchant={transaction.merchant} />}
kind="neutral"
/> */}
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2023-01-20 19:44:53 +01:00
text={
<TipDetails amount={getAmountWithFee(effective, raw, "credit")} />
}
2021-11-15 15:18:58 +01:00
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refund) {
2021-11-15 15:18:58 +01:00
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refund`}
2023-01-20 19:44:53 +01:00
total={effective}
2021-11-15 15:18:58 +01:00
kind="positive"
>
2023-05-22 15:40:13 +02:00
{transaction.paymentInfo ? (
2022-05-14 23:09:33 +02:00
<a
2022-06-02 17:20:36 +02:00
href={Pages.balanceTransaction({
tid: transaction.refundedTransactionId,
})}
2022-05-14 23:09:33 +02:00
>
2023-05-22 15:40:13 +02:00
{transaction.paymentInfo.summary}
2022-05-14 23:09:33 +02:00
</a>
2023-05-22 15:40:13 +02:00
) : (
<span style={{ color: "gray" }}>-- deleted --</span>
)}
</Header>
<Part
title={i18n.str`Merchant`}
text={
(transaction.paymentInfo
? transaction.paymentInfo.merchant.name
: "-- deleted --") as TranslatedString
2022-03-17 19:00:34 +01:00
}
2022-02-23 19:18:37 +01:00
kind="neutral"
/>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Purchase summary`}
2023-05-22 15:40:13 +02:00
text={
(transaction.paymentInfo
? transaction.paymentInfo.summary
: "-- deleted --") as TranslatedString
}
2021-11-15 15:18:58 +01:00
kind="neutral"
/>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2023-01-20 19:44:53 +01:00
text={
<RefundDetails
amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
2021-11-15 15:18:58 +01:00
</TransactionTemplate>
);
}
2022-08-31 05:20:35 +02:00
if (transaction.type === TransactionType.PeerPullCredit) {
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
2022-08-31 05:20:35 +02:00
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
2023-01-20 19:44:53 +01:00
total={effective}
2022-08-31 05:20:35 +02:00
kind="positive"
>
2022-09-13 16:07:39 +02:00
<i18n.Translate>Invoice</i18n.Translate>
2022-08-31 05:20:35 +02:00
</Header>
{transaction.info.summary ? (
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Subject`}
text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
2022-08-31 05:20:35 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Exchange`}
text={transaction.exchangeBaseUrl as TranslatedString}
2022-08-31 05:20:35 +02:00
kind="neutral"
/>
2023-05-10 18:35:18 +02:00
{transaction.txState.major === TransactionMajorState.Pending &&
!transaction.error && (
<Part
title={i18n.str`URI`}
text={<ShowQrWithCopy text={transaction.talerUri} />}
kind="neutral"
/>
)}
2022-08-31 05:20:35 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2022-08-31 05:20:35 +02:00
text={
<InvoiceDetails
2023-01-20 19:44:53 +01:00
amount={getAmountWithFee(effective, raw, "credit")}
2022-08-31 05:20:35 +02:00
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPullDebit) {
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
2022-08-31 05:20:35 +02:00
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
2023-01-20 19:44:53 +01:00
total={effective}
2022-08-31 05:20:35 +02:00
kind="negative"
>
2022-09-13 16:07:39 +02:00
<i18n.Translate>Invoice</i18n.Translate>
2022-08-31 05:20:35 +02:00
</Header>
{transaction.info.summary ? (
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Subject`}
text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
2022-08-31 05:20:35 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Exchange`}
text={transaction.exchangeBaseUrl as TranslatedString}
2022-08-31 05:20:35 +02:00
kind="neutral"
/>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2022-08-31 05:20:35 +02:00
text={
<InvoiceDetails
2023-01-20 19:44:53 +01:00
amount={getAmountWithFee(effective, raw, "debit")}
2022-08-31 05:20:35 +02:00
/>
}
/>
</TransactionTemplate>
);
}
2023-01-20 19:44:53 +01:00
2022-08-31 05:20:35 +02:00
if (transaction.type === TransactionType.PeerPushDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
2022-08-31 05:20:35 +02:00
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
total={total}
kind="negative"
>
2022-09-13 16:07:39 +02:00
<i18n.Translate>Transfer</i18n.Translate>
2022-08-31 05:20:35 +02:00
</Header>
{transaction.info.summary ? (
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Subject`}
text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
2022-08-31 05:20:35 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Exchange`}
text={transaction.exchangeBaseUrl as TranslatedString}
2022-08-31 05:20:35 +02:00
kind="neutral"
/>
2023-01-16 17:50:34 +01:00
{/* {transaction.pending && ( //pending is not-received
2022-09-16 21:03:58 +02:00
)} */}
2022-09-16 21:04:41 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`URI`}
2022-09-16 21:04:41 +02:00
text={<ShowQrWithCopy text={transaction.talerUri} />}
kind="neutral"
/>
2022-08-31 05:20:35 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2022-08-31 05:20:35 +02:00
text={
<TransferDetails
2023-01-20 19:44:53 +01:00
amount={getAmountWithFee(effective, raw, "debit")}
2022-08-31 05:20:35 +02:00
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPushCredit) {
return (
2023-01-13 20:09:33 +01:00
<TransactionTemplate
transaction={transaction}
onDelete={onDelete}
onRetry={onRetry}
2023-05-30 04:47:21 +02:00
onAbort={onAbort}
onResume={onResume}
onSuspend={onSuspend}
2023-01-13 20:09:33 +01:00
onCancel={onCancel}
>
2022-08-31 05:20:35 +02:00
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
2023-01-20 19:44:53 +01:00
total={effective}
2022-08-31 05:20:35 +02:00
kind="positive"
>
2022-09-13 16:07:39 +02:00
<i18n.Translate>Transfer</i18n.Translate>
2022-08-31 05:20:35 +02:00
</Header>
{transaction.info.summary ? (
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Subject`}
text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
2022-08-31 05:20:35 +02:00
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Exchange`}
text={transaction.exchangeBaseUrl as TranslatedString}
2022-08-31 05:20:35 +02:00
kind="neutral"
/>
<Part
2023-01-09 12:38:48 +01:00
title={i18n.str`Details`}
2022-08-31 05:20:35 +02:00
text={
<TransferDetails
2023-01-20 19:44:53 +01:00
amount={getAmountWithFee(effective, raw, "credit")}
2022-08-31 05:20:35 +02:00
/>
}
/>
</TransactionTemplate>
);
}
2023-01-20 19:44:53 +01:00
assertUnreachable(transaction);
}
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>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Date</i18n.Translate>
</td>
<td>
<Time
timestamp={AbsoluteTime.fromProtocolTimestamp(date)}
format="dd MMMM yyyy, HH:mm"
/>
</td>
</tr>
</Fragment>
)}
</PurchaseDetailsTable>
);
}
2022-08-10 16:50:46 +02:00
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 {
2023-01-20 19:44:53 +01:00
value: AmountJson;
fee: AmountJson;
total: AmountJson;
maxFrac: number;
}
2022-08-10 16:50:46 +02:00
2023-01-20 19:44:53 +01:00
export function getAmountWithFee(
effective: AmountJson,
raw: AmountJson,
direction: "credit" | "debit",
): AmountWithFee {
const fee =
direction === "credit"
? Amounts.sub(raw, effective).amount
: Amounts.sub(effective, raw).amount;
const maxFrac = [effective, raw, fee]
2022-08-31 05:20:35 +02:00
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
2023-01-20 19:44:53 +01:00
return {
total: effective,
value: raw,
fee,
maxFrac,
};
}
export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
2022-08-31 05:20:35 +02:00
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Invoice</i18n.Translate>
</td>
2022-08-31 05:20:35 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
2022-08-31 05:20:35 +02:00
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(amount.fee) && (
2022-08-31 05:20:35 +02:00
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
2022-08-31 05:20:35 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
2022-08-31 05:20:35 +02:00
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
2022-08-31 05:20:35 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
2022-08-31 05:20:35 +02:00
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Transfer</i18n.Translate>
</td>
2022-08-31 05:20:35 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
2022-08-31 05:20:35 +02:00
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(amount.fee) && (
2022-08-31 05:20:35 +02:00
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
2022-08-31 05:20:35 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
2022-08-31 05:20:35 +02:00
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
2022-08-31 05:20:35 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
2022-08-31 05:20:35 +02:00
</td>
</tr>
</PurchaseDetailsTable>
);
}
2022-08-10 16:50:46 +02:00
export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
2023-01-20 19:44:53 +01:00
const maxFrac = [amount.fee, amount.fee]
2022-08-10 16:50:46 +02:00
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
2023-01-20 19:44:53 +01:00
const total = Amounts.add(amount.value, amount.fee).amount;
2022-08-10 16:50:46 +02:00
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Withdraw</i18n.Translate>
</td>
2022-08-10 16:50:46 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
2022-08-10 16:50:46 +02:00
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(amount.fee) && (
2022-08-10 16:50:46 +02:00
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
2022-08-10 16:50:46 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
2022-08-10 16:50:46 +02:00
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
2022-08-10 16:50:46 +02:00
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
2022-08-10 16:50:46 +02:00
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function PurchaseDetails({
price,
2023-01-20 19:44:53 +01:00
effectiveRefund,
info,
proposalId,
}: {
price: AmountWithFee;
2023-01-20 19:44:53 +01:00
effectiveRefund?: AmountJson;
info: OrderShortInfo;
proposalId: string;
}): VNode {
const { i18n } = useTranslationContext();
2023-01-20 19:44:53 +01:00
const total = Amounts.add(price.value, price.fee).amount;
const hasProducts = info.products && info.products.length > 0;
const hasShipping =
info.delivery_date !== undefined || info.delivery_location !== undefined;
const showLargePic = (): void => {
return;
};
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Price</i18n.Translate>
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={price.value} />
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(price.fee) && (
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Transaction fees</i18n.Translate>
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={price.fee} />
</td>
</tr>
)}
2023-01-20 19:44:53 +01:00
{effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
<Fragment>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Subtotal</i18n.Translate>
</td>
<td>
<Amount value={price.total} />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Refunded</i18n.Translate>
</td>
<td>
<Amount value={effectiveRefund} negative />
</td>
</tr>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={Amounts.sub(total, effectiveRefund).amount} />
</td>
</tr>
</Fragment>
) : (
<Fragment>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={price.total} />
</td>
</tr>
</Fragment>
)}
{hasProducts && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
2023-01-09 12:38:48 +01:00
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
2023-01-09 12:38:48 +01:00
title={i18n.str`Delivery`}
text={
<DeliveryDetails
date={info.delivery_date}
location={info.delivery_location}
/>
}
/>
</td>
</tr>
)}
<tr>
<td>
<ShowFullContractTermPopup proposalId={proposalId} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
2023-01-20 19:44:53 +01:00
function RefundDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Refund</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(amount.fee) && (
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
2023-03-31 19:51:58 +02:00
type AmountAmountByWireTransferByWire = {
id: string;
amount: string;
}[];
function calculateAmountByWireTransfer(
state: TransactionDeposit["trackingState"],
): AmountAmountByWireTransferByWire {
const allTracking = Object.values(state ?? {});
//group tracking by wtid, sum amounts
const trackByWtid = allTracking.reduce((prev, cur) => {
const fee = Amounts.parseOrThrow(cur.wireFee);
const raw = Amounts.parseOrThrow(cur.amountRaw);
const total = !prev[cur.wireTransferId]
? raw
: Amounts.add(prev[cur.wireTransferId].total, raw).amount;
prev[cur.wireTransferId] = {
total,
fee,
};
return prev;
}, {} as Record<string, { total: AmountJson; fee: AmountJson }>);
//remove wire fee from total amount
return Object.entries(trackByWtid).map(([id, info]) => ({
id,
amount: Amounts.stringify(Amounts.sub(info.total, info.fee).amount),
}));
}
function TrackingDepositDetails({
trackingState,
}: {
trackingState: TransactionDeposit["trackingState"];
}): VNode {
const { i18n } = useTranslationContext();
2023-03-31 19:51:58 +02:00
const wireTransfers = calculateAmountByWireTransfer(trackingState);
return (
<PurchaseDetailsTable>
<tr>
<td>
<i18n.Translate>Transfer identification</i18n.Translate>
</td>
<td>
<i18n.Translate>Amount</i18n.Translate>
</td>
</tr>
{wireTransfers.map((wire) => (
<tr>
<td>{wire.id}</td>
<td>
<Amount value={wire.amount} />
</td>
</tr>
))}
</PurchaseDetailsTable>
);
}
2023-01-20 19:44:53 +01:00
function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Deposit</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(amount.fee) && (
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total transfer</i18n.Translate>
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
2023-01-20 19:44:53 +01:00
function RefreshDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Refresh</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td>
</tr>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
2023-01-20 19:44:53 +01:00
function TipDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Tip</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
2023-01-20 19:44:53 +01:00
{Amounts.isNonZero(amount.fee) && (
<tr>
2022-09-13 16:07:39 +02:00
<td>
2023-01-20 19:44:53 +01:00
<i18n.Translate>Fees</i18n.Translate>
2022-09-13 16:07:39 +02:00
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
2022-09-13 16:07:39 +02:00
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
2023-01-20 19:44:53 +01:00
<Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
function Header({
timestamp,
total,
children,
kind,
type,
}: {
timestamp: TalerPreciseTimestamp;
total: AmountJson;
children: ComponentChildren;
kind: Kind;
2023-01-09 12:38:48 +01:00
type: TranslatedString;
}): VNode {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "row",
}}
>
<div>
<SubTitle>{children}</SubTitle>
<Time
timestamp={AbsoluteTime.fromPreciseTimestamp(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>
2023-05-30 04:47:21 +02:00
<div>{"payto.account"}</div>
<SmallLightText>
<a href={url.href} target="_bank" rel="noreferrer">
2023-05-30 04:47:21 +02:00
{url.href}
</a>
</SmallLightText>
</Fragment>
);
}
case "iban": {
return <div>{payto.targetPath.substring(0, 20)}</div>;
}
}
}
return <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
}
2023-01-20 19:44:53 +01:00
function ShowQrWithCopy({ text }: { text: string }): VNode {
const [showing, setShowing] = useState(false);
const { i18n } = useTranslationContext();
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 as SafeHandler<void>}>
<i18n.Translate>copy</i18n.Translate>
</Button>
<Button onClick={toggle as SafeHandler<void>}>
<i18n.Translate>hide qr</i18n.Translate>
</Button>
</div>
);
}
return (
<div>
<div>{text.substring(0, 64)}...</div>
<Button onClick={copy as SafeHandler<void>}>
<i18n.Translate>copy</i18n.Translate>
</Button>
<Button onClick={toggle as SafeHandler<void>}>
<i18n.Translate>show qr</i18n.Translate>
</Button>
</div>
);
}