refactor reserve creation dialog
This commit is contained in:
parent
cb424d3699
commit
7f95c83f2f
@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
/// <reference path="../lib/decl/preact.d.ts" />
|
/// <reference path="../lib/decl/preact.d.ts" />
|
||||||
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
|
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
|
||||||
import m from "mithril";
|
|
||||||
import {Contract, AmountJson} from "../lib/wallet/types";
|
import {Contract, AmountJson} from "../lib/wallet/types";
|
||||||
import {renderContract, prettyAmount} from "../lib/wallet/renderHtml";
|
import {renderContract, prettyAmount} from "../lib/wallet/renderHtml";
|
||||||
"use strict";
|
"use strict";
|
||||||
@ -38,8 +37,6 @@ interface DetailProps {
|
|||||||
contract: Contract;
|
contract: Contract;
|
||||||
}
|
}
|
||||||
|
|
||||||
let h = preact.h;
|
|
||||||
|
|
||||||
|
|
||||||
class Details extends preact.Component<DetailProps, DetailState> {
|
class Details extends preact.Component<DetailProps, DetailState> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -60,18 +57,20 @@ class Details extends preact.Component<DetailProps, DetailState> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return h("div", {},
|
return (
|
||||||
h("button", {
|
<div>
|
||||||
className: "linky",
|
<button className="linky"
|
||||||
onClick: () => {
|
onClick={() => this.setState({collapsed: true})}>
|
||||||
this.setState({collapsed: true});
|
show less details
|
||||||
}
|
</button>
|
||||||
}, "show less details"),
|
<div>
|
||||||
h("div", {},
|
Accepted exchanges:
|
||||||
"Accepted exchanges:",
|
<ul>
|
||||||
h("ul", {},
|
{props.contract.exchanges.map(
|
||||||
...props.contract.exchanges.map(
|
e => <li>{`${e.url}: ${e.master_pub}`}</li>)}
|
||||||
e => h("li", {}, `${e.url}: ${e.master_pub}`)))));
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,19 +156,17 @@ class ContractPrompt extends preact.Component<ContractPromptProps, ContractPromp
|
|||||||
|
|
||||||
render(props: ContractPromptProps, state: ContractPromptState) {
|
render(props: ContractPromptProps, state: ContractPromptState) {
|
||||||
let c = props.offer.contract;
|
let c = props.offer.contract;
|
||||||
return h("div", {},
|
return (
|
||||||
renderContract(c),
|
<div>
|
||||||
h("button",
|
{renderContract(c)}
|
||||||
{
|
<button onClick={() => this.doPayment()}
|
||||||
onClick: () => this.doPayment(),
|
disabled={state.payDisabled}
|
||||||
disabled: state.payDisabled,
|
className="accept">
|
||||||
"className": "accept"
|
Confirm payment
|
||||||
},
|
</button>
|
||||||
i18n`Confirm Payment`),
|
(state.error ? <p className="errorbox">{state.error}</p> : <p />)
|
||||||
(state.error ? h("p",
|
<Details contract={c} />
|
||||||
{className: "errorbox"},
|
</div>
|
||||||
state.error) : h("p", "")),
|
|
||||||
h(Details, {contract: c})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,7 +179,6 @@ export function main() {
|
|||||||
console.dir(offer);
|
console.dir(offer);
|
||||||
let contract = offer.contract;
|
let contract = offer.contract;
|
||||||
|
|
||||||
|
preact.render(<ContractPrompt offer={offer}/>, document.getElementById(
|
||||||
let prompt = h(ContractPrompt, {offer});
|
"contract")!);
|
||||||
preact.render(prompt, document.getElementById("contract")!);
|
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<script src="../lib/vendor/system-csp-production.src.js"></script>
|
<script src="../lib/vendor/system-csp-production.src.js"></script>
|
||||||
<script src="../lib/module-trampoline.js"></script>
|
<script src="../lib/module-trampoline.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#main {
|
#main {
|
||||||
border: solid 1px black;
|
border: solid 1px black;
|
||||||
@ -47,6 +48,17 @@
|
|||||||
cursor:pointer;
|
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 {
|
input.url {
|
||||||
width: 25em;
|
width: 25em;
|
||||||
}
|
}
|
||||||
|
@ -32,35 +32,37 @@ import {getReserveCreationInfo} from "../lib/wallet/wxApi";
|
|||||||
|
|
||||||
let h = preact.h;
|
let h = preact.h;
|
||||||
|
|
||||||
/**
|
function delay<T>(delayMs: number, value: T): Promise<T> {
|
||||||
* Execute something after a delay, with the possibility
|
return new Promise<T>((resolve, reject) => {
|
||||||
* to reset the delay.
|
setTimeout(() => resolve(value), delayMs);
|
||||||
*/
|
});
|
||||||
class DelayTimer {
|
}
|
||||||
ms: number;
|
|
||||||
f: () => void;
|
|
||||||
timerId: number|undefined = undefined;
|
|
||||||
|
|
||||||
constructor(ms: number, f: () => void) {
|
class EventTrigger {
|
||||||
this.f = f;
|
triggerResolve: any;
|
||||||
this.ms = ms;
|
triggerPromise: Promise<boolean>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
bump() {
|
private reset() {
|
||||||
this.stop();
|
this.triggerPromise = new Promise<boolean>((resolve, reject) => {
|
||||||
const handler = () => {
|
this.triggerResolve = resolve;
|
||||||
this.f();
|
});
|
||||||
};
|
|
||||||
this.timerId = window.setTimeout(handler, this.ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
trigger() {
|
||||||
if (this.timerId != undefined) {
|
this.triggerResolve(false);
|
||||||
window.clearTimeout(this.timerId);
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async wait(delayMs: number): Promise<boolean> {
|
||||||
|
return await Promise.race([this.triggerPromise, delay(delayMs, true)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface StateHolder<T> {
|
interface StateHolder<T> {
|
||||||
(): T;
|
(): T;
|
||||||
(newState: T): void;
|
(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 denoms = rci.selectedDenoms;
|
||||||
|
|
||||||
let countByPub: {[s: string]: number} = {};
|
let countByPub: {[s: string]: number} = {};
|
||||||
@ -153,6 +159,17 @@ function getSuggestedExchange(currency: string): Promise<string> {
|
|||||||
return Promise.resolve(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>Withdraw fees: {amountToPretty(totalCost)}</p>;
|
||||||
|
}
|
||||||
|
return <p />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ExchangeSelectionProps {
|
interface ExchangeSelectionProps {
|
||||||
suggestedExchangeUrl: string;
|
suggestedExchangeUrl: string;
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
@ -162,84 +179,77 @@ interface ExchangeSelectionProps {
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||||
complexViewRequested: StateHolder<boolean> = this.makeState(false);
|
|
||||||
statusString: StateHolder<string|null> = this.makeState(null);
|
statusString: StateHolder<string|null> = this.makeState(null);
|
||||||
reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState(
|
reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState(
|
||||||
null);
|
null);
|
||||||
url: StateHolder<string|null> = this.makeState(null);
|
url: StateHolder<string|null> = this.makeState(null);
|
||||||
detailCollapsed: StateHolder<boolean> = this.makeState(true);
|
detailCollapsed: StateHolder<boolean> = this.makeState(true);
|
||||||
|
|
||||||
private timer: DelayTimer;
|
updateEvent = new EventTrigger();
|
||||||
|
|
||||||
isValidExchange: boolean;
|
|
||||||
|
|
||||||
constructor(props: ExchangeSelectionProps) {
|
constructor(props: ExchangeSelectionProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.timer = new DelayTimer(800, () => this.update());
|
this.onUrlChanged(props.suggestedExchangeUrl || null);
|
||||||
this.url(props.suggestedExchangeUrl || null);
|
}
|
||||||
this.update();
|
|
||||||
|
|
||||||
|
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 {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{header}
|
<p>
|
||||||
{this.viewSimple()}
|
{"You are about to withdraw "}
|
||||||
</div>);
|
<strong>{amountToPretty(props.amount)}</strong>
|
||||||
}
|
{" from your bank account into your wallet."}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
viewSimple() {
|
The exchange provider will charge
|
||||||
let advancedButton = (
|
{" "}
|
||||||
<button className="linky"
|
{this.renderFee()}
|
||||||
onClick={() => this.complexViewRequested(true)}>
|
{" "}
|
||||||
advanced options
|
in fees.
|
||||||
</button>
|
</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);
|
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.statusString(null);
|
||||||
this.timer.stop();
|
let parsedUrl = URI(this.url()!);
|
||||||
const doUpdate = () => {
|
if (parsedUrl.is("relative")) {
|
||||||
this.reserveCreationInfo(null);
|
this.statusString(i18n`Error: URL may not be relative`);
|
||||||
if (!this.url()) {
|
return;
|
||||||
this.statusString = i18n`Error: URL is empty`;
|
}
|
||||||
m.redraw(true);
|
|
||||||
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() {
|
reset() {
|
||||||
this.isValidExchange = false;
|
|
||||||
this.statusString(null);
|
this.statusString(null);
|
||||||
this.reserveCreationInfo(null);
|
this.reserveCreationInfo(null);
|
||||||
}
|
}
|
||||||
@ -338,75 +336,28 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
|||||||
chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
|
chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUrlChanged(url: string) {
|
async onUrlChanged(url: string|null) {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.url(url);
|
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() {
|
renderStatus(): any {
|
||||||
function *f(): IterableIterator<any> {
|
if (this.statusString()) {
|
||||||
if (this.reserveCreationInfo()) {
|
return <p><strong style="color: red;">{this.statusString()}</strong></p>;
|
||||||
let {overhead, withdrawFee} = this.reserveCreationInfo()!;
|
} else if (!this.reserveCreationInfo()) {
|
||||||
let totalCost = Amounts.add(overhead, withdrawFee).amount;
|
return <p>Checking URL, please wait ...</p>;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
return Array.from(f.call(this));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user