diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet')
13 files changed, 684 insertions, 257 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx new file mode 100644 index 000000000..54e4eb1f2 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.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 { AddNewActionView as TestedComponent } from "./AddNewActionView"; + +export default { + title: "wallet/add new action", + component: TestedComponent, + argTypes: { + setDeviceName: () => Promise.resolve(), + }, +}; + +export const Initial = createExample(TestedComponent, {}); diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx new file mode 100644 index 000000000..d4158973e --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx @@ -0,0 +1,64 @@ +import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Button, ButtonSuccess, InputWithLabel } from "../components/styled"; +import { actionForTalerUri } from "../utils/index"; + +export interface Props { + onCancel: () => void; +} + +function buttonLabelByTalerType(type: TalerUriType): string { + switch (type) { + case TalerUriType.TalerNotifyReserve: + return "Open reserve page"; + case TalerUriType.TalerPay: + return "Open pay page"; + case TalerUriType.TalerRefund: + return "Open refund page"; + case TalerUriType.TalerTip: + return "Open tip page"; + case TalerUriType.TalerWithdraw: + return "Open withdraw page"; + } + return ""; +} + +export function AddNewActionView({ onCancel }: Props): VNode { + const [url, setUrl] = useState(""); + const uriType = classifyTalerUri(url); + + return ( + <Fragment> + <section> + <InputWithLabel + invalid={url !== "" && uriType === TalerUriType.Unknown} + > + <label>GNU Taler URI</label> + <div> + <input + style={{ width: "100%" }} + type="text" + value={url} + placeholder="taler://pay/...." + onInput={(e) => setUrl(e.currentTarget.value)} + /> + </div> + </InputWithLabel> + </section> + <footer> + <Button onClick={onCancel}>Back</Button> + {uriType !== TalerUriType.Unknown && ( + <ButtonSuccess + onClick={() => { + // eslint-disable-next-line no-undef + chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); + }} + > + {buttonLabelByTalerType(uriType)} + </ButtonSuccess> + )} + </footer> + </Fragment> + ); +} 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, +]; |