diff options
35 files changed, 1521 insertions, 1067 deletions
diff --git a/packages/taler-wallet-webextension/.storybook/preview.js b/packages/taler-wallet-webextension/.storybook/preview.js index 6331a7fa8..b770d7b63 100644 --- a/packages/taler-wallet-webextension/.storybook/preview.js +++ b/packages/taler-wallet-webextension/.storybook/preview.js @@ -48,7 +48,7 @@ export const decorators = [ const isTestingHeader = (/.*\/header\/?.*/.test(kind)); if (isTestingHeader) { // simple box with correct width and height - return <div style={{ width: 400, height: 320 }}> + return <div style={{ width: "fit-content" }}> <Story /> </div> } @@ -90,7 +90,7 @@ export const decorators = [ font-family: Arial, Helvetica, sans-serif; }`} </style> - <div style={{ width: 400, border: 'black solid 1px' }}> + <div style={{ border: 'black solid 1px', width: "fit-content" }}> <Body /> </div> </div> diff --git a/packages/taler-wallet-webextension/clean_and_build.sh b/packages/taler-wallet-webextension/clean_and_build.sh index be20d80d9..0cfbe0946 100755 --- a/packages/taler-wallet-webextension/clean_and_build.sh +++ b/packages/taler-wallet-webextension/clean_and_build.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # This file is in the public domain. +set -e [ "also-wallet" == "$1" ] && { pnpm -C ../taler-wallet-core/ compile || exit 1; } [ "also-util" == "$1" ] && { pnpm -C ../taler-util/ prepare || exit 1; } pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index c02e48983..44e8af78e 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -28,18 +28,18 @@ import { i18n } from "@gnu-taler/taler-util"; import { ComponentChildren, h, VNode } from "preact"; import Match from "preact-router/match"; import { PopupNavigation } from "./components/styled"; -import { useDevContext } from "./context/devContext"; export enum Pages { welcome = "/welcome", balance = "/balance", - manual_withdraw = "/manual-withdraw", + balance_history = "/balance/history/:currency", + manual_withdraw = "/manual-withdraw/:currency?", deposit = "/deposit/:currency", settings = "/settings", dev = "/dev", cta = "/cta/:action", backup = "/backup", - history = "/history", + last_activity = "/last-activity", transaction = "/transaction/:tid", provider_detail = "/provider/:pid", provider_add = "/provider/add", @@ -78,7 +78,10 @@ export function NavBar({ devMode, path }: { path: string; devMode: boolean }) { <PopupNavigation devMode={devMode}> <div> <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab> - <Tab target="/history" current={path}>{i18n.str`History`}</Tab> + <Tab + target="/last-activity" + current={path} + >{i18n.str`Last Activity`}</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>} @@ -87,8 +90,8 @@ export function NavBar({ devMode, path }: { path: string; devMode: boolean }) { ); } -export function WalletNavBar() { - const { devMode } = useDevContext(); +export function WalletNavBar({ devMode }: { devMode: boolean }) { + // const { devMode } = useDevContext(); return ( <Match> {({ path }: any) => { diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx index 05a7d28dd..c69625cd2 100644 --- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx +++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx @@ -14,31 +14,28 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util"; +import { Amounts, amountToPretty, Balance } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; -import { - ButtonPrimary, - TableWithRoundRows as TableWithRoundedRows, -} from "./styled"; +import { TableWithRoundRows as TableWithRoundedRows } from "./styled"; export function BalanceTable({ balances, - goToWalletDeposit, + goToWalletHistory, }: { balances: Balance[]; - goToWalletDeposit: (currency: string) => void; + goToWalletHistory: (currency: string) => void; }): VNode { - const currencyFormatter = new Intl.NumberFormat("en-US"); return ( <TableWithRoundedRows> {balances.map((entry, idx) => { const av = Amounts.parseOrThrow(entry.available); - const v = currencyFormatter.format( - av.value + av.fraction / amountFractionalBase, - ); return ( - <tr key={idx}> + <tr + key={idx} + onClick={() => goToWalletHistory(av.currency)} + style={{ cursor: "pointer" }} + > <td>{av.currency}</td> <td style={{ @@ -47,12 +44,7 @@ export function BalanceTable({ width: "100%", }} > - {v} - </td> - <td> - <ButtonPrimary onClick={() => goToWalletDeposit(av.currency)}> - Deposit - </ButtonPrimary> + {Amounts.stringifyValue(av)} </td> </tr> ); diff --git a/packages/taler-wallet-webextension/src/components/Loading.tsx b/packages/taler-wallet-webextension/src/components/Loading.tsx new file mode 100644 index 000000000..34edac551 --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/Loading.tsx @@ -0,0 +1,20 @@ +/* + 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 { h, VNode } from "preact"; + +export function Loading(): VNode { + return <div>Loading...</div>; +} diff --git a/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx new file mode 100644 index 000000000..70d53640d --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx @@ -0,0 +1,95 @@ +import { h, VNode } from "preact"; +import arrowDown from "../../static/img/chevron-down.svg"; +import { ButtonBoxPrimary, ButtonPrimary, ParagraphClickable } from "./styled"; +import { useState } from "preact/hooks"; + +export interface Props { + label: (s: string) => string; + actions: string[]; + onClick: (s: string) => void; +} + +/** + * functionality: it will receive a list of actions, take the first actions as + * the first chosen action + * the user may change the chosen action + * when the user click the button it will call onClick with the chosen action + * as argument + * + * visually: it is a primary button with a select handler on the right + * + * @returns + */ +export function MultiActionButton({ + label, + actions, + onClick: doClick, +}: Props): VNode { + const defaultAction = actions.length > 0 ? actions[0] : ""; + + const [opened, setOpened] = useState(false); + const [selected, setSelected] = useState<string>(defaultAction); + + const canChange = actions.length > 1; + const options = canChange ? actions.filter((a) => a !== selected) : []; + function select(m: string): void { + setSelected(m); + setOpened(false); + } + + if (!canChange) { + return ( + <ButtonPrimary onClick={() => doClick(selected)}> + {label(selected)} + </ButtonPrimary> + ); + } + + return ( + <div style={{ position: "relative", display: "inline-block" }}> + {opened && ( + <div + style={{ + position: "absolute", + bottom: 32 + 5, + right: 0, + marginLeft: 8, + marginRight: 8, + borderRadius: 5, + border: "1px solid blue", + background: "white", + boxShadow: "0px 8px 16px 0px rgba(0,0,0,0.2)", + zIndex: 1, + }} + > + {options.map((m) => ( + <ParagraphClickable key={m} onClick={() => select(m)}> + {label(m)} + </ParagraphClickable> + ))} + </div> + )} + <ButtonPrimary + onClick={() => doClick(selected)} + style={{ + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + marginRight: 0, + }} + > + {label(selected)} + </ButtonPrimary> + + <ButtonBoxPrimary + onClick={() => setOpened((s) => !s)} + style={{ + marginLeft: 0, + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }} + > + <img style={{ height: 14 }} src={arrowDown} /> + </ButtonBoxPrimary> + </div> + ); +} diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 216a1fabc..2d16b496c 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -43,7 +43,7 @@ export const WalletAction = styled.div` } section { margin-bottom: 2em; - & button { + button { margin-right: 8px; margin-left: 8px; } @@ -92,6 +92,10 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>` border-bottom: 1px solid black; border-top: 1px solid black; } + button { + margin-right: 8px; + margin-left: 8px; + } } & > header { @@ -123,7 +127,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>` justify-content: space-between; display: flex; background-color: #f7f7f7; - & button { + button { margin-right: 8px; margin-left: 8px; } @@ -136,9 +140,9 @@ export const Middle = styled.div` height: 100%; `; -export const PopupBox = styled.div<{ noPadding?: boolean }>` +export const PopupBox = styled.div<{ noPadding?: boolean; devMode: boolean }>` height: 290px; - width: 400px; + width: ${({ devMode }) => (!devMode ? "400px" : "500px")}; display: flex; flex-direction: column; justify-content: space-between; @@ -156,6 +160,10 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>` border-bottom: 1px solid black; border-top: 1px solid black; } + button { + margin-right: 8px; + margin-left: 8px; + } } & > section[data-expanded] { @@ -196,7 +204,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>` flex-direction: row; justify-content: space-between; display: flex; - & button { + button { margin-right: 8px; margin-left: 8px; } @@ -363,11 +371,11 @@ export const CenteredDialog = styled.div` export const Button = styled.button<{ upperCased?: boolean }>` display: inline-block; - zoom: 1; + /* zoom: 1; */ line-height: normal; white-space: nowrap; - vertical-align: middle; - text-align: center; + vertical-align: middle; //check this + /* text-align: center; */ cursor: pointer; user-select: none; box-sizing: border-box; @@ -379,7 +387,7 @@ export const Button = styled.button<{ upperCased?: boolean }>` /* color: #444; rgba not supported (IE 8) */ color: rgba(0, 0, 0, 0.8); /* rgba supported */ border: 1px solid #999; /*IE 6/7/8*/ - border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ + /* border: none rgba(0, 0, 0, 0); IE9 + everything else */ background-color: "#e6e6e6"; text-decoration: none; border-radius: 2px; @@ -401,11 +409,11 @@ export const Button = styled.button<{ upperCased?: boolean }>` } :hover { - filter: alpha(opacity=90); + filter: alpha(opacity=80); background-image: linear-gradient( transparent, - rgba(0, 0, 0, 0.05) 40%, - rgba(0, 0, 0, 0.1) + rgba(0, 0, 0, 0.1) 40%, + rgba(0, 0, 0, 0.2) ); } `; @@ -415,7 +423,7 @@ export const Link = styled.a<{ upperCased?: boolean }>` zoom: 1; line-height: normal; white-space: nowrap; - vertical-align: middle; + /* vertical-align: middle; */ text-align: center; cursor: pointer; user-select: none; @@ -463,8 +471,8 @@ export const FontIcon = styled.div` /* vertical-align: text-top; */ `; export const ButtonBox = styled(Button)` - padding: 0.5em; - font-size: x-small; + padding: 8px; + /* font-size: small; */ & > ${FontIcon} { width: 1em; @@ -472,12 +480,13 @@ export const ButtonBox = styled(Button)` display: inline; line-height: 0px; } - background-color: transparent; + background-color: #f7f7f7; border: 1px solid; border-radius: 4px; border-color: black; color: black; + /* text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); */ /* -webkit-border-horizontal-spacing: 0px; -webkit-border-vertical-spacing: 0px; */ `; @@ -499,6 +508,7 @@ export const LinkPrimary = styled(Link)` export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>` font-size: ${({ small }) => (small ? "small" : "inherit")}; background-color: rgb(66, 184, 221); + border-color: rgb(66, 184, 221); `; export const ButtonBoxPrimary = styled(ButtonBox)` color: rgb(66, 184, 221); @@ -714,6 +724,7 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>` border-top-right-radius: 0.25em; border-color: ${({ invalid }) => (!invalid ? "lightgray" : "red")}; } + margin-bottom: 16px; `; export const ErrorText = styled.div` @@ -772,13 +783,13 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>` display: flex; & > div { - width: 400px; + width: ${({ devMode }) => (!devMode ? "400px" : "500px")}; } & > div > a { color: #f8faf7; display: inline-block; - width: calc(400px / ${({ devMode }) => (!devMode ? 4 : 5)}); + width: 100px; text-align: center; text-decoration: none; vertical-align: middle; @@ -804,10 +815,9 @@ export const NiceSelect = styled.div` box-shadow: none; background-image: ${image}; - background-position: right 0.5rem center; + background-position: right 8px center; background-repeat: no-repeat; background-size: 1.5em 1.5em; - padding-right: 2.5rem; background-color: white; @@ -967,3 +977,17 @@ export const StyledCheckboxLabel = styled.div` box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor; } `; + +export const ParagraphClickable = styled.p` + cursor: pointer; + margin: 0px; + padding: 8px 16px; + :hover { + filter: alpha(opacity=80); + background-image: linear-gradient( + transparent, + rgba(0, 0, 0, 0.1) 40%, + rgba(0, 0, 0, 0.2) + ); + } +`; diff --git a/packages/taler-wallet-webextension/src/context/devContext.ts b/packages/taler-wallet-webextension/src/context/devContext.ts index 7ed6858a7..4b8ba2bcd 100644 --- a/packages/taler-wallet-webextension/src/context/devContext.ts +++ b/packages/taler-wallet-webextension/src/context/devContext.ts @@ -42,5 +42,6 @@ export const DevContextProvider = ({ children }: { children: any }): VNode => { const [value, setter] = useLocalStorage("devMode"); const devMode = value === "true"; const toggleDevMode = () => setter((v) => (!v ? "true" : undefined)); + children = children.length === 1 && typeof children === "function" ? children({ devMode }) : children; return h(Context.Provider, { value: { devMode, toggleDevMode }, children }); }; diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx b/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx new file mode 100644 index 000000000..df5947bb1 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx @@ -0,0 +1,168 @@ +/* + 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 { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util"; +import { createExample } from "../test-utils"; +import { PaymentRequestView as TestedComponent } from "./Deposit"; + +export default { + title: "cta/deposit", + component: TestedComponent, + argTypes: {}, +}; + +export const NoBalance = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.InsufficientBalance, + noncePriv: "", + proposalId: "proposal1234", + contractTerms: { + merchant: { + name: "someone", + }, + summary: "some beers", + amount: "USD:10", + } as Partial<ContractTerms> as any, + amountRaw: "USD:10", + }, +}); + +export const NoEnoughBalance = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.InsufficientBalance, + noncePriv: "", + proposalId: "proposal1234", + contractTerms: { + merchant: { + name: "someone", + }, + summary: "some beers", + amount: "USD:10", + } as Partial<ContractTerms> as any, + amountRaw: "USD:10", + }, + balance: { + currency: "USD", + fraction: 40000000, + value: 9, + }, +}); + +export const PaymentPossible = createExample(TestedComponent, { + uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + payStatus: { + status: PreparePayResultType.PaymentPossible, + amountEffective: "USD:10", + amountRaw: "USD:10", + noncePriv: "", + contractTerms: { + nonce: "123213123", + merchant: { + name: "someone", + }, + amount: "USD:10", + summary: "some beers", + } as Partial<ContractTerms> as any, + contractTermsHash: "123456", + proposalId: "proposal1234", + }, +}); + +export const PaymentPossibleWithFee = createExample(TestedComponent, { + uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + payStatus: { + status: PreparePayResultType.PaymentPossible, + amountEffective: "USD:10.20", + amountRaw: "USD:10", + noncePriv: "", + contractTerms: { + nonce: "123213123", + merchant: { + name: "someone", + }, + amount: "USD:10", + summary: "some beers", + } as Partial<ContractTerms> as any, + contractTermsHash: "123456", + proposalId: "proposal1234", + }, +}); + +export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.AlreadyConfirmed, + amountEffective: "USD:10", + amountRaw: "USD:10", + contractTerms: { + merchant: { + name: "someone", + }, + fulfillment_message: + "congratulations! you are looking at the fulfillment message! ", + summary: "some beers", + amount: "USD:10", + } as Partial<ContractTerms> as any, + contractTermsHash: "123456", + proposalId: "proposal1234", + paid: false, + }, +}); + +export const AlreadyConfirmedWithoutFullfilment = createExample( + TestedComponent, + { + payStatus: { + status: PreparePayResultType.AlreadyConfirmed, + amountEffective: "USD:10", + amountRaw: "USD:10", + contractTerms: { + merchant: { + name: "someone", + }, + summary: "some beers", + amount: "USD:10", + } as Partial<ContractTerms> as any, + contractTermsHash: "123456", + proposalId: "proposal1234", + paid: false, + }, + }, +); + +export const AlreadyPaid = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.AlreadyConfirmed, + amountEffective: "USD:10", + amountRaw: "USD:10", + contractTerms: { + merchant: { + name: "someone", + }, + fulfillment_message: + "congratulations! you are looking at the fulfillment message! ", + summary: "some beers", + amount: "USD:10", + } as Partial<ContractTerms> as any, + contractTermsHash: "123456", + proposalId: "proposal1234", + paid: true, + }, +}); diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.tsx b/packages/taler-wallet-webextension/src/cta/Deposit.tsx new file mode 100644 index 000000000..3696b0c2d --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Deposit.tsx @@ -0,0 +1,251 @@ +/* + This file is part of TALER + (C) 2015 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/> + */ + +/** + * Page shown to the user to confirm entering + * a contract. + */ + +/** + * Imports. + */ +// import * as i18n from "../i18n"; + +import { + AmountJson, + Amounts, + amountToPretty, + ConfirmPayResult, + ConfirmPayResultDone, + ConfirmPayResultType, + ContractTerms, + i18n, + NotificationType, + PreparePayResult, + PreparePayResultType, +} from "@gnu-taler/taler-util"; +import { OperationFailedError } from "@gnu-taler/taler-wallet-core"; +import { Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { ErrorTalerOperation } from "../components/ErrorTalerOperation"; +import { LogoHeader } from "../components/LogoHeader"; +import { Part } from "../components/Part"; +import { + ErrorBox, + SuccessBox, + WalletAction, + WarningBox, +} from "../components/styled"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import * as wxApi from "../wxApi"; + +interface Props { + talerPayUri?: string; + goBack: () => void; +} + +export function DepositPage({ talerPayUri, goBack }: Props): VNode { + const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>( + undefined, + ); + const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>( + undefined, + ); + const [payErrMsg, setPayErrMsg] = useState< + OperationFailedError | string | undefined + >(undefined); + + const balance = useAsyncAsHook(wxApi.getBalance, [ + NotificationType.CoinWithdrawn, + ]); + const balanceWithoutError = balance?.hasError + ? [] + : balance?.response.balances || []; + + const foundBalance = balanceWithoutError.find( + (b) => + payStatus && + Amounts.parseOrThrow(b.available).currency === + Amounts.parseOrThrow(payStatus?.amountRaw).currency, + ); + const foundAmount = foundBalance + ? Amounts.parseOrThrow(foundBalance.available) + : undefined; + // We use a string here so that dependency tracking for useEffect works properly + const foundAmountStr = foundAmount + ? Amounts.stringify(foundAmount) + : undefined; + + useEffect(() => { + if (!talerPayUri) return; + const doFetch = async (): Promise<void> => { + try { + const p = await wxApi.preparePay(talerPayUri); + setPayStatus(p); + } catch (e) { + console.log("Got error while trying to pay", e); + if (e instanceof OperationFailedError) { + setPayErrMsg(e); + } + if (e instanceof Error) { + setPayErrMsg(e.message); + } + } + }; + doFetch(); + }, [talerPayUri, foundAmountStr]); + + if (!talerPayUri) { + return <span>missing pay uri</span>; + } + + if (!payStatus) { + if (payErrMsg instanceof OperationFailedError) { + return ( + <WalletAction> + <LogoHeader /> + <h2>{i18n.str`Digital cash payment`}</h2> + <section> + <ErrorTalerOperation + title="Could not get the payment information for this order" + error={payErrMsg?.operationError} + /> + </section> + </WalletAction> + ); + } + if (payErrMsg) { + return ( + <WalletAction> + <LogoHeader /> + <h2>{i18n.str`Digital cash payment`}</h2> + <section> + <p>Could not get the payment information for this order</p> + <ErrorBox>{payErrMsg}</ErrorBox> + </section> + </WalletAction> + ); + } + return <span>Loading payment information ...</span>; + } + + const onClick = async (): Promise<void> => { + // try { + // const res = await doPayment(payStatus); + // setPayResult(res); + // } catch (e) { + // console.error(e); + // if (e instanceof Error) { + // setPayErrMsg(e.message); + // } + // } + }; + + return ( + <PaymentRequestView + uri={talerPayUri} + payStatus={payStatus} + payResult={payResult} + onClick={onClick} + balance={foundAmount} + /> + ); +} + +export interface PaymentRequestViewProps { + payStatus: PreparePayResult; + payResult?: ConfirmPayResult; + onClick: () => void; + payErrMsg?: string; + uri: string; + balance: AmountJson | undefined; +} +export function PaymentRequestView({ + uri, + payStatus, + payResult, + onClick, + balance, +}: PaymentRequestViewProps): VNode { + let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw); + const contractTerms: ContractTerms = payStatus.contractTerms; + + return ( + <WalletAction> + <LogoHeader /> + + <h2>{i18n.str`Digital cash deposit`}</h2> + {payStatus.status === PreparePayResultType.AlreadyConfirmed && + (payStatus.paid ? ( + <SuccessBox> Already paid </SuccessBox> + ) : ( + <WarningBox> Already claimed </WarningBox> + ))} + {payResult && payResult.type === ConfirmPayResultType.Done && ( + <SuccessBox> + <h3>Payment complete</h3> + <p> + {!payResult.contractTerms.fulfillment_message + ? "You will now be sent back to the merchant you came from." + : payResult.contractTerms.fulfillment_message} + </p> + </SuccessBox> + )} + <section> + {payStatus.status !== PreparePayResultType.InsufficientBalance && + Amounts.isNonZero(totalFees) && ( + <Part + big + title="Total to pay" + text={amountToPretty( + Amounts.parseOrThrow(payStatus.amountEffective), + )} + kind="negative" + /> + )} + <Part + big + title="Purchase amount" + text={amountToPretty(Amounts.parseOrThrow(payStatus.amountRaw))} + kind="neutral" + /> + {Amounts.isNonZero(totalFees) && ( + <Fragment> + <Part + big + title="Fee" + text={amountToPretty(totalFees)} + kind="negative" + /> + </Fragment> + )} + <Part + title="Merchant" + text={contractTerms.merchant.name} + kind="neutral" + /> + <Part title="Purchase" text={contractTerms.summary} kind="neutral" /> + {contractTerms.order_id && ( + <Part + title="Receipt" + text={`#${contractTerms.order_id}`} + kind="neutral" + /> + )} + </section> + </WalletAction> + ); +} diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index d7419d410..e61d3a9d6 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -57,35 +57,10 @@ import * as wxApi from "../wxApi"; interface Props { talerPayUri?: string; - goToWalletManualWithdraw: () => void; + goToWalletManualWithdraw: (currency?: string) => void; + goBack: () => void; } -// export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) { -// const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; -// let message; -// if (fulfillmentUrl) { -// message = ( -// <span> -// You have already paid for this article. Click{" "} -// <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again. -// </span> -// ); -// } else { -// message = <span> -// You have already paid for this article:{" "} -// <em> -// {payStatus.contractTerms.fulfillment_message ?? "no message given"} -// </em> -// </span>; -// } -// return <section class="main"> -// <h1>GNU Taler Wallet</h1> -// <article class="fade"> -// {message} -// </article> -// </section> -// } - const doPayment = async ( payStatus: PreparePayResult, ): Promise<ConfirmPayResultDone> => { diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx index a4988cf2d..1af3b5858 100644 --- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { createExample, NullLink } from "../test-utils"; +import { createExample } from "../test-utils"; import { BalanceView as TestedComponent } from "./BalancePage"; export default { @@ -28,211 +28,124 @@ export default { argTypes: {}, }; -export const NotYetLoaded = createExample(TestedComponent, {}); - -export const GotError = createExample(TestedComponent, { - balance: { - hasError: true, - message: "Network error", - }, - Linker: NullLink, -}); - export const EmptyBalance = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [], - }, - }, - Linker: NullLink, + balances: [], }); export const SomeCoins = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:10.5", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - ], + balances: [ + { + available: "USD:10.5", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + ], }); -export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:2.23", - hasPendingTransactions: false, - pendingIncoming: "USD:5.11", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - ], +export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, { + balances: [ + { + available: "EUR:1", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, -}); - -export const SomeCoinsAndOutgoingMoney = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:2.23", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:5.11", - requiresUserInput: false, - }, - ], + { + available: "TESTKUDOS:2000", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, -}); - -export const SomeCoinsAndMovingMoney = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:2.23", - hasPendingTransactions: false, - pendingIncoming: "USD:2", - pendingOutgoing: "USD:5.11", - requiresUserInput: false, - }, - ], + { + available: "JPY:4", + hasPendingTransactions: false, + pendingIncoming: "EUR:15", + pendingOutgoing: "EUR:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + ], }); -export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:2", - hasPendingTransactions: false, - pendingIncoming: "USD:5.1", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "EUR:4", - hasPendingTransactions: false, - pendingIncoming: "EUR:0", - pendingOutgoing: "EUR:3.01", - requiresUserInput: false, - }, - ], +export const NoCoinsInTreeCurrencies = createExample(TestedComponent, { + balances: [ + { + available: "EUR:3", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, -}); - -export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:1", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "TESTKUDOS:2000", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "EUR:4", - hasPendingTransactions: false, - pendingIncoming: "EUR:15", - pendingOutgoing: "EUR:0", - requiresUserInput: false, - }, - ], + { + available: "USD:2", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "ARS:1", + hasPendingTransactions: false, + pendingIncoming: "EUR:15", + pendingOutgoing: "EUR:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + ], }); export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:13451", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "EUR:202.02", - hasPendingTransactions: false, - pendingIncoming: "EUR:0", - pendingOutgoing: "EUR:0", - requiresUserInput: false, - }, - { - available: "ARS:30", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "JPY:51223233", - hasPendingTransactions: false, - pendingIncoming: "EUR:0", - pendingOutgoing: "EUR:0", - requiresUserInput: false, - }, - { - available: "JPY:51223233", - hasPendingTransactions: false, - pendingIncoming: "EUR:0", - pendingOutgoing: "EUR:0", - requiresUserInput: false, - }, - { - available: "DEMOKUDOS:6", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "TESTKUDOS:6", - hasPendingTransactions: false, - pendingIncoming: "USD:5", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - ], + balances: [ + { + available: "USD:0", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "ARS:13451", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "EUR:202.02", + hasPendingTransactions: false, + pendingIncoming: "EUR:0", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + { + available: "JPY:0", + hasPendingTransactions: false, + pendingIncoming: "EUR:0", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + { + available: "JPY:51223233", + hasPendingTransactions: false, + pendingIncoming: "EUR:0", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + { + available: "DEMOKUDOS:6", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "TESTKUDOS:6", + hasPendingTransactions: false, + pendingIncoming: "USD:5", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + ], }); diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index 3eb5f4270..014d3b18e 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -14,70 +14,81 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { BalancesResponse, i18n } from "@gnu-taler/taler-util"; +import { Amounts, Balance, i18n } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { BalanceTable } from "../components/BalanceTable"; import { ButtonPrimary, ErrorBox } from "../components/styled"; -import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { PageLink } from "../renderHtml"; import * as wxApi from "../wxApi"; +import { MultiActionButton } from "../components/MultiActionButton"; +import { Loading } from "../components/Loading"; + interface Props { goToWalletDeposit: (currency: string) => void; + goToWalletHistory: (currency: string) => void; goToWalletManualWithdraw: () => void; } export function BalancePage({ goToWalletManualWithdraw, goToWalletDeposit, + goToWalletHistory, }: Props): VNode { const state = useAsyncAsHook(wxApi.getBalance); + const balances = !state || state.hasError ? [] : state.response.balances; + + if (!state) { + return <Loading />; + } + + if (state.hasError) { + return ( + <Fragment> + <ErrorBox>{state.message}</ErrorBox> + <p> + Click <PageLink pageName="welcome">here</PageLink> for help and + diagnostics. + </p> + </Fragment> + ); + } + return ( <BalanceView - balance={state} - Linker={PageLink} + balances={balances} goToWalletManualWithdraw={goToWalletManualWithdraw} goToWalletDeposit={goToWalletDeposit} + goToWalletHistory={goToWalletHistory} /> ); } export interface BalanceViewProps { - balance: HookResponse<BalancesResponse>; - Linker: typeof PageLink; + balances: Balance[]; goToWalletManualWithdraw: () => void; goToWalletDeposit: (currency: string) => void; + goToWalletHistory: (currency: string) => void; } export function BalanceView({ - balance, - Linker, + balances, goToWalletManualWithdraw, goToWalletDeposit, + goToWalletHistory, }: BalanceViewProps): VNode { - if (!balance) { - return <div>Loading...</div>; - } + const currencyWithNonZeroAmount = balances + .filter((b) => !Amounts.isZero(b.available)) + .map((b) => b.available.split(":")[0]); - if (balance.hasError) { - return ( - <Fragment> - <ErrorBox>{balance.message}</ErrorBox> - <p> - Click <Linker pageName="welcome">here</Linker> for help and - diagnostics. - </p> - </Fragment> - ); - } - if (balance.response.balances.length === 0) { + if (balances.length === 0) { return ( <Fragment> <p> <i18n.Translate> You have no balance to show. Need some{" "} - <Linker pageName="/welcome">help</Linker> getting started? + <PageLink pageName="/welcome">help</PageLink> getting started? </i18n.Translate> </p> <footer style={{ justifyContent: "space-between" }}> - <div /> <ButtonPrimary onClick={goToWalletManualWithdraw}> Withdraw </ButtonPrimary> @@ -90,15 +101,21 @@ export function BalanceView({ <Fragment> <section> <BalanceTable - balances={balance.response.balances} - goToWalletDeposit={goToWalletDeposit} + balances={balances} + goToWalletHistory={goToWalletHistory} /> </section> <footer style={{ justifyContent: "space-between" }}> - <div /> <ButtonPrimary onClick={goToWalletManualWithdraw}> Withdraw </ButtonPrimary> + {currencyWithNonZeroAmount.length > 0 && ( + <MultiActionButton + label={(s) => `Deposit ${s}`} + actions={currencyWithNonZeroAmount} + onClick={(c) => goToWalletDeposit(c)} + /> + )} </footer> </Fragment> ); diff --git a/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx index aec288dec..840398a44 100644 --- a/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx @@ -86,10 +86,6 @@ export function View({ return ( <div> <p>Debug tools:</p> - <button onClick={openExtensionPage("/static/popup.html")}> - wallet tab - </button> - <button onClick={confirmReset}>reset</button> <br /> <button onClick={onExportDatabase}>export database</button> @@ -109,7 +105,8 @@ export function View({ "yyyy/MM/dd_HH:mm", )}.json`} > - click here + {" "} + click here{" "} </a> to download </div> diff --git a/packages/taler-wallet-webextension/src/popup/History.stories.tsx b/packages/taler-wallet-webextension/src/popup/History.stories.tsx deleted file mode 100644 index 43d39da82..000000000 --- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx +++ /dev/null @@ -1,213 +0,0 @@ -/* - 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 { createExample } from "../test-utils"; -import { HistoryView as TestedComponent } from "./History"; - -export default { - title: "popup/history/list", - component: TestedComponent, -}; - -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.demo.taler.net", - withdrawalDetails: { - reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", - 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, -}; - -export const EmptyWithBalance = createExample(TestedComponent, { - list: [], - balances: [ - { - available: "TESTKUDOS:10", - pendingIncoming: "TESTKUDOS:0", - pendingOutgoing: "TESTKUDOS:0", - hasPendingTransactions: false, - requiresUserInput: false, - }, - ], -}); - -export const EmptyWithNoBalance = createExample(TestedComponent, { - list: [], - balances: [], -}); - -export const One = createExample(TestedComponent, { - list: [exampleData.withdraw], - balances: [ - { - available: "USD:10", - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - hasPendingTransactions: false, - requiresUserInput: false, - }, - ], -}); - -export const OnePending = createExample(TestedComponent, { - list: [ - { - ...exampleData.withdraw, - pending: true, - }, - ], - 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, - }, - ], -}); diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx deleted file mode 100644 index 2dfddb8c4..000000000 --- a/packages/taler-wallet-webextension/src/popup/History.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - 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, - i18n, - Transaction, - TransactionsResponse, -} from "@gnu-taler/taler-util"; -import { Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { ButtonPrimary } from "../components/styled"; -import { TransactionItem } from "../components/TransactionItem"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; -import * as wxApi from "../wxApi"; -import { AddNewActionView } from "./AddNewActionView"; - -export function HistoryPage(): VNode { - const [transactions, setTransactions] = useState< - TransactionsResponse | undefined - >(undefined); - const balance = useAsyncAsHook(wxApi.getBalance); - const balanceWithoutError = balance?.hasError - ? [] - : balance?.response.balances || []; - - useEffect(() => { - const fetchData = async (): Promise<void> => { - const res = await wxApi.getTransactions(); - setTransactions(res); - }; - fetchData(); - }, []); - - const [addingAction, setAddingAction] = useState(false); - - if (addingAction) { - return <AddNewActionView onCancel={() => setAddingAction(false)} />; - } - - if (!transactions) { - return <div>Loading ...</div>; - } - - return ( - <HistoryView - balances={balanceWithoutError} - list={[...transactions.transactions].reverse()} - onAddNewAction={() => setAddingAction(true)} - /> - ); -} - -function amountToString(c: AmountString): string { - const idx = c.indexOf(":"); - return `${c.substring(idx + 1)} ${c.substring(0, idx)}`; -} - -export function HistoryView({ - list, - balances, - onAddNewAction, -}: { - list: Transaction[]; - balances: Balance[]; - onAddNewAction: () => void; -}): VNode { - const multiCurrency = balances.length > 1; - return ( - <Fragment> - <header> - {balances.length > 0 ? ( - <Fragment> - {multiCurrency ? ( - <div class="title"> - Balance:{" "} - <ul style={{ margin: 0 }}> - {balances.map((b, i) => ( - <li key={i}>{b.available}</li> - ))} - </ul> - </div> - ) : ( - <div class="title"> - Balance: <span>{amountToString(balances[0].available)}</span> - </div> - )} - </Fragment> - ) : ( - <div /> - )} - <div> - <ButtonPrimary onClick={onAddNewAction}> - <b>+</b> - </ButtonPrimary> - </div> - </header> - {list.length === 0 ? ( - <section data-expanded data-centered> - <p> - <i18n.Translate> - You have no history yet, here you will be able to check your last - transactions. - </i18n.Translate> - </p> - </section> - ) : ( - <section> - {list.slice(0, 3).map((tx, i) => ( - <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} /> - ))} - </section> - )} - <footer style={{ justifyContent: "space-around" }}> - {list.length > 0 && ( - <a - target="_blank" - rel="noopener noreferrer" - style={{ color: "darkgreen", textDecoration: "none" }} - href={ - // eslint-disable-next-line no-undef - typeof chrome !== "undefined" && chrome.extension - ? // eslint-disable-next-line no-undef - chrome.extension.getURL(`/static/wallet.html#/history`) - : "#" - } - > - VIEW MORE TRANSACTIONS - </a> - )} - </footer> - </Fragment> - ); -} diff --git a/packages/taler-wallet-webextension/src/popup/index.stories.tsx b/packages/taler-wallet-webextension/src/popup/index.stories.tsx index c3e60c4ed..3abb80021 100644 --- a/packages/taler-wallet-webextension/src/popup/index.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/index.stories.tsx @@ -19,11 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import * as a1 from "./AddNewActionView.stories"; +import * as a1 from "../wallet/AddNewActionView.stories"; import * as a2 from "./Balance.stories"; import * as a3 from "./DeveloperPage.stories"; -import * as a4 from "./History.stories"; import * as a5 from "./Popup.stories"; import * as a6 from "./TalerActionFound.stories"; -export default [a1, a2, a3, a4, a5, a6]; +export default [a1, a2, a3, a5, a6]; diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index 27372db5e..908349e89 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -33,13 +33,13 @@ import { Pages, WalletNavBar } from "./NavigationBar"; import { BackupPage } from "./wallet/BackupPage"; import { BalancePage } from "./popup/BalancePage"; import { DeveloperPage } from "./popup/DeveloperPage"; -import { HistoryPage } from "./popup/History"; import { ProviderAddPage } from "./wallet/ProviderAddPage"; import { ProviderDetailPage } from "./wallet/ProviderDetailPage"; import { SettingsPage } from "./popup/Settings"; import { TalerActionFound } from "./popup/TalerActionFound"; import { ExchangeAddPage } from "./wallet/ExchangeAddPage"; import { IoCProviderForRuntime } from "./context/iocContext"; +import { LastActivityPage } from "./wallet/LastActivityPage"; function main(): void { try { @@ -77,12 +77,13 @@ function CheckTalerActionComponent(): VNode { function Application() { return ( - <div> - <DevContextProvider> + // <div> + <DevContextProvider> + {({ devMode }: { devMode: boolean }) => ( <IoCProviderForRuntime> - <WalletNavBar /> + <WalletNavBar devMode={devMode} /> <CheckTalerActionComponent /> - <PopupBox> + <PopupBox devMode={devMode}> <Router history={createHashHistory()}> <Route path={Pages.dev} component={DeveloperPage} /> @@ -90,10 +91,14 @@ function Application() { path={Pages.balance} component={BalancePage} goToWalletManualWithdraw={() => - goToWalletPage(Pages.manual_withdraw) + goToWalletPage( + Pages.manual_withdraw.replace(":currency?", ""), + ) } - goToWalletDeposit={(currency: string) => - goToWalletPage(Pages.deposit.replace(":currency", currency)) + goToWalletHistory={(currency: string) => + goToWalletPage( + Pages.balance_history.replace(":currency", currency), + ) } /> <Route path={Pages.settings} component={SettingsPage} /> @@ -114,6 +119,8 @@ function Application() { }} /> + <Route path={Pages.last_activity} component={LastActivityPage} /> + <Route path={Pages.transaction} component={({ tid }: { tid: string }) => @@ -121,8 +128,6 @@ function Application() { } /> - <Route path={Pages.history} component={HistoryPage} /> - <Route path={Pages.backup} component={BackupPage} @@ -157,8 +162,9 @@ function Application() { </Router> </PopupBox> </IoCProviderForRuntime> - </DevContextProvider> - </div> + )} + </DevContextProvider> + // </div> ); } diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx index 15986d5d1..ba98ae237 100644 --- a/packages/taler-wallet-webextension/src/renderHtml.tsx +++ b/packages/taler-wallet-webextension/src/renderHtml.tsx @@ -162,7 +162,12 @@ export function PageLink(props: { children?: ComponentChildren; }): VNode { // eslint-disable-next-line no-undef - const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`); + + const url = + typeof chrome === "undefined" + ? undefined + : // eslint-disable-next-line no-undef + chrome.extension?.getURL(`/static/wallet.html#/${props.pageName}`); return ( <a class="actionLink" href={url} target="_blank" rel="noopener noreferrer"> {props.children} diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts index fbb7c56ff..8c721a9d5 100644 --- a/packages/taler-wallet-webextension/src/test-utils.ts +++ b/packages/taler-wallet-webextension/src/test-utils.ts @@ -117,5 +117,6 @@ export function mountBrowser<T>(callback: () => T, Context?: ({ children }: { ch } } +const nullTestFunction = {} as TestFunction export const justBrowser_it: PendingTestFunction | TestFunction = - typeof window === 'undefined' ? it.skip : it
\ No newline at end of file + typeof it === 'undefined' ? nullTestFunction : (typeof window === 'undefined' ? it.skip : it)
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/popup/AddNewActionView.stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx index 6ee56ef77..54e4eb1f2 100644 --- a/packages/taler-wallet-webextension/src/popup/AddNewActionView.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx @@ -23,7 +23,7 @@ import { createExample } from "../test-utils"; import { AddNewActionView as TestedComponent } from "./AddNewActionView"; export default { - title: "popup/add new action", + title: "wallet/add new action", component: TestedComponent, argTypes: { setDeviceName: () => Promise.resolve(), diff --git a/packages/taler-wallet-webextension/src/popup/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx index d4158973e..d4158973e 100644 --- a/packages/taler-wallet-webextension/src/popup/AddNewActionView.tsx +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx diff --git a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx index 2432c31eb..6c670b019 100644 --- a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { createExample, NullLink } from "../test-utils"; +import { createExample } from "../test-utils"; import { BalanceView as TestedComponent } from "./BalancePage"; export default { @@ -28,83 +28,124 @@ export default { argTypes: {}, }; -export const NotYetLoaded = createExample(TestedComponent, {}); - -export const GotError = createExample(TestedComponent, { - balance: { - hasError: true, - message: "Network error", - }, - Linker: NullLink, +export const EmptyBalance = createExample(TestedComponent, { + balances: [], }); -export const EmptyBalance = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [], +export const SomeCoins = createExample(TestedComponent, { + balances: [ + { + available: "USD:10.5", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + ], }); -export const SomeCoins = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:10.5", - hasPendingTransactions: false, - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - ], +export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, { + balances: [ + { + available: "EUR:1", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "TESTKUDOS:2000", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + { + available: "JPY:4", + hasPendingTransactions: false, + pendingIncoming: "EUR:15", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + ], }); -export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:2.23", - hasPendingTransactions: false, - pendingIncoming: "USD:5.11", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - ], +export const NoCoinsInTreeCurrencies = createExample(TestedComponent, { + balances: [ + { + available: "EUR:3", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "USD:2", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + { + available: "ARS:1", + hasPendingTransactions: false, + pendingIncoming: "EUR:15", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + ], }); -export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, { - balance: { - hasError: false, - response: { - balances: [ - { - available: "USD:2", - hasPendingTransactions: false, - pendingIncoming: "USD:5", - pendingOutgoing: "USD:0", - requiresUserInput: false, - }, - { - available: "EUR:4", - hasPendingTransactions: false, - pendingIncoming: "EUR:5", - pendingOutgoing: "EUR:0", - requiresUserInput: false, - }, - ], +export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, { + balances: [ + { + available: "USD:0", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "ARS:13451", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "EUR:202.02", + hasPendingTransactions: false, + pendingIncoming: "EUR:0", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + { + available: "JPY:0", + hasPendingTransactions: false, + pendingIncoming: "EUR:0", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + { + available: "JPY:51223233", + hasPendingTransactions: false, + pendingIncoming: "EUR:0", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + { + available: "DEMOKUDOS:6", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "TESTKUDOS:6", + hasPendingTransactions: false, + pendingIncoming: "USD:5", + pendingOutgoing: "USD:0", + requiresUserInput: false, }, - }, - Linker: NullLink, + ], }); diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx index 33182a38d..5fa08f8a6 100644 --- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx @@ -14,68 +14,87 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { BalancesResponse, i18n } from "@gnu-taler/taler-util"; +import { Amounts, Balance, i18n } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { BalanceTable } from "../components/BalanceTable"; -import { ButtonPrimary, Centered, ErrorBox } from "../components/styled"; -import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import { Loading } from "../components/Loading"; +import { MultiActionButton } from "../components/MultiActionButton"; +import { + ButtonPrimary, + Centered, + ErrorBox, + SuccessBox, +} from "../components/styled"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { PageLink } from "../renderHtml"; import * as wxApi from "../wxApi"; +interface Props { + goToWalletDeposit: (currency: string) => void; + goToWalletHistory: (currency: string) => void; + goToWalletManualWithdraw: () => void; +} + export function BalancePage({ goToWalletManualWithdraw, goToWalletDeposit, -}: { - goToWalletDeposit: (currency: string) => void; - goToWalletManualWithdraw: () => void; -}): VNode { + goToWalletHistory, +}: Props): VNode { const state = useAsyncAsHook(wxApi.getBalance); + + const balances = !state || state.hasError ? [] : state.response.balances; + + if (!state) { + return <Loading />; + } + + if (state.hasError) { + return ( + <Fragment> + <ErrorBox>{state.message}</ErrorBox> + <p> + Click <PageLink pageName="welcome">here</PageLink> for help and + diagnostics. + </p> + </Fragment> + ); + } + return ( <BalanceView - balance={state} - Linker={PageLink} + balances={balances} goToWalletManualWithdraw={goToWalletManualWithdraw} goToWalletDeposit={goToWalletDeposit} + goToWalletHistory={goToWalletHistory} /> ); } export interface BalanceViewProps { - balance: HookResponse<BalancesResponse>; - Linker: typeof PageLink; + balances: Balance[]; goToWalletManualWithdraw: () => void; goToWalletDeposit: (currency: string) => void; + goToWalletHistory: (currency: string) => void; } export function BalanceView({ - balance, - Linker, + balances, goToWalletManualWithdraw, goToWalletDeposit, + goToWalletHistory, }: BalanceViewProps): VNode { - if (!balance) { - return <div>Loading...</div>; - } + const currencyWithNonZeroAmount = balances + .filter((b) => !Amounts.isZero(b.available)) + .map((b) => b.available.split(":")[0]); - if (balance.hasError) { - return ( - <Fragment> - <ErrorBox>{balance.message}</ErrorBox> - <p> - Click <Linker pageName="welcome">here</Linker> for help and - diagnostics. - </p> - </Fragment> - ); - } - if (balance.response.balances.length === 0) { + if (balances.length === 0) { return ( <Fragment> <p> <Centered style={{ marginTop: 100 }}> <i18n.Translate> You have no balance to show. Need some{" "} - <Linker pageName="/welcome">help</Linker> getting started? + <PageLink pageName="/welcome">help</PageLink> getting started? </i18n.Translate> </Centered> </p> @@ -93,15 +112,21 @@ export function BalanceView({ <Fragment> <section> <BalanceTable - balances={balance.response.balances} - goToWalletDeposit={goToWalletDeposit} + balances={balances} + goToWalletHistory={goToWalletHistory} /> </section> <footer style={{ justifyContent: "space-between" }}> - <div /> <ButtonPrimary onClick={goToWalletManualWithdraw}> Withdraw </ButtonPrimary> + {currencyWithNonZeroAmount.length > 0 && ( + <MultiActionButton + label={(s) => `Deposit ${s}`} + actions={currencyWithNonZeroAmount} + onClick={(c) => goToWalletDeposit(c)} + /> + )} </footer> </Fragment> ); diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 36feeb76e..f32a2aa5c 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -41,12 +41,14 @@ export interface Props { exchangeList: Record<string, string>; onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>; onAddExchange: () => void; + initialCurrency?: string; } export function CreateManualWithdraw({ initialAmount, exchangeList, error, + initialCurrency, onCreate, onAddExchange, }: Props): VNode { @@ -61,8 +63,16 @@ export function CreateManualWithdraw({ {} as Record<string, string>, ); + const foundExchangeForCurrency = exchangeSelectList.findIndex( + (e) => exchangeList[e] === initialCurrency, + ); + const initialExchange = - exchangeSelectList.length > 0 ? exchangeSelectList[0] : ""; + foundExchangeForCurrency !== -1 + ? exchangeSelectList[foundExchangeForCurrency] + : exchangeSelectList.length > 0 + ? exchangeSelectList[0] + : ""; const [exchange, setExchange] = useState(initialExchange || ""); const [currency, setCurrency] = useState(exchangeList[initialExchange] ?? ""); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx index 5c931394d..9e15daa97 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx @@ -23,23 +23,24 @@ import { import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; -import { Part } from "../components/Part"; +import { Loading } from "../components/Loading"; import { SelectList } from "../components/SelectList"; import { + ButtonBoxWarning, ButtonPrimary, ErrorText, Input, InputWithLabel, + WarningBox, } from "../components/styled"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import * as wxApi from "../wxApi"; interface Props { currency: string; + onSuccess: (currency: string) => void; } -export function DepositPage({ currency }: Props): VNode { - const [success, setSuccess] = useState(false); - +export function DepositPage({ currency, onSuccess }: Props): VNode { const state = useAsyncAsHook(async () => { const balance = await wxApi.getBalance(); const bs = balance.balances.filter((b) => b.available.startsWith(currency)); @@ -63,7 +64,7 @@ export function DepositPage({ currency }: Props): VNode { async function doSend(account: string, amount: AmountString): Promise<void> { await wxApi.createDepositGroup(account, amount); - setSuccess(true); + onSuccess(currency); } async function getFeeForAmount( @@ -73,8 +74,8 @@ export function DepositPage({ currency }: Props): VNode { return await wxApi.getFeeForDeposit(account, amount); } - if (accounts.length === 0) return <div>loading..</div>; - if (success) return <div>deposit created</div>; + if (accounts.length === 0) return <Loading />; + return ( <View knownBankAccounts={accounts} @@ -105,8 +106,17 @@ export function View({ const [accountIdx, setAccountIdx] = useState(0); const [amount, setAmount] = useState<number | undefined>(undefined); const [fee, setFee] = useState<DepositFee | undefined>(undefined); + function updateAmount(num: number | undefined) { + setAmount(num); + setFee(undefined); + } + const feeHasBeenCalculated = fee !== undefined; const currency = balance.currency; const amountStr: AmountString = `${currency}:${amount}`; + const feeSum = + fee !== undefined + ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount + : Amounts.getZero(currency); const account = knownBankAccounts.length ? knownBankAccounts[accountIdx] @@ -126,7 +136,12 @@ export function View({ return <div>no balance</div>; } if (!knownBankAccounts || !knownBankAccounts.length) { - return <div>there is no known bank account to send money to</div>; + return ( + <WarningBox> + <p>There is no known bank account to send money to</p> + <ButtonBoxWarning>Withdraw</ButtonBoxWarning> + </WarningBox> + ); } const parsedAmount = amount === undefined ? undefined : Amounts.parse(amountStr); @@ -136,9 +151,16 @@ export function View({ : !parsedAmount ? "Invalid amount" : Amounts.cmp(balance, parsedAmount) === -1 - ? `To much, your current balance is ${balance.value}` + ? `To much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; + const totalToDeposit = parsedAmount + ? Amounts.sub(parsedAmount, feeSum).amount + : Amounts.getZero(currency); + + const unableToDeposit = + Amounts.isZero(totalToDeposit) && feeHasBeenCalculated; + return ( <Fragment> <h2>Send {currency} to your account</h2> @@ -153,7 +175,7 @@ export function View({ /> </Input> <InputWithLabel invalid={!!error}> - <label>Amount to send</label> + <label>Amount</label> <div> <span>{currency}</span> <input @@ -161,11 +183,10 @@ export function View({ value={amount} onInput={(e) => { const num = parseFloat(e.currentTarget.value); - console.log(num); if (!Number.isNaN(num)) { - setAmount(num); + updateAmount(num); } else { - setAmount(undefined); + updateAmount(undefined); setFee(undefined); } }} @@ -173,40 +194,41 @@ export function View({ </div> {error && <ErrorText>{error}</ErrorText>} </InputWithLabel> - {!error && fee && ( - <div style={{ textAlign: "center" }}> - <Part - title="Exchange fee" - text={Amounts.stringify(Amounts.sum([fee.wire, fee.coin]).amount)} - kind="negative" - /> - <Part - title="Change cost" - text={Amounts.stringify(fee.refresh)} - kind="negative" - /> - {parsedAmount && ( - <Part - title="Total received" - text={Amounts.stringify( - Amounts.sub( - parsedAmount, - Amounts.sum([fee.wire, fee.coin]).amount, - ).amount, - )} - kind="positive" - /> - )} - </div> - )} + { + <Fragment> + <InputWithLabel> + <label>Deposit fee</label> + <div> + <span>{currency}</span> + <input + type="number" + disabled + value={Amounts.stringifyValue(feeSum)} + /> + </div> + </InputWithLabel> + + <InputWithLabel> + <label>Total deposit</label> + <div> + <span>{currency}</span> + <input + type="number" + disabled + value={Amounts.stringifyValue(totalToDeposit)} + /> + </div> + </InputWithLabel> + </Fragment> + } </section> <footer> <div /> <ButtonPrimary - disabled={!parsedAmount} + disabled={unableToDeposit} onClick={() => onSend(accountURI, amountStr)} > - Send + Deposit {Amounts.stringifyValue(totalToDeposit)} {currency} </ButtonPrimary> </footer> </Fragment> diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index ce4b0fb74..3f550175d 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -35,7 +35,7 @@ import { HistoryView as TestedComponent } from "./History"; import { createExample } from "../test-utils"; export default { - title: "wallet/history/list", + title: "wallet/balance/history", component: TestedComponent, }; @@ -114,8 +114,13 @@ const exampleData = { } as TransactionRefund, }; -export const Empty = createExample(TestedComponent, { - list: [], +export const NoBalance = createExample(TestedComponent, { + transactions: [], + balances: [], +}); + +export const SomeBalanceWithNoTransactions = createExample(TestedComponent, { + transactions: [], balances: [ { available: "TESTKUDOS:10", @@ -127,16 +132,24 @@ export const Empty = createExample(TestedComponent, { ], }); -export const EmptyWithNoBalance = createExample(TestedComponent, { - list: [], - balances: [], +export const OneSimpleTransaction = createExample(TestedComponent, { + transactions: [exampleData.withdraw], + balances: [ + { + available: "USD:10", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], }); -export const One = createExample(TestedComponent, { - list: [exampleData.withdraw], +export const TwoTransactionsAndZeroBalance = createExample(TestedComponent, { + transactions: [exampleData.withdraw, exampleData.deposit], balances: [ { - available: "USD:10", + available: "USD:0", pendingIncoming: "USD:0", pendingOutgoing: "USD:0", hasPendingTransactions: false, @@ -145,8 +158,8 @@ export const One = createExample(TestedComponent, { ], }); -export const OnePending = createExample(TestedComponent, { - list: [ +export const OneTransactionPending = createExample(TestedComponent, { + transactions: [ { ...exampleData.withdraw, pending: true, @@ -163,8 +176,8 @@ export const OnePending = createExample(TestedComponent, { ], }); -export const Several = createExample(TestedComponent, { - list: [ +export const SomeTransactions = createExample(TestedComponent, { + transactions: [ exampleData.withdraw, exampleData.payment, exampleData.withdraw, @@ -183,38 +196,82 @@ export const Several = createExample(TestedComponent, { ], balances: [ { - available: "TESTKUDOS:10", - pendingIncoming: "TESTKUDOS:0", - pendingOutgoing: "TESTKUDOS:0", + available: "USD:10", + pendingIncoming: "USD:0", + pendingOutgoing: "USD: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, - ], +export const SomeTransactionsWithTwoCurrencies = createExample( + TestedComponent, + { + transactions: [ + exampleData.withdraw, + exampleData.payment, + exampleData.withdraw, + exampleData.payment, + exampleData.refresh, + exampleData.refund, + exampleData.tip, + exampleData.deposit, + ], + balances: [ + { + available: "USD:0", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + { + available: "TESTKUDOS:10", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], + }, +); + +export const FiveOfficialCurrencies = createExample(TestedComponent, { + transactions: [exampleData.withdraw], balances: [ { - available: "TESTKUDOS:10", + available: "USD:1000", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + { + available: "EUR:881", pendingIncoming: "TESTKUDOS:0", pendingOutgoing: "TESTKUDOS:0", hasPendingTransactions: false, requiresUserInput: false, }, { - available: "USD:10", - pendingIncoming: "USD:0", - pendingOutgoing: "USD:0", + available: "COL:4043000.5", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + { + available: "JPY:11564450.6", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + { + available: "GBP:736", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", hasPendingTransactions: false, requiresUserInput: false, }, diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index 58db0360b..7912d169a 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -15,21 +15,38 @@ */ import { - AmountString, + Amounts, Balance, NotificationType, Transaction, } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { ButtonPrimary, DateSeparator } from "../components/styled"; +import { Loading } from "../components/Loading"; +import { + ButtonBoxPrimary, + ButtonBoxWarning, + ButtonPrimary, + DateSeparator, + ErrorBox, + NiceSelect, + WarningBox, +} from "../components/styled"; import { Time } from "../components/Time"; import { TransactionItem } from "../components/TransactionItem"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; -import { AddNewActionView } from "../popup/AddNewActionView"; import * as wxApi from "../wxApi"; -export function HistoryPage(): VNode { +interface Props { + currency?: string; + goToWalletDeposit: (currency: string) => void; + goToWalletManualWithdraw: (currency?: string) => void; +} +export function HistoryPage({ + currency, + goToWalletManualWithdraw, + goToWalletDeposit, +}: Props): VNode { const balance = useAsyncAsHook(wxApi.getBalance); const balanceWithoutError = balance?.hasError ? [] @@ -39,110 +56,166 @@ export function HistoryPage(): VNode { NotificationType.WithdrawGroupFinished, ]); - const [addingAction, setAddingAction] = useState(false); - - if (addingAction) { - return <AddNewActionView onCancel={() => setAddingAction(false)} />; + if (!transactionQuery || !balance) { + return <Loading />; } - if (!transactionQuery) { - return <div>Loading ...</div>; - } if (transactionQuery.hasError) { - return <div>There was an error loading the transactions.</div>; + return ( + <Fragment> + <ErrorBox>{transactionQuery.message}</ErrorBox> + <p>There was an error loading the transactions.</p> + </Fragment> + ); } return ( <HistoryView balances={balanceWithoutError} - list={[...transactionQuery.response.transactions].reverse()} - onAddNewAction={() => setAddingAction(true)} + defaultCurrency={currency} + goToWalletManualWithdraw={goToWalletManualWithdraw} + goToWalletDeposit={goToWalletDeposit} + transactions={[...transactionQuery.response.transactions].reverse()} /> ); } -function amountToString(c: AmountString): string { - const idx = c.indexOf(":"); - return `${c.substring(idx + 1)} ${c.substring(0, idx)}`; -} - const term = 1000 * 60 * 60 * 24; function normalizeToDay(x: number): number { return Math.round(x / term) * term; } export function HistoryView({ - list, + defaultCurrency, + transactions, balances, - onAddNewAction, + goToWalletManualWithdraw, + goToWalletDeposit, }: { - list: Transaction[]; + goToWalletDeposit: (currency: string) => void; + goToWalletManualWithdraw: (currency?: string) => void; + defaultCurrency?: string; + transactions: Transaction[]; balances: Balance[]; - onAddNewAction: () => void; }): VNode { - const byDate = list.reduce((rv, x) => { - const theDate = - x.timestamp.t_ms === "never" ? 0 : normalizeToDay(x.timestamp.t_ms); - if (theDate) { - (rv[theDate] = rv[theDate] || []).push(x); - } + const currencies = balances.map((b) => b.available.split(":")[0]); - return rv; - }, {} as { [x: string]: Transaction[] }); + const defaultCurrencyIndex = currencies.findIndex( + (c) => c === defaultCurrency, + ); + const [currencyIndex, setCurrencyIndex] = useState( + defaultCurrencyIndex === -1 ? 0 : defaultCurrencyIndex, + ); + const selectedCurrency = + currencies.length > 0 ? currencies[currencyIndex] : undefined; + + const currencyAmount = balances[currencyIndex] + ? Amounts.jsonifyAmount(balances[currencyIndex].available) + : undefined; + + const byDate = transactions + .filter((t) => t.amountRaw.split(":")[0] === selectedCurrency) + .reduce((rv, x) => { + const theDate = + x.timestamp.t_ms === "never" ? 0 : normalizeToDay(x.timestamp.t_ms); + if (theDate) { + (rv[theDate] = rv[theDate] || []).push(x); + } + + return rv; + }, {} as { [x: string]: Transaction[] }); + const datesWithTransaction = Object.keys(byDate); const multiCurrency = balances.length > 1; + if (balances.length === 0 || !selectedCurrency) { + return ( + <WarningBox> + <p> + You have <b>no balance</b>. Withdraw some founds into your wallet + </p> + <ButtonBoxWarning onClick={() => goToWalletManualWithdraw()}> + Withdraw + </ButtonBoxWarning> + </WarningBox> + ); + } return ( <Fragment> - <header> - {balances.length > 0 ? ( - <Fragment> - {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, i) => ( - <li key={i}>{b.available}</li> - ))} - </ul> - </div> - )} - </Fragment> - ) : ( - <div /> - )} - <div> - <ButtonPrimary onClick={onAddNewAction}> - <b>+</b> + <section> + <p + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }} + > + {currencies.length === 1 ? ( + <div style={{ fontSize: "large" }}>{selectedCurrency}</div> + ) : ( + <NiceSelect> + <select + value={currencyIndex} + onChange={(e) => { + setCurrencyIndex(Number(e.currentTarget.value)); + }} + > + {currencies.map((currency, index) => { + return ( + <option value={index} key={currency}> + {currency} + </option> + ); + })} + </select> + </NiceSelect> + )} + {currencyAmount && ( + <h2 style={{ margin: 0 }}> + {Amounts.stringifyValue(currencyAmount)} + </h2> + )} + </p> + <div style={{ marginLeft: "auto", width: "fit-content" }}> + <ButtonPrimary + onClick={() => goToWalletManualWithdraw(selectedCurrency)} + > + Withdraw </ButtonPrimary> + {currencyAmount && Amounts.isNonZero(currencyAmount) && ( + <ButtonBoxPrimary + onClick={() => goToWalletDeposit(selectedCurrency)} + > + Deposit + </ButtonBoxPrimary> + )} </div> - </header> - <section> - {Object.keys(byDate).map((d, i) => { - return ( - <Fragment key={i}> - <DateSeparator> - <Time - timestamp={{ t_ms: Number.parseInt(d, 10) }} - format="dd MMMM yyyy" - /> - </DateSeparator> - {byDate[d].map((tx, i) => ( - <TransactionItem - key={i} - tx={tx} - multiCurrency={multiCurrency} - /> - ))} - </Fragment> - ); - })} </section> + {datesWithTransaction.length === 0 ? ( + <section>There is no history for this currency</section> + ) : ( + <section> + {datesWithTransaction.map((d, i) => { + return ( + <Fragment key={i}> + <DateSeparator> + <Time + timestamp={{ t_ms: Number.parseInt(d, 10) }} + format="dd MMMM yyyy" + /> + </DateSeparator> + {byDate[d].map((tx, i) => ( + <TransactionItem + key={i} + tx={tx} + multiCurrency={multiCurrency} + /> + ))} + </Fragment> + ); + })} + </section> + )} </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/LastActivityPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/LastActivityPage.stories.tsx new file mode 100644 index 000000000..e729c2982 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/LastActivityPage.stories.tsx @@ -0,0 +1,33 @@ +/* + 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 { createExample } from "../test-utils"; +import { queryToSlashKeys } from "../utils/index"; +import { LastActivityPage as TestedComponent } from "./LastActivityPage"; + +export default { + title: "wallet/last activity", + component: TestedComponent, +}; + +export const InitialState = createExample(TestedComponent, { + onVerify: queryToSlashKeys, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/LastActivityPage.tsx b/packages/taler-wallet-webextension/src/wallet/LastActivityPage.tsx new file mode 100644 index 000000000..8ec4c8759 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/LastActivityPage.tsx @@ -0,0 +1,35 @@ +/* + 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 { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { ButtonPrimary } from "../components/styled"; +import { AddNewActionView } from "./AddNewActionView"; + +export function LastActivityPage(): VNode { + const [addingAction, setAddingAction] = useState(false); + + if (addingAction) { + return <AddNewActionView onCancel={() => setAddingAction(false)} />; + } + + return ( + <section> + <div /> + <ButtonPrimary onClick={() => setAddingAction(true)}>+</ButtonPrimary> + </section> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx index b3e8a2c25..c7958eb8a 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { VNode, h } from "preact"; +import { VNode, h, Fragment } from "preact"; import { useState } from "preact/hooks"; import { CreateManualWithdraw } from "./CreateManualWithdraw"; import * as wxApi from "../wxApi"; @@ -29,8 +29,10 @@ import { route } from "preact-router"; import { Pages } from "../NavigationBar"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { ExchangeAddPage } from "./ExchangeAddPage"; +import { Loading } from "../components/Loading"; +import { ErrorBox } from "../components/styled"; -export function ManualWithdrawPage(): VNode { +export function ManualWithdrawPage({ currency }: { currency?: string }): VNode { const [success, setSuccess] = useState< | { response: AcceptManualWithdrawalResult; @@ -86,10 +88,15 @@ export function ManualWithdrawPage(): VNode { } if (!state) { - return <div>loading...</div>; + return <Loading />; } if (state.hasError) { - return <div>There was an error getting the known exchanges</div>; + return ( + <Fragment> + <ErrorBox>{state.message}</ErrorBox> + <p>There was an error getting the known exchanges</p> + </Fragment> + ); } const exchangeList = state.response.exchanges.reduce( (p, c) => ({ @@ -105,6 +112,7 @@ export function ManualWithdrawPage(): VNode { error={error} exchangeList={exchangeList} onCreate={doCreate} + initialCurrency={currency} /> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 8172e02a2..21bfc943d 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -73,7 +73,7 @@ export function TransactionPage({ tid }: { tid: string }): VNode { } if (state.hasError) { - route(Pages.history); + route(Pages.balance); return ( <div> <i18n.Translate> @@ -84,7 +84,16 @@ export function TransactionPage({ tid }: { tid: string }): VNode { } function goToHistory(): void { - route(Pages.history); + const currency = + state !== undefined && !state.hasError + ? Amounts.parseOrThrow(state.response.amountRaw).currency + : undefined; + + if (currency) { + route(Pages.balance_history.replace(":currency", currency)); + } else { + route(Pages.balance); + } } return ( diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx index 644ab1c59..55f350d46 100644 --- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx @@ -33,5 +33,22 @@ import * as a11 from "./ReserveCreated.stories"; import * as a12 from "./Settings.stories"; import * as a13 from "./Transaction.stories"; import * as a14 from "./Welcome.stories"; +import * as a15 from "./AddNewActionView.stories"; -export default [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14]; +export default [ + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, +]; diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index 938892874..b54d49ded 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -22,31 +22,32 @@ import { setupI18n } from "@gnu-taler/taler-util"; import { createHashHistory } from "history"; -import { Fragment, h, render, VNode } from "preact"; +import { h, render, VNode } from "preact"; import Router, { route, Route } from "preact-router"; -import { useEffect } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { LogoHeader } from "./components/LogoHeader"; +import { SuccessBox, WalletBox } from "./components/styled"; import { DevContextProvider } from "./context/devContext"; +import { IoCProviderForRuntime } from "./context/iocContext"; import { PayPage } from "./cta/Pay"; import { RefundPage } from "./cta/Refund"; import { TipPage } from "./cta/Tip"; import { WithdrawPage } from "./cta/Withdraw"; import { strings } from "./i18n/strings"; import { Pages, WalletNavBar } from "./NavigationBar"; +import { DeveloperPage } from "./popup/DeveloperPage"; +import { BackupPage } from "./wallet/BackupPage"; import { BalancePage } from "./wallet/BalancePage"; +import { DepositPage } from "./wallet/DepositPage"; +import { ExchangeAddPage } from "./wallet/ExchangeAddPage"; import { HistoryPage } from "./wallet/History"; +import { LastActivityPage } from "./wallet/LastActivityPage"; +import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage"; +import { ProviderAddPage } from "./wallet/ProviderAddPage"; +import { ProviderDetailPage } from "./wallet/ProviderDetailPage"; import { SettingsPage } from "./wallet/Settings"; import { TransactionPage } from "./wallet/Transaction"; import { WelcomePage } from "./wallet/Welcome"; -import { BackupPage } from "./wallet/BackupPage"; -import { DeveloperPage } from "./popup/DeveloperPage"; -import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage"; -import { WalletBox } from "./components/styled"; -import { ProviderDetailPage } from "./wallet/ProviderDetailPage"; -import { ProviderAddPage } from "./wallet/ProviderAddPage"; -import { ExchangeAddPage } from "./wallet/ExchangeAddPage"; -import { DepositPage } from "./wallet/DepositPage"; -import { IoCProviderForRuntime } from "./context/iocContext"; function main(): void { try { @@ -71,140 +72,156 @@ if (document.readyState === "loading") { main(); } -function withLogoAndNavBar(Component: any) { - return function withLogoAndNavBarComponent(props: any): VNode { - return ( - <Fragment> - <LogoHeader /> - <WalletNavBar /> - <WalletBox> - <Component {...props} /> - </WalletBox> - </Fragment> - ); - }; -} - function Application(): VNode { + const [globalNotification, setGlobalNotification] = useState< + string | undefined + >(undefined); return ( <div> <DevContextProvider> - <IoCProviderForRuntime> - <Router history={createHashHistory()}> - <Route - path={Pages.welcome} - component={withLogoAndNavBar(WelcomePage)} - /> - - <Route - path={Pages.history} - component={withLogoAndNavBar(HistoryPage)} - /> - <Route - path={Pages.transaction} - component={withLogoAndNavBar(TransactionPage)} - /> - <Route - path={Pages.balance} - component={withLogoAndNavBar(BalancePage)} - goToWalletManualWithdraw={() => route(Pages.manual_withdraw)} - goToWalletDeposit={(currency: string) => - route(Pages.deposit.replace(":currency", currency)) - } - /> - <Route - path={Pages.settings} - component={withLogoAndNavBar(SettingsPage)} - /> - <Route - path={Pages.backup} - component={withLogoAndNavBar(BackupPage)} - onAddProvider={() => { - route(Pages.provider_add); - }} - /> - <Route - path={Pages.provider_detail} - component={withLogoAndNavBar(ProviderDetailPage)} - onBack={() => { - route(Pages.backup); - }} - /> - <Route - path={Pages.provider_add} - component={withLogoAndNavBar(ProviderAddPage)} - onBack={() => { - route(Pages.backup); - }} - /> - - <Route - path={Pages.exchange_add} - component={withLogoAndNavBar(ExchangeAddPage)} - onBack={() => { - route(Pages.balance); - }} - /> - - <Route - path={Pages.manual_withdraw} - component={withLogoAndNavBar(ManualWithdrawPage)} - /> - - <Route - path={Pages.deposit} - component={withLogoAndNavBar(DepositPage)} - /> - <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.dev} - component={withLogoAndNavBar(DeveloperPage)} - /> - - {/** call to action */} - <Route - path={Pages.pay} - component={PayPage} - goToWalletManualWithdraw={() => - goToWalletPage(Pages.manual_withdraw) - } - /> - <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> - </IoCProviderForRuntime> + {({ devMode }: { devMode: boolean }) => ( + <IoCProviderForRuntime> + <LogoHeader /> + <WalletNavBar devMode={devMode} /> + <WalletBox> + {globalNotification && ( + <SuccessBox onClick={() => setGlobalNotification(undefined)}> + <div>{globalNotification}</div> + </SuccessBox> + )} + <Router history={createHashHistory()}> + <Route path={Pages.welcome} component={WelcomePage} /> + + <Route + path={Pages.balance} + component={BalancePage} + goToWalletManualWithdraw={() => + route(Pages.manual_withdraw.replace(":currency?", "")) + } + goToWalletDeposit={(currency: string) => + route(Pages.deposit.replace(":currency", currency)) + } + goToWalletHistory={(currency: string) => + route(Pages.balance_history.replace(":currency", currency)) + } + /> + <Route + path={Pages.balance_history} + component={HistoryPage} + goToWalletDeposit={(currency: string) => + route(Pages.deposit.replace(":currency", currency)) + } + goToWalletManualWithdraw={(currency?: string) => + route( + Pages.manual_withdraw.replace( + ":currency?", + currency || "", + ), + ) + } + /> + <Route + path={Pages.last_activity} + component={LastActivityPage} + /> + <Route path={Pages.transaction} component={TransactionPage} /> + <Route path={Pages.settings} component={SettingsPage} /> + <Route + path={Pages.backup} + component={BackupPage} + onAddProvider={() => { + route(Pages.provider_add); + }} + /> + <Route + path={Pages.provider_detail} + component={ProviderDetailPage} + onBack={() => { + route(Pages.backup); + }} + /> + <Route + path={Pages.provider_add} + component={ProviderAddPage} + onBack={() => { + route(Pages.backup); + }} + /> + + <Route + path={Pages.exchange_add} + component={ExchangeAddPage} + onBack={() => { + route(Pages.balance); + }} + /> + + <Route + path={Pages.manual_withdraw} + component={ManualWithdrawPage} + /> + + <Route + path={Pages.deposit} + component={DepositPage} + onSuccess={(currency: string) => { + route(Pages.balance_history.replace(":currency", currency)); + setGlobalNotification( + "All done, your transaction is in progress", + ); + }} + /> + <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.dev} component={DeveloperPage} /> + + {/** call to action */} + <Route + path={Pages.pay} + component={PayPage} + goToWalletManualWithdraw={(currency?: string) => + route( + Pages.manual_withdraw.replace( + ":currency?", + currency || "", + ), + ) + } + goBack={() => route(Pages.balance)} + /> + <Route + path={Pages.pay} + component={PayPage} + goBack={() => route(Pages.balance)} + /> + <Route path={Pages.refund} component={RefundPage} /> + <Route path={Pages.tips} component={TipPage} /> + <Route path={Pages.withdraw} component={WithdrawPage} /> + + <Route default component={Redirect} to={Pages.balance} /> + </Router> + </WalletBox> + </IoCProviderForRuntime> + )} </DevContextProvider> </div> ); } -function goToWalletPage(page: Pages | string): null { - // eslint-disable-next-line no-undef - chrome.tabs.create({ - active: true, - // eslint-disable-next-line no-undef - url: chrome.extension.getURL(`/static/wallet.html#${page}`), - }); - return null; -} - function Redirect({ to }: { to: string }): null { useEffect(() => { + console.log("go some wrong route"); route(to, true); }); return null; diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 5fe30bc4b..dc96efc75 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -24,7 +24,7 @@ import { AcceptExchangeTosRequest, AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse, - AddExchangeRequest, AmountJson, AmountString, ApplyRefundResponse, BalancesResponse, ConfirmPayResult, + AddExchangeRequest, AmountString, ApplyRefundResponse, BalancesResponse, ConfirmPayResult, CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, DeleteTransactionRequest, ExchangesListRespose, GetExchangeTosResult, GetExchangeWithdrawalInfo, GetFeeForDepositRequest, |