diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | src/db.ts | 2 | ||||
-rw-r--r-- | src/dbTypes.ts | 2 | ||||
-rw-r--r-- | src/i18n/de.po | 16 | ||||
-rw-r--r-- | src/i18n/en-US.po | 16 | ||||
-rw-r--r-- | src/i18n/fr.po | 16 | ||||
-rw-r--r-- | src/i18n/it.po | 16 | ||||
-rw-r--r-- | src/i18n/sv.po | 16 | ||||
-rw-r--r-- | src/i18n/taler-wallet-webex.pot | 16 | ||||
-rw-r--r-- | src/walletTypes.ts | 18 | ||||
-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 | ||||
-rw-r--r-- | tsconfig.json | 3 | ||||
-rw-r--r-- | webpack.config.js | 2 |
27 files changed, 535 insertions, 907 deletions
@@ -86,3 +86,7 @@ install: tsc npm install -g --prefix $(prefix) . endif +.PHONY: watch +watch: tsconfig.json + + ./node_modules/.bin/webpack --watch @@ -17,7 +17,7 @@ export function openTalerDb( const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION); req.onerror = e => { console.log("taler database error", e); - reject(e); + reject(new Error("database error")); }; req.onsuccess = e => { req.result.onversionchange = (evt: IDBVersionChangeEvent) => { diff --git a/src/dbTypes.ts b/src/dbTypes.ts index 0ca2b6262..ef79ae193 100644 --- a/src/dbTypes.ts +++ b/src/dbTypes.ts @@ -43,7 +43,7 @@ import { Index, Store } from "./query"; * In the future we might consider adding migration functions for * each version increment. */ -export const WALLET_DB_VERSION = 26; +export const WALLET_DB_VERSION = 27; /** * A reserve record as stored in the wallet's database. diff --git a/src/i18n/de.po b/src/i18n/de.po index 6a27556c7..9d0687909 100644 --- a/src/i18n/de.po +++ b/src/i18n/de.po @@ -250,42 +250,42 @@ msgstr "" msgid "Cancel withdraw operation" msgstr "" -#: src/webex/renderHtml.tsx:225 +#: src/webex/renderHtml.tsx:226 #, fuzzy, c-format msgid "Withdrawal fees:" msgstr "Abheben bei" -#: src/webex/renderHtml.tsx:226 +#: src/webex/renderHtml.tsx:227 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/renderHtml.tsx:227 +#: src/webex/renderHtml.tsx:228 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/renderHtml.tsx:233 +#: src/webex/renderHtml.tsx:234 #, c-format msgid "# Coins" msgstr "" -#: src/webex/renderHtml.tsx:234 +#: src/webex/renderHtml.tsx:235 #, c-format msgid "Value" msgstr "" -#: src/webex/renderHtml.tsx:235 +#: src/webex/renderHtml.tsx:236 #, fuzzy, c-format msgid "Withdraw Fee" msgstr "Abheben bei %1$s" -#: src/webex/renderHtml.tsx:236 +#: src/webex/renderHtml.tsx:237 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/renderHtml.tsx:237 +#: src/webex/renderHtml.tsx:238 #, c-format msgid "Deposit Fee" msgstr "" diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po index 558ec7177..0f73aa141 100644 --- a/src/i18n/en-US.po +++ b/src/i18n/en-US.po @@ -241,42 +241,42 @@ msgstr "" msgid "Cancel withdraw operation" msgstr "" -#: src/webex/renderHtml.tsx:225 +#: src/webex/renderHtml.tsx:226 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/renderHtml.tsx:226 +#: src/webex/renderHtml.tsx:227 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/renderHtml.tsx:227 +#: src/webex/renderHtml.tsx:228 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/renderHtml.tsx:233 +#: src/webex/renderHtml.tsx:234 #, c-format msgid "# Coins" msgstr "" -#: src/webex/renderHtml.tsx:234 +#: src/webex/renderHtml.tsx:235 #, c-format msgid "Value" msgstr "" -#: src/webex/renderHtml.tsx:235 +#: src/webex/renderHtml.tsx:236 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/renderHtml.tsx:236 +#: src/webex/renderHtml.tsx:237 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/renderHtml.tsx:237 +#: src/webex/renderHtml.tsx:238 #, c-format msgid "Deposit Fee" msgstr "" diff --git a/src/i18n/fr.po b/src/i18n/fr.po index ba7f9ecff..ee6b66eb0 100644 --- a/src/i18n/fr.po +++ b/src/i18n/fr.po @@ -241,42 +241,42 @@ msgstr "" msgid "Cancel withdraw operation" msgstr "" -#: src/webex/renderHtml.tsx:225 +#: src/webex/renderHtml.tsx:226 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/renderHtml.tsx:226 +#: src/webex/renderHtml.tsx:227 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/renderHtml.tsx:227 +#: src/webex/renderHtml.tsx:228 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/renderHtml.tsx:233 +#: src/webex/renderHtml.tsx:234 #, c-format msgid "# Coins" msgstr "" -#: src/webex/renderHtml.tsx:234 +#: src/webex/renderHtml.tsx:235 #, c-format msgid "Value" msgstr "" -#: src/webex/renderHtml.tsx:235 +#: src/webex/renderHtml.tsx:236 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/renderHtml.tsx:236 +#: src/webex/renderHtml.tsx:237 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/renderHtml.tsx:237 +#: src/webex/renderHtml.tsx:238 #, c-format msgid "Deposit Fee" msgstr "" diff --git a/src/i18n/it.po b/src/i18n/it.po index ba7f9ecff..ee6b66eb0 100644 --- a/src/i18n/it.po +++ b/src/i18n/it.po @@ -241,42 +241,42 @@ msgstr "" msgid "Cancel withdraw operation" msgstr "" -#: src/webex/renderHtml.tsx:225 +#: src/webex/renderHtml.tsx:226 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/renderHtml.tsx:226 +#: src/webex/renderHtml.tsx:227 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/renderHtml.tsx:227 +#: src/webex/renderHtml.tsx:228 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/renderHtml.tsx:233 +#: src/webex/renderHtml.tsx:234 #, c-format msgid "# Coins" msgstr "" -#: src/webex/renderHtml.tsx:234 +#: src/webex/renderHtml.tsx:235 #, c-format msgid "Value" msgstr "" -#: src/webex/renderHtml.tsx:235 +#: src/webex/renderHtml.tsx:236 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/renderHtml.tsx:236 +#: src/webex/renderHtml.tsx:237 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/renderHtml.tsx:237 +#: src/webex/renderHtml.tsx:238 #, c-format msgid "Deposit Fee" msgstr "" diff --git a/src/i18n/sv.po b/src/i18n/sv.po index 23e1a3645..97e510f44 100644 --- a/src/i18n/sv.po +++ b/src/i18n/sv.po @@ -245,42 +245,42 @@ msgstr "Acceptera avgifter och utbetala" msgid "Cancel withdraw operation" msgstr "" -#: src/webex/renderHtml.tsx:225 +#: src/webex/renderHtml.tsx:226 #, c-format msgid "Withdrawal fees:" msgstr "Utbetalnings avgifter:" -#: src/webex/renderHtml.tsx:226 +#: src/webex/renderHtml.tsx:227 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/renderHtml.tsx:227 +#: src/webex/renderHtml.tsx:228 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/renderHtml.tsx:233 +#: src/webex/renderHtml.tsx:234 #, c-format msgid "# Coins" msgstr "# Mynt" -#: src/webex/renderHtml.tsx:234 +#: src/webex/renderHtml.tsx:235 #, c-format msgid "Value" msgstr "Värde" -#: src/webex/renderHtml.tsx:235 +#: src/webex/renderHtml.tsx:236 #, c-format msgid "Withdraw Fee" msgstr "Utbetalnings avgift" -#: src/webex/renderHtml.tsx:236 +#: src/webex/renderHtml.tsx:237 #, c-format msgid "Refresh Fee" msgstr "Återhämtnings avgift" -#: src/webex/renderHtml.tsx:237 +#: src/webex/renderHtml.tsx:238 #, c-format msgid "Deposit Fee" msgstr "Depostitions avgift" diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot index ba7f9ecff..ee6b66eb0 100644 --- a/src/i18n/taler-wallet-webex.pot +++ b/src/i18n/taler-wallet-webex.pot @@ -241,42 +241,42 @@ msgstr "" msgid "Cancel withdraw operation" msgstr "" -#: src/webex/renderHtml.tsx:225 +#: src/webex/renderHtml.tsx:226 #, c-format msgid "Withdrawal fees:" msgstr "" -#: src/webex/renderHtml.tsx:226 +#: src/webex/renderHtml.tsx:227 #, c-format msgid "Rounding loss:" msgstr "" -#: src/webex/renderHtml.tsx:227 +#: src/webex/renderHtml.tsx:228 #, c-format msgid "Earliest expiration (for deposit): %1$s" msgstr "" -#: src/webex/renderHtml.tsx:233 +#: src/webex/renderHtml.tsx:234 #, c-format msgid "# Coins" msgstr "" -#: src/webex/renderHtml.tsx:234 +#: src/webex/renderHtml.tsx:235 #, c-format msgid "Value" msgstr "" -#: src/webex/renderHtml.tsx:235 +#: src/webex/renderHtml.tsx:236 #, c-format msgid "Withdraw Fee" msgstr "" -#: src/webex/renderHtml.tsx:236 +#: src/webex/renderHtml.tsx:237 #, c-format msgid "Refresh Fee" msgstr "" -#: src/webex/renderHtml.tsx:237 +#: src/webex/renderHtml.tsx:238 #, c-format msgid "Deposit Fee" msgstr "" diff --git a/src/walletTypes.ts b/src/walletTypes.ts index 0d18e4a9b..47360c660 100644 --- a/src/walletTypes.ts +++ b/src/walletTypes.ts @@ -507,8 +507,16 @@ export interface AcceptWithdrawalResponse { * Details about a purchase, including refund status. */ export interface PurchaseDetails { - contractTerms: ContractTerms, - hasRefund: boolean, - totalRefundAmount: AmountJson, - totalRefundAndRefreshFees: AmountJson, -}
\ No newline at end of file + contractTerms: ContractTerms; + hasRefund: boolean; + totalRefundAmount: AmountJson; + totalRefundAndRefreshFees: AmountJson; +} + +export interface WalletDiagnostics { + walletManifestVersion: string; + walletManifestDisplayVersion: string; + errors: string[]; + firefoxIdbProblem: boolean; + dbOutdated: boolean; +} 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 diff --git a/tsconfig.json b/tsconfig.json index 7aca07913..307918760 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -66,7 +66,6 @@ "src/webex/background.ts", "src/webex/chromeBadge.ts", "src/webex/compat.ts", - "src/webex/components.ts", "src/webex/messages.ts", "src/webex/notify.ts", "src/webex/pages/add-auditor.tsx", @@ -84,7 +83,7 @@ "src/webex/pages/return-coins.tsx", "src/webex/pages/show-db.ts", "src/webex/pages/tip.tsx", - "src/webex/pages/tree.tsx", + "src/webex/pages/welcome.tsx", "src/webex/pages/withdraw.tsx", "src/webex/renderHtml.tsx", "src/webex/wxApi.ts", diff --git a/webpack.config.js b/webpack.config.js index df9ebbcf4..096455887 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -79,6 +79,7 @@ module.exports = function (env) { "benchmark": "./src/webex/pages/benchmark.tsx", "pay": "./src/webex/pages/pay.tsx", "withdraw": "./src/webex/pages/withdraw.tsx", + "welcome": "./src/webex/pages/welcome.tsx", "error": "./src/webex/pages/error.tsx", "logs": "./src/webex/pages/logs.tsx", "payback": "./src/webex/pages/payback.tsx", @@ -88,7 +89,6 @@ module.exports = function (env) { "refund": "./src/webex/pages/refund.tsx", "show-db": "./src/webex/pages/show-db.ts", "tip": "./src/webex/pages/tip.tsx", - "tree": "./src/webex/pages/tree.tsx", }, name: "pages", optimization: { |