/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* 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 */ import { AmountJson, Amounts, AmountString, Balance, 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 { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { SelectList } from "../components/SelectList.js"; import { Button, ButtonPrimary, ErrorText, Input, InputWithLabel, SubTitle, 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 { SelectFieldHandler, TextFieldHandler, } from "./CreateManualWithdraw.js"; interface Props { currency: string; onCancel: (currency: string) => void; 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 ; 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} /> ); } interface ViewProps { accounts: Array; currency: string; balances: Balance[]; onCancel: () => void; onSend: (account: PaytoUri, amount: AmountJson) => Promise; onCalculateFee: ( account: PaytoUri, amount: AmountJson, ) => Promise; } type State = NoBalanceState | NoAccountsState | DepositState; interface NoBalanceState { status: "no-balance"; } interface NoAccountsState { status: "no-accounts"; } interface DepositState { status: "deposit"; amount: TextFieldHandler; account: SelectFieldHandler; totalFee: AmountJson; totalToDeposit: AmountJson; unableToDeposit: boolean; selectedAccount: PaytoUri; parsedAmount: AmountJson | undefined; } export function useComponentState( currency: string, accounts: PaytoUri[], balances: Balance[], onCalculateFee: ( account: PaytoUri, amount: AmountJson, ) => Promise, ): State { const accountMap = createLabelsForBankAccount(accounts); const [accountIdx, setAccountIdx] = useState(0); const [amount, setAmount] = useState(undefined); 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 selectedAccount = accounts.length ? accounts[accountIdx] : undefined; const parsedAmount = amount === undefined ? undefined : Amounts.parse(selectedAmountSTR); useEffect(() => { if (selectedAccount === undefined || parsedAmount === undefined) return; onCalculateFee(selectedAccount, parsedAmount).then((result) => { setFee(result); }); }, [amount, selectedAccount, parsedAmount, onCalculateFee]); const bs = balances.filter((b) => b.available.startsWith(currency)); const balance = bs.length > 0 ? Amounts.parseOrThrow(bs[0].available) : Amounts.getZero(currency); const isDirty = amount !== 0; const amountError = !isDirty ? undefined : !parsedAmount ? "Invalid amount" : Amounts.cmp(balance, parsedAmount) === -1 ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; const totalToDeposit = parsedAmount ? Amounts.sub(parsedAmount, totalFee).amount : Amounts.getZero(currency); const unableToDeposit = Amounts.isZero(totalToDeposit) || fee === undefined || amountError !== undefined; if (Amounts.isZero(balance)) { return { status: "no-balance", }; } if (!accounts || !accounts.length || !selectedAccount) { return { status: "no-accounts", }; } return { status: "deposit", amount: { value: String(amount), onInput: (e) => { const num = parseFloat(e); if (!Number.isNaN(num)) { updateAmount(num); } else { updateAmount(undefined); setFee(undefined); } }, error: amountError, }, account: { list: accountMap, value: String(accountIdx), onChange: (s) => setAccountIdx(parseInt(s, 10)), }, totalFee, totalToDeposit, unableToDeposit, selectedAccount, parsedAmount, }; } export function View({ onCancel, currency, accounts, balances, onSend, onCalculateFee, }: ViewProps): VNode { const { i18n } = useTranslationContext(); const state = useComponentState(currency, accounts, balances, onCalculateFee); if (state.status === "no-balance") { return (
no balance
); } if (state.status === "no-accounts") { return (

There is no known bank account to send money to

); } return ( Send {currency} to your account
Bank account IBAN number} list={state.account.list} name="account" value={state.account.value} onChange={state.account.onChange} />
{currency} state.amount.onInput(e.currentTarget.value)} />
{state.amount.error && {state.amount.error}}
{
{currency}
{currency}
}
{state.unableToDeposit ? ( Deposit ) : ( onSend(state.selectedAccount, state.parsedAmount!)} > Deposit {Amounts.stringifyValue(state.totalToDeposit)} {currency} )}
); } function createLabelsForBankAccount(knownBankAccounts: Array): { [label: number]: string; } { if (!knownBankAccounts) return {}; return knownBankAccounts.reduce((prev, cur, i) => { let label = cur.targetPath; if (cur.isKnown) { switch (cur.targetType) { case "x-taler-bank": { label = cur.account; break; } case "iban": { label = cur.iban; break; } } } return { ...prev, [i]: label, }; }, {} as { [label: number]: string }); }