diff options
Diffstat (limited to 'src/webex')
| -rw-r--r-- | src/webex/components.ts | 63 | ||||
| -rw-r--r-- | src/webex/messages.ts | 5 | ||||
| -rw-r--r-- | src/webex/pages/add-auditor.tsx | 119 | ||||
| -rw-r--r-- | src/webex/pages/help/empty-wallet.html | 30 | ||||
| -rw-r--r-- | src/webex/pages/payback.tsx | 87 | ||||
| -rw-r--r-- | src/webex/pages/popup.tsx | 255 | ||||
| -rw-r--r-- | src/webex/pages/tree.html | 27 | ||||
| -rw-r--r-- | src/webex/pages/tree.tsx | 402 | ||||
| -rw-r--r-- | src/webex/pages/welcome.html | 24 | ||||
| -rw-r--r-- | src/webex/pages/welcome.tsx | 113 | ||||
| -rw-r--r-- | src/webex/pages/withdraw.html | 2 | ||||
| -rw-r--r-- | src/webex/pages/withdraw.tsx | 10 | ||||
| -rw-r--r-- | src/webex/renderHtml.tsx | 82 | ||||
| -rw-r--r-- | src/webex/wxApi.ts | 8 | ||||
| -rw-r--r-- | src/webex/wxBackend.ts | 88 | 
15 files changed, 466 insertions, 849 deletions
diff --git a/src/webex/components.ts b/src/webex/components.ts deleted file mode 100644 index 1f5d18731..000000000 --- a/src/webex/components.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of TALER - (C) 2016 Inria - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE.  See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> - */ - - -/** - * General helper React components. - */ - - -/** - * Imports. - */ -import * as React from "react"; - -/** - * Wrapper around state that will cause updates to the - * containing component. - */ -export interface StateHolder<T> { -  (): T; -  (newState: T): void; -} - -/** - * Component that doesn't hold its state in one object, - * but has multiple state holders. - */ -export abstract class ImplicitStateComponent<PropType> extends React.Component<PropType, any> { -  private _implicit = {needsUpdate: false, didMount: false}; -  componentDidMount() { -    this._implicit.didMount = true; -    if (this._implicit.needsUpdate) { -      this.setState({} as any); -    } -  } -  makeState<StateType>(initial: StateType): StateHolder<StateType> { -    let state: StateType = initial; -    return (s?: StateType): StateType => { -      if (s !== undefined) { -        state = s; -        if (this._implicit.didMount) { -          this.setState({} as any); -        } else { -          this._implicit.needsUpdate = true; -        } -      } -      return state; -    }; -  } -} diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 7e99cfc77..27d85a1f3 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -205,6 +205,11 @@ export interface MessageMap {      request: { talerPayUri: string };      response: walletTypes.PreparePayResult;    }; + +  "get-diagnostics": { +    request: { }; +    response: walletTypes.WalletDiagnostics; +  };  } diff --git a/src/webex/pages/add-auditor.tsx b/src/webex/pages/add-auditor.tsx index 1ab6fdf9c..7e3e06322 100644 --- a/src/webex/pages/add-auditor.tsx +++ b/src/webex/pages/add-auditor.tsx @@ -20,20 +20,11 @@   * @author Florian Dold   */ - -import { -  CurrencyRecord, -} from "../../dbTypes"; - -import { ImplicitStateComponent, StateHolder } from "../components"; -import { -  getCurrencies, -  updateCurrency, -} from "../wxApi"; - -import * as React from "react"; -import * as ReactDOM from "react-dom"; +import { CurrencyRecord } from "../../dbTypes"; +import { getCurrencies, updateCurrency } from "../wxApi"; +import React, { useState } from "react";  import URI = require("urijs"); +import { registerMountPage } from "../renderHtml";  interface ConfirmAuditorProps {    url: string; @@ -42,36 +33,39 @@ interface ConfirmAuditorProps {    expirationStamp: number;  } -class ConfirmAuditor extends ImplicitStateComponent<ConfirmAuditorProps> { -  private addDone: StateHolder<boolean> = this.makeState(false); -  constructor(props: ConfirmAuditorProps) { -    super(props); -  } +function ConfirmAuditor(props: ConfirmAuditorProps) { +  const [addDone, setAddDone] = useState(false); + -  async add() { +  const add = async() => {      const currencies = await getCurrencies(); -    let currency: CurrencyRecord|undefined; +    let currency: CurrencyRecord | undefined;      for (const c of currencies) { -      if (c.name === this.props.currency) { +      if (c.name === props.currency) {          currency = c;        }      }      if (!currency) { -      currency = { name: this.props.currency, auditors: [], fractionalDigits: 2, exchanges: [] }; +      currency = { +        name: props.currency, +        auditors: [], +        fractionalDigits: 2, +        exchanges: [], +      };      }      const newAuditor = { -      auditorPub: this.props.auditorPub, -      baseUrl: this.props.url, -      expirationStamp: this.props.expirationStamp, +      auditorPub: props.auditorPub, +      baseUrl: props.url, +      expirationStamp: props.expirationStamp,      };      let auditorFound = false;      for (const idx in currency.auditors) {        const a = currency.auditors[idx]; -      if (a.baseUrl === this.props.url) { +      if (a.baseUrl === props.url) {          auditorFound = true;          // Update auditor if already found by URL.          currency.auditors[idx] = newAuditor; @@ -84,47 +78,54 @@ class ConfirmAuditor extends ImplicitStateComponent<ConfirmAuditorProps> {      await updateCurrency(currency); -    this.addDone(true); +    setAddDone(true);    } -  back() { +  const back = () => {      window.history.back(); -  } - -  render(): JSX.Element { -    return ( -      <div id="main"> -        <p>Do you want to let <strong>{this.props.auditorPub}</strong> audit the currency "{this.props.currency}"?</p> -        {this.addDone() ? -          ( -            <div> -              Auditor was added! You can also{" "} -              <a href={chrome.extension.getURL("/src/webex/pages/auditors.html")}>view and edit</a>{" "} -              auditors. -            </div> -          ) -          : -          ( -            <div> -              <button onClick={() => this.add()} className="pure-button pure-button-primary">Yes</button> -              <button onClick={() => this.back()} className="pure-button">No</button> -            </div> -          ) -        } -      </div> -    ); -  } +  }; + +  return ( +    <div id="main"> +      <p> +        Do you want to let <strong>{props.auditorPub}</strong> audit the +        currency "{props.currency}"? +      </p> +      {addDone ? ( +        <div> +          Auditor was added! You can also{" "} +          <a href={chrome.extension.getURL("/src/webex/pages/auditors.html")}> +            view and edit +          </a>{" "} +          auditors. +        </div> +      ) : ( +        <div> +          <button +            onClick={() => add()} +            className="pure-button pure-button-primary" +          > +            Yes +          </button> +          <button onClick={() => back()} className="pure-button"> +            No +          </button> +        </div> +      )} +    </div> +  );  } -function main() { + +registerMountPage(() => {    const walletPageUrl = new URI(document.location.href); -  const query: any = JSON.parse((URI.parseQuery(walletPageUrl.query()) as any).req); +  const query: any = JSON.parse( +    (URI.parseQuery(walletPageUrl.query()) as any).req, +  );    const url = query.url;    const currency: string = query.currency;    const auditorPub: string = query.auditorPub;    const expirationStamp = Number.parseInt(query.expirationStamp);    const args = { url, currency, auditorPub, expirationStamp }; -  ReactDOM.render(<ConfirmAuditor {...args} />, document.getElementById("container")!); -} - -document.addEventListener("DOMContentLoaded", main); +  return <ConfirmAuditor {...args}/>; +}); diff --git a/src/webex/pages/help/empty-wallet.html b/src/webex/pages/help/empty-wallet.html deleted file mode 100644 index dd29d9689..000000000 --- a/src/webex/pages/help/empty-wallet.html +++ /dev/null @@ -1,30 +0,0 @@ -<!DOCTYPE html> -<html> -  <head> -    <meta charset="utf-8"> -    <title>GNU Taler Help - Empty Wallet</title> -    <link rel="icon" href="/img/icon.png"> -    <meta name="description" content=""> -    <link rel="stylesheet" type="text/css" href="/src/style/wallet.css"> -  </head> -  <body> -    <div class="container" id="main"> -      <div class="row"> -        <div class="col-lg-12"> -          <h2 lang="en">Your wallet is empty!</h2> -          <p lang="en">You have succeeded with installing the Taler wallet.  However, before -             you can buy articles using the Taler wallet, you must withdraw electronic coins. -             This is typically done by visiting your bank's online banking Web site.  There, -             you instruct your bank to transfer the funds to a Taler exchange operator.  In -             return, your wallet will be allowed to withdraw electronic coins.</p> -          <p lang="en">At this stage, we are not aware of any regular exchange operators issuing -             coins in well-known currencies.  However, to see how Taler would work, you -             can visit our "fake" bank at -             <a href="https://bank.demo.taler.net/">bank.demo.taler.net</a> to -             withdraw coins in the "KUDOS" currency that we created just for -             demonstrating the system.</p> -        </div> -      </div> -    </div> -  </body> -</html> diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx index f69a33493..934c28c0a 100644 --- a/src/webex/pages/payback.tsx +++ b/src/webex/pages/payback.tsx @@ -20,73 +20,54 @@   * @author Florian Dold   */ -  /**   * Imports.   */ -import { -  ReserveRecord, -} from "../../dbTypes"; +import { ReserveRecord } from "../../dbTypes"; +import { renderAmount, registerMountPage } from "../renderHtml"; +import { getPaybackReserves, withdrawPaybackReserve } from "../wxApi"; +import * as React from "react"; +import { useState } from "react"; -import { ImplicitStateComponent, StateHolder } from "../components"; -import { renderAmount } from "../renderHtml"; -import { -  getPaybackReserves, -  withdrawPaybackReserve, -} from "../wxApi"; +function Payback() { +  const [reserves, setReserves] = useState<ReserveRecord[] | null>(null); -import * as React from "react"; -import * as ReactDOM from "react-dom"; +  useState(() => { +    const update = async () => { +      const r = await getPaybackReserves(); +      setReserves(r); +    }; -class Payback extends ImplicitStateComponent<{}> { -  private reserves: StateHolder<ReserveRecord[]|null> = this.makeState(null); -  constructor(props: {}) { -    super(props);      const port = chrome.runtime.connect();      port.onMessage.addListener((msg: any) => {        if (msg.notify) {          console.log("got notified"); -        this.update(); +        update();        }      }); -    this.update(); -  } +  }); -  async update() { -    const reserves = await getPaybackReserves(); -    this.reserves(reserves); +  if (!reserves) { +    return <span>loading ...</span>;    } - -  withdrawPayback(pub: string) { -    withdrawPaybackReserve(pub); +  if (reserves.length === 0) { +    return <span>No reserves with payback available.</span>;    } - -  render(): JSX.Element { -    const reserves = this.reserves(); -    if (!reserves) { -      return <span>loading ...</span>; -    } -    if (reserves.length === 0) { -      return <span>No reserves with payback available.</span>; -    } -    return ( -      <div> -        {reserves.map((r) => ( -          <div> -            <h2>Reserve for ${renderAmount(r.current_amount!)}</h2> -            <ul> -              <li>Exchange: ${r.exchange_base_url}</li> -            </ul> -            <button onClick={() => this.withdrawPayback(r.reserve_pub)}>Withdraw again</button> -          </div> -        ))} -      </div> -    ); -  } -} - -function main() { -  ReactDOM.render(<Payback />, document.getElementById("container")!); +  return ( +    <div> +      {reserves.map(r => ( +        <div> +          <h2>Reserve for ${renderAmount(r.current_amount!)}</h2> +          <ul> +            <li>Exchange: ${r.exchange_base_url}</li> +          </ul> +          <button onClick={() => withdrawPaybackReserve(r.reserve_pub)}> +            Withdraw again +          </button> +        </div> +      ))} +    </div> +  );  } -document.addEventListener("DOMContentLoaded", main); +registerMountPage(() => <Payback />); diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index 2cdfd8235..91ab515e4 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -14,7 +14,6 @@   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -  /**   * Popup shown to the user when they click   * the Taler browser action button. @@ -38,7 +37,7 @@ import {    WalletBalanceEntry,  } from "../../walletTypes"; -import { abbrev, renderAmount } from "../renderHtml"; +import { abbrev, renderAmount, PageLink } from "../renderHtml";  import * as wxApi from "../wxApi";  import * as React from "react"; @@ -47,7 +46,7 @@ import * as ReactDOM from "react-dom";  import URI = require("urijs");  function onUpdateNotification(f: () => void): () => void { -  const port = chrome.runtime.connect({name: "notifications"}); +  const port = chrome.runtime.connect({ name: "notifications" });    const listener = () => {      f();    }; @@ -57,7 +56,6 @@ function onUpdateNotification(f: () => void): () => void {    };  } -  class Router extends React.Component<any, any> {    static setRoute(s: string): void {      window.location.hash = s; @@ -92,13 +90,12 @@ class Router extends React.Component<any, any> {      console.log("router unmounted");    } -    render(): JSX.Element {      const route = window.location.hash.substring(1);      console.log("rendering route", route); -    let defaultChild: React.ReactChild|null = null; -    let foundChild: React.ReactChild|null = null; -    React.Children.forEach(this.props.children, (child) => { +    let defaultChild: React.ReactChild | null = null; +    let foundChild: React.ReactChild | null = null; +    React.Children.forEach(this.props.children, child => {        const childProps: any = (child as any).props;        if (!childProps) {          return; @@ -119,7 +116,6 @@ class Router extends React.Component<any, any> {    }  } -  interface TabProps {    target: string;    children?: React.ReactNode; @@ -141,7 +137,6 @@ function Tab(props: TabProps) {    );  } -  class WalletNavBar extends React.Component<any, any> {    private cancelSubscription: any; @@ -161,20 +156,14 @@ class WalletNavBar extends React.Component<any, any> {      console.log("rendering nav bar");      return (        <div className="nav" id="header"> -        <Tab target="/balance"> -          {i18n.str`Balance`} -        </Tab> -        <Tab target="/history"> -          {i18n.str`History`} -        </Tab> -        <Tab target="/debug"> -          {i18n.str`Debug`} -        </Tab> -      </div>); +        <Tab target="/balance">{i18n.str`Balance`}</Tab> +        <Tab target="/history">{i18n.str`History`}</Tab> +        <Tab target="/debug">{i18n.str`Debug`}</Tab> +      </div> +    );    }  } -  function ExtensionLink(props: any) {    const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {      chrome.tabs.create({ @@ -189,7 +178,6 @@ function ExtensionLink(props: any) {    );  } -  /**   * Render an amount as a large number with a small currency symbol.   */ @@ -197,10 +185,21 @@ function bigAmount(amount: AmountJson): JSX.Element {    const v = amount.value + amount.fraction / Amounts.fractionalBase;    return (      <span> -      <span style={{fontSize: "300%"}}>{v}</span> -      {" "} +      <span style={{ fontSize: "300%" }}>{v}</span>{" "}        <span>{amount.currency}</span> -      </span> +    </span> +  ); +} + +function EmptyBalanceView() { +  return ( +    <div> +      <i18n.Translate wrap="p"> +        You have no balance to show. Need some{" "} +        <PageLink pageName="welcome.html">help</PageLink> getting +        started? +      </i18n.Translate> +    </div>    );  } @@ -245,57 +244,44 @@ class WalletBalanceView extends React.Component<any, any> {      this.setState({});    } -  renderEmpty(): JSX.Element { -    const helpLink = ( -      <ExtensionLink target="/src/webex/pages/help/empty-wallet.html"> -        {i18n.str`help`} -      </ExtensionLink> -    ); -    return ( -      <div> -        <i18n.Translate wrap="p"> -        You have no balance to show. Need some -          {" "}<span>{helpLink}</span>{" "} -          getting started? -        </i18n.Translate> -      </div> -    ); -  } -    formatPending(entry: WalletBalanceEntry): JSX.Element {      let incoming: JSX.Element | undefined;      let payment: JSX.Element | undefined; -    console.log("available: ", entry.pendingIncoming ? renderAmount(entry.available) : null); -    console.log("incoming: ", entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null); +    console.log( +      "available: ", +      entry.pendingIncoming ? renderAmount(entry.available) : null, +    ); +    console.log( +      "incoming: ", +      entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null, +    );      if (Amounts.isNonZero(entry.pendingIncoming)) {        incoming = (          <i18n.Translate wrap="span"> -          <span style={{color: "darkgreen"}}> +          <span style={{ color: "darkgreen" }}>              {"+"}              {renderAmount(entry.pendingIncoming)} -          </span> -          {" "} +          </span>{" "}            incoming -      </i18n.Translate> +        </i18n.Translate>        );      }      if (Amounts.isNonZero(entry.pendingPayment)) {        payment = (          <i18n.Translate wrap="span"> -          <span style={{color: "red"}}> +          <span style={{ color: "red" }}>              {"-"}              {renderAmount(entry.pendingPayment)} -          </span> -          {" "} +          </span>{" "}            being spent          </i18n.Translate>        );      } -    const l = [incoming, payment].filter((x) => x !== undefined); +    const l = [incoming, payment].filter(x => x !== undefined);      if (l.length === 0) {        return <span />;      } @@ -303,49 +289,41 @@ class WalletBalanceView extends React.Component<any, any> {      if (l.length === 1) {        return <span>({l})</span>;      } -    return <span>({l[0]}, {l[1]})</span>; - +    return ( +      <span> +        ({l[0]}, {l[1]}) +      </span> +    );    }    render(): JSX.Element {      const wallet = this.balance;      if (this.gotError) { -      return i18n.str`Error: could not retrieve balance information.`; +      return ( +        <div> +          <p>{i18n.str`Error: could not retrieve balance information.`}</p> +          <p> +            Click <PageLink pageName="welcome.html">here</PageLink> for help and diagnostics. +          </p> +        </div> +      );      }      if (!wallet) {        return <span></span>;      }      console.log(wallet); -    let paybackAvailable = false; -    const listing = Object.keys(wallet.byCurrency).map((key) => { +    const listing = Object.keys(wallet.byCurrency).map(key => {        const entry: WalletBalanceEntry = wallet.byCurrency[key]; -      if (entry.paybackAmount.value !== 0 || entry.paybackAmount.fraction !== 0) { -        paybackAvailable = true; -      }        return (          <p> -          {bigAmount(entry.available)} -          {" "} -          {this.formatPending(entry)} +          {bigAmount(entry.available)} {this.formatPending(entry)}          </p>        );      }); -    const makeLink = (page: string, name: string) => { -      const url = chrome.extension.getURL(`/src/webex/pages/${page}`); -      return <div><a className="actionLink" href={url} target="_blank">{name}</a></div>; -    }; -    return ( -      <div> -        {listing.length > 0 ? listing : this.renderEmpty()} -        {paybackAvailable && makeLink("payback", i18n.str`Payback`)} -        {makeLink("return-coins.html#dissolve", i18n.str`Return Electronic Cash to Bank Account`)} -        {makeLink("auditors.html", i18n.str`Manage Trusted Auditors and Exchanges`)} -      </div> -    ); +    return <div>{listing.length > 0 ? listing : <EmptyBalanceView />}</div>;    }  } -  function formatHistoryItem(historyItem: HistoryRecord) {    const d = historyItem.detail;    console.log("hist item", historyItem); @@ -353,13 +331,12 @@ function formatHistoryItem(historyItem: HistoryRecord) {      case "create-reserve":        return (          <i18n.Translate wrap="p"> -          Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for -          {" "} +          Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for{" "}            <span>{renderAmount(d.requestedAmount)}</span>.          </i18n.Translate>        );      case "confirm-reserve": { -      const exchange = (new URI(d.exchangeBaseUrl)).host(); +      const exchange = new URI(d.exchangeBaseUrl).host();        const pub = abbrev(d.reservePub);        return (          <i18n.Translate wrap="p"> @@ -372,30 +349,37 @@ function formatHistoryItem(historyItem: HistoryRecord) {      case "offer-contract": {        return (          <i18n.Translate wrap="p"> -          Merchant <em>{abbrev(d.merchantName, 15)}</em> offered -          contract <span>{abbrev(d.contractTermsHash)}</span>. +          Merchant <em>{abbrev(d.merchantName, 15)}</em> offered contract{" "} +          <span>{abbrev(d.contractTermsHash)}</span>.          </i18n.Translate>        );      }      case "depleted-reserve": { -      const exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??"; +      const exchange = d.exchangeBaseUrl +        ? new URI(d.exchangeBaseUrl).host() +        : "??";        const amount = renderAmount(d.requestedAmount);        const pub = abbrev(d.reservePub);        return (          <i18n.Translate wrap="p"> -          Withdrew <span>{amount}</span> from <span>{exchange}</span> (<span>{pub}</span>). +          Withdrew <span>{amount}</span> from <span>{exchange}</span> ( +          <span>{pub}</span>).          </i18n.Translate>        );      }      case "pay": {        const url = d.fulfillmentUrl;        const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>; -      const fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>; +      const fulfillmentLinkElem = ( +        <a href={url} onClick={openTab(url)}> +          view product +        </a> +      );        return (          <i18n.Translate wrap="p"> -          Paid <span>{renderAmount(d.amount)}</span> to merchant <span>{merchantElem}</span>. -          <span> </span> -          (<span>{fulfillmentLinkElem}</span>) +          Paid <span>{renderAmount(d.amount)}</span> to merchant{" "} +          <span>{merchantElem}</span>.<span> </span>( +          <span>{fulfillmentLinkElem}</span>)          </i18n.Translate>        );      } @@ -403,12 +387,15 @@ function formatHistoryItem(historyItem: HistoryRecord) {        const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;        return (          <i18n.Translate wrap="p"> -          Merchant <span>{merchantElem}</span> gave a refund over <span>{renderAmount(d.refundAmount)}</span>. +          Merchant <span>{merchantElem}</span> gave a refund over{" "} +          <span>{renderAmount(d.refundAmount)}</span>.          </i18n.Translate>        );      }      case "tip": { -      const tipPageUrl = new URI(chrome.extension.getURL("/src/webex/pages/tip.html")); +      const tipPageUrl = new URI( +        chrome.extension.getURL("/src/webex/pages/tip.html"), +      );        const params = { tip_id: d.tipId, merchant_domain: d.merchantDomain };        const url = tipPageUrl.query(params).href();        const tipLink = <a href={url} onClick={openTab(url)}>{i18n.str`tip`}</a>; @@ -416,19 +403,23 @@ function formatHistoryItem(historyItem: HistoryRecord) {        return (          <>            <i18n.Translate wrap="p"> -            Merchant <span>{d.merchantDomain}</span> gave -            a <span>{tipLink}</span> of <span>{renderAmount(d.amount)}</span>. +            Merchant <span>{d.merchantDomain}</span> gave a{" "} +            <span>{tipLink}</span> of <span>{renderAmount(d.amount)}</span>.            </i18n.Translate> -          <span> { d.accepted ? null : <i18n.Translate>You did not accept the tip yet.</i18n.Translate> }</span> +          <span> +            {" "} +            {d.accepted ? null : ( +              <i18n.Translate>You did not accept the tip yet.</i18n.Translate> +            )} +          </span>          </>        );      }      default: -      return (<p>{i18n.str`Unknown event (${historyItem.type})`}</p>); +      return <p>{i18n.str`Unknown event (${historyItem.type})`}</p>;    }  } -  class WalletHistory extends React.Component<any, any> {    private myHistory: any[];    private gotError = false; @@ -445,7 +436,7 @@ class WalletHistory extends React.Component<any, any> {    }    update() { -    chrome.runtime.sendMessage({type: "get-history"}, (resp) => { +    chrome.runtime.sendMessage({ type: "get-history" }, resp => {        if (this.unmounted) {          return;        } @@ -480,7 +471,7 @@ class WalletHistory extends React.Component<any, any> {        const item = (          <div className="historyItem">            <div className="historyDate"> -            {(new Date(record.timestamp)).toString()} +            {new Date(record.timestamp).toString()}            </div>            {formatHistoryItem(record)}          </div> @@ -494,10 +485,8 @@ class WalletHistory extends React.Component<any, any> {      }      return <p>{i18n.str`Your wallet has no events recorded.`}</p>;    } -  } -  function reload() {    try {      chrome.runtime.reload(); @@ -508,43 +497,43 @@ function reload() {  }  function confirmReset() { -  if (confirm("Do you want to IRREVOCABLY DESTROY everything inside your" + -              " wallet and LOSE ALL YOUR COINS?")) { +  if ( +    confirm( +      "Do you want to IRREVOCABLY DESTROY everything inside your" + +        " wallet and LOSE ALL YOUR COINS?", +    ) +  ) {      wxApi.resetDb();      window.close();    }  } -  function WalletDebug(props: any) { -  return (<div> -    <p>Debug tools:</p> -    <button onClick={openExtensionPage("/src/webex/pages/popup.html")}> -      wallet tab -    </button> -    <button onClick={openExtensionPage("/src/webex/pages/benchmark.html")}> -      benchmark -    </button> -    <button onClick={openExtensionPage("/src/webex/pages/show-db.html")}> -      show db -    </button> -    <button onClick={openExtensionPage("/src/webex/pages/tree.html")}> -      show tree -    </button> -    <button onClick={openExtensionPage("/src/webex/pages/logs.html")}> -      show logs -    </button> -    <br /> -    <button onClick={confirmReset}> -      reset -    </button> -    <button onClick={reload}> -      reload chrome extension -    </button> -  </div>); +  return ( +    <div> +      <p>Debug tools:</p> +      <button onClick={openExtensionPage("/src/webex/pages/popup.html")}> +        wallet tab +      </button> +      <button onClick={openExtensionPage("/src/webex/pages/benchmark.html")}> +        benchmark +      </button> +      <button onClick={openExtensionPage("/src/webex/pages/show-db.html")}> +        show db +      </button> +      <button onClick={openExtensionPage("/src/webex/pages/tree.html")}> +        show tree +      </button> +      <button onClick={openExtensionPage("/src/webex/pages/logs.html")}> +        show logs +      </button> +      <br /> +      <button onClick={confirmReset}>reset</button> +      <button onClick={reload}>reload chrome extension</button> +    </div> +  );  } -  function openExtensionPage(page: string) {    return () => {      chrome.tabs.create({ @@ -553,7 +542,6 @@ function openExtensionPage(page: string) {    };  } -  function openTab(page: string) {    return (evt: React.SyntheticEvent<any>) => {      evt.preventDefault(); @@ -563,15 +551,14 @@ function openTab(page: string) {    };  } -  const el = (    <div>      <WalletNavBar /> -    <div style={{margin: "1em"}}> +    <div style={{ margin: "1em" }}>        <Router> -        <WalletBalanceView route="/balance" default/> -        <WalletHistory route="/history"/> -        <WalletDebug route="/debug"/> +        <WalletBalanceView route="/balance" default /> +        <WalletHistory route="/history" /> +        <WalletDebug route="/debug" />        </Router>      </div>    </div> @@ -581,5 +568,5 @@ runOnceWhenReady(() => {    ReactDOM.render(el, document.getElementById("content")!);    // Will be used by the backend to detect when the popup gets closed,    // so we can clear notifications -  chrome.runtime.connect({name: "popup"}); +  chrome.runtime.connect({ name: "popup" });  }); diff --git a/src/webex/pages/tree.html b/src/webex/pages/tree.html deleted file mode 100644 index 0c0a368b3..000000000 --- a/src/webex/pages/tree.html +++ /dev/null @@ -1,27 +0,0 @@ -<!DOCTYPE html> -<html> - -<head> -  <meta charset="UTF-8"> -  <title>Taler Wallet: Tree View</title> - -  <link rel="stylesheet" type="text/css" href="../style/wallet.css"> - -  <link rel="icon" href="/img/icon.png"> - -  <script src="/dist/page-common-bundle.js"></script> -  <script src="/dist/tree-bundle.js"></script> - -  <style> -          .tree-item { -                  margin: 2em; -                  border-radius: 5px; -                  border: 1px solid gray; -                  padding: 1em; -          } -  </style> - -  <body> -    <div id="container"></div> -  </body> -</html> diff --git a/src/webex/pages/tree.tsx b/src/webex/pages/tree.tsx deleted file mode 100644 index 67e58a1df..000000000 --- a/src/webex/pages/tree.tsx +++ /dev/null @@ -1,402 +0,0 @@ -/* - This file is part of TALER - (C) 2016 Inria - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE.  See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Show contents of the wallet as a tree. - * - * @author Florian Dold - */ - - -import { getTalerStampDate } from "../../helpers"; - -import { -  CoinRecord, -  CoinStatus, -  DenominationRecord, -  ExchangeRecord, -  PreCoinRecord, -  ReserveRecord, -} from "../../dbTypes"; - -import { ImplicitStateComponent, StateHolder } from "../components"; -import { -  getCoins, -  getDenoms, -  getExchanges, -  getPreCoins, -  getReserves, -  payback, -  refresh, -} from "../wxApi"; - -import { ExpanderText, renderAmount } from "../renderHtml"; - -import * as React from "react"; -import * as ReactDOM from "react-dom"; - -interface ReserveViewProps { -  reserve: ReserveRecord; -} - -class ReserveView extends React.Component<ReserveViewProps, {}> { -  render(): JSX.Element { -    const r: ReserveRecord = this.props.reserve; -    return ( -      <div className="tree-item"> -        <ul> -          <li>Key: {r.reserve_pub}</li> -          <li>Created: {(new Date(r.created * 1000).toString())}</li> -          <li>Current: {r.current_amount ? renderAmount(r.current_amount!) : "null"}</li> -          <li>Requested: {renderAmount(r.requested_amount)}</li> -          <li>Confirmed: {r.timestamp_confirmed}</li> -        </ul> -      </div> -    ); -  } -} - -interface ReserveListProps { -  exchangeBaseUrl: string; -} - -interface ToggleProps { -  expanded: StateHolder<boolean>; -} - -class Toggle extends ImplicitStateComponent<ToggleProps> { -  renderButton() { -    const show = () => { -      this.props.expanded(true); -      this.setState({}); -    }; -    const hide = () => { -      this.props.expanded(false); -      this.setState({}); -    }; -    if (this.props.expanded()) { -      return <button onClick={hide}>hide</button>; -    } -    return <button onClick={show}>show</button>; - -  } -  render() { -    return ( -      <div style={{display: "inline"}}> -        {this.renderButton()} -        {this.props.expanded() ? this.props.children : []} -      </div>); -  } -} - - -interface CoinViewProps { -  coin: CoinRecord; -} - -interface RefreshDialogProps { -  coin: CoinRecord; -} - -class RefreshDialog extends ImplicitStateComponent<RefreshDialogProps> { -  private refreshRequested = this.makeState<boolean>(false); -  render(): JSX.Element { -    if (!this.refreshRequested()) { -      return ( -        <div style={{display: "inline"}}> -          <button onClick={() => this.refreshRequested(true)}>refresh</button> -        </div> -      ); -    } -    return ( -      <div> -        Refresh amount: <input type="text" size={10} /> -        <button onClick={() => refresh(this.props.coin.coinPub)}>ok</button> -        <button onClick={() => this.refreshRequested(false)}>cancel</button> -      </div> -      ); -  } -} - -class CoinView extends React.Component<CoinViewProps, {}> { -  render() { -    const c = this.props.coin; -    return ( -      <div className="tree-item"> -        <ul> -          <li>Key: {c.coinPub}</li> -          <li>Current amount: {renderAmount(c.currentAmount)}</li> -          <li>Denomination: <ExpanderText text={c.denomPub} /></li> -          <li>Suspended: {(c.suspended || false).toString()}</li> -          <li>Status: {CoinStatus[c.status]}</li> -          <li><RefreshDialog coin={c} /></li> -          <li><button onClick={() => payback(c.coinPub)}>Payback</button></li> -        </ul> -      </div> -    ); -  } -} - - -interface PreCoinViewProps { -  precoin: PreCoinRecord; -} - -class PreCoinView extends React.Component<PreCoinViewProps, {}> { -  render() { -    const c = this.props.precoin; -    return ( -      <div className="tree-item"> -        <ul> -          <li>Key: {c.coinPub}</li> -        </ul> -      </div> -    ); -  } -} - -interface CoinListProps { -  exchangeBaseUrl: string; -} - -class CoinList extends ImplicitStateComponent<CoinListProps> { -  private coins = this.makeState<CoinRecord[] | null>(null); -  private expanded = this.makeState<boolean>(false); - -  constructor(props: CoinListProps) { -    super(props); -    this.update(props); -  } - -  async update(props: CoinListProps) { -    const coins = await getCoins(props.exchangeBaseUrl); -    this.coins(coins); -  } - -  componentWillReceiveProps(newProps: CoinListProps) { -    this.update(newProps); -  } - -  render(): JSX.Element { -    if (!this.coins()) { -      return <div>...</div>; -    } -    return ( -      <div className="tree-item"> -        Coins ({this.coins() !.length.toString()}) -        {" "} -        <Toggle expanded={this.expanded}> -          {this.coins() !.map((c) => <CoinView coin={c} />)} -        </Toggle> -      </div> -    ); -  } -} - - -interface PreCoinListProps { -  exchangeBaseUrl: string; -} - -class PreCoinList extends ImplicitStateComponent<PreCoinListProps> { -  private precoins = this.makeState<PreCoinRecord[] | null>(null); -  private expanded = this.makeState<boolean>(false); - -  constructor(props: PreCoinListProps) { -    super(props); -    this.update(); -  } - -  async update() { -    const precoins = await getPreCoins(this.props.exchangeBaseUrl); -    this.precoins(precoins); -  } - -  render(): JSX.Element { -    if (!this.precoins()) { -      return <div>...</div>; -    } -    return ( -      <div className="tree-item"> -        Planchets ({this.precoins() !.length.toString()}) -        {" "} -        <Toggle expanded={this.expanded}> -          {this.precoins() !.map((c) => <PreCoinView precoin={c} />)} -        </Toggle> -      </div> -    ); -  } -} - -interface DenominationListProps { -  exchange: ExchangeRecord; -} - -class DenominationList extends ImplicitStateComponent<DenominationListProps> { -  private expanded = this.makeState<boolean>(false); -  private denoms = this.makeState<undefined|DenominationRecord[]>(undefined); - -  constructor(props: DenominationListProps) { -    super(props); -    this.update(); -  } - -  async update() { -    const d = await getDenoms(this.props.exchange.baseUrl); -    this.denoms(d); -  } - -  renderDenom(d: DenominationRecord) { -    return ( -      <div className="tree-item"> -        <ul> -          <li>Offered: {d.isOffered ? "yes" : "no"}</li> -          <li>Value: {renderAmount(d.value)}</li> -          <li>Withdraw fee: {renderAmount(d.feeWithdraw)}</li> -          <li>Refresh fee: {renderAmount(d.feeRefresh)}</li> -          <li>Deposit fee: {renderAmount(d.feeDeposit)}</li> -          <li>Refund fee: {renderAmount(d.feeRefund)}</li> -          <li>Start: {getTalerStampDate(d.stampStart)!.toString()}</li> -          <li>Withdraw expiration: {getTalerStampDate(d.stampExpireWithdraw)!.toString()}</li> -          <li>Legal expiration: {getTalerStampDate(d.stampExpireLegal)!.toString()}</li> -          <li>Deposit expiration: {getTalerStampDate(d.stampExpireDeposit)!.toString()}</li> -          <li>Denom pub: <ExpanderText text={d.denomPub} /></li> -        </ul> -      </div> -    ); -  } - -  render(): JSX.Element { -    const denoms = this.denoms(); -    if (!denoms) { -      return ( -        <div className="tree-item"> -        Denominations (...) -        {" "} -        <Toggle expanded={this.expanded}> -          ... -        </Toggle> -      </div> -      ); -    } -    return ( -      <div className="tree-item"> -        Denominations ({denoms.length.toString()}) -        {" "} -        <Toggle expanded={this.expanded}> -          {denoms.map((d) => this.renderDenom(d))} -        </Toggle> -      </div> -    ); -  } -} - - -class ReserveList extends ImplicitStateComponent<ReserveListProps> { -  private reserves = this.makeState<ReserveRecord[] | null>(null); -  private expanded = this.makeState<boolean>(false); - -  constructor(props: ReserveListProps) { -    super(props); -    this.update(); -  } - -  async update() { -    const reserves = await getReserves(this.props.exchangeBaseUrl); -    this.reserves(reserves); -  } - -  render(): JSX.Element { -    if (!this.reserves()) { -      return <div>...</div>; -    } -    return ( -      <div className="tree-item"> -        Reserves ({this.reserves() !.length.toString()}) -        {" "} -        <Toggle expanded={this.expanded}> -          {this.reserves() !.map((r) => <ReserveView reserve={r} />)} -        </Toggle> -      </div> -    ); -  } -} - -interface ExchangeProps { -  exchange: ExchangeRecord; -} - -class ExchangeView extends React.Component<ExchangeProps, {}> { -  render(): JSX.Element { -    const e = this.props.exchange; -    return ( -      <div className="tree-item"> -        <ul> -          <li>Exchange Base Url: {this.props.exchange.baseUrl}</li> -          <li>Master public key: <ExpanderText text={this.props.exchange.masterPublicKey} /></li> -        </ul> -        <DenominationList exchange={e} /> -        <ReserveList exchangeBaseUrl={this.props.exchange.baseUrl} /> -        <CoinList exchangeBaseUrl={this.props.exchange.baseUrl} /> -        <PreCoinList exchangeBaseUrl={this.props.exchange.baseUrl} /> -      </div> -    ); -  } -} - -interface ExchangesListState { -  exchanges?: ExchangeRecord[]; -} - -class ExchangesList extends React.Component<{}, ExchangesListState> { -  constructor(props: {}) { -    super(props); -    const port = chrome.runtime.connect(); -    port.onMessage.addListener((msg: any) => { -      if (msg.notify) { -        console.log("got notified"); -        this.update(); -      } -    }); -    this.update(); -    this.state = {} as any; -  } - -  async update() { -    const exchanges = await getExchanges(); -    console.log("exchanges: ", exchanges); -    this.setState({ exchanges }); -  } - -  render(): JSX.Element { -    const exchanges = this.state.exchanges; -    if (!exchanges) { -      return <span>...</span>; -    } -    return ( -      <div className="tree-item"> -        Exchanges ({exchanges.length.toString()}): -        {exchanges.map((e) => <ExchangeView exchange={e} />)} -      </div> -    ); -  } -} - -function main() { -  ReactDOM.render(<ExchangesList />, document.getElementById("container")!); -} - -document.addEventListener("DOMContentLoaded", main); diff --git a/src/webex/pages/welcome.html b/src/webex/pages/welcome.html new file mode 100644 index 000000000..9a96d04a7 --- /dev/null +++ b/src/webex/pages/welcome.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> + +<head> +  <meta charset="UTF-8"> +  <title>Taler Wallet: Withdraw</title> + +  <link rel="icon" href="/img/icon.png"> +  <link rel="stylesheet" type="text/css" href="../style/pure.css"> +  <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + +  <script src="/dist/page-common-bundle.js"></script> +  <script src="/dist/welcome-bundle.js"></script> + +</head> + +<body> +  <section id="main"> +    <h1>GNU Taler Wallet Installed!</h1> +    <div id="container">Loading...</div> +  </section> +</body> + +</html> diff --git a/src/webex/pages/welcome.tsx b/src/webex/pages/welcome.tsx new file mode 100644 index 000000000..1026e6e6e --- /dev/null +++ b/src/webex/pages/welcome.tsx @@ -0,0 +1,113 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Welcome page, shown on first installs. + * + * @author Florian Dold + */ + +import React, { useState, useEffect } from "react"; +import { getDiagnostics } from "../wxApi"; +import { registerMountPage, PageLink } from "../renderHtml"; +import { WalletDiagnostics } from "../../walletTypes"; + +function Diagnostics() { +  const [timedOut, setTimedOut] = useState(false); +  const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>( +    undefined, +  ); + +  useEffect(() => { +    let gotDiagnostics = false; +    setTimeout(() => { +      if (!gotDiagnostics) { +        console.error("timed out"); +        setTimedOut(true); +      } +    }, 1000); +    const doFetch = async () => { +      const d = await getDiagnostics(); +      console.log("got diagnostics", d); +      gotDiagnostics = true; +      setDiagnostics(d); +    }; +    console.log("fetching diagnostics"); +    doFetch(); +  }, []); + +  if (timedOut) { +    return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>; +  } + +  if (diagnostics) { +    if (diagnostics.errors.length === 0) { +      return <p>Running diagnostics ... everything looks fine.</p>; +    } else { +      return ( +        <div +          style={{ +            borderLeft: "0.5em solid red", +            paddingLeft: "1em", +            paddingTop: "0.2em", +            paddingBottom: "0.2em", +          }} +        > +          <p>Problems detected:</p> +          <ol> +            {diagnostics.errors.map(errMsg => ( +              <li>{errMsg}</li> +            ))} +          </ol> +          {diagnostics.firefoxIdbProblem ? ( +            <p> +              Please check in your <code>about:config</code> settings that you +              have IndexedDB enabled (check the preference name{" "} +              <code>dom.indexedDB.enabled</code>). +            </p> +          ) : null} +          {diagnostics.dbOutdated ? ( +            <p> +              Your wallet database is outdated. Currently automatic migration is +              not supported. Please go{" "} +              <PageLink pageName="reset-required.html">here</PageLink> to reset +              the wallet database. +            </p> +          ) : null} +        </div> +      ); +    } +  } + +  return <p>Running diagnostics ...</p>; +} + +function Welcome() { +  return ( +    <> +      <p>Thank you for installing the wallet.</p> +      <h2>First Steps</h2> +      <p> +        Check out <a href="https://demo.taler.net/">demo.taler.net</a> for a +        demo. +      </p> +      <h2>Troubleshooting</h2> +      <Diagnostics /> +    </> +  ); +} + +registerMountPage(() => <Welcome />); diff --git a/src/webex/pages/withdraw.html b/src/webex/pages/withdraw.html index 8b1e59b1d..e5c527275 100644 --- a/src/webex/pages/withdraw.html +++ b/src/webex/pages/withdraw.html @@ -3,7 +3,7 @@  <head>    <meta charset="UTF-8"> -  <title>Taler Wallet: Select Taler Provider</title> +  <title>Taler Wallet: Withdraw</title>    <link rel="icon" href="/img/icon.png">    <link rel="stylesheet" type="text/css" href="../style/pure.css"> diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx index 66617373b..39b27f2d8 100644 --- a/src/webex/pages/withdraw.tsx +++ b/src/webex/pages/withdraw.tsx @@ -21,21 +21,13 @@   * @author Florian Dold   */ -import { canonicalizeBaseUrl } from "../../helpers"; -import * as i18n from "../../i18n"; -import { AmountJson } from "../../amounts"; -import * as Amounts from "../../amounts"; +import * as i18n from "../../i18n"; -import { CurrencyRecord } from "../../dbTypes";  import { -  CreateReserveResponse, -  ReserveCreationInfo,    WithdrawDetails,  } from "../../walletTypes"; -import { ImplicitStateComponent, StateHolder } from "../components"; -  import { WithdrawDetailView, renderAmount } from "../renderHtml";  import React, { useState, useEffect } from "react"; diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx index 1c50aa1ad..0f736d1b6 100644 --- a/src/webex/renderHtml.tsx +++ b/src/webex/renderHtml.tsx @@ -26,22 +26,16 @@   */  import { AmountJson } from "../amounts";  import * as Amounts from "../amounts"; -  import {    DenominationRecord,  } from "../dbTypes";  import {    ReserveCreationInfo,  } from "../walletTypes"; - - -import { ImplicitStateComponent } from "./components"; -  import * as moment from "moment"; -  import * as i18n from "../i18n"; - -import * as React from "react"; +import React from "react"; +import ReactDOM from "react-dom";  /** @@ -274,49 +268,16 @@ interface ExpanderTextProps {    text: string;  } +  /**   * Show a heading with a toggle to show/hide the expandable content.   */ -export class ExpanderText extends ImplicitStateComponent<ExpanderTextProps> { -  private expanded = this.makeState<boolean>(false); -  private textArea: any = undefined; - -  componentDidUpdate() { -    if (this.expanded() && this.textArea) { -      this.textArea.focus(); -      this.textArea.scrollTop = 0; -    } -  } - -  render(): JSX.Element { -    if (!this.expanded()) { -      return ( -        <span onClick={() => { this.expanded(true); }}> -          {(this.props.text.length <= 10) -            ?  this.props.text -            : ( -                <span> -                  {this.props.text.substring(0, 10)} -                  <span style={{textDecoration: "underline"}}>...</span> -                </span> -              ) -          } -        </span> -      ); -    } -    return ( -      <textarea -        readOnly -        style={{display: "block"}} -        onBlur={() => this.expanded(false)} -        ref={(e) => this.textArea = e}> -        {this.props.text} -      </textarea> -    ); -  } +export function ExpanderText({ text }: ExpanderTextProps) { +  return <span>{text}</span>;  } +  export interface LoadingButtonProps {    loading: boolean;  } @@ -340,4 +301,35 @@ export function ProgressButton(        {props.children}      </button>    ); +} + +export function registerMountPage(mainFn: () => React.ReactElement) { +  async function main() { +    try { +    const mainElement = mainFn(); +    const container = document.getElementById("container"); +    if (!container) { +      throw Error("container not found, can't mount page contents"); +    } +      ReactDOM.render( +        mainElement, +        container, +      ); +    } catch (e) { +      document.body.innerText = `Fatal error: "${e.message}".  Please report this bug at https://bugs.gnunet.org/.`; +      console.error("got error", e); +    } +  } + +  if (document.readyState === "loading") { +    document.addEventListener("DOMContentLoaded", main); +    return; +  } else { +    main(); +  } +} + +export function PageLink(props: React.PropsWithChildren<{pageName: string}>) { +  const url = chrome.extension.getURL(`/src/webex/pages/${props.pageName}`); +  return <a className="actionLink" href={url} target="_blank">{props.children}</a>;  }
\ No newline at end of file diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 7e4d17e37..61dc2ca69 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -42,6 +42,7 @@ import {    TipStatus,    WalletBalance,    PurchaseDetails, +  WalletDiagnostics,  } from "../walletTypes";  import { @@ -396,3 +397,10 @@ export function preparePay(talerPayUri: string) {  export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) {    return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange });  } + +/** + * Get diagnostics information + */ +export function getDiagnostics(): Promise<WalletDiagnostics> { +  return callBackend("get-diagnostics", {}); +} diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index ea43f65c2..0cfaf2346 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -25,40 +25,34 @@   */  import { BrowserHttpLib } from "../http";  import * as logging from "../logging"; -  import { AmountJson } from "../amounts"; -  import {    ConfirmReserveRequest,    CreateReserveRequest,    Notifier,    ReturnCoinsRequest, +  WalletDiagnostics,  } from "../walletTypes"; -  import { Wallet } from "../wallet"; -  import { isFirefox } from "./compat"; - -import { PurchaseRecord, WALLET_DB_VERSION } from "../dbTypes"; - +import { WALLET_DB_VERSION } from "../dbTypes";  import { openTalerDb, exportDb, importDb, deleteDb } from "../db"; -  import { ChromeBadge } from "./chromeBadge";  import { MessageType } from "./messages";  import * as wxApi from "./wxApi"; -  import URI = require("urijs");  import Port = chrome.runtime.Port;  import MessageSender = chrome.runtime.MessageSender;  import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi"; +import { OpenedPromise, openPromise } from "../promiseUtils";  const NeedsWallet = Symbol("NeedsWallet"); -function handleMessage( +async function handleMessage(    sender: MessageSender,    type: MessageType,    detail: any, -): any { +): Promise<any> {    function assertNotFound(t: never): never {      console.error(`Request type ${t as string} unknown`);      console.error(`Request detail was ${detail}`); @@ -251,7 +245,7 @@ function handleMessage(        const resp: wxApi.UpgradeResponse = {          currentDbVersion: WALLET_DB_VERSION.toString(),          dbResetRequired, -        oldDbVersion: (oldDbVersion || "unknown").toString(), +        oldDbVersion: (outdatedDbVersion || "unknown").toString(),        };        return resp;      } @@ -314,6 +308,39 @@ function handleMessage(          detail.selectedExchange,        );      } +    case "get-diagnostics": { +      const manifestData = chrome.runtime.getManifest(); +      const errors: string[] = []; +      let firefoxIdbProblem = false; +      let dbOutdated = false; +      try { +        await walletInit.promise; +      } catch (e) { +        errors.push("Error during wallet initialization: " + e); +        if (currentDatabase === undefined && outdatedDbVersion === undefined && isFirefox()) { +          firefoxIdbProblem = true; +        } +      } +      if (!currentWallet) { +        errors.push("Could not create wallet backend."); +      } +      if (!currentDatabase) { +        errors.push("Could not open database"); +      } +      if (outdatedDbVersion !== undefined) { +        errors.push(`Outdated DB version: ${outdatedDbVersion}`); +        dbOutdated = true; +      } +      const diagnostics: WalletDiagnostics = { +        walletManifestDisplayVersion: +          manifestData.version_name || "(undefined)", +        walletManifestVersion: manifestData.version, +        errors, +        firefoxIdbProblem, +        dbOutdated, +      }; +      return diagnostics; +    }      case "prepare-pay":        return needsWallet().preparePay(detail.talerPayUri);      default: @@ -351,7 +378,7 @@ async function dispatch(          error: {            message: e.message,            stack, -        } +        },        });      } catch (e) {        console.log(e); @@ -441,26 +468,24 @@ function makeSyncWalletRedirect(    return { redirectUrl: outerUrl.href() };  } -// Rate limit cache for executePayment operations, to break redirect loops -let rateLimitCache: { [n: number]: number } = {}; - -function clearRateLimitCache() { -  rateLimitCache = {}; -} -  /**   * Currently active wallet instance.  Might be unloaded and   * re-instantiated when the database is reset.   */  let currentWallet: Wallet | undefined; +let currentDatabase: IDBDatabase | undefined; +  /**   * Last version if an outdated DB, if applicable.   */ -let oldDbVersion: number | undefined; +let outdatedDbVersion: number | undefined; + +let walletInit: OpenedPromise<void> = openPromise<void>();  function handleUpgradeUnsupported(oldDbVersion: number, newDbVersion: number) {    console.log("DB migration not supported"); +  outdatedDbVersion = oldDbVersion;    chrome.tabs.create({      url: chrome.extension.getURL("/src/webex/pages/reset-required.html"),    }); @@ -473,20 +498,25 @@ async function reinitWallet() {      currentWallet.stop();      currentWallet = undefined;    } +  currentDatabase = undefined;    setBadgeText({ text: "" });    const badge = new ChromeBadge(); -  let db: IDBDatabase;    try { -    db = await openTalerDb(indexedDB, reinitWallet, handleUpgradeUnsupported); +    currentDatabase = await openTalerDb( +      indexedDB, +      reinitWallet, +      handleUpgradeUnsupported, +    );    } catch (e) {      console.error("could not open database", e); +    walletInit.reject(e);      return;    }    const http = new BrowserHttpLib();    const notifier = new ChromeNotifier();    console.log("setting wallet");    const wallet = new Wallet( -    db, +    currentDatabase,      http,      badge,      notifier, @@ -495,6 +525,7 @@ async function reinitWallet() {    // Useful for debugging in the background page.    (window as any).talerWallet = wallet;    currentWallet = wallet; +  walletInit.resolve();  }  /** @@ -528,6 +559,13 @@ function injectScript(   * Sets up all event handlers and other machinery.   */  export async function wxMain() { +  chrome.runtime.onInstalled.addListener(details => { +    if (details.reason === "install") { +      const url = chrome.extension.getURL("/src/webex/pages/welcome.html"); +      chrome.tabs.create({ active: true, url: url }); +    } +  }); +    // Explicitly unload the extension page as soon as an update is available,    // so the update gets installed as soon as possible.    chrome.runtime.onUpdateAvailable.addListener(details => { @@ -630,8 +668,6 @@ export async function wxMain() {      tabTimers[tabId] = timers;    }); -  chrome.extension.getBackgroundPage()!.setInterval(clearRateLimitCache, 5000); -    reinitWallet();    // Handlers for messages coming directly from the content  | 
