diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-19 00:34:47 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-19 00:35:21 -0300 |
commit | 97a05ff659af274dcfcd9c76bf19100bbd51ce0e (patch) | |
tree | 9cce837ec9a5ec06279dc48eac75e1993ede983f /packages/taler-wallet-webextension/src | |
parent | b015f76e7268cb5caff14a0ed88cb5e8fa53dc2e (diff) | |
download | wallet-core-97a05ff659af274dcfcd9c76bf19100bbd51ce0e.tar.xz |
new wallet history and view refactoring
Diffstat (limited to 'packages/taler-wallet-webextension/src')
25 files changed, 785 insertions, 138 deletions
diff --git a/packages/taler-wallet-webextension/src/popup/popup.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index 4aee48fb7..e07032d0a 100644 --- a/packages/taler-wallet-webextension/src/popup/popup.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -27,10 +27,11 @@ import { i18n } from "@gnu-taler/taler-util"; import { ComponentChildren, JSX } from "preact"; import Match from "preact-router/match"; -import { useDevContext } from "../context/devContext"; -import { PopupNavigation } from '../components/styled' +import { useDevContext } from "./context/devContext"; +import { PopupNavigation } from './components/styled' export enum Pages { + welcome = '/welcome', balance = '/balance', settings = '/settings', dev = '/dev', @@ -39,6 +40,15 @@ export enum Pages { transaction = '/transaction/:tid', provider_detail = '/provider/:pid', provider_add = '/provider/add', + + reset_required = '/reset-required', + payback = '/payback', + return_coins = '/return-coins', + + pay = '/pay', + refund = '/refund', + tips = '/tips', + withdraw = '/withdraw', } interface TabProps { @@ -59,18 +69,23 @@ function Tab(props: TabProps): JSX.Element { ); } -export function NavBar({devMode, path}:{path:string, devMode:boolean}) { +export function NavBar({ devMode, path }: { path: string, devMode: boolean }) { return <PopupNavigation devMode={devMode}> - <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab> - <Tab target="/history" current={path}>{i18n.str`History`}</Tab> - <Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab> - <Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab> - {devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>} + <div> + <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab> + <Tab target="/history" current={path}>{i18n.str`History`}</Tab> + <Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab> + <Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab> + {devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>} + </div> </PopupNavigation> } export function WalletNavBar() { const { devMode } = useDevContext() - return <Match>{({ path }: any) => <NavBar devMode={devMode} path={path} />}</Match> + return <Match>{({ path }: any) => { + console.log("path", path) + return <NavBar devMode={devMode} path={path} /> + }}</Match> } diff --git a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx new file mode 100644 index 000000000..0217289eb --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx @@ -0,0 +1,13 @@ +export function LogoHeader() { + return <div style={{ + display: 'flex', + justifyContent: 'space-around', + margin: '2em', + }}> + <img style={{ + width: 150, + height: 70, + }} src="/static/img/logo-2021.svg" width="150" /> + </div> + +}
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 7f709db46..6067fa446 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -11,7 +11,7 @@ export const PaymentStatus = styled.div<{ color: string }>` background-color: ${p => p.color}; ` -export const WalletPage = styled.section` +export const WalletAction = styled.section` border: solid 5px black; border-radius: 10px; margin-left: auto; @@ -28,8 +28,73 @@ export const WalletPage = styled.section` } ` +export const DateSeparator = styled.div` + color: gray; + margin: .2em; + margin-top: 1em; +` +export const WalletBox = styled.div<{ noPadding?: boolean }>` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + & > * { + width: 400px; + } + & > section { + padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'}; + padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'}; + // this margin will send the section up when used with a header + margin-bottom: auto; + overflow: auto; + + table td { + padding: 5px 10px; + } + table tr { + border-bottom: 1px solid black; + border-top: 1px solid black; + } + } + + & > header { + flex-direction: row; + justify-content: space-between; + display: flex; + padding: 8px; + margin-bottom: 5px; + + & > div { + align-self: center; + } + + & > h3 { + margin: 0px; + } + + & > .title { + /* margin: 1em; */ + font-size: large; + color: #3c4e92; + } + } + + & > footer { + padding-top: 8px; + padding-bottom: 8px; + flex-direction: row; + justify-content: space-between; + display: flex; + & button { + margin-right: 8px; + margin-left: 8px; + } + } +` + export const PopupBox = styled.div<{ noPadding?: boolean }>` height: 290px; + width: 400px; display: flex; flex-direction: column; justify-content: space-between; @@ -194,10 +259,32 @@ export const RowBorderGray = styled(Row)` export const RowLightBorderGray = styled(Row2)` border: 1px solid lightgray; - /* border-radius: 0.5em; */ + border-top: 0px; + + ${DateSeparator} + & { + border: 1px solid lightgray; + background-color: red; + } ` -export const HistoryRow = styled(RowLightBorderGray)` +export const HistoryRow = styled.a` + text-decoration: none; + + display: flex; + justify-content: space-between; + padding: 0.5em; + + border: 1px solid lightgray; + border-top: 0px; + + ${DateSeparator} + & { + border: 1px solid lightgray; + } + + :hover { + background-color: lightgray; + } + & > ${Column}:last-of-type { margin-left: auto; align-self: center; @@ -284,11 +371,17 @@ export const ErrorBox = styled.div` } } ` -export const PopupNavigation = styled.div<{devMode?:boolean}>` +export const PopupNavigation = styled.div<{ devMode?: boolean }>` background-color:#0042b2; height: 35px; - - & > a { + justify-content: space-around; + display: flex; + + & > div { + width: 400px; + } + + & > div > a { color: #f8faf7; display: inline-block; width: calc(400px / ${({ devMode }) => !devMode ? 4 : 5}); @@ -298,7 +391,7 @@ export const PopupNavigation = styled.div<{devMode?:boolean}>` line-height: 35px; } - & > a.active { + & > div > a.active { background-color: #f8faf7; color: #0042b2; font-weight: bold; diff --git a/packages/taler-wallet-webextension/src/wallet/Pay.stories.tsx b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx index 0297d6264..38e3d0f35 100644 --- a/packages/taler-wallet-webextension/src/wallet/Pay.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx @@ -20,23 +20,16 @@ */ import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util'; -import { FunctionalComponent, h } from 'preact'; +import { createExample } from '../test-utils'; import { PaymentRequestView as TestedComponent } from './Pay'; - export default { - title: 'wallet/pay', + title: 'cta/pay', component: TestedComponent, argTypes: { }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r -} - export const InsufficientBalance = createExample(TestedComponent, { payStatus: { status: PreparePayResultType.InsufficientBalance, diff --git a/packages/taler-wallet-webextension/src/wallet/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index a5849bb28..a5849bb28 100644 --- a/packages/taler-wallet-webextension/src/wallet/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx diff --git a/packages/taler-wallet-webextension/src/wallet/Refund.stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx index 044141f0c..88e714cb7 100644 --- a/packages/taler-wallet-webextension/src/wallet/Refund.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx @@ -19,24 +19,18 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ContractTerms, OrderShortInfo, PreparePayResultType } from '@gnu-taler/taler-util'; -import { FunctionalComponent, h } from 'preact'; +import { OrderShortInfo } from '@gnu-taler/taler-util'; +import { createExample } from '../test-utils'; import { View as TestedComponent } from './Refund'; export default { - title: 'wallet/refund', + title: 'cta/refund', component: TestedComponent, argTypes: { }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r -} - export const Complete = createExample(TestedComponent, { applyResult: { amountEffectivePaid: 'USD:10', diff --git a/packages/taler-wallet-webextension/src/wallet/Refund.tsx b/packages/taler-wallet-webextension/src/cta/Refund.tsx index bb26d933b..bb26d933b 100644 --- a/packages/taler-wallet-webextension/src/wallet/Refund.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund.tsx diff --git a/packages/taler-wallet-webextension/src/wallet/Tip.stories.tsx b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx index ffd976144..389b183f0 100644 --- a/packages/taler-wallet-webextension/src/wallet/Tip.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx @@ -19,24 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util'; -import { FunctionalComponent, h } from 'preact'; +import { createExample } from '../test-utils'; import { View as TestedComponent } from './Tip'; export default { - title: 'wallet/tip', + title: 'cta/tip', component: TestedComponent, argTypes: { }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r -} - export const Accepted = createExample(TestedComponent, { prepareTipResult: { accepted: true, diff --git a/packages/taler-wallet-webextension/src/wallet/Tip.tsx b/packages/taler-wallet-webextension/src/cta/Tip.tsx index 69886668b..69886668b 100644 --- a/packages/taler-wallet-webextension/src/wallet/Tip.tsx +++ b/packages/taler-wallet-webextension/src/cta/Tip.tsx diff --git a/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx index fef36b820..747f855fa 100644 --- a/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx @@ -19,32 +19,27 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h } from 'preact'; -import { View, ViewProps } from './Withdraw'; +import { createExample } from '../test-utils'; +import { View as TestedComponent } from './Withdraw'; export default { - title: 'wallet/withdraw', - component: View, + title: 'cta/withdraw', + component: TestedComponent, argTypes: { }, }; -export const WithoutDetails = (a: any) => <View {...a} />; -WithoutDetails.args = { -} as ViewProps - -export const CompleteWithExchange = (a: any) => <View {...a} />; -CompleteWithExchange.args = { +export const CompleteWithExchange = createExample(TestedComponent, { details: { amount: 'USD:2', + possibleExchanges: [], }, selectedExchange: 'Some exchange' -} as ViewProps - -export const CompleteWithoutExchange = (a: any) => <View {...a} />; -CompleteWithoutExchange.args = { +}) +export const CompleteWithoutExchange = createExample(TestedComponent, { details: { amount: 'USD:2', + possibleExchanges: [], }, -} as ViewProps +}) diff --git a/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx index 442ee7dae..b5182b070 100644 --- a/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx @@ -32,14 +32,13 @@ import { } from "../wxApi"; import { WithdrawUriInfoResponse } from "@gnu-taler/taler-util"; import { JSX } from "preact/jsx-runtime"; -import { WalletPage } from '../components/styled'; +import { WalletAction } from '../components/styled'; interface Props { talerWithdrawUri?: string; } export interface ViewProps { - talerWithdrawUri?: string; details: WithdrawUriInfoResponse; selectedExchange?: string; accept: () => Promise<void>; @@ -50,7 +49,7 @@ export interface ViewProps { export function View({ details, selectedExchange, accept, setCancelled, setSelecting }: ViewProps) { return ( - <WalletPage> + <WalletAction> <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;"> <h1 style="font-family: monospace; font-size: 250%;"> <span style="color: #aa3939;">❰</span>Taler Wallet<span style="color: #aa3939;">❱</span> @@ -101,26 +100,19 @@ export function View({ details, selectedExchange, accept, setCancelled, setSelec </div> </div> </div> - </WalletPage> + </WalletAction> ) } export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element { const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined); - const [selectedExchange, setSelectedExchange] = useState< - string | undefined - >(undefined); + const [selectedExchange, setSelectedExchange] = useState<string | undefined>(undefined); const [cancelled, setCancelled] = useState(false); const [selecting, setSelecting] = useState(false); - const [errMsg, setErrMsg] = useState<string | undefined>(""); + const [error, setError] = useState<boolean>(false); const [updateCounter, setUpdateCounter] = useState(1); const [state, setState] = useState(1) - // setTimeout(() => { - // console.log('tick...') - // setState(s => s + 1) - // }, 1000); - useEffect(() => { return onUpdateNotification(() => { console.log('updating...') @@ -132,20 +124,19 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element console.log('on effect yes', talerWithdrawUri) if (!talerWithdrawUri) return const fetchData = async (): Promise<void> => { - console.log('que pasa') try { const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); - console.log('res', res) setDetails(res); if (res.defaultExchangeBaseUrl) { setSelectedExchange(res.defaultExchangeBaseUrl); } } catch (e) { - console.error(e) + console.error('error',JSON.stringify(e,undefined,2)) + setError(true) } }; fetchData(); - }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter, state]); + }, [selectedExchange, selecting, talerWithdrawUri, updateCounter, state]); if (!talerWithdrawUri) { return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; @@ -169,6 +160,9 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element if (cancelled) { return <span><i18n.Translate>Withdraw operation has been cancelled.</i18n.Translate></span>; } + if (error) { + return <span><i18n.Translate>This URI is not valid anymore.</i18n.Translate></span>; + } return <View accept={accept} setCancelled={setCancelled} setSelecting={setSelecting} diff --git a/packages/taler-wallet-webextension/src/wallet/payback.tsx b/packages/taler-wallet-webextension/src/cta/payback.tsx index 4233b1f96..4233b1f96 100644 --- a/packages/taler-wallet-webextension/src/wallet/payback.tsx +++ b/packages/taler-wallet-webextension/src/cta/payback.tsx diff --git a/packages/taler-wallet-webextension/src/wallet/reset-required.tsx b/packages/taler-wallet-webextension/src/cta/reset-required.tsx index 87751561c..87751561c 100644 --- a/packages/taler-wallet-webextension/src/wallet/reset-required.tsx +++ b/packages/taler-wallet-webextension/src/cta/reset-required.tsx diff --git a/packages/taler-wallet-webextension/src/wallet/return-coins.tsx b/packages/taler-wallet-webextension/src/cta/return-coins.tsx index 2273d1454..2273d1454 100644 --- a/packages/taler-wallet-webextension/src/wallet/return-coins.tsx +++ b/packages/taler-wallet-webextension/src/cta/return-coins.tsx diff --git a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx index 940d1f2a4..72139e1f8 100644 --- a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx @@ -25,7 +25,7 @@ import { SmallText, SmallTextLight } from "../components/styled"; import { useBackupStatus } from "../hooks/useBackupStatus"; -import { Pages } from "./popup"; +import { Pages } from "../NavigationBar"; interface Props { onAddProvider: () => void; diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx index b6b65314e..7c9eae54b 100644 --- a/packages/taler-wallet-webextension/src/popup/History.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.tsx @@ -14,11 +14,19 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountJson, Amounts, AmountString, Balance, Timestamp, Transaction, TransactionsResponse, TransactionType } from "@gnu-taler/taler-util"; +import { AmountString, Balance, Timestamp, Transaction, TransactionsResponse, TransactionType } from "@gnu-taler/taler-util"; +import { formatDistance } from "date-fns"; import { JSX } from "preact"; import { useEffect, useState } from "preact/hooks"; +import imageBank from '../../static/img/ri-bank-line.svg'; +import imageHandHeart from '../../static/img/ri-hand-heart-line.svg'; +import imageRefresh from '../../static/img/ri-refresh-line.svg'; +import imageRefund from '../../static/img/ri-refund-2-line.svg'; +import imageShoppingCart from '../../static/img/ri-shopping-cart-line.svg'; +import { Column, ExtraLargeText, HistoryRow, PopupBox, SmallTextLight } from "../components/styled"; +import { useBalances } from "../hooks/useBalances"; import * as wxApi from "../wxApi"; -import { Pages } from "./popup"; +import { Pages } from "../NavigationBar"; export function HistoryPage(props: any): JSX.Element { @@ -45,7 +53,7 @@ export function HistoryPage(props: any): JSX.Element { function amountToString(c: AmountString) { const idx = c.indexOf(':') - return `${c.substring(idx+1)} ${c.substring(0,idx)}` + return `${c.substring(idx + 1)} ${c.substring(0, idx)}` } @@ -68,20 +76,14 @@ export function HistoryView({ list, balances }: { list: Transaction[], balances: ))} </section> <footer style={{ justifyContent: 'space-around' }}> - <a style={{ color: 'darkgreen', textDecoration:'none' }} href={Pages.transaction.replace(':tid', 'asd')}>VIEW MORE TRANSACTIONS</a> + <a target="_blank" + rel="noopener noreferrer" + style={{ color: 'darkgreen', textDecoration: 'none' }} + href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/history`) : '#'}>VIEW MORE TRANSACTIONS</a> </footer> </PopupBox> } -import imageBank from '../../static/img/ri-bank-line.svg'; -import imageShoppingCart from '../../static/img/ri-shopping-cart-line.svg'; -import imageRefund from '../../static/img/ri-refund-2-line.svg'; -import imageHandHeart from '../../static/img/ri-hand-heart-line.svg'; -import imageRefresh from '../../static/img/ri-refresh-line.svg'; -import { Column, ExtraLargeText, HistoryRow, PopupBox, Row, RowBorderGray, SmallTextLight } from "../components/styled"; -import { useBalances } from "../hooks/useBalances"; -import { formatDistance } from "date-fns"; - function TransactionItem(props: { tx: Transaction }): JSX.Element { const tx = props.tx; switch (tx.type) { @@ -171,18 +173,16 @@ function TransactionLayout(props: TransactionLayoutProps): JSX.Element { const now = new Date(); const dateStr = formatDistance(date, now, { addSuffix: true }) return ( - <HistoryRow> + <HistoryRow href={Pages.transaction.replace(':tid', props.id)}> <img src={props.iconPath} /> <Column> <ExtraLargeText> - <a href={Pages.transaction.replace(':tid', props.id)}><span>{props.title}</span></a> + <span>{props.title}</span> {props.pending ? ( <span style={{ color: "darkblue" }}> (Pending)</span> ) : null} </ExtraLargeText> <SmallTextLight>{dateStr}</SmallTextLight> - - {/* <div>{props.subtitle}</div> */} </Column> <TransactionAmount pending={props.pending} diff --git a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx index ec3634d9b..ce5b11c32 100644 --- a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx @@ -20,7 +20,7 @@ */ import { Fragment, FunctionalComponent } from 'preact'; -import { NavBar as TestedComponent } from './popup'; +import { NavBar as TestedComponent } from '../NavigationBar'; export default { title: 'popup/header', diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.tsx b/packages/taler-wallet-webextension/src/popup/Transaction.tsx index fd7389c04..8177b74ad 100644 --- a/packages/taler-wallet-webextension/src/popup/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/popup/Transaction.tsx @@ -20,7 +20,7 @@ import { Fragment, JSX, VNode } from "preact"; import { route } from 'preact-router'; import { useEffect, useState } from "preact/hooks"; import * as wxApi from "../wxApi"; -import { Pages } from "./popup"; +import { Pages } from "../NavigationBar"; import emptyImg from "../../static/img/empty.png" import { Button, ButtonDestructive, ButtonPrimary, ListOfProducts, PopupBox, Row, RowBorderGray, SmallTextLight } from "../components/styled"; import { ErrorMessage } from "../components/ErrorMessage"; diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index faa5149ac..4a9fe9abc 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -34,7 +34,7 @@ import { DeveloperPage as DeveloperPage } from "./popup/Debug"; import { HistoryPage } from "./popup/History"; import { Pages, WalletNavBar -} from "./popup/popup"; +} from "./NavigationBar"; import { ProviderAddPage } from "./popup/ProviderAddPage"; import { ProviderDetailPage } from "./popup/ProviderDetailPage"; import { SettingsPage } from "./popup/Settings"; @@ -129,4 +129,4 @@ function Redirect({ to }: { to: string }): null { route(to, true) }) return null -}
\ No newline at end of file +} diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts new file mode 100644 index 000000000..16262b3c2 --- /dev/null +++ b/packages/taler-wallet-webextension/src/test-utils.ts @@ -0,0 +1,8 @@ +import { FunctionalComponent, h as render } from 'preact'; + +export function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => render(Component, args) + r.args = props + return r +} + diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx new file mode 100644 index 000000000..f50fd3b68 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -0,0 +1,294 @@ +/* + 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, + TransactionCommon, TransactionDeposit, TransactionPayment, + TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, + TransactionWithdrawal, + WithdrawalType +} from '@gnu-taler/taler-util'; +import { FunctionalComponent } from 'preact'; +import { HistoryView as TestedComponent } from './History'; + +export default { + title: 'wallet/history/list', + component: TestedComponent, +}; + +let count = 0 +const commonTransaction = () => ({ + amountRaw: 'USD:10', + amountEffective: 'USD:9', + pending: false, + timestamp: { + t_ms: new Date().getTime() - (count++ * 1000*60*60*7) + }, + 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(), + amountEffective: 'USD:11', + type: TransactionType.Payment, + info: { + contractTermsHash: 'ASDZXCASD', + merchant: { + name: 'the merchant', + }, + orderId: '2021.167-03NPY6MCYMVGT', + products: [], + summary: 'the summary', + fulfillmentMessage: '', + }, + proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + 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: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + info: { + contractTermsHash: 'ASDZXCASD', + merchant: { + name: 'the merchant', + }, + orderId: '2021.167-03NPY6MCYMVGT', + products: [], + summary: 'the summary', + fulfillmentMessage: '', + }, + } as TransactionRefund, +} + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const Empty = createExample(TestedComponent, { + list: [], + balances: [{ + available: 'TESTKUDOS:10', + pendingIncoming: 'TESTKUDOS:0', + pendingOutgoing: 'TESTKUDOS:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] +}); + + +export const One = createExample(TestedComponent, { + list: [exampleData.withdraw], + balances: [{ + available: 'USD:10', + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] +}); + +export const Several = createExample(TestedComponent, { + list: [ + exampleData.withdraw, + exampleData.payment, + exampleData.withdraw, + exampleData.payment, + exampleData.refresh, + exampleData.refund, + exampleData.tip, + exampleData.deposit, + ], + balances: [{ + available: 'TESTKUDOS:10', + pendingIncoming: 'TESTKUDOS:0', + pendingOutgoing: 'TESTKUDOS:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] +}); + +export const SeveralWithTwoCurrencies = createExample(TestedComponent, { + list: [ + exampleData.withdraw, + exampleData.payment, + exampleData.withdraw, + exampleData.payment, + exampleData.refresh, + exampleData.refund, + exampleData.tip, + exampleData.deposit, + ], + balances: [{ + available: 'TESTKUDOS:10', + pendingIncoming: 'TESTKUDOS:0', + pendingOutgoing: 'TESTKUDOS:0', + hasPendingTransactions: false, + requiresUserInput: false, + },{ + available: 'USD:10', + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] +}); + +// export const WithdrawPending = createExample(TestedComponent, { +// transaction: { ...exampleData.withdraw, pending: true }, +// }); + + +// export const Payment = createExample(TestedComponent, { +// transaction: exampleData.payment +// }); + +// export const PaymentWithoutFee = createExample(TestedComponent, { +// transaction: { +// ...exampleData.payment, +// amountRaw: 'USD:11', + +// } +// }); + +// export const PaymentPending = createExample(TestedComponent, { +// transaction: { ...exampleData.payment, pending: true }, +// }); + +// export const PaymentWithProducts = createExample(TestedComponent, { +// transaction: { +// ...exampleData.payment, +// info: { +// ...exampleData.payment.info, +// summary: 'this order has 5 products', +// products: [{ +// description: 't-shirt', +// unit: 'shirts', +// quantity: 1, +// }, { +// description: 't-shirt', +// unit: 'shirts', +// quantity: 1, +// }, { +// description: 'e-book', +// }, { +// description: 'beer', +// unit: 'pint', +// quantity: 15, +// }, { +// description: 'beer', +// unit: 'pint', +// quantity: 15, +// }] +// } +// } as TransactionPayment, +// }); + +// export const PaymentWithLongSummary = createExample(TestedComponent, { +// transaction: { +// ...exampleData.payment, +// info: { +// ...exampleData.payment.info, +// summary: 'this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ', +// products: [{ +// description: 'an xl sized t-shirt with some drawings on it, color pink', +// unit: 'shirts', +// quantity: 1, +// }, { +// description: 'beer', +// unit: 'pint', +// quantity: 15, +// }] +// } +// } as TransactionPayment, +// }); + + +// export const Deposit = createExample(TestedComponent, { +// transaction: exampleData.deposit +// }); + +// export const DepositPending = createExample(TestedComponent, { +// transaction: { ...exampleData.deposit, pending: true } +// }); + +// export const Refresh = createExample(TestedComponent, { +// transaction: exampleData.refresh +// }); + +// export const Tip = createExample(TestedComponent, { +// transaction: exampleData.tip +// }); + +// export const TipPending = createExample(TestedComponent, { +// transaction: { ...exampleData.tip, pending: true } +// }); + +// export const Refund = createExample(TestedComponent, { +// transaction: exampleData.refund +// }); + +// export const RefundPending = createExample(TestedComponent, { +// transaction: { ...exampleData.refund, pending: true } +// }); + +// export const RefundWithProducts = createExample(TestedComponent, { +// transaction: { +// ...exampleData.refund, +// info: { +// ...exampleData.refund.info, +// products: [{ +// description: 't-shirt', +// }, { +// description: 'beer', +// }] +// } +// } as TransactionRefund, +// }); diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx new file mode 100644 index 000000000..6ef5047ae --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -0,0 +1,248 @@ +/* + 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/> + */ + +import { AmountString, Balance, Timestamp, Transaction, TransactionsResponse, TransactionType } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { Fragment, JSX } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import imageBank from '../../static/img/ri-bank-line.svg'; +import imageHandHeart from '../../static/img/ri-hand-heart-line.svg'; +import imageRefresh from '../../static/img/ri-refresh-line.svg'; +import imageRefund from '../../static/img/ri-refund-2-line.svg'; +import imageShoppingCart from '../../static/img/ri-shopping-cart-line.svg'; +import { Column, ExtraLargeText, HistoryRow, WalletBox, DateSeparator, SmallTextLight } from "../components/styled"; +import { useBalances } from "../hooks/useBalances"; +import * as wxApi from "../wxApi"; +import { Pages } from "../NavigationBar"; + + +export function HistoryPage(props: any): JSX.Element { + const [transactions, setTransactions] = useState< + TransactionsResponse | undefined + >(undefined); + const balance = useBalances() + const balanceWithoutError = balance?.error ? [] : (balance?.response.balances || []) + + useEffect(() => { + const fetchData = async (): Promise<void> => { + const res = await wxApi.getTransactions(); + setTransactions(res); + }; + fetchData(); + }, []); + + if (!transactions) { + return <div>Loading ...</div>; + } + + return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />; +} + +function amountToString(c: AmountString) { + const idx = c.indexOf(':') + return `${c.substring(idx + 1)} ${c.substring(0, idx)}` +} + + + +export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) { + const byDate = list.reduce(function (rv, x) { + const theDate = x.timestamp.t_ms === "never" ? "never" : format(x.timestamp.t_ms, 'dd MMMM yyyy'); + (rv[theDate] = rv[theDate] || []).push(x); + return rv; + }, {} as { [x: string]: Transaction[] }); + + return <WalletBox noPadding> + {balances.length > 0 && <header> + {balances.length === 1 && <div class="title"> + Balance: <span>{amountToString(balances[0].available)}</span> + </div>} + {balances.length > 1 && <div class="title"> + Balance: <ul style={{ margin: 0 }}> + {balances.map(b => <li>{b.available}</li>)} + </ul> + </div>} + </header>} + <section> + {Object.keys(byDate).map(d => { + return <Fragment> + <DateSeparator>{d}</DateSeparator> + {byDate[d].map((tx, i) => ( + <TransactionItem key={i} tx={tx} /> + ))} + </Fragment> + })} + </section> + </WalletBox> +} + +function TransactionItem(props: { tx: Transaction }): JSX.Element { + const tx = props.tx; + switch (tx.type) { + case TransactionType.Withdrawal: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"credit"} + title="Withdrawal" + subtitle={`via ${tx.exchangeBaseUrl}`} + timestamp={tx.timestamp} + iconPath={imageBank} + pending={tx.pending} + ></TransactionLayout> + ); + case TransactionType.Payment: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"debit"} + title="Payment" + subtitle={tx.info.summary} + timestamp={tx.timestamp} + iconPath={imageShoppingCart} + pending={tx.pending} + ></TransactionLayout> + ); + case TransactionType.Refund: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"credit"} + title="Refund" + subtitle={tx.info.summary} + timestamp={tx.timestamp} + iconPath={imageRefund} + pending={tx.pending} + ></TransactionLayout> + ); + case TransactionType.Tip: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"credit"} + title="Tip" + subtitle={`from ${new URL(tx.merchantBaseUrl).hostname}`} + timestamp={tx.timestamp} + iconPath={imageHandHeart} + pending={tx.pending} + ></TransactionLayout> + ); + case TransactionType.Refresh: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"credit"} + title="Refresh" + subtitle={`via exchange ${tx.exchangeBaseUrl}`} + timestamp={tx.timestamp} + iconPath={imageRefresh} + pending={tx.pending} + ></TransactionLayout> + ); + case TransactionType.Deposit: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"debit"} + title="Refresh" + subtitle={`to ${tx.targetPaytoUri}`} + timestamp={tx.timestamp} + iconPath={imageRefresh} + pending={tx.pending} + ></TransactionLayout> + ); + } +} + +function TransactionLayout(props: TransactionLayoutProps): JSX.Element { + const date = new Date(props.timestamp.t_ms); + const dateStr = format(date, 'HH:mm:ss') + return ( + // <a href={Pages.transaction.replace(':tid', props.id)}> + <HistoryRow href={Pages.transaction.replace(':tid', props.id)}> + <img src={props.iconPath} /> + <Column> + <ExtraLargeText> + <span>{props.title}</span> + {props.pending ? ( + <span style={{ color: "darkblue" }}> (Pending)</span> + ) : null} + </ExtraLargeText> + <SmallTextLight>{dateStr}</SmallTextLight> + </Column> + <TransactionAmount + pending={props.pending} + amount={props.amount} + debitCreditIndicator={props.debitCreditIndicator} + /> + </HistoryRow> + // </a> + ); +} + +interface TransactionLayoutProps { + debitCreditIndicator: "debit" | "credit" | "unknown"; + amount: AmountString | "unknown"; + timestamp: Timestamp; + title: string; + id: string; + subtitle: string; + iconPath: string; + pending: boolean; +} + +interface TransactionAmountProps { + debitCreditIndicator: "debit" | "credit" | "unknown"; + amount: AmountString | "unknown"; + pending: boolean; +} + +function TransactionAmount(props: TransactionAmountProps): JSX.Element { + const [currency, amount] = props.amount.split(":"); + let sign: string; + switch (props.debitCreditIndicator) { + case "credit": + sign = "+"; + break; + case "debit": + sign = "-"; + break; + case "unknown": + sign = ""; + } + return ( + <Column style={{ + color: + props.pending ? "gray" : + (sign === '+' ? 'darkgreen' : + (sign === '-' ? 'darkred' : + undefined)) + }}> + <ExtraLargeText> + {sign} + {amount} + </ExtraLargeText> + <div>{currency}</div> + </Column> + ); +} + diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx index 4fa87a137..6579450b3 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { FunctionalComponent, h } from 'preact'; +import { createExample } from '../test-utils'; import { View as TestedComponent } from './Welcome'; @@ -28,12 +28,6 @@ export default { component: TestedComponent, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r -} - export const Normal = createExample(TestedComponent, { permissionsEnabled: true, diagnostics: { diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx index 4c33e1c72..0738e14b6 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -24,7 +24,7 @@ import { JSX } from "preact/jsx-runtime"; import { Checkbox } from "../components/Checkbox"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; import { Diagnostics } from "../components/Diagnostics"; -import { WalletPage } from "../components/styled"; +import { WalletBox } from "../components/styled"; import { useDiagnostics } from "../hooks/useDiagnostics"; import { WalletDiagnostics } from "@gnu-taler/taler-util"; @@ -44,12 +44,7 @@ export interface ViewProps { timedOut: boolean, } export function View({ permissionsEnabled, togglePermissions, diagnostics, timedOut }: ViewProps): JSX.Element { - return (<WalletPage> - <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;"> - <h1 style="font-family: monospace; font-size: 250%;"> - <span style="color: #aa3939;">❰</span>Taler Wallet<span style="color: #aa3939;">❱</span> - </h1> - </div> + return (<WalletBox> <h1>Browser Extension Installed!</h1> <div> <p>Thank you for installing the wallet.</p> @@ -68,6 +63,6 @@ export function View({ permissionsEnabled, togglePermissions, diagnostics, timed Learn how to top up your wallet balance » </a> </div> - </WalletPage> + </WalletBox> ); } diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index f487e54fc..f8191a0fb 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -20,17 +20,24 @@ * @author Florian Dold <dold@taler.net> */ -import { render } from "preact"; +import { Fragment, render } from "preact"; import { setupI18n } from "@gnu-taler/taler-util"; import { strings } from "./i18n/strings"; import { createHashHistory } from 'history'; -import { WithdrawPage } from "./wallet/Withdraw"; import { WelcomePage } from "./wallet/Welcome"; -import { PayPage } from "./wallet/Pay"; -import { RefundPage } from "./wallet/Refund"; -import { TipPage } from './wallet/Tip'; +import { HistoryPage } from "./wallet/History"; +import { WithdrawPage } from "./cta/Withdraw"; +import { PayPage } from "./cta/Pay"; +import { RefundPage } from "./cta/Refund"; +import { TipPage } from './cta/Tip'; import Router, { route, Route } from "preact-router"; +import { DevContextProvider } from "./context/devContext"; +import { LogoHeader } from "./components/LogoHeader"; +import { useEffect } from "preact/hooks"; +import { + Pages, WalletNavBar +} from "./NavigationBar"; function main(): void { try { @@ -53,32 +60,43 @@ if (document.readyState === "loading") { main(); } - -enum Pages { - welcome = '/welcome', - pay = '/pay', - payback = '/payback', - refund = '/refund', - reset_required = '/reset-required', - return_coins = '/return-coins', - tips = '/tips', - withdraw = '/withdraw', +function withLogoAndNavBar(Component: any) { + return () => <Fragment> + <LogoHeader /> + <WalletNavBar /> + <Component /> + </Fragment> } function Application() { - const h = createHashHistory(); - return <Router history={h} > + return <div> + <DevContextProvider> + <Router history={createHashHistory()} > + + <Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} /> - <Route path={Pages.welcome} component={WelcomePage} /> - <Route path={Pages.pay} component={PayPage} /> - <Route path={Pages.refund} component={RefundPage} /> + <Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} /> + <Route path={Pages.transaction} component={withLogoAndNavBar(HistoryPage)} /> - <Route path={Pages.tips} component={TipPage} /> - <Route path={Pages.withdraw} component={WithdrawPage} /> + <Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} /> + <Route path={Pages.payback} component={() => <div>no yet implemented</div>} /> + <Route path={Pages.return_coins} component={() => <div>no yet implemented</div>} /> - <Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} /> - <Route path={Pages.payback} component={() => <div>no yet implemented</div>} /> - <Route path={Pages.return_coins} component={() => <div>no yet implemented</div>} /> + {/** call to action */} + <Route path={Pages.pay} component={PayPage} /> + <Route path={Pages.refund} component={RefundPage} /> + <Route path={Pages.tips} component={TipPage} /> + <Route path={Pages.withdraw} component={WithdrawPage} /> + + <Route default component={Redirect} to={Pages.history} /> + </Router> + </DevContextProvider> + </div> +} - </Router> +function Redirect({ to }: { to: string }): null { + useEffect(() => { + route(to, true) + }) + return null } |