/* This file is part of TALER (C) 2015-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 */ /** * Page shown to the user to confirm creation * of a reserve, usually requested by the bank. * * @author sebasjm */ import { AmountJson, Amounts, ExchangeListItem, WithdrawUriInfoResponse, } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { useCallback, useMemo, useState } from "preact/hooks"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js"; import { LogoHeader } from "../components/LogoHeader.js"; import { Part } from "../components/Part.js"; import { SelectList } from "../components/SelectList.js"; import { ButtonSuccess, ButtonWarning, LinkSuccess, SubTitle, WalletAction, } from "../components/styled/index.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { amountToString, buildTermsOfServiceState, TermsState, } from "../utils/index.js"; import * as wxApi from "../wxApi.js"; import { TermsOfServiceSection } from "./TermsOfServiceSection.js"; import { useTranslationContext } from "../context/translation.js"; import { TalerError } from "@gnu-taler/taler-wallet-core"; interface Props { talerWithdrawUri?: string; } export interface ViewProps { withdrawalFee: AmountJson; exchangeBaseUrl?: string; amount: AmountJson; onSwitchExchange: (ex: string) => void; onWithdraw: () => Promise; onReview: (b: boolean) => void; onAccept: (b: boolean) => void; reviewing: boolean; reviewed: boolean; terms: TermsState; knownExchanges: ExchangeListItem[]; } export function View({ withdrawalFee, exchangeBaseUrl, knownExchanges, amount, onWithdraw, onSwitchExchange, terms, reviewing, onReview, onAccept, reviewed, }: ViewProps): VNode { const { i18n } = useTranslationContext(); const [withdrawError, setWithdrawError] = useState( undefined, ); const [confirmDisabled, setConfirmDisabled] = useState(false); const needsReview = terms.status === "changed" || terms.status === "new"; const [switchingExchange, setSwitchingExchange] = useState(false); const [nextExchange, setNextExchange] = useState( undefined, ); const exchanges = knownExchanges .filter((e) => e.currency === amount.currency) .reduce( (prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {}, ); async function doWithdrawAndCheckError(): Promise { try { setConfirmDisabled(true); await onWithdraw(); } catch (e) { if (e instanceof TalerError) { setWithdrawError(e); } setConfirmDisabled(false); } } return ( Digital cash withdrawal {withdrawError && ( Could not finish the withdrawal operation } error={withdrawError.errorDetail} /> )}
Total to withdraw} text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} kind="positive" /> {Amounts.isNonZero(withdrawalFee) && ( Chosen amount} text={amountToString(amount)} kind="neutral" /> Exchange fee} text={amountToString(withdrawalFee)} kind="negative" /> )} {exchangeBaseUrl && ( Exchange} text={exchangeBaseUrl} kind="neutral" big /> )} {!reviewing && (switchingExchange ? (
Known exchanges} list={exchanges} value={nextExchange} name="switchingExchange" onChange={setNextExchange} />
{ if (nextExchange !== undefined) { onSwitchExchange(nextExchange); } setSwitchingExchange(false); }} > {nextExchange === undefined ? ( Cancel exchange selection ) : ( Confirm exchange selection )}
) : ( setSwitchingExchange(true)} > Edit exchange ))}
{(terms.status === "accepted" || (needsReview && reviewed)) && ( Confirm withdrawal )} {terms.status === "notfound" && ( Withdraw anyway )}
); } export function WithdrawPageWithParsedURI({ uri, uriInfo, }: { uri: string; uriInfo: WithdrawUriInfoResponse; }): VNode { const { i18n } = useTranslationContext(); const [customExchange, setCustomExchange] = useState( undefined, ); const [reviewing, setReviewing] = useState(false); const [reviewed, setReviewed] = useState(false); const knownExchangesHook = useAsyncAsHook(wxApi.listExchanges); const knownExchanges = useMemo( () => !knownExchangesHook || knownExchangesHook.hasError ? [] : knownExchangesHook.response.exchanges, [knownExchangesHook], ); const withdrawAmount = useMemo( () => Amounts.parseOrThrow(uriInfo.amount), [uriInfo.amount], ); const thisCurrencyExchanges = useMemo( () => knownExchanges.filter((ex) => ex.currency === withdrawAmount.currency), [knownExchanges, withdrawAmount.currency], ); const exchange: string | undefined = useMemo( () => customExchange ?? uriInfo.defaultExchangeBaseUrl ?? (thisCurrencyExchanges[0] ? thisCurrencyExchanges[0].exchangeBaseUrl : undefined), [customExchange, thisCurrencyExchanges, uriInfo.defaultExchangeBaseUrl], ); const detailsHook = useAsyncAsHook(async () => { if (!exchange) throw Error("no default exchange"); const tos = await wxApi.getExchangeTos(exchange, ["text/xml"]); const tosState = buildTermsOfServiceState(tos); const info = await wxApi.getExchangeWithdrawalInfo({ exchangeBaseUrl: exchange, amount: withdrawAmount, tosAcceptedFormat: ["text/xml"], }); return { tos: tosState, info }; }); if (!detailsHook) { return ; } if (detailsHook.hasError) { return ( Could not load the withdrawal details } error={detailsHook} /> ); } const details = detailsHook.response; const onAccept = async (accepted: boolean): Promise => { if (!exchange) return; try { await wxApi.setExchangeTosAccepted( exchange, accepted ? details.tos.version : undefined, ); setReviewed(accepted); } catch (e) { if (e instanceof Error) { //FIXME: uncomment this and display error // setErrorAccepting(e.message); } } }; const onWithdraw = async (): Promise => { if (!exchange) return; const res = await wxApi.acceptWithdrawal(uri, exchange); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; } }; const withdrawalFee = Amounts.sub( Amounts.parseOrThrow(details.info.withdrawalAmountRaw), Amounts.parseOrThrow(details.info.withdrawalAmountEffective), ).amount; return ( ); } export function WithdrawPage({ talerWithdrawUri }: Props): VNode { const { i18n } = useTranslationContext(); const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) : wxApi.getWithdrawalDetailsForUri({ talerWithdrawUri }), ); if (!talerWithdrawUri) { return ( missing withdraw uri ); } if (!uriInfoHook) { return ; } if (uriInfoHook.hasError) { return ( Could not get the info from the URI } error={uriInfoHook} /> ); } return ( ); }