diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-11-13 23:30:18 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-11-13 23:31:17 +0100 |
commit | f3fb8be7db6de87dae40d41bd5597a735c800ca1 (patch) | |
tree | 1a061db04de8f5bb5a6b697fa56a9948f67fac2f /src/pages | |
parent | 200d83c3886149ebb3f018530302079e12a81f6b (diff) |
restructuring
Diffstat (limited to 'src/pages')
-rw-r--r-- | src/pages/confirm-contract.html | 75 | ||||
-rw-r--r-- | src/pages/confirm-contract.tsx | 231 | ||||
-rw-r--r-- | src/pages/confirm-create-reserve.html | 93 | ||||
-rw-r--r-- | src/pages/confirm-create-reserve.tsx | 397 | ||||
-rw-r--r-- | src/pages/debug.html | 13 | ||||
-rw-r--r-- | src/pages/help/empty-wallet.html | 30 | ||||
-rw-r--r-- | src/pages/show-db.html | 15 | ||||
-rw-r--r-- | src/pages/show-db.ts | 57 | ||||
-rw-r--r-- | src/pages/tree.html | 36 | ||||
-rw-r--r-- | src/pages/tree.tsx | 400 |
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, '&').replace(/\\"/g, '"') + .replace(/</g, '<').replace(/>/g, '>') + .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")!); +} |