From 9811e19252ef859099fa5c16d703808f6c778a94 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 23 Sep 2022 15:18:18 -0300 Subject: new deposit page --- .../src/wallet/DepositPage/index.ts | 112 +++++ .../src/wallet/DepositPage/state.ts | 240 ++++++++++ .../src/wallet/DepositPage/stories.tsx | 131 ++++++ .../src/wallet/DepositPage/test.ts | 508 +++++++++++++++++++++ .../src/wallet/DepositPage/views.tsx | 242 ++++++++++ 5 files changed, 1233 insertions(+) create mode 100644 packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx (limited to 'packages/taler-wallet-webextension/src/wallet/DepositPage') diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts new file mode 100644 index 000000000..eb97ccf7f --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -0,0 +1,112 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { Loading } from "../../components/Loading.js"; +import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import { AmountOrCurrencyErrorView, LoadingErrorView, NoAccountToDepositView, NoEnoughBalanceView, ReadyView } from "./views.js"; +import * as wxApi from "../../wxApi.js"; +import { useComponentState } from "./state.js"; +import { AmountJson, PaytoUri } from "@gnu-taler/taler-util"; +import { ButtonHandler, SelectFieldHandler, TextFieldHandler, ToggleHandler } from "../../mui/handlers.js"; +import { AddAccountPage } from "../AddAccount/index.js"; + +export interface Props { + amount?: string; + currency?: string; + onCancel: (currency: string) => void; + onSuccess: (currency: string) => void; +} + +export type State = State.Loading + | State.LoadingUriError + | State.AmountOrCurrencyError + | State.NoEnoughBalance + | State.Ready + | State.NoAccounts + | State.AddingAccount; + +export namespace State { + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "loading-error"; + error: HookError; + } + + export interface AddingAccount { + status: "adding-account"; + error: undefined; + currency: string; + onAccountAdded: (p: string) => void; + onCancel: () => void; + } + + export interface AmountOrCurrencyError { + status: "amount-or-currency-error"; + error: undefined; + } + + export interface BaseInfo { + error: undefined; + } + + export interface NoEnoughBalance extends BaseInfo { + status: "no-enough-balance"; + currency: string; + } + + export interface NoAccounts extends BaseInfo { + status: "no-accounts"; + currency: string; + onAddAccount: ButtonHandler; + } + + export interface Ready extends BaseInfo { + status: "ready"; + error: undefined; + currency: string; + + selectedAccount: PaytoUri | undefined; + totalFee: AmountJson; + totalToDeposit: AmountJson; + + amount: TextFieldHandler; + account: SelectFieldHandler; + cancelHandler: ButtonHandler; + depositHandler: ButtonHandler; + onAddAccount: ButtonHandler; + } +} + +const viewMapping: StateViewMap = { + loading: Loading, + "loading-error": LoadingErrorView, + "amount-or-currency-error": AmountOrCurrencyErrorView, + "no-enough-balance": NoEnoughBalanceView, + "no-accounts": NoAccountToDepositView, + "adding-account": AddAccountPage, + ready: ReadyView, +}; + +export const DepositPage = compose( + "DepositPage", + (p: Props) => useComponentState(p, wxApi), + viewMapping, +); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts new file mode 100644 index 000000000..87705507c --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -0,0 +1,240 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { AmountJson, Amounts, DepositGroupFees, KnownBankAccountsInfo, parsePaytoUri, PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import * as wxApi from "../../wxApi.js"; +import { Props, State } from "./index.js"; + +export function useComponentState({ amount: amountStr, currency: currencyStr, onCancel, onSuccess }: Props, api: typeof wxApi): State { + const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr); + const currency = parsed !== undefined ? parsed.currency : currencyStr; + + const hook = useAsyncAsHook(async () => { + const { balances } = await api.getBalance(); + const { accounts } = await api.listKnownBankAccounts(currency); + + return { accounts, balances }; + }); + + const initialValue = + parsed !== undefined ? Amounts.stringifyValue(parsed) : "0"; + // const [accountIdx, setAccountIdx] = useState(0); + const [amount, setAmount] = useState(initialValue); + + const [selectedAccount, setSelectedAccount] = useState< + PaytoUri | undefined + >(); + + const [fee, setFee] = useState(undefined); + const [addingAccount, setAddingAccount] = useState(false); + + if (!currency) { + return { + status: "amount-or-currency-error", + error: undefined + } + } + + if (!hook) { + return { + status: "loading", + error: undefined, + }; + } + if (hook.hasError) { + return { + status: "loading-error", + error: hook, + } + } + const { accounts, balances } = hook.response; + + const parsedAmount = Amounts.parse(`${currency}:${amount}`); + + if (addingAccount) { + return { + status: "adding-account", + error: undefined, + currency, + onAccountAdded: (p: string) => { + updateAccountFromList(p); + setAddingAccount(false); + hook.retry() + }, + onCancel: () => { + setAddingAccount(false); + } + , + } + } + + const bs = balances.filter((b) => b.available.startsWith(currency)); + const balance = + bs.length > 0 + ? Amounts.parseOrThrow(bs[0].available) + : Amounts.getZero(currency); + + if (Amounts.isZero(balance)) { + return { + status: "no-enough-balance", + error: undefined, + currency, + }; + } + + if (accounts.length === 0) { + return { + status: "no-accounts", + error: undefined, + currency, + onAddAccount: { + onClick: async () => { setAddingAccount(true) } + }, + } + } + + const accountMap = createLabelsForBankAccount(accounts); + accountMap[""] = "Select one account..." + + async function updateAccountFromList(accountStr: string): Promise { + // const newSelected = !accountMap[accountStr] ? undefined : accountMap[accountStr]; + // if (!newSelected) return; + const uri = !accountStr ? undefined : parsePaytoUri(accountStr); + setSelectedAccount(uri); + if (uri && parsedAmount) { + try { + const result = await getFeeForAmount(uri, parsedAmount, api); + setFee(result); + } catch (e) { + setFee(undefined); + } + } + } + + async function updateAmount(numStr: string): Promise { + setAmount(numStr); + const parsed = Amounts.parse(`${currency}:${numStr}`); + if (parsed && selectedAccount) { + try { + const result = await getFeeForAmount(selectedAccount, parsed, api); + setFee(result); + } catch (e) { + setFee(undefined); + } + } + } + + const totalFee = + fee !== undefined + ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount + : Amounts.getZero(currency); + + const totalToDeposit = parsedAmount && fee !== undefined + ? Amounts.sub(parsedAmount, totalFee).amount + : Amounts.getZero(currency); + + const isDirty = amount !== initialValue; + const amountError = !isDirty + ? undefined + : !parsedAmount + ? "Invalid amount" + : Amounts.cmp(balance, parsedAmount) === -1 + ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` + : undefined; + + const unableToDeposit = + !parsedAmount || + selectedAccount === undefined || + Amounts.isZero(totalToDeposit) || + fee === undefined || + amountError !== undefined; + + async function doSend(): Promise { + if (!selectedAccount || !parsedAmount || !currency) return; + + const account = `payto://${selectedAccount.targetType}/${selectedAccount.targetPath}`; + const amount = Amounts.stringify(parsedAmount); + await api.createDepositGroup(account, amount); + onSuccess(currency); + } + + return { + status: "ready", + error: undefined, + currency, + amount: { + value: String(amount), + onInput: updateAmount, + error: amountError, + + }, + onAddAccount: { + onClick: async () => { setAddingAccount(true) } + }, + account: { + list: accountMap, + value: !selectedAccount ? "" : stringifyPaytoUri(selectedAccount), + onChange: updateAccountFromList, + }, + selectedAccount, + cancelHandler: { + onClick: async () => { + onCancel(currency); + }, + }, + depositHandler: { + onClick: unableToDeposit ? undefined : doSend, + }, + totalFee, + totalToDeposit, + // currentAccount, + // parsedAmount, + }; +} + +async function getFeeForAmount( + p: PaytoUri, + a: AmountJson, + api: typeof wxApi, +): Promise { + const account = `payto://${p.targetType}/${p.targetPath}`; + const amount = Amounts.stringify(a); + return await api.getFeeForDeposit(account, amount); +} + +export function labelForAccountType(id: string) { + switch (id) { + case "": return "Choose one"; + case "x-taler-bank": return "Taler Bank"; + case "bitcoin": return "Bitcoin"; + case "iban": return "IBAN"; + default: return id; + } +} + +export function createLabelsForBankAccount( + knownBankAccounts: Array, +): { [value: string]: string } { + const initialList: Record = { + } + if (!knownBankAccounts.length) return initialList; + return knownBankAccounts.reduce((prev, cur, i) => { + prev[stringifyPaytoUri(cur.uri)] = cur.alias + return prev; + }, initialList); +} diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx new file mode 100644 index 000000000..ed5945c06 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx @@ -0,0 +1,131 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts, DepositGroupFees } from "@gnu-taler/taler-util"; +import { createExample } from "../../test-utils.js"; +import { labelForAccountType } from "./state.js"; +import { ReadyView } from "./views.js"; + +export default { + title: "wallet/deposit", +}; + +const accountsType = { + "": labelForAccountType(""), + iban: labelForAccountType("iban"), + bitcoin: labelForAccountType("bitcoin"), + "x-taler-bank": labelForAccountType("x-taler-bank"), +}; +async function alwaysReturnFeeToOne(): Promise { + const fee = { + currency: "EUR", + value: 1, + fraction: 0, + }; + return { coin: fee, refresh: fee, wire: fee }; +} + +// const ac = parsePaytoUri("payto://iban/ES8877998399652238")!; +// const accountMap = createLabelsForBankAccount([ac]); + +export const WithNoAccountForIBAN = createExample(ReadyView, { + status: "ready", + account: { + list: {}, + value: "", + onChange: async () => { + null; + }, + }, + currency: "USD", + amount: { + onInput: async () => { + null; + }, + value: "10:USD", + }, + onAddAccount: {}, + cancelHandler: {}, + depositHandler: { + onClick: async () => { + return; + }, + }, + totalFee: Amounts.getZero("USD"), + totalToDeposit: Amounts.parseOrThrow("USD:10"), + // onCalculateFee: alwaysReturnFeeToOne, +}); + +export const WithIBANAccountTypeSelected = createExample(ReadyView, { + status: "ready", + account: { + list: { asdlkajsdlk: "asdlkajsdlk", qwerqwer: "qwerqwer" }, + value: "asdlkajsdlk", + onChange: async () => { + null; + }, + }, + currency: "USD", + amount: { + onInput: async () => { + null; + }, + value: "10:USD", + }, + onAddAccount: {}, + cancelHandler: {}, + depositHandler: { + onClick: async () => { + return; + }, + }, + totalFee: Amounts.getZero("USD"), + totalToDeposit: Amounts.parseOrThrow("USD:10"), + // onCalculateFee: alwaysReturnFeeToOne, +}); + +export const NewBitcoinAccountTypeSelected = createExample(ReadyView, { + status: "ready", + account: { + list: {}, + value: "asdlkajsdlk", + onChange: async () => { + null; + }, + }, + onAddAccount: {}, + currency: "USD", + amount: { + onInput: async () => { + null; + }, + value: "10:USD", + }, + cancelHandler: {}, + depositHandler: { + onClick: async () => { + return; + }, + }, + totalFee: Amounts.getZero("USD"), + totalToDeposit: Amounts.parseOrThrow("USD:10"), + // onCalculateFee: alwaysReturnFeeToOne, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts new file mode 100644 index 000000000..a1d4ca85a --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -0,0 +1,508 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Amounts, Balance, BalancesResponse, DepositGroupFees, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { expect } from "chai"; +import { mountHook } from "../../test-utils.js"; + +import * as wxApi from "../../wxApi.js"; +import { useComponentState } from "./state.js"; + +const currency = "EUR"; +const withoutFee = async (): Promise => ({ + coin: Amounts.parseOrThrow(`${currency}:0`), + wire: Amounts.parseOrThrow(`${currency}:0`), + refresh: Amounts.parseOrThrow(`${currency}:0`), +}); + +const withSomeFee = async (): Promise => ({ + coin: Amounts.parseOrThrow(`${currency}:1`), + wire: Amounts.parseOrThrow(`${currency}:1`), + refresh: Amounts.parseOrThrow(`${currency}:1`), +}); + +const freeJustForIBAN = async (account: string): Promise => + /IBAN/i.test(account) ? withSomeFee() : withoutFee(); + +const someBalance = [ + { + available: "EUR:10", + } as Balance, +]; + +const nullFunction: any = () => null; +type VoidFunction = () => void; + +describe("DepositPage states", () => { + + it("should have status 'no-enough-balance' when balance is empty", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + getBalance: async () => + ({ + balances: [{ available: `${currency}:0` }], + } as Partial), + listKnownBankAccounts: async () => ({ accounts: {} }), + } as Partial as any), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("loading"); + } + + await waitNextUpdate(); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("no-enough-balance"); + } + + await assertNoPendingUpdate(); + }); + + // it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => { + // const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + // mountHook(() => + // useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + // getBalance: async () => + // ({ + // balances: [{ available: `${currency}:1` }], + // } as Partial), + // listKnownBankAccounts: async () => ({ accounts: {} }), + // } as Partial as any), + // ); + + // { + // const { status } = getLastResultOrThrow(); + // expect(status).equal("loading"); + // } + + // await waitNextUpdate(); + // { + // const r = getLastResultOrThrow(); + // if (r.status !== "no-accounts") expect.fail(); + // expect(r.cancelHandler.onClick).not.undefined; + // } + + // await assertNoPendingUpdate(); + // }); + + const ibanPayto = { + uri: parsePaytoUri("payto://iban/ES8877998399652238")!, + kyc_completed: false, + currency: "EUR", + alias: "my iban account" + }; + const talerBankPayto = { + uri: parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!, + kyc_completed: false, + currency: "EUR", + alias: "my taler account" + }; + + it("should have status 'ready' but unable to deposit ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + } as Partial as any), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("loading"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("0"); + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate(); + }); + + it.skip("should not be able to deposit more than the balance ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withoutFee, + } as Partial as any), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("loading"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("0"); + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + + r.amount.onInput("10"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.depositHandler.onClick).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate(); + }); + + it.skip("should calculate the fee upon entering amount ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withSomeFee, + } as Partial as any), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("loading"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("0"); + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + + r.amount.onInput("10"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)); + expect(r.depositHandler.onClick).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate(); + }); + + it("should calculate the fee upon selecting account ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + getBalance: async () => + ({ + balances: [{ available: `${currency}:1` }], + } as Partial), + listKnownBankAccounts: async () => ({ + accounts: [ibanPayto, talerBankPayto], + }), + getFeeForDeposit: freeJustForIBAN, + } as Partial as any), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("loading"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("0"); + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + + if (r.account.onChange === undefined) expect.fail(); + r.account.onChange(stringifyPaytoUri(ibanPayto.uri)); + } + + await waitNextUpdate(""); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); + expect(r.amount.value).eq("0"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.depositHandler.onClick).undefined; + + } + + await waitNextUpdate(""); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); + expect(r.amount.value).eq("0"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.depositHandler.onClick).undefined; + + r.amount.onInput("10"); + } + + await waitNextUpdate(""); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); + expect(r.depositHandler.onClick).undefined; + + } + + await waitNextUpdate(""); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); + expect(r.depositHandler.onClick).undefined; + + + if (r.account.onChange === undefined) expect.fail(); + r.account.onChange(stringifyPaytoUri(talerBankPayto.uri)); + } + + await waitNextUpdate(""); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); + expect(r.depositHandler.onClick).undefined; + + } + + await waitNextUpdate(""); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)); + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate(); + }); + + it.skip("should be able to deposit if has the enough balance ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ currency, onCancel: nullFunction, onSuccess: nullFunction }, { + getBalance: async () => + ({ + balances: [{ available: `${currency}:15` }], + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withSomeFee, + } as Partial as any), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("loading"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("0"); + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + + r.amount.onInput("10"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)); + expect(r.depositHandler.onClick).undefined; + + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("10"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); + expect(r.depositHandler.onClick).not.undefined; + + r.amount.onInput("13"); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("13"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)); + expect(r.depositHandler.onClick).not.undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq(""); + expect(r.amount.value).eq("13"); + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)); + expect(r.depositHandler.onClick).not.undefined; + } + + await assertNoPendingUpdate(); + }); +}); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx new file mode 100644 index 000000000..ddb23c9bb --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx @@ -0,0 +1,242 @@ +/* + This file is part of GNU Taler + (C) 2022 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 + */ + +import { Amounts, PaytoUri } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { ErrorMessage } from "../../components/ErrorMessage.js"; +import { LoadingError } from "../../components/LoadingError.js"; +import { SelectList } from "../../components/SelectList.js"; +import { + ErrorText, + Input, + InputWithLabel, + SubTitle, + WarningBox, +} from "../../components/styled/index.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { Button } from "../../mui/Button.js"; +import { State } from "./index.js"; + +export function LoadingErrorView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + Could not load deposit balance} + error={error} + /> + ); +} + +export function AmountOrCurrencyErrorView( + p: State.AmountOrCurrencyError, +): VNode { + const { i18n } = useTranslationContext(); + + return ( + + A currency or an amount should be indicated + + } + /> + ); +} + +export function NoEnoughBalanceView({ + currency, +}: State.NoEnoughBalance): VNode { + const { i18n } = useTranslationContext(); + + return ( + + There is no enough balance to make a deposit for currency {currency} + + } + /> + ); +} + +function AccountDetails({ account }: { account: PaytoUri }): VNode { + if (account.isKnown) { + if (account.targetType === "bitcoin") { + return ( +
+
Bitcoin
+
{account.targetPath}
+
+ ); + } + if (account.targetType === "x-taler-bank") { + return ( +
+
Bank host
+
{account.targetPath.split("/")[0]}
+
Account name
+
{account.targetPath.split("/")[1]}
+
+ ); + } + if (account.targetType === "iban") { + return ( +
+
IBAN
+
{account.targetPath}
+
+ ); + } + } + return ; +} + +export function NoAccountToDepositView({ + currency, + onAddAccount, +}: State.NoAccounts): VNode { + const { i18n } = useTranslationContext(); + + return ( + + + Send {currency} to your account + + + + + There is no account to make a deposit for currency {currency} + + + + + + ); +} + +export function ReadyView(state: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + + + Send {state.currency} to your account + +
+
+ + Select account} + list={state.account.list} + name="account" + value={state.account.value} + onChange={state.account.onChange} + /> + + +
+ + {state.selectedAccount && ( + +

+ +

+ + +
+ {state.currency} + state.amount.onInput(e.currentTarget.value)} + /> +
+ {state.amount.error && ( + {state.amount.error} + )} +
+ + + +
+ {state.currency} + +
+
+ + + +
+ {state.currency} + +
+
+
+ )} +
+ +
+ ); +} -- cgit v1.2.3