diff options
Diffstat (limited to 'pages')
| -rw-r--r-- | pages/confirm-create-reserve.html | 8 | ||||
| -rw-r--r-- | pages/confirm-create-reserve.tsx | 503 | 
2 files changed, 282 insertions, 229 deletions
diff --git a/pages/confirm-create-reserve.html b/pages/confirm-create-reserve.html index b8a825bd1..165ac32f4 100644 --- a/pages/confirm-create-reserve.html +++ b/pages/confirm-create-reserve.html @@ -7,11 +7,15 @@    <link rel="icon" href="../img/icon.png">    <script src="../lib/vendor/URI.js"></script> -  <script src="../lib/vendor/mithril.js"></script> -  <script src="../lib/vendor/system-csp-production.src.js"></script> +  <script src="../lib/vendor/preact.js"></script> + +  <!-- i18n -->    <script src="../lib/vendor/jed.js"></script>    <script src="../lib/i18n.js"></script>    <script src="../i18n/strings.js"></script> + +  <!-- module loading --> +  <script src="../lib/vendor/system-csp-production.src.js"></script>    <script src="../lib/module-trampoline.js"></script>    <style> diff --git a/pages/confirm-create-reserve.tsx b/pages/confirm-create-reserve.tsx index 8e8067052..666f8c68a 100644 --- a/pages/confirm-create-reserve.tsx +++ b/pages/confirm-create-reserve.tsx @@ -22,18 +22,16 @@   * @author Florian Dold   */ -/// <reference path="../lib/decl/mithril.d.ts" /> -  import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers";  import {AmountJson, CreateReserveResponse} from "../lib/wallet/types"; -import m from "mithril";  import {ReserveCreationInfo, Amounts} from "../lib/wallet/types"; -import MithrilComponent = _mithril.MithrilComponent;  import {Denomination} from "../lib/wallet/types";  import {getReserveCreationInfo} from "../lib/wallet/wxApi";  "use strict"; +let h = preact.h; +  /**   * Execute something after a delay, with the possibility   * to reset the delay. @@ -63,73 +61,232 @@ class DelayTimer {    }  } +interface StateHolder<T> { +  (): T; +  (newState: T): void; +} -class Controller { -  url = m.prop<string>(); -  statusString: string | null = null; -  isValidExchange = false; -  reserveCreationInfo?: ReserveCreationInfo; -  private timer: DelayTimer; -  amount: AmountJson; -  callbackUrl: string; -  wtTypes: string[]; -  detailCollapsed = m.prop<boolean>(true); +/** + * Component that doesn't hold its state in one object, + * but has multiple state holders. + */ +abstract class ImplicitStateComponent<PropType> extends preact.Component<PropType, void> { +  makeState<StateType>(initial: StateType): StateHolder<StateType> { +    let state: StateType = initial; +    return (s?: StateType): StateType => { +      if (s !== undefined) { +        state = s; +        // In preact, this will always schedule a (debounced) redraw +        this.setState({} as any); +      } +      return state; +    }; +  } +} + + +function renderReserveCreationDetails(rci: ReserveCreationInfo) { +  let denoms = rci.selectedDenoms; + +  let countByPub: {[s: string]: number} = {}; +  let uniq: Denomination[] = []; + +  denoms.forEach((x: Denomination) => { +    let c = countByPub[x.denom_pub] || 0; +    if (c == 0) { +      uniq.push(x); +    } +    c += 1; +    countByPub[x.denom_pub] = c; +  }); + +  function row(denom: Denomination) { +    return ( +      <tr> +        <td>{countByPub[denom.denom_pub] + "x"}</td> +        <td>{amountToPretty(denom.value)}</td> +        <td>{amountToPretty(denom.fee_withdraw)}</td> +        <td>{amountToPretty(denom.fee_refresh)}</td> +        <td>{amountToPretty(denom.fee_deposit)}</td> +      </tr> +    ); +  } + +  let withdrawFeeStr = amountToPretty(rci.withdrawFee); +  let overheadStr = amountToPretty(rci.overhead); + +  return ( +    <div> +      <p>{`Withdrawal fees: ${withdrawFeeStr}`}</p> +      <p>{`Rounding loss: ${overheadStr}`}</p> +      <table> +        <thead> +        <th># Coins</th> +        <th>Value</th> +        <th>Withdraw Fee</th> +        <th>Refresh Fee</th> +        <th>Deposit fee</th> +        </thead> +        <tbody> +        {uniq.map(row)} +        </tbody> +      </table> +    </div> +  ); +} + + +function getSuggestedExchange(currency: string): Promise<string> { +  // TODO: make this request go to the wallet backend +  // Right now, this is a stub. +  const defaultExchange: {[s: string]: string} = { +    "KUDOS": "https://exchange.demo.taler.net", +    "PUDOS": "https://exchange.test.taler.net", +  }; + +  let exchange = defaultExchange[currency]; + +  if (!exchange) { +    exchange = "" +  } + +  return Promise.resolve(exchange); +} + +interface ExchangeSelectionProps {    suggestedExchangeUrl: string; -  complexViewRequested = false; -  urlOkay = false; - -  constructor(suggestedExchangeUrl: string, -              amount: AmountJson, -              callbackUrl: string, -              wt_types: string[]) { -    console.log("creating main controller"); -    this.suggestedExchangeUrl = suggestedExchangeUrl; -    this.amount = amount; -    this.callbackUrl = callbackUrl; -    this.wtTypes = wt_types; +  amount: AmountJson; +  callback_url: string; +  wt_types: string[]; +} + + +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; + +  constructor(props: ExchangeSelectionProps) { +    super(props);      this.timer = new DelayTimer(800, () => this.update()); -    this.url(suggestedExchangeUrl); +    this.url(props.suggestedExchangeUrl || null);      this.update();    } -  private update() { +  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> +    ); +    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> +    } +  } + + +  confirmReserve() { +    this.confirmReserveImpl(this.reserveCreationInfo()!, +                            this.url()!, +                            this.props.amount, +                            this.props.callback_url); +  } + + +  update() {      this.timer.stop();      const doUpdate = () => { -      this.reserveCreationInfo = undefined; +      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()); +      this.statusString(null); +      let parsedUrl = URI(this.url()!);        if (parsedUrl.is("relative")) {          this.statusString = i18n`Error: URL may not be relative`; -        m.redraw(true); +        this.forceUpdate();          return;        } -      m.redraw(true); +      this.forceUpdate();        console.log("doing get exchange info"); -      getReserveCreationInfo(this.url(), this.amount) +      getReserveCreationInfo(this.url()!, this.props.amount)          .then((r: ReserveCreationInfo) => {            console.log("get exchange info resolved");            this.isValidExchange = true; -          this.reserveCreationInfo = r; +          this.reserveCreationInfo(r);            console.dir(r); -          m.endComputation();          })          .catch((e) => {            console.log("get exchange info rejected");            if (e.hasOwnProperty("httpStatus")) { -            this.statusString = `Error: request failed with status ${e.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(`Error: ${resp.error} (${resp.hint})`);            } -          m.endComputation();          });      }; @@ -140,14 +297,14 @@ class Controller {    reset() {      this.isValidExchange = false; -    this.statusString = null; -    this.reserveCreationInfo = undefined; +    this.statusString(null); +    this.reserveCreationInfo(null);    } -  confirmReserve(rci: ReserveCreationInfo, -                 exchange: string, -                 amount: AmountJson, -                 callback_url: string) { +  confirmReserveImpl(rci: ReserveCreationInfo, +                     exchange: string, +                     amount: AmountJson, +                     callback_url: string) {      const d = {exchange, amount};      const cb = (rawResp: any) => {        if (!rawResp) { @@ -173,9 +330,9 @@ class Controller {          document.location.href = url.href();        } else {          this.reset(); -        this.statusString = ( -        `Oops, something went wrong.` + -        `The wallet responded with error status (${rawResp.error}).`); +        this.statusString( +          `Oops, something went wrong.` + +          `The wallet responded with error status (${rawResp.error}).`);        }      };      chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); @@ -186,188 +343,74 @@ class Controller {      this.url(url);      this.timer.bump();    } -} - -function view(ctrl: Controller): any { -  function* f(): IterableIterator<any> { -    yield m("p", -            i18n.parts`You are about to withdraw ${m("strong", amountToPretty( -              ctrl.amount))} from your bank account into your wallet.`); - -    if (ctrl.complexViewRequested || !ctrl.suggestedExchangeUrl) { -      yield viewComplex(ctrl); -      return; -    } -    yield viewSimple(ctrl); -  } -  return Array.from(f()); -} - -function viewSimple(ctrl: Controller) { -  function *f() { -    if (ctrl.statusString) { -      yield m("p", "Error: ", ctrl.statusString); -      yield m("button.linky", { -        onclick: () => { -          ctrl.complexViewRequested = true; -        } -      }, "advanced options"); -    } -    else if (ctrl.reserveCreationInfo != undefined) { -      let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead, -                                  ctrl.reserveCreationInfo.withdrawFee).amount; -      yield m("p", `Withdraw fees: ${amountToPretty(totalCost)}`); - -      yield m("button.accept", { -           onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo!, -                                              ctrl.url(), -                                              ctrl.amount, -                                              ctrl.callbackUrl), -           disabled: !ctrl.isValidExchange -         }, -         "Accept fees and withdraw"); -      yield m("span.spacer"); -      yield m("button.linky", { -        onclick: () => { -          ctrl.complexViewRequested = true; -        } -      }, "advanced options"); -    } else { -      yield m("p", "Please wait ..."); -    } -  } - -  return Array.from(f()); -} - -function viewComplex(ctrl: Controller) { -  function *f() { -    if (ctrl.reserveCreationInfo) { -      let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead, -                                  ctrl.reserveCreationInfo.withdrawFee).amount; -      yield m("p", `Withdraw fees: ${amountToPretty(totalCost)}`); -    } - -    yield m("button.accept", { -         onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo!, -                                            ctrl.url(), -                                            ctrl.amount, -                                            ctrl.callbackUrl), -         disabled: !ctrl.isValidExchange -       }, -       "Accept fees and withdraw"); -    yield m("span.spacer"); -    yield m("button.linky", { -      onclick: () => { -        ctrl.complexViewRequested = false; +  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>;        } -    }, "back to simple view"); -    yield m("br"); +      yield ( +        <button className="accept" disabled={!this.isValidExchange} +                onClick={() => this.confirmReserve()}> +          Accept fees and withdraw +        </button> +      ); +      yield <span className="spacer"/>; -    yield m("input", { -         className: "url", -         type: "text", -         spellcheck: false, -         value: ctrl.url(), -         oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), -       }); +      yield ( +        <button className="linky" +                onClick={() => this.complexViewRequested(true)}/> +      ); -    yield m("br"); +      yield <br/>; -    if (ctrl.statusString) { -      yield m("p", ctrl.statusString); -    } else if (!ctrl.reserveCreationInfo) { -      yield m("p", "Checking URL, please wait ..."); -    } - -    if (ctrl.reserveCreationInfo) { -      if (ctrl.detailCollapsed()) { -        yield m("button.linky", { -          onclick: () => { -            ctrl.detailCollapsed(false); -          } -        }, "show more details"); -      } else { -        yield m("button.linky", { -          onclick: () => { -            ctrl.detailCollapsed(true); -          } -        }, "hide details"); -        yield m("div", {}, renderReserveCreationDetails(ctrl.reserveCreationInfo)) -      } -    } -  } -  return Array.from(f()); -} +      yield ( +        <input className="url" type="text" spellCheck={false} +               value={this.url()!} +               onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)}/> +      ); +      yield <br/>; -function renderReserveCreationDetails(rci: ReserveCreationInfo) { -  let denoms = rci.selectedDenoms; - -  let countByPub: {[s: string]: number} = {}; -  let uniq: Denomination[] = []; +      if (this.statusString()) { +        yield <p>{this.statusString()}</p>; +      } else if (!this.reserveCreationInfo()) { +        yield <p>Checking URL, please wait ...</p>; +      } -  denoms.forEach((x: Denomination) => { -    let c = countByPub[x.denom_pub] || 0; -    if (c == 0) { -      uniq.push(x); +      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> +          ); +        } +      }      } -    c += 1; -    countByPub[x.denom_pub] = c; -  }); -  function row(denom: Denomination) { -    return m("tr", [ -      m("td", countByPub[denom.denom_pub] + "x"), -      m("td", amountToPretty(denom.value)), -      m("td", amountToPretty(denom.fee_withdraw)), -      m("td", amountToPretty(denom.fee_refresh)), -      m("td", amountToPretty(denom.fee_deposit)), -    ]); +    return Array.from(f.call(this));    } - -  let withdrawFeeStr = amountToPretty(rci.withdrawFee); -  let overheadStr = amountToPretty(rci.overhead); -  return [ -    m("p", `Withdrawal fees: ${withdrawFeeStr}`), -    m("p", `Rounding loss: ${overheadStr}`), -    m("table", [ -      m("tr", [ -        m("th", "Count"), -        m("th", "Value"), -        m("th", "Withdraw Fee"), -        m("th", "Refresh Fee"), -        m("th", "Deposit Fee"), -      ]), -      uniq.map(row) -    ]) -  ];  } - -function getSuggestedExchange(currency: string): Promise<string> { -  // TODO: make this request go to the wallet backend -  // Right now, this is a stub. -  const defaultExchange: {[s: string]: string} = { -    "KUDOS": "https://exchange.demo.taler.net", -    "PUDOS": "https://exchange.test.taler.net", -  }; - -  let exchange = defaultExchange[currency]; - -  if (!exchange) { -    exchange = "" -  } - -  return Promise.resolve(exchange); -} - - - -export function main() { +export async function main() {    const url = URI(document.location.href);    const query: any = URI.parseQuery(url.query());    const amount = AmountJson.checked(JSON.parse(query.amount)); @@ -375,16 +418,22 @@ export function main() {    const bank_url = query.bank_url;    const wt_types = JSON.parse(query.wt_types); -  getSuggestedExchange(amount.currency) -    .then((suggestedExchangeUrl) => { -      const controller = function () { return new Controller(suggestedExchangeUrl, amount, callback_url, wt_types); }; -      const ExchangeSelection = {controller, view}; -      m.mount(document.getElementById("exchange-selection")!, ExchangeSelection); -    }) -    .catch((e) => { -      // TODO: provide more context information, maybe factor it out into a -      // TODO:generic error reporting function or component. -      document.body.innerText = `Fatal error: "${e.message}".`; -      console.error(`got error "${e.message}"`, e); -    }); +  try { +    const suggestedExchangeUrl = await getSuggestedExchange(amount.currency); +    let args = { +      wt_types, +      suggestedExchangeUrl, +      callback_url, +      amount +    }; + +    preact.render(<ExchangeSelection {...args} />, document.getElementById( +      "exchange-selection")!); + +  } catch (e) { +    // TODO: provide more context information, maybe factor it out into a +    // TODO:generic error reporting function or component. +    document.body.innerText = `Fatal error: "${e.message}".`; +    console.error(`got error "${e.message}"`, e); +  }  }
\ No newline at end of file  | 
