From b6e774585d32017e5f1ceeeb2b2e2a5e350354d3 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sun, 28 May 2017 23:15:41 +0200 Subject: move webex specific things in their own directory --- src/background/background.html | 11 - src/background/background.ts | 30 - src/chromeBadge.ts | 225 ---- src/components.ts | 63 -- src/content_scripts/notify.ts | 571 ---------- src/pages/add-auditor.html | 34 - src/pages/add-auditor.tsx | 125 --- src/pages/auditors.html | 36 - src/pages/auditors.tsx | 146 --- src/pages/confirm-contract.html | 69 -- src/pages/confirm-contract.tsx | 240 ----- src/pages/confirm-create-reserve.html | 52 - src/pages/confirm-create-reserve.tsx | 639 ------------ src/pages/error.html | 18 - src/pages/error.tsx | 63 -- src/pages/help/empty-wallet.html | 30 - src/pages/logs.html | 27 - src/pages/logs.tsx | 82 -- src/pages/payback.html | 36 - src/pages/payback.tsx | 98 -- src/pages/popup.css | 84 -- src/pages/popup.html | 18 - src/pages/popup.tsx | 545 ---------- src/pages/show-db.html | 18 - src/pages/show-db.ts | 94 -- src/pages/tree.html | 27 - src/pages/tree.tsx | 436 -------- src/renderHtml.tsx | 78 -- src/style/pure.css | 1508 --------------------------- src/style/wallet.css | 222 ---- src/webex/background.html | 11 + src/webex/background.ts | 30 + src/webex/chromeBadge.ts | 225 ++++ src/webex/components.ts | 63 ++ src/webex/notify.ts | 571 ++++++++++ src/webex/pages/add-auditor.html | 34 + src/webex/pages/add-auditor.tsx | 126 +++ src/webex/pages/auditors.html | 36 + src/webex/pages/auditors.tsx | 147 +++ src/webex/pages/confirm-contract.html | 69 ++ src/webex/pages/confirm-contract.tsx | 242 +++++ src/webex/pages/confirm-create-reserve.html | 52 + src/webex/pages/confirm-create-reserve.tsx | 641 ++++++++++++ src/webex/pages/error.html | 18 + src/webex/pages/error.tsx | 63 ++ src/webex/pages/help/empty-wallet.html | 30 + src/webex/pages/logs.html | 27 + src/webex/pages/logs.tsx | 83 ++ src/webex/pages/payback.html | 36 + src/webex/pages/payback.tsx | 100 ++ src/webex/pages/popup.css | 84 ++ src/webex/pages/popup.html | 18 + src/webex/pages/popup.tsx | 548 ++++++++++ src/webex/pages/show-db.html | 18 + src/webex/pages/show-db.ts | 94 ++ src/webex/pages/tree.html | 27 + src/webex/pages/tree.tsx | 437 ++++++++ src/webex/renderHtml.tsx | 79 ++ src/webex/style/pure.css | 1508 +++++++++++++++++++++++++++ src/webex/style/wallet.css | 222 ++++ src/webex/wxApi.ts | 174 ++++ src/webex/wxBackend.ts | 719 +++++++++++++ src/wxApi.ts | 174 ---- src/wxBackend.ts | 718 ------------- 64 files changed, 6532 insertions(+), 6517 deletions(-) delete mode 100644 src/background/background.html delete mode 100644 src/background/background.ts delete mode 100644 src/chromeBadge.ts delete mode 100644 src/components.ts delete mode 100644 src/content_scripts/notify.ts delete mode 100644 src/pages/add-auditor.html delete mode 100644 src/pages/add-auditor.tsx delete mode 100644 src/pages/auditors.html delete mode 100644 src/pages/auditors.tsx delete mode 100644 src/pages/confirm-contract.html delete mode 100644 src/pages/confirm-contract.tsx delete mode 100644 src/pages/confirm-create-reserve.html delete mode 100644 src/pages/confirm-create-reserve.tsx delete mode 100644 src/pages/error.html delete mode 100644 src/pages/error.tsx delete mode 100644 src/pages/help/empty-wallet.html delete mode 100644 src/pages/logs.html delete mode 100644 src/pages/logs.tsx delete mode 100644 src/pages/payback.html delete mode 100644 src/pages/payback.tsx delete mode 100644 src/pages/popup.css delete mode 100644 src/pages/popup.html delete mode 100644 src/pages/popup.tsx delete mode 100644 src/pages/show-db.html delete mode 100644 src/pages/show-db.ts delete mode 100644 src/pages/tree.html delete mode 100644 src/pages/tree.tsx delete mode 100644 src/renderHtml.tsx delete mode 100644 src/style/pure.css delete mode 100644 src/style/wallet.css create mode 100644 src/webex/background.html create mode 100644 src/webex/background.ts create mode 100644 src/webex/chromeBadge.ts create mode 100644 src/webex/components.ts create mode 100644 src/webex/notify.ts create mode 100644 src/webex/pages/add-auditor.html create mode 100644 src/webex/pages/add-auditor.tsx create mode 100644 src/webex/pages/auditors.html create mode 100644 src/webex/pages/auditors.tsx create mode 100644 src/webex/pages/confirm-contract.html create mode 100644 src/webex/pages/confirm-contract.tsx create mode 100644 src/webex/pages/confirm-create-reserve.html create mode 100644 src/webex/pages/confirm-create-reserve.tsx create mode 100644 src/webex/pages/error.html create mode 100644 src/webex/pages/error.tsx create mode 100644 src/webex/pages/help/empty-wallet.html create mode 100644 src/webex/pages/logs.html create mode 100644 src/webex/pages/logs.tsx create mode 100644 src/webex/pages/payback.html create mode 100644 src/webex/pages/payback.tsx create mode 100644 src/webex/pages/popup.css create mode 100644 src/webex/pages/popup.html create mode 100644 src/webex/pages/popup.tsx create mode 100644 src/webex/pages/show-db.html create mode 100644 src/webex/pages/show-db.ts create mode 100644 src/webex/pages/tree.html create mode 100644 src/webex/pages/tree.tsx create mode 100644 src/webex/renderHtml.tsx create mode 100644 src/webex/style/pure.css create mode 100644 src/webex/style/wallet.css create mode 100644 src/webex/wxApi.ts create mode 100644 src/webex/wxBackend.ts delete mode 100644 src/wxApi.ts delete mode 100644 src/wxBackend.ts (limited to 'src') diff --git a/src/background/background.html b/src/background/background.html deleted file mode 100644 index 0535dd5f3..000000000 --- a/src/background/background.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - (wallet bg page) - - - - - diff --git a/src/background/background.ts b/src/background/background.ts deleted file mode 100644 index aca27ef47..000000000 --- a/src/background/background.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - This file is part of TALER - (C) 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 - */ - -/** - * Entry point for the background page. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import {wxMain} from "./../wxBackend"; - -window.addEventListener("load", () => { - wxMain(); -}); diff --git a/src/chromeBadge.ts b/src/chromeBadge.ts deleted file mode 100644 index 702cefea8..000000000 --- a/src/chromeBadge.ts +++ /dev/null @@ -1,225 +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 - */ - -import { - Badge, -} from "./wallet"; - - -/** - * Polyfill for requestAnimationFrame, which - * doesn't work from a background page. - */ -function rAF(cb: (ts: number) => void) { - window.setTimeout(() => { - cb(performance.now()); - }, 100 /* 100 ms delay between frames */); -} - - -/** - * Badge for Chrome that renders a Taler logo with a rotating ring if some - * background activity is happening. - */ -export class ChromeBadge implements Badge { - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - /** - * True if animation running. The animation - * might still be running even if we're not busy anymore, - * just to transition to the "normal" state in a animated way. - */ - private animationRunning: boolean = false; - - /** - * Is the wallet still busy? Note that we do not stop the - * animation immediately when the wallet goes idle, but - * instead slowly close the gap. - */ - private isBusy: boolean = false; - - /** - * Current rotation angle, ranges from 0 to rotationAngleMax. - */ - private rotationAngle: number = 0; - - /** - * While animating, how wide is the current gap in the circle? - * Ranges from 0 to openMax. - */ - private gapWidth: number = 0; - - /** - * Maximum value for our rotationAngle, corresponds to 2 Pi. - */ - static rotationAngleMax = 1000; - - /** - * How fast do we rotate? Given in rotation angle (relative to rotationAngleMax) per millisecond. - */ - static rotationSpeed = 0.5; - - /** - * How fast to we open? Given in rotation angle (relative to rotationAngleMax) per millisecond. - */ - static openSpeed = 0.15; - - /** - * How fast to we close? Given as a multiplication factor per frame update. - */ - static closeSpeed = 0.7; - - /** - * How far do we open? Given relative to rotationAngleMax. - */ - static openMax = 100; - - constructor(window?: Window) { - // Allow injecting another window for testing - const bg = window || chrome.extension.getBackgroundPage(); - if (!bg) { - throw Error("no window available"); - } - this.canvas = bg.document.createElement("canvas"); - // Note: changing the width here means changing the font - // size in draw() as well! - this.canvas.width = 32; - this.canvas.height = 32; - this.ctx = this.canvas.getContext("2d")!; - this.draw(); - } - - /** - * Draw the badge based on the current state. - */ - private draw() { - this.ctx.setTransform(1, 0, 0, 1, 0, 0); - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); - - this.ctx.beginPath(); - this.ctx.arc(0, 0, this.canvas.width / 2 - 2, 0, 2 * Math.PI); - this.ctx.fillStyle = "white"; - this.ctx.fill(); - - // move into the center, off by 2 for aligning the "T" with the bottom - // of the circle. - this.ctx.translate(0, 2); - - // pick sans-serif font; note: 14px is based on the 32px width above! - this.ctx.font = "bold 24px sans-serif"; - // draw the "T" perfectly centered (x and y) to the current position - this.ctx.textAlign = "center"; - this.ctx.textBaseline = "middle"; - this.ctx.fillStyle = "black"; - this.ctx.fillText("T", 0, 0); - // now move really into the center - this.ctx.translate(0, -2); - // start drawing the (possibly open) circle - this.ctx.beginPath(); - this.ctx.lineWidth = 2.5; - if (this.animationRunning) { - /* Draw circle around the "T" with an opening of this.gapWidth */ - const aMax = ChromeBadge.rotationAngleMax; - const startAngle = this.rotationAngle / aMax * Math.PI * 2; - const stopAngle = ((this.rotationAngle + aMax - this.gapWidth) / aMax) * Math.PI * 2; - this.ctx.arc(0, 0, this.canvas.width / 2 - 2, /* radius */ startAngle, stopAngle, false); - } else { - /* Draw full circle */ - this.ctx.arc(0, 0, - this.canvas.width / 2 - 2, /* radius */ - 0, - Math.PI * 2, - false); - } - this.ctx.stroke(); - // go back to the origin - this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); - - // Allow running outside the extension for testing - // tslint:disable-next-line:no-string-literal - if (window["chrome"] && window.chrome["browserAction"]) { - try { - const imageData = this.ctx.getImageData(0, - 0, - this.canvas.width, - this.canvas.height); - chrome.browserAction.setIcon({imageData}); - } catch (e) { - // Might fail if browser has over-eager canvas fingerprinting countermeasures. - // There's nothing we can do then ... - } - } - } - - private animate() { - if (this.animationRunning) { - return; - } - this.animationRunning = true; - let start: number|undefined; - const step = (timestamp: number) => { - if (!this.animationRunning) { - return; - } - if (!start) { - start = timestamp; - } - if (!this.isBusy && 0 === this.gapWidth) { - // stop if we're close enough to origin - this.rotationAngle = 0; - } else { - this.rotationAngle = (this.rotationAngle + (timestamp - start) * - ChromeBadge.rotationSpeed) % ChromeBadge.rotationAngleMax; - } - if (this.isBusy) { - if (this.gapWidth < ChromeBadge.openMax) { - this.gapWidth += ChromeBadge.openSpeed * (timestamp - start); - } - if (this.gapWidth > ChromeBadge.openMax) { - this.gapWidth = ChromeBadge.openMax; - } - } else { - if (this.gapWidth > 0) { - this.gapWidth--; - this.gapWidth *= ChromeBadge.closeSpeed; - } - } - - if (this.isBusy || this.gapWidth > 0) { - start = timestamp; - rAF(step); - } else { - this.animationRunning = false; - } - this.draw(); - }; - rAF(step); - } - - startBusy() { - if (this.isBusy) { - return; - } - this.isBusy = true; - this.animate(); - } - - stopBusy() { - this.isBusy = false; - } -} diff --git a/src/components.ts b/src/components.ts deleted file mode 100644 index 1f5d18731..000000000 --- a/src/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 - */ - - -/** - * 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; - (newState: T): void; -} - -/** - * Component that doesn't hold its state in one object, - * but has multiple state holders. - */ -export abstract class ImplicitStateComponent extends React.Component { - private _implicit = {needsUpdate: false, didMount: false}; - componentDidMount() { - this._implicit.didMount = true; - if (this._implicit.needsUpdate) { - this.setState({} as any); - } - } - makeState(initial: StateType): StateHolder { - 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/content_scripts/notify.ts b/src/content_scripts/notify.ts deleted file mode 100644 index 733367a59..000000000 --- a/src/content_scripts/notify.ts +++ /dev/null @@ -1,571 +0,0 @@ -/* - 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 - */ - -// tslint:disable:no-unused-expression - -/** - * Module that is injected into (all!) pages to allow them - * to interact with the GNU Taler wallet via DOM Events. - */ - - -/** - * Imports. - */ -import URI = require("urijs"); - -declare var cloneInto: any; - -const PROTOCOL_VERSION = 1; - -let logVerbose: boolean = false; -try { - logVerbose = !!localStorage.getItem("taler-log-verbose"); -} catch (e) { - // can't read from local storage -} - -if (document.documentElement.getAttribute("data-taler-nojs")) { - document.dispatchEvent(new Event("taler-probe-result")); -} - - -function subst(url: string, H_contract: string) { - url = url.replace("${H_contract}", H_contract); - url = url.replace("${$}", "$"); - return url; -} - -interface Handler { - type: string; - listener: (e: CustomEvent) => void|Promise; -} -const handlers: Handler[] = []; - -function hashContract(contract: string): Promise { - const walletHashContractMsg = { - detail: {contract}, - type: "hash-contract", - }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { - if (!resp.hash) { - console.log("error", resp); - reject(Error("hashing failed")); - } - resolve(resp.hash); - }); - }); -} - -function queryPayment(url: string): Promise { - const walletMsg = { - detail: { url }, - type: "query-payment", - }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - resolve(resp); - }); - }); -} - -function putHistory(historyEntry: any): Promise { - const walletMsg = { - detail: { - historyEntry, - }, - type: "put-history-entry", - }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - resolve(); - }); - }); -} - -function saveOffer(offer: any): Promise { - const walletMsg = { - detail: { - offer: { - H_contract: offer.hash, - contract: offer.data, - merchant_sig: offer.sig, - offer_time: new Date().getTime() / 1000, - }, - type: "save-offer", - }, - }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - if (resp && resp.error) { - reject(resp); - } else { - resolve(resp); - } - }); - }); -} - - -let sheet: CSSStyleSheet|null; - -function initStyle() { - logVerbose && console.log("taking over styles"); - const name = "taler-presence-stylesheet"; - const content = "/* Taler stylesheet controlled by JS */"; - let style = document.getElementById(name) as HTMLStyleElement|null; - if (!style) { - style = document.createElement("style"); - // Needed by WebKit - style.appendChild(document.createTextNode(content)); - style.id = name; - document.head.appendChild(style); - sheet = style.sheet as CSSStyleSheet; - } else { - // We've taken over the stylesheet now, - // make it clear by clearing all the rules in it - // and making it obvious in the DOM. - if (style.tagName.toLowerCase() === "style") { - style.innerText = content; - } - if (!style.sheet) { - throw Error("taler-presence-stylesheet should be a style sheet ( or - - -
- - diff --git a/src/pages/add-auditor.tsx b/src/pages/add-auditor.tsx deleted file mode 100644 index 8bef557d9..000000000 --- a/src/pages/add-auditor.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - This file is part of TALER - (C) 2017 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 - */ - -/** - * View and edit auditors. - * - * @author Florian Dold - */ - - -import { - ExchangeRecord, - DenominationRecord, - AuditorRecord, - CurrencyRecord, - ReserveRecord, - CoinRecord, - PreCoinRecord, - Denomination -} from "../types"; -import { ImplicitStateComponent, StateHolder } from "../components"; -import { - getCurrencies, - updateCurrency, -} from "../wxApi"; -import { getTalerStampDate } from "../helpers"; - -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); - -interface ConfirmAuditorProps { - url: string; - currency: string; - auditorPub: string; - expirationStamp: number; -} - -class ConfirmAuditor extends ImplicitStateComponent { - addDone: StateHolder = this.makeState(false); - constructor() { - super(); - } - - async add() { - let currencies = await getCurrencies(); - let currency: CurrencyRecord|undefined = undefined; - - for (let c of currencies) { - if (c.name == this.props.currency) { - currency = c; - } - } - - if (!currency) { - currency = { name: this.props.currency, auditors: [], fractionalDigits: 2, exchanges: [] }; - } - - let newAuditor = { auditorPub: this.props.auditorPub, baseUrl: this.props.url, expirationStamp: this.props.expirationStamp }; - - let auditorFound = false; - for (let idx in currency.auditors) { - let a = currency.auditors[idx]; - if (a.baseUrl == this.props.url) { - auditorFound = true; - // Update auditor if already found by URL. - currency.auditors[idx] = newAuditor; - } - } - - if (!auditorFound) { - currency.auditors.push(newAuditor); - } - - await updateCurrency(currency); - - this.addDone(true); - } - - back() { - window.history.back(); - } - - render(): JSX.Element { - return ( -
-

Do you want to let {this.props.auditorPub} audit the currency "{this.props.currency}"?

- {this.addDone() ? - (
Auditor was added! You can also view and edit auditors.
) - : - (
- - -
) - } -
- ); - } -} - -export function main() { - const walletPageUrl = new URI(document.location.href); - 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(, document.getElementById("container")!); -} - -document.addEventListener("DOMContentLoaded", main); diff --git a/src/pages/auditors.html b/src/pages/auditors.html deleted file mode 100644 index cbfc3b4b5..000000000 --- a/src/pages/auditors.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Taler Wallet: Auditors - - - - - - - - - - - -
- - diff --git a/src/pages/auditors.tsx b/src/pages/auditors.tsx deleted file mode 100644 index f263d2ec9..000000000 --- a/src/pages/auditors.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - This file is part of TALER - (C) 2017 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 - */ - -/** - * View and edit auditors. - * - * @author Florian Dold - */ - - -import { - ExchangeRecord, - ExchangeForCurrencyRecord, - DenominationRecord, - AuditorRecord, - CurrencyRecord, - ReserveRecord, - CoinRecord, - PreCoinRecord, - Denomination -} from "../types"; -import { ImplicitStateComponent, StateHolder } from "../components"; -import { - getCurrencies, - updateCurrency, -} from "../wxApi"; -import { getTalerStampDate } from "../helpers"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; - -interface CurrencyListState { - currencies?: CurrencyRecord[]; -} - -class CurrencyList extends React.Component { - 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 currencies = await getCurrencies(); - console.log("currencies: ", currencies); - this.setState({ currencies }); - } - - async confirmRemoveAuditor(c: CurrencyRecord, a: AuditorRecord) { - if (window.confirm(`Do you really want to remove auditor ${a.baseUrl} for currency ${c.name}?`)) { - c.auditors = c.auditors.filter((x) => x.auditorPub != a.auditorPub); - await updateCurrency(c); - } - } - - async confirmRemoveExchange(c: CurrencyRecord, e: ExchangeForCurrencyRecord) { - if (window.confirm(`Do you really want to remove exchange ${e.baseUrl} for currency ${c.name}?`)) { - c.exchanges = c.exchanges.filter((x) => x.baseUrl != e.baseUrl); - await updateCurrency(c); - } - } - - renderAuditors(c: CurrencyRecord): any { - if (c.auditors.length == 0) { - return

No trusted auditors for this currency.

- } - return ( -
-

Trusted Auditors:

-
    - {c.auditors.map(a => ( -
  • {a.baseUrl} -
      -
    • valid until {new Date(a.expirationStamp).toString()}
    • -
    • public key {a.auditorPub}
    • -
    -
  • - ))} -
-
- ); - } - - renderExchanges(c: CurrencyRecord): any { - if (c.exchanges.length == 0) { - return

No trusted exchanges for this currency.

- } - return ( -
-

Trusted Exchanges:

-
    - {c.exchanges.map(e => ( -
  • {e.baseUrl} -
  • - ))} -
-
- ); - } - - render(): JSX.Element { - let currencies = this.state.currencies; - if (!currencies) { - return ...; - } - return ( -
- {currencies.map(c => ( -
-

Currency {c.name}

-

Displayed with {c.fractionalDigits} fractional digits.

-

Auditors

-
{this.renderAuditors(c)}
-

Exchanges

-
{this.renderExchanges(c)}
-
- ))} -
- ); - } -} - -export function main() { - ReactDOM.render(, document.getElementById("container")!); -} - -document.addEventListener("DOMContentLoaded", main); diff --git a/src/pages/confirm-contract.html b/src/pages/confirm-contract.html deleted file mode 100644 index 6713b2e2c..000000000 --- a/src/pages/confirm-contract.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - Taler Wallet: Confirm Reserve Creation - - - - - - - - - - - - -
-

GNU Taler Wallet

-
-
- - - diff --git a/src/pages/confirm-contract.tsx b/src/pages/confirm-contract.tsx deleted file mode 100644 index 47db94ee8..000000000 --- a/src/pages/confirm-contract.tsx +++ /dev/null @@ -1,240 +0,0 @@ -/* - 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 - */ - -/** - * Page shown to the user to confirm entering - * a contract. - */ - - -/** - * Imports. - */ -import { Contract, AmountJson, ExchangeRecord } from "../types"; -import { OfferRecord } from "../wallet"; -import { renderContract } from "../renderHtml"; -import { getExchanges } from "../wxApi"; -import * as i18n from "../i18n"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); - - -interface DetailState { - collapsed: boolean; -} - -interface DetailProps { - contract: Contract - collapsed: boolean - exchanges: null|ExchangeRecord[]; -} - - -class Details extends React.Component { - constructor(props: DetailProps) { - super(props); - console.log("new Details component created"); - this.state = { - collapsed: props.collapsed, - }; - - console.log("initial state:", this.state); - } - - render() { - if (this.state.collapsed) { - return ( -
- -
- ); - } else { - return ( -
- -
- {i18n.str`Accepted exchanges:`} -
    - {this.props.contract.exchanges.map( - e =>
  • {`${e.url}: ${e.master_pub}`}
  • )} -
- {i18n.str`Exchanges in the wallet:`} -
    - {(this.props.exchanges || []).map( - (e: ExchangeRecord) => -
  • {`${e.baseUrl}: ${e.masterPublicKey}`}
  • )} -
-
-
); - } - } -} - -interface ContractPromptProps { - offerId: number; -} - -interface ContractPromptState { - offer: OfferRecord|null; - error: string|null; - payDisabled: boolean; - exchanges: null|ExchangeRecord[]; -} - -class ContractPrompt extends React.Component { - constructor() { - super(); - this.state = { - offer: null, - error: null, - payDisabled: true, - exchanges: null - } - } - - componentWillMount() { - this.update(); - } - - componentWillUnmount() { - // FIXME: abort running ops - } - - async update() { - let offer = await this.getOffer(); - this.setState({offer} as any); - this.checkPayment(); - let exchanges = await getExchanges(); - this.setState({exchanges} as any); - } - - getOffer(): Promise { - 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": - let msgInsufficient = i18n.str`You have insufficient funds of the requested currency in your wallet.`; - let msgNoMatch = i18n.str`You do not have any funds from an exchange that is accepted by this merchant. None of the exchanges accepted by the merchant is known to your wallet.`; - if (this.state.exchanges && this.state.offer) { - let acceptedExchangePubs = this.state.offer.contract.exchanges.map((e) => e.master_pub); - let ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0); - if (ex) { - this.setState({error: msgInsufficient}); - } else { - this.setState({error: msgNoMatch}); - } - } else { - this.setState({error: msgInsufficient}); - } - break; - default: - this.setState({error: `Error: ${resp.error}`}); - break; - } - this.setState({payDisabled: true}); - } else { - this.setState({payDisabled: false, 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.setState({error: "You do not have enough coins of the requested currency."}); - break; - default: - this.setState({error: `Error: ${resp.error}`}); - break; - } - return; - } - let c = d.offer!.contract; - console.log("contract", c); - document.location.href = c.fulfillment_url; - }); - } - - - render() { - if (!this.state.offer) { - return ...; - } - let c = this.state.offer.contract; - return ( -
-
- {renderContract(c)} -
- -
- {(this.state.error ?

{this.state.error}

:

)} -

-
-
- ); - } -} - - -document.addEventListener("DOMContentLoaded", () => { - let url = new URI(document.location.href); - let query: any = URI.parseQuery(url.query()); - let offerId = JSON.parse(query.offerId); - - ReactDOM.render(, document.getElementById( - "contract")!); -}); diff --git a/src/pages/confirm-create-reserve.html b/src/pages/confirm-create-reserve.html deleted file mode 100644 index 16ab12a30..000000000 --- a/src/pages/confirm-create-reserve.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - Taler Wallet: Select Taler Provider - - - - - - - - - - - - - -
-

GNU Taler Wallet

