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, "amountEffective" in state.payStatus
effective: ? Amounts.parseOrThrow(state.payStatus.amountEffective)
"amountEffective" in state.payStatus : state.amount;
? Amounts.parseOrThrow(state.payStatus.amountEffective)
: 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

@ -584,26 +584,26 @@ function setAlertedIcon(): void {
interface OffscreenCanvasRenderingContext2D interface OffscreenCanvasRenderingContext2D
extends CanvasState, extends CanvasState,
CanvasTransform, CanvasTransform,
CanvasCompositing, CanvasCompositing,
CanvasImageSmoothing, CanvasImageSmoothing,
CanvasFillStrokeStyles, CanvasFillStrokeStyles,
CanvasShadowStyles, CanvasShadowStyles,
CanvasFilters, CanvasFilters,
CanvasRect, CanvasRect,
CanvasDrawPath, CanvasDrawPath,
CanvasUserInterface, CanvasUserInterface,
CanvasText, CanvasText,
CanvasDrawImage, CanvasDrawImage,
CanvasImageData, CanvasImageData,
CanvasPathDrawingStyles, CanvasPathDrawingStyles,
CanvasTextDrawingStyles, CanvasTextDrawingStyles,
CanvasPath { CanvasPath {
readonly canvas: OffscreenCanvas; readonly canvas: OffscreenCanvas;
} }
declare const OffscreenCanvasRenderingContext2D: { declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D; prototype: OffscreenCanvasRenderingContext2D;
new(): OffscreenCanvasRenderingContext2D; new (): OffscreenCanvasRenderingContext2D;
}; };
interface OffscreenCanvas extends EventTarget { interface OffscreenCanvas extends EventTarget {
@ -616,7 +616,7 @@ interface OffscreenCanvas extends EventTarget {
} }
declare const OffscreenCanvas: { declare const OffscreenCanvas: {
prototype: OffscreenCanvas; prototype: OffscreenCanvas;
new(width: number, height: number): OffscreenCanvas; new (width: number, height: number): OffscreenCanvas;
}; };
function createCanvas(size: number): OffscreenCanvas { function createCanvas(size: number): OffscreenCanvas {

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,43 +1365,73 @@ 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>
)} )}
<tr> {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
<td colSpan={2}> <Fragment>
<hr /> <tr>
</td> <td colSpan={2}>
</tr> <hr />
<tr> </td>
<td> </tr>
<i18n.Translate>Total</i18n.Translate> <tr>
</td> <td>
<td> <i18n.Translate>Subtotal</i18n.Translate>
<Amount value={total} /> </td>
</td> <td>
</tr> <Amount value={price.total} />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Refunded</i18n.Translate>
</td>
<td>
<Amount value={effectiveRefund} negative />
</td>
</tr>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={Amounts.sub(total, effectiveRefund).amount} />
</td>
</tr>
</Fragment>
) : (
<Fragment>
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>
<i18n.Translate>Total</i18n.Translate>
</td>
<td>
<Amount value={price.total} />
</td>
</tr>
</Fragment>
)}
{hasProducts && ( {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>
);
}