2021-06-16 23:21:03 +02:00
|
|
|
/*
|
|
|
|
This file is part of TALER
|
|
|
|
(C) 2016 GNUnet e.V.
|
|
|
|
|
|
|
|
TALER is free software; you can redistribute it and/or modify it under the
|
|
|
|
terms of the GNU General Public License as published by the Free Software
|
|
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
|
|
|
|
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
|
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
*/
|
|
|
|
|
2021-08-24 20:16:11 +02:00
|
|
|
import { AmountJson, AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util";
|
2021-06-16 23:21:03 +02:00
|
|
|
import { format } from "date-fns";
|
2021-08-23 21:51:49 +02:00
|
|
|
import { Fragment, JSX, VNode, h } from "preact";
|
2021-06-16 23:21:03 +02:00
|
|
|
import { route } from 'preact-router';
|
|
|
|
import { useEffect, useState } from "preact/hooks";
|
|
|
|
import * as wxApi from "../wxApi";
|
2021-08-19 05:34:47 +02:00
|
|
|
import { Pages } from "../NavigationBar";
|
2021-06-21 01:37:35 +02:00
|
|
|
import emptyImg from "../../static/img/empty.png"
|
2021-08-24 20:16:11 +02:00
|
|
|
import { Button, ButtonBox, ButtonBoxDestructive, ButtonDestructive, ButtonPrimary, ExtraLargeText, FontIcon, LargeText, ListOfProducts, PopupBox, Row, RowBorderGray, SmallTextLight, WalletBox } from "../components/styled";
|
2021-07-13 20:33:28 +02:00
|
|
|
import { ErrorMessage } from "../components/ErrorMessage";
|
2021-06-16 23:21:03 +02:00
|
|
|
|
|
|
|
export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
|
|
|
|
const [transaction, setTransaction] = useState<
|
|
|
|
Transaction | undefined
|
|
|
|
>(undefined);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const fetchData = async (): Promise<void> => {
|
|
|
|
const res = await wxApi.getTransactions();
|
|
|
|
const ts = res.transactions.filter(t => t.transactionId === tid);
|
|
|
|
if (ts.length === 1) {
|
|
|
|
setTransaction(ts[0]);
|
|
|
|
} else {
|
|
|
|
route(Pages.history);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
fetchData();
|
|
|
|
}, []);
|
|
|
|
|
2021-07-13 20:33:28 +02:00
|
|
|
if (!transaction) {
|
|
|
|
return <div><i18n.Translate>Loading ...</i18n.Translate></div>;
|
|
|
|
}
|
2021-06-16 23:21:03 +02:00
|
|
|
return <TransactionView
|
|
|
|
transaction={transaction}
|
|
|
|
onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
|
2021-06-28 16:38:29 +02:00
|
|
|
onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))}
|
2021-06-16 23:21:03 +02:00
|
|
|
onBack={() => { history.go(-1); }} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface WalletTransactionProps {
|
2021-07-13 20:33:28 +02:00
|
|
|
transaction: Transaction,
|
2021-06-16 23:21:03 +02:00
|
|
|
onDelete: () => void,
|
2021-06-28 16:38:29 +02:00
|
|
|
onRetry: () => void,
|
2021-06-16 23:21:03 +02:00
|
|
|
onBack: () => void,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-13 20:33:28 +02:00
|
|
|
export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) {
|
2021-06-16 23:21:03 +02:00
|
|
|
|
2021-06-28 16:38:29 +02:00
|
|
|
function Status() {
|
2021-07-13 20:33:28 +02:00
|
|
|
if (transaction.error) {
|
2021-06-28 16:38:29 +02:00
|
|
|
return <span style={{ fontWeight: 'normal', fontSize: 16, color: 'red' }}>(failed)</span>
|
|
|
|
}
|
2021-07-13 20:33:28 +02:00
|
|
|
if (transaction.pending) {
|
|
|
|
return <span style={{ fontWeight: 'normal', fontSize: 16, color: 'gray' }}>(pending...)</span>
|
|
|
|
}
|
|
|
|
return null
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
2021-08-24 20:16:11 +02:00
|
|
|
function TransactionTemplate({ children }: { children: VNode[] }) {
|
2021-08-24 18:29:37 +02:00
|
|
|
return <WalletBox>
|
2021-07-13 20:33:28 +02:00
|
|
|
<section>
|
|
|
|
<ErrorMessage title={transaction?.error?.hint} />
|
2021-08-24 20:16:11 +02:00
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
{children}
|
|
|
|
</div>
|
2021-07-13 20:33:28 +02:00
|
|
|
</section>
|
|
|
|
<footer>
|
2021-08-24 20:16:11 +02:00
|
|
|
<ButtonBox onClick={onBack}><i18n.Translate> <FontIcon>←</FontIcon> </i18n.Translate></ButtonBox>
|
2021-07-13 20:33:28 +02:00
|
|
|
<div>
|
|
|
|
{transaction?.error ? <ButtonPrimary onClick={onRetry}><i18n.Translate>retry</i18n.Translate></ButtonPrimary> : null}
|
2021-08-24 20:16:11 +02:00
|
|
|
<ButtonBoxDestructive onClick={onDelete}><i18n.Translate>🗑</i18n.Translate></ButtonBoxDestructive>
|
2021-07-13 20:33:28 +02:00
|
|
|
</div>
|
|
|
|
</footer>
|
2021-08-24 18:29:37 +02:00
|
|
|
</WalletBox>
|
2021-07-13 20:33:28 +02:00
|
|
|
}
|
2021-08-24 20:16:11 +02:00
|
|
|
type Kind = 'positive' | 'negative' | 'neutral';
|
|
|
|
function Part({ text, title, kind, big }: { title: string, text: AmountLike, kind: Kind, big?: boolean }) {
|
|
|
|
const Text = big ? ExtraLargeText : LargeText;
|
|
|
|
return <div style={{ margin: '1em' }}>
|
|
|
|
<SmallTextLight style={{ margin: '.5em' }}>{title}</SmallTextLight>
|
|
|
|
<Text style={{ color: kind == 'positive' ? 'green' : (kind == 'negative' ? 'red' : 'black') }}>
|
|
|
|
{text}
|
|
|
|
</Text>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
|
|
|
|
function amountToString(text: AmountLike) {
|
|
|
|
const aj = Amounts.jsonifyAmount(text)
|
|
|
|
const amount = Amounts.stringifyValue(aj)
|
|
|
|
return `${amount} ${aj.currency}`
|
|
|
|
}
|
|
|
|
|
2021-06-21 01:37:35 +02:00
|
|
|
|
2021-06-16 23:21:03 +02:00
|
|
|
if (transaction.type === TransactionType.Withdrawal) {
|
2021-06-21 01:37:35 +02:00
|
|
|
const fee = Amounts.sub(
|
|
|
|
Amounts.parseOrThrow(transaction.amountRaw),
|
|
|
|
Amounts.parseOrThrow(transaction.amountEffective),
|
|
|
|
).amount
|
2021-08-24 20:16:11 +02:00
|
|
|
return <TransactionTemplate>
|
|
|
|
<h2>Withdrawal <Status /></h2>
|
|
|
|
<div>{transaction.timestamp.t_ms === 'never' ? 'never': format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
|
|
|
|
<br />
|
|
|
|
<Part title="Total withdrawn" text={amountToString(transaction.amountEffective)} kind='positive' />
|
|
|
|
<Part title="Chosen amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
|
|
|
|
<Part title="Exchange fee" text={amountToString(fee)} kind='negative' />
|
|
|
|
<Part title="Exchange" text={new URL(transaction.exchangeBaseUrl).hostname} kind='neutral' />
|
2021-07-13 20:33:28 +02:00
|
|
|
</TransactionTemplate>
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
2021-06-21 01:37:35 +02:00
|
|
|
const showLargePic = () => {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-16 23:21:03 +02:00
|
|
|
if (transaction.type === TransactionType.Payment) {
|
2021-06-21 01:37:35 +02:00
|
|
|
const fee = Amounts.sub(
|
|
|
|
Amounts.parseOrThrow(transaction.amountEffective),
|
|
|
|
Amounts.parseOrThrow(transaction.amountRaw),
|
|
|
|
).amount
|
|
|
|
|
2021-08-24 20:16:11 +02:00
|
|
|
return <TransactionTemplate>
|
|
|
|
<h2>Payment <Status /></h2>
|
|
|
|
<div>{transaction.timestamp.t_ms === 'never' ? 'never': format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
|
|
|
|
<br />
|
|
|
|
<Part big title="Total paid" text={amountToString(transaction.amountEffective)} kind='negative' />
|
|
|
|
<Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
|
|
|
|
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
|
|
|
|
<Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' />
|
|
|
|
<Part title="Purchase" text={transaction.info.summary} kind='neutral' />
|
|
|
|
<Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' />
|
|
|
|
|
2021-07-13 20:33:28 +02:00
|
|
|
<div>
|
|
|
|
{transaction.info.products && transaction.info.products.length > 0 &&
|
|
|
|
<ListOfProducts>
|
|
|
|
{transaction.info.products.map(p => <RowBorderGray>
|
|
|
|
<a href="#" onClick={showLargePic}>
|
|
|
|
<img src={p.image ? p.image : emptyImg} />
|
|
|
|
</a>
|
|
|
|
<div>
|
|
|
|
{p.quantity && p.quantity > 0 && <SmallTextLight>x {p.quantity} {p.unit}</SmallTextLight>}
|
|
|
|
<div>{p.description}</div>
|
|
|
|
</div>
|
|
|
|
</RowBorderGray>)}
|
|
|
|
</ListOfProducts>
|
|
|
|
}
|
2021-06-16 23:21:03 +02:00
|
|
|
</div>
|
2021-07-13 20:33:28 +02:00
|
|
|
</TransactionTemplate>
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (transaction.type === TransactionType.Deposit) {
|
2021-06-21 01:37:35 +02:00
|
|
|
const fee = Amounts.sub(
|
|
|
|
Amounts.parseOrThrow(transaction.amountRaw),
|
|
|
|
Amounts.parseOrThrow(transaction.amountEffective),
|
|
|
|
).amount
|
2021-08-24 20:16:11 +02:00
|
|
|
return <TransactionTemplate>
|
|
|
|
<h2>Deposit <Status /></h2>
|
|
|
|
<div>{transaction.timestamp.t_ms === 'never' ? 'never': format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
|
|
|
|
<br />
|
|
|
|
<Part big title="Total deposit" text={amountToString(transaction.amountEffective)} kind='negative' />
|
|
|
|
<Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
|
|
|
|
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
|
2021-07-13 20:33:28 +02:00
|
|
|
</TransactionTemplate>
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (transaction.type === TransactionType.Refresh) {
|
2021-06-21 01:37:35 +02:00
|
|
|
const fee = Amounts.sub(
|
|
|
|
Amounts.parseOrThrow(transaction.amountRaw),
|
|
|
|
Amounts.parseOrThrow(transaction.amountEffective),
|
|
|
|
).amount
|
2021-08-24 20:16:11 +02:00
|
|
|
return <TransactionTemplate>
|
|
|
|
<h2>Refresh <Status /></h2>
|
|
|
|
<div>{transaction.timestamp.t_ms === 'never' ? 'never': format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
|
|
|
|
<br />
|
|
|
|
<Part big title="Total refresh" text={amountToString(transaction.amountEffective)} kind='negative' />
|
|
|
|
<Part big title="Refresh amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
|
|
|
|
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
|
2021-07-13 20:33:28 +02:00
|
|
|
</TransactionTemplate>
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (transaction.type === TransactionType.Tip) {
|
2021-06-21 01:37:35 +02:00
|
|
|
const fee = Amounts.sub(
|
|
|
|
Amounts.parseOrThrow(transaction.amountRaw),
|
|
|
|
Amounts.parseOrThrow(transaction.amountEffective),
|
|
|
|
).amount
|
2021-08-24 20:16:11 +02:00
|
|
|
return <TransactionTemplate>
|
|
|
|
<h2>Tip <Status /></h2>
|
|
|
|
<div>{transaction.timestamp.t_ms === 'never' ? 'never': format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
|
|
|
|
<br />
|
|
|
|
<Part big title="Total tip" text={amountToString(transaction.amountEffective)} kind='positive' />
|
|
|
|
<Part big title="Received amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
|
|
|
|
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
|
2021-07-13 20:33:28 +02:00
|
|
|
</TransactionTemplate>
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (transaction.type === TransactionType.Refund) {
|
2021-06-21 01:37:35 +02:00
|
|
|
const fee = Amounts.sub(
|
|
|
|
Amounts.parseOrThrow(transaction.amountRaw),
|
|
|
|
Amounts.parseOrThrow(transaction.amountEffective),
|
|
|
|
).amount
|
2021-08-24 20:16:11 +02:00
|
|
|
return <TransactionTemplate>
|
|
|
|
<h2>Refund <Status /></h2>
|
|
|
|
<div>{transaction.timestamp.t_ms === 'never' ? 'never': format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
|
|
|
|
<br />
|
|
|
|
<Part big title="Total refund" text={amountToString(transaction.amountEffective)} kind='positive' />
|
|
|
|
<Part big title="Refund amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
|
|
|
|
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
|
|
|
|
<Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' />
|
|
|
|
<Part title="Purchase" text={transaction.info.summary} kind='neutral' />
|
|
|
|
<Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' />
|
2021-07-13 20:33:28 +02:00
|
|
|
|
|
|
|
<p>
|
|
|
|
{transaction.info.summary}
|
|
|
|
</p>
|
|
|
|
<div>
|
|
|
|
{transaction.info.products && transaction.info.products.length > 0 &&
|
|
|
|
<ListOfProducts>
|
|
|
|
{transaction.info.products.map(p => <RowBorderGray>
|
|
|
|
<a href="#" onClick={showLargePic}>
|
|
|
|
<img src={p.image ? p.image : emptyImg} />
|
|
|
|
</a>
|
|
|
|
<div>
|
|
|
|
{p.quantity && p.quantity > 0 && <SmallTextLight>x {p.quantity} {p.unit}</SmallTextLight>}
|
|
|
|
<div>{p.description}</div>
|
|
|
|
</div>
|
|
|
|
</RowBorderGray>)}
|
|
|
|
</ListOfProducts>
|
|
|
|
}
|
2021-06-16 23:21:03 +02:00
|
|
|
</div>
|
2021-07-13 20:33:28 +02:00
|
|
|
</TransactionTemplate>
|
2021-06-16 23:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <div></div>
|
|
|
|
}
|