/* 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 { 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 exchangeAndAmount = useAsyncAsHook( async () => { if (!uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response) return; const { uriInfo, knownExchanges } = uriInfoHook.response; const amount = Amounts.parseOrThrow(uriInfo.amount); const thisCurrencyExchanges = knownExchanges.filter( (ex) => ex.currency === amount.currency, ); const thisExchange: string | undefined = customExchange ?? uriInfo.defaultExchangeBaseUrl ?? (thisCurrencyExchanges[0] ? thisCurrencyExchanges[0].exchangeBaseUrl : undefined); if (!thisExchange) throw Error("ERROR_NO-DEFAULT-EXCHANGE"); return { amount, thisExchange, thisCurrencyExchanges }; }, [], [!uriInfoHook || uriInfoHook.hasError ? undefined : uriInfoHook], ); /** * For the exchange selected, bring the status of the terms of service */ const terms = useAsyncAsHook( async () => { if ( !exchangeAndAmount || exchangeAndAmount.hasError || !exchangeAndAmount.response ) return; const { thisExchange } = exchangeAndAmount.response; const exchangeTos = await api.getExchangeTos(thisExchange, ["text/xml"]); const state = buildTermsOfServiceState(exchangeTos); return { state }; }, [], [ !exchangeAndAmount || exchangeAndAmount.hasError ? undefined : exchangeAndAmount, ], ); /** * With the exchange and amount, ask the wallet the information * about the withdrawal */ const info = useAsyncAsHook( async () => { if ( !exchangeAndAmount || exchangeAndAmount.hasError || !exchangeAndAmount.response ) return; const { thisExchange, amount } = exchangeAndAmount.response; 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 }; }, [], [ !exchangeAndAmount || exchangeAndAmount.hasError ? undefined : exchangeAndAmount, ], ); 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 (!exchangeAndAmount || exchangeAndAmount.hasError) { return { status: "loading-exchange", hook: exchangeAndAmount, }; } if (!exchangeAndAmount.response) { return { status: "loading-exchange", hook: undefined, }; } const { thisExchange, thisCurrencyExchanges, amount } = exchangeAndAmount.response; async function doWithdrawAndCheckError(): Promise { try { setConfirmDisabled(true); if (!talerWithdrawUri) return; const res = await api.acceptWithdrawal(talerWithdrawUri, thisExchange); 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( thisExchange, 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 ; }