/* 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 { AmountString, Amounts, TalerError, TalerErrorCode, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { isFuture, parse } from "date-fns"; import { useEffect, 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 { Props, State } from "./index.js"; import { BackgroundError, WxApiType } from "../../wxApi.js"; export function useComponentState({ amount: amountStr, onClose, onSuccess, }: Props): State { const api = useBackendContext(); const { pushAlertOnError } = useAlertContext(); const amount = Amounts.parseOrThrow(amountStr); const { i18n } = useTranslationContext(); const [subject, setSubject] = useState(); const [timestamp, setTimestamp] = useState(); const hook = useAsyncAsHook(async () => { const resp = await checkPeerPushDebitAndCheckMax(api, amountStr); return resp; }); if (!hook) { return { status: "loading", error: undefined, }; } if (hook.hasError) { return { status: "error", error: alertFromError( i18n.str`Could not load the max amount to transfer`, hook, ), }; } const { amountEffective, amountRaw } = hook.response; const debitAmount = Amounts.parseOrThrow(amountEffective); const toBeReceived = Amounts.parseOrThrow(amountRaw); let purse_expiration: TalerProtocolTimestamp | undefined = undefined; let timestampError: string | undefined = undefined; const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()); if (t !== undefined) { if (Number.isNaN(t.getTime())) { timestampError = 'Should have the format "dd/MM/yyyy"'; } else { if (!isFuture(t)) { timestampError = "Should be in the future"; } else { purse_expiration = { t_s: t.getTime() / 1000, }; } } } async function accept(): Promise { if (!subject || !purse_expiration) return; const resp = await api.wallet.call( WalletApiOperation.InitiatePeerPushDebit, { partialContractTerms: { summary: subject, amount: amountStr, purse_expiration, }, }, ); onSuccess(resp.transactionId); } const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration; return { status: "ready", cancel: { onClick: pushAlertOnError(onClose), }, subject: { error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, value: subject ?? "", onInput: pushAlertOnError(async (e) => setSubject(e)), }, expiration: { error: timestampError, value: timestamp === undefined ? "" : timestamp, onInput: pushAlertOnError(async (e) => { setTimestamp(e); }), }, create: { onClick: unableToCreate ? undefined : pushAlertOnError(accept), }, debitAmount, toBeReceived, error: undefined, }; } async function checkPeerPushDebitAndCheckMax( api: WxApiType, amountState: AmountString, ) { // FIXME : https://bugs.gnunet.org/view.php?id=7872 try { return await api.wallet.call(WalletApiOperation.CheckPeerPushDebit, { amount: amountState, }); } catch (e) { if (!(e instanceof BackgroundError)) { throw e; } if ( !e.hasErrorCode( TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, ) ) { throw e; } const material = Amounts.parseOrThrow( e.errorDetail.insufficientBalanceDetails.balanceMaterial, ); const gap = Amounts.parseOrThrow( e.errorDetail.insufficientBalanceDetails.feeGapEstimate, ); const newAmount = Amounts.sub(material, gap).amount; const amount = Amounts.parseOrThrow(amountState); if (Amounts.cmp(newAmount, amount) === 0) { //insufficient balance and the exception didn't give //a good response that allow us to try again throw e; } if (Amounts.cmp(newAmount, amount) === 1) { //how can this happen? throw e; } return checkPeerPushDebitAndCheckMax(api, Amounts.stringify(newAmount)); } }