From d6bf24902a34f2094363121c8d9f4d54db6f7b6c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 28 Apr 2017 23:28:27 +0200 Subject: implement new reserve creation dialog and auditor management --- src/pages/auditors.html | 3 + src/pages/auditors.tsx | 32 ++- src/pages/confirm-create-reserve.html | 29 +++ src/pages/confirm-create-reserve.tsx | 401 +++++++++++++++++++++++++--------- src/pages/popup.html | 1 + src/pages/popup.tsx | 22 +- 6 files changed, 373 insertions(+), 115 deletions(-) (limited to 'src/pages') diff --git a/src/pages/auditors.html b/src/pages/auditors.html index 7e01f4e1f..2f50b28a1 100644 --- a/src/pages/auditors.html +++ b/src/pages/auditors.html @@ -14,6 +14,9 @@ + diff --git a/src/pages/confirm-create-reserve.tsx b/src/pages/confirm-create-reserve.tsx index a7fd7b0fd..6b618c273 100644 --- a/src/pages/confirm-create-reserve.tsx +++ b/src/pages/confirm-create-reserve.tsx @@ -26,14 +26,15 @@ import {amountToPretty, canonicalizeBaseUrl} from "../helpers"; import { AmountJson, CreateReserveResponse, ReserveCreationInfo, Amounts, - Denomination, DenominationRecord, + Denomination, DenominationRecord, CurrencyRecord } from "../types"; -import {getReserveCreationInfo} from "../wxApi"; +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(delayMs: number, value: T): Promise { @@ -67,10 +68,72 @@ class EventTrigger { } +interface CollapsibleState { + collapsed: boolean; +} + +interface CollapsibleProps { + initiallyCollapsed: boolean; + title: string; +} + +class Collapsible extends React.Component { + 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

{this.props.title}

; + } + return ( +
+

{this.props.title}

+ {this.props.children} +
+ ); + } +} + +function renderAuditorDetails(rci: ReserveCreationInfo|null) { + if (!rci) { + return ( +

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

+ ); + } + if (rci.exchangeInfo.auditors.length == 0) { + return ( +

+ The exchange is not audited by any auditors. +

+ ); + } + return ( +
+ {rci.exchangeInfo.auditors.map(a => ( +

Auditor {a.url}

+ ))} +
+ ); +} + function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { if (!rci) { - return

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

+ return ( +

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

+ ); } let denoms = rci.selectedDenoms; @@ -99,25 +162,57 @@ function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { ); } + function wireFee(s: string) { + return [ + + + Wire Method {s} + + + Applies Until + Wire Fee + Closing Fee + + , + + {rci!.wireFees.feesForType[s].map(f => ( + + {moment.unix(f.endStamp).format("llll")} + {amountToPretty(f.wireFee)} + {amountToPretty(f.closingFee)} + + ))} + + ]; + } + let withdrawFeeStr = amountToPretty(rci.withdrawFee); let overheadStr = amountToPretty(rci.overhead); return (
+

Overview

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

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

- +

Coin Fees

+
- - - - - + + + + + + + {uniq.map(row)}
{i18n.str`# Coins`}{i18n.str`Value`}{i18n.str`Withdraw Fee`}{i18n.str`Refresh Fee`}{i18n.str`Deposit Fee`}
{i18n.str`# Coins`}{i18n.str`Value`}{i18n.str`Withdraw Fee`}{i18n.str`Refresh Fee`}{i18n.str`Deposit Fee`}
+

Wire Fees

+ + {Object.keys(rci.wireFees.feesForType).map(wireFee)} +
); } @@ -156,6 +251,87 @@ interface ExchangeSelectionProps { amount: AmountJson; callback_url: string; wt_types: string[]; + currencyRecord: CurrencyRecord|null; +} + +interface ManualSelectionProps { + onSelect(url: string): void; + initialUrl: string; +} + +class ManualSelection extends ImplicitStateComponent { + url: StateHolder = this.makeState(""); + errorMessage: StateHolder = this.makeState(null); + isOkay: StateHolder = this.makeState(false); + updateEvent = new EventTrigger(); + constructor(p: ManualSelectionProps) { + super(p); + this.url(p.initialUrl); + this.update(); + } + render() { + return ( +
+
+ + this.onUrlChanged((e.target as HTMLInputElement).value)} /> +
+
+ + {this.errorMessage()} +
+
+ ); + } + + 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(); + } + } } @@ -164,60 +340,64 @@ class ExchangeSelection extends ImplicitStateComponent { reserveCreationInfo: StateHolder = this.makeState( null); url: StateHolder = this.makeState(null); - detailCollapsed: StateHolder = this.makeState(true); - updateEvent = new EventTrigger(); + selectingExchange: StateHolder = this.makeState(false); constructor(props: ExchangeSelectionProps) { super(props); - this.onUrlChanged(props.suggestedExchangeUrl || null); - this.forceReserveUpdate(); - } - - - renderAdvanced(): JSX.Element { - if (this.detailCollapsed() && this.url() !== null && !this.statusString()) { - return ( - - ); + let prefilledExchangesUrls = []; + if (props.currencyRecord) { + let exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl); + prefilledExchangesUrls.push(...exchanges); } - 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 "??"; + if (props.suggestedExchangeUrl) { + prefilledExchangesUrls.push(props.suggestedExchangeUrl); + } + if (prefilledExchangesUrls.length != 0) { + this.url(prefilledExchangesUrls[0]); + this.forceReserveUpdate(); + } else { + this.selectingExchange(true); } - let rci = this.reserveCreationInfo()!; - let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; - return `${amountToPretty(totalCost)}`; } renderFeeStatus() { - if (this.reserveCreationInfo()) { + let rci = this.reserveCreationInfo(); + if (rci) { + let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + let trustMessage; + if (rci.isTrusted) { + trustMessage = ( + + The exchange is trusted by the wallet. + + ); + } else if (rci.isAudited) { + trustMessage = ( + + The exchange is audited by a trusted auditor. + + ); + } else { + trustMessage = ( + + 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. + + ); + } return ( +
+ Using exchange provider {this.url()}. The exchange provider will charge {" "} - {this.renderFee()} + {amountToPretty(totalCost)} {" "} in fees. + {trustMessage} +
); } if (this.url() && !this.statusString()) { @@ -233,7 +413,7 @@ class ExchangeSelection extends ImplicitStateComponent { if (this.statusString()) { return (

- {i18n.str`A problem occured, see below.`} + {i18n.str`A problem occured, see below. ${this.statusString()}`}

); } @@ -244,22 +424,80 @@ class ExchangeSelection extends ImplicitStateComponent { ); } - render(): JSX.Element { + renderConfirm() { return (
- - {"You are about to withdraw "} - {amountToPretty(this.props.amount)} - {" from your bank account into your wallet."} - {this.renderFeeStatus()} - + { " " } +
- {this.renderAdvanced()} + + {renderReserveCreationDetails(this.reserveCreationInfo())} + + + {renderAuditorDetails(this.reserveCreationInfo())} + +
+ ); + } + + 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 ( +
+ Please select an exchange. You can review the details before after your selection. + + {this.props.suggestedExchangeUrl && ( +
+

Bank Suggestion

+ +
+ )} + + {exchanges.length > 0 && ( +
+

Known Exchanges

+ {exchanges.map(e => ( + + ))} +
+ )} + +

Manual Selection

+ this.select(url)} /> +
+ ); + } + + render(): JSX.Element { + return ( +
+ + {"You are about to withdraw "} + {amountToPretty(this.props.amount)} + {" from your bank account into your wallet."} + + {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()}
); } @@ -277,20 +515,6 @@ class ExchangeSelection extends ImplicitStateComponent { */ 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 = new 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, @@ -299,23 +523,16 @@ class ExchangeSelection extends ImplicitStateComponent { this.reserveCreationInfo(r); console.dir(r); } catch (e) { - console.log("get exchange info rejected"); + console.log("get exchange info rejected", e); 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, @@ -358,30 +575,13 @@ class ExchangeSelection extends ImplicitStateComponent { 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()}

; @@ -411,16 +611,15 @@ export async function main() { throw Error(i18n.str`Can't parse wire_types: ${e.message}`); } - let suggestedExchangeUrl = await getSuggestedExchange(amount.currency); - if (!suggestedExchangeUrl && query.suggested_exchange_url) { - suggestedExchangeUrl = query.suggested_exchange_url; - } + let suggestedExchangeUrl = query.suggested_exchange_url; + let currencyRecord = await getCurrency(amount.currency); let args = { wt_types, suggestedExchangeUrl, callback_url, - amount + amount, + currencyRecord, }; ReactDOM.render(, document.getElementById( diff --git a/src/pages/popup.html b/src/pages/popup.html index 7ff5cffaf..702f43cde 100644 --- a/src/pages/popup.html +++ b/src/pages/popup.html @@ -5,6 +5,7 @@ + diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx index c8d52b45c..7f179366a 100644 --- a/src/pages/popup.tsx +++ b/src/pages/popup.tsx @@ -309,18 +309,16 @@ class WalletBalanceView extends React.Component {

); }); - if (listing.length > 0) { - let link = chrome.extension.getURL("/src/pages/auditors.html"); - let linkElem = auditors; - return ( -
- {listing} - {linkElem} -
- ); - } - - return this.renderEmpty(); + let link = chrome.extension.getURL("/src/pages/auditors.html"); + let linkElem = Trusted Auditors and Exchanges; + return ( +
+

Available Balance

+ {listing.length > 0 ? listing : this.renderEmpty()} +

Settings

+ {linkElem} +
+ ); } } -- cgit v1.2.3