summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-06-03 01:07:29 -0300
committerSebastian <sebasjm@gmail.com>2021-06-03 01:07:34 -0300
commitaa0edbdd6875113976ec2b27efe2d82625ed2fde (patch)
treeb208e3725f2c074ea5313ae347ba9cf808edbbdd /packages/taler-wallet-webextension/src
parent9f09f5a1a5f0028bba1f76b4c8740734102cc0cf (diff)
downloadwallet-core-aa0edbdd6875113976ec2b27efe2d82625ed2fde.tar.gz
wallet-core-aa0edbdd6875113976ec2b27efe2d82625ed2fde.tar.bz2
wallet-core-aa0edbdd6875113976ec2b27efe2d82625ed2fde.zip
wallet transaction detail
Diffstat (limited to 'packages/taler-wallet-webextension/src')
-rw-r--r--packages/taler-wallet-webextension/src/Application.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/pages/popup.stories.tsx191
-rw-r--r--packages/taler-wallet-webextension/src/pages/popup.tsx231
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts10
4 files changed, 431 insertions, 3 deletions
diff --git a/packages/taler-wallet-webextension/src/Application.tsx b/packages/taler-wallet-webextension/src/Application.tsx
index 096f6a09a..6e10786d2 100644
--- a/packages/taler-wallet-webextension/src/Application.tsx
+++ b/packages/taler-wallet-webextension/src/Application.tsx
@@ -19,7 +19,7 @@ export enum Pages {
return_coins = '/return-coins',
tips = '/tips',
withdraw = '/withdraw',
- popup = '/popup/:rest',
+ popup = '/popup/:rest*',
}
export function Application() {
diff --git a/packages/taler-wallet-webextension/src/pages/popup.stories.tsx b/packages/taler-wallet-webextension/src/pages/popup.stories.tsx
new file mode 100644
index 000000000..e9202fbea
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/popup.stories.tsx
@@ -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,
+});
diff --git a/packages/taler-wallet-webextension/src/pages/popup.tsx b/packages/taler-wallet-webextension/src/pages/popup.tsx
index c361f4d99..4693c94c3 100644
--- a/packages/taler-wallet-webextension/src/pages/popup.tsx
+++ b/packages/taler-wallet-webextension/src/pages/popup.tsx
@@ -38,7 +38,8 @@ import {
Timestamp,
amountFractionalBase,
} 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 { Match } from 'preact-router/match';
import { useEffect, useState } from "preact/hooks";
@@ -268,6 +269,7 @@ interface TransactionLayoutProps {
amount: AmountString | "unknown";
timestamp: Timestamp;
title: string;
+ id: string;
subtitle: string;
iconPath: string;
pending: boolean;
@@ -297,7 +299,7 @@ function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
>
<div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div>
<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 ? (
<span style={{ color: "darkblue" }}> (Pending)</span>
) : null}
@@ -320,6 +322,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Withdrawal:
return (
<TransactionLayout
+ id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"credit"}
title="Withdrawal"
@@ -332,6 +335,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Payment:
return (
<TransactionLayout
+ id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"debit"}
title="Payment"
@@ -344,6 +348,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Refund:
return (
<TransactionLayout
+ id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"credit"}
title="Refund"
@@ -356,6 +361,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Tip:
return (
<TransactionLayout
+ id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"credit"}
title="Tip"
@@ -368,6 +374,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Refresh:
return (
<TransactionLayout
+ id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"credit"}
title="Refresh"
@@ -380,6 +387,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
case TransactionType.Deposit:
return (
<TransactionLayout
+ id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"debit"}
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> {
render(): JSX.Element {
return (
@@ -597,6 +822,7 @@ export function WalletPopup(): JSX.Element {
<Route path={Pages.settings} component={WalletSettings} />
<Route path={Pages.debug} component={WalletDebug} />
<Route path={Pages.history} component={WalletHistory} />
+ <Route path={Pages.transaction} component={WalletTransaction} />
</Router>
</div>
</div>
@@ -605,6 +831,7 @@ export function WalletPopup(): JSX.Element {
enum Pages {
balance = '/popup/balance',
+ transaction = '/popup/transaction/:tid',
settings = '/popup/settings',
debug = '/popup/debug',
history = '/popup/history',
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index cbebfb214..3340f27ce 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -35,6 +35,7 @@ import {
PrepareTipRequest,
PrepareTipResult,
AcceptTipRequest,
+ DeleteTransactionRequest,
} from "@gnu-taler/taler-util";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
@@ -131,6 +132,15 @@ export function getTransactions(): Promise<TransactionsResponse> {
}
/**
+ * 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.
*/
export function applyRefund(