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/>
*/
import { Amounts } from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { h, VNode } from "preact";
import { LogoHeader } from "../../components/LogoHeader.js";
@ -27,7 +28,11 @@ import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
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";
export function ReadyView({
@ -144,10 +149,7 @@ export function ReadyView({
title={i18n.str`Details`}
text={
<InvoiceDetails
amount={{
effective: toBeReceived,
raw: requestAmount,
}}
amount={getAmountWithFee(toBeReceived, requestAmount, "credit")}
/>
}
/>

View File

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

View File

@ -14,6 +14,7 @@
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 { h, VNode } from "preact";
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 { Button } from "../../mui/Button.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";
export function ReadyView({
@ -114,10 +115,7 @@ export function ReadyView({
title={i18n.str`Details`}
text={
<TransferDetails
amount={{
effective: toBeReceived,
raw: debitAmount,
}}
amount={getAmountWithFee(debitAmount, toBeReceived, "debit")}
/>
}
/>

View File

@ -14,7 +14,7 @@
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 { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
@ -26,7 +26,11 @@ import { TermsOfService } from "../../components/TermsOfService/index.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
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";
export function SuccessView(state: State.Success): VNode {
@ -64,10 +68,11 @@ export function SuccessView(state: State.Success): VNode {
title={i18n.str`Details`}
text={
<WithdrawDetails
amount={{
effective: state.toBeReceived,
raw: state.chosenAmount,
}}
amount={getAmountWithFee(
state.toBeReceived,
state.chosenAmount,
"credit",
)}
/>
}
/>

View File

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

View File

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