diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index 1b628047a..d7d9f2da7 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -29,13 +29,14 @@ import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; import { useTranslationContext } from "../../context/translation.js"; 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): State { +}: Props): RecursiveState { const api = useBackendContext(); const { i18n } = useTranslationContext(); const { pushAlertOnError } = useAlertContext(); @@ -49,9 +50,7 @@ export function useComponentState({ ); const { accounts } = await api.wallet.call( WalletApiOperation.ListKnownBankAccounts, - { - currency, - }, + { currency }, ); return { accounts, balances }; @@ -61,13 +60,11 @@ export function useComponentState({ parsed !== undefined ? parsed : currency !== undefined - ? Amounts.zeroOfCurrency(currency) - : undefined; + ? Amounts.zeroOfCurrency(currency) + : undefined; // const [accountIdx, setAccountIdx] = useState(0); - const [amount, setAmount] = useState(initialValue ?? ({} as any)); const [selectedAccount, setSelectedAccount] = useState(); - const [fee, setFee] = useState(undefined); const [addingAccount, setAddingAccount] = useState(false); if (!currency) { @@ -91,7 +88,12 @@ export function useComponentState({ } const { accounts, balances } = hook.response; - // const parsedAmount = Amounts.parse(`${currency}:${amount}`); + async function updateAccountFromList(accountStr: string): Promise { + const uri = !accountStr ? undefined : parsePaytoUri(accountStr); + if (uri) { + setSelectedAccount(uri); + } + } if (addingAccount) { return { @@ -139,130 +141,112 @@ export function useComponentState({ const firstAccount = accounts[0].uri; const currentAccount = !selectedAccount ? firstAccount : selectedAccount; - if (fee === undefined) { - getFeeForAmount(currentAccount, amount, api.wallet).then((initialFee) => { - setFee(initialFee); - }); - return { - status: "loading", - error: undefined, - }; - } - - const accountMap = createLabelsForBankAccount(accounts); - - async function updateAccountFromList(accountStr: string): Promise { - const uri = !accountStr ? undefined : parsePaytoUri(accountStr); - if (uri) { - try { - const result = await getFeeForAmount(uri, amount, api.wallet); - setSelectedAccount(uri); - setFee(result); - } catch (e) { - setSelectedAccount(uri); - setFee(undefined); - } - } - } - - async function updateAmount(newAmount: AmountJson): Promise { - // const parsed = Amounts.parse(`${currency}:${numStr}`); - try { - const result = await getFeeForAmount( - currentAccount, - newAmount, - api.wallet, - ); - setAmount(newAmount); - setFee(result); - } catch (e) { - setAmount(newAmount); - setFee(undefined); - } - } - - const totalFee = - fee !== undefined - ? Amounts.sum([fee.wire, fee.coin, fee.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); + return () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [amount, setAmount] = useState( + initialValue ?? ({} as any), + ); const amountStr = Amounts.stringify(amount); - await api.wallet.call(WalletApiOperation.CreateDepositGroup, { - amount: amountStr, - depositPaytoUri, - }); - onSuccess(currency); - } + const depositPaytoUri = `payto://${currentAccount.targetType}/${currentAccount.targetPath}`; - return { - status: "ready", - error: undefined, - currency, - amount: { - value: amount, - onInput: pushAlertOnError(updateAmount), - 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, - // currentAccount, - // parsedAmount, + // eslint-disable-next-line react-hooks/rules-of-hooks + const hook = useAsyncAsHook(async () => { + const fee = await api.wallet.call(WalletApiOperation.GetFeeForDeposit, { + amount: amountStr, + depositPaytoUri, + }); + + return { fee }; + }, [amountStr, depositPaytoUri]); + + if (!hook) { + return { + status: "loading", + error: undefined, + }; + } + if (hook.hasError) { + return { + status: "error", + error: alertFromError( + 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.wire, fee.coin, fee.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, + }; }; } -async function getFeeForAmount( - p: PaytoUri, - a: AmountJson, - wallet: ReturnType["wallet"], -): Promise { - const depositPaytoUri = `payto://${p.targetType}/${p.targetPath}`; - const amount = Amounts.stringify(a); - return await wallet.call(WalletApiOperation.GetFeeForDeposit, { - amount, - depositPaytoUri, - }); -} - export function labelForAccountType(id: string): string { switch (id) { case "": diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 42b76cf50..bfd69f945 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -264,6 +264,15 @@ describe("DepositPage states", () => { expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(state.depositHandler.onClick).undefined; }, + (state) => { + if (state.status !== "ready") expect.fail(); + expect(state.cancelHandler.onClick).not.undefined; + expect(state.currency).eq(currency); + expect(state.account.value).eq(accountSelected); + expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); + expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); + expect(state.depositHandler.onClick).undefined; + }, ], TestingContext, ); @@ -341,7 +350,7 @@ describe("DepositPage states", () => { expect(state.account.value).eq(accountSelected); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(state.depositHandler.onClick).undefined; - expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(state.amount.onInput).not.undefined; if (!state.amount.onInput) return; @@ -359,6 +368,18 @@ describe("DepositPage states", () => { ); expect(state.depositHandler.onClick).not.undefined; }, + (state) => { + if (state.status !== "ready") expect.fail(); + expect(state.cancelHandler.onClick).not.undefined; + expect(state.currency).eq(currency); + expect(state.account.value).eq(accountSelected); + expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); + expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(state.totalToDeposit).deep.eq( + Amounts.parseOrThrow(`${currency}:7`), + ); + expect(state.depositHandler.onClick).not.undefined; + }, ], TestingContext, ); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 5ed05f87f..cc3a65f2d 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -38,7 +38,7 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { styled } from "@linaria/react"; -import { differenceInSeconds } from "date-fns"; +import { differenceInSeconds, isAfter, isFuture, isPast } from "date-fns"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import emptyImg from "../../static/img/empty.png"; @@ -641,6 +641,11 @@ export function TransactionView({ if (transaction.type === TransactionType.Deposit) { const total = Amounts.parseOrThrow(transaction.amountRaw); const payto = parsePaytoUri(transaction.targetPaytoUri); + + const wireTime = AbsoluteTime.fromTimestamp( + transaction.wireTransferDeadline, + ); + const shouldBeWired = wireTime.t_ms !== "never" && isPast(wireTime.t_ms); return ( } kind="neutral" /> - - } - kind="neutral" - /> + {!shouldBeWired ? ( + + } + kind="neutral" + /> + ) : transaction.wireTransferProgress === 0 ? ( + + ) : transaction.wireTransferProgress === 100 ? ( + + ) : ( + + )} ); }