diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts | 276 |
1 files changed, 276 insertions, 0 deletions
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..97b2ab517 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -0,0 +1,276 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +import { + AmountJson, + Amounts, + DepositGroupFees, + KnownBankAccountsInfo, + parsePaytoUri, + PaytoUri, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useState } from "preact/hooks"; +import { alertFromError, useAlertContext } from "../../context/alert.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import { RecursiveState } from "../../utils/index.js"; +import { Props, State } from "./index.js"; + +export function useComponentState({ + amount: amountStr, + onCancel, + onSuccess, +}: Props): RecursiveState<State> { + const api = useBackendContext(); + const { i18n } = useTranslationContext(); + const { pushAlertOnError } = useAlertContext(); + const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr); + const currency = parsed !== undefined ? parsed.currency : undefined; + + const hook = useAsyncAsHook(async () => { + const { balances } = await api.wallet.call( + WalletApiOperation.GetBalances, + {}, + ); + const { accounts } = await api.wallet.call( + WalletApiOperation.ListKnownBankAccounts, + { currency }, + ); + + return { accounts, balances }; + }); + + const initialValue = + parsed !== undefined + ? parsed + : currency !== undefined + ? Amounts.zeroOfCurrency(currency) + : undefined; + // const [accountIdx, setAccountIdx] = useState<number>(0); + const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); + + 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: "error", + error: alertFromError(i18n, + i18n.str`Could not load balance information`, hook), + }; + } + const { accounts, balances } = hook.response; + + async function updateAccountFromList(accountStr: string): Promise<void> { + const uri = !accountStr ? undefined : parsePaytoUri(accountStr); + if (uri) { + setSelectedAccount(uri); + } + } + + if (addingAccount) { + return { + status: "manage-account", + error: undefined, + currency, + onAccountAdded: (p: string) => { + updateAccountFromList(p); + setAddingAccount(false); + hook.retry(); + }, + onCancel: () => { + setAddingAccount(false); + hook.retry(); + }, + }; + } + + const bs = balances.filter((b) => b.available.startsWith(currency)); + const balance = + bs.length > 0 + ? Amounts.parseOrThrow(bs[0].available) + : Amounts.zeroOfCurrency(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: pushAlertOnError(async () => { + setAddingAccount(true); + }), + }, + }; + } + const firstAccount = accounts[0].uri; + const currentAccount = !selectedAccount ? firstAccount : selectedAccount; + + return () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [amount, setAmount] = useState<AmountJson>( + initialValue ?? ({} as any), + ); + const amountStr = Amounts.stringify(amount); + const depositPaytoUri = stringifyPaytoUri(currentAccount); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const hook = useAsyncAsHook(async () => { + const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, { + amount: amountStr, + depositPaytoUri, + }); + + return { fee }; + }, [amountStr, depositPaytoUri]); + + if (!hook) { + return { + status: "loading", + error: undefined, + }; + } + if (hook.hasError) { + return { + status: "error", + error: alertFromError( + i18n, + i18n.str`Could not load fee for amount ${amountStr}`, + hook, + ), + }; + } + + const { fee } = hook.response; + + const accountMap = createLabelsForBankAccount(accounts); + + const totalFee = + fee !== undefined + ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount + : Amounts.zeroOfCurrency(currency); + + const totalToDeposit = + fee !== undefined + ? Amounts.sub(amount, totalFee).amount + : Amounts.zeroOfCurrency(currency); + + const isDirty = amount !== initialValue; + const amountError = !isDirty + ? undefined + : Amounts.cmp(balance, amount) === -1 + ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` + : undefined; + + const unableToDeposit = + Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee + fee === undefined || //no fee calculated yet + amountError !== undefined; //amount field may be invalid + + async function doSend(): Promise<void> { + if (!currency) return; + + const depositPaytoUri = stringifyPaytoUri(currentAccount); + const amountStr = Amounts.stringify(amount); + await api.wallet.call(WalletApiOperation.CreateDepositGroup, { + amount: amountStr, + depositPaytoUri, + }); + onSuccess(currency); + } + + return { + status: "ready", + error: undefined, + currency, + amount: { + value: amount, + onInput: pushAlertOnError(async (a) => setAmount(a)), + error: amountError, + }, + onAddAccount: { + onClick: pushAlertOnError(async () => { + setAddingAccount(true); + }), + }, + account: { + list: accountMap, + value: stringifyPaytoUri(currentAccount), + onChange: pushAlertOnError(updateAccountFromList), + }, + currentAccount, + cancelHandler: { + onClick: pushAlertOnError(async () => { + onCancel(currency); + }), + }, + depositHandler: { + onClick: unableToDeposit ? undefined : pushAlertOnError(doSend), + }, + totalFee, + totalToDeposit, + }; + }; +} + +export function labelForAccountType(id: string): 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<KnownBankAccountsInfo>, +): { [value: string]: string } { + const initialList: Record<string, string> = {}; + if (!knownBankAccounts.length) return initialList; + return knownBankAccounts.reduce((prev, cur, i) => { + prev[stringifyPaytoUri(cur.uri)] = cur.alias; + return prev; + }, initialList); +} |