/* 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 Florian Dold */ import {amountToPretty, canonicalizeBaseUrl} from "src/helpers"; import { AmountJson, CreateReserveResponse, ReserveCreationInfo, Amounts, Denomination, DenominationRecord, } from "src/types"; import {getReserveCreationInfo} from "src/wxApi"; import {ImplicitStateComponent, StateHolder} from "src/components"; import * as i18n from "src/i18n"; "use strict"; function delay(delayMs: number, value: T): Promise { return new Promise((resolve, reject) => { setTimeout(() => resolve(value), delayMs); }); } class EventTrigger { triggerResolve: any; triggerPromise: Promise; constructor() { this.reset(); } private reset() { this.triggerPromise = new Promise((resolve, reject) => { this.triggerResolve = resolve; }); } trigger() { this.triggerResolve(false); this.reset(); } async wait(delayMs: number): Promise { return await Promise.race([this.triggerPromise, delay(delayMs, true)]); } } function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { if (!rci) { return

Details will be displayed when a valid exchange provider URL is entered.

} let denoms = rci.selectedDenoms; let countByPub: {[s: string]: number} = {}; let uniq: DenominationRecord[] = []; denoms.forEach((x: DenominationRecord) => { let c = countByPub[x.denomPub] || 0; if (c == 0) { uniq.push(x); } c += 1; countByPub[x.denomPub] = c; }); function row(denom: DenominationRecord) { return ( {countByPub[denom.denomPub] + "x"} {amountToPretty(denom.value)} {amountToPretty(denom.feeWithdraw)} {amountToPretty(denom.feeRefresh)} {amountToPretty(denom.feeDeposit)} ); } let withdrawFeeStr = amountToPretty(rci.withdrawFee); let overheadStr = amountToPretty(rci.overhead); return (

{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}

{i18n.str`Rounding loss: ${overheadStr}`}

{uniq.map(row)}
{i18n.str`# Coins`} {i18n.str`Value`} {i18n.str`Withdraw Fee`} {i18n.str`Refresh Fee`} {i18n.str`Deposit Fee`}
); } function getSuggestedExchange(currency: string): Promise { // TODO: make this request go to the wallet backend // Right now, this is a stub. const defaultExchange: {[s: string]: string} = { "KUDOS": "https://exchange.demo.taler.net", "PUDOS": "https://exchange.test.taler.net", }; let exchange = defaultExchange[currency]; if (!exchange) { exchange = "" } return Promise.resolve(exchange); } function WithdrawFee(props: {reserveCreationInfo: ReserveCreationInfo|null}): JSX.Element { if (props.reserveCreationInfo) { let {overhead, withdrawFee} = props.reserveCreationInfo; let totalCost = Amounts.add(overhead, withdrawFee).amount; return

{i18n.str`Withdraw fees:`} {amountToPretty(totalCost)}

; } return

