From aa0edbdd6875113976ec2b27efe2d82625ed2fde Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 3 Jun 2021 01:07:29 -0300 Subject: [PATCH] wallet transaction detail --- packages/taler-util/src/transactionsTypes.ts | 10 +- .../taler-wallet-webextension/package.json | 3 +- .../src/Application.tsx | 2 +- .../src/pages/popup.stories.tsx | 191 +++++++++++++++ .../src/pages/popup.tsx | 231 +++++++++++++++++- .../taler-wallet-webextension/src/wxApi.ts | 10 + .../static/style/popup.css | 22 ++ pnpm-lock.yaml | 7 + 8 files changed, 467 insertions(+), 9 deletions(-) create mode 100644 packages/taler-wallet-webextension/src/pages/popup.stories.tsx diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts index b3cc274a0..e29a5549d 100644 --- a/packages/taler-util/src/transactionsTypes.ts +++ b/packages/taler-util/src/transactionsTypes.ts @@ -145,7 +145,7 @@ interface WithdrawalDetailsForTalerBankIntegrationApi { // This should only be used for actual withdrawals // and not for tips that have their own transactions type. -interface TransactionWithdrawal extends TransactionCommon { +export interface TransactionWithdrawal extends TransactionCommon { type: TransactionType.Withdrawal; /** @@ -266,7 +266,7 @@ export interface OrderShortInfo { fulfillmentMessage_i18n?: InternationalizedString; } -interface TransactionRefund extends TransactionCommon { +export interface TransactionRefund extends TransactionCommon { type: TransactionType.Refund; // ID for the transaction that is refunded @@ -282,7 +282,7 @@ interface TransactionRefund extends TransactionCommon { amountEffective: AmountString; } -interface TransactionTip extends TransactionCommon { +export interface TransactionTip extends TransactionCommon { type: TransactionType.Tip; // 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 // such as a refresh necessary before coin expiration. // 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; // Exchange that the coins are refreshed with @@ -314,7 +314,7 @@ interface TransactionRefresh extends TransactionCommon { * Deposit transaction, which effectively sends * money from this wallet somewhere else. */ -interface TransactionDeposit extends TransactionCommon { +export interface TransactionDeposit extends TransactionCommon { type: TransactionType.Deposit; depositGroupId: string; diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 5a6775b27..60a2ea5d4 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -12,12 +12,13 @@ "test": "jest ./tests", "compile": "tsc && rollup -c", "build-storybook": "build-storybook", - "storybook": "start-storybook -p 6006", + "storybook": "start-storybook -s static -p 6006", "watch": "tsc --watch & rollup -w -c" }, "dependencies": { "@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-wallet-core": "workspace:*", + "date-fns": "^2.22.1", "preact": "^10.5.13", "preact-router": "^3.2.1", "tslib": "^2.1.0" 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 + */ + +/** +* +* @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) =>
+ + + +
+ +
+
+ ], +}; + +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(props: any) { + const r = (args: any) => + 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 { >
{dateStr}
- {props.title} + {props.title} {props.pending ? ( (Pending) ) : null} @@ -320,6 +322,7 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element { case TransactionType.Withdrawal: return ( void, + onBack: () => void, +} + +export function WalletTransactionView({ transaction, onDelete, onBack }: WalletTransactionProps) { + if (!transaction) { + return
Loading ...
; + } + + function Footer() { + return
+ +
+ + +
+ +
+ } + + function Pending() { + if (!transaction?.pending) return null + return (pending...) + } + + function CommonFields() { + if (!transaction) return null; + return + + Amount deduce + {transaction.amountRaw} + + + Amount received + {transaction.amountEffective} + + + Exchange fee + {Amounts.stringify( + Amounts.sub( + Amounts.parseOrThrow(transaction.amountRaw), + Amounts.parseOrThrow(transaction.amountEffective), + ).amount + )} + + + When + {transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')} + + + } + + if (transaction.type === TransactionType.Withdrawal) { + return ( +
+
+

Withdrawal

+

+ From {transaction.exchangeBaseUrl} +

+ + +
+
+
+
+ ); + } + + if (transaction.type === TransactionType.Payment) { + return ( +
+
+

Payment ({transaction.proposalId})

+

+ To {transaction.info.merchant.name} +

+ + + + + + + + + + {transaction.info.products && transaction.info.products.length > 0 && + + + + + } + +
Order id{transaction.info.orderId}
Summary{transaction.info.summary}
Products
    + {transaction.info.products.map(p => +
  1. {p.description}
  2. + )}
+
+
+
+ ); + } + + if (transaction.type === TransactionType.Deposit) { + return ( +
+
+

Deposit ({transaction.depositGroupId})

+

+ To {transaction.targetPaytoUri} +

+ + +
+
+
+
+ ); + } + + if (transaction.type === TransactionType.Refresh) { + return ( +
+
+

Refresh

+

+ From {transaction.exchangeBaseUrl} +

+ + +
+
+
+
+ ); + } + + if (transaction.type === TransactionType.Tip) { + return ( +
+
+

Tip

+

+ From {transaction.merchantBaseUrl} +

+ + +
+
+
+
+ ); + } + + if (transaction.type === TransactionType.Refund) { + return ( +
+
+

Refund ({transaction.refundedTransactionId})

+

+ From {transaction.info.merchant.name} +

+ + + + + + + + + + {transaction.info.products && transaction.info.products.length > 0 && + + + + + } + +
Order id{transaction.info.orderId}
Summary{transaction.info.summary}
Products
    + {transaction.info.products.map(p => +
  1. {p.description}
  2. + )}
+
+
+
+ ); + } + + + return
+} + +function WalletTransaction({ tid }: { tid: string }): JSX.Element { + const [transaction, setTransaction] = useState< + Transaction | undefined + >(undefined); + + useEffect(() => { + const fetchData = async (): Promise => { + const res = await wxApi.getTransactions(); + const ts = res.transactions.filter(t => t.transactionId === tid) + if (ts.length === 1) { + setTransaction(ts[0]); + } + }; + fetchData(); + }, []); + + return wxApi.deleteTransaction(tid)} + onBack={() => { history.go(-1) }} + /> +} + class WalletSettings extends Component { render(): JSX.Element { return ( @@ -597,6 +822,7 @@ export function WalletPopup(): JSX.Element { +
@@ -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"; @@ -130,6 +131,15 @@ export function getTransactions(): Promise { return callBackend("getTransactions", {}); } +/** + * Get balances for all currencies/exchanges. + */ +export function deleteTransaction(transactionId: string): Promise { + return callBackend("deleteTransaction", { + transactionId + } as DeleteTransactionRequest); +} + /** * Download a refund and accept it. */ diff --git a/packages/taler-wallet-webextension/static/style/popup.css b/packages/taler-wallet-webextension/static/style/popup.css index c0201e584..a234f2a2c 100644 --- a/packages/taler-wallet-webextension/static/style/popup.css +++ b/packages/taler-wallet-webextension/static/style/popup.css @@ -238,3 +238,25 @@ button.accept:disabled { font-weight: bold; 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; +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea20e1836..1cf013b82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -224,6 +224,7 @@ importers: '@types/node': ^14.14.22 ava: 3.15.0 babel-plugin-transform-react-jsx: ^6.24.1 + date-fns: ^2.22.1 enzyme: ^3.11.0 enzyme-adapter-preact-pure: ^3.1.0 history: 4.10.1 @@ -243,6 +244,7 @@ importers: dependencies: '@gnu-taler/taler-util': link:../taler-util '@gnu-taler/taler-wallet-core': link:../taler-wallet-core + date-fns: 2.22.1 preact: 10.5.13 preact-router: 3.2.1_preact@10.5.13 tslib: 2.1.0 @@ -8087,6 +8089,11 @@ packages: whatwg-url: 8.5.0 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: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} engines: {node: '>=6'}