-
-
- - - diff --git a/src/pages/confirm-create-reserve.tsx b/src/pages/confirm-create-reserve.tsx deleted file mode 100644 index 2f341bb4e..000000000 --- a/src/pages/confirm-create-reserve.tsx +++ /dev/null @@ -1,639 +0,0 @@ -/* - 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 - */ - - -/** - * Page shown to the user to confirm creation - * of a reserve, usually requested by the bank. - * - * @author Florian Dold - */ - -import {amountToPretty, canonicalizeBaseUrl} from "../helpers"; -import { - AmountJson, CreateReserveResponse, - ReserveCreationInfo, Amounts, - Denomination, DenominationRecord, CurrencyRecord -} from "../types"; -import {getReserveCreationInfo, getCurrency, getExchangeInfo} from "../wxApi"; -import {ImplicitStateComponent, StateHolder} from "../components"; -import * as i18n from "../i18n"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); -import * as moment from "moment"; - - -function delay(delayMs: number, value: T): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(value), delayMs); - }); -} - -class EventTrigger { - triggerResolve: any; - triggerPromise: Promise; - - constructor() { - this.reset(); - } - - private reset() { - this.triggerPromise = new Promise((resolve, reject) => { - this.triggerResolve = resolve; - }); - } - - trigger() { - this.triggerResolve(false); - this.reset(); - } - - async wait(delayMs: number): Promise { - return await Promise.race([this.triggerPromise, delay(delayMs, true)]); - } -} - - -interface CollapsibleState { - collapsed: boolean; -} - -interface CollapsibleProps { - initiallyCollapsed: boolean; - title: string; -} - -class Collapsible extends React.Component { - constructor(props: CollapsibleProps) { - super(props); - this.state = { collapsed: props.initiallyCollapsed }; - } - render() { - const doOpen = (e: any) => { - this.setState({collapsed: false}) - e.preventDefault() - }; - const doClose = (e: any) => { - this.setState({collapsed: true}) - e.preventDefault(); - }; - if (this.state.collapsed) { - return

{this.props.title}

; - } - return ( -
-

{this.props.title}

- {this.props.children} -
- ); - } -} - -function renderAuditorDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( -

- Details will be displayed when a valid exchange provider URL is entered. -

- ); - } - if (rci.exchangeInfo.auditors.length == 0) { - return ( -

- The exchange is not audited by any auditors. -

- ); - } - return ( -
- {rci.exchangeInfo.auditors.map(a => ( -

Auditor {a.url}

- ))} -
- ); -} - -function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( -

- Details will be displayed when a valid exchange provider URL is entered. -

- ); - } - - let denoms = rci.selectedDenoms; - - let countByPub: {[s: string]: number} = {}; - let uniq: DenominationRecord[] = []; - - denoms.forEach((x: DenominationRecord) => { - let c = countByPub[x.denomPub] || 0; - if (c == 0) { - uniq.push(x); - } - c += 1; - countByPub[x.denomPub] = c; - }); - - function row(denom: DenominationRecord) { - return ( - - {countByPub[denom.denomPub] + "x"} - {amountToPretty(denom.value)} - {amountToPretty(denom.feeWithdraw)} - {amountToPretty(denom.feeRefresh)} - {amountToPretty(denom.feeDeposit)} - - ); - } - - function wireFee(s: string) { - return [ - - - Wire Method {s} - - - Applies Until - Wire Fee - Closing Fee - - , - - {rci!.wireFees.feesForType[s].map(f => ( - - {moment.unix(f.endStamp).format("llll")} - {amountToPretty(f.wireFee)} - {amountToPretty(f.closingFee)} - - ))} - - ]; - } - - let withdrawFeeStr = amountToPretty(rci.withdrawFee); - let overheadStr = amountToPretty(rci.overhead); - - return ( -
-

Overview

-

{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}

-

{i18n.str`Rounding loss: ${overheadStr}`}

-

{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}

-

Coin Fees

- - - - - - - - - - - - {uniq.map(row)} - -
{i18n.str`# Coins`}{i18n.str`Value`}{i18n.str`Withdraw Fee`}{i18n.str`Refresh Fee`}{i18n.str`Deposit Fee`}
-

Wire Fees

- - {Object.keys(rci.wireFees.feesForType).map(wireFee)} -
-
- ); -} - - -function getSuggestedExchange(currency: string): Promise { - // 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

{i18n.str`Withdraw fees:`} {amountToPretty(totalCost)}

; - } - return

; -} - - -interface ExchangeSelectionProps { - suggestedExchangeUrl: string; - amount: AmountJson; - callback_url: string; - wt_types: string[]; - currencyRecord: CurrencyRecord|null; -} - -interface ManualSelectionProps { - onSelect(url: string): void; - initialUrl: string; -} - -class ManualSelection extends ImplicitStateComponent { - url: StateHolder = this.makeState(""); - errorMessage: StateHolder = this.makeState(null); - isOkay: StateHolder = this.makeState(false); - updateEvent = new EventTrigger(); - constructor(p: ManualSelectionProps) { - super(p); - this.url(p.initialUrl); - this.update(); - } - render() { - return ( -

-
- - this.onUrlChanged((e.target as HTMLInputElement).value)} /> -
-
- - {this.errorMessage()} -
-
- ); - } - - async update() { - this.errorMessage(null); - this.isOkay(false); - if (!this.url()) { - return; - } - let parsedUrl = new URI(this.url()!); - if (parsedUrl.is("relative")) { - this.errorMessage(i18n.str`Error: URL may not be relative`); - this.isOkay(false); - return; - } - try { - let url = canonicalizeBaseUrl(this.url()!); - let r = await getExchangeInfo(url) - console.log("getExchangeInfo returned") - this.isOkay(true); - } catch (e) { - console.log("got error", e); - if (e.hasOwnProperty("httpStatus")) { - this.errorMessage(`Error: request failed with status ${e.httpStatus}`); - } else if (e.hasOwnProperty("errorResponse")) { - let resp = e.errorResponse; - this.errorMessage(`Error: ${resp.error} (${resp.hint})`); - } else { - this.errorMessage("invalid exchange URL"); - } - } - } - - async onUrlChanged(s: string) { - this.url(s); - this.errorMessage(null); - this.isOkay(false); - this.updateEvent.trigger(); - let waited = await this.updateEvent.wait(200); - if (waited) { - // Run the actual update if nobody else preempted us. - this.update(); - } - } -} - - -class ExchangeSelection extends ImplicitStateComponent { - statusString: StateHolder = this.makeState(null); - reserveCreationInfo: StateHolder = this.makeState( - null); - url: StateHolder = this.makeState(null); - - selectingExchange: StateHolder = this.makeState(false); - - constructor(props: ExchangeSelectionProps) { - super(props); - let prefilledExchangesUrls = []; - if (props.currencyRecord) { - let exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl); - prefilledExchangesUrls.push(...exchanges); - } - if (props.suggestedExchangeUrl) { - prefilledExchangesUrls.push(props.suggestedExchangeUrl); - } - if (prefilledExchangesUrls.length != 0) { - this.url(prefilledExchangesUrls[0]); - this.forceReserveUpdate(); - } else { - this.selectingExchange(true); - } - } - - renderFeeStatus() { - let rci = this.reserveCreationInfo(); - if (rci) { - let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; - let trustMessage; - if (rci.isTrusted) { - trustMessage = ( - - The exchange is trusted by the wallet. - - ); - } else if (rci.isAudited) { - trustMessage = ( - - The exchange is audited by a trusted auditor. - - ); - } else { - trustMessage = ( - - Warning: The exchange is neither directly trusted nor audited by a trusted auditor. - If you withdraw from this exchange, it will be trusted in the future. - - ); - } - return ( -
- - Using exchange provider {this.url()}. - The exchange provider will charge - {" "} - {amountToPretty(totalCost)} - {" "} - in fees. - - {trustMessage} -
- ); - } - if (this.url() && !this.statusString()) { - let shortName = new URI(this.url()!).host(); - return ( - - Waiting for a response from - {" "} - {shortName} - - ); - } - if (this.statusString()) { - return ( -

- {i18n.str`A problem occured, see below. ${this.statusString()}`} -

- ); - } - return ( -

- {i18n.str`Information about fees will be available when an exchange provider is selected.`} -

- ); - } - - renderConfirm() { - return ( -
- {this.renderFeeStatus()} - - { " " } - -
- - {renderReserveCreationDetails(this.reserveCreationInfo())} - - - {renderAuditorDetails(this.reserveCreationInfo())} - -
- ); - } - - select(url: string) { - this.reserveCreationInfo(null); - this.url(url); - this.selectingExchange(false); - this.forceReserveUpdate(); - } - - renderSelect() { - let exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || []; - console.log(exchanges); - return ( -
- Please select an exchange. You can review the details before after your selection. - - {this.props.suggestedExchangeUrl && ( -
-

Bank Suggestion

- -
- )} - - {exchanges.length > 0 && ( -
-

Known Exchanges

- {exchanges.map(e => ( - - ))} -
- )} - -

Manual Selection

- this.select(url)} /> -
- ); - } - - render(): JSX.Element { - return ( -
- - {"You are about to withdraw "} - {amountToPretty(this.props.amount)} - {" from your bank account into your wallet."} - - {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()} -
- ); - } - - - 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); - try { - let url = canonicalizeBaseUrl(this.url()!); - let r = await getReserveCreationInfo(url, - this.props.amount); - console.log("get exchange info resolved"); - this.reserveCreationInfo(r); - console.dir(r); - } catch (e) { - console.log("get exchange info rejected", e); - 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})`); - } - } - } - - confirmReserveImpl(rci: ReserveCreationInfo, - exchange: string, - amount: AmountJson, - callback_url: string) { - const d = {exchange: canonicalizeBaseUrl(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 wireDetails = rci.wireInfo; - let filteredWireDetails: any = {}; - for (let wireType in wireDetails) { - if (this.props.wt_types.findIndex((x) => x.toLowerCase() == wireType.toLowerCase()) < 0) { - continue; - } - let obj = Object.assign({}, wireDetails[wireType]); - // The bank doesn't need to know about fees - delete obj.fees; - // Consequently the bank can't verify signatures anyway, so - // we delete this extra data, to make the request URL shorter. - delete obj.salt; - delete obj.sig; - filteredWireDetails[wireType] = obj; - } - if (!rawResp.error) { - const resp = CreateReserveResponse.checked(rawResp); - let q: {[name: string]: string|number} = { - wire_details: JSON.stringify(filteredWireDetails), - exchange: resp.exchange, - reserve_pub: resp.reservePub, - amount_value: amount.value, - amount_fraction: amount.fraction, - amount_currency: amount.currency, - }; - let url = new 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.statusString( - i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`); - } - }; - chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); - } - - renderStatus(): any { - if (this.statusString()) { - return

{this.statusString()}

; - } else if (!this.reserveCreationInfo()) { - return

{i18n.str`Checking URL, please wait ...`}

; - } - return ""; - } -} - -export async function main() { - try { - const url = new URI(document.location.href); - const query: any = URI.parseQuery(url.query()); - let amount; - try { - amount = AmountJson.checked(JSON.parse(query.amount)); - } catch (e) { - throw Error(i18n.str`Can't parse amount: ${e.message}`); - } - const callback_url = query.callback_url; - const bank_url = query.bank_url; - let wt_types; - try { - wt_types = JSON.parse(query.wt_types); - } catch (e) { - throw Error(i18n.str`Can't parse wire_types: ${e.message}`); - } - - let suggestedExchangeUrl = query.suggested_exchange_url; - let currencyRecord = await getCurrency(amount.currency); - - let args = { - wt_types, - suggestedExchangeUrl, - callback_url, - amount, - currencyRecord, - }; - - ReactDOM.render(, 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 = i18n.str`Fatal error: "${e.message}".`; - console.error(`got error "${e.message}"`, e); - } -} - -document.addEventListener("DOMContentLoaded", () => { - main(); -}); diff --git a/src/pages/error.html b/src/pages/error.html deleted file mode 100644 index 51a8fd73a..000000000 --- a/src/pages/error.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Taler Wallet: Error Occured - - - - - - - - - -
- - diff --git a/src/pages/error.tsx b/src/pages/error.tsx deleted file mode 100644 index f278bd224..000000000 --- a/src/pages/error.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - 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 - */ - - -/** - * Page shown to the user to confirm creation - * of a reserve, usually requested by the bank. - * - * @author Florian Dold - */ - -import {ImplicitStateComponent, StateHolder} from "../components"; - -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); - -"use strict"; - -interface ErrorProps { - message: string; -} - -class ErrorView extends React.Component { - render(): JSX.Element { - return ( -
- An error occurred: {this.props.message} -
- ); - } -} - -export async function main() { - try { - const url = new URI(document.location.href); - const query: any = URI.parseQuery(url.query()); - - const message: string = query.message || "unknown error"; - - ReactDOM.render(, document.getElementById( - "container")!); - - } 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/help/empty-wallet.html b/src/pages/help/empty-wallet.html deleted file mode 100644 index dd29d9689..000000000 --- a/src/pages/help/empty-wallet.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - GNU Taler Help - Empty Wallet - - - - - -
-
-
-

Your wallet is empty!

-

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.

-

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 - bank.demo.taler.net to - withdraw coins in the "KUDOS" currency that we created just for - demonstrating the system.

-
-
-
- - diff --git a/src/pages/logs.html b/src/pages/logs.html deleted file mode 100644 index 9545269e3..000000000 --- a/src/pages/logs.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - Taler Wallet: Logs - - - - - - - - - - - -
- - diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx deleted file mode 100644 index a1e5161ec..000000000 --- a/src/pages/logs.tsx +++ /dev/null @@ -1,82 +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 - */ - -/** - * Show wallet logs. - * - * @author Florian Dold - */ - -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import {LogEntry, getLogs} from "../logging"; - -interface LogViewProps { - log: LogEntry; -} - -class LogView extends React.Component { - render(): JSX.Element { - let e = this.props.log; - return ( -
-
    -
  • level: {e.level}
  • -
  • msg: {e.msg}
  • -
  • id: {e.id || "unknown"}
  • -
  • file: {e.source || "(unknown)"}
  • -
  • line: {e.line || "(unknown)"}
  • -
  • col: {e.col || "(unknown)"}
  • - {(e.detail ?
  • detail:
    {e.detail}
  • : [])} -
-
- ); - } -} - -interface LogsState { - logs: LogEntry[]|undefined; -} - -class Logs extends React.Component { - constructor() { - super(); - this.update(); - this.state = {} as any; - } - - async update() { - let logs = await getLogs(); - this.setState({logs}); - } - - render(): JSX.Element { - let logs = this.state.logs; - if (!logs) { - return ...; - } - return ( -
- Logs: - {logs.map(e => )} -
- ); - } -} - -document.addEventListener("DOMContentLoaded", () => { - ReactDOM.render(, document.getElementById("container")!); -}); diff --git a/src/pages/payback.html b/src/pages/payback.html deleted file mode 100644 index d6fe334c8..000000000 --- a/src/pages/payback.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Taler Wallet: Payback - - - - - - - - - - - -
- - diff --git a/src/pages/payback.tsx b/src/pages/payback.tsx deleted file mode 100644 index 01f5a64e4..000000000 --- a/src/pages/payback.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - This file is part of TALER - (C) 2017 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 - */ - -/** - * View and edit auditors. - * - * @author Florian Dold - */ - - -import { - ExchangeRecord, - ExchangeForCurrencyRecord, - DenominationRecord, - AuditorRecord, - CurrencyRecord, - ReserveRecord, - CoinRecord, - PreCoinRecord, - Denomination, - WalletBalance, -} from "../types"; -import { ImplicitStateComponent, StateHolder } from "../components"; -import { - getCurrencies, - updateCurrency, - getPaybackReserves, - withdrawPaybackReserve, -} from "../wxApi"; -import { amountToPretty, getTalerStampDate } from "../helpers"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; - -class Payback extends ImplicitStateComponent { - reserves: StateHolder = this.makeState(null); - constructor() { - super(); - let port = chrome.runtime.connect(); - port.onMessage.addListener((msg: any) => { - if (msg.notify) { - console.log("got notified"); - this.update(); - } - }); - this.update(); - } - - async update() { - let reserves = await getPaybackReserves(); - this.reserves(reserves); - } - - withdrawPayback(pub: string) { - withdrawPaybackReserve(pub); - } - - render(): JSX.Element { - let reserves = this.reserves(); - if (!reserves) { - return loading ...; - } - if (reserves.length == 0) { - return No reserves with payback available.; - } - return ( -
- {reserves.map(r => ( -
-

Reserve for ${amountToPretty(r.current_amount!)}

-
    -
  • Exchange: ${r.exchange_base_url}
  • -
- -
- ))} -
- ); - } -} - -export function main() { - ReactDOM.render(, document.getElementById("container")!); -} - -document.addEventListener("DOMContentLoaded", main); diff --git a/src/pages/popup.css b/src/pages/popup.css deleted file mode 100644 index 675412c11..000000000 --- a/src/pages/popup.css +++ /dev/null @@ -1,84 +0,0 @@ - -/** - * @author Gabor X. Toth - * @author Marcello Stanisci - * @author Florian Dold - */ - -body { - min-height: 20em; - width: 30em; - margin: 0; - padding: 0; - max-height: 800px; - overflow: hidden; -} - -.nav { - background-color: #ddd; - padding: 0.5em 0; -} - -.nav a { - color: black; - padding: 0.5em; - text-decoration: none; -} - -.nav a.active { - background-color: white; - font-weight: bold; -} - - -.container { - overflow-y: scroll; - max-height: 400px; -} - -.abbrev { - text-decoration-style: dotted; -} - -#content { - padding: 1em; -} - - -#wallet-table .amount { - text-align: right; -} - -.hidden { - display: none; -} - -#transactions-table th, -#transactions-table td { - padding: 0.2em 0.5em; -} - -#reserve-create table { - width: 100%; -} - -#reserve-create table td.label { - width: 5em; -} - -#reserve-create table .input input[type="text"] { - width: 100%; -} - -.historyItem { - border: 1px solid black; - border-radius: 10px; - padding-left: 0.5em; - margin: 0.5em; -} - -.historyDate { - font-size: 90%; - margin: 0.3em; - color: slategray; -} diff --git a/src/pages/popup.html b/src/pages/popup.html deleted file mode 100644 index 98f24bccc..000000000 --- a/src/pages/popup.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - -
- - - diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx deleted file mode 100644 index aef5a3df8..000000000 --- a/src/pages/popup.tsx +++ /dev/null @@ -1,545 +0,0 @@ -/* - This file is part of TALER - (C) 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 - */ - - -/** - * Popup shown to the user when they click - * the Taler browser action button. - * - * @author Florian Dold - */ - - -"use strict"; - -import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent; -import { HistoryRecord, HistoryLevel } from "../wallet"; -import { - AmountJson, WalletBalance, Amounts, - WalletBalanceEntry -} from "../types"; -import { amountToPretty } from "../helpers"; -import { abbrev } from "../renderHtml"; -import * as i18n from "../i18n"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); - -function onUpdateNotification(f: () => void): () => void { - let port = chrome.runtime.connect({name: "notifications"}); - let listener = (msg: any, port: any) => { - f(); - }; - port.onMessage.addListener(listener); - return () => { - port.onMessage.removeListener(listener); - } -} - - -class Router extends React.Component { - static setRoute(s: string): void { - window.location.hash = s; - } - - static getRoute(): string { - // Omit the '#' at the beginning - return window.location.hash.substring(1); - } - - static onRoute(f: any): () => void { - Router.routeHandlers.push(f); - return () => { - let i = Router.routeHandlers.indexOf(f); - this.routeHandlers = this.routeHandlers.splice(i, 1); - } - } - - static routeHandlers: any[] = []; - - componentWillMount() { - console.log("router mounted"); - window.onhashchange = () => { - this.setState({}); - for (let f of Router.routeHandlers) { - f(); - } - } - } - - componentWillUnmount() { - console.log("router unmounted"); - } - - - render(): JSX.Element { - let 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 childProps: any = (child as any).props; - if (!childProps) { - return; - } - if (childProps["default"]) { - defaultChild = child; - } - if (childProps["route"] == route) { - foundChild = child; - } - }) - let child: React.ReactChild | null = foundChild || defaultChild; - if (!child) { - throw Error("unknown route"); - } - Router.setRoute((child as any).props["route"]); - return
{child}
; - } -} - - -interface TabProps { - target: string; - children?: React.ReactNode; -} - -function Tab(props: TabProps) { - let cssClass = ""; - if (props.target == Router.getRoute()) { - cssClass = "active"; - } - let onClick = (e: React.MouseEvent) => { - Router.setRoute(props.target); - e.preventDefault(); - }; - return ( - - {props.children} - - ); -} - - -class WalletNavBar extends React.Component { - cancelSubscription: any; - - componentWillMount() { - this.cancelSubscription = Router.onRoute(() => { - this.setState({}); - }); - } - - componentWillUnmount() { - if (this.cancelSubscription) { - this.cancelSubscription(); - } - } - - render() { - console.log("rendering nav bar"); - return ( - ); - } -} - - -function ExtensionLink(props: any) { - let onClick = (e: React.MouseEvent) => { - chrome.tabs.create({ - "url": chrome.extension.getURL(props.target) - }); - e.preventDefault(); - }; - return ( - - {props.children} - ) -} - - -export function bigAmount(amount: AmountJson): JSX.Element { - let v = amount.value + amount.fraction / Amounts.fractionalBase; - return ( - - {v} - {" "} - {amount.currency} - - ); -} - -class WalletBalanceView extends React.Component { - balance: WalletBalance; - gotError = false; - canceler: (() => void) | undefined = undefined; - unmount = false; - - componentWillMount() { - this.canceler = onUpdateNotification(() => this.updateBalance()); - this.updateBalance(); - } - - componentWillUnmount() { - console.log("component WalletBalanceView will unmount"); - if (this.canceler) { - this.canceler(); - } - this.unmount = true; - } - - updateBalance() { - chrome.runtime.sendMessage({type: "balances"}, (resp) => { - if (this.unmount) { - return; - } - if (resp.error) { - this.gotError = true; - console.error("could not retrieve balances", resp); - this.setState({}); - return; - } - this.gotError = false; - console.log("got wallet", resp); - this.balance = resp; - this.setState({}); - }); - } - - renderEmpty(): JSX.Element { - let helpLink = ( - - {i18n.str`help`} - - ); - return ( -
- - You have no balance to show. Need some - {" "}{helpLink}{" "} - getting started? - -
- ); - } - - formatPending(entry: WalletBalanceEntry): JSX.Element { - let incoming: JSX.Element | undefined; - let payment: JSX.Element | undefined; - - console.log("available: ", entry.pendingIncoming ? amountToPretty(entry.available) : null); - console.log("incoming: ", entry.pendingIncoming ? amountToPretty(entry.pendingIncoming) : null); - - if (Amounts.isNonZero(entry.pendingIncoming)) { - incoming = ( - - - {"+"} - {amountToPretty(entry.pendingIncoming)} - - {" "} - incoming - - ); - } - - if (Amounts.isNonZero(entry.pendingPayment)) { - payment = ( - - - {amountToPretty(entry.pendingPayment)} - - {" "} - being spent - - ); - } - - let l = [incoming, payment].filter((x) => x !== undefined); - if (l.length == 0) { - return ; - } - - if (l.length == 1) { - return ({l}) - } - return ({l[0]}, {l[1]}); - - } - - render(): JSX.Element { - let wallet = this.balance; - if (this.gotError) { - return i18n.str`Error: could not retrieve balance information.`; - } - if (!wallet) { - return ; - } - console.log(wallet); - let paybackAvailable = false; - let listing = Object.keys(wallet).map((key) => { - let entry: WalletBalanceEntry = wallet[key]; - if (entry.paybackAmount.value != 0 || entry.paybackAmount.fraction != 0) { - paybackAvailable = true; - } - return ( -

- {bigAmount(entry.available)} - {" "} - {this.formatPending(entry)} -

- ); - }); - let link = chrome.extension.getURL("/src/pages/auditors.html"); - let linkElem = Trusted Auditors and Exchanges; - let paybackLink = chrome.extension.getURL("/src/pages/payback.html"); - let paybackLinkElem = Trusted Auditors and Exchanges; - return ( -
- {listing.length > 0 ? listing : this.renderEmpty()} - {paybackAvailable && paybackLinkElem} - {linkElem} -
- ); - } -} - - -function formatHistoryItem(historyItem: HistoryRecord) { - const d = historyItem.detail; - const t = historyItem.timestamp; - console.log("hist item", historyItem); - switch (historyItem.type) { - case "create-reserve": - return ( - - Bank requested reserve ({abbrev(d.reservePub)}) for {amountToPretty(d.requestedAmount)}. - - ); - case "confirm-reserve": { - // FIXME: eventually remove compat fix - let exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??"; - let pub = abbrev(d.reservePub); - return ( - - Started to withdraw - {" "}{amountToPretty(d.requestedAmount)}{" "} - from {exchange} ({pub}). - - ); - } - case "offer-contract": { - let link = chrome.extension.getURL("view-contract.html"); - let linkElem = {abbrev(d.contractHash)}; - let merchantElem = {abbrev(d.merchantName, 15)}; - return ( - - Merchant {abbrev(d.merchantName, 15)} offered contract {abbrev(d.contractHash)}; - - ); - } - case "depleted-reserve": { - let exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??"; - let amount = amountToPretty(d.requestedAmount); - let pub = abbrev(d.reservePub); - return ( - - Withdrew {amount} from {exchange} ({pub}). - - ); - } - case "pay": { - let url = d.fulfillmentUrl; - let merchantElem = {abbrev(d.merchantName, 15)}; - let fulfillmentLinkElem = view product; - return ( - - Paid {amountToPretty(d.amount)} to merchant {merchantElem}. ({fulfillmentLinkElem}) - - ); - } - default: - return (

{i18n.str`Unknown event (${historyItem.type})`}

); - } -} - - -class WalletHistory extends React.Component { - myHistory: any[]; - gotError = false; - unmounted = false; - - componentWillMount() { - this.update(); - onUpdateNotification(() => this.update()); - } - - componentWillUnmount() { - console.log("history component unmounted"); - this.unmounted = true; - } - - update() { - chrome.runtime.sendMessage({type: "get-history"}, (resp) => { - if (this.unmounted) { - return; - } - console.log("got history response"); - if (resp.error) { - this.gotError = true; - console.error("could not retrieve history", resp); - this.setState({}); - return; - } - this.gotError = false; - console.log("got history", resp.history); - this.myHistory = resp.history; - this.setState({}); - }); - } - - render(): JSX.Element { - console.log("rendering history"); - let history: HistoryRecord[] = this.myHistory; - if (this.gotError) { - return i18n.str`Error: could not retrieve event history`; - } - - if (!history) { - // We're not ready yet - return ; - } - - let subjectMemo: {[s: string]: boolean} = {}; - let listing: any[] = []; - for (let record of history.reverse()) { - if (record.subjectId && subjectMemo[record.subjectId]) { - continue; - } - if (record.level != undefined && record.level < HistoryLevel.User) { - continue; - } - subjectMemo[record.subjectId as string] = true; - - let item = ( -
-
- {(new Date(record.timestamp)).toString()} -
- {formatHistoryItem(record)} -
- ); - - listing.push(item); - } - - if (listing.length > 0) { - return
{listing}
; - } - return

{i18n.str`Your wallet has no events recorded.`}

- } - -} - - -function reload() { - try { - chrome.runtime.reload(); - window.close(); - } catch (e) { - // Functionality missing in firefox, ignore! - } -} - -function confirmReset() { - if (confirm("Do you want to IRREVOCABLY DESTROY everything inside your" + - " wallet and LOSE ALL YOUR COINS?")) { - chrome.runtime.sendMessage({type: "reset"}); - window.close(); - } -} - - -function WalletDebug(props: any) { - return (
-

Debug tools:

- - - - -
- - -
); -} - - -function openExtensionPage(page: string) { - return function() { - chrome.tabs.create({ - "url": chrome.extension.getURL(page) - }); - } -} - - -function openTab(page: string) { - return function() { - chrome.tabs.create({ - "url": page - }); - } -} - - -let el = ( -
- -
- - - - - -
-
-); - -document.addEventListener("DOMContentLoaded", () => { - ReactDOM.render(el, document.getElementById("content")!); -}) diff --git a/src/pages/show-db.html b/src/pages/show-db.html deleted file mode 100644 index 215c726d9..000000000 --- a/src/pages/show-db.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Taler Wallet: Reserve Created - - - - - - -

DB Dump

- - - -

