aboutsummaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-11-13 23:30:18 +0100
committerFlorian Dold <florian.dold@gmail.com>2016-11-13 23:31:17 +0100
commitf3fb8be7db6de87dae40d41bd5597a735c800ca1 (patch)
tree1a061db04de8f5bb5a6b697fa56a9948f67fac2f /src/pages
parent200d83c3886149ebb3f018530302079e12a81f6b (diff)
restructuring
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/confirm-contract.html75
-rw-r--r--src/pages/confirm-contract.tsx231
-rw-r--r--src/pages/confirm-create-reserve.html93
-rw-r--r--src/pages/confirm-create-reserve.tsx397
-rw-r--r--src/pages/debug.html13
-rw-r--r--src/pages/help/empty-wallet.html30
-rw-r--r--src/pages/show-db.html15
-rw-r--r--src/pages/show-db.ts57
-rw-r--r--src/pages/tree.html36
-rw-r--r--src/pages/tree.tsx400
10 files changed, 1347 insertions, 0 deletions
diff --git a/src/pages/confirm-contract.html b/src/pages/confirm-contract.html
new file mode 100644
index 000000000..54a4d618d
--- /dev/null
+++ b/src/pages/confirm-contract.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Taler Wallet: Confirm Reserve Creation</title>
+
+ <link rel="stylesheet" type="text/css" href="/src/style/lang.css">
+ <link rel="stylesheet" type="text/css" href="/src/style/wallet.css">
+
+ <link rel="icon" href="/img/icon.png">
+
+ <script src="/src/vendor/URI.js"></script>
+ <script src="/src/vendor/react.js"></script>
+ <script src="/src/vendor/react-dom.js"></script>
+ <script src="/src/vendor/system-csp-production.src.js"></script>
+ <!-- <script src="/src/vendor/jed.js"></script> -->
+ <script src="/src/i18n.js"></script>
+ <script src="/src/i18n/strings.js"></script>
+ <script src="/src/module-trampoline.js"></script>
+
+ <style>
+ button.accept {
+ background-color: #5757D2;
+ border: 1px solid black;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: white;
+ }
+ button.linky {
+ background:none!important;
+ border:none;
+ padding:0!important;
+
+ font-family:arial,sans-serif;
+ color:#069;
+ text-decoration:underline;
+ cursor:pointer;
+ }
+
+ input.url {
+ width: 25em;
+ }
+
+
+ button.accept:disabled {
+ background-color: #dedbe8;
+ border: 1px solid white;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: #2C2C2C;
+ }
+
+ .errorbox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #FF8A8A;
+ }
+ </style>
+</head>
+
+<body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <article id="contract" class="fade"></article>
+ </section>
+</body>
+
+</html>
diff --git a/src/pages/confirm-contract.tsx b/src/pages/confirm-contract.tsx
new file mode 100644
index 000000000..7bae691b1
--- /dev/null
+++ b/src/pages/confirm-contract.tsx
@@ -0,0 +1,231 @@
+/*
+ This file is part of TALER
+ (C) 2015 GNUnet e.V.
+
+ 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/>
+ */
+
+/**
+ * Page shown to the user to confirm entering
+ * a contract.
+ *
+ * @author Florian Dold
+ */
+
+"use strict";
+
+import {substituteFulfillmentUrl} from "src/helpers";
+import {Contract, AmountJson, IExchangeInfo} from "src/types";
+import {Offer} from "src/wallet";
+import {renderContract, prettyAmount} from "src/renderHtml";
+import {getExchanges} from "src/wxApi";
+
+
+interface DetailState {
+ collapsed: boolean;
+ exchanges: null|IExchangeInfo[];
+}
+
+interface DetailProps {
+ contract: Contract
+ collapsed: boolean
+}
+
+
+class Details extends React.Component<DetailProps, DetailState> {
+ constructor(props: DetailProps) {
+ super(props);
+ console.log("new Details component created");
+ this.state = {
+ collapsed: props.collapsed,
+ exchanges: null
+ };
+
+ console.log("initial state:", this.state);
+
+ this.update();
+ }
+
+ async update() {
+ let exchanges = await getExchanges();
+ this.setState({exchanges} as any);
+ }
+
+ render() {
+ if (this.state.collapsed) {
+ return (
+ <div>
+ <button className="linky"
+ onClick={() => { this.setState({collapsed: false} as any)}}>
+ show more details
+ </button>
+ </div>
+ );
+ } else {
+ return (
+ <div>
+ <button className="linky"
+ onClick={() => this.setState({collapsed: true} as any)}>
+ show less details
+ </button>
+ <div>
+ Accepted exchanges:
+ <ul>
+ {this.props.contract.exchanges.map(
+ e => <li>{`${e.url}: ${e.master_pub}`}</li>)}
+ </ul>
+ Exchanges in the wallet:
+ <ul>
+ {(this.state.exchanges || []).map(
+ (e: IExchangeInfo) =>
+ <li>{`${e.baseUrl}: ${e.masterPublicKey}`}</li>)}
+ </ul>
+ </div>
+ </div>);
+ }
+ }
+}
+
+interface ContractPromptProps {
+ offerId: number;
+}
+
+interface ContractPromptState {
+ offer: any;
+ error: string|null;
+ payDisabled: boolean;
+}
+
+class ContractPrompt extends React.Component<ContractPromptProps, ContractPromptState> {
+ constructor() {
+ super();
+ this.state = {
+ offer: undefined,
+ error: null,
+ payDisabled: true,
+ }
+ }
+
+ componentWillMount() {
+ this.update();
+ }
+
+ componentWillUnmount() {
+ // FIXME: abort running ops
+ }
+
+ async update() {
+ let offer = await this.getOffer();
+ this.setState({offer} as any);
+ this.checkPayment();
+ }
+
+ getOffer(): Promise<Offer> {
+ return new Promise((resolve, reject) => {
+ let msg = {
+ type: 'get-offer',
+ detail: {
+ offerId: this.props.offerId
+ }
+ };
+ chrome.runtime.sendMessage(msg, (resp) => {
+ resolve(resp);
+ });
+ })
+ }
+
+ checkPayment() {
+ let msg = {
+ type: 'check-pay',
+ detail: {
+ offer: this.state.offer
+ }
+ };
+ chrome.runtime.sendMessage(msg, (resp) => {
+ if (resp.error) {
+ console.log("check-pay error", JSON.stringify(resp));
+ switch (resp.error) {
+ case "coins-insufficient":
+ this.state.error = i18n`You have insufficient funds of the requested currency in your wallet.`;
+ break;
+ default:
+ this.state.error = `Error: ${resp.error}`;
+ break;
+ }
+ this.state.payDisabled = true;
+ } else {
+ this.state.payDisabled = false;
+ this.state.error = null;
+ }
+ this.setState({} as any);
+ window.setTimeout(() => this.checkPayment(), 500);
+ });
+ }
+
+ doPayment() {
+ let d = {offer: this.state.offer};
+ chrome.runtime.sendMessage({type: 'confirm-pay', detail: d}, (resp) => {
+ if (resp.error) {
+ console.log("confirm-pay error", JSON.stringify(resp));
+ switch (resp.error) {
+ case "coins-insufficient":
+ this.state.error = "You do not have enough coins of the" +
+ " requested currency.";
+ break;
+ default:
+ this.state.error = `Error: ${resp.error}`;
+ break;
+ }
+ this.setState({} as any);
+ return;
+ }
+ let c = d.offer.contract;
+ console.log("contract", c);
+ document.location.href = substituteFulfillmentUrl(c.fulfillment_url,
+ this.state.offer);
+ });
+ }
+
+
+ render() {
+ if (!this.state.offer) {
+ return <span>...</span>;
+ }
+ let c = this.state.offer.contract;
+ return (
+ <div>
+ <div>
+ {renderContract(c)}
+ </div>
+ <button onClick={() => this.doPayment()}
+ disabled={this.state.payDisabled}
+ className="accept">
+ Confirm payment
+ </button>
+ <div>
+ {(this.state.error ? <p className="errorbox">{this.state.error}</p> : <p />)}
+ </div>
+ <Details contract={c} collapsed={!this.state.error}/>
+ </div>
+ );
+ }
+}
+
+
+export function main() {
+ let url = URI(document.location.href);
+ let query: any = URI.parseQuery(url.query());
+ let offerId = JSON.parse(query.offerId);
+
+ ReactDOM.render(<ContractPrompt offerId={offerId}/>, document.getElementById(
+ "contract")!);
+}
diff --git a/src/pages/confirm-create-reserve.html b/src/pages/confirm-create-reserve.html
new file mode 100644
index 000000000..c67c7e960
--- /dev/null
+++ b/src/pages/confirm-create-reserve.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Taler Wallet: Select Taler Provider</title>
+
+ <link rel="icon" href="/img/icon.png">
+
+ <script src="/src/vendor/URI.js"></script>
+ <script src="/src/vendor/react.js"></script>
+ <script src="/src/vendor/react-dom.js"></script>
+
+ <!-- i18n -->
+ <script src="/src/vendor/jed.js"></script>
+ <script src="/src/i18n.js"></script>
+ <script src="/src/i18n/strings.js"></script>
+
+ <!-- module loading -->
+ <script src="/src/vendor/system-csp-production.src.js"></script>
+ <script src="/src/module-trampoline.js"></script>
+
+
+ <style>
+ #main {
+ border: solid 1px black;
+ border-radius: 10px;
+ margin: auto;
+ max-width: 50%;
+ padding: 2em;
+ }
+
+ button.accept {
+ background-color: #5757D2;
+ border: 1px solid black;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: white;
+ }
+ button.linky {
+ background:none!important;
+ border:none;
+ padding:0!important;
+
+ font-family:arial,sans-serif;
+ color:#069;
+ text-decoration:underline;
+ cursor:pointer;
+ }
+
+
+ button.accept:disabled {
+ background-color: #dedbe8;
+ border: 1px solid white;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: #2C2C2C;
+ }
+
+ input.url {
+ width: 25em;
+ }
+
+ table {
+ border-collapse: collapse;
+ }
+
+ td {
+ border-left: 1px solid black;
+ border-right: 1px solid black;
+ text-align: center;
+ padding: 0.3em;
+ }
+
+ span.spacer {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ }
+
+ </style>
+</head>
+
+<body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <div class="fade" id="exchange-selection"></div>
+ </section>
+</body>
+
+</html>
diff --git a/src/pages/confirm-create-reserve.tsx b/src/pages/confirm-create-reserve.tsx
new file mode 100644
index 000000000..372f11a4b
--- /dev/null
+++ b/src/pages/confirm-create-reserve.tsx
@@ -0,0 +1,397 @@
+/*
+ This file is part of TALER
+ (C) 2015-2016 GNUnet e.V.
+
+ 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/>
+ */
+
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import {amountToPretty, canonicalizeBaseUrl} from "src/helpers";
+import {
+ AmountJson, CreateReserveResponse,
+ ReserveCreationInfo, Amounts,
+ Denomination,
+} from "src/types";
+import {getReserveCreationInfo} from "src/wxApi";
+import {ImplicitStateComponent, StateHolder} from "src/components";
+
+"use strict";
+
+
+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();
+ }
+
+ private reset() {
+ this.triggerPromise = new Promise<boolean>((resolve, reject) => {
+ this.triggerResolve = resolve;
+ });
+ }
+
+ trigger() {
+ this.triggerResolve(false);
+ this.reset();
+ }
+
+ async wait(delayMs: number): Promise<boolean> {
+ return await Promise.race([this.triggerPromise, delay(delayMs, true)]);
+ }
+}
+
+
+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} = {};
+ 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);
+}
+
+
+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;
+ callback_url: string;
+ wt_types: string[];
+}
+
+
+class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
+ 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);
+
+ updateEvent = new EventTrigger();
+
+ constructor(props: ExchangeSelectionProps) {
+ super(props);
+ this.onUrlChanged(props.suggestedExchangeUrl || null);
+ }
+
+
+ renderAdvanced(): JSX.Element {
+ if (this.detailCollapsed() && this.url() !== null && !this.statusString()) {
+ 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)}`;
+ }
+
+ renderFeeStatus() {
+ if (this.reserveCreationInfo()) {
+ return (
+ <p>
+ The exchange provider will charge
+ {" "}
+ {this.renderFee()}
+ {" "}
+ in fees.
+ </p>
+ );
+ }
+ if (this.url() && !this.statusString()) {
+ let shortName = URI(this.url()!).host();
+ return <p>
+ Waiting for a response from
+ {" "}
+ <em>{shortName}</em>
+ </p>;
+ }
+ if (this.statusString()) {
+ return (
+ <p>
+ <strong style={{color: "red"}}>A problem occured, see below.</strong>
+ </p>
+ );
+ }
+ return (
+ <p>
+ Information about fees will be available when an exchange provider is selected.
+ </p>
+ );
+ }
+
+ render(): JSX.Element {
+ return (
+ <div>
+ <p>
+ {"You are about to withdraw "}
+ <strong>{amountToPretty(this.props.amount)}</strong>
+ {" from your bank account into your wallet."}
+ </p>
+ {this.renderFeeStatus()}
+ <button className="accept"
+ disabled={this.reserveCreationInfo() == null}
+ onClick={() => this.confirmReserve()}>
+ Accept fees and withdraw
+ </button>
+ <br/>
+ {this.renderAdvanced()}
+ </div>
+ );
+ }
+
+
+ confirmReserve() {
+ this.confirmReserveImpl(this.reserveCreationInfo()!,
+ this.url()!,
+ this.props.amount,
+ 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;
+ }
+
+ this.statusString(null);
+ let parsedUrl = URI(this.url()!);
+ if (parsedUrl.is("relative")) {
+ this.statusString(i18n`Error: URL may not be relative`);
+ return;
+ }
+
+ try {
+ let r = await getReserveCreationInfo(this.url()!,
+ this.props.amount);
+ console.log("get exchange info resolved");
+ this.reserveCreationInfo(r);
+ console.dir(r);
+ } catch (e) {
+ console.log("get exchange info rejected");
+ if (e.hasOwnProperty("httpStatus")) {
+ this.statusString(`Error: request failed with status ${e.httpStatus}`);
+ } else if (e.hasOwnProperty("errorResponse")) {
+ let resp = e.errorResponse;
+ this.statusString(`Error: ${resp.error} (${resp.hint})`);
+ }
+ }
+ }
+
+ reset() {
+ this.statusString(null);
+ this.reserveCreationInfo(null);
+ }
+
+ confirmReserveImpl(rci: ReserveCreationInfo,
+ exchange: string,
+ amount: AmountJson,
+ callback_url: string) {
+ const d = {exchange, amount};
+ const cb = (rawResp: any) => {
+ if (!rawResp) {
+ throw Error("empty response");
+ }
+ // FIXME: filter out types that bank/exchange don't have in common
+ let wire_details = rci.wireInfo;
+ if (!rawResp.error) {
+ const resp = CreateReserveResponse.checked(rawResp);
+ let q: {[name: string]: string|number} = {
+ wire_details: JSON.stringify(wire_details),
+ exchange: resp.exchange,
+ reserve_pub: resp.reservePub,
+ amount_value: amount.value,
+ amount_fraction: amount.fraction,
+ amount_currency: amount.currency,
+ };
+ let url = URI(callback_url).addQuery(q);
+ if (!url.is("absolute")) {
+ throw Error("callback url is not absolute");
+ }
+ console.log("going to", url.href());
+ document.location.href = url.href();
+ } else {
+ this.reset();
+ this.statusString(
+ `Oops, something went wrong.` +
+ `The wallet responded with error status (${rawResp.error}).`);
+ }
+ };
+ chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
+ }
+
+ async onUrlChanged(url: string|null) {
+ this.reset();
+ this.url(url);
+ 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();
+ }
+ }
+
+ 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 "";
+ }
+}
+
+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));
+ const callback_url = query.callback_url;
+ const bank_url = query.bank_url;
+ const wt_types = JSON.parse(query.wt_types);
+
+ try {
+ const suggestedExchangeUrl = await getSuggestedExchange(amount.currency);
+ let args = {
+ wt_types,
+ suggestedExchangeUrl,
+ callback_url,
+ amount
+ };
+
+ ReactDOM.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);
+ }
+}
diff --git a/src/pages/debug.html b/src/pages/debug.html
new file mode 100644
index 000000000..b8ddc7ccb
--- /dev/null
+++ b/src/pages/debug.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Taler Wallet Debugging</title>
+ <link rel="icon" href="../img/icon.png">
+ </head>
+ <body>
+ <h1>Debug Pages</h1>
+ <a href="show-db.html">Show DB</a> <br>
+ <a href="/src/popup/balance-overview.html">Show balance</a>
+
+ </body>
+</html>
diff --git a/src/pages/help/empty-wallet.html b/src/pages/help/empty-wallet.html
new file mode 100644
index 000000000..dd29d9689
--- /dev/null
+++ b/src/pages/help/empty-wallet.html
@@ -0,0 +1,30 @@
+<!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/pages/show-db.html b/src/pages/show-db.html
new file mode 100644
index 000000000..af8ca6eb1
--- /dev/null
+++ b/src/pages/show-db.html
@@ -0,0 +1,15 @@
+
+<!doctype html>
+
+<html>
+ <head>
+ <title>Taler Wallet: Reserve Created</title>
+ <link rel="stylesheet" type="text/css" href="../style/wallet.css">
+ <link rel="icon" href="/img/icon.png">
+ <script src="show-db.js"></script>
+ </head>
+ <body>
+ <h1>DB Dump</h1>
+ <pre id="dump"></pre>
+ </body>
+</html>
diff --git a/src/pages/show-db.ts b/src/pages/show-db.ts
new file mode 100644
index 000000000..71e74388b
--- /dev/null
+++ b/src/pages/show-db.ts
@@ -0,0 +1,57 @@
+/*
+ This file is part of TALER
+ (C) 2015 GNUnet e.V.
+
+ 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/>
+ */
+
+
+/**
+ * Wallet database dump for debugging.
+ *
+ * @author Florian Dold
+ */
+
+function replacer(match: string, pIndent: string, pKey: string, pVal: string,
+ pEnd: string) {
+ var key = '<span class=json-key>';
+ var val = '<span class=json-value>';
+ var str = '<span class=json-string>';
+ var r = pIndent || '';
+ if (pKey) {
+ r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
+ }
+ if (pVal) {
+ r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
+ }
+ return r + (pEnd || '');
+}
+
+
+function prettyPrint(obj: any) {
+ var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
+ return JSON.stringify(obj, null as any, 3)
+ .replace(/&/g, '&amp;').replace(/\\"/g, '&quot;')
+ .replace(/</g, '&lt;').replace(/>/g, '&gt;')
+ .replace(jsonLine, replacer);
+}
+
+
+document.addEventListener("DOMContentLoaded", () => {
+ chrome.runtime.sendMessage({type: 'dump-db'}, (resp) => {
+ const el = document.getElementById('dump');
+ if (!el) {
+ throw Error();
+ }
+ el.innerHTML = prettyPrint(resp);
+ });
+});
diff --git a/src/pages/tree.html b/src/pages/tree.html
new file mode 100644
index 000000000..306044159
--- /dev/null
+++ b/src/pages/tree.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Taler Wallet: Tree View</title>
+
+ <link rel="stylesheet" type="text/css" href="../style/lang.css">
+ <link rel="stylesheet" type="text/css" href="../style/wallet.css">
+
+ <link rel="icon" href="/img/icon.png">
+
+ <script src="/src/vendor/URI.js"></script>
+ <script src="/src/vendor/react.js"></script>
+ <script src="/src/vendor/react-dom.js"></script>
+
+ <!-- i18n -->
+ <script src="/src/vendor/jed.js"></script>
+ <script src="/src/i18n.js"></script>
+ <script src="/src/i18n/strings.js"></script>
+
+ <script src="/src/vendor/system-csp-production.src.js"></script>
+ <script src="/src/module-trampoline.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/pages/tree.tsx b/src/pages/tree.tsx
new file mode 100644
index 000000000..e368ffe9b
--- /dev/null
+++ b/src/pages/tree.tsx
@@ -0,0 +1,400 @@
+/*
+ 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 { IExchangeInfo } from "src/types";
+import { ReserveRecord, Coin, PreCoin, Denomination } from "src/types";
+import { ImplicitStateComponent, StateHolder } from "src/components";
+import {
+ getReserves, getExchanges, getCoins, getPreCoins,
+ refresh
+} from "src/wxApi";
+import { prettyAmount, abbrev } from "src/renderHtml";
+import { getTalerStampDate } from "src/helpers";
+
+interface ReserveViewProps {
+ reserve: ReserveRecord;
+}
+
+class ReserveView extends React.Component<ReserveViewProps, void> {
+ render(): JSX.Element {
+ let 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 ? prettyAmount(r.current_amount!) : "null"}</li>
+ <li>Requested: {prettyAmount(r.requested_amount)}</li>
+ <li>Confirmed: {r.confirmed}</li>
+ </ul>
+ </div>
+ );
+ }
+}
+
+interface ReserveListProps {
+ exchangeBaseUrl: string;
+}
+
+interface ToggleProps {
+ expanded: StateHolder<boolean>;
+}
+
+class Toggle extends ImplicitStateComponent<ToggleProps> {
+ renderButton() {
+ let show = () => {
+ this.props.expanded(true);
+ this.setState({});
+ };
+ let 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: Coin;
+}
+
+interface RefreshDialogProps {
+ coin: Coin;
+}
+
+class RefreshDialog extends ImplicitStateComponent<RefreshDialogProps> {
+ 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, void> {
+ render() {
+ let c = this.props.coin;
+ return (
+ <div className="tree-item">
+ <ul>
+ <li>Key: {c.coinPub}</li>
+ <li>Current amount: {prettyAmount(c.currentAmount)}</li>
+ <li>Denomination: {abbrev(c.denomPub, 20)}</li>
+ <li>Suspended: {(c.suspended || false).toString()}</li>
+ <li><RefreshDialog coin={c} /></li>
+ </ul>
+ </div>
+ );
+ }
+}
+
+
+
+interface PreCoinViewProps {
+ precoin: PreCoin;
+}
+
+class PreCoinView extends React.Component<PreCoinViewProps, void> {
+ render() {
+ let 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> {
+ coins = this.makeState<Coin[] | null>(null);
+ expanded = this.makeState<boolean>(false);
+
+ constructor(props: CoinListProps) {
+ super(props);
+ this.update(props);
+ }
+
+ async update(props: CoinListProps) {
+ let 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> {
+ precoins = this.makeState<PreCoin[] | null>(null);
+ expanded = this.makeState<boolean>(false);
+
+ constructor(props: PreCoinListProps) {
+ super(props);
+ this.update();
+ }
+
+ async update() {
+ let precoins = await getPreCoins(this.props.exchangeBaseUrl);
+ this.precoins(precoins);
+ }
+
+ render(): JSX.Element {
+ if (!this.precoins()) {
+ return <div>...</div>;
+ }
+ return (
+ <div className="tree-item">
+ Pre-Coins ({this.precoins() !.length.toString()})
+ {" "}
+ <Toggle expanded={this.expanded}>
+ {this.precoins() !.map((c) => <PreCoinView precoin={c} />)}
+ </Toggle>
+ </div>
+ );
+ }
+}
+
+interface DenominationListProps {
+ exchange: IExchangeInfo;
+}
+
+interface ExpanderTextProps {
+ text: string;
+}
+
+class ExpanderText extends ImplicitStateComponent<ExpanderTextProps> {
+ expanded = this.makeState<boolean>(false);
+ 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>
+ );
+ }
+}
+
+class DenominationList extends ImplicitStateComponent<DenominationListProps> {
+ expanded = this.makeState<boolean>(false);
+
+ renderDenom(d: Denomination) {
+ return (
+ <div className="tree-item">
+ <ul>
+ <li>Value: {prettyAmount(d.value)}</li>
+ <li>Withdraw fee: {prettyAmount(d.fee_withdraw)}</li>
+ <li>Refresh fee: {prettyAmount(d.fee_refresh)}</li>
+ <li>Deposit fee: {prettyAmount(d.fee_deposit)}</li>
+ <li>Refund fee: {prettyAmount(d.fee_refund)}</li>
+ <li>Start: {getTalerStampDate(d.stamp_start)!.toString()}</li>
+ <li>Withdraw expiration: {getTalerStampDate(d.stamp_expire_withdraw)!.toString()}</li>
+ <li>Legal expiration: {getTalerStampDate(d.stamp_expire_legal)!.toString()}</li>
+ <li>Deposit expiration: {getTalerStampDate(d.stamp_expire_deposit)!.toString()}</li>
+ <li>Denom pub: <ExpanderText text={d.denom_pub} /></li>
+ </ul>
+ </div>
+ );
+ }
+
+ render(): JSX.Element {
+ return (
+ <div className="tree-item">
+ Denominations ({this.props.exchange.active_denoms.length.toString()})
+ {" "}
+ <Toggle expanded={this.expanded}>
+ {this.props.exchange.active_denoms.map((d) => this.renderDenom(d))}
+ </Toggle>
+ </div>
+ );
+ }
+}
+
+class ReserveList extends ImplicitStateComponent<ReserveListProps> {
+ reserves = this.makeState<ReserveRecord[] | null>(null);
+ expanded = this.makeState<boolean>(false);
+
+ constructor(props: ReserveListProps) {
+ super(props);
+ this.update();
+ }
+
+ async update() {
+ let 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: IExchangeInfo;
+}
+
+class ExchangeView extends React.Component<ExchangeProps, void> {
+ render(): JSX.Element {
+ let 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?: IExchangeInfo[];
+}
+
+class ExchangesList extends React.Component<any, ExchangesListState> {
+ constructor() {
+ super();
+ let 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() {
+ let exchanges = await getExchanges();
+ console.log("exchanges: ", exchanges);
+ this.setState({ exchanges });
+ }
+
+ render(): JSX.Element {
+ let 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>
+ );
+ }
+}
+
+export function main() {
+ ReactDOM.render(<ExchangesList />, document.getElementById("container")!);
+}