wallet transaction detail

This commit is contained in:
Sebastian 2021-06-03 01:07:29 -03:00
parent 9f09f5a1a5
commit aa0edbdd68
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
8 changed files with 467 additions and 9 deletions

View File

@ -145,7 +145,7 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
// This should only be used for actual withdrawals // This should only be used for actual withdrawals
// and not for tips that have their own transactions type. // and not for tips that have their own transactions type.
interface TransactionWithdrawal extends TransactionCommon { export interface TransactionWithdrawal extends TransactionCommon {
type: TransactionType.Withdrawal; type: TransactionType.Withdrawal;
/** /**
@ -266,7 +266,7 @@ export interface OrderShortInfo {
fulfillmentMessage_i18n?: InternationalizedString; fulfillmentMessage_i18n?: InternationalizedString;
} }
interface TransactionRefund extends TransactionCommon { export interface TransactionRefund extends TransactionCommon {
type: TransactionType.Refund; type: TransactionType.Refund;
// ID for the transaction that is refunded // ID for the transaction that is refunded
@ -282,7 +282,7 @@ interface TransactionRefund extends TransactionCommon {
amountEffective: AmountString; amountEffective: AmountString;
} }
interface TransactionTip extends TransactionCommon { export interface TransactionTip extends TransactionCommon {
type: TransactionType.Tip; type: TransactionType.Tip;
// Raw amount of the tip, without extra fees that apply // Raw amount of the tip, without extra fees that apply
@ -297,7 +297,7 @@ interface TransactionTip extends TransactionCommon {
// A transaction shown for refreshes that are not associated to other transactions // A transaction shown for refreshes that are not associated to other transactions
// such as a refresh necessary before coin expiration. // such as a refresh necessary before coin expiration.
// It should only be returned by the API if the effective amount is different from zero. // It should only be returned by the API if the effective amount is different from zero.
interface TransactionRefresh extends TransactionCommon { export interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh; type: TransactionType.Refresh;
// Exchange that the coins are refreshed with // Exchange that the coins are refreshed with
@ -314,7 +314,7 @@ interface TransactionRefresh extends TransactionCommon {
* Deposit transaction, which effectively sends * Deposit transaction, which effectively sends
* money from this wallet somewhere else. * money from this wallet somewhere else.
*/ */
interface TransactionDeposit extends TransactionCommon { export interface TransactionDeposit extends TransactionCommon {
type: TransactionType.Deposit; type: TransactionType.Deposit;
depositGroupId: string; depositGroupId: string;

View File

@ -12,12 +12,13 @@
"test": "jest ./tests", "test": "jest ./tests",
"compile": "tsc && rollup -c", "compile": "tsc && rollup -c",
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"storybook": "start-storybook -p 6006", "storybook": "start-storybook -s static -p 6006",
"watch": "tsc --watch & rollup -w -c" "watch": "tsc --watch & rollup -w -c"
}, },
"dependencies": { "dependencies": {
"@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/taler-wallet-core": "workspace:*", "@gnu-taler/taler-wallet-core": "workspace:*",
"date-fns": "^2.22.1",
"preact": "^10.5.13", "preact": "^10.5.13",
"preact-router": "^3.2.1", "preact-router": "^3.2.1",
"tslib": "^2.1.0" "tslib": "^2.1.0"

View File

@ -19,7 +19,7 @@ export enum Pages {
return_coins = '/return-coins', return_coins = '/return-coins',
tips = '/tips', tips = '/tips',
withdraw = '/withdraw', withdraw = '/withdraw',
popup = '/popup/:rest', popup = '/popup/:rest*',
} }
export function Application() { export function Application() {

View File

@ -0,0 +1,191 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU 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.
GNU 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
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { PaymentStatus, TransactionPayment, TransactionType, TransactionWithdrawal, TransactionDeposit, TransactionRefresh, TransactionTip, TransactionRefund, WithdrawalType, TransactionCommon } from '@gnu-taler/taler-util';
import { Fragment, h } from 'preact';
import { WalletTransactionView as Component } from './popup';
export default {
title: 'popup/transaction details',
component: Component,
decorators: [
(Story: any) => <div>
<link key="1" rel="stylesheet" type="text/css" href="/style/pure.css" />
<link key="2" rel="stylesheet" type="text/css" href="/style/popup.css" />
<link key="3" rel="stylesheet" type="text/css" href="/style/wallet.css" />
<div style={{ margin: "1em", width: 400 }}>
<Story />
</div>
</div>
],
};
const commonTransaction = {
amountRaw: 'USD:10',
amountEffective: 'USD:9',
pending: false,
timestamp: {
t_ms: new Date().getTime()
},
transactionId: '12',
} as TransactionCommon
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
exchangeBaseUrl: 'http://exchange.taler',
withdrawalDetails: {
confirmed: false,
exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
type: WithdrawalType.ManualTransfer,
}
} as TransactionWithdrawal,
payment: {
...commonTransaction,
type: TransactionType.Payment,
info: {
contractTermsHash: 'ASDZXCASD',
merchant: {
name: 'the merchant',
},
orderId: '#12345',
products: [],
summary: 'the summary',
fulfillmentMessage: '',
},
proposalId: '#proposalId',
status: PaymentStatus.Accepted,
} as TransactionPayment,
deposit: {
...commonTransaction,
type: TransactionType.Deposit,
depositGroupId: '#groupId',
targetPaytoUri: 'payto://x-taler-bank/bank/account',
} as TransactionDeposit,
refresh: {
...commonTransaction,
type: TransactionType.Refresh,
exchangeBaseUrl: 'http://exchange.taler',
} as TransactionRefresh,
tip: {
...commonTransaction,
type: TransactionType.Tip,
merchantBaseUrl: 'http://merchant.taler',
} as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
refundedTransactionId: '#refundId',
info: {
contractTermsHash: 'ASDZXCASD',
merchant: {
name: 'the merchant',
},
orderId: '#12345',
products: [],
summary: 'the summary',
fulfillmentMessage: '',
},
} as TransactionRefund,
}
function dynamic<T>(props: any) {
const r = (args: any) => <Component {...args} />
r.args = props
return r
}
export const NotYetLoaded = dynamic({});
export const Withdraw = dynamic({
transaction: exampleData.withdraw
});
export const WithdrawPending = dynamic({
transaction: { ...exampleData.withdraw, pending: true },
});
export const Payment = dynamic({
transaction: exampleData.payment
});
export const PaymentPending = dynamic({
transaction: { ...exampleData.payment, pending: true },
});
export const PaymentWithProducts = dynamic({
transaction: {
...exampleData.payment,
info: {
...exampleData.payment.info,
products: [{
description: 't-shirt',
}, {
description: 'beer',
}]
}
} as TransactionPayment,
});
export const Deposit = dynamic({
transaction: exampleData.deposit
});
export const DepositPending = dynamic({
transaction: { ...exampleData.deposit, pending: true }
});
export const Refresh = dynamic({
transaction: exampleData.refresh
});
export const Tip = dynamic({
transaction: exampleData.tip
});
export const TipPending = dynamic({
transaction: { ...exampleData.tip, pending: true }
});
export const Refund = dynamic({
transaction: exampleData.refund
});
export const RefundPending = dynamic({
transaction: { ...exampleData.refund , pending: true }
});
export const RefundWithProducts = dynamic({
transaction: {
...exampleData.refund,
info: {
...exampleData.refund.info,
products: [{
description: 't-shirt',
}, {
description: 'beer',
}]
}
} as TransactionRefund,
});

View File

@ -38,7 +38,8 @@ import {
Timestamp, Timestamp,
amountFractionalBase, amountFractionalBase,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Component, ComponentChildren, JSX } from "preact"; import { format } from "date-fns";
import { Component, ComponentChildren, Fragment, JSX } from "preact";
import { route, Route, Router } from 'preact-router'; import { route, Route, Router } from 'preact-router';
import { Match } from 'preact-router/match'; import { Match } from 'preact-router/match';
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
@ -268,6 +269,7 @@ interface TransactionLayoutProps {
amount: AmountString | "unknown"; amount: AmountString | "unknown";
timestamp: Timestamp; timestamp: Timestamp;
title: string; title: string;
id: string;
subtitle: string; subtitle: string;
iconPath: string; iconPath: string;
pending: boolean; pending: boolean;
@ -297,7 +299,7 @@ function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
> >
<div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div> <div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div>
<div style={{ fontVariant: "small-caps", fontSize: "x-large" }}> <div style={{ fontVariant: "small-caps", fontSize: "x-large" }}>
<span>{props.title}</span> <a href={Pages.transaction.replace(':tid', props.id)}><span>{props.title}</span></a>
{props.pending ? ( {props.pending ? (
<span style={{ color: "darkblue" }}> (Pending)</span> <span style={{ color: "darkblue" }}> (Pending)</span>
) : null} ) : null}
@ -320,6 +322,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Withdrawal: case TransactionType.Withdrawal:
return ( return (
<TransactionLayout <TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"credit"} debitCreditIndicator={"credit"}
title="Withdrawal" title="Withdrawal"
@ -332,6 +335,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Payment: case TransactionType.Payment:
return ( return (
<TransactionLayout <TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"debit"} debitCreditIndicator={"debit"}
title="Payment" title="Payment"
@ -344,6 +348,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Refund: case TransactionType.Refund:
return ( return (
<TransactionLayout <TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"credit"} debitCreditIndicator={"credit"}
title="Refund" title="Refund"
@ -356,6 +361,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Tip: case TransactionType.Tip:
return ( return (
<TransactionLayout <TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"credit"} debitCreditIndicator={"credit"}
title="Tip" title="Tip"
@ -368,6 +374,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Refresh: case TransactionType.Refresh:
return ( return (
<TransactionLayout <TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"credit"} debitCreditIndicator={"credit"}
title="Refresh" title="Refresh"
@ -380,6 +387,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Deposit: case TransactionType.Deposit:
return ( return (
<TransactionLayout <TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective} amount={tx.amountEffective}
debitCreditIndicator={"debit"} debitCreditIndicator={"debit"}
title="Refresh" title="Refresh"
@ -420,6 +428,223 @@ function WalletHistory(props: any): JSX.Element {
); );
} }
interface WalletTransactionProps {
transaction?: Transaction,
onDelete: () => void,
onBack: () => void,
}
export function WalletTransactionView({ transaction, onDelete, onBack }: WalletTransactionProps) {
if (!transaction) {
return <div>Loading ...</div>;
}
function Footer() {
return <footer style={{ marginTop: 'auto', display: 'flex' }}>
<button onClick={onBack}>back</button>
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
<button onClick={onDelete}>remove</button>
</div>
</footer>
}
function Pending() {
if (!transaction?.pending) return null
return <span style={{fontWeight:'normal', fontSize:16, color: 'gray'}}>(pending...)</span>
}
function CommonFields() {
if (!transaction) return null;
return <Fragment>
<tr>
<td>Amount deduce</td>
<td>{transaction.amountRaw}</td>
</tr>
<tr>
<td>Amount received</td>
<td>{transaction.amountEffective}</td>
</tr>
<tr>
<td>Exchange fee</td>
<td>{Amounts.stringify(
Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount
)}</td>
</tr>
<tr>
<td>When</td>
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
</tr>
</Fragment>
}
if (transaction.type === TransactionType.Withdrawal) {
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
<section>
<h1>Withdrawal <Pending /></h1>
<p>
From <b>{transaction.exchangeBaseUrl}</b>
</p>
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
<CommonFields />
</table>
</section>
<Footer />
</div>
);
}
if (transaction.type === TransactionType.Payment) {
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
<section>
<h1>Payment ({transaction.proposalId}) <Pending /></h1>
<p>
To <b>{transaction.info.merchant.name}</b>
</p>
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
<tr>
<td>Order id</td>
<td>{transaction.info.orderId}</td>
</tr>
<tr>
<td>Summary</td>
<td>{transaction.info.summary}</td>
</tr>
{transaction.info.products && transaction.info.products.length > 0 &&
<tr>
<td>Products</td>
<td><ol style={{margin:0, textAlign:'left'}}>
{transaction.info.products.map(p =>
<li>{p.description}</li>
)}</ol></td>
</tr>
}
<CommonFields />
</table>
</section>
<Footer />
</div>
);
}
if (transaction.type === TransactionType.Deposit) {
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
<section>
<h1>Deposit ({transaction.depositGroupId}) <Pending /></h1>
<p>
To <b>{transaction.targetPaytoUri}</b>
</p>
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
<CommonFields />
</table>
</section>
<Footer />
</div>
);
}
if (transaction.type === TransactionType.Refresh) {
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
<section>
<h1>Refresh <Pending /></h1>
<p>
From <b>{transaction.exchangeBaseUrl}</b>
</p>
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
<CommonFields />
</table>
</section>
<Footer />
</div>
);
}
if (transaction.type === TransactionType.Tip) {
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
<section>
<h1>Tip <Pending /></h1>
<p>
From <b>{transaction.merchantBaseUrl}</b>
</p>
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
<CommonFields />
</table>
</section>
<Footer />
</div>
);
}
if (transaction.type === TransactionType.Refund) {
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
<section>
<h1>Refund ({transaction.refundedTransactionId}) <Pending /></h1>
<p>
From <b>{transaction.info.merchant.name}</b>
</p>
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
<tr>
<td>Order id</td>
<td>{transaction.info.orderId}</td>
</tr>
<tr>
<td>Summary</td>
<td>{transaction.info.summary}</td>
</tr>
{transaction.info.products && transaction.info.products.length > 0 &&
<tr>
<td>Products</td>
<td><ol>
{transaction.info.products.map(p =>
<li>{p.description}</li>
)}</ol></td>
</tr>
}
<CommonFields />
</table>
</section>
<Footer />
</div>
);
}
return <div></div>
}
function WalletTransaction({ 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]);
}
};
fetchData();
}, []);
return <WalletTransactionView
transaction={transaction}
onDelete={() => wxApi.deleteTransaction(tid)}
onBack={() => { history.go(-1) }}
/>
}
class WalletSettings extends Component<any, any> { class WalletSettings extends Component<any, any> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
@ -597,6 +822,7 @@ export function WalletPopup(): JSX.Element {
<Route path={Pages.settings} component={WalletSettings} /> <Route path={Pages.settings} component={WalletSettings} />
<Route path={Pages.debug} component={WalletDebug} /> <Route path={Pages.debug} component={WalletDebug} />
<Route path={Pages.history} component={WalletHistory} /> <Route path={Pages.history} component={WalletHistory} />
<Route path={Pages.transaction} component={WalletTransaction} />
</Router> </Router>
</div> </div>
</div> </div>
@ -605,6 +831,7 @@ export function WalletPopup(): JSX.Element {
enum Pages { enum Pages {
balance = '/popup/balance', balance = '/popup/balance',
transaction = '/popup/transaction/:tid',
settings = '/popup/settings', settings = '/popup/settings',
debug = '/popup/debug', debug = '/popup/debug',
history = '/popup/history', history = '/popup/history',

View File

@ -35,6 +35,7 @@ import {
PrepareTipRequest, PrepareTipRequest,
PrepareTipResult, PrepareTipResult,
AcceptTipRequest, AcceptTipRequest,
DeleteTransactionRequest,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core"; import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
@ -130,6 +131,15 @@ export function getTransactions(): Promise<TransactionsResponse> {
return callBackend("getTransactions", {}); return callBackend("getTransactions", {});
} }
/**
* Get balances for all currencies/exchanges.
*/
export function deleteTransaction(transactionId: string): Promise<void> {
return callBackend("deleteTransaction", {
transactionId
} as DeleteTransactionRequest);
}
/** /**
* Download a refund and accept it. * Download a refund and accept it.
*/ */

View File

@ -238,3 +238,25 @@ button.accept:disabled {
font-weight: bold; font-weight: bold;
background: #00fa9a; background: #00fa9a;
} }
table.detailsTable td {
text-align: right;
border: 0px;
border-bottom: 1px;
}
table.detailsTable td {
border-bottom: 1px solid black;
}
table.detailsTable tr:last-child td {
border-bottom: 0px;
}
table.detailsTable {
border: 0px;
}
table.detailsTable.pending {
color: gray;
}

View File

@ -224,6 +224,7 @@ importers:
'@types/node': ^14.14.22 '@types/node': ^14.14.22
ava: 3.15.0 ava: 3.15.0
babel-plugin-transform-react-jsx: ^6.24.1 babel-plugin-transform-react-jsx: ^6.24.1
date-fns: ^2.22.1
enzyme: ^3.11.0 enzyme: ^3.11.0
enzyme-adapter-preact-pure: ^3.1.0 enzyme-adapter-preact-pure: ^3.1.0
history: 4.10.1 history: 4.10.1
@ -243,6 +244,7 @@ importers:
dependencies: dependencies:
'@gnu-taler/taler-util': link:../taler-util '@gnu-taler/taler-util': link:../taler-util
'@gnu-taler/taler-wallet-core': link:../taler-wallet-core '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
date-fns: 2.22.1
preact: 10.5.13 preact: 10.5.13
preact-router: 3.2.1_preact@10.5.13 preact-router: 3.2.1_preact@10.5.13
tslib: 2.1.0 tslib: 2.1.0
@ -8087,6 +8089,11 @@ packages:
whatwg-url: 8.5.0 whatwg-url: 8.5.0
dev: true dev: true
/date-fns/2.22.1:
resolution: {integrity: sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==}
engines: {node: '>=0.11'}
dev: false
/date-time/3.1.0: /date-time/3.1.0:
resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==}
engines: {node: '>=6'} engines: {node: '>=6'}