-  
-
diff --git a/src/pages/show-db.ts b/src/pages/show-db.ts
deleted file mode 100644
index d95951385..000000000
--- a/src/pages/show-db.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- 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 
- */
-
-
-/**
- * Wallet database dump for debugging.
- *
- * @author Florian Dold
- */
-
-function replacer(match: string, pIndent: string, pKey: string, pVal: string,
-                  pEnd: string) {
-  const key = "";
-  const val = "";
-  const str = "";
-  let r = pIndent || "";
-  if (pKey) {
-    r = r + key + '"' + pKey.replace(/[": ]/g, "") + '": ';
-  }
-  if (pVal) {
-    r = r + (pVal[0] === '"' ? str : val) + pVal + "";
-  }
-  return r + (pEnd || "");
-}
-
-
-function prettyPrint(obj: any) {
-  const jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
-  return JSON.stringify(obj, null as any, 3)
-             .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);
-
-    document.getElementById("download")!.addEventListener("click", (evt) => {
-      console.log("creating download");
-      const element = document.createElement("a");
-      element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(resp)));
-      element.setAttribute("download", "wallet-dump.txt");
-      element.style.display = "none";
-      document.body.appendChild(element);
-      element.click();
-    });
-
-  });
-
-
-  const fileInput = document.getElementById("fileInput")! as HTMLInputElement;
-  fileInput.onchange = (evt) => {
-    if (!fileInput.files || fileInput.files.length !== 1) {
-      alert("please select exactly one file to import");
-      return;
-    }
-    const file = fileInput.files[0];
-    const fr = new FileReader();
-    fr.onload = (e: any) => {
-      console.log("got file");
-      const dump = JSON.parse(e.target.result);
-      console.log("parsed contents", dump);
-      chrome.runtime.sendMessage({ type: "import-db", detail: { dump } }, (resp) => {
-        alert("loaded");
-      });
-    };
-    console.log("reading file", file);
-    fr.readAsText(file);
-  };
-
-  document.getElementById("import")!.addEventListener("click", (evt) => {
-    fileInput.click();
-    evt.preventDefault();
-  });
-});
diff --git a/src/pages/tree.html b/src/pages/tree.html
deleted file mode 100644
index 0c0a368b3..000000000
--- a/src/pages/tree.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-  
-  Taler Wallet: Tree View
-
-  
-
-  
-
-  
-  
-
-  
-
-  
-    
- - diff --git a/src/pages/tree.tsx b/src/pages/tree.tsx deleted file mode 100644 index 8d1258c51..000000000 --- a/src/pages/tree.tsx +++ /dev/null @@ -1,436 +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 - */ - -/** - * Show contents of the wallet as a tree. - * - * @author Florian Dold - */ - - -import { - ExchangeRecord, - DenominationRecord, - CoinStatus, - ReserveRecord, - CoinRecord, - PreCoinRecord, - Denomination, -} from "../types"; -import { ImplicitStateComponent, StateHolder } from "../components"; -import { - getReserves, getExchanges, getCoins, getPreCoins, - refresh, getDenoms, payback, -} from "../wxApi"; -import { amountToPretty, getTalerStampDate } from "../helpers"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; - -interface ReserveViewProps { - reserve: ReserveRecord; -} - -class ReserveView extends React.Component { - render(): JSX.Element { - let r: ReserveRecord = this.props.reserve; - return ( -
-
    -
  • Key: {r.reserve_pub}
  • -
  • Created: {(new Date(r.created * 1000).toString())}
  • -
  • Current: {r.current_amount ? amountToPretty(r.current_amount!) : "null"}
  • -
  • Requested: {amountToPretty(r.requested_amount)}
  • -
  • Confirmed: {r.confirmed}
  • -
-
- ); - } -} - -interface ReserveListProps { - exchangeBaseUrl: string; -} - -interface ToggleProps { - expanded: StateHolder; -} - -class Toggle extends ImplicitStateComponent { - renderButton() { - let show = () => { - this.props.expanded(true); - this.setState({}); - }; - let hide = () => { - this.props.expanded(false); - this.setState({}); - }; - if (this.props.expanded()) { - return ; - } - return ; - - } - render() { - return ( -
- {this.renderButton()} - {this.props.expanded() ? this.props.children : []} -
); - } -} - - -interface CoinViewProps { - coin: CoinRecord; -} - -interface RefreshDialogProps { - coin: CoinRecord; -} - -class RefreshDialog extends ImplicitStateComponent { - refreshRequested = this.makeState(false); - render(): JSX.Element { - if (!this.refreshRequested()) { - return ( -
- -
- ); - } - return ( -
- Refresh amount: - - -
- ); - } -} - -class CoinView extends React.Component { - render() { - let c = this.props.coin; - return ( -
-
    -
  • Key: {c.coinPub}
  • -
  • Current amount: {amountToPretty(c.currentAmount)}
  • -
  • Denomination:
  • -
  • Suspended: {(c.suspended || false).toString()}
  • -
  • Status: {CoinStatus[c.status]}
  • -
  • -
  • -
-
- ); - } -} - - - -interface PreCoinViewProps { - precoin: PreCoinRecord; -} - -class PreCoinView extends React.Component { - render() { - let c = this.props.precoin; - return ( -
-
    -
  • Key: {c.coinPub}
  • -
-
- ); - } -} - -interface CoinListProps { - exchangeBaseUrl: string; -} - -class CoinList extends ImplicitStateComponent { - coins = this.makeState(null); - expanded = this.makeState(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
...
; - } - return ( -
- Coins ({this.coins() !.length.toString()}) - {" "} - - {this.coins() !.map((c) => )} - -
- ); - } -} - - -interface PreCoinListProps { - exchangeBaseUrl: string; -} - -class PreCoinList extends ImplicitStateComponent { - precoins = this.makeState(null); - expanded = this.makeState(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
...
; - } - return ( -
- Planchets ({this.precoins() !.length.toString()}) - {" "} - - {this.precoins() !.map((c) => )} - -
- ); - } -} - -interface DenominationListProps { - exchange: ExchangeRecord; -} - -interface ExpanderTextProps { - text: string; -} - -class ExpanderText extends ImplicitStateComponent { - expanded = this.makeState(false); - textArea: any = undefined; - - componentDidUpdate() { - if (this.expanded() && this.textArea) { - this.textArea.focus(); - this.textArea.scrollTop = 0; - } - } - - render(): JSX.Element { - if (!this.expanded()) { - return ( - { this.expanded(true); }}> - {(this.props.text.length <= 10) - ? this.props.text - : ( - - {this.props.text.substring(0,10)} - ... - - ) - } - - ); - } - return ( - - ); - } -} - -class DenominationList extends ImplicitStateComponent { - expanded = this.makeState(false); - denoms = this.makeState(undefined); - - constructor(props: DenominationListProps) { - super(props); - this.update(); - } - - async update() { - let d = await getDenoms(this.props.exchange.baseUrl); - this.denoms(d); - } - - renderDenom(d: DenominationRecord) { - return ( -
-
    -
  • Offered: {d.isOffered ? "yes" : "no"}
  • -
  • Value: {amountToPretty(d.value)}
  • -
  • Withdraw fee: {amountToPretty(d.feeWithdraw)}
  • -
  • Refresh fee: {amountToPretty(d.feeRefresh)}
  • -
  • Deposit fee: {amountToPretty(d.feeDeposit)}
  • -
  • Refund fee: {amountToPretty(d.feeRefund)}
  • -
  • Start: {getTalerStampDate(d.stampStart)!.toString()}
  • -
  • Withdraw expiration: {getTalerStampDate(d.stampExpireWithdraw)!.toString()}
  • -
  • Legal expiration: {getTalerStampDate(d.stampExpireLegal)!.toString()}
  • -
  • Deposit expiration: {getTalerStampDate(d.stampExpireDeposit)!.toString()}
  • -
  • Denom pub:
  • -
-
- ); - } - - render(): JSX.Element { - let denoms = this.denoms() - if (!denoms) { - return ( -
- Denominations (...) - {" "} - - ... - -
- ); - } - return ( -
- Denominations ({denoms.length.toString()}) - {" "} - - {denoms.map((d) => this.renderDenom(d))} - -
- ); - } -} - -class ReserveList extends ImplicitStateComponent { - reserves = this.makeState(null); - expanded = this.makeState(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
...
; - } - return ( -
- Reserves ({this.reserves() !.length.toString()}) - {" "} - - {this.reserves() !.map((r) => )} - -
- ); - } -} - -interface ExchangeProps { - exchange: ExchangeRecord; -} - -class ExchangeView extends React.Component { - render(): JSX.Element { - let e = this.props.exchange; - return ( -
-
    -
  • Exchange Base Url: {this.props.exchange.baseUrl}
  • -
  • Master public key:
  • -
- - - - -
- ); - } -} - -interface ExchangesListState { - exchanges?: ExchangeRecord[]; -} - -class ExchangesList extends React.Component { - 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 ...; - } - return ( -
- Exchanges ({exchanges.length.toString()}): - {exchanges.map(e => )} -
- ); - } -} - -export function main() { - ReactDOM.render(, document.getElementById("container")!); -} - -document.addEventListener("DOMContentLoaded", main); diff --git a/src/renderHtml.tsx b/src/renderHtml.tsx deleted file mode 100644 index 9d14d6cb7..000000000 --- a/src/renderHtml.tsx +++ /dev/null @@ -1,78 +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 - */ - -/** - * Helpers functions to render Taler-related data structures to HTML. - * - * @author Florian Dold - */ - - -/** - * Imports. - */ -import { - AmountJson, - Amounts, - Contract, -} from "./types"; -import * as i18n from "./i18n"; -import { amountToPretty } from "./helpers"; -import * as React from "react"; - - -export function renderContract(contract: Contract): JSX.Element { - let merchantName; - if (contract.merchant && contract.merchant.name) { - merchantName = {contract.merchant.name}; - } else { - merchantName = (pub: {contract.merchant_pub}); - } - let amount = {amountToPretty(contract.amount)}; - - return ( -
- - The merchant {merchantName} - wants to enter a contract over {amount}{" "} - with you. - -

{i18n.str`You are about to purchase:`}

-
    - {contract.products.map( - (p: any, i: number) => (
  • {`${p.description}: ${amountToPretty(p.price)}`}
  • )) - } -
-
- ); -} - - -/** - * Abbreviate a string to a given length, and show the full - * string on hover as a tooltip. - */ -export function abbrev(s: string, n: number = 5) { - let sAbbrev = s; - if (s.length > n) { - sAbbrev = s.slice(0, n) + ".."; - } - return ( - - {sAbbrev} - - ); -} diff --git a/src/style/pure.css b/src/style/pure.css deleted file mode 100644 index 739113970..000000000 --- a/src/style/pure.css +++ /dev/null @@ -1,1508 +0,0 @@ -/*! -Pure v0.6.2 -Copyright 2013 Yahoo! -Licensed under the BSD License. -https://github.com/yahoo/pure/blob/master/LICENSE.md -*/ -/*! -normalize.css v^3.0 | MIT License | git.io/normalize -Copyright (c) Nicolas Gallagher and Jonathan Neal -*/ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS and IE text size adjust after device orientation change, - * without disabling user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability of focused elements when they are also in an - * active/hover state. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - box-sizing: content-box; /* 2 */ -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} - -/*csslint important:false*/ - -/* ========================================================================== - Pure Base Extras - ========================================================================== */ - -/** - * Extra rules that Pure adds on top of Normalize.css - */ - -/** - * Always hide an element when it has the `hidden` HTML attribute. - */ - -.hidden, -[hidden] { - display: none !important; -} - -/** - * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining - * aspect ratio. - */ -.pure-img { - max-width: 100%; - height: auto; - display: block; -} - -/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/ - -.pure-g { - letter-spacing: -0.31em; /* Webkit: collapse white-space between units */ - *letter-spacing: normal; /* reset IE < 8 */ - *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */ - text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */ - - /* - Sets the font stack to fonts known to work properly with the above letter - and word spacings. See: https://github.com/yahoo/pure/issues/41/ - - The following font stack makes Pure Grids work on all known environments. - - * FreeSans: Ships with many Linux distros, including Ubuntu - - * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and - Arial to get picked up by the browser, even though neither is available - in Chrome OS. - - * Droid Sans: Ships with all versions of Android. - - * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows. - */ - font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif; - - /* Use flexbox when possible to avoid `letter-spacing` side-effects. */ - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-flow: row wrap; - -ms-flex-flow: row wrap; - flex-flow: row wrap; - - /* Prevents distributing space between rows */ - -webkit-align-content: flex-start; - -ms-flex-line-pack: start; - align-content: flex-start; -} - -/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside a table; fall back to block and rely on font hack */ -@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - table .pure-g { - display: block; - } -} - -/* Opera as of 12 on Windows needs word-spacing. - The ".opera-only" selector is used to prevent actual prefocus styling - and is not required in markup. -*/ -.opera-only :-o-prefocus, -.pure-g { - word-spacing: -0.43em; -} - -.pure-u { - display: inline-block; - *display: inline; /* IE < 8: fake inline-block */ - zoom: 1; - letter-spacing: normal; - word-spacing: normal; - vertical-align: top; - text-rendering: auto; -} - -/* -Resets the font family back to the OS/browser's default sans-serif font, -this the same font stack that Normalize.css sets for the `body`. -*/ -.pure-g [class *= "pure-u"] { - font-family: sans-serif; -} - -.pure-u-1, -.pure-u-1-1, -.pure-u-1-2, -.pure-u-1-3, -.pure-u-2-3, -.pure-u-1-4, -.pure-u-3-4, -.pure-u-1-5, -.pure-u-2-5, -.pure-u-3-5, -.pure-u-4-5, -.pure-u-5-5, -.pure-u-1-6, -.pure-u-5-6, -.pure-u-1-8, -.pure-u-3-8, -.pure-u-5-8, -.pure-u-7-8, -.pure-u-1-12, -.pure-u-5-12, -.pure-u-7-12, -.pure-u-11-12, -.pure-u-1-24, -.pure-u-2-24, -.pure-u-3-24, -.pure-u-4-24, -.pure-u-5-24, -.pure-u-6-24, -.pure-u-7-24, -.pure-u-8-24, -.pure-u-9-24, -.pure-u-10-24, -.pure-u-11-24, -.pure-u-12-24, -.pure-u-13-24, -.pure-u-14-24, -.pure-u-15-24, -.pure-u-16-24, -.pure-u-17-24, -.pure-u-18-24, -.pure-u-19-24, -.pure-u-20-24, -.pure-u-21-24, -.pure-u-22-24, -.pure-u-23-24, -.pure-u-24-24 { - display: inline-block; - *display: inline; - zoom: 1; - letter-spacing: normal; - word-spacing: normal; - vertical-align: top; - text-rendering: auto; -} - -.pure-u-1-24 { - width: 4.1667%; - *width: 4.1357%; -} - -.pure-u-1-12, -.pure-u-2-24 { - width: 8.3333%; - *width: 8.3023%; -} - -.pure-u-1-8, -.pure-u-3-24 { - width: 12.5000%; - *width: 12.4690%; -} - -.pure-u-1-6, -.pure-u-4-24 { - width: 16.6667%; - *width: 16.6357%; -} - -.pure-u-1-5 { - width: 20%; - *width: 19.9690%; -} - -.pure-u-5-24 { - width: 20.8333%; - *width: 20.8023%; -} - -.pure-u-1-4, -.pure-u-6-24 { - width: 25%; - *width: 24.9690%; -} - -.pure-u-7-24 { - width: 29.1667%; - *width: 29.1357%; -} - -.pure-u-1-3, -.pure-u-8-24 { - width: 33.3333%; - *width: 33.3023%; -} - -.pure-u-3-8, -.pure-u-9-24 { - width: 37.5000%; - *width: 37.4690%; -} - -.pure-u-2-5 { - width: 40%; - *width: 39.9690%; -} - -.pure-u-5-12, -.pure-u-10-24 { - width: 41.6667%; - *width: 41.6357%; -} - -.pure-u-11-24 { - width: 45.8333%; - *width: 45.8023%; -} - -.pure-u-1-2, -.pure-u-12-24 { - width: 50%; - *width: 49.9690%; -} - -.pure-u-13-24 { - width: 54.1667%; - *width: 54.1357%; -} - -.pure-u-7-12, -.pure-u-14-24 { - width: 58.3333%; - *width: 58.3023%; -} - -.pure-u-3-5 { - width: 60%; - *width: 59.9690%; -} - -.pure-u-5-8, -.pure-u-15-24 { - width: 62.5000%; - *width: 62.4690%; -} - -.pure-u-2-3, -.pure-u-16-24 { - width: 66.6667%; - *width: 66.6357%; -} - -.pure-u-17-24 { - width: 70.8333%; - *width: 70.8023%; -} - -.pure-u-3-4, -.pure-u-18-24 { - width: 75%; - *width: 74.9690%; -} - -.pure-u-19-24 { - width: 79.1667%; - *width: 79.1357%; -} - -.pure-u-4-5 { - width: 80%; - *width: 79.9690%; -} - -.pure-u-5-6, -.pure-u-20-24 { - width: 83.3333%; - *width: 83.3023%; -} - -.pure-u-7-8, -.pure-u-21-24 { - width: 87.5000%; - *width: 87.4690%; -} - -.pure-u-11-12, -.pure-u-22-24 { - width: 91.6667%; - *width: 91.6357%; -} - -.pure-u-23-24 { - width: 95.8333%; - *width: 95.8023%; -} - -.pure-u-1, -.pure-u-1-1, -.pure-u-5-5, -.pure-u-24-24 { - width: 100%; -} -.pure-button { - /* Structure */ - display: inline-block; - zoom: 1; - line-height: normal; - white-space: nowrap; - vertical-align: middle; - text-align: center; - cursor: pointer; - -webkit-user-drag: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - box-sizing: border-box; -} - -/* Firefox: Get rid of the inner focus border */ -.pure-button::-moz-focus-inner { - padding: 0; - border: 0; -} - -/* Inherit .pure-g styles */ -.pure-button-group { - letter-spacing: -0.31em; /* Webkit: collapse white-space between units */ - *letter-spacing: normal; /* reset IE < 8 */ - *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */ - text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */ -} - -.opera-only :-o-prefocus, -.pure-button-group { - word-spacing: -0.43em; -} - -.pure-button-group .pure-button { - letter-spacing: normal; - word-spacing: normal; - vertical-align: top; - text-rendering: auto; -} - -/*csslint outline-none:false*/ - -.pure-button { - font-family: inherit; - font-size: 100%; - padding: 0.5em 1em; - color: #444; /* rgba not supported (IE 8) */ - color: rgba(0, 0, 0, 0.80); /* rgba supported */ - border: 1px solid #999; /*IE 6/7/8*/ - border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ - background-color: #E6E6E6; - text-decoration: none; - border-radius: 2px; -} - -.pure-button-hover, -.pure-button:hover, -.pure-button:focus { - /* csslint ignore:start */ - filter: alpha(opacity=90); - /* csslint ignore:end */ - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -.pure-button:focus { - outline: 0; -} -.pure-button-active, -.pure-button:active { - box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20) inset; - border-color: #000\9; -} - -.pure-button[disabled], -.pure-button-disabled, -.pure-button-disabled:hover, -.pure-button-disabled:focus, -.pure-button-disabled:active { - border: none; - background-image: none; - /* csslint ignore:start */ - filter: alpha(opacity=40); - /* csslint ignore:end */ - opacity: 0.40; - cursor: not-allowed; - box-shadow: none; - pointer-events: none; -} - -.pure-button-hidden { - display: none; -} - -.pure-button-primary, -.pure-button-selected, -a.pure-button-primary, -a.pure-button-selected { - background-color: rgb(0, 120, 231); - color: #fff; -} - -/* Button Groups */ -.pure-button-group .pure-button { - margin: 0; - border-radius: 0; - border-right: 1px solid #111; /* fallback color for rgba() for IE7/8 */ - border-right: 1px solid rgba(0, 0, 0, 0.2); - -} - -.pure-button-group .pure-button:first-child { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} -.pure-button-group .pure-button:last-child { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - border-right: none; -} - -/*csslint box-model:false*/ -/* -Box-model set to false because we're setting a height on select elements, which -also have border and padding. This is done because some browsers don't render -the padding. We explicitly set the box-model for select elements to border-box, -so we can ignore the csslint warning. -*/ - -.pure-form input[type="text"], -.pure-form input[type="password"], -.pure-form input[type="email"], -.pure-form input[type="url"], -.pure-form input[type="date"], -.pure-form input[type="month"], -.pure-form input[type="time"], -.pure-form input[type="datetime"], -.pure-form input[type="datetime-local"], -.pure-form input[type="week"], -.pure-form input[type="number"], -.pure-form input[type="search"], -.pure-form input[type="tel"], -.pure-form input[type="color"], -.pure-form select, -.pure-form textarea { - padding: 0.5em 0.6em; - display: inline-block; - border: 1px solid #ccc; - box-shadow: inset 0 1px 3px #ddd; - border-radius: 4px; - vertical-align: middle; - box-sizing: border-box; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -.pure-form input:not([type]) { - padding: 0.5em 0.6em; - display: inline-block; - border: 1px solid #ccc; - box-shadow: inset 0 1px 3px #ddd; - border-radius: 4px; - box-sizing: border-box; -} - - -/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */ -/* May be able to remove this tweak as color inputs become more standardized across browsers. */ -.pure-form input[type="color"] { - padding: 0.2em 0.5em; -} - - -.pure-form input[type="text"]:focus, -.pure-form input[type="password"]:focus, -.pure-form input[type="email"]:focus, -.pure-form input[type="url"]:focus, -.pure-form input[type="date"]:focus, -.pure-form input[type="month"]:focus, -.pure-form input[type="time"]:focus, -.pure-form input[type="datetime"]:focus, -.pure-form input[type="datetime-local"]:focus, -.pure-form input[type="week"]:focus, -.pure-form input[type="number"]:focus, -.pure-form input[type="search"]:focus, -.pure-form input[type="tel"]:focus, -.pure-form input[type="color"]:focus, -.pure-form select:focus, -.pure-form textarea:focus { - outline: 0; - border-color: #129FEA; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -.pure-form input:not([type]):focus { - outline: 0; - border-color: #129FEA; -} - -.pure-form input[type="file"]:focus, -.pure-form input[type="radio"]:focus, -.pure-form input[type="checkbox"]:focus { - outline: thin solid #129FEA; - outline: 1px auto #129FEA; -} -.pure-form .pure-checkbox, -.pure-form .pure-radio { - margin: 0.5em 0; - display: block; -} - -.pure-form input[type="text"][disabled], -.pure-form input[type="password"][disabled], -.pure-form input[type="email"][disabled], -.pure-form input[type="url"][disabled], -.pure-form input[type="date"][disabled], -.pure-form input[type="month"][disabled], -.pure-form input[type="time"][disabled], -.pure-form input[type="datetime"][disabled], -.pure-form input[type="datetime-local"][disabled], -.pure-form input[type="week"][disabled], -.pure-form input[type="number"][disabled], -.pure-form input[type="search"][disabled], -.pure-form input[type="tel"][disabled], -.pure-form input[type="color"][disabled], -.pure-form select[disabled], -.pure-form textarea[disabled] { - cursor: not-allowed; - background-color: #eaeded; - color: #cad2d3; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -.pure-form input:not([type])[disabled] { - cursor: not-allowed; - background-color: #eaeded; - color: #cad2d3; -} -.pure-form input[readonly], -.pure-form select[readonly], -.pure-form textarea[readonly] { - background-color: #eee; /* menu hover bg color */ - color: #777; /* menu text color */ - border-color: #ccc; -} - -.pure-form input:focus:invalid, -.pure-form textarea:focus:invalid, -.pure-form select:focus:invalid { - color: #b94a48; - border-color: #e9322d; -} -.pure-form input[type="file"]:focus:invalid:focus, -.pure-form input[type="radio"]:focus:invalid:focus, -.pure-form input[type="checkbox"]:focus:invalid:focus { - outline-color: #e9322d; -} -.pure-form select { - /* Normalizes the height; padding is not sufficient. */ - height: 2.25em; - border: 1px solid #ccc; - background-color: white; -} -.pure-form select[multiple] { - height: auto; -} -.pure-form label { - margin: 0.5em 0 0.2em; -} -.pure-form fieldset { - margin: 0; - padding: 0.35em 0 0.75em; - border: 0; -} -.pure-form legend { - display: block; - width: 100%; - padding: 0.3em 0; - margin-bottom: 0.3em; - color: #333; - border-bottom: 1px solid #e5e5e5; -} - -.pure-form-stacked input[type="text"], -.pure-form-stacked input[type="password"], -.pure-form-stacked input[type="email"], -.pure-form-stacked input[type="url"], -.pure-form-stacked input[type="date"], -.pure-form-stacked input[type="month"], -.pure-form-stacked input[type="time"], -.pure-form-stacked input[type="datetime"], -.pure-form-stacked input[type="datetime-local"], -.pure-form-stacked input[type="week"], -.pure-form-stacked input[type="number"], -.pure-form-stacked input[type="search"], -.pure-form-stacked input[type="tel"], -.pure-form-stacked input[type="color"], -.pure-form-stacked input[type="file"], -.pure-form-stacked select, -.pure-form-stacked label, -.pure-form-stacked textarea { - display: block; - margin: 0.25em 0; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -.pure-form-stacked input:not([type]) { - display: block; - margin: 0.25em 0; -} -.pure-form-aligned input, -.pure-form-aligned textarea, -.pure-form-aligned select, -/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ -.pure-form-aligned .pure-help-inline, -.pure-form-message-inline { - display: inline-block; - *display: inline; - *zoom: 1; - vertical-align: middle; -} -.pure-form-aligned textarea { - vertical-align: top; -} - -/* Aligned Forms */ -.pure-form-aligned .pure-control-group { - margin-bottom: 0.5em; -} -.pure-form-aligned .pure-control-group label { - text-align: right; - display: inline-block; - vertical-align: middle; - width: 10em; - margin: 0 1em 0 0; -} -.pure-form-aligned .pure-controls { - margin: 1.5em 0 0 11em; -} - -/* Rounded Inputs */ -.pure-form input.pure-input-rounded, -.pure-form .pure-input-rounded { - border-radius: 2em; - padding: 0.5em 1em; -} - -/* Grouped Inputs */ -.pure-form .pure-group fieldset { - margin-bottom: 10px; -} -.pure-form .pure-group input, -.pure-form .pure-group textarea { - display: block; - padding: 10px; - margin: 0 0 -1px; - border-radius: 0; - position: relative; - top: -1px; -} -.pure-form .pure-group input:focus, -.pure-form .pure-group textarea:focus { - z-index: 3; -} -.pure-form .pure-group input:first-child, -.pure-form .pure-group textarea:first-child { - top: 1px; - border-radius: 4px 4px 0 0; - margin: 0; -} -.pure-form .pure-group input:first-child:last-child, -.pure-form .pure-group textarea:first-child:last-child { - top: 1px; - border-radius: 4px; - margin: 0; -} -.pure-form .pure-group input:last-child, -.pure-form .pure-group textarea:last-child { - top: -2px; - border-radius: 0 0 4px 4px; - margin: 0; -} -.pure-form .pure-group button { - margin: 0.35em 0; -} - -.pure-form .pure-input-1 { - width: 100%; -} -.pure-form .pure-input-3-4 { - width: 75%; -} -.pure-form .pure-input-2-3 { - width: 66%; -} -.pure-form .pure-input-1-2 { - width: 50%; -} -.pure-form .pure-input-1-3 { - width: 33%; -} -.pure-form .pure-input-1-4 { - width: 25%; -} - -/* Inline help for forms */ -/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ -.pure-form .pure-help-inline, -.pure-form-message-inline { - display: inline-block; - padding-left: 0.3em; - color: #666; - vertical-align: middle; - font-size: 0.875em; -} - -/* Block help for forms */ -.pure-form-message { - display: block; - color: #666; - font-size: 0.875em; -} - -@media only screen and (max-width : 480px) { - .pure-form button[type="submit"] { - margin: 0.7em 0 0; - } - - .pure-form input:not([type]), - .pure-form input[type="text"], - .pure-form input[type="password"], - .pure-form input[type="email"], - .pure-form input[type="url"], - .pure-form input[type="date"], - .pure-form input[type="month"], - .pure-form input[type="time"], - .pure-form input[type="datetime"], - .pure-form input[type="datetime-local"], - .pure-form input[type="week"], - .pure-form input[type="number"], - .pure-form input[type="search"], - .pure-form input[type="tel"], - .pure-form input[type="color"], - .pure-form label { - margin-bottom: 0.3em; - display: block; - } - - .pure-group input:not([type]), - .pure-group input[type="text"], - .pure-group input[type="password"], - .pure-group input[type="email"], - .pure-group input[type="url"], - .pure-group input[type="date"], - .pure-group input[type="month"], - .pure-group input[type="time"], - .pure-group input[type="datetime"], - .pure-group input[type="datetime-local"], - .pure-group input[type="week"], - .pure-group input[type="number"], - .pure-group input[type="search"], - .pure-group input[type="tel"], - .pure-group input[type="color"] { - margin-bottom: 0; - } - - .pure-form-aligned .pure-control-group label { - margin-bottom: 0.3em; - text-align: left; - display: block; - width: 100%; - } - - .pure-form-aligned .pure-controls { - margin: 1.5em 0 0 0; - } - - /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ - .pure-form .pure-help-inline, - .pure-form-message-inline, - .pure-form-message { - display: block; - font-size: 0.75em; - /* Increased bottom padding to make it group with its related input element. */ - padding: 0.2em 0 0.8em; - } -} - -/*csslint adjoining-classes: false, box-model:false*/ -.pure-menu { - box-sizing: border-box; -} - -.pure-menu-fixed { - position: fixed; - left: 0; - top: 0; - z-index: 3; -} - -.pure-menu-list, -.pure-menu-item { - position: relative; -} - -.pure-menu-list { - list-style: none; - margin: 0; - padding: 0; -} - -.pure-menu-item { - padding: 0; - margin: 0; - height: 100%; -} - -.pure-menu-link, -.pure-menu-heading { - display: block; - text-decoration: none; - white-space: nowrap; -} - -/* HORIZONTAL MENU */ -.pure-menu-horizontal { - width: 100%; - white-space: nowrap; -} - -.pure-menu-horizontal .pure-menu-list { - display: inline-block; -} - -/* Initial menus should be inline-block so that they are horizontal */ -.pure-menu-horizontal .pure-menu-item, -.pure-menu-horizontal .pure-menu-heading, -.pure-menu-horizontal .pure-menu-separator { - display: inline-block; - *display: inline; - zoom: 1; - vertical-align: middle; -} - -/* Submenus should still be display: block; */ -.pure-menu-item .pure-menu-item { - display: block; -} - -.pure-menu-children { - display: none; - position: absolute; - left: 100%; - top: 0; - margin: 0; - padding: 0; - z-index: 3; -} - -.pure-menu-horizontal .pure-menu-children { - left: 0; - top: auto; - width: inherit; -} - -.pure-menu-allow-hover:hover > .pure-menu-children, -.pure-menu-active > .pure-menu-children { - display: block; - position: absolute; -} - -/* Vertical Menus - show the dropdown arrow */ -.pure-menu-has-children > .pure-menu-link:after { - padding-left: 0.5em; - content: "\25B8"; - font-size: small; -} - -/* Horizontal Menus - show the dropdown arrow */ -.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after { - content: "\25BE"; -} - -/* scrollable menus */ -.pure-menu-scrollable { - overflow-y: scroll; - overflow-x: hidden; -} - -.pure-menu-scrollable .pure-menu-list { - display: block; -} - -.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list { - display: inline-block; -} - -.pure-menu-horizontal.pure-menu-scrollable { - white-space: nowrap; - overflow-y: hidden; - overflow-x: auto; - -ms-overflow-style: none; - -webkit-overflow-scrolling: touch; - /* a little extra padding for this style to allow for scrollbars */ - padding: .5em 0; -} - -.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar { - display: none; -} - -/* misc default styling */ - -.pure-menu-separator, -.pure-menu-horizontal .pure-menu-children .pure-menu-separator { - background-color: #ccc; - height: 1px; - margin: .3em 0; -} - -.pure-menu-horizontal .pure-menu-separator { - width: 1px; - height: 1.3em; - margin: 0 .3em ; -} - -/* Need to reset the separator since submenu is vertical */ -.pure-menu-horizontal .pure-menu-children .pure-menu-separator { - display: block; - width: auto; -} - -.pure-menu-heading { - text-transform: uppercase; - color: #565d64; -} - -.pure-menu-link { - color: #777; -} - -.pure-menu-children { - background-color: #fff; -} - -.pure-menu-link, -.pure-menu-disabled, -.pure-menu-heading { - padding: .5em 1em; -} - -.pure-menu-disabled { - opacity: .5; -} - -.pure-menu-disabled .pure-menu-link:hover { - background-color: transparent; -} - -.pure-menu-active > .pure-menu-link, -.pure-menu-link:hover, -.pure-menu-link:focus { - background-color: #eee; -} - -.pure-menu-selected .pure-menu-link, -.pure-menu-selected .pure-menu-link:visited { - color: #000; -} - -.pure-table { - /* Remove spacing between table cells (from Normalize.css) */ - border-collapse: collapse; - border-spacing: 0; - empty-cells: show; - border: 1px solid #cbcbcb; -} - -.pure-table caption { - color: #000; - font: italic 85%/1 arial, sans-serif; - padding: 1em 0; - text-align: center; -} - -.pure-table td, -.pure-table th { - border-left: 1px solid #cbcbcb;/* inner column border */ - border-width: 0 0 0 1px; - font-size: inherit; - margin: 0; - overflow: visible; /*to make ths where the title is really long work*/ - padding: 0.5em 1em; /* cell padding */ -} - -/* Consider removing this next declaration block, as it causes problems when -there's a rowspan on the first cell. Case added to the tests. issue#432 */ -.pure-table td:first-child, -.pure-table th:first-child { - border-left-width: 0; -} - -.pure-table thead { - background-color: #e0e0e0; - color: #000; - text-align: left; - vertical-align: bottom; -} - -/* -striping: - even - #fff (white) - odd - #f2f2f2 (light gray) -*/ -.pure-table td { - background-color: transparent; -} -.pure-table-odd td { - background-color: #f2f2f2; -} - -/* nth-child selector for modern browsers */ -.pure-table-striped tr:nth-child(2n-1) td { - background-color: #f2f2f2; -} - -/* BORDERED TABLES */ -.pure-table-bordered td { - border-bottom: 1px solid #cbcbcb; -} -.pure-table-bordered tbody > tr:last-child > td { - border-bottom-width: 0; -} - - -/* HORIZONTAL BORDERED TABLES */ - -.pure-table-horizontal td, -.pure-table-horizontal th { - border-width: 0 0 1px 0; - border-bottom: 1px solid #cbcbcb; -} -.pure-table-horizontal tbody > tr:last-child > td { - border-bottom-width: 0; -} diff --git a/src/style/wallet.css b/src/style/wallet.css deleted file mode 100644 index 752fc6d75..000000000 --- a/src/style/wallet.css +++ /dev/null @@ -1,222 +0,0 @@ -#main { - border: solid 1px black; - border-radius: 10px; - margin-left: auto; - margin-right: auto; - margin-top: 2em; - max-width: 50%; - padding: 2em; -} - -header { - width: 100%; - height: 100px; - margin: 0; - padding: 0; - border-bottom: 1px solid black; -} - -header h1 { - font-size: 200%; - margin: 0; - padding: 0 0 0 120px; - position: relative; - top: 50%; - transform: translateY(-50%); -} - -header #logo { - float: left; - width: 100px; - height: 100px; - padding: 0; - margin: 0; - text-align: center; - border-right: 1px solid black; - background-image: url(/img/logo.png); - background-size: 100px; -} - -aside { - width: 100px; - float: left; -} - -section#main { - margin: auto; - padding: 20px; - border-left: 1px solid black; - height: 100%; - max-width: 50%; -} - -section#main h1:first-child { - margin-top: 0; -} - -h1 { - font-size: 160%; -} - -h2 { - font-size: 140%; -} - -h3 { - font-size: 120%; -} - -h4, h5, h6 { - font-size: 100%; -} - -.form-row { - padding-top: 5px; - padding-bottom: 5px; -} - -label { - padding-right: 1em; -} - -label::after { - content: ":"; -} - -input.url { - width: 25em; -} - -.formish { -} - -.json-key { - color: brown; -} -.json-value { - color: navy; - } -.json-string { - color: olive; -} - -button { - font-size: 120%; - padding: 0.5em; -} - -button.confirm-pay { - float: right; -} - -/* We use fading to hide slower DOM updates */ -.fade { - -webkit-animation: fade 0.7s; - animation: fade 0.7s; - opacity: 1; -} - -@-webkit-keyframes fade { - from {opacity: 0} - to {opacity: 1} -} -@keyframes fade { - from {opacity: 0} - to {opacity: 1} - } - -button.linky { - background:none!important; - border:none; - padding:0!important; - - font-family:arial,sans-serif; - color:#069; - text-decoration:underline; - cursor:pointer; -} - -table, th, td { - border: 1px solid black; -} - - -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; -} - -.button-success, -.button-destructive, -.button-warning, -.button-secondary { - color: white; - border-radius: 4px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -} - -.button-success { - background: rgb(28, 184, 65); -} - -.button-destructive { - background: rgb(202, 60, 60); -} - -.button-warning { - background: rgb(223, 117, 20); -} - -.button-secondary { - background: rgb(66, 184, 221); -} - -a.actionLink { - color: black; -} diff --git a/src/webex/background.html b/src/webex/background.html new file mode 100644 index 000000000..0535dd5f3 --- /dev/null +++ b/src/webex/background.html @@ -0,0 +1,11 @@ + + + + + + (wallet bg page) + + + + + diff --git a/src/webex/background.ts b/src/webex/background.ts new file mode 100644 index 000000000..3c63f323e --- /dev/null +++ b/src/webex/background.ts @@ -0,0 +1,30 @@ +/* + This file is part of TALER + (C) 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 + */ + +/** + * Entry point for the background page. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import {wxMain} from "./wxBackend"; + +window.addEventListener("load", () => { + wxMain(); +}); diff --git a/src/webex/chromeBadge.ts b/src/webex/chromeBadge.ts new file mode 100644 index 000000000..13add9b3f --- /dev/null +++ b/src/webex/chromeBadge.ts @@ -0,0 +1,225 @@ +/* + 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 + */ + +import { + Badge, +} from "../wallet"; + + +/** + * Polyfill for requestAnimationFrame, which + * doesn't work from a background page. + */ +function rAF(cb: (ts: number) => void) { + window.setTimeout(() => { + cb(performance.now()); + }, 100 /* 100 ms delay between frames */); +} + + +/** + * Badge for Chrome that renders a Taler logo with a rotating ring if some + * background activity is happening. + */ +export class ChromeBadge implements Badge { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + /** + * True if animation running. The animation + * might still be running even if we're not busy anymore, + * just to transition to the "normal" state in a animated way. + */ + private animationRunning: boolean = false; + + /** + * Is the wallet still busy? Note that we do not stop the + * animation immediately when the wallet goes idle, but + * instead slowly close the gap. + */ + private isBusy: boolean = false; + + /** + * Current rotation angle, ranges from 0 to rotationAngleMax. + */ + private rotationAngle: number = 0; + + /** + * While animating, how wide is the current gap in the circle? + * Ranges from 0 to openMax. + */ + private gapWidth: number = 0; + + /** + * Maximum value for our rotationAngle, corresponds to 2 Pi. + */ + static rotationAngleMax = 1000; + + /** + * How fast do we rotate? Given in rotation angle (relative to rotationAngleMax) per millisecond. + */ + static rotationSpeed = 0.5; + + /** + * How fast to we open? Given in rotation angle (relative to rotationAngleMax) per millisecond. + */ + static openSpeed = 0.15; + + /** + * How fast to we close? Given as a multiplication factor per frame update. + */ + static closeSpeed = 0.7; + + /** + * How far do we open? Given relative to rotationAngleMax. + */ + static openMax = 100; + + constructor(window?: Window) { + // Allow injecting another window for testing + const bg = window || chrome.extension.getBackgroundPage(); + if (!bg) { + throw Error("no window available"); + } + this.canvas = bg.document.createElement("canvas"); + // Note: changing the width here means changing the font + // size in draw() as well! + this.canvas.width = 32; + this.canvas.height = 32; + this.ctx = this.canvas.getContext("2d")!; + this.draw(); + } + + /** + * Draw the badge based on the current state. + */ + private draw() { + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); + + this.ctx.beginPath(); + this.ctx.arc(0, 0, this.canvas.width / 2 - 2, 0, 2 * Math.PI); + this.ctx.fillStyle = "white"; + this.ctx.fill(); + + // move into the center, off by 2 for aligning the "T" with the bottom + // of the circle. + this.ctx.translate(0, 2); + + // pick sans-serif font; note: 14px is based on the 32px width above! + this.ctx.font = "bold 24px sans-serif"; + // draw the "T" perfectly centered (x and y) to the current position + this.ctx.textAlign = "center"; + this.ctx.textBaseline = "middle"; + this.ctx.fillStyle = "black"; + this.ctx.fillText("T", 0, 0); + // now move really into the center + this.ctx.translate(0, -2); + // start drawing the (possibly open) circle + this.ctx.beginPath(); + this.ctx.lineWidth = 2.5; + if (this.animationRunning) { + /* Draw circle around the "T" with an opening of this.gapWidth */ + const aMax = ChromeBadge.rotationAngleMax; + const startAngle = this.rotationAngle / aMax * Math.PI * 2; + const stopAngle = ((this.rotationAngle + aMax - this.gapWidth) / aMax) * Math.PI * 2; + this.ctx.arc(0, 0, this.canvas.width / 2 - 2, /* radius */ startAngle, stopAngle, false); + } else { + /* Draw full circle */ + this.ctx.arc(0, 0, + this.canvas.width / 2 - 2, /* radius */ + 0, + Math.PI * 2, + false); + } + this.ctx.stroke(); + // go back to the origin + this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); + + // Allow running outside the extension for testing + // tslint:disable-next-line:no-string-literal + if (window["chrome"] && window.chrome["browserAction"]) { + try { + const imageData = this.ctx.getImageData(0, + 0, + this.canvas.width, + this.canvas.height); + chrome.browserAction.setIcon({imageData}); + } catch (e) { + // Might fail if browser has over-eager canvas fingerprinting countermeasures. + // There's nothing we can do then ... + } + } + } + + private animate() { + if (this.animationRunning) { + return; + } + this.animationRunning = true; + let start: number|undefined; + const step = (timestamp: number) => { + if (!this.animationRunning) { + return; + } + if (!start) { + start = timestamp; + } + if (!this.isBusy && 0 === this.gapWidth) { + // stop if we're close enough to origin + this.rotationAngle = 0; + } else { + this.rotationAngle = (this.rotationAngle + (timestamp - start) * + ChromeBadge.rotationSpeed) % ChromeBadge.rotationAngleMax; + } + if (this.isBusy) { + if (this.gapWidth < ChromeBadge.openMax) { + this.gapWidth += ChromeBadge.openSpeed * (timestamp - start); + } + if (this.gapWidth > ChromeBadge.openMax) { + this.gapWidth = ChromeBadge.openMax; + } + } else { + if (this.gapWidth > 0) { + this.gapWidth--; + this.gapWidth *= ChromeBadge.closeSpeed; + } + } + + if (this.isBusy || this.gapWidth > 0) { + start = timestamp; + rAF(step); + } else { + this.animationRunning = false; + } + this.draw(); + }; + rAF(step); + } + + startBusy() { + if (this.isBusy) { + return; + } + this.isBusy = true; + this.animate(); + } + + stopBusy() { + this.isBusy = false; + } +} diff --git a/src/webex/components.ts b/src/webex/components.ts new file mode 100644 index 000000000..1f5d18731 --- /dev/null +++ b/src/webex/components.ts @@ -0,0 +1,63 @@ +/* + 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 + */ + + +/** + * 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; + (newState: T): void; +} + +/** + * Component that doesn't hold its state in one object, + * but has multiple state holders. + */ +export abstract class ImplicitStateComponent extends React.Component { + private _implicit = {needsUpdate: false, didMount: false}; + componentDidMount() { + this._implicit.didMount = true; + if (this._implicit.needsUpdate) { + this.setState({} as any); + } + } + makeState(initial: StateType): StateHolder { + 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/notify.ts b/src/webex/notify.ts new file mode 100644 index 000000000..733367a59 --- /dev/null +++ b/src/webex/notify.ts @@ -0,0 +1,571 @@ +/* + 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 + */ + +// tslint:disable:no-unused-expression + +/** + * Module that is injected into (all!) pages to allow them + * to interact with the GNU Taler wallet via DOM Events. + */ + + +/** + * Imports. + */ +import URI = require("urijs"); + +declare var cloneInto: any; + +const PROTOCOL_VERSION = 1; + +let logVerbose: boolean = false; +try { + logVerbose = !!localStorage.getItem("taler-log-verbose"); +} catch (e) { + // can't read from local storage +} + +if (document.documentElement.getAttribute("data-taler-nojs")) { + document.dispatchEvent(new Event("taler-probe-result")); +} + + +function subst(url: string, H_contract: string) { + url = url.replace("${H_contract}", H_contract); + url = url.replace("${$}", "$"); + return url; +} + +interface Handler { + type: string; + listener: (e: CustomEvent) => void|Promise; +} +const handlers: Handler[] = []; + +function hashContract(contract: string): Promise { + const walletHashContractMsg = { + detail: {contract}, + type: "hash-contract", + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { + if (!resp.hash) { + console.log("error", resp); + reject(Error("hashing failed")); + } + resolve(resp.hash); + }); + }); +} + +function queryPayment(url: string): Promise { + const walletMsg = { + detail: { url }, + type: "query-payment", + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(resp); + }); + }); +} + +function putHistory(historyEntry: any): Promise { + const walletMsg = { + detail: { + historyEntry, + }, + type: "put-history-entry", + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(); + }); + }); +} + +function saveOffer(offer: any): Promise { + const walletMsg = { + detail: { + offer: { + H_contract: offer.hash, + contract: offer.data, + merchant_sig: offer.sig, + offer_time: new Date().getTime() / 1000, + }, + type: "save-offer", + }, + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + if (resp && resp.error) { + reject(resp); + } else { + resolve(resp); + } + }); + }); +} + + +let sheet: CSSStyleSheet|null; + +function initStyle() { + logVerbose && console.log("taking over styles"); + const name = "taler-presence-stylesheet"; + const content = "/* Taler stylesheet controlled by JS */"; + let style = document.getElementById(name) as HTMLStyleElement|null; + if (!style) { + style = document.createElement("style"); + // Needed by WebKit + style.appendChild(document.createTextNode(content)); + style.id = name; + document.head.appendChild(style); + sheet = style.sheet as CSSStyleSheet; + } else { + // We've taken over the stylesheet now, + // make it clear by clearing all the rules in it + // and making it obvious in the DOM. + if (style.tagName.toLowerCase() === "style") { + style.innerText = content; + } + if (!style.sheet) { + throw Error("taler-presence-stylesheet should be a style sheet ( or + + +
+ + diff --git a/src/webex/pages/add-auditor.tsx b/src/webex/pages/add-auditor.tsx new file mode 100644 index 000000000..c1a9f997f --- /dev/null +++ b/src/webex/pages/add-auditor.tsx @@ -0,0 +1,126 @@ +/* + This file is part of TALER + (C) 2017 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 + */ + +/** + * View and edit auditors. + * + * @author Florian Dold + */ + + +import { getTalerStampDate } from "../../helpers"; +import { + ExchangeRecord, + DenominationRecord, + AuditorRecord, + CurrencyRecord, + ReserveRecord, + CoinRecord, + PreCoinRecord, + Denomination +} from "../../types"; + +import { ImplicitStateComponent, StateHolder } from "../components"; +import { + getCurrencies, + updateCurrency, +} from "../wxApi"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); + +interface ConfirmAuditorProps { + url: string; + currency: string; + auditorPub: string; + expirationStamp: number; +} + +class ConfirmAuditor extends ImplicitStateComponent { + addDone: StateHolder = this.makeState(false); + constructor() { + super(); + } + + async add() { + let currencies = await getCurrencies(); + let currency: CurrencyRecord|undefined = undefined; + + for (let c of currencies) { + if (c.name == this.props.currency) { + currency = c; + } + } + + if (!currency) { + currency = { name: this.props.currency, auditors: [], fractionalDigits: 2, exchanges: [] }; + } + + let newAuditor = { auditorPub: this.props.auditorPub, baseUrl: this.props.url, expirationStamp: this.props.expirationStamp }; + + let auditorFound = false; + for (let idx in currency.auditors) { + let a = currency.auditors[idx]; + if (a.baseUrl == this.props.url) { + auditorFound = true; + // Update auditor if already found by URL. + currency.auditors[idx] = newAuditor; + } + } + + if (!auditorFound) { + currency.auditors.push(newAuditor); + } + + await updateCurrency(currency); + + this.addDone(true); + } + + back() { + window.history.back(); + } + + render(): JSX.Element { + return ( +
+

Do you want to let {this.props.auditorPub} audit the currency "{this.props.currency}"?

+ {this.addDone() ? + (
Auditor was added! You can also view and edit auditors.
) + : + (
+ + +
) + } +
+ ); + } +} + +export function main() { + const walletPageUrl = new URI(document.location.href); + 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(, document.getElementById("container")!); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/webex/pages/auditors.html b/src/webex/pages/auditors.html new file mode 100644 index 000000000..cbfc3b4b5 --- /dev/null +++ b/src/webex/pages/auditors.html @@ -0,0 +1,36 @@ + + + + + + Taler Wallet: Auditors + + + + + + + + + + + +
+ + diff --git a/src/webex/pages/auditors.tsx b/src/webex/pages/auditors.tsx new file mode 100644 index 000000000..dac3c2be9 --- /dev/null +++ b/src/webex/pages/auditors.tsx @@ -0,0 +1,147 @@ +/* + This file is part of TALER + (C) 2017 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 + */ + +/** + * View and edit auditors. + * + * @author Florian Dold + */ + + +import { getTalerStampDate } from "../../helpers"; +import { + ExchangeRecord, + ExchangeForCurrencyRecord, + DenominationRecord, + AuditorRecord, + CurrencyRecord, + ReserveRecord, + CoinRecord, + PreCoinRecord, + Denomination +} from "../../types"; + +import { ImplicitStateComponent, StateHolder } from "../components"; +import { + getCurrencies, + updateCurrency, +} from "../wxApi"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +interface CurrencyListState { + currencies?: CurrencyRecord[]; +} + +class CurrencyList extends React.Component { + 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 currencies = await getCurrencies(); + console.log("currencies: ", currencies); + this.setState({ currencies }); + } + + async confirmRemoveAuditor(c: CurrencyRecord, a: AuditorRecord) { + if (window.confirm(`Do you really want to remove auditor ${a.baseUrl} for currency ${c.name}?`)) { + c.auditors = c.auditors.filter((x) => x.auditorPub != a.auditorPub); + await updateCurrency(c); + } + } + + async confirmRemoveExchange(c: CurrencyRecord, e: ExchangeForCurrencyRecord) { + if (window.confirm(`Do you really want to remove exchange ${e.baseUrl} for currency ${c.name}?`)) { + c.exchanges = c.exchanges.filter((x) => x.baseUrl != e.baseUrl); + await updateCurrency(c); + } + } + + renderAuditors(c: CurrencyRecord): any { + if (c.auditors.length == 0) { + return

No trusted auditors for this currency.

+ } + return ( +
+

Trusted Auditors:

+
    + {c.auditors.map(a => ( +
  • {a.baseUrl} +
      +
    • valid until {new Date(a.expirationStamp).toString()}
    • +
    • public key {a.auditorPub}
    • +
    +
  • + ))} +
+
+ ); + } + + renderExchanges(c: CurrencyRecord): any { + if (c.exchanges.length == 0) { + return

No trusted exchanges for this currency.

+ } + return ( +
+

Trusted Exchanges:

+
    + {c.exchanges.map(e => ( +
  • {e.baseUrl} +
  • + ))} +
+
+ ); + } + + render(): JSX.Element { + let currencies = this.state.currencies; + if (!currencies) { + return ...; + } + return ( +
+ {currencies.map(c => ( +
+

Currency {c.name}

+

Displayed with {c.fractionalDigits} fractional digits.

+

Auditors

+
{this.renderAuditors(c)}
+

Exchanges

+
{this.renderExchanges(c)}
+
+ ))} +
+ ); + } +} + +export function main() { + ReactDOM.render(, document.getElementById("container")!); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/webex/pages/confirm-contract.html b/src/webex/pages/confirm-contract.html new file mode 100644 index 000000000..6713b2e2c --- /dev/null +++ b/src/webex/pages/confirm-contract.html @@ -0,0 +1,69 @@ + + + + + + Taler Wallet: Confirm Reserve Creation + + + + + + + + + + + + +
+

GNU Taler Wallet

+
+
+ + + diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx new file mode 100644 index 000000000..011df27a1 --- /dev/null +++ b/src/webex/pages/confirm-contract.tsx @@ -0,0 +1,242 @@ +/* + 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 + */ + +/** + * Page shown to the user to confirm entering + * a contract. + */ + + +/** + * Imports. + */ +import * as i18n from "../../i18n"; +import { Contract, AmountJson, ExchangeRecord } from "../../types"; +import { OfferRecord } from "../../wallet"; + +import { renderContract } from "../renderHtml"; +import { getExchanges } from "../wxApi"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); + + +interface DetailState { + collapsed: boolean; +} + +interface DetailProps { + contract: Contract + collapsed: boolean + exchanges: null|ExchangeRecord[]; +} + + +class Details extends React.Component { + constructor(props: DetailProps) { + super(props); + console.log("new Details component created"); + this.state = { + collapsed: props.collapsed, + }; + + console.log("initial state:", this.state); + } + + render() { + if (this.state.collapsed) { + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ {i18n.str`Accepted exchanges:`} +
    + {this.props.contract.exchanges.map( + e =>
  • {`${e.url}: ${e.master_pub}`}
  • )} +
+ {i18n.str`Exchanges in the wallet:`} +
    + {(this.props.exchanges || []).map( + (e: ExchangeRecord) => +
  • {`${e.baseUrl}: ${e.masterPublicKey}`}
  • )} +
+
+
); + } + } +} + +interface ContractPromptProps { + offerId: number; +} + +interface ContractPromptState { + offer: OfferRecord|null; + error: string|null; + payDisabled: boolean; + exchanges: null|ExchangeRecord[]; +} + +class ContractPrompt extends React.Component { + constructor() { + super(); + this.state = { + offer: null, + error: null, + payDisabled: true, + exchanges: null + } + } + + componentWillMount() { + this.update(); + } + + componentWillUnmount() { + // FIXME: abort running ops + } + + async update() { + let offer = await this.getOffer(); + this.setState({offer} as any); + this.checkPayment(); + let exchanges = await getExchanges(); + this.setState({exchanges} as any); + } + + getOffer(): Promise { + 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": + let msgInsufficient = i18n.str`You have insufficient funds of the requested currency in your wallet.`; + let msgNoMatch = i18n.str`You do not have any funds from an exchange that is accepted by this merchant. None of the exchanges accepted by the merchant is known to your wallet.`; + if (this.state.exchanges && this.state.offer) { + let acceptedExchangePubs = this.state.offer.contract.exchanges.map((e) => e.master_pub); + let ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0); + if (ex) { + this.setState({error: msgInsufficient}); + } else { + this.setState({error: msgNoMatch}); + } + } else { + this.setState({error: msgInsufficient}); + } + break; + default: + this.setState({error: `Error: ${resp.error}`}); + break; + } + this.setState({payDisabled: true}); + } else { + this.setState({payDisabled: false, 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.setState({error: "You do not have enough coins of the requested currency."}); + break; + default: + this.setState({error: `Error: ${resp.error}`}); + break; + } + return; + } + let c = d.offer!.contract; + console.log("contract", c); + document.location.href = c.fulfillment_url; + }); + } + + + render() { + if (!this.state.offer) { + return ...; + } + let c = this.state.offer.contract; + return ( +
+
+ {renderContract(c)} +
+ +
+ {(this.state.error ?

{this.state.error}

:

)} +

+
+
+ ); + } +} + + +document.addEventListener("DOMContentLoaded", () => { + let url = new URI(document.location.href); + let query: any = URI.parseQuery(url.query()); + let offerId = JSON.parse(query.offerId); + + ReactDOM.render(, document.getElementById( + "contract")!); +}); diff --git a/src/webex/pages/confirm-create-reserve.html b/src/webex/pages/confirm-create-reserve.html new file mode 100644 index 000000000..16ab12a30 --- /dev/null +++ b/src/webex/pages/confirm-create-reserve.html @@ -0,0 +1,52 @@ + + + + + + Taler Wallet: Select Taler Provider + + + + + + + + + + + + + +
+

