From 7f95c83f2f993fc3a64d4f5cad1d2d5fd29b08b3 Mon Sep 17 00:00:00 2001
From: Florian Dold <florian.dold@gmail.com>
Date: Fri, 7 Oct 2016 17:10:22 +0200
Subject: refactor reserve creation dialog

---
 pages/confirm-create-reserve.tsx | 325 +++++++++++++++++----------------------
 1 file changed, 138 insertions(+), 187 deletions(-)

(limited to 'pages/confirm-create-reserve.tsx')

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 "";
   }
 }
 
-- 
cgit v1.2.3

