fix wrong fee calculation

This commit is contained in:
Sebastian 2023-01-20 15:44:53 -03:00
parent 5f31dad2d3
commit 03b12d2b27
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
6 changed files with 260 additions and 283 deletions

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts } from "@gnu-taler/taler-util";
import { format } from "date-fns"; import { format } from "date-fns";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { LogoHeader } from "../../components/LogoHeader.js"; import { LogoHeader } from "../../components/LogoHeader.js";
@ -27,7 +28,11 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js"; import { TextField } from "../../mui/TextField.js";
import editIcon from "../../svg/edit_24px.svg"; import editIcon from "../../svg/edit_24px.svg";
import { ExchangeDetails, InvoiceDetails } from "../../wallet/Transaction.js"; import {
ExchangeDetails,
getAmountWithFee,
InvoiceDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js"; import { State } from "./index.js";
export function ReadyView({ export function ReadyView({
@ -144,10 +149,7 @@ export function ReadyView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<InvoiceDetails <InvoiceDetails
amount={{ amount={getAmountWithFee(toBeReceived, requestAmount, "credit")}
effective: toBeReceived,
raw: requestAmount,
}}
/> />
} }
/> />

View File

@ -27,7 +27,11 @@ import { PaymentButtons } from "../../components/PaymentButtons.js";
import { SuccessBox, WarningBox } from "../../components/styled/index.js"; import { SuccessBox, WarningBox } from "../../components/styled/index.js";
import { Time } from "../../components/Time.js"; import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js"; import {
getAmountWithFee,
MerchantDetails,
PurchaseDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js"; import { State } from "./index.js";
type SupportedStates = type SupportedStates =
@ -41,13 +45,10 @@ export function BaseView(state: SupportedStates): VNode {
const contractTerms: ContractTerms = state.payStatus.contractTerms; const contractTerms: ContractTerms = state.payStatus.contractTerms;
const price = { const effective =
raw: state.amount,
effective:
"amountEffective" in state.payStatus "amountEffective" in state.payStatus
? Amounts.parseOrThrow(state.payStatus.amountEffective) ? Amounts.parseOrThrow(state.payStatus.amountEffective)
: state.amount, : state.amount;
};
return ( return (
<Fragment> <Fragment>
@ -68,7 +69,7 @@ export function BaseView(state: SupportedStates): VNode {
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<PurchaseDetails <PurchaseDetails
price={price} price={getAmountWithFee(effective, state.amount, "debit")}
info={{ info={{
...contractTerms, ...contractTerms,
orderId: contractTerms.order_id, orderId: contractTerms.order_id,

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts } from "@gnu-taler/taler-util";
import { format } from "date-fns"; import { format } from "date-fns";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
@ -23,7 +24,7 @@ import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js"; import { TextField } from "../../mui/TextField.js";
import { TransferDetails } from "../../wallet/Transaction.js"; import { getAmountWithFee, TransferDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js"; import { State } from "./index.js";
export function ReadyView({ export function ReadyView({
@ -114,10 +115,7 @@ export function ReadyView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<TransferDetails <TransferDetails
amount={{ amount={getAmountWithFee(debitAmount, toBeReceived, "debit")}
effective: toBeReceived,
raw: debitAmount,
}}
/> />
} }
/> />

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { ExchangeTosStatus } from "@gnu-taler/taler-util"; import { Amounts, ExchangeTosStatus } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js"; import { Amount } from "../../components/Amount.js";
@ -26,7 +26,11 @@ import { TermsOfService } from "../../components/TermsOfService/index.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import editIcon from "../../svg/edit_24px.svg"; import editIcon from "../../svg/edit_24px.svg";
import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js"; import {
ExchangeDetails,
getAmountWithFee,
WithdrawDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js"; import { State } from "./index.js";
export function SuccessView(state: State.Success): VNode { export function SuccessView(state: State.Success): VNode {
@ -64,10 +68,11 @@ export function SuccessView(state: State.Success): VNode {
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<WithdrawDetails <WithdrawDetails
amount={{ amount={getAmountWithFee(
effective: state.toBeReceived, state.toBeReceived,
raw: state.chosenAmount, state.chosenAmount,
}} "credit",
)}
/> />
} }
/> />

View File

@ -28,17 +28,13 @@ import {
stringifyPaytoUri, stringifyPaytoUri,
TalerProtocolTimestamp, TalerProtocolTimestamp,
Transaction, Transaction,
TransactionDeposit,
TransactionRefresh,
TransactionRefund,
TransactionTip,
TransactionType, TransactionType,
TranslatedString, TranslatedString,
WithdrawalType, WithdrawalType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { styled } from "@linaria/react"; import { styled } from "@linaria/react";
import { differenceInSeconds, isAfter, isFuture, isPast } from "date-fns"; import { differenceInSeconds, isPast } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import emptyImg from "../../static/img/empty.png"; import emptyImg from "../../static/img/empty.png";
@ -68,6 +64,7 @@ import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { SafeHandler } from "../mui/handlers.js"; import { SafeHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { assertUnreachable } from "../utils/index.js";
interface Props { interface Props {
tid: string; tid: string;
@ -392,9 +389,10 @@ export function TransactionView({
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const { safely } = useAlertContext(); const { safely } = useAlertContext();
const raw = Amounts.parseOrThrow(transaction.amountRaw);
const effective = Amounts.parseOrThrow(transaction.amountEffective);
if (transaction.type === TransactionType.Withdrawal) { if (transaction.type === TransactionType.Withdrawal) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
const chosen = Amounts.parseOrThrow(transaction.amountRaw);
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -406,7 +404,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Withdrawal`} type={i18n.str`Withdrawal`}
total={total} total={effective}
kind="positive" kind="positive"
> >
{transaction.exchangeBaseUrl} {transaction.exchangeBaseUrl}
@ -417,7 +415,7 @@ export function TransactionView({
.type === WithdrawalType.ManualTransfer ? ( .type === WithdrawalType.ManualTransfer ? (
<Fragment> <Fragment>
<BankDetailsByPaytoType <BankDetailsByPaytoType
amount={chosen} amount={raw}
exchangeBaseUrl={transaction.exchangeBaseUrl} exchangeBaseUrl={transaction.exchangeBaseUrl}
payto={parsePaytoUri( payto={parsePaytoUri(
transaction.withdrawalDetails.exchangePaytoUris[0], transaction.withdrawalDetails.exchangePaytoUris[0],
@ -500,10 +498,7 @@ export function TransactionView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<WithdrawDetails <WithdrawDetails
amount={{ amount={getAmountWithFee(effective, raw, "credit")}
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/> />
} }
/> />
@ -517,15 +512,9 @@ export function TransactionView({
? undefined ? undefined
: Amounts.parseOrThrow(transaction.refundPending); : Amounts.parseOrThrow(transaction.refundPending);
const price = { const effectiveRefund = Amounts.parseOrThrow(
raw: Amounts.parseOrThrow(transaction.amountRaw), transaction.totalRefundEffective,
effective: Amounts.parseOrThrow(transaction.amountEffective), );
};
const refund = {
raw: Amounts.parseOrThrow(transaction.totalRefundRaw),
effective: Amounts.parseOrThrow(transaction.totalRefundEffective),
};
const total = Amounts.sub(price.effective, refund.effective).amount;
return ( return (
<TransactionTemplate <TransactionTemplate
@ -537,7 +526,7 @@ export function TransactionView({
> >
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
total={total} total={effective}
type={i18n.str`Payment`} type={i18n.str`Payment`}
kind="negative" kind="negative"
> >
@ -632,8 +621,8 @@ export function TransactionView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<PurchaseDetails <PurchaseDetails
price={price} price={getAmountWithFee(effective, raw, "debit")}
refund={refund} effectiveRefund={effectiveRefund}
info={transaction.info} info={transaction.info}
proposalId={transaction.proposalId} proposalId={transaction.proposalId}
/> />
@ -645,7 +634,6 @@ export function TransactionView({
} }
if (transaction.type === TransactionType.Deposit) { if (transaction.type === TransactionType.Deposit) {
const total = Amounts.parseOrThrow(transaction.amountRaw);
const payto = parsePaytoUri(transaction.targetPaytoUri); const payto = parsePaytoUri(transaction.targetPaytoUri);
const wireTime = AbsoluteTime.fromTimestamp( const wireTime = AbsoluteTime.fromTimestamp(
@ -663,7 +651,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Deposit`} type={i18n.str`Deposit`}
total={total} total={effective}
kind="negative" kind="negative"
> >
{!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />} {!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />}
@ -671,7 +659,11 @@ export function TransactionView({
{payto && <PartPayto payto={payto} kind="neutral" />} {payto && <PartPayto payto={payto} kind="neutral" />}
<Part <Part
title={i18n.str`Details`} title={i18n.str`Details`}
text={<DepositDetails transaction={transaction} />} text={
<DepositDetails
amount={getAmountWithFee(effective, raw, "debit")}
/>
}
kind="neutral" kind="neutral"
/> />
{!shouldBeWired ? ( {!shouldBeWired ? (
@ -712,11 +704,6 @@ export function TransactionView({
} }
if (transaction.type === TransactionType.Refresh) { if (transaction.type === TransactionType.Refresh) {
const total = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount;
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -728,22 +715,24 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Refresh`} type={i18n.str`Refresh`}
total={total} total={effective}
kind="negative" kind="negative"
> >
{transaction.exchangeBaseUrl} {transaction.exchangeBaseUrl}
</Header> </Header>
<Part <Part
title={i18n.str`Details`} title={i18n.str`Details`}
text={<RefreshDetails transaction={transaction} />} text={
<RefreshDetails
amount={getAmountWithFee(effective, raw, "debit")}
/>
}
/> />
</TransactionTemplate> </TransactionTemplate>
); );
} }
if (transaction.type === TransactionType.Tip) { if (transaction.type === TransactionType.Tip) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -755,7 +744,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Tip`} type={i18n.str`Tip`}
total={total} total={effective}
kind="positive" kind="positive"
> >
{transaction.merchantBaseUrl} {transaction.merchantBaseUrl}
@ -767,14 +756,15 @@ export function TransactionView({
/> */} /> */}
<Part <Part
title={i18n.str`Details`} title={i18n.str`Details`}
text={<TipDetails transaction={transaction} />} text={
<TipDetails amount={getAmountWithFee(effective, raw, "credit")} />
}
/> />
</TransactionTemplate> </TransactionTemplate>
); );
} }
if (transaction.type === TransactionType.Refund) { if (transaction.type === TransactionType.Refund) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -786,7 +776,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Refund`} type={i18n.str`Refund`}
total={total} total={effective}
kind="positive" kind="positive"
> >
{transaction.info.summary} {transaction.info.summary}
@ -817,48 +807,17 @@ export function TransactionView({
/> />
<Part <Part
title={i18n.str`Details`} title={i18n.str`Details`}
text={<RefundDetails transaction={transaction} />} text={
<RefundDetails
amount={getAmountWithFee(effective, raw, "credit")}
/>
}
/> />
</TransactionTemplate> </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 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>
);
}
if (transaction.type === TransactionType.PeerPullCredit) { if (transaction.type === TransactionType.PeerPullCredit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -870,7 +829,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Credit`} type={i18n.str`Credit`}
total={total} total={effective}
kind="positive" kind="positive"
> >
<i18n.Translate>Invoice</i18n.Translate> <i18n.Translate>Invoice</i18n.Translate>
@ -900,10 +859,7 @@ export function TransactionView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<InvoiceDetails <InvoiceDetails
amount={{ amount={getAmountWithFee(effective, raw, "credit")}
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/> />
} }
/> />
@ -912,7 +868,6 @@ export function TransactionView({
} }
if (transaction.type === TransactionType.PeerPullDebit) { if (transaction.type === TransactionType.PeerPullDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -924,7 +879,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Debit`} type={i18n.str`Debit`}
total={total} total={effective}
kind="negative" kind="negative"
> >
<i18n.Translate>Invoice</i18n.Translate> <i18n.Translate>Invoice</i18n.Translate>
@ -946,16 +901,14 @@ export function TransactionView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<InvoiceDetails <InvoiceDetails
amount={{ amount={getAmountWithFee(effective, raw, "debit")}
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/> />
} }
/> />
</TransactionTemplate> </TransactionTemplate>
); );
} }
if (transaction.type === TransactionType.PeerPushDebit) { if (transaction.type === TransactionType.PeerPushDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective); const total = Amounts.parseOrThrow(transaction.amountEffective);
return ( return (
@ -998,10 +951,7 @@ export function TransactionView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<TransferDetails <TransferDetails
amount={{ amount={getAmountWithFee(effective, raw, "debit")}
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/> />
} }
/> />
@ -1010,7 +960,6 @@ export function TransactionView({
} }
if (transaction.type === TransactionType.PeerPushCredit) { if (transaction.type === TransactionType.PeerPushCredit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}
@ -1022,7 +971,7 @@ export function TransactionView({
<Header <Header
timestamp={transaction.timestamp} timestamp={transaction.timestamp}
type={i18n.str`Credit`} type={i18n.str`Credit`}
total={total} total={effective}
kind="positive" kind="positive"
> >
<i18n.Translate>Transfer</i18n.Translate> <i18n.Translate>Transfer</i18n.Translate>
@ -1044,17 +993,14 @@ export function TransactionView({
title={i18n.str`Details`} title={i18n.str`Details`}
text={ text={
<TransferDetails <TransferDetails
amount={{ amount={getAmountWithFee(effective, raw, "credit")}
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/> />
} }
/> />
</TransactionTemplate> </TransactionTemplate>
); );
} }
return <div />; assertUnreachable(transaction);
} }
export function MerchantDetails({ export function MerchantDetails({
@ -1231,19 +1177,37 @@ export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
} }
export interface AmountWithFee { export interface AmountWithFee {
effective: AmountJson; value: AmountJson;
raw: AmountJson; fee: AmountJson;
total: AmountJson;
maxFrac: number;
}
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]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return {
total: effective,
value: raw,
fee,
maxFrac,
};
} }
export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode { export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext(); 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 ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
@ -1251,17 +1215,17 @@ export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Invoice</i18n.Translate> <i18n.Translate>Invoice</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={amount.raw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(fee) && ( {Amounts.isNonZero(amount.fee) && (
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} negative maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
)} )}
@ -1275,7 +1239,7 @@ export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={amount.effective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
@ -1285,12 +1249,6 @@ export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode { export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.effective, amount.raw).amount;
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
@ -1298,17 +1256,17 @@ export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Transfer</i18n.Translate> <i18n.Translate>Transfer</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={amount.raw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(fee) && ( {Amounts.isNonZero(amount.fee) && (
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} negative maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
)} )}
@ -1322,7 +1280,7 @@ export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={amount.effective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
@ -1332,12 +1290,12 @@ export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode { export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.raw, amount.effective).amount; const maxFrac = [amount.fee, amount.fee]
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a)) .map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0); .reduce((c, p) => Math.max(c, p), 0);
const total = Amounts.add(amount.value, amount.fee).amount;
return ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
@ -1345,17 +1303,17 @@ export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Withdraw</i18n.Translate> <i18n.Translate>Withdraw</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={amount.raw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(fee) && ( {Amounts.isNonZero(amount.fee) && (
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} negative maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
)} )}
@ -1369,7 +1327,7 @@ export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={amount.effective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
@ -1378,24 +1336,18 @@ export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
export function PurchaseDetails({ export function PurchaseDetails({
price, price,
refund, effectiveRefund,
info, info,
proposalId, proposalId,
}: { }: {
price: AmountWithFee; price: AmountWithFee;
refund?: AmountWithFee; effectiveRefund?: AmountJson;
info: OrderShortInfo; info: OrderShortInfo;
proposalId: string; proposalId: string;
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const partialFee = Amounts.sub(price.effective, price.raw).amount; const total = Amounts.add(price.value, price.fee).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 hasProducts = info.products && info.products.length > 0;
@ -1406,10 +1358,6 @@ export function PurchaseDetails({
return; return;
}; };
const total = !refund
? price.effective
: Amounts.sub(price.effective, refund.effective).amount;
return ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
@ -1417,30 +1365,42 @@ export function PurchaseDetails({
<i18n.Translate>Price</i18n.Translate> <i18n.Translate>Price</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={price.raw} /> <Amount value={price.value} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(price.fee) && (
{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> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Transaction fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} /> <Amount value={price.fee} />
</td> </td>
</tr> </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> <tr>
<td colSpan={2}> <td colSpan={2}>
<hr /> <hr />
@ -1451,9 +1411,27 @@ export function PurchaseDetails({
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={total} /> <Amount value={Amounts.sub(total, effectiveRefund).amount} />
</td> </td>
</tr> </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 && ( {hasProducts && (
<tr> <tr>
<td colSpan={2}> <td colSpan={2}>
@ -1508,39 +1486,27 @@ export function PurchaseDetails({
); );
} }
function RefundDetails({ function RefundDetails({ amount }: { amount: AmountWithFee }): VNode {
transaction,
}: {
transaction: TransactionRefund;
}): VNode {
const { i18n } = useTranslationContext(); 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 ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
<td> <td>
<i18n.Translate>Amount</i18n.Translate> <i18n.Translate>Refund</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(fee) && ( {Amounts.isNonZero(amount.fee) && (
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} negative maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
)} )}
@ -1554,45 +1520,34 @@ function RefundDetails({
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
); );
} }
function DepositDetails({ function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
transaction,
}: {
transaction: TransactionDeposit;
}): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const r = Amounts.parseOrThrow(transaction.amountRaw);
const e = Amounts.parseOrThrow(transaction.amountEffective);
const fee = Amounts.sub(e, r).amount;
const maxFrac = [r, e, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
<td> <td>
<i18n.Translate>Amount</i18n.Translate> <i18n.Translate>Deposit</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(fee) && ( {Amounts.isNonZero(amount.fee) && (
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
)} )}
@ -1606,43 +1561,32 @@ function DepositDetails({
<i18n.Translate>Total transfer</i18n.Translate> <i18n.Translate>Total transfer</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
); );
} }
function RefreshDetails({
transaction, function RefreshDetails({ amount }: { amount: AmountWithFee }): VNode {
}: {
transaction: TransactionRefresh;
}): VNode {
const { i18n } = useTranslationContext(); 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 ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
<td> <td>
<i18n.Translate>Amount</i18n.Translate> <i18n.Translate>Refresh</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} negative maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -1655,42 +1599,34 @@ function RefreshDetails({
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
); );
} }
function TipDetails({ transaction }: { transaction: TransactionTip }): VNode { function TipDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext(); 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 ( return (
<PurchaseDetailsTable> <PurchaseDetailsTable>
<tr> <tr>
<td> <td>
<i18n.Translate>Amount</i18n.Translate> <i18n.Translate>Tip</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountRaw} maxFracSize={maxFrac} /> <Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
{Amounts.isNonZero(fee) && ( {Amounts.isNonZero(amount.fee) && (
<tr> <tr>
<td> <td>
<i18n.Translate>Transaction fees</i18n.Translate> <i18n.Translate>Fees</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={fee} negative maxFracSize={maxFrac} /> <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
)} )}
@ -1704,7 +1640,7 @@ function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
<i18n.Translate>Total</i18n.Translate> <i18n.Translate>Total</i18n.Translate>
</td> </td>
<td> <td>
<Amount value={transaction.amountEffective} maxFracSize={maxFrac} /> <Amount value={amount.total} maxFracSize={amount.maxFrac} />
</td> </td>
</tr> </tr>
</PurchaseDetailsTable> </PurchaseDetailsTable>
@ -1778,3 +1714,38 @@ function NicePayto({ payto }: { payto: PaytoUri }): VNode {
} }
return <Fragment>{stringifyPaytoUri(payto)}</Fragment>; 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>
);
}