/* 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 } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-wallet-core"; import { Fragment, h, VNode } from "preact"; import { useMemo, useState } from "preact/hooks"; import { Amount } from "../components/Amount.js"; import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.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 { useTranslationContext } from "../context/translation.js"; import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { buildTermsOfServiceState } from "../utils/index.js"; import { ButtonHandler, SelectFieldHandler, } from "../wallet/CreateManualWithdraw.js"; import * as wxApi from "../wxApi.js"; import { Props as TermsOfServiceSectionProps, TermsOfServiceSection, } from "./TermsOfServiceSection.js"; interface Props { talerWithdrawUri?: string; } type State = LoadingUri | LoadingExchange | LoadingInfoError | Success; interface LoadingUri { status: "loading-uri"; hook: HookError | undefined; } interface LoadingExchange { status: "loading-exchange"; hook: HookError | undefined; } interface LoadingInfoError { status: "loading-info"; hook: HookError | undefined; } type Success = { status: "success"; hook: undefined; exchange: SelectFieldHandler; editExchange: ButtonHandler; cancelEditExchange: ButtonHandler; confirmEditExchange: ButtonHandler; showExchangeSelection: boolean; chosenAmount: AmountJson; withdrawalFee: AmountJson; toBeReceived: AmountJson; doWithdrawal: ButtonHandler; tosProps?: TermsOfServiceSectionProps; mustAcceptFirst: boolean; }; export function useComponentState( talerWithdrawUri: string | undefined, api: typeof wxApi, ): State { const [customExchange, setCustomExchange] = useState( undefined, ); /** * Ask the wallet about the withdraw URI */ const uriInfoHook = useAsyncAsHook(async () => { if (!talerWithdrawUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL"); const uriInfo = await api.getWithdrawalDetailsForUri({ talerWithdrawUri, }); const { exchanges: knownExchanges } = await api.listExchanges(); return { uriInfo, knownExchanges }; }); /** * Get the amount and select one exchange */ const uriHookDep = !uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response ? undefined : uriInfoHook; const { amount, thisExchange, thisCurrencyExchanges } = useMemo(() => { if (!uriHookDep) return { amount: undefined, thisExchange: undefined, thisCurrencyExchanges: [], }; const { uriInfo, knownExchanges } = uriHookDep.response; const amount = uriInfo ? Amounts.parseOrThrow(uriInfo.amount) : undefined; const thisCurrencyExchanges = !amount || !knownExchanges ? [] : knownExchanges.filter((ex) => ex.currency === amount.currency); const thisExchange: string | undefined = customExchange ?? uriInfo?.defaultExchangeBaseUrl ?? (thisCurrencyExchanges && thisCurrencyExchanges[0] ? thisCurrencyExchanges[0].exchangeBaseUrl : undefined); return { amount, thisExchange, thisCurrencyExchanges }; }, [uriHookDep, customExchange]); /** * For the exchange selected, bring the status of the terms of service */ const terms = useAsyncAsHook( async () => { if (!thisExchange) return false; const exchangeTos = await api.getExchangeTos(thisExchange, ["text/xml"]); const state = buildTermsOfServiceState(exchangeTos); return { state }; }, [], [thisExchange], ); /** * With the exchange and amount, ask the wallet the information * about the withdrawal */ const info = useAsyncAsHook( async () => { if (!thisExchange || !amount) return false; const info = await api.getExchangeWithdrawalInfo({ exchangeBaseUrl: thisExchange, amount, tosAcceptedFormat: ["text/xml"], }); const withdrawalFee = Amounts.sub( Amounts.parseOrThrow(info.withdrawalAmountRaw), Amounts.parseOrThrow(info.withdrawalAmountEffective), ).amount; return { info, withdrawalFee }; }, [], [thisExchange, amount], ); const [reviewing, setReviewing] = useState(false); const [reviewed, setReviewed] = useState(false); const [withdrawError, setWithdrawError] = useState( undefined, ); const [confirmDisabled, setConfirmDisabled] = useState(false); const [showExchangeSelection, setShowExchangeSelection] = useState(false); const [nextExchange, setNextExchange] = useState(); if (!uriInfoHook || uriInfoHook.hasError) { return { status: "loading-uri", hook: uriInfoHook, }; } if (!thisExchange || !amount) { return { status: "loading-exchange", hook: { hasError: true, operational: false, message: "ERROR_NO-DEFAULT-EXCHANGE", }, }; } const selectedExchange = thisExchange; async function doWithdrawAndCheckError(): Promise { try { setConfirmDisabled(true); if (!talerWithdrawUri) return; const res = await api.acceptWithdrawal( talerWithdrawUri, selectedExchange, ); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; } } catch (e) { if (e instanceof TalerError) { setWithdrawError(e); } setConfirmDisabled(false); } } const exchanges = thisCurrencyExchanges.reduce( (prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {}, ); if (!info || info.hasError) { return { status: "loading-info", hook: info, }; } if (!info.response) { return { status: "loading-info", hook: undefined, }; } const exchangeHandler: SelectFieldHandler = { onChange: setNextExchange, value: nextExchange || thisExchange, list: exchanges, isDirty: nextExchange !== thisExchange, }; const editExchange: ButtonHandler = { onClick: async () => { setShowExchangeSelection(true); }, }; const cancelEditExchange: ButtonHandler = { onClick: async () => { setShowExchangeSelection(false); }, }; const confirmEditExchange: ButtonHandler = { onClick: async () => { setCustomExchange(exchangeHandler.value); setShowExchangeSelection(false); }, }; const { withdrawalFee } = info.response; const toBeReceived = Amounts.sub(amount, withdrawalFee).amount; const { state: termsState } = (!terms ? undefined : terms.hasError ? undefined : terms.response) || { state: undefined }; async function onAccept(accepted: boolean): Promise { if (!termsState) return; try { await api.setExchangeTosAccepted( selectedExchange, accepted ? termsState.version : undefined, ); setReviewed(accepted); } catch (e) { if (e instanceof Error) { //FIXME: uncomment this and display error // setErrorAccepting(e.message); } } } return { status: "success", hook: undefined, exchange: exchangeHandler, editExchange, cancelEditExchange, confirmEditExchange, showExchangeSelection, toBeReceived, withdrawalFee, chosenAmount: amount, doWithdrawal: { onClick: doWithdrawAndCheckError, error: withdrawError, disabled: confirmDisabled, }, tosProps: !termsState ? undefined : { onAccept, onReview: setReviewing, reviewed: reviewed, reviewing: reviewing, terms: termsState, }, mustAcceptFirst: termsState !== undefined && (termsState.status === "changed" || termsState.status === "new"), }; } export function View({ state }: { state: Success }): VNode { const { i18n } = useTranslationContext(); return ( Digital cash withdrawal {state.doWithdrawal.error && ( Could not finish the withdrawal operation } error={state.doWithdrawal.error.errorDetail} /> )}
Total to withdraw} text={} kind="positive" /> {Amounts.isNonZero(state.withdrawalFee) && ( Chosen amount} text={} kind="neutral" /> Exchange fee} text={} kind="negative" /> )} Exchange} text={state.exchange.value} kind="neutral" big /> {state.showExchangeSelection ? (
Known exchanges} list={state.exchange.list} value={state.exchange.value} name="switchingExchange" onChange={state.exchange.onChange} />
{state.exchange.isDirty ? ( Confirm exchange selection ) : ( Cancel exchange selection )}
) : ( Edit exchange )}
{state.tosProps && } {state.tosProps ? (
{(state.tosProps.terms.status === "accepted" || (state.mustAcceptFirst && state.tosProps.reviewed)) && ( Confirm withdrawal )} {state.tosProps.terms.status === "notfound" && ( Withdraw anyway )}
) : (
Loading terms of service...
)}
); } export function WithdrawPage({ talerWithdrawUri }: Props): VNode { const { i18n } = useTranslationContext(); const state = useComponentState(talerWithdrawUri, wxApi); if (!talerWithdrawUri) { return ( missing withdraw uri ); } if (!state) { return ; } if (state.status === "loading-uri") { if (!state.hook) return ; return ( Could not get the info from the URI } error={state.hook} /> ); } if (state.status === "loading-exchange") { if (!state.hook) return ; return ( Could not get exchange} error={state.hook} /> ); } if (state.status === "loading-info") { if (!state.hook) return ; return ( Could not get info of withdrawal } error={state.hook} /> ); } return ; }