aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-10-07 17:10:22 +0200
committerFlorian Dold <florian.dold@gmail.com>2016-10-07 17:10:29 +0200
commit7f95c83f2f993fc3a64d4f5cad1d2d5fd29b08b3 (patch)
tree82b7de6780475e08d2b2036e3b102b8f1ed5d8e2
parentcb424d369919ef29f9a3729fedab5323b1883aca (diff)
downloadwallet-core-7f95c83f2f993fc3a64d4f5cad1d2d5fd29b08b3.tar.xz
refactor reserve creation dialog
-rw-r--r--pages/confirm-contract.tsx58
-rw-r--r--pages/confirm-create-reserve.html12
-rw-r--r--pages/confirm-create-reserve.tsx325
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 "";
}
}