; } interface ExchangeSelectionProps { suggestedExchangeUrl: string; amount: AmountJson; callback_url: string; wt_types: string[]; } class ExchangeSelection extends ImplicitStateComponent { statusString: StateHolder = this.makeState(null); reserveCreationInfo: StateHolder = this.makeState( null); url: StateHolder = this.makeState(null); detailCollapsed: StateHolder = this.makeState(true); updateEvent = new EventTrigger(); constructor(props: ExchangeSelectionProps) { super(props); this.onUrlChanged(props.suggestedExchangeUrl || null); this.forceReserveUpdate(); } renderAdvanced(): JSX.Element { if (this.detailCollapsed() && this.url() !== null && !this.statusString()) { return ( ); } return (

Provider Selection

this.onUrlChanged((e.target as HTMLInputElement).value)}/>
{this.renderStatus()}

{i18n.str`Detailed Fee Structure`}

{renderReserveCreationDetails(this.reserveCreationInfo())}
) } renderFee() { if (!this.reserveCreationInfo()) { return "??"; } let rci = this.reserveCreationInfo()!; let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; return `${amountToPretty(totalCost)}`; } renderFeeStatus() { if (this.reserveCreationInfo()) { return ( The exchange provider will charge {" "} {this.renderFee()} {" "} in fees. ); } if (this.url() && !this.statusString()) { let shortName = URI(this.url()!).host(); return ( Waiting for a response from {" "} {shortName} ); } if (this.statusString()) { return (

{i18n.str`A problem occured, see below.`}

); } return (

{i18n.str`Information about fees will be available when an exchange provider is selected.`}

); } render(): JSX.Element { return (
{"You are about to withdraw "} {amountToPretty(this.props.amount)} {" from your bank account into your wallet."} {this.renderFeeStatus()}
{this.renderAdvanced()}
); } confirmReserve() { this.confirmReserveImpl(this.reserveCreationInfo()!, this.url()!, this.props.amount, this.props.callback_url); } /** * Do an update of the reserve creation info, without any debouncing. */ async forceReserveUpdate() { this.reserveCreationInfo(null); if (!this.url()) { this.statusString(i18n.str`Error: URL is empty`); this.detailCollapsed(false); return; } this.statusString(null); let parsedUrl = URI(this.url()!); if (parsedUrl.is("relative")) { this.statusString(i18n.str`Error: URL may not be relative`); this.detailCollapsed(false); return; } try { let url = canonicalizeBaseUrl(this.url()!); let r = await getReserveCreationInfo(url, this.props.amount); console.log("get exchange info resolved"); this.reserveCreationInfo(r); console.dir(r); } catch (e) { console.log("get exchange info rejected"); if (e.hasOwnProperty("httpStatus")) { this.statusString(`Error: request failed with status ${e.httpStatus}`); this.detailCollapsed(false); } else if (e.hasOwnProperty("errorResponse")) { let resp = e.errorResponse; this.statusString(`Error: ${resp.error} (${resp.hint})`); this.detailCollapsed(false); } } } reset() { this.statusString(null); this.reserveCreationInfo(null); } confirmReserveImpl(rci: ReserveCreationInfo, exchange: string, amount: AmountJson, callback_url: string) { const d = {exchange: canonicalizeBaseUrl(exchange), amount}; const cb = (rawResp: any) => { if (!rawResp) { throw Error("empty response"); } // FIXME: filter out types that bank/exchange don't have in common let wire_details = rci.wireInfo; if (!rawResp.error) { const resp = CreateReserveResponse.checked(rawResp); let q: {[name: string]: string|number} = { wire_details: JSON.stringify(wire_details), exchange: resp.exchange, reserve_pub: resp.reservePub, amount_value: amount.value, amount_fraction: amount.fraction, amount_currency: amount.currency, }; let url = URI(callback_url).addQuery(q); if (!url.is("absolute")) { throw Error("callback url is not absolute"); } console.log("going to", url.href()); document.location.href = url.href(); } else { this.reset(); this.statusString( i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`); this.detailCollapsed(false); } }; chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); } async onUrlChanged(url: string|null) { this.reset(); this.url(url); if (url == undefined) { return; } this.updateEvent.trigger(); let waited = await this.updateEvent.wait(200); if (waited) { // Run the actual update if nobody else preempted us. this.forceReserveUpdate(); this.forceUpdate(); } } renderStatus(): any { if (this.statusString()) { return

{this.statusString()}

; } else if (!this.reserveCreationInfo()) { return

{i18n.str`Checking URL, please wait ...`}

; } return ""; } } export async function main() { try { const url = URI(document.location.href); const query: any = URI.parseQuery(url.query()); let amount; try { amount = AmountJson.checked(JSON.parse(query.amount)); } catch (e) { throw Error(i18n.str`Can't parse amount: ${e.message}`); } const callback_url = query.callback_url; const bank_url = query.bank_url; let wt_types; try { wt_types = JSON.parse(query.wt_types); } catch (e) { throw Error(i18n.str`Can't parse wire_types: ${e.message}`); } const suggestedExchangeUrl = await getSuggestedExchange(amount.currency); let args = { wt_types, suggestedExchangeUrl, callback_url, amount }; ReactDOM.render(, document.getElementById( "exchange-selection")!); } catch (e) { // TODO: provide more context information, maybe factor it out into a // TODO:generic error reporting function or component. document.body.innerText = i18n.str`Fatal error: "${e.message}".`; console.error(`got error "${e.message}"`, e); } }