/*
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 { 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 {
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(0);
const [selectedAccount, setSelectedAccount] = useState();
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 {
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(
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 {
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,
): { [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);
}