GNU Taler Wallet

+
+
+ + + diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx new file mode 100644 index 000000000..6ece92e21 --- /dev/null +++ b/src/webex/pages/confirm-create-reserve.tsx @@ -0,0 +1,641 @@ +/* + 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 + */ + + +/** + * Page shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +import {amountToPretty, canonicalizeBaseUrl} from "../../helpers"; +import { + AmountJson, CreateReserveResponse, + ReserveCreationInfo, Amounts, + Denomination, DenominationRecord, CurrencyRecord +} from "../../types"; +import * as i18n from "../../i18n"; + +import {getReserveCreationInfo, getCurrency, getExchangeInfo} from "../wxApi"; +import {ImplicitStateComponent, StateHolder} from "../components"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); +import * as moment from "moment"; + + +function delay(delayMs: number, value: T): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(value), delayMs); + }); +} + +class EventTrigger { + triggerResolve: any; + triggerPromise: Promise; + + constructor() { + this.reset(); + } + + private reset() { + this.triggerPromise = new Promise((resolve, reject) => { + this.triggerResolve = resolve; + }); + } + + trigger() { + this.triggerResolve(false); + this.reset(); + } + + async wait(delayMs: number): Promise { + return await Promise.race([this.triggerPromise, delay(delayMs, true)]); + } +} + + +interface CollapsibleState { + collapsed: boolean; +} + +interface CollapsibleProps { + initiallyCollapsed: boolean; + title: string; +} + +class Collapsible extends React.Component { + constructor(props: CollapsibleProps) { + super(props); + this.state = { collapsed: props.initiallyCollapsed }; + } + render() { + const doOpen = (e: any) => { + this.setState({collapsed: false}) + e.preventDefault() + }; + const doClose = (e: any) => { + this.setState({collapsed: true}) + e.preventDefault(); + }; + if (this.state.collapsed) { + return

{this.props.title}

; + } + return ( +
+

{this.props.title}

+ {this.props.children} +
+ ); + } +} + +function renderAuditorDetails(rci: ReserveCreationInfo|null) { + if (!rci) { + return ( +

+ Details will be displayed when a valid exchange provider URL is entered. +

+ ); + } + if (rci.exchangeInfo.auditors.length == 0) { + return ( +

+ The exchange is not audited by any auditors. +

+ ); + } + return ( +
+ {rci.exchangeInfo.auditors.map(a => ( +

Auditor {a.url}

+ ))} +
+ ); +} + +function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { + if (!rci) { + return ( +

+ Details will be displayed when a valid exchange provider URL is entered. +

+ ); + } + + let denoms = rci.selectedDenoms; + + let countByPub: {[s: string]: number} = {}; + let uniq: DenominationRecord[] = []; + + denoms.forEach((x: DenominationRecord) => { + let c = countByPub[x.denomPub] || 0; + if (c == 0) { + uniq.push(x); + } + c += 1; + countByPub[x.denomPub] = c; + }); + + function row(denom: DenominationRecord) { + return ( + + {countByPub[denom.denomPub] + "x"} + {amountToPretty(denom.value)} + {amountToPretty(denom.feeWithdraw)} + {amountToPretty(denom.feeRefresh)} + {amountToPretty(denom.feeDeposit)} + + ); + } + + function wireFee(s: string) { + return [ + + + Wire Method {s} + + + Applies Until + Wire Fee + Closing Fee + + , + + {rci!.wireFees.feesForType[s].map(f => ( + + {moment.unix(f.endStamp).format("llll")} + {amountToPretty(f.wireFee)} + {amountToPretty(f.closingFee)} + + ))} + + ]; + } + + let withdrawFeeStr = amountToPretty(rci.withdrawFee); + let overheadStr = amountToPretty(rci.overhead); + + return ( +
+

Overview

+

{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}

+

{i18n.str`Rounding loss: ${overheadStr}`}

+

{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}

+

Coin Fees

+ + + + + + + + + + + + {uniq.map(row)} + +
{i18n.str`# Coins`}{i18n.str`Value`}{i18n.str`Withdraw Fee`}{i18n.str`Refresh Fee`}{i18n.str`Deposit Fee`}
+

Wire Fees

+ + {Object.keys(rci.wireFees.feesForType).map(wireFee)} +
+
+ ); +} + + +function getSuggestedExchange(currency: string): Promise { + // 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

{i18n.str`Withdraw fees:`} {amountToPretty(totalCost)}

; + } + return

; +} + + +interface ExchangeSelectionProps { + suggestedExchangeUrl: string; + amount: AmountJson; + callback_url: string; + wt_types: string[]; + currencyRecord: CurrencyRecord|null; +} + +interface ManualSelectionProps { + onSelect(url: string): void; + initialUrl: string; +} + +class ManualSelection extends ImplicitStateComponent { + url: StateHolder = this.makeState(""); + errorMessage: StateHolder = this.makeState(null); + isOkay: StateHolder = this.makeState(false); + updateEvent = new EventTrigger(); + constructor(p: ManualSelectionProps) { + super(p); + this.url(p.initialUrl); + this.update(); + } + render() { + return ( +

+
+ + this.onUrlChanged((e.target as HTMLInputElement).value)} /> +
+
+ + {this.errorMessage()} +
+
+ ); + } + + async update() { + this.errorMessage(null); + this.isOkay(false); + if (!this.url()) { + return; + } + let parsedUrl = new URI(this.url()!); + if (parsedUrl.is("relative")) { + this.errorMessage(i18n.str`Error: URL may not be relative`); + this.isOkay(false); + return; + } + try { + let url = canonicalizeBaseUrl(this.url()!); + let r = await getExchangeInfo(url) + console.log("getExchangeInfo returned") + this.isOkay(true); + } catch (e) { + console.log("got error", e); + if (e.hasOwnProperty("httpStatus")) { + this.errorMessage(`Error: request failed with status ${e.httpStatus}`); + } else if (e.hasOwnProperty("errorResponse")) { + let resp = e.errorResponse; + this.errorMessage(`Error: ${resp.error} (${resp.hint})`); + } else { + this.errorMessage("invalid exchange URL"); + } + } + } + + async onUrlChanged(s: string) { + this.url(s); + this.errorMessage(null); + this.isOkay(false); + this.updateEvent.trigger(); + let waited = await this.updateEvent.wait(200); + if (waited) { + // Run the actual update if nobody else preempted us. + this.update(); + } + } +} + + +class ExchangeSelection extends ImplicitStateComponent { + statusString: StateHolder = this.makeState(null); + reserveCreationInfo: StateHolder = this.makeState( + null); + url: StateHolder = this.makeState(null); + + selectingExchange: StateHolder = this.makeState(false); + + constructor(props: ExchangeSelectionProps) { + super(props); + let prefilledExchangesUrls = []; + if (props.currencyRecord) { + let exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl); + prefilledExchangesUrls.push(...exchanges); + } + if (props.suggestedExchangeUrl) { + prefilledExchangesUrls.push(props.suggestedExchangeUrl); + } + if (prefilledExchangesUrls.length != 0) { + this.url(prefilledExchangesUrls[0]); + this.forceReserveUpdate(); + } else { + this.selectingExchange(true); + } + } + + renderFeeStatus() { + let rci = this.reserveCreationInfo(); + if (rci) { + let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + let trustMessage; + if (rci.isTrusted) { + trustMessage = ( + + The exchange is trusted by the wallet. + + ); + } else if (rci.isAudited) { + trustMessage = ( + + The exchange is audited by a trusted auditor. + + ); + } else { + trustMessage = ( + + Warning: The exchange is neither directly trusted nor audited by a trusted auditor. + If you withdraw from this exchange, it will be trusted in the future. + + ); + } + return ( +
+ + Using exchange provider {this.url()}. + The exchange provider will charge + {" "} + {amountToPretty(totalCost)} + {" "} + in fees. + + {trustMessage} +
+ ); + } + if (this.url() && !this.statusString()) { + let shortName = new URI(this.url()!).host(); + return ( + + Waiting for a response from + {" "} + {shortName} + + ); + } + if (this.statusString()) { + return ( +

+ {i18n.str`A problem occured, see below. ${this.statusString()}`} +

+ ); + } + return ( +

+ {i18n.str`Information about fees will be available when an exchange provider is selected.`} +

+ ); + } + + renderConfirm() { + return ( +
+ {this.renderFeeStatus()} + + { " " } + +
+ + {renderReserveCreationDetails(this.reserveCreationInfo())} + + + {renderAuditorDetails(this.reserveCreationInfo())} + +
+ ); + } + + select(url: string) { + this.reserveCreationInfo(null); + this.url(url); + this.selectingExchange(false); + this.forceReserveUpdate(); + } + + renderSelect() { + let exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || []; + console.log(exchanges); + return ( +
+ Please select an exchange. You can review the details before after your selection. + + {this.props.suggestedExchangeUrl && ( +
+

Bank Suggestion

+ +
+ )} + + {exchanges.length > 0 && ( +
+

Known Exchanges

+ {exchanges.map(e => ( + + ))} +
+ )} + +

Manual Selection

+ this.select(url)} /> +
+ ); + } + + render(): JSX.Element { + return ( +
+ + {"You are about to withdraw "} + {amountToPretty(this.props.amount)} + {" from your bank account into your wallet."} + + {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()} +
+ ); + } + + + 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); + try { + let url = canonicalizeBaseUrl(this.url()!); + let r = await getReserveCreationInfo(url, + this.props.amount); + console.log("get exchange info resolved"); + this.reserveCreationInfo(r); + console.dir(r); + } catch (e) { + console.log("get exchange info rejected", e); + 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})`); + } + } + } + + confirmReserveImpl(rci: ReserveCreationInfo, + exchange: string, + amount: AmountJson, + callback_url: string) { + const d = {exchange: canonicalizeBaseUrl(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 wireDetails = rci.wireInfo; + let filteredWireDetails: any = {}; + for (let wireType in wireDetails) { + if (this.props.wt_types.findIndex((x) => x.toLowerCase() == wireType.toLowerCase()) < 0) { + continue; + } + let obj = Object.assign({}, wireDetails[wireType]); + // The bank doesn't need to know about fees + delete obj.fees; + // Consequently the bank can't verify signatures anyway, so + // we delete this extra data, to make the request URL shorter. + delete obj.salt; + delete obj.sig; + filteredWireDetails[wireType] = obj; + } + if (!rawResp.error) { + const resp = CreateReserveResponse.checked(rawResp); + let q: {[name: string]: string|number} = { + wire_details: JSON.stringify(filteredWireDetails), + exchange: resp.exchange, + reserve_pub: resp.reservePub, + amount_value: amount.value, + amount_fraction: amount.fraction, + amount_currency: amount.currency, + }; + let url = new 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.statusString( + i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`); + } + }; + chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); + } + + renderStatus(): any { + if (this.statusString()) { + return

{this.statusString()}

; + } else if (!this.reserveCreationInfo()) { + return

{i18n.str`Checking URL, please wait ...`}

; + } + return ""; + } +} + +export async function main() { + try { + const url = new URI(document.location.href); + const query: any = URI.parseQuery(url.query()); + let amount; + try { + amount = AmountJson.checked(JSON.parse(query.amount)); + } catch (e) { + throw Error(i18n.str`Can't parse amount: ${e.message}`); + } + const callback_url = query.callback_url; + const bank_url = query.bank_url; + let wt_types; + try { + wt_types = JSON.parse(query.wt_types); + } catch (e) { + throw Error(i18n.str`Can't parse wire_types: ${e.message}`); + } + + let suggestedExchangeUrl = query.suggested_exchange_url; + let currencyRecord = await getCurrency(amount.currency); + + let args = { + wt_types, + suggestedExchangeUrl, + callback_url, + amount, + currencyRecord, + }; + + ReactDOM.render(, 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 = i18n.str`Fatal error: "${e.message}".`; + console.error(`got error "${e.message}"`, e); + } +} + +document.addEventListener("DOMContentLoaded", () => { + main(); +}); diff --git a/src/webex/pages/error.html b/src/webex/pages/error.html new file mode 100644 index 000000000..51a8fd73a --- /dev/null +++ b/src/webex/pages/error.html @@ -0,0 +1,18 @@ + + + + + + Taler Wallet: Error Occured + + + + + + + + + +
+ + diff --git a/src/webex/pages/error.tsx b/src/webex/pages/error.tsx new file mode 100644 index 000000000..f278bd224 --- /dev/null +++ b/src/webex/pages/error.tsx @@ -0,0 +1,63 @@ +/* + 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 + */ + + +/** + * Page shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +import {ImplicitStateComponent, StateHolder} from "../components"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); + +"use strict"; + +interface ErrorProps { + message: string; +} + +class ErrorView extends React.Component { + render(): JSX.Element { + return ( +
+ An error occurred: {this.props.message} +
+ ); + } +} + +export async function main() { + try { + const url = new URI(document.location.href); + const query: any = URI.parseQuery(url.query()); + + const message: string = query.message || "unknown error"; + + ReactDOM.render(, document.getElementById( + "container")!); + + } 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/webex/pages/help/empty-wallet.html b/src/webex/pages/help/empty-wallet.html new file mode 100644 index 000000000..dd29d9689 --- /dev/null +++ b/src/webex/pages/help/empty-wallet.html @@ -0,0 +1,30 @@ + + + + + GNU Taler Help - Empty Wallet + + + + + +
+
+
+

Your wallet is empty!

+

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.

+

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 + bank.demo.taler.net to + withdraw coins in the "KUDOS" currency that we created just for + demonstrating the system.

+
+
+
+ + diff --git a/src/webex/pages/logs.html b/src/webex/pages/logs.html new file mode 100644 index 000000000..9545269e3 --- /dev/null +++ b/src/webex/pages/logs.html @@ -0,0 +1,27 @@ + + + + + + Taler Wallet: Logs + + + + + + + + + + + +
+ + diff --git a/src/webex/pages/logs.tsx b/src/webex/pages/logs.tsx new file mode 100644 index 000000000..0c533bfa8 --- /dev/null +++ b/src/webex/pages/logs.tsx @@ -0,0 +1,83 @@ +/* + 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 + */ + +/** + * Show wallet logs. + * + * @author Florian Dold + */ + +import {LogEntry, getLogs} from "../../logging"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +interface LogViewProps { + log: LogEntry; +} + +class LogView extends React.Component { + render(): JSX.Element { + let e = this.props.log; + return ( +
+
    +
  • level: {e.level}
  • +
  • msg: {e.msg}
  • +
  • id: {e.id || "unknown"}
  • +
  • file: {e.source || "(unknown)"}
  • +
  • line: {e.line || "(unknown)"}
  • +
  • col: {e.col || "(unknown)"}
  • + {(e.detail ?
  • detail:
    {e.detail}
  • : [])} +
+
+ ); + } +} + +interface LogsState { + logs: LogEntry[]|undefined; +} + +class Logs extends React.Component { + constructor() { + super(); + this.update(); + this.state = {} as any; + } + + async update() { + let logs = await getLogs(); + this.setState({logs}); + } + + render(): JSX.Element { + let logs = this.state.logs; + if (!logs) { + return ...; + } + return ( +
+ Logs: + {logs.map(e => )} +
+ ); + } +} + +document.addEventListener("DOMContentLoaded", () => { + ReactDOM.render(, document.getElementById("container")!); +}); diff --git a/src/webex/pages/payback.html b/src/webex/pages/payback.html new file mode 100644 index 000000000..d6fe334c8 --- /dev/null +++ b/src/webex/pages/payback.html @@ -0,0 +1,36 @@ + + + + + + Taler Wallet: Payback + + + + + + + + + + + +
+ + diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx new file mode 100644 index 000000000..7bcc581d8 --- /dev/null +++ b/src/webex/pages/payback.tsx @@ -0,0 +1,100 @@ +/* + This file is part of TALER + (C) 2017 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 + */ + +/** + * View and edit auditors. + * + * @author Florian Dold + */ + + +import { amountToPretty, getTalerStampDate } from "../../helpers"; +import { + ExchangeRecord, + ExchangeForCurrencyRecord, + DenominationRecord, + AuditorRecord, + CurrencyRecord, + ReserveRecord, + CoinRecord, + PreCoinRecord, + Denomination, + WalletBalance, +} from "../../types"; + +import { ImplicitStateComponent, StateHolder } from "../components"; +import { + getCurrencies, + updateCurrency, + getPaybackReserves, + withdrawPaybackReserve, +} from "../wxApi"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +class Payback extends ImplicitStateComponent { + reserves: StateHolder = this.makeState(null); + constructor() { + super(); + let port = chrome.runtime.connect(); + port.onMessage.addListener((msg: any) => { + if (msg.notify) { + console.log("got notified"); + this.update(); + } + }); + this.update(); + } + + async update() { + let reserves = await getPaybackReserves(); + this.reserves(reserves); + } + + withdrawPayback(pub: string) { + withdrawPaybackReserve(pub); + } + + render(): JSX.Element { + let reserves = this.reserves(); + if (!reserves) { + return loading ...; + } + if (reserves.length == 0) { + return No reserves with payback available.; + } + return ( +
+ {reserves.map(r => ( +
+

Reserve for ${amountToPretty(r.current_amount!)}

+
    +
  • Exchange: ${r.exchange_base_url}
  • +
+ +
+ ))} +
+ ); + } +} + +export function main() { + ReactDOM.render(, document.getElementById("container")!); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/webex/pages/popup.css b/src/webex/pages/popup.css new file mode 100644 index 000000000..675412c11 --- /dev/null +++ b/src/webex/pages/popup.css @@ -0,0 +1,84 @@ + +/** + * @author Gabor X. Toth + * @author Marcello Stanisci + * @author Florian Dold + */ + +body { + min-height: 20em; + width: 30em; + margin: 0; + padding: 0; + max-height: 800px; + overflow: hidden; +} + +.nav { + background-color: #ddd; + padding: 0.5em 0; +} + +.nav a { + color: black; + padding: 0.5em; + text-decoration: none; +} + +.nav a.active { + background-color: white; + font-weight: bold; +} + + +.container { + overflow-y: scroll; + max-height: 400px; +} + +.abbrev { + text-decoration-style: dotted; +} + +#content { + padding: 1em; +} + + +#wallet-table .amount { + text-align: right; +} + +.hidden { + display: none; +} + +#transactions-table th, +#transactions-table td { + padding: 0.2em 0.5em; +} + +#reserve-create table { + width: 100%; +} + +#reserve-create table td.label { + width: 5em; +} + +#reserve-create table .input input[type="text"] { + width: 100%; +} + +.historyItem { + border: 1px solid black; + border-radius: 10px; + padding-left: 0.5em; + margin: 0.5em; +} + +.historyDate { + font-size: 90%; + margin: 0.3em; + color: slategray; +} diff --git a/src/webex/pages/popup.html b/src/webex/pages/popup.html new file mode 100644 index 000000000..98f24bccc --- /dev/null +++ b/src/webex/pages/popup.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + +
+ + + diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx new file mode 100644 index 000000000..a806cfef9 --- /dev/null +++ b/src/webex/pages/popup.tsx @@ -0,0 +1,548 @@ +/* + This file is part of TALER + (C) 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 + */ + + +/** + * Popup shown to the user when they click + * the Taler browser action button. + * + * @author Florian Dold + */ + + +"use strict"; + +import { + AmountJson, + Amounts, + WalletBalance, + WalletBalanceEntry +} from "../../types"; +import { HistoryRecord, HistoryLevel } from "../../wallet"; +import { amountToPretty } from "../../helpers"; +import * as i18n from "../../i18n"; + +import { abbrev } from "../renderHtml"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); + +function onUpdateNotification(f: () => void): () => void { + let port = chrome.runtime.connect({name: "notifications"}); + let listener = (msg: any, port: any) => { + f(); + }; + port.onMessage.addListener(listener); + return () => { + port.onMessage.removeListener(listener); + } +} + + +class Router extends React.Component { + static setRoute(s: string): void { + window.location.hash = s; + } + + static getRoute(): string { + // Omit the '#' at the beginning + return window.location.hash.substring(1); + } + + static onRoute(f: any): () => void { + Router.routeHandlers.push(f); + return () => { + let i = Router.routeHandlers.indexOf(f); + this.routeHandlers = this.routeHandlers.splice(i, 1); + } + } + + static routeHandlers: any[] = []; + + componentWillMount() { + console.log("router mounted"); + window.onhashchange = () => { + this.setState({}); + for (let f of Router.routeHandlers) { + f(); + } + } + } + + componentWillUnmount() { + console.log("router unmounted"); + } + + + render(): JSX.Element { + let 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 childProps: any = (child as any).props; + if (!childProps) { + return; + } + if (childProps["default"]) { + defaultChild = child; + } + if (childProps["route"] == route) { + foundChild = child; + } + }) + let child: React.ReactChild | null = foundChild || defaultChild; + if (!child) { + throw Error("unknown route"); + } + Router.setRoute((child as any).props["route"]); + return
{child}
; + } +} + + +interface TabProps { + target: string; + children?: React.ReactNode; +} + +function Tab(props: TabProps) { + let cssClass = ""; + if (props.target == Router.getRoute()) { + cssClass = "active"; + } + let onClick = (e: React.MouseEvent) => { + Router.setRoute(props.target); + e.preventDefault(); + }; + return ( + + {props.children} + + ); +} + + +class WalletNavBar extends React.Component { + cancelSubscription: any; + + componentWillMount() { + this.cancelSubscription = Router.onRoute(() => { + this.setState({}); + }); + } + + componentWillUnmount() { + if (this.cancelSubscription) { + this.cancelSubscription(); + } + } + + render() { + console.log("rendering nav bar"); + return ( + ); + } +} + + +function ExtensionLink(props: any) { + let onClick = (e: React.MouseEvent) => { + chrome.tabs.create({ + "url": chrome.extension.getURL(props.target) + }); + e.preventDefault(); + }; + return ( + + {props.children} + ) +} + + +export function bigAmount(amount: AmountJson): JSX.Element { + let v = amount.value + amount.fraction / Amounts.fractionalBase; + return ( + + {v} + {" "} + {amount.currency} + + ); +} + +class WalletBalanceView extends React.Component { + balance: WalletBalance; + gotError = false; + canceler: (() => void) | undefined = undefined; + unmount = false; + + componentWillMount() { + this.canceler = onUpdateNotification(() => this.updateBalance()); + this.updateBalance(); + } + + componentWillUnmount() { + console.log("component WalletBalanceView will unmount"); + if (this.canceler) { + this.canceler(); + } + this.unmount = true; + } + + updateBalance() { + chrome.runtime.sendMessage({type: "balances"}, (resp) => { + if (this.unmount) { + return; + } + if (resp.error) { + this.gotError = true; + console.error("could not retrieve balances", resp); + this.setState({}); + return; + } + this.gotError = false; + console.log("got wallet", resp); + this.balance = resp; + this.setState({}); + }); + } + + renderEmpty(): JSX.Element { + let helpLink = ( + + {i18n.str`help`} + + ); + return ( +
+ + You have no balance to show. Need some + {" "}{helpLink}{" "} + getting started? + +
+ ); + } + + formatPending(entry: WalletBalanceEntry): JSX.Element { + let incoming: JSX.Element | undefined; + let payment: JSX.Element | undefined; + + console.log("available: ", entry.pendingIncoming ? amountToPretty(entry.available) : null); + console.log("incoming: ", entry.pendingIncoming ? amountToPretty(entry.pendingIncoming) : null); + + if (Amounts.isNonZero(entry.pendingIncoming)) { + incoming = ( + + + {"+"} + {amountToPretty(entry.pendingIncoming)} + + {" "} + incoming + + ); + } + + if (Amounts.isNonZero(entry.pendingPayment)) { + payment = ( + + + {amountToPretty(entry.pendingPayment)} + + {" "} + being spent + + ); + } + + let l = [incoming, payment].filter((x) => x !== undefined); + if (l.length == 0) { + return ; + } + + if (l.length == 1) { + return ({l}) + } + return ({l[0]}, {l[1]}); + + } + + render(): JSX.Element { + let wallet = this.balance; + if (this.gotError) { + return i18n.str`Error: could not retrieve balance information.`; + } + if (!wallet) { + return ; + } + console.log(wallet); + let paybackAvailable = false; + let listing = Object.keys(wallet).map((key) => { + let entry: WalletBalanceEntry = wallet[key]; + if (entry.paybackAmount.value != 0 || entry.paybackAmount.fraction != 0) { + paybackAvailable = true; + } + return ( +

+ {bigAmount(entry.available)} + {" "} + {this.formatPending(entry)} +

+ ); + }); + let link = chrome.extension.getURL("/src/pages/auditors.html"); + let linkElem = Trusted Auditors and Exchanges; + let paybackLink = chrome.extension.getURL("/src/pages/payback.html"); + let paybackLinkElem = Trusted Auditors and Exchanges; + return ( +
+ {listing.length > 0 ? listing : this.renderEmpty()} + {paybackAvailable && paybackLinkElem} + {linkElem} +
+ ); + } +} + + +function formatHistoryItem(historyItem: HistoryRecord) { + const d = historyItem.detail; + const t = historyItem.timestamp; + console.log("hist item", historyItem); + switch (historyItem.type) { + case "create-reserve": + return ( + + Bank requested reserve ({abbrev(d.reservePub)}) for {amountToPretty(d.requestedAmount)}. + + ); + case "confirm-reserve": { + // FIXME: eventually remove compat fix + let exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??"; + let pub = abbrev(d.reservePub); + return ( + + Started to withdraw + {" "}{amountToPretty(d.requestedAmount)}{" "} + from {exchange} ({pub}). + + ); + } + case "offer-contract": { + let link = chrome.extension.getURL("view-contract.html"); + let linkElem = {abbrev(d.contractHash)}; + let merchantElem = {abbrev(d.merchantName, 15)}; + return ( + + Merchant {abbrev(d.merchantName, 15)} offered contract {abbrev(d.contractHash)}; + + ); + } + case "depleted-reserve": { + let exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??"; + let amount = amountToPretty(d.requestedAmount); + let pub = abbrev(d.reservePub); + return ( + + Withdrew {amount} from {exchange} ({pub}). + + ); + } + case "pay": { + let url = d.fulfillmentUrl; + let merchantElem = {abbrev(d.merchantName, 15)}; + let fulfillmentLinkElem = view product; + return ( + + Paid {amountToPretty(d.amount)} to merchant {merchantElem}. ({fulfillmentLinkElem}) + + ); + } + default: + return (

{i18n.str`Unknown event (${historyItem.type})`}

); + } +} + + +class WalletHistory extends React.Component { + myHistory: any[]; + gotError = false; + unmounted = false; + + componentWillMount() { + this.update(); + onUpdateNotification(() => this.update()); + } + + componentWillUnmount() { + console.log("history component unmounted"); + this.unmounted = true; + } + + update() { + chrome.runtime.sendMessage({type: "get-history"}, (resp) => { + if (this.unmounted) { + return; + } + console.log("got history response"); + if (resp.error) { + this.gotError = true; + console.error("could not retrieve history", resp); + this.setState({}); + return; + } + this.gotError = false; + console.log("got history", resp.history); + this.myHistory = resp.history; + this.setState({}); + }); + } + + render(): JSX.Element { + console.log("rendering history"); + let history: HistoryRecord[] = this.myHistory; + if (this.gotError) { + return i18n.str`Error: could not retrieve event history`; + } + + if (!history) { + // We're not ready yet + return ; + } + + let subjectMemo: {[s: string]: boolean} = {}; + let listing: any[] = []; + for (let record of history.reverse()) { + if (record.subjectId && subjectMemo[record.subjectId]) { + continue; + } + if (record.level != undefined && record.level < HistoryLevel.User) { + continue; + } + subjectMemo[record.subjectId as string] = true; + + let item = ( +
+
+ {(new Date(record.timestamp)).toString()} +
+ {formatHistoryItem(record)} +
+ ); + + listing.push(item); + } + + if (listing.length > 0) { + return
{listing}
; + } + return

{i18n.str`Your wallet has no events recorded.`}

+ } + +} + + +function reload() { + try { + chrome.runtime.reload(); + window.close(); + } catch (e) { + // Functionality missing in firefox, ignore! + } +} + +function confirmReset() { + if (confirm("Do you want to IRREVOCABLY DESTROY everything inside your" + + " wallet and LOSE ALL YOUR COINS?")) { + chrome.runtime.sendMessage({type: "reset"}); + window.close(); + } +} + + +function WalletDebug(props: any) { + return (
+

Debug tools:

+ + + + +
+ + +
); +} + + +function openExtensionPage(page: string) { + return function() { + chrome.tabs.create({ + "url": chrome.extension.getURL(page) + }); + } +} + + +function openTab(page: string) { + return function() { + chrome.tabs.create({ + "url": page + }); + } +} + + +let el = ( +
+ +
+ + + + + +
+
+); + +document.addEventListener("DOMContentLoaded", () => { + ReactDOM.render(el, document.getElementById("content")!); +}) diff --git a/src/webex/pages/show-db.html b/src/webex/pages/show-db.html new file mode 100644 index 000000000..215c726d9 --- /dev/null +++ b/src/webex/pages/show-db.html @@ -0,0 +1,18 @@ + + + + + Taler Wallet: Reserve Created + + + + + + +

