diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-05-28 23:15:41 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-05-28 23:15:41 +0200 |
commit | b6e774585d32017e5f1ceeeb2b2e2a5e350354d3 (patch) | |
tree | 080cb5afe3b48c0428abd2d7de1ff7fe34d9b9b1 /src/pages/confirm-create-reserve.tsx | |
parent | 38a74188d759444d7e1abac856f78ae710e2a4c5 (diff) | |
download | wallet-core-b6e774585d32017e5f1ceeeb2b2e2a5e350354d3.tar.xz |
move webex specific things in their own directory
Diffstat (limited to 'src/pages/confirm-create-reserve.tsx')
-rw-r--r-- | src/pages/confirm-create-reserve.tsx | 639 |
1 files changed, 0 insertions, 639 deletions
diff --git a/src/pages/confirm-create-reserve.tsx b/src/pages/confirm-create-reserve.tsx deleted file mode 100644 index 2f341bb4e..000000000 --- a/src/pages/confirm-create-reserve.tsx +++ /dev/null @@ -1,639 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - - -/** - * Page shown to the user to confirm creation - * of a reserve, usually requested by the bank. - * - * @author Florian Dold - */ - -import {amountToPretty, canonicalizeBaseUrl} from "../helpers"; -import { - AmountJson, CreateReserveResponse, - ReserveCreationInfo, Amounts, - Denomination, DenominationRecord, CurrencyRecord -} from "../types"; -import {getReserveCreationInfo, getCurrency, getExchangeInfo} from "../wxApi"; -import {ImplicitStateComponent, StateHolder} from "../components"; -import * as i18n from "../i18n"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); -import * as moment from "moment"; - - -function delay<T>(delayMs: number, value: T): Promise<T> { - return new Promise<T>((resolve, reject) => { - setTimeout(() => resolve(value), delayMs); - }); -} - -class EventTrigger { - triggerResolve: any; - triggerPromise: Promise<boolean>; - - constructor() { - this.reset(); - } - - private reset() { - this.triggerPromise = new Promise<boolean>((resolve, reject) => { - this.triggerResolve = resolve; - }); - } - - trigger() { - this.triggerResolve(false); - this.reset(); - } - - async wait(delayMs: number): Promise<boolean> { - return await Promise.race([this.triggerPromise, delay(delayMs, true)]); - } -} - - -interface CollapsibleState { - collapsed: boolean; -} - -interface CollapsibleProps { - initiallyCollapsed: boolean; - title: string; -} - -class Collapsible extends React.Component<CollapsibleProps, CollapsibleState> { - constructor(props: CollapsibleProps) { - super(props); - this.state = { collapsed: props.initiallyCollapsed }; - } - render() { - const doOpen = (e: any) => { - this.setState({collapsed: false}) - e.preventDefault() - }; - const doClose = (e: any) => { - this.setState({collapsed: true}) - e.preventDefault(); - }; - if (this.state.collapsed) { - return <h2><a className="opener opener-collapsed" href="#" onClick={doOpen}>{this.props.title}</a></h2>; - } - return ( - <div> - <h2><a className="opener opener-open" href="#" onClick={doClose}>{this.props.title}</a></h2> - {this.props.children} - </div> - ); - } -} - -function renderAuditorDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( - <p> - Details will be displayed when a valid exchange provider URL is entered. - </p> - ); - } - if (rci.exchangeInfo.auditors.length == 0) { - return ( - <p> - The exchange is not audited by any auditors. - </p> - ); - } - return ( - <div> - {rci.exchangeInfo.auditors.map(a => ( - <h3>Auditor {a.url}</h3> - ))} - </div> - ); -} - -function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( - <p> - Details will be displayed when a valid exchange provider URL is entered. - </p> - ); - } - - 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 ( - <tr> - <td>{countByPub[denom.denomPub] + "x"}</td> - <td>{amountToPretty(denom.value)}</td> - <td>{amountToPretty(denom.feeWithdraw)}</td> - <td>{amountToPretty(denom.feeRefresh)}</td> - <td>{amountToPretty(denom.feeDeposit)}</td> - </tr> - ); - } - - function wireFee(s: string) { - return [ - <thead> - <tr> - <th colSpan={3}>Wire Method {s}</th> - </tr> - <tr> - <th>Applies Until</th> - <th>Wire Fee</th> - <th>Closing Fee</th> - </tr> - </thead>, - <tbody> - {rci!.wireFees.feesForType[s].map(f => ( - <tr> - <td>{moment.unix(f.endStamp).format("llll")}</td> - <td>{amountToPretty(f.wireFee)}</td> - <td>{amountToPretty(f.closingFee)}</td> - </tr> - ))} - </tbody> - ]; - } - - let withdrawFeeStr = amountToPretty(rci.withdrawFee); - let overheadStr = amountToPretty(rci.overhead); - - return ( - <div> - <h3>Overview</h3> - <p>{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}</p> - <p>{i18n.str`Rounding loss: ${overheadStr}`}</p> - <p>{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p> - <h3>Coin Fees</h3> - <table className="pure-table"> - <thead> - <tr> - <th>{i18n.str`# Coins`}</th> - <th>{i18n.str`Value`}</th> - <th>{i18n.str`Withdraw Fee`}</th> - <th>{i18n.str`Refresh Fee`}</th> - <th>{i18n.str`Deposit Fee`}</th> - </tr> - </thead> - <tbody> - {uniq.map(row)} - </tbody> - </table> - <h3>Wire Fees</h3> - <table className="pure-table"> - {Object.keys(rci.wireFees.feesForType).map(wireFee)} - </table> - </div> - ); -} - - -function getSuggestedExchange(currency: string): Promise<string> { - // 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 <p>{i18n.str`Withdraw fees:`} {amountToPretty(totalCost)}</p>; - } - return <p />; -} - - -interface ExchangeSelectionProps { - suggestedExchangeUrl: string; - amount: AmountJson; - callback_url: string; - wt_types: string[]; - currencyRecord: CurrencyRecord|null; -} - -interface ManualSelectionProps { - onSelect(url: string): void; - initialUrl: string; -} - -class ManualSelection extends ImplicitStateComponent<ManualSelectionProps> { - url: StateHolder<string> = this.makeState(""); - errorMessage: StateHolder<string|null> = this.makeState(null); - isOkay: StateHolder<boolean> = this.makeState(false); - updateEvent = new EventTrigger(); - constructor(p: ManualSelectionProps) { - super(p); - this.url(p.initialUrl); - this.update(); - } - render() { - return ( - <div className="pure-g pure-form pure-form-stacked"> - <div className="pure-u-1"> - <label>URL</label> - <input className="url" type="text" spellCheck={false} - value={this.url()} - key="exchange-url-input" - onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)} /> - </div> - <div className="pure-u-1"> - <button className="pure-button button-success" - disabled={!this.isOkay()} - onClick={() => this.props.onSelect(this.url())}> - {i18n.str`Select`} - </button> - {this.errorMessage()} - </div> - </div> - ); - } - - async update() { - this.errorMessage(null); - this.isOkay(false); - if (!this.url()) { - return; - } - let parsedUrl = new URI(this.url()!); - if (parsedUrl.is("relative")) { - this.errorMessage(i18n.str`Error: URL may not be relative`); - this.isOkay(false); - return; - } - try { - let url = canonicalizeBaseUrl(this.url()!); - let r = await getExchangeInfo(url) - console.log("getExchangeInfo returned") - this.isOkay(true); - } catch (e) { - console.log("got error", e); - if (e.hasOwnProperty("httpStatus")) { - this.errorMessage(`Error: request failed with status ${e.httpStatus}`); - } else if (e.hasOwnProperty("errorResponse")) { - let resp = e.errorResponse; - this.errorMessage(`Error: ${resp.error} (${resp.hint})`); - } else { - this.errorMessage("invalid exchange URL"); - } - } - } - - async onUrlChanged(s: string) { - this.url(s); - this.errorMessage(null); - this.isOkay(false); - this.updateEvent.trigger(); - let waited = await this.updateEvent.wait(200); - if (waited) { - // Run the actual update if nobody else preempted us. - this.update(); - } - } -} - - -class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { - statusString: StateHolder<string|null> = this.makeState(null); - reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState( - null); - url: StateHolder<string|null> = this.makeState(null); - - selectingExchange: StateHolder<boolean> = this.makeState(false); - - constructor(props: ExchangeSelectionProps) { - super(props); - let prefilledExchangesUrls = []; - if (props.currencyRecord) { - let exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl); - prefilledExchangesUrls.push(...exchanges); - } - if (props.suggestedExchangeUrl) { - prefilledExchangesUrls.push(props.suggestedExchangeUrl); - } - if (prefilledExchangesUrls.length != 0) { - this.url(prefilledExchangesUrls[0]); - this.forceReserveUpdate(); - } else { - this.selectingExchange(true); - } - } - - renderFeeStatus() { - let rci = this.reserveCreationInfo(); - if (rci) { - let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; - let trustMessage; - if (rci.isTrusted) { - trustMessage = ( - <i18n.Translate wrap="p"> - The exchange is trusted by the wallet. - </i18n.Translate> - ); - } else if (rci.isAudited) { - trustMessage = ( - <i18n.Translate wrap="p"> - The exchange is audited by a trusted auditor. - </i18n.Translate> - ); - } else { - trustMessage = ( - <i18n.Translate wrap="p"> - Warning: The exchange is neither directly trusted nor audited by a trusted auditor. - If you withdraw from this exchange, it will be trusted in the future. - </i18n.Translate> - ); - } - return ( - <div> - <i18n.Translate wrap="p"> - Using exchange provider <strong>{this.url()}</strong>. - The exchange provider will charge - {" "} - <span>{amountToPretty(totalCost)}</span> - {" "} - in fees. - </i18n.Translate> - {trustMessage} - </div> - ); - } - if (this.url() && !this.statusString()) { - let shortName = new URI(this.url()!).host(); - return ( - <i18n.Translate wrap="p"> - Waiting for a response from - {" "} - <em>{shortName}</em> - </i18n.Translate> - ); - } - if (this.statusString()) { - return ( - <p> - <strong style={{color: "red"}}>{i18n.str`A problem occured, see below. ${this.statusString()}`}</strong> - </p> - ); - } - return ( - <p> - {i18n.str`Information about fees will be available when an exchange provider is selected.`} - </p> - ); - } - - renderConfirm() { - return ( - <div> - {this.renderFeeStatus()} - <button className="pure-button button-success" - disabled={this.reserveCreationInfo() == null} - onClick={() => this.confirmReserve()}> - {i18n.str`Accept fees and withdraw`} - </button> - { " " } - <button className="pure-button button-secondary" - onClick={() => this.selectingExchange(true)}> - {i18n.str`Change Exchange Provider`} - </button> - <br/> - <Collapsible initiallyCollapsed={true} title="Fee and Spending Details"> - {renderReserveCreationDetails(this.reserveCreationInfo())} - </Collapsible> - <Collapsible initiallyCollapsed={true} title="Auditor Details"> - {renderAuditorDetails(this.reserveCreationInfo())} - </Collapsible> - </div> - ); - } - - select(url: string) { - this.reserveCreationInfo(null); - this.url(url); - this.selectingExchange(false); - this.forceReserveUpdate(); - } - - renderSelect() { - let exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || []; - console.log(exchanges); - return ( - <div> - Please select an exchange. You can review the details before after your selection. - - {this.props.suggestedExchangeUrl && ( - <div> - <h2>Bank Suggestion</h2> - <button className="pure-button button-success" onClick={() => this.select(this.props.suggestedExchangeUrl)}> - Select <strong>{this.props.suggestedExchangeUrl}</strong> - </button> - </div> - )} - - {exchanges.length > 0 && ( - <div> - <h2>Known Exchanges</h2> - {exchanges.map(e => ( - <button className="pure-button button-success" onClick={() => this.select(e.baseUrl)}> - Select <strong>{e.baseUrl}</strong> - </button> - ))} - </div> - )} - - <h2>Manual Selection</h2> - <ManualSelection initialUrl={this.url() || ""} onSelect={(url: string) => this.select(url)} /> - </div> - ); - } - - render(): JSX.Element { - return ( - <div> - <i18n.Translate wrap="p"> - {"You are about to withdraw "} - <strong>{amountToPretty(this.props.amount)}</strong> - {" from your bank account into your wallet."} - </i18n.Translate> - {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()} - </div> - ); - } - - - 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); - 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", e); - if (e.hasOwnProperty("httpStatus")) { - this.statusString(`Error: request failed with status ${e.httpStatus}`); - } else if (e.hasOwnProperty("errorResponse")) { - let resp = e.errorResponse; - this.statusString(`Error: ${resp.error} (${resp.hint})`); - } - } - } - - 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 wireDetails = rci.wireInfo; - let filteredWireDetails: any = {}; - for (let wireType in wireDetails) { - if (this.props.wt_types.findIndex((x) => x.toLowerCase() == wireType.toLowerCase()) < 0) { - continue; - } - let obj = Object.assign({}, wireDetails[wireType]); - // The bank doesn't need to know about fees - delete obj.fees; - // Consequently the bank can't verify signatures anyway, so - // we delete this extra data, to make the request URL shorter. - delete obj.salt; - delete obj.sig; - filteredWireDetails[wireType] = obj; - } - if (!rawResp.error) { - const resp = CreateReserveResponse.checked(rawResp); - let q: {[name: string]: string|number} = { - wire_details: JSON.stringify(filteredWireDetails), - exchange: resp.exchange, - reserve_pub: resp.reservePub, - amount_value: amount.value, - amount_fraction: amount.fraction, - amount_currency: amount.currency, - }; - let url = new 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.statusString( - i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`); - } - }; - chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); - } - - renderStatus(): any { - if (this.statusString()) { - return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>; - } else if (!this.reserveCreationInfo()) { - return <p>{i18n.str`Checking URL, please wait ...`}</p>; - } - return ""; - } -} - -export async function main() { - try { - const url = new 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}`); - } - - let suggestedExchangeUrl = query.suggested_exchange_url; - let currencyRecord = await getCurrency(amount.currency); - - let args = { - wt_types, - suggestedExchangeUrl, - callback_url, - amount, - currencyRecord, - }; - - ReactDOM.render(<ExchangeSelection {...args} />, 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); - } -} - -document.addEventListener("DOMContentLoaded", () => { - main(); -}); |