From c5f484d18a89bd6cda0c7a89eea5ee9d7fe4ba09 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 22 Apr 2022 16:10:21 -0300 Subject: deposit test case --- .../src/wallet/CreateManualWithdraw.test.ts | 3 +- .../src/wallet/CreateManualWithdraw.tsx | 26 +- .../src/wallet/DepositPage.stories.tsx | 60 +++- .../src/wallet/DepositPage.test.ts | 362 ++++++++++++++++++++- .../src/wallet/DepositPage.tsx | 308 ++++++++++-------- 5 files changed, 566 insertions(+), 193 deletions(-) (limited to 'packages/taler-wallet-webextension/src/wallet') diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts index f2bb4a7d2..a4b333f02 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts @@ -21,8 +21,9 @@ */ import { expect } from "chai"; +import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js"; import { mountHook } from "../test-utils.js"; -import { SelectFieldHandler, TextFieldHandler, useComponentState } from "./CreateManualWithdraw.js"; +import { useComponentState } from "./CreateManualWithdraw.js"; const exchangeListWithARSandUSD = { diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 0440c50a9..11bade6f5 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -37,6 +37,7 @@ import { SubTitle, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; +import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js"; import { Pages } from "../NavigationBar.js"; export interface Props { @@ -55,25 +56,6 @@ export interface State { exchange: SelectFieldHandler; } -export interface TextFieldHandler { - onInput: (value: string) => void; - value: string; - error?: string; -} - -export interface ButtonHandler { - onClick?: () => Promise; - error?: TalerError; -} - -export interface SelectFieldHandler { - onChange: (value: string) => void; - error?: string; - value: string; - isDirty?: boolean; - list: Record; -} - export function useComponentState( exchangeUrlWithCurrency: Record, initialAmount: string | undefined, @@ -109,12 +91,12 @@ export function useComponentState( const [amount, setAmount] = useState(initialAmount || ""); const parsedAmount = Amounts.parse(`${currency}:${amount}`); - function changeExchange(exchange: string): void { + async function changeExchange(exchange: string): Promise { setExchange(exchange); setCurrency(exchangeUrlWithCurrency[exchange]); } - function changeCurrency(currency: string): void { + async function changeCurrency(currency: string): Promise { setCurrency(currency); const found = Object.entries(exchangeUrlWithCurrency).find( (e) => e[1] === currency, @@ -140,7 +122,7 @@ export function useComponentState( }, amount: { value: amount, - onInput: (e: string) => setAmount(e), + onInput: async (e: string) => setAmount(e), }, parsedAmount, }; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx index edc2f971f..5f7966417 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx @@ -20,10 +20,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Balance, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, Balance, parsePaytoUri } from "@gnu-taler/taler-util"; import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits.js"; import { createExample } from "../test-utils.js"; -import { View as TestedComponent } from "./DepositPage.js"; +import { + createLabelsForBankAccount, + View as TestedComponent, +} from "./DepositPage.js"; export default { title: "wallet/deposit", @@ -41,23 +44,44 @@ async function alwaysReturnFeeToOne(): Promise { } export const WithEmptyAccountList = createExample(TestedComponent, { - accounts: [], - balances: [ - { - available: "USD:10", - } as Balance, - ], - currency: "USD", - onCalculateFee: alwaysReturnFeeToOne, + state: { + status: "no-accounts", + cancelHandler: {}, + }, + // accounts: [], + // balances: [ + // { + // available: "USD:10", + // } as Balance, + // ], + // currency: "USD", + // onCalculateFee: alwaysReturnFeeToOne, }); +const ac = parsePaytoUri("payto://iban/ES8877998399652238")!; +const accountMap = createLabelsForBankAccount([ac]); + export const WithSomeBankAccounts = createExample(TestedComponent, { - accounts: [parsePaytoUri("payto://iban/ES8877998399652238")!], - balances: [ - { - available: "USD:10", - } as Balance, - ], - currency: "USD", - onCalculateFee: alwaysReturnFeeToOne, + state: { + status: "ready", + account: { + list: accountMap, + value: accountMap[0], + onChange: async () => { + null; + }, + }, + currency: "USD", + amount: { + onInput: async () => { + null; + }, + value: "10:USD", + }, + cancelHandler: {}, + depositHandler: {}, + 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 index ac4e0ea93..c863b27d5 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts @@ -19,46 +19,390 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts, Balance } from "@gnu-taler/taler-util"; +import { Amounts, Balance, BalancesResponse, parsePaytoUri } from "@gnu-taler/taler-util"; import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { expect } from "chai"; import { mountHook } from "../test-utils.js"; import { useComponentState } from "./DepositPage.js"; +import * as wxApi from "../wxApi.js"; const currency = "EUR" -const feeCalculator = async (): Promise => ({ +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) ? withoutFee() : withSomeFee() + const someBalance = [{ available: 'EUR:10' } as Balance] +const nullFunction: any = () => null; +type VoidFunction = () => void; + describe("DepositPage states", () => { - it("should have status 'no-balance' when balance is empty", () => { - const { getLastResultOrThrow } = mountHook(() => - useComponentState(currency, [], [], feeCalculator), + it("should have status 'no-balance' when balance is empty", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, 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-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, nullFunction, 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 = parsePaytoUri("payto://iban/ES8877998399652238")!; + const talerBankPayto = parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!; + + it("should have status 'ready' but unable to deposit ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, 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("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should not be able to deposit more than the balance ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, 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("0") + 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("0") + expect(r.amount.value).eq("10") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should calculate the fee upon entering amount ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, 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("0") + 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("0") + 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, nullFunction, 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("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + + r.account.onChange("1") + } + + 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("1") + 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("1") + 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; + + r.account.onChange("0") + } + + 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("0") + 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("should have status 'no-accounts' when balance is not empty and accounts is empty", () => { - const { getLastResultOrThrow } = mountHook(() => - useComponentState(currency, [], someBalance, feeCalculator), + + it("should be able to deposit if has the enough balance ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, 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("no-accounts") + 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("0") + 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("0") + 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("0") + 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; + + r.amount.onInput("15") } + 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("0") + expect(r.amount.value).eq("15") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:12`)) + expect(r.depositHandler.onClick).not.undefined; + r.amount.onInput("17") + } + 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("0") + expect(r.amount.value).eq("17") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:14`)) + expect(r.depositHandler.onClick).undefined; + } + await assertNoPendingUpdate() }); + }); \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx index 335dfd3c7..98328ae4a 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx @@ -15,16 +15,10 @@ TALER; see the file COPYING. If not, see */ -import { - AmountJson, - Amounts, - AmountString, - Balance, - PaytoUri, -} from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, PaytoUri } from "@gnu-taler/taler-util"; import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { SelectList } from "../components/SelectList.js"; @@ -38,12 +32,13 @@ import { WarningBox, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; -import * as wxApi from "../wxApi.js"; +import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { + ButtonHandler, SelectFieldHandler, TextFieldHandler, -} from "./CreateManualWithdraw.js"; +} from "../mui/handlers.js"; +import * as wxApi from "../wxApi.js"; interface Props { currency: string; @@ -51,119 +46,90 @@ interface Props { onSuccess: (currency: string) => void; } export function DepositPage({ currency, onCancel, onSuccess }: Props): VNode { - const state = useAsyncAsHook(async () => { - const { balances } = await wxApi.getBalance(); - const { accounts } = await wxApi.listKnownBankAccounts(currency); - return { accounts, balances }; - }); - - const { i18n } = useTranslationContext(); - - async function doSend(p: PaytoUri, a: AmountJson): Promise { - const account = `payto://${p.targetType}/${p.targetPath}`; - const amount = Amounts.stringify(a); - await wxApi.createDepositGroup(account, amount); - onSuccess(currency); - } - - async function getFeeForAmount( - p: PaytoUri, - a: AmountJson, - ): Promise { - const account = `payto://${p.targetType}/${p.targetPath}`; - const amount = Amounts.stringify(a); - return await wxApi.getFeeForDeposit(account, amount); - } - - if (state === undefined) return ; + const state = useComponentState(currency, onCancel, onSuccess, wxApi); - if (state.hasError) { - return ( - Could not load deposit balance} - error={state} - /> - ); - } - - return ( - onCancel(currency)} - currency={currency} - accounts={state.response.accounts} - balances={state.response.balances} - onSend={doSend} - onCalculateFee={getFeeForAmount} - /> - ); + return ; } interface ViewProps { - accounts: Array; - currency: string; - balances: Balance[]; - onCancel: () => void; - onSend: (account: PaytoUri, amount: AmountJson) => Promise; - onCalculateFee: ( - account: PaytoUri, - amount: AmountJson, - ) => Promise; + state: State; } -type State = NoBalanceState | NoAccountsState | DepositState; +type State = Loading | NoBalanceState | NoAccountsState | DepositState; + +interface Loading { + status: "loading"; + hook: HookError | undefined; +} interface NoBalanceState { status: "no-balance"; } interface NoAccountsState { status: "no-accounts"; + cancelHandler: ButtonHandler; } interface DepositState { - status: "deposit"; + status: "ready"; + currency: string; amount: TextFieldHandler; account: SelectFieldHandler; totalFee: AmountJson; totalToDeposit: AmountJson; - unableToDeposit: boolean; - selectedAccount: PaytoUri; - parsedAmount: AmountJson | undefined; + // currentAccount: PaytoUri; + // parsedAmount: AmountJson | undefined; + cancelHandler: ButtonHandler; + depositHandler: ButtonHandler; +} + +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 useComponentState( currency: string, - accounts: PaytoUri[], - balances: Balance[], - onCalculateFee: ( - account: PaytoUri, - amount: AmountJson, - ) => Promise, + onCancel: (currency: string) => void, + onSuccess: (currency: string) => void, + api: typeof wxApi, ): State { - const accountMap = createLabelsForBankAccount(accounts); + const hook = useAsyncAsHook(async () => { + const { balances } = await api.getBalance(); + const { accounts } = await api.listKnownBankAccounts(currency); + const defaultSelectedAccount = + accounts.length > 0 ? accounts[0] : undefined; + return { accounts, balances, defaultSelectedAccount }; + }); + const [accountIdx, setAccountIdx] = useState(0); - const [amount, setAmount] = useState(undefined); + const [amount, setAmount] = useState(0); + + const [selectedAccount, setSelectedAccount] = useState< + PaytoUri | undefined + >(); + + const parsedAmount = Amounts.parse(`${currency}:${amount}`); + const [fee, setFee] = useState(undefined); - function updateAmount(num: number | undefined): void { - setAmount(num); - setFee(undefined); - } - const selectedAmountSTR: AmountString = `${currency}:${amount}`; - const totalFee = - fee !== undefined - ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount - : Amounts.getZero(currency); + // const hookResponse = !hook || hook.hasError ? undefined : hook.response; - const selectedAccount = accounts.length ? accounts[accountIdx] : undefined; + // useEffect(() => {}, [hookResponse]); - const parsedAmount = - amount === undefined ? undefined : Amounts.parse(selectedAmountSTR); + if (!hook || hook.hasError) { + return { + status: "loading", + hook, + }; + } - useEffect(() => { - if (selectedAccount === undefined || parsedAmount === undefined) return; - onCalculateFee(selectedAccount, parsedAmount).then((result) => { - setFee(result); - }); - }, [amount, selectedAccount, parsedAmount, onCalculateFee]); + const { accounts, balances, defaultSelectedAccount } = hook.response; + const currentAccount = selectedAccount ?? defaultSelectedAccount; const bs = balances.filter((b) => b.available.startsWith(currency)); const balance = @@ -171,6 +137,63 @@ export function useComponentState( ? Amounts.parseOrThrow(bs[0].available) : Amounts.getZero(currency); + if (Amounts.isZero(balance)) { + return { + status: "no-balance", + }; + } + + if (!currentAccount) { + return { + status: "no-accounts", + cancelHandler: { + onClick: async () => { + onCancel(currency); + }, + }, + }; + } + const accountMap = createLabelsForBankAccount(accounts); + + async function updateAccount(accountStr: string): Promise { + const idx = parseInt(accountStr, 10); + const newSelected = accounts.length > idx ? accounts[idx] : undefined; + if (accountIdx === idx || !newSelected) return; + + if (!parsedAmount) { + setAccountIdx(idx); + setSelectedAccount(newSelected); + } else { + const result = await getFeeForAmount(newSelected, parsedAmount, api); + setAccountIdx(idx); + setSelectedAccount(newSelected); + setFee(result); + } + } + + async function updateAmount(numStr: string): Promise { + const num = parseFloat(numStr); + const newAmount = Number.isNaN(num) ? 0 : num; + if (amount === newAmount || !currentAccount) return; + const parsed = Amounts.parse(`${currency}:${newAmount}`); + if (!parsed) { + setAmount(newAmount); + } else { + const result = await getFeeForAmount(currentAccount, parsed, api); + setAmount(newAmount); + setFee(result); + } + } + + const totalFee = + fee !== undefined + ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount + : Amounts.getZero(currency); + + const totalToDeposit = parsedAmount + ? Amounts.sub(parsedAmount, totalFee).amount + : Amounts.getZero(currency); + const isDirty = amount !== 0; const amountError = !isDirty ? undefined @@ -180,65 +203,63 @@ export function useComponentState( ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; - const totalToDeposit = parsedAmount - ? Amounts.sub(parsedAmount, totalFee).amount - : Amounts.getZero(currency); - const unableToDeposit = + !parsedAmount || Amounts.isZero(totalToDeposit) || fee === undefined || amountError !== undefined; - if (Amounts.isZero(balance)) { - return { - status: "no-balance", - }; - } + async function doSend(): Promise { + if (!currentAccount || !parsedAmount) return; - if (!accounts || !accounts.length || !selectedAccount) { - return { - status: "no-accounts", - }; + const account = `payto://${currentAccount.targetType}/${currentAccount.targetPath}`; + const amount = Amounts.stringify(parsedAmount); + await api.createDepositGroup(account, amount); + onSuccess(currency); } return { - status: "deposit", + status: "ready", + currency, amount: { value: String(amount), - onInput: (e) => { - const num = parseFloat(e); - if (!Number.isNaN(num)) { - updateAmount(num); - } else { - updateAmount(undefined); - setFee(undefined); - } - }, + onInput: updateAmount, error: amountError, }, account: { list: accountMap, value: String(accountIdx), - onChange: (s) => setAccountIdx(parseInt(s, 10)), + onChange: updateAccount, + }, + cancelHandler: { + onClick: async () => { + onCancel(currency); + }, + }, + depositHandler: { + onClick: unableToDeposit ? undefined : doSend, }, totalFee, totalToDeposit, - unableToDeposit, - selectedAccount, - parsedAmount, + // currentAccount, + // parsedAmount, }; } -export function View({ - onCancel, - currency, - accounts, - balances, - onSend, - onCalculateFee, -}: ViewProps): VNode { +export function View({ state }: ViewProps): VNode { const { i18n } = useTranslationContext(); - const state = useComponentState(currency, accounts, balances, onCalculateFee); + + if (state === undefined) return ; + + if (state.status === "loading") { + if (!state.hook) return ; + return ( + Could not load deposit balance} + error={state.hook} + /> + ); + } if (state.status === "no-balance") { return ( @@ -258,7 +279,7 @@ export function View({

-
@@ -269,7 +290,7 @@ export function View({ return ( - Send {currency} to your account + Send {state.currency} to your account
@@ -286,7 +307,7 @@ export function View({ Amount
- {currency} + {state.currency} Deposit fee
- {currency} + {state.currency} Total deposit
- {currency} + {state.currency}