diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-10-07 17:10:22 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-10-07 17:10:29 +0200 |
commit | 7f95c83f2f993fc3a64d4f5cad1d2d5fd29b08b3 (patch) | |
tree | 82b7de6780475e08d2b2036e3b102b8f1ed5d8e2 | |
parent | cb424d369919ef29f9a3729fedab5323b1883aca (diff) |
refactor reserve creation dialog
-rw-r--r-- | pages/confirm-contract.tsx | 58 | ||||
-rw-r--r-- | pages/confirm-create-reserve.html | 12 | ||||
-rw-r--r-- | pages/confirm-create-reserve.tsx | 325 |
3 files changed, 177 insertions, 218 deletions
diff --git a/pages/confirm-contract.tsx b/pages/confirm-contract.tsx index 9c1752568..6cdc5ddf9 100644 --- a/pages/confirm-contract.tsx +++ b/pages/confirm-contract.tsx @@ -24,7 +24,6 @@ /// <reference path="../lib/decl/preact.d.ts" /> import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; -import m from "mithril"; import {Contract, AmountJson} from "../lib/wallet/types"; import {renderContract, prettyAmount} from "../lib/wallet/renderHtml"; "use strict"; @@ -38,8 +37,6 @@ interface DetailProps { contract: Contract; } -let h = preact.h; - class Details extends preact.Component<DetailProps, DetailState> { constructor() { @@ -60,18 +57,20 @@ class Details extends preact.Component<DetailProps, DetailState> { </div> ); } else { - return h("div", {}, - h("button", { - className: "linky", - onClick: () => { - this.setState({collapsed: true}); - } - }, "show less details"), - h("div", {}, - "Accepted exchanges:", - h("ul", {}, - ...props.contract.exchanges.map( - e => h("li", {}, `${e.url}: ${e.master_pub}`))))); + return ( + <div> + <button className="linky" + onClick={() => this.setState({collapsed: true})}> + show less details + </button> + <div> + Accepted exchanges: + <ul> + {props.contract.exchanges.map( + e => <li>{`${e.url}: ${e.master_pub}`}</li>)} + </ul> + </div> + </div>); } } } @@ -157,19 +156,17 @@ class ContractPrompt extends preact.Component<ContractPromptProps, ContractPromp render(props: ContractPromptProps, state: ContractPromptState) { let c = props.offer.contract; - return h("div", {}, - renderContract(c), - h("button", - { - onClick: () => this.doPayment(), - disabled: state.payDisabled, - "className": "accept" - }, - i18n`Confirm Payment`), - (state.error ? h("p", - {className: "errorbox"}, - state.error) : h("p", "")), - h(Details, {contract: c}) + return ( + <div> + {renderContract(c)} + <button onClick={() => this.doPayment()} + disabled={state.payDisabled} + className="accept"> + Confirm payment + </button> + (state.error ? <p className="errorbox">{state.error}</p> : <p />) + <Details contract={c} /> + </div> ); } } @@ -182,7 +179,6 @@ export function main() { console.dir(offer); let contract = offer.contract; - - let prompt = h(ContractPrompt, {offer}); - preact.render(prompt, document.getElementById("contract")!); + preact.render(<ContractPrompt offer={offer}/>, document.getElementById( + "contract")!); } diff --git a/pages/confirm-create-reserve.html b/pages/confirm-create-reserve.html index 165ac32f4..5c42a68f6 100644 --- a/pages/confirm-create-reserve.html +++ b/pages/confirm-create-reserve.html @@ -18,6 +18,7 @@ <script src="../lib/vendor/system-csp-production.src.js"></script> <script src="../lib/module-trampoline.js"></script> + <style> #main { border: solid 1px black; @@ -47,6 +48,17 @@ cursor:pointer; } + + button.accept:disabled { + background-color: #dedbe8; + border: 1px solid white; + border-radius: 5px; + margin: 1em 0; + padding: 0.5em; + font-weight: bold; + color: #2C2C2C; + } + input.url { width: 25em; } diff --git a/pages/confirm-create-reserve.tsx b/pages/confirm-create-reserve.tsx index 666f8c68a..d0a08aac3 100644 --- a/pages/confirm-create-reserve.tsx +++ b/pages/confirm-create-reserve.tsx @@ -32,35 +32,37 @@ import {getReserveCreationInfo} from "../lib/wallet/wxApi"; let h = preact.h; -/** - * Execute something after a delay, with the possibility - * to reset the delay. - */ -class DelayTimer { - ms: number; - f: () => void; - timerId: number|undefined = undefined; - - constructor(ms: number, f: () => void) { - this.f = f; - this.ms = ms; +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(); } - bump() { - this.stop(); - const handler = () => { - this.f(); - }; - this.timerId = window.setTimeout(handler, this.ms); + private reset() { + this.triggerPromise = new Promise<boolean>((resolve, reject) => { + this.triggerResolve = resolve; + }); } - stop() { - if (this.timerId != undefined) { - window.clearTimeout(this.timerId); - } + trigger() { + this.triggerResolve(false); + this.reset(); + } + + async wait(delayMs: number): Promise<boolean> { + return await Promise.race([this.triggerPromise, delay(delayMs, true)]); } } + interface StateHolder<T> { (): T; (newState: T): void; @@ -85,7 +87,11 @@ abstract class ImplicitStateComponent<PropType> extends preact.Component<PropTyp } -function renderReserveCreationDetails(rci: ReserveCreationInfo) { +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} = {}; @@ -153,6 +159,17 @@ function getSuggestedExchange(currency: string): Promise<string> { 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>Withdraw fees: {amountToPretty(totalCost)}</p>; + } + return <p />; +} + + interface ExchangeSelectionProps { suggestedExchangeUrl: string; amount: AmountJson; @@ -162,84 +179,77 @@ interface ExchangeSelectionProps { class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { - complexViewRequested: StateHolder<boolean> = this.makeState(false); statusString: StateHolder<string|null> = this.makeState(null); reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState( null); url: StateHolder<string|null> = this.makeState(null); detailCollapsed: StateHolder<boolean> = this.makeState(true); - private timer: DelayTimer; - - isValidExchange: boolean; + updateEvent = new EventTrigger(); constructor(props: ExchangeSelectionProps) { super(props); - this.timer = new DelayTimer(800, () => this.update()); - this.url(props.suggestedExchangeUrl || null); - this.update(); + this.onUrlChanged(props.suggestedExchangeUrl || null); } - render(props: ExchangeSelectionProps): JSX.Element { - - console.log("props", props); - - let header = ( - <p> - {"You are about to withdraw "} - <strong>{amountToPretty(props.amount)}</strong> - {" from your bank account into your wallet."} - </p> - ); - if (this.complexViewRequested() || !props.suggestedExchangeUrl) { + renderAdvanced(): JSX.Element { + if (this.detailCollapsed()) { return ( - <div> - {header} - {this.viewComplex()} - </div>); + <button className="linky" + onClick={() => this.detailCollapsed(false)}> + view fee structure / select different exchange provider + </button> + ); } - return ( <div> - {header} - {this.viewSimple()} - </div>); + <h2>Provider Selection</h2> + <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)}/> + <br /> + {this.renderStatus()} + <h2>Detailed Fee Structure</h2> + {renderReserveCreationDetails(this.reserveCreationInfo())} + </div>) } + renderFee() { + if (!this.reserveCreationInfo()) { + return "??"; + } + let rci = this.reserveCreationInfo()!; + let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + return `${amountToPretty(totalCost)}`; + } - viewSimple() { - let advancedButton = ( - <button className="linky" - onClick={() => this.complexViewRequested(true)}> - advanced options - </button> + render(props: ExchangeSelectionProps): JSX.Element { + return ( + <div> + <p> + {"You are about to withdraw "} + <strong>{amountToPretty(props.amount)}</strong> + {" from your bank account into your wallet."} + </p> + <p> + The exchange provider will charge + {" "} + {this.renderFee()} + {" "} + in fees. + </p> + <button className="accept" + disabled={this.reserveCreationInfo() == null} + onClick={() => this.confirmReserve()}> + Accept fees and withdraw + </button> + <br/> + {this.renderAdvanced()} + </div> ); - if (this.statusString()) { - return ( - <div> - <p>Error: {this.statusString()}</p> - {advancedButton} - </div> - ); - } - else if (this.reserveCreationInfo() != undefined) { - let {overhead, withdrawFee} = this.reserveCreationInfo()!; - let totalCost = Amounts.add(overhead, withdrawFee).amount; - return ( - <div> - <p>{`Withdraw fees: ${amountToPretty(totalCost)}`}</p> - <button className="accept" - onClick={() => this.confirmReserve()}> - Accept fees and withdraw - </button> - <span className="spacer"/> - {advancedButton} - </div> - ); - } else { - return <p>Please wait...</p> - } } @@ -250,53 +260,41 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { 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`Error: URL is empty`); + return; + } - update() { - this.timer.stop(); - const doUpdate = () => { - this.reserveCreationInfo(null); - if (!this.url()) { - this.statusString = i18n`Error: URL is empty`; - m.redraw(true); - return; - } - this.statusString(null); - let parsedUrl = URI(this.url()!); - if (parsedUrl.is("relative")) { - this.statusString = i18n`Error: URL may not be relative`; - this.forceUpdate(); - return; - } - - this.forceUpdate(); - - console.log("doing get exchange info"); - - getReserveCreationInfo(this.url()!, this.props.amount) - .then((r: ReserveCreationInfo) => { - console.log("get exchange info resolved"); - this.isValidExchange = true; - 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}`); - } else if (e.hasOwnProperty("errorResponse")) { - let resp = e.errorResponse; - this.statusString(`Error: ${resp.error} (${resp.hint})`); - } - }); - }; - - doUpdate(); + this.statusString(null); + let parsedUrl = URI(this.url()!); + if (parsedUrl.is("relative")) { + this.statusString(i18n`Error: URL may not be relative`); + return; + } - console.log("got update", this.url()); + try { + let r = await getReserveCreationInfo(this.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}`); + } else if (e.hasOwnProperty("errorResponse")) { + let resp = e.errorResponse; + this.statusString(`Error: ${resp.error} (${resp.hint})`); + } + } } reset() { - this.isValidExchange = false; this.statusString(null); this.reserveCreationInfo(null); } @@ -338,75 +336,28 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); } - onUrlChanged(url: string) { + async onUrlChanged(url: string|null) { this.reset(); this.url(url); - this.timer.bump(); + 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(); + } } - viewComplex() { - function *f(): IterableIterator<any> { - if (this.reserveCreationInfo()) { - let {overhead, withdrawFee} = this.reserveCreationInfo()!; - let totalCost = Amounts.add(overhead, withdrawFee).amount; - yield <p>Withdraw fees: {amountToPretty(totalCost)}</p>; - } - - yield ( - <button className="accept" disabled={!this.isValidExchange} - onClick={() => this.confirmReserve()}> - Accept fees and withdraw - </button> - ); - - yield <span className="spacer"/>; - - yield ( - <button className="linky" - onClick={() => this.complexViewRequested(true)}/> - ); - - yield <br/>; - - yield ( - <input className="url" type="text" spellCheck={false} - value={this.url()!} - onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)}/> - ); - - yield <br/>; - - if (this.statusString()) { - yield <p>{this.statusString()}</p>; - } else if (!this.reserveCreationInfo()) { - yield <p>Checking URL, please wait ...</p>; - } - - if (this.reserveCreationInfo()) { - if (this.detailCollapsed()) { - yield ( - <button className="linky" - onClick={() => this.detailCollapsed(false)}> - show more details - </button> - ); - } else { - yield ( - <button className="linky" - onClick={() => this.detailCollapsed(true)}> - hide details - </button> - ); - yield ( - <div> - {renderReserveCreationDetails(this.reserveCreationInfo()!)} - </div> - ); - } - } + renderStatus(): any { + if (this.statusString()) { + return <p><strong style="color: red;">{this.statusString()}</strong></p>; + } else if (!this.reserveCreationInfo()) { + return <p>Checking URL, please wait ...</p>; } - - return Array.from(f.call(this)); + return ""; } } |