summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/Transaction.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx2060
1 files changed, 1262 insertions, 798 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index e70f5fbd1..1f0293352 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -18,76 +18,84 @@ import {
AbsoluteTime,
AmountJson,
Amounts,
- Location,
+ AmountString,
+ DenomLossEventType,
MerchantInfo,
NotificationType,
OrderShortInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
- TalerProtocolTimestamp,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
Transaction,
+ TransactionAction,
TransactionDeposit,
- TransactionRefresh,
- TransactionRefund,
- TransactionTip,
+ TransactionIdStr,
+ TransactionInternalWithdrawal,
+ TransactionMajorState,
+ TransactionMinorState,
TransactionType,
+ TransactionWithdrawal,
+ TranslatedString,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
-import { differenceInSeconds } from "date-fns";
+import { isPast } 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 { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
+import { AlertView, ErrorAlertView } from "../components/CurrentAlerts.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Loading } from "../components/Loading.js";
-import { LoadingError } from "../components/LoadingError.js";
-import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
+import { Kind, Part, PartPayto } from "../components/Part.js";
import { QR } from "../components/QR.js";
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
import {
CenteredDialog,
+ ErrorBox,
InfoBox,
- ListOfProducts,
+ Link,
Overlay,
- Row,
SmallLightText,
SubTitle,
+ SvgIcon,
WarningBox,
} from "../components/styled/index.js";
import { Time } from "../components/Time.js";
-import { useTranslationContext } from "../context/translation.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
+import { SafeHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js";
-import { wxApi } from "../wxApi.js";
+import refreshIcon from "../svg/refresh_24px.inline.svg";
+import { assertUnreachable } from "../utils/index.js";
interface Props {
tid: string;
goToWalletHistory: (currency?: string) => Promise<void>;
}
-export function TransactionPage({
- tid: transactionId,
- goToWalletHistory,
-}: Props): VNode {
+export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
+ const transactionId = tid as TransactionIdStr; //FIXME: validate
const { i18n } = useTranslationContext();
-
+ const api = useBackendContext();
const state = useAsyncAsHook(
() =>
- wxApi.wallet.call(WalletApiOperation.GetTransactionById, {
+ api.wallet.call(WalletApiOperation.GetTransactionById, {
transactionId,
}),
[transactionId],
);
useEffect(() =>
- wxApi.listener.onUpdateNotification(
- [NotificationType.WithdrawGroupFinished],
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
state?.retry,
),
);
@@ -98,13 +106,12 @@ export function TransactionPage({
if (state.hasError) {
return (
- <LoadingError
- title={
- <i18n.Translate>
- Could not load the transaction information
- </i18n.Translate>
- }
- error={state}
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load transaction information`,
+ state,
+ )}
/>
);
}
@@ -114,24 +121,45 @@ export function TransactionPage({
return (
<TransactionView
transaction={state.response}
- onSend={async () => {
- null;
+ onCancel={async () => {
+ await api.wallet.call(WalletApiOperation.FailTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
}}
- onDelete={async () => {
- await wxApi.wallet.call(WalletApiOperation.DeleteTransaction, {
+ onSuspend={async () => {
+ await api.wallet.call(WalletApiOperation.SuspendTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onResume={async () => {
+ await api.wallet.call(WalletApiOperation.ResumeTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onAbort={async () => {
+ await api.wallet.call(WalletApiOperation.AbortTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onRetry={async () => {
- await wxApi.wallet.call(WalletApiOperation.RetryTransaction, {
+ await api.wallet.call(WalletApiOperation.RetryTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
- onRefund={async (purchaseId) => {
- await wxApi.wallet.call(WalletApiOperation.ApplyRefundFromPurchaseId, {
- purchaseId,
+ onDelete={async () => {
+ await api.wallet.call(WalletApiOperation.DeleteTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onRefund={async (transactionId) => {
+ await api.wallet.call(WalletApiOperation.StartRefundQuery, {
+ transactionId,
});
}}
onBack={() => goToWalletHistory(currency)}
@@ -141,10 +169,13 @@ export function TransactionPage({
export interface WalletTransactionProps {
transaction: Transaction;
- onSend: () => Promise<void>;
+ onCancel: () => Promise<void>;
+ onSuspend: () => Promise<void>;
+ onResume: () => Promise<void>;
+ onAbort: () => Promise<void>;
onDelete: () => Promise<void>;
onRetry: () => Promise<void>;
- onRefund: (id: string) => Promise<void>;
+ onRefund: (id: TransactionIdStr) => Promise<void>;
onBack: () => Promise<void>;
}
@@ -156,18 +187,32 @@ const PurchaseDetailsTable = styled.table`
}
`;
-export function TransactionView({
+type TransactionTemplateProps = Omit<
+ Omit<WalletTransactionProps, "onRefund">,
+ "onBack"
+> & {
+ children: ComponentChildren;
+};
+
+function TransactionTemplate({
transaction,
onDelete,
onRetry,
- onSend,
- onRefund,
-}: WalletTransactionProps): VNode {
+ onAbort,
+ onResume,
+ onSuspend,
+ onCancel,
+ children,
+}: TransactionTemplateProps): VNode {
+ const { i18n } = useTranslationContext();
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
+ const [confirmBeforeCancel, setConfirmBeforeCancel] = useState(false);
+ const { safely } = useAlertContext();
+ const [settings] = useSettings();
async function doCheckBeforeForget(): Promise<void> {
if (
- transaction.pending &&
+ transaction.txState.major === TransactionMajorState.Pending &&
transaction.type === TransactionType.Withdrawal
) {
setConfirmBeforeForget(true);
@@ -176,76 +221,87 @@ export function TransactionView({
}
}
- const SHOWING_RETRY_THRESHOLD_SECS = 30;
-
- const { i18n } = useTranslationContext();
+ async function doCheckBeforeCancel(): Promise<void> {
+ setConfirmBeforeCancel(true);
+ }
- 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);
+ const showButton = getShowButtonStates(transaction);
- return (
- <Fragment>
- <section style={{ padding: 8, textAlign: "center" }}>
- <ErrorTalerOperation
- title={
+ return (
+ <Fragment>
+ <section style={{ padding: 8, textAlign: "center" }}>
+ {transaction?.error &&
+ // FIXME: wallet core should stop sending this error on KYC
+ transaction.error.code !==
+ TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`There was an error trying to complete the transaction.`,
+ transaction.error,
+ )}
+ />
+ ) : undefined}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ (transaction.txState.minor === TransactionMinorState.KycRequired ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`KYC check required for the transaction to complete.`,
+ description:
+ transaction.kycUrl &&
+ typeof transaction.kycUrl === "string" ? (
+ <div>
+ <i18n.Translate>
+ Follow this link to the{` `}
+ <a
+ rel="noreferrer"
+ target="_bank"
+ href={transaction.kycUrl}
+ >
+ KYC verifier.
+ </a>
+ </i18n.Translate>
+ </div>
+ ) : (
+ i18n.str`No additional information has been provided.`
+ ),
+ }}
+ />
+ ) : transaction.txState.minor ===
+ TransactionMinorState.AmlRequired ? (
+ <WarningBox>
<i18n.Translate>
- There was an error trying to complete the transaction
+ The transaction has been blocked since the account required an
+ AML check.
</i18n.Translate>
- }
- error={transaction?.error}
- />
- {transaction.pending && (
+ </WarningBox>
+ ) : (
<WarningBox>
- <i18n.Translate>This transaction is not completed</i18n.Translate>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This transaction is not completed
+ </i18n.Translate>
+ <Link onClick={onRetry} style={{ padding: 0 }}>
+ <SvgIcon
+ title={i18n.str`Retry`}
+ dangerouslySetInnerHTML={{ __html: refreshIcon }}
+ color="black"
+ />
+ </Link>
+ </div>
</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>
+ ))}
+ {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>
@@ -262,118 +318,205 @@ export function TransactionView({
<Button
variant="contained"
color="secondary"
- onClick={async () => setConfirmBeforeForget(false)}
+ onClick={
+ (async () =>
+ setConfirmBeforeForget(false)) as SafeHandler<void>
+ }
>
<i18n.Translate>Cancel</i18n.Translate>
</Button>
- <Button variant="contained" color="error" onClick={onDelete}>
+ <Button
+ variant="contained"
+ color="error"
+ onClick={safely("delete transaction", onDelete)}
+ >
<i18n.Translate>Confirm</i18n.Translate>
</Button>
</footer>
</CenteredDialog>
</Overlay>
) : undefined}
+ {confirmBeforeCancel ? (
+ <Overlay>
+ <CenteredDialog>
+ <header>
+ <i18n.Translate>Caution!</i18n.Translate>
+ </header>
+ <section>
+ <i18n.Translate>
+ Doing a cancellation while the transaction still active might
+ 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)}
+ >
+ <i18n.Translate>Yes</i18n.Translate>
+ </Button>
+ </footer>
+ </CenteredDialog>
+ </Overlay>
+ ) : undefined}
+ </section>
+ <section>{children}</section>
+ <footer>
+ <div />
+ <div>
+ {showButton.abort && (
+ <Button
+ variant="contained"
+ onClick={safely("abort transaction", onAbort)}
+ >
+ <i18n.Translate>Abort</i18n.Translate>
+ </Button>
+ )}
+ {showButton.resume && settings.suspendIndividualTransaction && (
+ <Button
+ variant="contained"
+ onClick={safely("resume transaction", onResume)}
+ >
+ <i18n.Translate>Resume</i18n.Translate>
+ </Button>
+ )}
+ {showButton.suspend && settings.suspendIndividualTransaction && (
+ <Button
+ variant="contained"
+ onClick={safely("suspend transaction", onSuspend)}
+ >
+ <i18n.Translate>Suspend</i18n.Translate>
+ </Button>
+ )}
+ {showButton.fail && (
+ <Button
+ variant="contained"
+ color="error"
+ onClick={doCheckBeforeCancel as SafeHandler<void>}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ )}
+ {showButton.remove && (
+ <Button
+ variant="contained"
+ color="error"
+ onClick={doCheckBeforeForget as SafeHandler<void>}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </Button>
+ )}
+ </div>
+ </footer>
+ </Fragment>
+ );
+}
+
+export function TransactionView({
+ transaction,
+ onDelete,
+ onAbort,
+ // onBack,
+ onResume,
+ onSuspend,
+ onRetry,
+ onRefund,
+ onCancel,
+}: WalletTransactionProps): VNode {
+ const { i18n } = useTranslationContext();
+ const { safely } = useAlertContext();
+
+ const raw = Amounts.parseOrThrow(transaction.amountRaw);
+ const effective = Amounts.parseOrThrow(transaction.amountEffective);
+
+ if (
+ transaction.type === TransactionType.Withdrawal ||
+ transaction.type === TransactionType.InternalWithdrawal
+ ) {
+ // const conversion =
+ // transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ // ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ // : [];
+ const blockedByKycOrAml =
+ transaction.txState.minor === TransactionMinorState.KycRequired ||
+ transaction.txState.minor === TransactionMinorState.AmlRequired;
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Withdrawal`}
- total={total}
+ total={effective}
kind="positive"
>
{transaction.exchangeBaseUrl}
</Header>
- {!transaction.pending ? undefined : transaction.withdrawalDetails
- .type === WithdrawalType.ManualTransfer ? (
+ {transaction.txState.major !== TransactionMajorState.Pending ||
+ blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
+ WithdrawalType.ManualTransfer &&
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
<Fragment>
- <BankDetailsByPaytoType
- amount={chosen}
- exchangeBaseUrl={transaction.exchangeBaseUrl}
- payto={parsePaytoUri(
- transaction.withdrawalDetails.exchangePaytoUris[0],
+ <InfoBox>
+ {transaction.withdrawalDetails.exchangeCreditAccountDetails
+ .length > 1 ? (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Select one of the
+ accounts and use the information below to complete the
+ operation by making a wire transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ ) : (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Use the
+ information below to complete the operation by making a wire
+ transfer from your bank account.
+ </i18n.Translate>
+ </span>
)}
+ </InfoBox>
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
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>
+ //integrated bank withdrawal
+ <ShowWithdrawalDetailForBankIntegrated transaction={transaction} />
)}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
<WithdrawDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
@@ -387,21 +530,23 @@ export function TransactionView({
? 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;
+ const effectiveRefund = Amounts.parseOrThrow(
+ transaction.totalRefundEffective,
+ );
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onRetry={onRetry}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
- total={total}
+ total={effective}
type={i18n.str`Payment`}
kind="negative"
>
@@ -420,7 +565,7 @@ export function TransactionView({
<br />
{transaction.refunds.length > 0 ? (
<Part
- title={<i18n.Translate>Refunds</i18n.Translate>}
+ title={i18n.str`Refunds`}
text={
<table>
{transaction.refunds.map((r, i) => {
@@ -439,12 +584,13 @@ export function TransactionView({
on{" "}
{
<Time
- timestamp={AbsoluteTime.fromTimestamp(
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
r.timestamp,
)}
format="dd MMMM yyyy"
/>
}
+ .
</i18n.Translate>
</td>
</tr>
@@ -457,225 +603,260 @@ export function TransactionView({
) : undefined}
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
<InfoBox>
- <i18n.Translate>
- Merchant created a refund for this order but was not automatically
- picked up.
- </i18n.Translate>
+ {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>
+ )}
<Part
- title={<i18n.Translate>Offer</i18n.Translate>}
+ title={i18n.str`Offer`}
text={<Amount value={pendingRefund} />}
kind="positive"
/>
- <div>
- <div />
+ {transaction.refundQueryActive ? undefined : (
<div>
- <Button
- variant="contained"
- onClick={() => onRefund(transaction.proposalId)}
- >
- <i18n.Translate>Accept</i18n.Translate>
- </Button>
+ <div />
+ <div>
+ <Button
+ variant="contained"
+ onClick={safely("refund transaction", () =>
+ onRefund(transaction.transactionId),
+ )}
+ >
+ <i18n.Translate>Accept</i18n.Translate>
+ </Button>
+ </div>
</div>
- </div>
+ )}
</InfoBox>
)}
+ {transaction.posConfirmation ? (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Confirmation code`,
+ description: <pre>{transaction.posConfirmation}</pre>,
+ }}
+ />
+ ) : undefined}
<Part
- title={<i18n.Translate>Merchant</i18n.Translate>}
+ title={i18n.str`Merchant`}
text={<MerchantDetails merchant={transaction.info.merchant} />}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Invoice ID</i18n.Translate>}
- text={transaction.info.orderId}
+ title={i18n.str`Invoice ID`}
+ text={transaction.info.orderId as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
<PurchaseDetails
- price={price}
- refund={refund}
+ price={getAmountWithFee(effective, raw, "debit")}
+ effectiveRefund={effectiveRefund}
info={transaction.info}
- proposalId={transaction.proposalId}
/>
}
kind="neutral"
/>
+ <ShowFullContractTermPopup transactionId={transaction.transactionId} />
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Deposit) {
- const total = Amounts.parseOrThrow(transaction.amountRaw);
const payto = parsePaytoUri(transaction.targetPaytoUri);
+
+ const wireTime = AbsoluteTime.fromProtocolTimestamp(
+ transaction.wireTransferDeadline,
+ );
+ const shouldBeWired = wireTime.t_ms !== "never" && isPast(wireTime.t_ms);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Deposit`}
- total={total}
+ total={effective}
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"
- />
- <Part
- title={<i18n.Translate>Wire transfer deadline</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <Time
- timestamp={AbsoluteTime.fromTimestamp(
- transaction.wireTransferDeadline,
- )}
- format="dd MMMM yyyy 'at' HH:mm"
+ <DepositDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
/>
}
kind="neutral"
/>
+ {!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>
+ ) : (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Wire transfer in progress.`,
+ description: i18n.str` `,
+ }}
+ />
+ )}
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refresh) {
- const total = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount;
-
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refresh`}
- total={total}
+ total={effective}
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}
+ {"Refresh"}
</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} />}
+ title={i18n.str`Details`}
+ text={
+ <RefreshDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refund) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Refund`}
- total={total}
+ total={effective}
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={
+ {transaction.paymentInfo ? (
<a
href={Pages.balanceTransaction({
tid: transaction.refundedTransactionId,
})}
>
- {transaction.info.orderId}
+ {transaction.paymentInfo.summary}
</a>
+ ) : (
+ <span style={{ color: "gray" }}>-- deleted --</span>
+ )}
+ </Header>
+
+ <Part
+ title={i18n.str`Merchant`}
+ text={
+ (transaction.paymentInfo
+ ? transaction.paymentInfo.merchant.name
+ : "-- deleted --") as TranslatedString
}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Purchase summary</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Purchase summary`}
+ text={
+ (transaction.paymentInfo
+ ? transaction.paymentInfo.summary
+ : "-- deleted --") as TranslatedString
+ }
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
- text={<RefundDetails transaction={transaction} />}
+ title={i18n.str`Details`}
+ text={
+ <RefundDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
/>
</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>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
- total={total}
+ total={effective}
kind="positive"
>
<i18n.Translate>Invoice</i18n.Translate>
@@ -683,31 +864,31 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
- {transaction.pending /** pending is not-pay */ && (
- <Part
- title={<i18n.Translate>URI</i18n.Translate>}
- text={<ShowQrWithCopy text={transaction.talerUri} />}
- kind="neutral"
- />
- )}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ transaction.txState.minor === TransactionMinorState.Ready &&
+ transaction.talerUri &&
+ !transaction.error && (
+ <Part
+ title={i18n.str`URI`}
+ text={<ShowQrWithCopy text={transaction.talerUri} />}
+ kind="neutral"
+ />
+ )}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <InvoiceDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <InvoiceCreationDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
@@ -716,13 +897,20 @@ export function TransactionView({
}
if (transaction.type === TransactionType.PeerPullDebit) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
- total={total}
+ total={effective}
kind="negative"
>
<i18n.Translate>Invoice</i18n.Translate>
@@ -730,34 +918,40 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <InvoiceDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <InvoicePaymentDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
/>
}
/>
</TransactionTemplate>
);
}
+
if (transaction.type === TransactionType.PeerPushDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
@@ -769,31 +963,28 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
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} />}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
+ {transaction.talerUri && (
+ <Part
+ title={i18n.str`URI`}
+ text={<ShowQrWithCopy text={transaction.talerUri} />}
+ kind="neutral"
+ />
+ )}
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <TransferDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <TransferCreationDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
/>
}
/>
@@ -802,13 +993,20 @@ export function TransactionView({
}
if (transaction.type === TransactionType.PeerPushCredit) {
- const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
- <TransactionTemplate>
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
- total={total}
+ total={effective}
kind="positive"
>
<i18n.Translate>Transfer</i18n.Translate>
@@ -816,31 +1014,135 @@ export function TransactionView({
{transaction.info.summary ? (
<Part
- title={<i18n.Translate>Subject</i18n.Translate>}
- text={transaction.info.summary}
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
kind="neutral"
/>
) : undefined}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={transaction.exchangeBaseUrl}
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Details</i18n.Translate>}
+ title={i18n.str`Details`}
text={
- <TransferDetails
- amount={{
- effective: Amounts.parseOrThrow(transaction.amountEffective),
- raw: Amounts.parseOrThrow(transaction.amountRaw),
- }}
+ <TransferPickupDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/>
</TransactionTemplate>
);
}
- return <div />;
+
+ if (transaction.type === TransactionType.DenomLoss) {
+ switch (transaction.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination expired.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination vanished.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination is unoffered.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ default: {
+ assertUnreachable(transaction.lossEventType);
+ }
+ }
+ }
+ if (transaction.type === TransactionType.Recoup) {
+ throw Error("recoup transaction not implemented");
+ }
+ assertUnreachable(transaction);
}
export function MerchantDetails({
@@ -883,127 +1185,6 @@ export function MerchantDetails({
);
}
-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>
@@ -1017,19 +1198,40 @@ export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
}
export interface AmountWithFee {
- effective: AmountJson;
- raw: AmountJson;
+ value: AmountJson;
+ fee: AmountJson;
+ total: AmountJson;
+ maxFrac: number;
}
-export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
- const { i18n } = useTranslationContext();
-
- const fee = Amounts.sub(amount.raw, amount.effective).amount;
+export function getAmountWithFee(
+ effective: AmountJson,
+ raw: AmountJson,
+ direction: "credit" | "debit",
+): AmountWithFee {
+ const total = direction === "credit" ? effective : raw;
+ const value = direction === "debit" ? effective : raw;
+ const fee = Amounts.sub(value, total).amount;
- const maxFrac = [amount.raw, amount.effective, fee]
+ const maxFrac = [effective, raw, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
+ return {
+ total,
+ value,
+ fee,
+ maxFrac,
+ };
+}
+
+export function InvoiceCreationDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
return (
<PurchaseDetailsTable>
<tr>
@@ -1037,164 +1239,264 @@ export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Invoice</i18n.Translate>
</td>
<td>
- <Amount value={amount.raw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <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 {
+export function InvoicePaymentDetails({
+ 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>
+ <i18n.Translate>Invoice</i18n.Translate>
</td>
<td>
- <Amount value={amount.raw} maxFracSize={maxFrac} />
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
+ </PurchaseDetailsTable>
+ );
+}
+
+export function TransferCreationDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Total</i18n.Translate>
+ <i18n.Translate>Sent</i18n.Translate>
</td>
<td>
- <Amount value={amount.effective} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</PurchaseDetailsTable>
);
}
-export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
+export function TransferPickupDetails({
+ 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>
+ <i18n.Translate>Transfer</i18n.Translate>
</td>
<td>
- <Amount value={amount.raw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function WithdrawDetails({
+ conversion,
+ amount,
+}: {
+ conversion?: AmountJson;
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ {conversion ? (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={conversion} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ {conversion.fraction === amount.value.fraction &&
+ conversion.value === amount.value.value ? undefined : (
+ <tr>
+ <td>
+ <i18n.Translate>Converted</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ ) : (
<tr>
<td>
- <i18n.Translate>Transaction fees</i18n.Translate>
+ <i18n.Translate>Transfer</i18n.Translate>
</td>
<td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.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>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</PurchaseDetailsTable>
);
}
export function PurchaseDetails({
price,
- refund,
- info,
- proposalId,
+ effectiveRefund,
+ info: _info,
}: {
price: AmountWithFee;
- refund?: AmountWithFee;
+ effectiveRefund?: AmountJson;
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;
+ const total = Amounts.add(price.value, price.fee).amount;
return (
<PurchaseDetailsTable>
@@ -1203,49 +1505,82 @@ export function PurchaseDetails({
<i18n.Translate>Price</i18n.Translate>
</td>
<td>
- <Amount value={price.raw} />
+ <Amount value={price.total} />
</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>
+ {Amounts.isNonZero(price.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.fee} />
+ </td>
+ </tr>
+ {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.value} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={total} />
- </td>
- </tr>
- {hasProducts && (
+
+ {/* {hasProducts && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
- title={<i18n.Translate>Products</i18n.Translate>}
+ title={i18n.str`Products`}
text={
<ListOfProducts>
{info.products?.map((p, k) => (
@@ -1268,13 +1603,13 @@ export function PurchaseDetails({
/>
</td>
</tr>
- )}
- {hasShipping && (
+ )} */}
+ {/* {hasShipping && (
<tr>
<td colSpan={2}>
<PartCollapsible
big
- title={<i18n.Translate>Delivery</i18n.Translate>}
+ title={i18n.str`Delivery`}
text={
<DeliveryDetails
date={info.delivery_date}
@@ -1284,202 +1619,185 @@ export function PurchaseDetails({
/>
</td>
</tr>
- )}
- <tr>
- <td>
- <ShowFullContractTermPopup proposalId={proposalId} />
- </td>
- </tr>
+ )} */}
</PurchaseDetailsTable>
);
}
-function RefundDetails({
- transaction,
-}: {
- transaction: TransactionRefund;
-}): VNode {
+function RefundDetails({ amount }: { amount: AmountWithFee }): 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>
+ <i18n.Translate>Refund</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
- <td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} negative maxFracSize={maxFrac} />
- </td>
- </tr>
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <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,
+type AmountAmountByWireTransferByWire = {
+ id: string;
+ amount: AmountString;
+}[];
+
+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,
}: {
- transaction: TransactionDeposit;
+ trackingState: TransactionDeposit["trackingState"];
}): 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);
+ const wireTransfers = calculateAmountByWireTransfer(trackingState);
return (
<PurchaseDetailsTable>
<tr>
<td>
- <i18n.Translate>Amount</i18n.Translate>
+ <i18n.Translate>Transfer identification</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
+ <i18n.Translate>Amount</i18n.Translate>
</td>
</tr>
- {Amounts.isNonZero(fee) && (
- <tr>
+ {wireTransfers.map((wire) => (
+ <tr key={wire.id}>
+ <td>{wire.id}</td>
<td>
- <i18n.Translate>Transaction fees</i18n.Translate>
- </td>
- <td>
- <Amount value={fee} maxFracSize={maxFrac} />
+ <Amount value={wire.amount} />
</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);
+function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
+ const { i18n } = useTranslationContext();
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>
+ <i18n.Translate>Sent</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</PurchaseDetailsTable>
);
}
-function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
+function RefreshDetails({ amount }: { amount: AmountWithFee }): 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>
+ <i18n.Translate>Refresh</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountRaw} maxFracSize={maxFrac} />
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.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 />
@@ -1490,7 +1808,7 @@ function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
- <Amount value={transaction.amountEffective} maxFracSize={maxFrac} />
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
@@ -1504,11 +1822,11 @@ function Header({
kind,
type,
}: {
- timestamp: TalerProtocolTimestamp;
+ timestamp: TalerPreciseTimestamp;
total: AmountJson;
children: ComponentChildren;
kind: Kind;
- type: string;
+ type: TranslatedString;
}): VNode {
return (
<div
@@ -1521,7 +1839,7 @@ function Header({
<div>
<SubTitle>{children}</SubTitle>
<Time
- timestamp={AbsoluteTime.fromTimestamp(timestamp)}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(timestamp)}
format="dd MMMM yyyy, HH:mm"
/>
</div>
@@ -1548,10 +1866,10 @@ function NicePayto({ payto }: { payto: PaytoUri }): VNode {
const url = new URL("/", `https://${payto.host}`);
return (
<Fragment>
- <div>{payto.account}</div>
+ <div>{"payto.account"}</div>
<SmallLightText>
<a href={url.href} target="_bank" rel="noreferrer">
- {url.toString()}
+ {url.href}
</a>
</SmallLightText>
</Fragment>
@@ -1564,3 +1882,149 @@ function NicePayto({ payto }: { payto: PaytoUri }): VNode {
}
return <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
}
+
+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>
+ );
+}
+
+function getShowButtonStates(transaction: Transaction) {
+ let abort = false;
+ let fail = false;
+ let resume = false;
+ let remove = false;
+ let suspend = false;
+
+ transaction.txActions.forEach((a) => {
+ switch (a) {
+ case TransactionAction.Delete:
+ remove = true;
+ break;
+ case TransactionAction.Suspend:
+ suspend = true;
+ break;
+ case TransactionAction.Resume:
+ resume = true;
+ break;
+ case TransactionAction.Abort:
+ abort = true;
+ break;
+ case TransactionAction.Fail:
+ fail = true;
+ break;
+ case TransactionAction.Retry:
+ break;
+ default:
+ assertUnreachable(a);
+ break;
+ }
+ });
+ return { abort, fail, resume, remove, suspend };
+}
+
+function ShowWithdrawalDetailForBankIntegrated({
+ transaction,
+}: {
+ transaction: TransactionWithdrawal | TransactionInternalWithdrawal;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [showDetails, setShowDetails] = useState(false);
+ if (
+ transaction.txState.major !== TransactionMajorState.Pending ||
+ transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ) {
+ return <Fragment />;
+ }
+ const raw = Amounts.parseOrThrow(transaction.amountRaw);
+ return (
+ <Fragment>
+ <EnabledBySettings name="advancedMode">
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowDetails(!showDetails);
+ }}
+ >
+ Show details.
+ </a>
+ </EnabledBySettings>
+
+ {showDetails && (
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
+ subject={transaction.withdrawalDetails.reservePub}
+ />
+ )}
+ {!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 &&
+ !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>
+ );
+}