diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src')
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( |