DB Dump

+ + + +

+  
+
diff --git a/src/webex/pages/show-db.ts b/src/webex/pages/show-db.ts
new file mode 100644
index 000000000..d95951385
--- /dev/null
+++ b/src/webex/pages/show-db.ts
@@ -0,0 +1,94 @@
+/*
+ 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 
+ */
+
+
+/**
+ * Wallet database dump for debugging.
+ *
+ * @author Florian Dold
+ */
+
+function replacer(match: string, pIndent: string, pKey: string, pVal: string,
+                  pEnd: string) {
+  const key = "";
+  const val = "";
+  const str = "";
+  let r = pIndent || "";
+  if (pKey) {
+    r = r + key + '"' + pKey.replace(/[": ]/g, "") + '": ';
+  }
+  if (pVal) {
+    r = r + (pVal[0] === '"' ? str : val) + pVal + "";
+  }
+  return r + (pEnd || "");
+}
+
+
+function prettyPrint(obj: any) {
+  const jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
+  return JSON.stringify(obj, null as any, 3)
+             .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);
+
+    document.getElementById("download")!.addEventListener("click", (evt) => {
+      console.log("creating download");
+      const element = document.createElement("a");
+      element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(JSON.stringify(resp)));
+      element.setAttribute("download", "wallet-dump.txt");
+      element.style.display = "none";
+      document.body.appendChild(element);
+      element.click();
+    });
+
+  });
+
+
+  const fileInput = document.getElementById("fileInput")! as HTMLInputElement;
+  fileInput.onchange = (evt) => {
+    if (!fileInput.files || fileInput.files.length !== 1) {
+      alert("please select exactly one file to import");
+      return;
+    }
+    const file = fileInput.files[0];
+    const fr = new FileReader();
+    fr.onload = (e: any) => {
+      console.log("got file");
+      const dump = JSON.parse(e.target.result);
+      console.log("parsed contents", dump);
+      chrome.runtime.sendMessage({ type: "import-db", detail: { dump } }, (resp) => {
+        alert("loaded");
+      });
+    };
+    console.log("reading file", file);
+    fr.readAsText(file);
+  };
+
+  document.getElementById("import")!.addEventListener("click", (evt) => {
+    fileInput.click();
+    evt.preventDefault();
+  });
+});
diff --git a/src/webex/pages/tree.html b/src/webex/pages/tree.html
new file mode 100644
index 000000000..0c0a368b3
--- /dev/null
+++ b/src/webex/pages/tree.html
@@ -0,0 +1,27 @@
+
+
+
+
+  
+  Taler Wallet: Tree View
+
+  
+
+  
+
+  
+  
+
+  
+
+  
+    
+ + diff --git a/src/webex/pages/tree.tsx b/src/webex/pages/tree.tsx new file mode 100644 index 000000000..ddf8f2dbc --- /dev/null +++ b/src/webex/pages/tree.tsx @@ -0,0 +1,437 @@ +/* + 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 + */ + +/** + * Show contents of the wallet as a tree. + * + * @author Florian Dold + */ + + +import { amountToPretty, getTalerStampDate } from "../../helpers"; +import { + CoinRecord, + CoinStatus, + Denomination, + DenominationRecord, + ExchangeRecord, + PreCoinRecord, + ReserveRecord, +} from "../../types"; + +import { ImplicitStateComponent, StateHolder } from "../components"; +import { + getReserves, getExchanges, getCoins, getPreCoins, + refresh, getDenoms, payback, +} from "../wxApi"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +interface ReserveViewProps { + reserve: ReserveRecord; +} + +class ReserveView extends React.Component { + render(): JSX.Element { + let r: ReserveRecord = this.props.reserve; + return ( +
+
    +
  • Key: {r.reserve_pub}
  • +
  • Created: {(new Date(r.created * 1000).toString())}
  • +
  • Current: {r.current_amount ? amountToPretty(r.current_amount!) : "null"}
  • +
  • Requested: {amountToPretty(r.requested_amount)}
  • +
  • Confirmed: {r.confirmed}
  • +
+
+ ); + } +} + +interface ReserveListProps { + exchangeBaseUrl: string; +} + +interface ToggleProps { + expanded: StateHolder; +} + +class Toggle extends ImplicitStateComponent { + renderButton() { + let show = () => { + this.props.expanded(true); + this.setState({}); + }; + let hide = () => { + this.props.expanded(false); + this.setState({}); + }; + if (this.props.expanded()) { + return ; + } + return ; + + } + render() { + return ( +
+ {this.renderButton()} + {this.props.expanded() ? this.props.children : []} +
); + } +} + + +interface CoinViewProps { + coin: CoinRecord; +} + +interface RefreshDialogProps { + coin: CoinRecord; +} + +class RefreshDialog extends ImplicitStateComponent { + refreshRequested = this.makeState(false); + render(): JSX.Element { + if (!this.refreshRequested()) { + return ( +
+ +
+ ); + } + return ( +
+ Refresh amount: + + +
+ ); + } +} + +class CoinView extends React.Component { + render() { + let c = this.props.coin; + return ( +
+
    +
  • Key: {c.coinPub}
  • +
  • Current amount: {amountToPretty(c.currentAmount)}
  • +
  • Denomination:
  • +
  • Suspended: {(c.suspended || false).toString()}
  • +
  • Status: {CoinStatus[c.status]}
  • +
  • +
  • +
+
+ ); + } +} + + + +interface PreCoinViewProps { + precoin: PreCoinRecord; +} + +class PreCoinView extends React.Component { + render() { + let c = this.props.precoin; + return ( +
+
    +
  • Key: {c.coinPub}
  • +
+
+ ); + } +} + +interface CoinListProps { + exchangeBaseUrl: string; +} + +class CoinList extends ImplicitStateComponent { + coins = this.makeState(null); + expanded = this.makeState(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
...
; + } + return ( +
+ Coins ({this.coins() !.length.toString()}) + {" "} + + {this.coins() !.map((c) => )} + +
+ ); + } +} + + +interface PreCoinListProps { + exchangeBaseUrl: string; +} + +class PreCoinList extends ImplicitStateComponent { + precoins = this.makeState(null); + expanded = this.makeState(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
...
; + } + return ( +
+ Planchets ({this.precoins() !.length.toString()}) + {" "} + + {this.precoins() !.map((c) => )} + +
+ ); + } +} + +interface DenominationListProps { + exchange: ExchangeRecord; +} + +interface ExpanderTextProps { + text: string; +} + +class ExpanderText extends ImplicitStateComponent { + expanded = this.makeState(false); + textArea: any = undefined; + + componentDidUpdate() { + if (this.expanded() && this.textArea) { + this.textArea.focus(); + this.textArea.scrollTop = 0; + } + } + + render(): JSX.Element { + if (!this.expanded()) { + return ( + { this.expanded(true); }}> + {(this.props.text.length <= 10) + ? this.props.text + : ( + + {this.props.text.substring(0,10)} + ... + + ) + } + + ); + } + return ( + + ); + } +} + +class DenominationList extends ImplicitStateComponent { + expanded = this.makeState(false); + denoms = this.makeState(undefined); + + constructor(props: DenominationListProps) { + super(props); + this.update(); + } + + async update() { + let d = await getDenoms(this.props.exchange.baseUrl); + this.denoms(d); + } + + renderDenom(d: DenominationRecord) { + return ( +
+
    +
  • Offered: {d.isOffered ? "yes" : "no"}
  • +
  • Value: {amountToPretty(d.value)}
  • +
  • Withdraw fee: {amountToPretty(d.feeWithdraw)}
  • +
  • Refresh fee: {amountToPretty(d.feeRefresh)}
  • +
  • Deposit fee: {amountToPretty(d.feeDeposit)}
  • +
  • Refund fee: {amountToPretty(d.feeRefund)}
  • +
  • Start: {getTalerStampDate(d.stampStart)!.toString()}
  • +
  • Withdraw expiration: {getTalerStampDate(d.stampExpireWithdraw)!.toString()}
  • +
  • Legal expiration: {getTalerStampDate(d.stampExpireLegal)!.toString()}
  • +
  • Deposit expiration: {getTalerStampDate(d.stampExpireDeposit)!.toString()}
  • +
  • Denom pub:
  • +
+
+ ); + } + + render(): JSX.Element { + let denoms = this.denoms() + if (!denoms) { + return ( +
+ Denominations (...) + {" "} + + ... + +
+ ); + } + return ( +
+ Denominations ({denoms.length.toString()}) + {" "} + + {denoms.map((d) => this.renderDenom(d))} + +
+ ); + } +} + +class ReserveList extends ImplicitStateComponent { + reserves = this.makeState(null); + expanded = this.makeState(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
...
; + } + return ( +
+ Reserves ({this.reserves() !.length.toString()}) + {" "} + + {this.reserves() !.map((r) => )} + +
+ ); + } +} + +interface ExchangeProps { + exchange: ExchangeRecord; +} + +class ExchangeView extends React.Component { + render(): JSX.Element { + let e = this.props.exchange; + return ( +
+
    +
  • Exchange Base Url: {this.props.exchange.baseUrl}
  • +
  • Master public key:
  • +
+ + + + +
+ ); + } +} + +interface ExchangesListState { + exchanges?: ExchangeRecord[]; +} + +class ExchangesList extends React.Component { + 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 ...; + } + return ( +
+ Exchanges ({exchanges.length.toString()}): + {exchanges.map(e => )} +
+ ); + } +} + +export function main() { + ReactDOM.render(, document.getElementById("container")!); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx new file mode 100644 index 000000000..440cd5789 --- /dev/null +++ b/src/webex/renderHtml.tsx @@ -0,0 +1,79 @@ +/* + 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 + */ + +/** + * Helpers functions to render Taler-related data structures to HTML. + * + * @author Florian Dold + */ + + +/** + * Imports. + */ +import { + AmountJson, + Amounts, + Contract, +} from "../types"; +import * as i18n from "../i18n"; +import { amountToPretty } from "../helpers"; + +import * as React from "react"; + + +export function renderContract(contract: Contract): JSX.Element { + let merchantName; + if (contract.merchant && contract.merchant.name) { + merchantName = {contract.merchant.name}; + } else { + merchantName = (pub: {contract.merchant_pub}); + } + let amount = {amountToPretty(contract.amount)}; + + return ( +
+ + The merchant {merchantName} + wants to enter a contract over {amount}{" "} + with you. + +

{i18n.str`You are about to purchase:`}

+
    + {contract.products.map( + (p: any, i: number) => (
  • {`${p.description}: ${amountToPretty(p.price)}`}
  • )) + } +
+
+ ); +} + + +/** + * Abbreviate a string to a given length, and show the full + * string on hover as a tooltip. + */ +export function abbrev(s: string, n: number = 5) { + let sAbbrev = s; + if (s.length > n) { + sAbbrev = s.slice(0, n) + ".."; + } + return ( + + {sAbbrev} + + ); +} diff --git a/src/webex/style/pure.css b/src/webex/style/pure.css new file mode 100644 index 000000000..739113970 --- /dev/null +++ b/src/webex/style/pure.css @@ -0,0 +1,1508 @@ +/*! +Pure v0.6.2 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/yahoo/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v^3.0 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS and IE text size adjust after device orientation change, + * without disabling user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability of focused elements when they are also in an + * active/hover state. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + box-sizing: content-box; /* 2 */ +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} + +/*csslint important:false*/ + +/* ========================================================================== + Pure Base Extras + ========================================================================== */ + +/** + * Extra rules that Pure adds on top of Normalize.css + */ + +/** + * Always hide an element when it has the `hidden` HTML attribute. + */ + +.hidden, +[hidden] { + display: none !important; +} + +/** + * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining + * aspect ratio. + */ +.pure-img { + max-width: 100%; + height: auto; + display: block; +} + +/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/ + +.pure-g { + letter-spacing: -0.31em; /* Webkit: collapse white-space between units */ + *letter-spacing: normal; /* reset IE < 8 */ + *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */ + text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */ + + /* + Sets the font stack to fonts known to work properly with the above letter + and word spacings. See: https://github.com/yahoo/pure/issues/41/ + + The following font stack makes Pure Grids work on all known environments. + + * FreeSans: Ships with many Linux distros, including Ubuntu + + * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and + Arial to get picked up by the browser, even though neither is available + in Chrome OS. + + * Droid Sans: Ships with all versions of Android. + + * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows. + */ + font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif; + + /* Use flexbox when possible to avoid `letter-spacing` side-effects. */ + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row wrap; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + + /* Prevents distributing space between rows */ + -webkit-align-content: flex-start; + -ms-flex-line-pack: start; + align-content: flex-start; +} + +/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside a table; fall back to block and rely on font hack */ +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + table .pure-g { + display: block; + } +} + +/* Opera as of 12 on Windows needs word-spacing. + The ".opera-only" selector is used to prevent actual prefocus styling + and is not required in markup. +*/ +.opera-only :-o-prefocus, +.pure-g { + word-spacing: -0.43em; +} + +.pure-u { + display: inline-block; + *display: inline; /* IE < 8: fake inline-block */ + zoom: 1; + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; +} + +/* +Resets the font family back to the OS/browser's default sans-serif font, +this the same font stack that Normalize.css sets for the `body`. +*/ +.pure-g [class *= "pure-u"] { + font-family: sans-serif; +} + +.pure-u-1, +.pure-u-1-1, +.pure-u-1-2, +.pure-u-1-3, +.pure-u-2-3, +.pure-u-1-4, +.pure-u-3-4, +.pure-u-1-5, +.pure-u-2-5, +.pure-u-3-5, +.pure-u-4-5, +.pure-u-5-5, +.pure-u-1-6, +.pure-u-5-6, +.pure-u-1-8, +.pure-u-3-8, +.pure-u-5-8, +.pure-u-7-8, +.pure-u-1-12, +.pure-u-5-12, +.pure-u-7-12, +.pure-u-11-12, +.pure-u-1-24, +.pure-u-2-24, +.pure-u-3-24, +.pure-u-4-24, +.pure-u-5-24, +.pure-u-6-24, +.pure-u-7-24, +.pure-u-8-24, +.pure-u-9-24, +.pure-u-10-24, +.pure-u-11-24, +.pure-u-12-24, +.pure-u-13-24, +.pure-u-14-24, +.pure-u-15-24, +.pure-u-16-24, +.pure-u-17-24, +.pure-u-18-24, +.pure-u-19-24, +.pure-u-20-24, +.pure-u-21-24, +.pure-u-22-24, +.pure-u-23-24, +.pure-u-24-24 { + display: inline-block; + *display: inline; + zoom: 1; + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; +} + +.pure-u-1-24 { + width: 4.1667%; + *width: 4.1357%; +} + +.pure-u-1-12, +.pure-u-2-24 { + width: 8.3333%; + *width: 8.3023%; +} + +.pure-u-1-8, +.pure-u-3-24 { + width: 12.5000%; + *width: 12.4690%; +} + +.pure-u-1-6, +.pure-u-4-24 { + width: 16.6667%; + *width: 16.6357%; +} + +.pure-u-1-5 { + width: 20%; + *width: 19.9690%; +} + +.pure-u-5-24 { + width: 20.8333%; + *width: 20.8023%; +} + +.pure-u-1-4, +.pure-u-6-24 { + width: 25%; + *width: 24.9690%; +} + +.pure-u-7-24 { + width: 29.1667%; + *width: 29.1357%; +} + +.pure-u-1-3, +.pure-u-8-24 { + width: 33.3333%; + *width: 33.3023%; +} + +.pure-u-3-8, +.pure-u-9-24 { + width: 37.5000%; + *width: 37.4690%; +} + +.pure-u-2-5 { + width: 40%; + *width: 39.9690%; +} + +.pure-u-5-12, +.pure-u-10-24 { + width: 41.6667%; + *width: 41.6357%; +} + +.pure-u-11-24 { + width: 45.8333%; + *width: 45.8023%; +} + +.pure-u-1-2, +.pure-u-12-24 { + width: 50%; + *width: 49.9690%; +} + +.pure-u-13-24 { + width: 54.1667%; + *width: 54.1357%; +} + +.pure-u-7-12, +.pure-u-14-24 { + width: 58.3333%; + *width: 58.3023%; +} + +.pure-u-3-5 { + width: 60%; + *width: 59.9690%; +} + +.pure-u-5-8, +.pure-u-15-24 { + width: 62.5000%; + *width: 62.4690%; +} + +.pure-u-2-3, +.pure-u-16-24 { + width: 66.6667%; + *width: 66.6357%; +} + +.pure-u-17-24 { + width: 70.8333%; + *width: 70.8023%; +} + +.pure-u-3-4, +.pure-u-18-24 { + width: 75%; + *width: 74.9690%; +} + +.pure-u-19-24 { + width: 79.1667%; + *width: 79.1357%; +} + +.pure-u-4-5 { + width: 80%; + *width: 79.9690%; +} + +.pure-u-5-6, +.pure-u-20-24 { + width: 83.3333%; + *width: 83.3023%; +} + +.pure-u-7-8, +.pure-u-21-24 { + width: 87.5000%; + *width: 87.4690%; +} + +.pure-u-11-12, +.pure-u-22-24 { + width: 91.6667%; + *width: 91.6357%; +} + +.pure-u-23-24 { + width: 95.8333%; + *width: 95.8023%; +} + +.pure-u-1, +.pure-u-1-1, +.pure-u-5-5, +.pure-u-24-24 { + width: 100%; +} +.pure-button { + /* Structure */ + display: inline-block; + zoom: 1; + line-height: normal; + white-space: nowrap; + vertical-align: middle; + text-align: center; + cursor: pointer; + -webkit-user-drag: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + box-sizing: border-box; +} + +/* Firefox: Get rid of the inner focus border */ +.pure-button::-moz-focus-inner { + padding: 0; + border: 0; +} + +/* Inherit .pure-g styles */ +.pure-button-group { + letter-spacing: -0.31em; /* Webkit: collapse white-space between units */ + *letter-spacing: normal; /* reset IE < 8 */ + *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */ + text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */ +} + +.opera-only :-o-prefocus, +.pure-button-group { + word-spacing: -0.43em; +} + +.pure-button-group .pure-button { + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; +} + +/*csslint outline-none:false*/ + +.pure-button { + font-family: inherit; + font-size: 100%; + padding: 0.5em 1em; + color: #444; /* rgba not supported (IE 8) */ + color: rgba(0, 0, 0, 0.80); /* rgba supported */ + border: 1px solid #999; /*IE 6/7/8*/ + border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ + background-color: #E6E6E6; + text-decoration: none; + border-radius: 2px; +} + +.pure-button-hover, +.pure-button:hover, +.pure-button:focus { + /* csslint ignore:start */ + filter: alpha(opacity=90); + /* csslint ignore:end */ + background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); + background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); +} +.pure-button:focus { + outline: 0; +} +.pure-button-active, +.pure-button:active { + box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20) inset; + border-color: #000\9; +} + +.pure-button[disabled], +.pure-button-disabled, +.pure-button-disabled:hover, +.pure-button-disabled:focus, +.pure-button-disabled:active { + border: none; + background-image: none; + /* csslint ignore:start */ + filter: alpha(opacity=40); + /* csslint ignore:end */ + opacity: 0.40; + cursor: not-allowed; + box-shadow: none; + pointer-events: none; +} + +.pure-button-hidden { + display: none; +} + +.pure-button-primary, +.pure-button-selected, +a.pure-button-primary, +a.pure-button-selected { + background-color: rgb(0, 120, 231); + color: #fff; +} + +/* Button Groups */ +.pure-button-group .pure-button { + margin: 0; + border-radius: 0; + border-right: 1px solid #111; /* fallback color for rgba() for IE7/8 */ + border-right: 1px solid rgba(0, 0, 0, 0.2); + +} + +.pure-button-group .pure-button:first-child { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +.pure-button-group .pure-button:last-child { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-right: none; +} + +/*csslint box-model:false*/ +/* +Box-model set to false because we're setting a height on select elements, which +also have border and padding. This is done because some browsers don't render +the padding. We explicitly set the box-model for select elements to border-box, +so we can ignore the csslint warning. +*/ + +.pure-form input[type="text"], +.pure-form input[type="password"], +.pure-form input[type="email"], +.pure-form input[type="url"], +.pure-form input[type="date"], +.pure-form input[type="month"], +.pure-form input[type="time"], +.pure-form input[type="datetime"], +.pure-form input[type="datetime-local"], +.pure-form input[type="week"], +.pure-form input[type="number"], +.pure-form input[type="search"], +.pure-form input[type="tel"], +.pure-form input[type="color"], +.pure-form select, +.pure-form textarea { + padding: 0.5em 0.6em; + display: inline-block; + border: 1px solid #ccc; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 4px; + vertical-align: middle; + box-sizing: border-box; +} + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form input:not([type]) { + padding: 0.5em 0.6em; + display: inline-block; + border: 1px solid #ccc; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 4px; + box-sizing: border-box; +} + + +/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */ +/* May be able to remove this tweak as color inputs become more standardized across browsers. */ +.pure-form input[type="color"] { + padding: 0.2em 0.5em; +} + + +.pure-form input[type="text"]:focus, +.pure-form input[type="password"]:focus, +.pure-form input[type="email"]:focus, +.pure-form input[type="url"]:focus, +.pure-form input[type="date"]:focus, +.pure-form input[type="month"]:focus, +.pure-form input[type="time"]:focus, +.pure-form input[type="datetime"]:focus, +.pure-form input[type="datetime-local"]:focus, +.pure-form input[type="week"]:focus, +.pure-form input[type="number"]:focus, +.pure-form input[type="search"]:focus, +.pure-form input[type="tel"]:focus, +.pure-form input[type="color"]:focus, +.pure-form select:focus, +.pure-form textarea:focus { + outline: 0; + border-color: #129FEA; +} + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form input:not([type]):focus { + outline: 0; + border-color: #129FEA; +} + +.pure-form input[type="file"]:focus, +.pure-form input[type="radio"]:focus, +.pure-form input[type="checkbox"]:focus { + outline: thin solid #129FEA; + outline: 1px auto #129FEA; +} +.pure-form .pure-checkbox, +.pure-form .pure-radio { + margin: 0.5em 0; + display: block; +} + +.pure-form input[type="text"][disabled], +.pure-form input[type="password"][disabled], +.pure-form input[type="email"][disabled], +.pure-form input[type="url"][disabled], +.pure-form input[type="date"][disabled], +.pure-form input[type="month"][disabled], +.pure-form input[type="time"][disabled], +.pure-form input[type="datetime"][disabled], +.pure-form input[type="datetime-local"][disabled], +.pure-form input[type="week"][disabled], +.pure-form input[type="number"][disabled], +.pure-form input[type="search"][disabled], +.pure-form input[type="tel"][disabled], +.pure-form input[type="color"][disabled], +.pure-form select[disabled], +.pure-form textarea[disabled] { + cursor: not-allowed; + background-color: #eaeded; + color: #cad2d3; +} + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form input:not([type])[disabled] { + cursor: not-allowed; + background-color: #eaeded; + color: #cad2d3; +} +.pure-form input[readonly], +.pure-form select[readonly], +.pure-form textarea[readonly] { + background-color: #eee; /* menu hover bg color */ + color: #777; /* menu text color */ + border-color: #ccc; +} + +.pure-form input:focus:invalid, +.pure-form textarea:focus:invalid, +.pure-form select:focus:invalid { + color: #b94a48; + border-color: #e9322d; +} +.pure-form input[type="file"]:focus:invalid:focus, +.pure-form input[type="radio"]:focus:invalid:focus, +.pure-form input[type="checkbox"]:focus:invalid:focus { + outline-color: #e9322d; +} +.pure-form select { + /* Normalizes the height; padding is not sufficient. */ + height: 2.25em; + border: 1px solid #ccc; + background-color: white; +} +.pure-form select[multiple] { + height: auto; +} +.pure-form label { + margin: 0.5em 0 0.2em; +} +.pure-form fieldset { + margin: 0; + padding: 0.35em 0 0.75em; + border: 0; +} +.pure-form legend { + display: block; + width: 100%; + padding: 0.3em 0; + margin-bottom: 0.3em; + color: #333; + border-bottom: 1px solid #e5e5e5; +} + +.pure-form-stacked input[type="text"], +.pure-form-stacked input[type="password"], +.pure-form-stacked input[type="email"], +.pure-form-stacked input[type="url"], +.pure-form-stacked input[type="date"], +.pure-form-stacked input[type="month"], +.pure-form-stacked input[type="time"], +.pure-form-stacked input[type="datetime"], +.pure-form-stacked input[type="datetime-local"], +.pure-form-stacked input[type="week"], +.pure-form-stacked input[type="number"], +.pure-form-stacked input[type="search"], +.pure-form-stacked input[type="tel"], +.pure-form-stacked input[type="color"], +.pure-form-stacked input[type="file"], +.pure-form-stacked select, +.pure-form-stacked label, +.pure-form-stacked textarea { + display: block; + margin: 0.25em 0; +} + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form-stacked input:not([type]) { + display: block; + margin: 0.25em 0; +} +.pure-form-aligned input, +.pure-form-aligned textarea, +.pure-form-aligned select, +/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ +.pure-form-aligned .pure-help-inline, +.pure-form-message-inline { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle; +} +.pure-form-aligned textarea { + vertical-align: top; +} + +/* Aligned Forms */ +.pure-form-aligned .pure-control-group { + margin-bottom: 0.5em; +} +.pure-form-aligned .pure-control-group label { + text-align: right; + display: inline-block; + vertical-align: middle; + width: 10em; + margin: 0 1em 0 0; +} +.pure-form-aligned .pure-controls { + margin: 1.5em 0 0 11em; +} + +/* Rounded Inputs */ +.pure-form input.pure-input-rounded, +.pure-form .pure-input-rounded { + border-radius: 2em; + padding: 0.5em 1em; +} + +/* Grouped Inputs */ +.pure-form .pure-group fieldset { + margin-bottom: 10px; +} +.pure-form .pure-group input, +.pure-form .pure-group textarea { + display: block; + padding: 10px; + margin: 0 0 -1px; + border-radius: 0; + position: relative; + top: -1px; +} +.pure-form .pure-group input:focus, +.pure-form .pure-group textarea:focus { + z-index: 3; +} +.pure-form .pure-group input:first-child, +.pure-form .pure-group textarea:first-child { + top: 1px; + border-radius: 4px 4px 0 0; + margin: 0; +} +.pure-form .pure-group input:first-child:last-child, +.pure-form .pure-group textarea:first-child:last-child { + top: 1px; + border-radius: 4px; + margin: 0; +} +.pure-form .pure-group input:last-child, +.pure-form .pure-group textarea:last-child { + top: -2px; + border-radius: 0 0 4px 4px; + margin: 0; +} +.pure-form .pure-group button { + margin: 0.35em 0; +} + +.pure-form .pure-input-1 { + width: 100%; +} +.pure-form .pure-input-3-4 { + width: 75%; +} +.pure-form .pure-input-2-3 { + width: 66%; +} +.pure-form .pure-input-1-2 { + width: 50%; +} +.pure-form .pure-input-1-3 { + width: 33%; +} +.pure-form .pure-input-1-4 { + width: 25%; +} + +/* Inline help for forms */ +/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ +.pure-form .pure-help-inline, +.pure-form-message-inline { + display: inline-block; + padding-left: 0.3em; + color: #666; + vertical-align: middle; + font-size: 0.875em; +} + +/* Block help for forms */ +.pure-form-message { + display: block; + color: #666; + font-size: 0.875em; +} + +@media only screen and (max-width : 480px) { + .pure-form button[type="submit"] { + margin: 0.7em 0 0; + } + + .pure-form input:not([type]), + .pure-form input[type="text"], + .pure-form input[type="password"], + .pure-form input[type="email"], + .pure-form input[type="url"], + .pure-form input[type="date"], + .pure-form input[type="month"], + .pure-form input[type="time"], + .pure-form input[type="datetime"], + .pure-form input[type="datetime-local"], + .pure-form input[type="week"], + .pure-form input[type="number"], + .pure-form input[type="search"], + .pure-form input[type="tel"], + .pure-form input[type="color"], + .pure-form label { + margin-bottom: 0.3em; + display: block; + } + + .pure-group input:not([type]), + .pure-group input[type="text"], + .pure-group input[type="password"], + .pure-group input[type="email"], + .pure-group input[type="url"], + .pure-group input[type="date"], + .pure-group input[type="month"], + .pure-group input[type="time"], + .pure-group input[type="datetime"], + .pure-group input[type="datetime-local"], + .pure-group input[type="week"], + .pure-group input[type="number"], + .pure-group input[type="search"], + .pure-group input[type="tel"], + .pure-group input[type="color"] { + margin-bottom: 0; + } + + .pure-form-aligned .pure-control-group label { + margin-bottom: 0.3em; + text-align: left; + display: block; + width: 100%; + } + + .pure-form-aligned .pure-controls { + margin: 1.5em 0 0 0; + } + + /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ + .pure-form .pure-help-inline, + .pure-form-message-inline, + .pure-form-message { + display: block; + font-size: 0.75em; + /* Increased bottom padding to make it group with its related input element. */ + padding: 0.2em 0 0.8em; + } +} + +/*csslint adjoining-classes: false, box-model:false*/ +.pure-menu { + box-sizing: border-box; +} + +.pure-menu-fixed { + position: fixed; + left: 0; + top: 0; + z-index: 3; +} + +.pure-menu-list, +.pure-menu-item { + position: relative; +} + +.pure-menu-list { + list-style: none; + margin: 0; + padding: 0; +} + +.pure-menu-item { + padding: 0; + margin: 0; + height: 100%; +} + +.pure-menu-link, +.pure-menu-heading { + display: block; + text-decoration: none; + white-space: nowrap; +} + +/* HORIZONTAL MENU */ +.pure-menu-horizontal { + width: 100%; + white-space: nowrap; +} + +.pure-menu-horizontal .pure-menu-list { + display: inline-block; +} + +/* Initial menus should be inline-block so that they are horizontal */ +.pure-menu-horizontal .pure-menu-item, +.pure-menu-horizontal .pure-menu-heading, +.pure-menu-horizontal .pure-menu-separator { + display: inline-block; + *display: inline; + zoom: 1; + vertical-align: middle; +} + +/* Submenus should still be display: block; */ +.pure-menu-item .pure-menu-item { + display: block; +} + +.pure-menu-children { + display: none; + position: absolute; + left: 100%; + top: 0; + margin: 0; + padding: 0; + z-index: 3; +} + +.pure-menu-horizontal .pure-menu-children { + left: 0; + top: auto; + width: inherit; +} + +.pure-menu-allow-hover:hover > .pure-menu-children, +.pure-menu-active > .pure-menu-children { + display: block; + position: absolute; +} + +/* Vertical Menus - show the dropdown arrow */ +.pure-menu-has-children > .pure-menu-link:after { + padding-left: 0.5em; + content: "\25B8"; + font-size: small; +} + +/* Horizontal Menus - show the dropdown arrow */ +.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after { + content: "\25BE"; +} + +/* scrollable menus */ +.pure-menu-scrollable { + overflow-y: scroll; + overflow-x: hidden; +} + +.pure-menu-scrollable .pure-menu-list { + display: block; +} + +.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list { + display: inline-block; +} + +.pure-menu-horizontal.pure-menu-scrollable { + white-space: nowrap; + overflow-y: hidden; + overflow-x: auto; + -ms-overflow-style: none; + -webkit-overflow-scrolling: touch; + /* a little extra padding for this style to allow for scrollbars */ + padding: .5em 0; +} + +.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar { + display: none; +} + +/* misc default styling */ + +.pure-menu-separator, +.pure-menu-horizontal .pure-menu-children .pure-menu-separator { + background-color: #ccc; + height: 1px; + margin: .3em 0; +} + +.pure-menu-horizontal .pure-menu-separator { + width: 1px; + height: 1.3em; + margin: 0 .3em ; +} + +/* Need to reset the separator since submenu is vertical */ +.pure-menu-horizontal .pure-menu-children .pure-menu-separator { + display: block; + width: auto; +} + +.pure-menu-heading { + text-transform: uppercase; + color: #565d64; +} + +.pure-menu-link { + color: #777; +} + +.pure-menu-children { + background-color: #fff; +} + +.pure-menu-link, +.pure-menu-disabled, +.pure-menu-heading { + padding: .5em 1em; +} + +.pure-menu-disabled { + opacity: .5; +} + +.pure-menu-disabled .pure-menu-link:hover { + background-color: transparent; +} + +.pure-menu-active > .pure-menu-link, +.pure-menu-link:hover, +.pure-menu-link:focus { + background-color: #eee; +} + +.pure-menu-selected .pure-menu-link, +.pure-menu-selected .pure-menu-link:visited { + color: #000; +} + +.pure-table { + /* Remove spacing between table cells (from Normalize.css) */ + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + border: 1px solid #cbcbcb; +} + +.pure-table caption { + color: #000; + font: italic 85%/1 arial, sans-serif; + padding: 1em 0; + text-align: center; +} + +.pure-table td, +.pure-table th { + border-left: 1px solid #cbcbcb;/* inner column border */ + border-width: 0 0 0 1px; + font-size: inherit; + margin: 0; + overflow: visible; /*to make ths where the title is really long work*/ + padding: 0.5em 1em; /* cell padding */ +} + +/* Consider removing this next declaration block, as it causes problems when +there's a rowspan on the first cell. Case added to the tests. issue#432 */ +.pure-table td:first-child, +.pure-table th:first-child { + border-left-width: 0; +} + +.pure-table thead { + background-color: #e0e0e0; + color: #000; + text-align: left; + vertical-align: bottom; +} + +/* +striping: + even - #fff (white) + odd - #f2f2f2 (light gray) +*/ +.pure-table td { + background-color: transparent; +} +.pure-table-odd td { + background-color: #f2f2f2; +} + +/* nth-child selector for modern browsers */ +.pure-table-striped tr:nth-child(2n-1) td { + background-color: #f2f2f2; +} + +/* BORDERED TABLES */ +.pure-table-bordered td { + border-bottom: 1px solid #cbcbcb; +} +.pure-table-bordered tbody > tr:last-child > td { + border-bottom-width: 0; +} + + +/* HORIZONTAL BORDERED TABLES */ + +.pure-table-horizontal td, +.pure-table-horizontal th { + border-width: 0 0 1px 0; + border-bottom: 1px solid #cbcbcb; +} +.pure-table-horizontal tbody > tr:last-child > td { + border-bottom-width: 0; +} diff --git a/src/webex/style/wallet.css b/src/webex/style/wallet.css new file mode 100644 index 000000000..752fc6d75 --- /dev/null +++ b/src/webex/style/wallet.css @@ -0,0 +1,222 @@ +#main { + border: solid 1px black; + border-radius: 10px; + margin-left: auto; + margin-right: auto; + margin-top: 2em; + max-width: 50%; + padding: 2em; +} + +header { + width: 100%; + height: 100px; + margin: 0; + padding: 0; + border-bottom: 1px solid black; +} + +header h1 { + font-size: 200%; + margin: 0; + padding: 0 0 0 120px; + position: relative; + top: 50%; + transform: translateY(-50%); +} + +header #logo { + float: left; + width: 100px; + height: 100px; + padding: 0; + margin: 0; + text-align: center; + border-right: 1px solid black; + background-image: url(/img/logo.png); + background-size: 100px; +} + +aside { + width: 100px; + float: left; +} + +section#main { + margin: auto; + padding: 20px; + border-left: 1px solid black; + height: 100%; + max-width: 50%; +} + +section#main h1:first-child { + margin-top: 0; +} + +h1 { + font-size: 160%; +} + +h2 { + font-size: 140%; +} + +h3 { + font-size: 120%; +} + +h4, h5, h6 { + font-size: 100%; +} + +.form-row { + padding-top: 5px; + padding-bottom: 5px; +} + +label { + padding-right: 1em; +} + +label::after { + content: ":"; +} + +input.url { + width: 25em; +} + +.formish { +} + +.json-key { + color: brown; +} +.json-value { + color: navy; + } +.json-string { + color: olive; +} + +button { + font-size: 120%; + padding: 0.5em; +} + +button.confirm-pay { + float: right; +} + +/* We use fading to hide slower DOM updates */ +.fade { + -webkit-animation: fade 0.7s; + animation: fade 0.7s; + opacity: 1; +} + +@-webkit-keyframes fade { + from {opacity: 0} + to {opacity: 1} +} +@keyframes fade { + from {opacity: 0} + to {opacity: 1} + } + +button.linky { + background:none!important; + border:none; + padding:0!important; + + font-family:arial,sans-serif; + color:#069; + text-decoration:underline; + cursor:pointer; +} + +table, th, td { + border: 1px solid black; +} + + +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; +} + +.button-success, +.button-destructive, +.button-warning, +.button-secondary { + color: white; + border-radius: 4px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); +} + +.button-success { + background: rgb(28, 184, 65); +} + +.button-destructive { + background: rgb(202, 60, 60); +} + +.button-warning { + background: rgb(223, 117, 20); +} + +.button-secondary { + background: rgb(66, 184, 221); +} + +a.actionLink { + color: black; +} diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts new file mode 100644 index 000000000..e5a502406 --- /dev/null +++ b/src/webex/wxApi.ts @@ -0,0 +1,174 @@ +/* + This file is part of TALER + (C) 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 + */ + +/** + * Interface to the wallet through WebExtension messaging. + */ + + +/** + * Imports. + */ +import { + AmountJson, + CoinRecord, + CurrencyRecord, + DenominationRecord, + ExchangeRecord, + PreCoinRecord, + ReserveCreationInfo, + ReserveRecord, +} from "../types"; + + +/** + * Query the wallet for the coins that would be used to withdraw + * from a given reserve. + */ +export function getReserveCreationInfo(baseUrl: string, + amount: AmountJson): Promise { + const m = { type: "reserve-creation-info", detail: { baseUrl, amount } }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(m, (resp) => { + if (resp.error) { + console.error("error response", resp); + const e = Error("call to reserve-creation-info failed"); + (e as any).errorResponse = resp; + reject(e); + return; + } + resolve(resp); + }); + }); +} + + +async function callBackend(type: string, detail?: any): Promise { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type, detail }, (resp) => { + if (resp && resp.error) { + reject(resp); + } else { + resolve(resp); + } + }); + }); +} + + +/** + * Get all exchanges the wallet knows about. + */ +export async function getExchanges(): Promise { + return await callBackend("get-exchanges"); +} + + +/** + * Get all currencies the exchange knows about. + */ +export async function getCurrencies(): Promise { + return await callBackend("get-currencies"); +} + + +/** + * Get information about a specific currency. + */ +export async function getCurrency(name: string): Promise { + return await callBackend("currency-info", {name}); +} + + +/** + * Get information about a specific exchange. + */ +export async function getExchangeInfo(baseUrl: string): Promise { + return await callBackend("exchange-info", {baseUrl}); +} + + +/** + * Replace an existing currency record with the one given. The currency to + * replace is specified inside the currency record. + */ +export async function updateCurrency(currencyRecord: CurrencyRecord): Promise { + return await callBackend("update-currency", { currencyRecord }); +} + + +/** + * Get all reserves the wallet has at an exchange. + */ +export async function getReserves(exchangeBaseUrl: string): Promise { + return await callBackend("get-reserves", { exchangeBaseUrl }); +} + + +/** + * Get all reserves for which a payback is available. + */ +export async function getPaybackReserves(): Promise { + return await callBackend("get-payback-reserves"); +} + + +/** + * Withdraw the payback that is available for a reserve. + */ +export async function withdrawPaybackReserve(reservePub: string): Promise { + return await callBackend("withdraw-payback-reserve", { reservePub }); +} + + +/** + * Get all coins withdrawn from the given exchange. + */ +export async function getCoins(exchangeBaseUrl: string): Promise { + return await callBackend("get-coins", { exchangeBaseUrl }); +} + + +/** + * Get all precoins withdrawn from the given exchange. + */ +export async function getPreCoins(exchangeBaseUrl: string): Promise { + return await callBackend("get-precoins", { exchangeBaseUrl }); +} + + +/** + * Get all denoms offered by the given exchange. + */ +export async function getDenoms(exchangeBaseUrl: string): Promise { + return await callBackend("get-denoms", { exchangeBaseUrl }); +} + + +/** + * Start refreshing a coin. + */ +export async function refresh(coinPub: string): Promise { + return await callBackend("refresh-coin", { coinPub }); +} + + +/** + * Request payback for a coin. Only works for non-refreshed coins. + */ +export async function payback(coinPub: string): Promise { + return await callBackend("payback-coin", { coinPub }); +} diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts new file mode 100644 index 000000000..35e1ff938 --- /dev/null +++ b/src/webex/wxBackend.ts @@ -0,0 +1,719 @@ +/* + This file is part of TALER + (C) 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 + */ + +/** + * Messaging for the WebExtensions wallet. Should contain + * parts that are specific for WebExtensions, but as little business + * logic as possible. + */ + + +/** + * Imports. + */ +import { Checkable } from "../checkable"; +import { BrowserHttpLib } from "../http"; +import * as logging from "../logging"; +import { + Index, + Store, +} from "../query"; +import { + AmountJson, + Contract, + Notifier, +} from "../types"; +import { + Badge, + ConfirmReserveRequest, + CreateReserveRequest, + OfferRecord, + Stores, + Wallet, +} from "../wallet"; + +import { ChromeBadge } from "./chromeBadge"; +import URI = require("urijs"); +import Port = chrome.runtime.Port; +import MessageSender = chrome.runtime.MessageSender; + + +const DB_NAME = "taler"; + +/** + * Current database version, should be incremented + * each time we do incompatible schema changes on the database. + * In the future we might consider adding migration functions for + * each version increment. + */ +const DB_VERSION = 17; + +type Handler = (detail: any, sender: MessageSender) => Promise; + +function makeHandlers(db: IDBDatabase, + wallet: Wallet): { [msg: string]: Handler } { + return { + ["balances"]: (detail, sender) => { + return wallet.getBalances(); + }, + ["dump-db"]: (detail, sender) => { + return exportDb(db); + }, + ["import-db"]: (detail, sender) => { + return importDb(db, detail.dump); + }, + ["get-tab-cookie"]: (detail, sender) => { + if (!sender || !sender.tab || !sender.tab.id) { + return Promise.resolve(); + } + const id: number = sender.tab.id; + const info: any = paymentRequestCookies[id] as any; + delete paymentRequestCookies[id]; + return Promise.resolve(info); + }, + ["ping"]: (detail, sender) => { + return Promise.resolve(); + }, + ["reset"]: (detail, sender) => { + if (db) { + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < db.objectStoreNames.length; i++) { + tx.objectStore(db.objectStoreNames[i]).clear(); + } + } + deleteDb(); + + chrome.browserAction.setBadgeText({ text: "" }); + console.log("reset done"); + // Response is synchronous + return Promise.resolve({}); + }, + ["create-reserve"]: (detail, sender) => { + const d = { + amount: detail.amount, + exchange: detail.exchange, + }; + const req = CreateReserveRequest.checked(d); + return wallet.createReserve(req); + }, + ["confirm-reserve"]: (detail, sender) => { + // TODO: make it a checkable + const d = { + reservePub: detail.reservePub, + }; + const req = ConfirmReserveRequest.checked(d); + return wallet.confirmReserve(req); + }, + ["generate-nonce"]: (detail, sender) => { + return wallet.generateNonce(); + }, + ["confirm-pay"]: (detail, sender) => { + let offer: OfferRecord; + try { + offer = OfferRecord.checked(detail.offer); + } catch (e) { + if (e instanceof Checkable.SchemaError) { + console.error("schema error:", e.message); + return Promise.resolve({ + detail, + error: "invalid contract", + hint: e.message, + }); + } else { + throw e; + } + } + + return wallet.confirmPay(offer); + }, + ["check-pay"]: (detail, sender) => { + let offer: OfferRecord; + try { + offer = OfferRecord.checked(detail.offer); + } catch (e) { + if (e instanceof Checkable.SchemaError) { + console.error("schema error:", e.message); + return Promise.resolve({ + detail, + error: "invalid contract", + hint: e.message, + }); + } else { + throw e; + } + } + return wallet.checkPay(offer); + }, + ["query-payment"]: (detail: any, sender: MessageSender) => { + if (sender.tab && sender.tab.id) { + rateLimitCache[sender.tab.id]++; + if (rateLimitCache[sender.tab.id] > 10) { + console.warn("rate limit for query-payment exceeded"); + const msg = { + error: "rate limit exceeded for query-payment", + hint: "Check for redirect loops", + rateLimitExceeded: true, + }; + return Promise.resolve(msg); + } + } + return wallet.queryPayment(detail.url); + }, + ["exchange-info"]: (detail) => { + if (!detail.baseUrl) { + return Promise.resolve({ error: "bad url" }); + } + return wallet.updateExchangeFromUrl(detail.baseUrl); + }, + ["currency-info"]: (detail) => { + if (!detail.name) { + return Promise.resolve({ error: "name missing" }); + } + return wallet.getCurrencyRecord(detail.name); + }, + ["hash-contract"]: (detail) => { + if (!detail.contract) { + return Promise.resolve({ error: "contract missing" }); + } + return wallet.hashContract(detail.contract).then((hash) => { + return { hash }; + }); + }, + ["put-history-entry"]: (detail: any) => { + if (!detail.historyEntry) { + return Promise.resolve({ error: "historyEntry missing" }); + } + return wallet.putHistory(detail.historyEntry); + }, + ["save-offer"]: (detail: any) => { + const offer = detail.offer; + if (!offer) { + return Promise.resolve({ error: "offer missing" }); + } + console.log("handling safe-offer", detail); + // FIXME: fully migrate to new terminology + const checkedOffer = OfferRecord.checked(offer); + return wallet.saveOffer(checkedOffer); + }, + ["reserve-creation-info"]: (detail, sender) => { + if (!detail.baseUrl || typeof detail.baseUrl !== "string") { + return Promise.resolve({ error: "bad url" }); + } + const amount = AmountJson.checked(detail.amount); + return wallet.getReserveCreationInfo(detail.baseUrl, amount); + }, + ["get-history"]: (detail, sender) => { + // TODO: limit history length + return wallet.getHistory(); + }, + ["get-offer"]: (detail, sender) => { + return wallet.getOffer(detail.offerId); + }, + ["get-exchanges"]: (detail, sender) => { + return wallet.getExchanges(); + }, + ["get-currencies"]: (detail, sender) => { + return wallet.getCurrencies(); + }, + ["update-currency"]: (detail, sender) => { + return wallet.updateCurrency(detail.currencyRecord); + }, + ["get-reserves"]: (detail, sender) => { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangeBaseUrl missing")); + } + return wallet.getReserves(detail.exchangeBaseUrl); + }, + ["get-payback-reserves"]: (detail, sender) => { + return wallet.getPaybackReserves(); + }, + ["withdraw-payback-reserve"]: (detail, sender) => { + if (typeof detail.reservePub !== "string") { + return Promise.reject(Error("reservePub missing")); + } + return wallet.withdrawPaybackReserve(detail.reservePub); + }, + ["get-coins"]: (detail, sender) => { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangBaseUrl missing")); + } + return wallet.getCoins(detail.exchangeBaseUrl); + }, + ["get-precoins"]: (detail, sender) => { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangBaseUrl missing")); + } + return wallet.getPreCoins(detail.exchangeBaseUrl); + }, + ["get-denoms"]: (detail, sender) => { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangBaseUrl missing")); + } + return wallet.getDenoms(detail.exchangeBaseUrl); + }, + ["refresh-coin"]: (detail, sender) => { + if (typeof detail.coinPub !== "string") { + return Promise.reject(Error("coinPub missing")); + } + return wallet.refresh(detail.coinPub); + }, + ["payback-coin"]: (detail, sender) => { + if (typeof detail.coinPub !== "string") { + return Promise.reject(Error("coinPub missing")); + } + return wallet.payback(detail.coinPub); + }, + ["payment-failed"]: (detail, sender) => { + // For now we just update exchanges (maybe the exchange did something + // wrong and the keys were messed up). + // FIXME: in the future we should look at what actually went wrong. + console.error("payment reported as failed"); + wallet.updateExchanges(); + return Promise.resolve(); + }, + ["payment-succeeded"]: (detail, sender) => { + const contractHash = detail.contractHash; + const merchantSig = detail.merchantSig; + if (!contractHash) { + return Promise.reject(Error("contractHash missing")); + } + if (!merchantSig) { + return Promise.reject(Error("merchantSig missing")); + } + return wallet.paymentSucceeded(contractHash, merchantSig); + }, + }; +} + + +async function dispatch(handlers: any, req: any, sender: any, sendResponse: any): Promise { + if (!(req.type in handlers)) { + console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); + try { + sendResponse({ error: "request unknown" }); + } catch (e) { + // might fail if tab disconnected + } + } + + try { + const p = handlers[req.type](req.detail, sender); + const r = await p; + try { + sendResponse(r); + } catch (e) { + // might fail if tab disconnected + } + } catch (e) { + console.log(`exception during wallet handler for '${req.type}'`); + console.log("request", req); + console.error(e); + let stack; + try { + stack = e.stack.toString(); + } catch (e) { + // might fail + } + try { + sendResponse({ + error: "exception", + hint: e.message, + stack, + }); + } catch (e) { + console.log(e); + // might fail if tab disconnected + } + } +} + + +class ChromeNotifier implements Notifier { + private ports: Port[] = []; + + constructor() { + chrome.runtime.onConnect.addListener((port) => { + console.log("got connect!"); + this.ports.push(port); + port.onDisconnect.addListener(() => { + const i = this.ports.indexOf(port); + if (i >= 0) { + this.ports.splice(i, 1); + } else { + console.error("port already removed"); + } + }); + }); + } + + notify() { + for (const p of this.ports) { + p.postMessage({ notify: true }); + } + } +} + + +/** + * Mapping from tab ID to payment information (if any). + */ +const paymentRequestCookies: { [n: number]: any } = {}; + + +/** + * Handle a HTTP response that has the "402 Payment Required" status. + * In this callback we don't have access to the body, and must communicate via + * shared state with the content script that will later be run later + * in this tab. + */ +function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any { + const headers: { [s: string]: string } = {}; + for (const kv of headerList) { + if (kv.value) { + headers[kv.name.toLowerCase()] = kv.value; + } + } + + const fields = { + contract_query: headers["x-taler-contract-query"], + contract_url: headers["x-taler-contract-url"], + offer_url: headers["x-taler-offer-url"], + }; + + const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0; + + if (!talerHeaderFound) { + // looks like it's not a taler request, it might be + // for a different payment system (or the shop is buggy) + console.log("ignoring non-taler 402 response"); + return; + } + + const payDetail = { + contract_url: fields.contract_url, + offer_url: fields.offer_url, + }; + + console.log("got pay detail", payDetail); + + // This cookie will be read by the injected content script + // in the tab that displays the page. + paymentRequestCookies[tabId] = { + payDetail, + type: "pay", + }; +} + + +function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHeader[], + url: string, tabId: number): any { + const headers: { [s: string]: string } = {}; + for (const kv of headerList) { + if (kv.value) { + headers[kv.name.toLowerCase()] = kv.value; + } + } + + const reservePub = headers["x-taler-reserve-pub"]; + if (reservePub !== undefined) { + console.log(`confirming reserve ${reservePub} via 201`); + wallet.confirmReserve({reservePub}); + return; + } + + const amount = headers["x-taler-amount"]; + if (amount) { + const callbackUrl = headers["x-taler-callback-url"]; + if (!callbackUrl) { + console.log("202 not understood (X-Taler-Callback-Url missing)"); + return; + } + let amountParsed; + try { + amountParsed = JSON.parse(amount); + } catch (e) { + const uri = new URI(chrome.extension.getURL("/src/pages/error.html")); + const p = { + message: `Can't parse amount ("${amount}"): ${e.message}`, + }; + const redirectUrl = uri.query(p).href(); + // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed + chrome.tabs.update(tabId, {url: redirectUrl}); + return; + } + const wtTypes = headers["x-taler-wt-types"]; + if (!wtTypes) { + console.log("202 not understood (X-Taler-Wt-Types missing)"); + return; + } + const params = { + amount, + bank_url: url, + callback_url: new URI(callbackUrl) .absoluteTo(url), + suggested_exchange_url: headers["x-taler-suggested-exchange"], + wt_types: wtTypes, + }; + const uri = new URI(chrome.extension.getURL("/src/pages/confirm-create-reserve.html")); + const redirectUrl = uri.query(params).href(); + console.log("redirecting to", redirectUrl); + // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed + chrome.tabs.update(tabId, {url: redirectUrl}); + return; + } + // no known headers found, not a taler request ... +} + + +// Rate limit cache for executePayment operations, to break redirect loops +let rateLimitCache: { [n: number]: number } = {}; + +function clearRateLimitCache() { + rateLimitCache = {}; +} + +/** + * Main function to run for the WebExtension backend. + * + * Sets up all event handlers and other machinery. + */ +export async function wxMain() { + window.onerror = (m, source, lineno, colno, error) => { + logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0); + }; + + chrome.browserAction.setBadgeText({ text: "" }); + const badge = new ChromeBadge(); + + chrome.tabs.query({}, (tabs) => { + for (const tab of tabs) { + if (!tab.url || !tab.id) { + return; + } + const uri = new URI(tab.url); + if (uri.protocol() === "http" || uri.protocol() === "https") { + console.log("injecting into existing tab", tab.id); + chrome.tabs.executeScript(tab.id, { file: "/dist/contentScript-bundle.js" }); + const code = ` + if (("taler" in window) || document.documentElement.getAttribute("data-taler-nojs")) { + document.dispatchEvent(new Event("taler-probe-result")); + } + `; + chrome.tabs.executeScript(tab.id, { code, runAt: "document_idle" }); + } + } + }); + + const tabTimers: {[n: number]: number[]} = {}; + + chrome.tabs.onRemoved.addListener((tabId, changeInfo) => { + const tt = tabTimers[tabId] || []; + for (const t of tt) { + chrome.extension.getBackgroundPage().clearTimeout(t); + } + }); + chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { + if (changeInfo.status !== "complete") { + return; + } + const timers: number[] = []; + + const addRun = (dt: number) => { + const id = chrome.extension.getBackgroundPage().setTimeout(run, dt); + timers.push(id); + }; + + const run = () => { + timers.shift(); + chrome.tabs.get(tabId, (tab) => { + if (chrome.runtime.lastError) { + return; + } + if (!tab.url || !tab.id) { + return; + } + const uri = new URI(tab.url); + if (!(uri.protocol() === "http" || uri.protocol() === "https")) { + return; + } + const code = ` + if (("taler" in window) || document.documentElement.getAttribute("data-taler-nojs")) { + document.dispatchEvent(new Event("taler-probe-result")); + } + `; + chrome.tabs.executeScript(tab.id!, { code, runAt: "document_start" }); + }); + }; + + addRun(0); + addRun(50); + addRun(300); + addRun(1000); + addRun(2000); + addRun(4000); + addRun(8000); + addRun(16000); + tabTimers[tabId] = timers; + }); + + chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000); + + let db: IDBDatabase; + try { + db = await openTalerDb(); + } catch (e) { + console.error("could not open database", e); + return; + } + const http = new BrowserHttpLib(); + const notifier = new ChromeNotifier(); + console.log("setting wallet"); + const wallet = new Wallet(db, http, badge!, notifier); + // Useful for debugging in the background page. + (window as any).talerWallet = wallet; + + // Handlers for messages coming directly from the content + // script on the page + const handlers = makeHandlers(db, wallet!); + chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { + dispatch(handlers, req, sender, sendResponse); + return true; + }); + + // Handlers for catching HTTP requests + chrome.webRequest.onHeadersReceived.addListener((details) => { + if (details.statusCode === 402) { + console.log(`got 402 from ${details.url}`); + return handleHttpPayment(details.responseHeaders || [], + details.url, + details.tabId); + } else if (details.statusCode === 202) { + return handleBankRequest(wallet!, details.responseHeaders || [], + details.url, + details.tabId); + } + }, { urls: [""] }, ["responseHeaders", "blocking"]); +} + + +/** + * Return a promise that resolves + * to the taler wallet db. + */ +function openTalerDb(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onerror = (e) => { + reject(e); + }; + req.onsuccess = (e) => { + resolve(req.result); + }; + req.onupgradeneeded = (e) => { + const db = req.result; + console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); + switch (e.oldVersion) { + case 0: // DB does not exist yet + + for (const n in Stores) { + if ((Stores as any)[n] instanceof Store) { + const si: Store = (Stores as any)[n]; + const s = db.createObjectStore(si.name, si.storeParams); + for (const indexName in (si as any)) { + if ((si as any)[indexName] instanceof Index) { + const ii: Index = (si as any)[indexName]; + s.createIndex(ii.indexName, ii.keyPath); + } + } + } + } + break; + default: + if (e.oldVersion !== DB_VERSION) { + window.alert("Incompatible wallet dababase version, please reset" + + " db."); + chrome.browserAction.setBadgeText({text: "err"}); + chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); + throw Error("incompatible DB"); + } + break; + } + }; + }); +} + + +function exportDb(db: IDBDatabase): Promise { + const dump = { + name: db.name, + stores: {} as {[s: string]: any}, + version: db.version, + }; + + return new Promise((resolve, reject) => { + + const tx = db.transaction(Array.from(db.objectStoreNames)); + tx.addEventListener("complete", () => { + resolve(dump); + }); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < db.objectStoreNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = {} as {[s: string]: any}; + dump.stores[name] = storeDump; + tx.objectStore(name) + .openCursor() + .addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + storeDump[cursor.key] = cursor.value; + cursor.continue(); + } + }); + } + }); +} + + +function importDb(db: IDBDatabase, dump: any): Promise { + console.log("importing db", dump); + return new Promise((resolve, reject) => { + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + if (dump.stores) { + for (const storeName in dump.stores) { + const objects = []; + const dumpStore = dump.stores[storeName]; + for (const key in dumpStore) { + objects.push(dumpStore[key]); + } + console.log(`importing ${objects.length} records into ${storeName}`); + const store = tx.objectStore(storeName); + const clearReq = store.clear(); + for (const obj of objects) { + store.put(obj); + } + } + } + tx.addEventListener("complete", () => { + resolve(); + }); + }); +} + + +function deleteDb() { + indexedDB.deleteDatabase(DB_NAME); +} diff --git a/src/wxApi.ts b/src/wxApi.ts deleted file mode 100644 index 8a95e75f5..000000000 --- a/src/wxApi.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - This file is part of TALER - (C) 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 - */ - -/** - * Interface to the wallet through WebExtension messaging. - */ - - -/** - * Imports. - */ -import { - AmountJson, - CoinRecord, - CurrencyRecord, - DenominationRecord, - ExchangeRecord, - PreCoinRecord, - ReserveCreationInfo, - ReserveRecord, -} from "./types"; - - -/** - * Query the wallet for the coins that would be used to withdraw - * from a given reserve. - */ -export function getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise { - const m = { type: "reserve-creation-info", detail: { baseUrl, amount } }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(m, (resp) => { - if (resp.error) { - console.error("error response", resp); - const e = Error("call to reserve-creation-info failed"); - (e as any).errorResponse = resp; - reject(e); - return; - } - resolve(resp); - }); - }); -} - - -async function callBackend(type: string, detail?: any): Promise { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage({ type, detail }, (resp) => { - if (resp && resp.error) { - reject(resp); - } else { - resolve(resp); - } - }); - }); -} - - -/** - * Get all exchanges the wallet knows about. - */ -export async function getExchanges(): Promise { - return await callBackend("get-exchanges"); -} - - -/** - * Get all currencies the exchange knows about. - */ -export async function getCurrencies(): Promise { - return await callBackend("get-currencies"); -} - - -/** - * Get information about a specific currency. - */ -export async function getCurrency(name: string): Promise { - return await callBackend("currency-info", {name}); -} - - -/** - * Get information about a specific exchange. - */ -export async function getExchangeInfo(baseUrl: string): Promise { - return await callBackend("exchange-info", {baseUrl}); -} - - -/** - * Replace an existing currency record with the one given. The currency to - * replace is specified inside the currency record. - */ -export async function updateCurrency(currencyRecord: CurrencyRecord): Promise { - return await callBackend("update-currency", { currencyRecord }); -} - - -/** - * Get all reserves the wallet has at an exchange. - */ -export async function getReserves(exchangeBaseUrl: string): Promise { - return await callBackend("get-reserves", { exchangeBaseUrl }); -} - - -/** - * Get all reserves for which a payback is available. - */ -export async function getPaybackReserves(): Promise { - return await callBackend("get-payback-reserves"); -} - - -/** - * Withdraw the payback that is available for a reserve. - */ -export async function withdrawPaybackReserve(reservePub: string): Promise { - return await callBackend("withdraw-payback-reserve", { reservePub }); -} - - -/** - * Get all coins withdrawn from the given exchange. - */ -export async function getCoins(exchangeBaseUrl: string): Promise { - return await callBackend("get-coins", { exchangeBaseUrl }); -} - - -/** - * Get all precoins withdrawn from the given exchange. - */ -export async function getPreCoins(exchangeBaseUrl: string): Promise { - return await callBackend("get-precoins", { exchangeBaseUrl }); -} - - -/** - * Get all denoms offered by the given exchange. - */ -export async function getDenoms(exchangeBaseUrl: string): Promise { - return await callBackend("get-denoms", { exchangeBaseUrl }); -} - - -/** - * Start refreshing a coin. - */ -export async function refresh(coinPub: string): Promise { - return await callBackend("refresh-coin", { coinPub }); -} - - -/** - * Request payback for a coin. Only works for non-refreshed coins. - */ -export async function payback(coinPub: string): Promise { - return await callBackend("payback-coin", { coinPub }); -} diff --git a/src/wxBackend.ts b/src/wxBackend.ts deleted file mode 100644 index a9a208dcd..000000000 --- a/src/wxBackend.ts +++ /dev/null @@ -1,718 +0,0 @@ -/* - This file is part of TALER - (C) 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 - */ - -/** - * Messaging for the WebExtensions wallet. Should contain - * parts that are specific for WebExtensions, but as little business - * logic as possible. - */ - - -/** - * Imports. - */ -import { Checkable } from "./checkable"; -import { ChromeBadge } from "./chromeBadge"; -import { BrowserHttpLib } from "./http"; -import * as logging from "./logging"; -import { - Index, - Store, -} from "./query"; -import { - AmountJson, - Contract, - Notifier, -} from "./types"; -import URI = require("urijs"); -import { - Badge, - ConfirmReserveRequest, - CreateReserveRequest, - OfferRecord, - Stores, - Wallet, -} from "./wallet"; -import Port = chrome.runtime.Port; -import MessageSender = chrome.runtime.MessageSender; - - -const DB_NAME = "taler"; - -/** - * Current database version, should be incremented - * each time we do incompatible schema changes on the database. - * In the future we might consider adding migration functions for - * each version increment. - */ -const DB_VERSION = 17; - -type Handler = (detail: any, sender: MessageSender) => Promise; - -function makeHandlers(db: IDBDatabase, - wallet: Wallet): { [msg: string]: Handler } { - return { - ["balances"]: (detail, sender) => { - return wallet.getBalances(); - }, - ["dump-db"]: (detail, sender) => { - return exportDb(db); - }, - ["import-db"]: (detail, sender) => { - return importDb(db, detail.dump); - }, - ["get-tab-cookie"]: (detail, sender) => { - if (!sender || !sender.tab || !sender.tab.id) { - return Promise.resolve(); - } - const id: number = sender.tab.id; - const info: any = paymentRequestCookies[id] as any; - delete paymentRequestCookies[id]; - return Promise.resolve(info); - }, - ["ping"]: (detail, sender) => { - return Promise.resolve(); - }, - ["reset"]: (detail, sender) => { - if (db) { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < db.objectStoreNames.length; i++) { - tx.objectStore(db.objectStoreNames[i]).clear(); - } - } - deleteDb(); - - chrome.browserAction.setBadgeText({ text: "" }); - console.log("reset done"); - // Response is synchronous - return Promise.resolve({}); - }, - ["create-reserve"]: (detail, sender) => { - const d = { - amount: detail.amount, - exchange: detail.exchange, - }; - const req = CreateReserveRequest.checked(d); - return wallet.createReserve(req); - }, - ["confirm-reserve"]: (detail, sender) => { - // TODO: make it a checkable - const d = { - reservePub: detail.reservePub, - }; - const req = ConfirmReserveRequest.checked(d); - return wallet.confirmReserve(req); - }, - ["generate-nonce"]: (detail, sender) => { - return wallet.generateNonce(); - }, - ["confirm-pay"]: (detail, sender) => { - let offer: OfferRecord; - try { - offer = OfferRecord.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - detail, - error: "invalid contract", - hint: e.message, - }); - } else { - throw e; - } - } - - return wallet.confirmPay(offer); - }, - ["check-pay"]: (detail, sender) => { - let offer: OfferRecord; - try { - offer = OfferRecord.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - detail, - error: "invalid contract", - hint: e.message, - }); - } else { - throw e; - } - } - return wallet.checkPay(offer); - }, - ["query-payment"]: (detail: any, sender: MessageSender) => { - if (sender.tab && sender.tab.id) { - rateLimitCache[sender.tab.id]++; - if (rateLimitCache[sender.tab.id] > 10) { - console.warn("rate limit for query-payment exceeded"); - const msg = { - error: "rate limit exceeded for query-payment", - hint: "Check for redirect loops", - rateLimitExceeded: true, - }; - return Promise.resolve(msg); - } - } - return wallet.queryPayment(detail.url); - }, - ["exchange-info"]: (detail) => { - if (!detail.baseUrl) { - return Promise.resolve({ error: "bad url" }); - } - return wallet.updateExchangeFromUrl(detail.baseUrl); - }, - ["currency-info"]: (detail) => { - if (!detail.name) { - return Promise.resolve({ error: "name missing" }); - } - return wallet.getCurrencyRecord(detail.name); - }, - ["hash-contract"]: (detail) => { - if (!detail.contract) { - return Promise.resolve({ error: "contract missing" }); - } - return wallet.hashContract(detail.contract).then((hash) => { - return { hash }; - }); - }, - ["put-history-entry"]: (detail: any) => { - if (!detail.historyEntry) { - return Promise.resolve({ error: "historyEntry missing" }); - } - return wallet.putHistory(detail.historyEntry); - }, - ["save-offer"]: (detail: any) => { - const offer = detail.offer; - if (!offer) { - return Promise.resolve({ error: "offer missing" }); - } - console.log("handling safe-offer", detail); - // FIXME: fully migrate to new terminology - const checkedOffer = OfferRecord.checked(offer); - return wallet.saveOffer(checkedOffer); - }, - ["reserve-creation-info"]: (detail, sender) => { - if (!detail.baseUrl || typeof detail.baseUrl !== "string") { - return Promise.resolve({ error: "bad url" }); - } - const amount = AmountJson.checked(detail.amount); - return wallet.getReserveCreationInfo(detail.baseUrl, amount); - }, - ["get-history"]: (detail, sender) => { - // TODO: limit history length - return wallet.getHistory(); - }, - ["get-offer"]: (detail, sender) => { - return wallet.getOffer(detail.offerId); - }, - ["get-exchanges"]: (detail, sender) => { - return wallet.getExchanges(); - }, - ["get-currencies"]: (detail, sender) => { - return wallet.getCurrencies(); - }, - ["update-currency"]: (detail, sender) => { - return wallet.updateCurrency(detail.currencyRecord); - }, - ["get-reserves"]: (detail, sender) => { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangeBaseUrl missing")); - } - return wallet.getReserves(detail.exchangeBaseUrl); - }, - ["get-payback-reserves"]: (detail, sender) => { - return wallet.getPaybackReserves(); - }, - ["withdraw-payback-reserve"]: (detail, sender) => { - if (typeof detail.reservePub !== "string") { - return Promise.reject(Error("reservePub missing")); - } - return wallet.withdrawPaybackReserve(detail.reservePub); - }, - ["get-coins"]: (detail, sender) => { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getCoins(detail.exchangeBaseUrl); - }, - ["get-precoins"]: (detail, sender) => { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getPreCoins(detail.exchangeBaseUrl); - }, - ["get-denoms"]: (detail, sender) => { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getDenoms(detail.exchangeBaseUrl); - }, - ["refresh-coin"]: (detail, sender) => { - if (typeof detail.coinPub !== "string") { - return Promise.reject(Error("coinPub missing")); - } - return wallet.refresh(detail.coinPub); - }, - ["payback-coin"]: (detail, sender) => { - if (typeof detail.coinPub !== "string") { - return Promise.reject(Error("coinPub missing")); - } - return wallet.payback(detail.coinPub); - }, - ["payment-failed"]: (detail, sender) => { - // For now we just update exchanges (maybe the exchange did something - // wrong and the keys were messed up). - // FIXME: in the future we should look at what actually went wrong. - console.error("payment reported as failed"); - wallet.updateExchanges(); - return Promise.resolve(); - }, - ["payment-succeeded"]: (detail, sender) => { - const contractHash = detail.contractHash; - const merchantSig = detail.merchantSig; - if (!contractHash) { - return Promise.reject(Error("contractHash missing")); - } - if (!merchantSig) { - return Promise.reject(Error("merchantSig missing")); - } - return wallet.paymentSucceeded(contractHash, merchantSig); - }, - }; -} - - -async function dispatch(handlers: any, req: any, sender: any, sendResponse: any): Promise { - if (!(req.type in handlers)) { - console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); - try { - sendResponse({ error: "request unknown" }); - } catch (e) { - // might fail if tab disconnected - } - } - - try { - const p = handlers[req.type](req.detail, sender); - const r = await p; - try { - sendResponse(r); - } catch (e) { - // might fail if tab disconnected - } - } catch (e) { - console.log(`exception during wallet handler for '${req.type}'`); - console.log("request", req); - console.error(e); - let stack; - try { - stack = e.stack.toString(); - } catch (e) { - // might fail - } - try { - sendResponse({ - error: "exception", - hint: e.message, - stack, - }); - } catch (e) { - console.log(e); - // might fail if tab disconnected - } - } -} - - -class ChromeNotifier implements Notifier { - private ports: Port[] = []; - - constructor() { - chrome.runtime.onConnect.addListener((port) => { - console.log("got connect!"); - this.ports.push(port); - port.onDisconnect.addListener(() => { - const i = this.ports.indexOf(port); - if (i >= 0) { - this.ports.splice(i, 1); - } else { - console.error("port already removed"); - } - }); - }); - } - - notify() { - for (const p of this.ports) { - p.postMessage({ notify: true }); - } - } -} - - -/** - * Mapping from tab ID to payment information (if any). - */ -const paymentRequestCookies: { [n: number]: any } = {}; - - -/** - * Handle a HTTP response that has the "402 Payment Required" status. - * In this callback we don't have access to the body, and must communicate via - * shared state with the content script that will later be run later - * in this tab. - */ -function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any { - const headers: { [s: string]: string } = {}; - for (const kv of headerList) { - if (kv.value) { - headers[kv.name.toLowerCase()] = kv.value; - } - } - - const fields = { - contract_query: headers["x-taler-contract-query"], - contract_url: headers["x-taler-contract-url"], - offer_url: headers["x-taler-offer-url"], - }; - - const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0; - - if (!talerHeaderFound) { - // looks like it's not a taler request, it might be - // for a different payment system (or the shop is buggy) - console.log("ignoring non-taler 402 response"); - return; - } - - const payDetail = { - contract_url: fields.contract_url, - offer_url: fields.offer_url, - }; - - console.log("got pay detail", payDetail); - - // This cookie will be read by the injected content script - // in the tab that displays the page. - paymentRequestCookies[tabId] = { - payDetail, - type: "pay", - }; -} - - -function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHeader[], - url: string, tabId: number): any { - const headers: { [s: string]: string } = {}; - for (const kv of headerList) { - if (kv.value) { - headers[kv.name.toLowerCase()] = kv.value; - } - } - - const reservePub = headers["x-taler-reserve-pub"]; - if (reservePub !== undefined) { - console.log(`confirming reserve ${reservePub} via 201`); - wallet.confirmReserve({reservePub}); - return; - } - - const amount = headers["x-taler-amount"]; - if (amount) { - const callbackUrl = headers["x-taler-callback-url"]; - if (!callbackUrl) { - console.log("202 not understood (X-Taler-Callback-Url missing)"); - return; - } - let amountParsed; - try { - amountParsed = JSON.parse(amount); - } catch (e) { - const uri = new URI(chrome.extension.getURL("/src/pages/error.html")); - const p = { - message: `Can't parse amount ("${amount}"): ${e.message}`, - }; - const redirectUrl = uri.query(p).href(); - // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed - chrome.tabs.update(tabId, {url: redirectUrl}); - return; - } - const wtTypes = headers["x-taler-wt-types"]; - if (!wtTypes) { - console.log("202 not understood (X-Taler-Wt-Types missing)"); - return; - } - const params = { - amount, - bank_url: url, - callback_url: new URI(callbackUrl) .absoluteTo(url), - suggested_exchange_url: headers["x-taler-suggested-exchange"], - wt_types: wtTypes, - }; - const uri = new URI(chrome.extension.getURL("/src/pages/confirm-create-reserve.html")); - const redirectUrl = uri.query(params).href(); - console.log("redirecting to", redirectUrl); - // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed - chrome.tabs.update(tabId, {url: redirectUrl}); - return; - } - // no known headers found, not a taler request ... -} - - -// Rate limit cache for executePayment operations, to break redirect loops -let rateLimitCache: { [n: number]: number } = {}; - -function clearRateLimitCache() { - rateLimitCache = {}; -} - -/** - * Main function to run for the WebExtension backend. - * - * Sets up all event handlers and other machinery. - */ -export async function wxMain() { - window.onerror = (m, source, lineno, colno, error) => { - logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0); - }; - - chrome.browserAction.setBadgeText({ text: "" }); - const badge = new ChromeBadge(); - - chrome.tabs.query({}, (tabs) => { - for (const tab of tabs) { - if (!tab.url || !tab.id) { - return; - } - const uri = new URI(tab.url); - if (uri.protocol() === "http" || uri.protocol() === "https") { - console.log("injecting into existing tab", tab.id); - chrome.tabs.executeScript(tab.id, { file: "/dist/contentScript-bundle.js" }); - const code = ` - if (("taler" in window) || document.documentElement.getAttribute("data-taler-nojs")) { - document.dispatchEvent(new Event("taler-probe-result")); - } - `; - chrome.tabs.executeScript(tab.id, { code, runAt: "document_idle" }); - } - } - }); - - const tabTimers: {[n: number]: number[]} = {}; - - chrome.tabs.onRemoved.addListener((tabId, changeInfo) => { - const tt = tabTimers[tabId] || []; - for (const t of tt) { - chrome.extension.getBackgroundPage().clearTimeout(t); - } - }); - chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { - if (changeInfo.status !== "complete") { - return; - } - const timers: number[] = []; - - const addRun = (dt: number) => { - const id = chrome.extension.getBackgroundPage().setTimeout(run, dt); - timers.push(id); - }; - - const run = () => { - timers.shift(); - chrome.tabs.get(tabId, (tab) => { - if (chrome.runtime.lastError) { - return; - } - if (!tab.url || !tab.id) { - return; - } - const uri = new URI(tab.url); - if (!(uri.protocol() === "http" || uri.protocol() === "https")) { - return; - } - const code = ` - if (("taler" in window) || document.documentElement.getAttribute("data-taler-nojs")) { - document.dispatchEvent(new Event("taler-probe-result")); - } - `; - chrome.tabs.executeScript(tab.id!, { code, runAt: "document_start" }); - }); - }; - - addRun(0); - addRun(50); - addRun(300); - addRun(1000); - addRun(2000); - addRun(4000); - addRun(8000); - addRun(16000); - tabTimers[tabId] = timers; - }); - - chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000); - - let db: IDBDatabase; - try { - db = await openTalerDb(); - } catch (e) { - console.error("could not open database", e); - return; - } - const http = new BrowserHttpLib(); - const notifier = new ChromeNotifier(); - console.log("setting wallet"); - const wallet = new Wallet(db, http, badge!, notifier); - // Useful for debugging in the background page. - (window as any).talerWallet = wallet; - - // Handlers for messages coming directly from the content - // script on the page - const handlers = makeHandlers(db, wallet!); - chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - dispatch(handlers, req, sender, sendResponse); - return true; - }); - - // Handlers for catching HTTP requests - chrome.webRequest.onHeadersReceived.addListener((details) => { - if (details.statusCode === 402) { - console.log(`got 402 from ${details.url}`); - return handleHttpPayment(details.responseHeaders || [], - details.url, - details.tabId); - } else if (details.statusCode === 202) { - return handleBankRequest(wallet!, details.responseHeaders || [], - details.url, - details.tabId); - } - }, { urls: [""] }, ["responseHeaders", "blocking"]); -} - - -/** - * Return a promise that resolves - * to the taler wallet db. - */ -function openTalerDb(): Promise { - return new Promise((resolve, reject) => { - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onerror = (e) => { - reject(e); - }; - req.onsuccess = (e) => { - resolve(req.result); - }; - req.onupgradeneeded = (e) => { - const db = req.result; - console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); - switch (e.oldVersion) { - case 0: // DB does not exist yet - - for (const n in Stores) { - if ((Stores as any)[n] instanceof Store) { - const si: Store = (Stores as any)[n]; - const s = db.createObjectStore(si.name, si.storeParams); - for (const indexName in (si as any)) { - if ((si as any)[indexName] instanceof Index) { - const ii: Index = (si as any)[indexName]; - s.createIndex(ii.indexName, ii.keyPath); - } - } - } - } - break; - default: - if (e.oldVersion !== DB_VERSION) { - window.alert("Incompatible wallet dababase version, please reset" + - " db."); - chrome.browserAction.setBadgeText({text: "err"}); - chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); - throw Error("incompatible DB"); - } - break; - } - }; - }); -} - - -function exportDb(db: IDBDatabase): Promise { - const dump = { - name: db.name, - stores: {} as {[s: string]: any}, - version: db.version, - }; - - return new Promise((resolve, reject) => { - - const tx = db.transaction(Array.from(db.objectStoreNames)); - tx.addEventListener("complete", () => { - resolve(dump); - }); - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < db.objectStoreNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = {} as {[s: string]: any}; - dump.stores[name] = storeDump; - tx.objectStore(name) - .openCursor() - .addEventListener("success", (e: Event) => { - const cursor = (e.target as any).result; - if (cursor) { - storeDump[cursor.key] = cursor.value; - cursor.continue(); - } - }); - } - }); -} - - -function importDb(db: IDBDatabase, dump: any): Promise { - console.log("importing db", dump); - return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - if (dump.stores) { - for (const storeName in dump.stores) { - const objects = []; - const dumpStore = dump.stores[storeName]; - for (const key in dumpStore) { - objects.push(dumpStore[key]); - } - console.log(`importing ${objects.length} records into ${storeName}`); - const store = tx.objectStore(storeName); - const clearReq = store.clear(); - for (const obj of objects) { - store.put(obj); - } - } - } - tx.addEventListener("complete", () => { - resolve(); - }); - }); -} - - -function deleteDb() { - indexedDB.deleteDatabase(DB_NAME); -} -- cgit v1.2.3