refactor reserve creation dialog
This commit is contained in:
parent
cb424d3699
commit
7f95c83f2f
@ -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")!);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
function delay<T>(delayMs: number, value: T): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
setTimeout(() => resolve(value), delayMs);
|
||||
});
|
||||
}
|
||||
|
||||
constructor(ms: number, f: () => void) {
|
||||
this.f = f;
|
||||
this.ms = ms;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
renderAdvanced(): JSX.Element {
|
||||
if (this.detailCollapsed()) {
|
||||
return (
|
||||
<button className="linky"
|
||||
onClick={() => this.detailCollapsed(false)}>
|
||||
view fee structure / select different exchange provider
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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)}`;
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
{this.viewComplex()}
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
{this.viewSimple()}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
viewSimple() {
|
||||
let advancedButton = (
|
||||
<button className="linky"
|
||||
onClick={() => this.complexViewRequested(true)}>
|
||||
advanced options
|
||||
</button>
|
||||
<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`);
|
||||
return;
|
||||
}
|
||||
|
||||
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})`);
|
||||
}
|
||||
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();
|
||||
|
||||
console.log("got update", this.url());
|
||||
}
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user