summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-08-03 13:00:48 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-08-03 13:01:05 +0530
commitffd2a62c3f7df94365980302fef3bc3376b48182 (patch)
tree270af6f16b4cc7f5da2afdba55c8bc9dbea5eca5 /packages/taler-wallet-webextension
parentaa481e42675fb7c4dcbbeec0ba1c61e1953b9596 (diff)
downloadwallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.gz
wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.bz2
wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.zip
modularize repo, use pnpm, improve typechecking
Diffstat (limited to 'packages/taler-wallet-webextension')
-rw-r--r--packages/taler-wallet-webextension/package.json41
-rw-r--r--packages/taler-wallet-webextension/rollup.config.js212
-rw-r--r--packages/taler-wallet-webextension/src/background.ts30
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js44
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map1
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts43
-rw-r--r--packages/taler-wallet-webextension/src/browserHttpLib.ts129
-rw-r--r--packages/taler-wallet-webextension/src/browserWorkerEntry.ts74
-rw-r--r--packages/taler-wallet-webextension/src/chromeBadge.ts288
-rw-r--r--packages/taler-wallet-webextension/src/compat.js61
-rw-r--r--packages/taler-wallet-webextension/src/compat.ts85
-rw-r--r--packages/taler-wallet-webextension/src/i18n-test.tsx68
-rw-r--r--packages/taler-wallet-webextension/src/i18n.tsx206
-rw-r--r--packages/taler-wallet-webextension/src/pageEntryPoint.ts72
-rw-r--r--packages/taler-wallet-webextension/src/pages/pay.tsx180
-rw-r--r--packages/taler-wallet-webextension/src/pages/payback.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/pages/popup.tsx502
-rw-r--r--packages/taler-wallet-webextension/src/pages/refund.tsx89
-rw-r--r--packages/taler-wallet-webextension/src/pages/reset-required.tsx93
-rw-r--r--packages/taler-wallet-webextension/src/pages/return-coins.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/pages/tip.tsx103
-rw-r--r--packages/taler-wallet-webextension/src/pages/welcome.tsx190
-rw-r--r--packages/taler-wallet-webextension/src/pages/withdraw.tsx229
-rw-r--r--packages/taler-wallet-webextension/src/permissions.ts20
-rw-r--r--packages/taler-wallet-webextension/src/renderHtml.tsx341
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts239
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts566
-rw-r--r--packages/taler-wallet-webextension/tsconfig.json19
-rw-r--r--packages/taler-wallet-webextension/webextension/manifest.json49
-rwxr-xr-xpackages/taler-wallet-webextension/webextension/pack.sh23
-rw-r--r--packages/taler-wallet-webextension/webextension/static/add-auditor.html33
-rw-r--r--packages/taler-wallet-webextension/webextension/static/auditors.html35
-rw-r--r--packages/taler-wallet-webextension/webextension/static/background.html11
-rw-r--r--packages/taler-wallet-webextension/webextension/static/benchmark.html16
-rw-r--r--packages/taler-wallet-webextension/webextension/static/img/icon.pngbin0 -> 830 bytes
-rw-r--r--packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.pngbin0 -> 65674 bytes
l---------packages/taler-wallet-webextension/webextension/static/img/logo.png1
-rw-r--r--packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg53
-rw-r--r--packages/taler-wallet-webextension/webextension/static/pay.html73
-rw-r--r--packages/taler-wallet-webextension/webextension/static/payback.html34
-rw-r--r--packages/taler-wallet-webextension/webextension/static/popup.html15
-rw-r--r--packages/taler-wallet-webextension/webextension/static/refund.html19
-rw-r--r--packages/taler-wallet-webextension/webextension/static/reset-required.html25
-rw-r--r--packages/taler-wallet-webextension/webextension/static/return-coins.html16
-rw-r--r--packages/taler-wallet-webextension/webextension/static/style/popup.css185
-rw-r--r--packages/taler-wallet-webextension/webextension/static/style/pure.css1513
-rw-r--r--packages/taler-wallet-webextension/webextension/static/style/wallet.css290
-rw-r--r--packages/taler-wallet-webextension/webextension/static/tip.html19
-rw-r--r--packages/taler-wallet-webextension/webextension/static/welcome.html24
-rw-r--r--packages/taler-wallet-webextension/webextension/static/withdraw.html22
50 files changed, 6441 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
new file mode 100644
index 000000000..b60d4ea98
--- /dev/null
+++ b/packages/taler-wallet-webextension/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "taler-wallet-webextension",
+ "version": "0.0.15",
+ "description": "GNU Taler Wallet browser extension",
+ "main": "./build/index.js",
+ "types": "./build/index.d.ts",
+ "author": "Florian Dold",
+ "license": "AGPL-3.0-or-later",
+ "private": false,
+ "scripts": {
+ "test": "tsc && ava",
+ "compile": "tsc"
+ },
+ "dependencies": {
+ "moment": "^2.27.0",
+ "taler-wallet-core": "workspace:*",
+ "tslib": "^2.0.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^14.0.0",
+ "@rollup/plugin-json": "^4.1.0",
+ "@rollup/plugin-node-resolve": "^8.4.0",
+ "@rollup/plugin-replace": "^2.3.3",
+ "@rollup/plugin-typescript": "^5.0.2",
+ "@types/chrome": "^0.0.103",
+ "@types/enzyme": "^3.10.5",
+ "@types/enzyme-adapter-react-16": "^1.0.6",
+ "@types/node": "^14.0.27",
+ "@types/react": "^16.9.44",
+ "@types/react-dom": "^16.9.8",
+ "ava": "3.11.0",
+ "enzyme": "^3.11.0",
+ "enzyme-adapter-react-16": "^1.15.2",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "rollup": "^2.23.0",
+ "rollup-plugin-sourcemaps": "^0.6.2",
+ "rollup-plugin-terser": "^6.1.0",
+ "typescript": "^3.9.7"
+ }
+}
diff --git a/packages/taler-wallet-webextension/rollup.config.js b/packages/taler-wallet-webextension/rollup.config.js
new file mode 100644
index 000000000..25ce768b4
--- /dev/null
+++ b/packages/taler-wallet-webextension/rollup.config.js
@@ -0,0 +1,212 @@
+// rollup.config.js
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import json from "@rollup/plugin-json";
+import replace from "@rollup/plugin-replace";
+import builtins from "builtin-modules";
+import { terser } from "rollup-plugin-terser";
+import typescript from "@rollup/plugin-typescript";
+
+// Base settings to use
+const baseTypescriptCompilerSettings = {
+ target: "ES6",
+ jsx: "react",
+ reactNamespace: "React",
+ moduleResolution: "node",
+ sourceMap: true,
+ lib: ["es6", "dom"],
+ noImplicitReturns: true,
+ noFallthroughCasesInSwitch: true,
+ strict: true,
+ strictPropertyInitialization: false,
+ noImplicitAny: true,
+ noImplicitThis: true,
+ allowJs: true,
+ checkJs: true,
+ incremental: false,
+ esModuleInterop: true,
+ importHelpers: true,
+ module: "ESNext",
+ include: ["src/**/*.+(ts|tsx)"],
+ rootDir: "./src",
+};
+
+const walletCli = {
+ input: "src/headless/taler-wallet-cli.ts",
+ output: {
+ file: "dist/standalone/taler-wallet-cli.js",
+ format: "cjs",
+ },
+ external: builtins,
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js", ".ts"],
+ ignoreGlobal: false, // Default: false
+ sourceMap: false,
+ ignore: ["taler-wallet"],
+ }),
+
+ json(),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const walletAndroid = {
+ input: "src/android/index.ts",
+ output: {
+ //dir: "dist/standalone",
+ file: "dist/standalone/taler-wallet-android.js",
+ format: "cjs",
+ exports: "named",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: ["node_modules/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const webExtensionPageEntryPoint = {
+ input: "src/webex/pageEntryPoint.ts",
+ output: {
+ file: "dist/webextension/pageEntryPoint.js",
+ format: "iife",
+ exports: "none",
+ name: "webExtensionPageEntry",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ terser(),
+
+ replace({
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const webExtensionBackgroundPageScript = {
+ input: "src/webex/background.ts",
+ output: {
+ file: "dist/webextension/background.js",
+ format: "iife",
+ exports: "none",
+ name: "webExtensionBackgroundScript",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ terser(),
+
+ replace({
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet", "crypto"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const webExtensionCryptoWorker = {
+ input: "src/crypto/workers/browserWorkerEntry.ts",
+ output: {
+ file: "dist/webextension/browserWorkerEntry.js",
+ format: "iife",
+ exports: "none",
+ name: "webExtensionCryptoWorker",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ terser(),
+
+ replace({
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet", "crypto"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+export default [
+ walletCli,
+ walletAndroid,
+ webExtensionPageEntryPoint,
+ webExtensionBackgroundPageScript,
+ webExtensionCryptoWorker,
+];
diff --git a/packages/taler-wallet-webextension/src/background.ts b/packages/taler-wallet-webextension/src/background.ts
new file mode 100644
index 000000000..dbc540df4
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Entry point for the background page.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { wxMain } from "./wxBackend";
+
+window.addEventListener("load", () => {
+ wxMain();
+});
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
new file mode 100644
index 000000000..e9492a2fb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
@@ -0,0 +1,44 @@
+"use strict";
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.BrowserCryptoWorkerFactory = void 0;
+/**
+ * API to access the Taler crypto worker thread.
+ * @author Florian Dold
+ */
+class BrowserCryptoWorkerFactory {
+ startWorker() {
+ const workerCtor = Worker;
+ const workerPath = "/browserWorkerEntry.js";
+ return new workerCtor(workerPath);
+ }
+ getConcurrency() {
+ let concurrency = 2;
+ try {
+ // only works in the browser
+ // tslint:disable-next-line:no-string-literal
+ concurrency = navigator["hardwareConcurrency"];
+ concurrency = Math.max(1, Math.ceil(concurrency / 2));
+ }
+ catch (e) {
+ concurrency = 2;
+ }
+ return concurrency;
+ }
+}
+exports.BrowserCryptoWorkerFactory = BrowserCryptoWorkerFactory;
+//# sourceMappingURL=browserCryptoWorkerFactory.js.map \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map
new file mode 100644
index 000000000..db56d4451
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"browserCryptoWorkerFactory.js","sourceRoot":"","sources":["browserCryptoWorkerFactory.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH;;;GAGG;AAEH,MAAa,0BAA0B;IACrC,WAAW;QACT,MAAM,UAAU,GAAG,MAAM,CAAC;QAC1B,MAAM,UAAU,GAAG,wBAAwB,CAAC;QAC5C,OAAO,IAAI,UAAU,CAAC,UAAU,CAAiB,CAAC;IACpD,CAAC;IAED,cAAc;QACZ,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI;YACF,4BAA4B;YAC5B,6CAA6C;YAC7C,WAAW,GAAI,SAAiB,CAAC,qBAAqB,CAAC,CAAC;YACxD,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC;SACvD;QAAC,OAAO,CAAC,EAAE;YACV,WAAW,GAAG,CAAC,CAAC;SACjB;QACD,OAAO,WAAW,CAAC;IACrB,CAAC;CACF;AAnBD,gEAmBC"} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
new file mode 100644
index 000000000..b91f49f17
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
@@ -0,0 +1,43 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * API to access the Taler crypto worker thread.
+ * @author Florian Dold
+ */
+
+import type { CryptoWorker, CryptoWorkerFactory } from "taler-wallet-core";
+
+export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory {
+ startWorker(): CryptoWorker {
+ const workerCtor = Worker;
+ const workerPath = "/browserWorkerEntry.js";
+ return new workerCtor(workerPath) as CryptoWorker;
+ }
+
+ getConcurrency(): number {
+ let concurrency = 2;
+ try {
+ // only works in the browser
+ // tslint:disable-next-line:no-string-literal
+ concurrency = (navigator as any)["hardwareConcurrency"];
+ concurrency = Math.max(1, Math.ceil(concurrency / 2));
+ } catch (e) {
+ concurrency = 2;
+ }
+ return concurrency;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts b/packages/taler-wallet-webextension/src/browserHttpLib.ts
new file mode 100644
index 000000000..2782e4a14
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserHttpLib.ts
@@ -0,0 +1,129 @@
+
+import { httpLib, OperationFailedError, Logger } from "taler-wallet-core";
+import { TalerErrorCode } from "taler-wallet-core/lib/TalerErrorCode";
+
+const logger = new Logger("browserHttpLib");
+
+/**
+ * An implementation of the [[HttpRequestLibrary]] using the
+ * browser's XMLHttpRequest.
+ */
+export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
+ private req(
+ method: string,
+ url: string,
+ requestBody?: any,
+ options?: httpLib.HttpRequestOptions,
+ ): Promise<httpLib.HttpResponse> {
+ return new Promise<httpLib.HttpResponse>((resolve, reject) => {
+ const myRequest = new XMLHttpRequest();
+ myRequest.open(method, url);
+ if (options?.headers) {
+ for (const headerName in options.headers) {
+ myRequest.setRequestHeader(headerName, options.headers[headerName]);
+ }
+ }
+ myRequest.setRequestHeader;
+ if (requestBody) {
+ myRequest.send(requestBody);
+ } else {
+ myRequest.send();
+ }
+
+ myRequest.onerror = (e) => {
+ logger.error("http request error");
+ reject(
+ OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ "Could not make request",
+ {
+ requestUrl: url,
+ },
+ ),
+ );
+ };
+
+ myRequest.addEventListener("readystatechange", (e) => {
+ if (myRequest.readyState === XMLHttpRequest.DONE) {
+ if (myRequest.status === 0) {
+ const exc = OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ "HTTP request failed (status 0, maybe URI scheme was wrong?)",
+ {
+ requestUrl: url,
+ },
+ );
+ reject(exc);
+ return;
+ }
+ const makeJson = async (): Promise<any> => {
+ let responseJson;
+ try {
+ responseJson = JSON.parse(myRequest.responseText);
+ } catch (e) {
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Invalid JSON from HTTP response",
+ {
+ requestUrl: url,
+ httpStatusCode: myRequest.status,
+ },
+ );
+ }
+ if (responseJson === null || typeof responseJson !== "object") {
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Invalid JSON from HTTP response",
+ {
+ requestUrl: url,
+ httpStatusCode: myRequest.status,
+ },
+ );
+ }
+ return responseJson;
+ };
+
+ const headers = myRequest.getAllResponseHeaders();
+ const arr = headers.trim().split(/[\r\n]+/);
+
+ // Create a map of header names to values
+ const headerMap: httpLib.Headers = new httpLib.Headers();
+ arr.forEach(function (line) {
+ const parts = line.split(": ");
+ const headerName = parts.shift();
+ if (!headerName) {
+ logger.warn("skipping invalid header");
+ return;
+ }
+ const value = parts.join(": ");
+ headerMap.set(headerName, value);
+ });
+ const resp: httpLib.HttpResponse = {
+ requestUrl: url,
+ status: myRequest.status,
+ headers: headerMap,
+ json: makeJson,
+ text: async () => myRequest.responseText,
+ };
+ resolve(resp);
+ }
+ });
+ });
+ }
+
+ get(url: string, opt?: httpLib.HttpRequestOptions): Promise<httpLib.HttpResponse> {
+ return this.req("get", url, undefined, opt);
+ }
+
+ postJson(
+ url: string,
+ body: unknown,
+ opt?: httpLib.HttpRequestOptions,
+ ): Promise<httpLib.HttpResponse> {
+ return this.req("post", url, JSON.stringify(body), opt);
+ }
+
+ stop(): void {
+ // Nothing to do
+ }
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
new file mode 100644
index 000000000..77c38fda9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
@@ -0,0 +1,74 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * Web worker for crypto operations.
+ */
+
+/**
+ * Imports.
+ */
+
+import { CryptoImplementation } from "taler-wallet-core";
+
+const worker: Worker = (self as any) as Worker;
+
+async function handleRequest(
+ operation: string,
+ id: number,
+ args: string[],
+): Promise<void> {
+ const impl = new CryptoImplementation();
+
+ if (!(operation in impl)) {
+ console.error(`crypto operation '${operation}' not found`);
+ return;
+ }
+
+ try {
+ const result = (impl as any)[operation](...args);
+ worker.postMessage({ result, id });
+ } catch (e) {
+ console.log("error during operation", e);
+ return;
+ }
+}
+
+worker.onmessage = (msg: MessageEvent) => {
+ const args = msg.data.args;
+ if (!Array.isArray(args)) {
+ console.error("args must be array");
+ return;
+ }
+ const id = msg.data.id;
+ if (typeof id !== "number") {
+ console.error("RPC id must be number");
+ return;
+ }
+ const operation = msg.data.operation;
+ if (typeof operation !== "string") {
+ console.error("RPC operation must be string");
+ return;
+ }
+
+ if (CryptoImplementation.enableTracing) {
+ console.log("onmessage with", operation);
+ }
+
+ handleRequest(operation, id, args).catch((e) => {
+ console.error("error in browsere worker", e);
+ });
+};
diff --git a/packages/taler-wallet-webextension/src/chromeBadge.ts b/packages/taler-wallet-webextension/src/chromeBadge.ts
new file mode 100644
index 000000000..7bc5d368d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/chromeBadge.ts
@@ -0,0 +1,288 @@
+/*
+ This file is part of TALER
+ (C) 2016 INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { isFirefox } from "./compat";
+
+/**
+ * Polyfill for requestAnimationFrame, which
+ * doesn't work from a background page.
+ */
+function rAF(cb: (ts: number) => void): 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 {
+ 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 = 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 = false;
+
+ /**
+ * Current rotation angle, ranges from 0 to rotationAngleMax.
+ */
+ private rotationAngle = 0;
+
+ /**
+ * While animating, how wide is the current gap in the circle?
+ * Ranges from 0 to openMax.
+ */
+ private gapWidth = 0;
+
+ /**
+ * Should we show the notification dot?
+ */
+ private hasNotification = false;
+
+ /**
+ * 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;
+ const ctx = this.canvas.getContext("2d");
+ if (!ctx) {
+ throw Error("unable to get canvas context");
+ }
+ this.ctx = ctx;
+ this.draw();
+ }
+
+ /**
+ * Draw the badge based on the current state.
+ */
+ private draw(): void {
+ 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);
+
+ if (this.hasNotification) {
+ // We draw a circle with a soft border in the
+ // lower right corner.
+ const r = 8;
+ const cw = this.canvas.width;
+ const ch = this.canvas.height;
+ this.ctx.beginPath();
+ this.ctx.arc(cw - r, ch - r, r, 0, 2 * Math.PI, false);
+ const gradient = this.ctx.createRadialGradient(
+ cw - r,
+ ch - r,
+ r,
+ cw - r,
+ ch - r,
+ 5,
+ );
+ gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
+ gradient.addColorStop(1, "blue");
+ this.ctx.fillStyle = gradient;
+ this.ctx.fill();
+ }
+
+ // 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(): void {
+ if (this.animationRunning) {
+ return;
+ }
+ if (isFirefox()) {
+ // Firefox does not support badge animations properly
+ return;
+ }
+ this.animationRunning = true;
+ let start: number | undefined;
+ const step = (timestamp: number): void => {
+ 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);
+ }
+
+ /**
+ * Draw the badge such that it shows the
+ * user that something happened (balance changed).
+ */
+ showNotification(): void {
+ this.hasNotification = true;
+ this.draw();
+ }
+
+ /**
+ * Draw the badge without the notification mark.
+ */
+ clearNotification(): void {
+ this.hasNotification = false;
+ this.draw();
+ }
+
+ startBusy(): void {
+ if (this.isBusy) {
+ return;
+ }
+ this.isBusy = true;
+ this.animate();
+ }
+
+ stopBusy(): void {
+ this.isBusy = false;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/compat.js b/packages/taler-wallet-webextension/src/compat.js
new file mode 100644
index 000000000..fdfcbd4b9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/compat.js
@@ -0,0 +1,61 @@
+"use strict";
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getPermissionsApi = exports.isNode = exports.isFirefox = void 0;
+/**
+ * Compatibility helpers needed for browsers that don't implement
+ * WebExtension APIs consistently.
+ */
+function isFirefox() {
+ const rt = chrome.runtime;
+ if (typeof rt.getBrowserInfo === "function") {
+ return true;
+ }
+ return false;
+}
+exports.isFirefox = isFirefox;
+/**
+ * Check if we are running under nodejs.
+ */
+function isNode() {
+ return typeof process !== "undefined" && process.release.name === "node";
+}
+exports.isNode = isNode;
+function getPermissionsApi() {
+ const myBrowser = globalThis.browser;
+ if (typeof myBrowser === "object" &&
+ typeof myBrowser.permissions === "object") {
+ return {
+ addPermissionsListener: () => {
+ // Not supported yet.
+ },
+ contains: myBrowser.permissions.contains,
+ request: myBrowser.permissions.request,
+ remove: myBrowser.permissions.remove,
+ };
+ }
+ else {
+ return {
+ addPermissionsListener: chrome.permissions.onAdded.addListener.bind(chrome.permissions.onAdded),
+ contains: chrome.permissions.contains,
+ request: chrome.permissions.request,
+ remove: chrome.permissions.remove,
+ };
+ }
+}
+exports.getPermissionsApi = getPermissionsApi;
+//# sourceMappingURL=compat.js.map \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/compat.ts b/packages/taler-wallet-webextension/src/compat.ts
new file mode 100644
index 000000000..4635abd80
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/compat.ts
@@ -0,0 +1,85 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Compatibility helpers needed for browsers that don't implement
+ * WebExtension APIs consistently.
+ */
+
+export function isFirefox(): boolean {
+ const rt = chrome.runtime as any;
+ if (typeof rt.getBrowserInfo === "function") {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Check if we are running under nodejs.
+ */
+export function isNode(): boolean {
+ return typeof process !== "undefined" && process.release.name === "node";
+}
+
+/**
+ * Compatibility API that works on multiple browsers.
+ */
+export interface CrossBrowserPermissionsApi {
+ contains(
+ permissions: chrome.permissions.Permissions,
+ callback: (result: boolean) => void,
+ ): void;
+
+ addPermissionsListener(
+ callback: (permissions: chrome.permissions.Permissions) => void,
+ ): void;
+
+ request(
+ permissions: chrome.permissions.Permissions,
+ callback?: (granted: boolean) => void,
+ ): void;
+
+ remove(
+ permissions: chrome.permissions.Permissions,
+ callback?: (removed: boolean) => void,
+ ): void;
+}
+
+export function getPermissionsApi(): CrossBrowserPermissionsApi {
+ const myBrowser = (globalThis as any).browser;
+ if (
+ typeof myBrowser === "object" &&
+ typeof myBrowser.permissions === "object"
+ ) {
+ return {
+ addPermissionsListener: () => {
+ // Not supported yet.
+ },
+ contains: myBrowser.permissions.contains,
+ request: myBrowser.permissions.request,
+ remove: myBrowser.permissions.remove,
+ };
+ } else {
+ return {
+ addPermissionsListener: chrome.permissions.onAdded.addListener.bind(
+ chrome.permissions.onAdded,
+ ),
+ contains: chrome.permissions.contains,
+ request: chrome.permissions.request,
+ remove: chrome.permissions.remove,
+ };
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/i18n-test.tsx b/packages/taler-wallet-webextension/src/i18n-test.tsx
new file mode 100644
index 000000000..e17a455ce
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n-test.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+import { internalSetStrings, str, Translate, strings } from "./i18n";
+import React from "react";
+import { render } from "enzyme";
+import { configure } from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+
+configure({ adapter: new Adapter() });
+
+const testStrings = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ str1: ["foo1"],
+ str2: [""],
+ "str3 %1$s / %2$s": ["foo3 %2$s ; %1$s"],
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ },
+ },
+};
+
+test("str translation", (t) => {
+ // Alias, so we nly use the function for lookups, not for string extranction.
+ const strAlias = str;
+ const TranslateAlias = Translate;
+ internalSetStrings(testStrings);
+ t.is(strAlias`str1`, "foo1");
+ t.is(strAlias`str2`, "str2");
+ const a = "a";
+ const b = "b";
+ t.is(strAlias`str3 ${a} / ${b}`, "foo3 b ; a");
+ const r = render(<TranslateAlias>str1</TranslateAlias>);
+ t.is(r.text(), "foo1");
+
+ const r2 = render(
+ <TranslateAlias>
+ str3 <span>{a}</span> / <span>{b}</span>
+ </TranslateAlias>,
+ );
+ t.is(r2.text(), "foo3 b ; a");
+
+ t.pass();
+});
+
+test("existing str translation", (t) => {
+ internalSetStrings(strings);
+ t.pass();
+});
diff --git a/packages/taler-wallet-webextension/src/i18n.tsx b/packages/taler-wallet-webextension/src/i18n.tsx
new file mode 100644
index 000000000..afbb0e278
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n.tsx
@@ -0,0 +1,206 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Translation helpers for React components and template literals.
+ */
+
+/**
+ * Imports
+ */
+import React from "react";
+
+import { i18n as i18nCore } from "taler-wallet-core";
+/**
+ * Convert template strings to a msgid
+ */
+function toI18nString(stringSeq: ReadonlyArray<string>): string {
+ let s = "";
+ for (let i = 0; i < stringSeq.length; i++) {
+ s += stringSeq[i];
+ if (i < stringSeq.length - 1) {
+ s += `%${i + 1}$s`;
+ }
+ }
+ return s;
+}
+
+
+export const str = i18nCore.str;
+export const internalSetStrings = i18nCore.internalSetStrings;
+export const strings = i18nCore.strings;
+
+
+interface TranslateSwitchProps {
+ target: number;
+}
+
+function stringifyChildren(children: any): string {
+ let n = 1;
+ const ss = React.Children.map(children, (c) => {
+ if (typeof c === "string") {
+ return c;
+ }
+ return `%${n++}$s`;
+ });
+ const s = ss.join("").replace(/ +/g, " ").trim();
+ console.log("translation lookup", JSON.stringify(s));
+ return s;
+}
+
+interface TranslateProps {
+ /**
+ * Component that the translated element should be wrapped in.
+ * Defaults to "div".
+ */
+ wrap?: any;
+
+ /**
+ * Props to give to the wrapped component.
+ */
+ wrapProps?: any;
+}
+
+function getTranslatedChildren(
+ translation: string,
+ children: React.ReactNode,
+): React.ReactNode[] {
+ const tr = translation.split(/%(\d+)\$s/);
+ const childArray = React.Children.toArray(children);
+ // Merge consecutive string children.
+ const placeholderChildren = [];
+ for (let i = 0; i < childArray.length; i++) {
+ const x = childArray[i];
+ if (x === undefined) {
+ continue;
+ } else if (typeof x === "string") {
+ continue;
+ } else {
+ placeholderChildren.push(x);
+ }
+ }
+ const result = [];
+ for (let i = 0; i < tr.length; i++) {
+ if (i % 2 == 0) {
+ // Text
+ result.push(tr[i]);
+ } else {
+ const childIdx = Number.parseInt(tr[i]) - 1;
+ result.push(placeholderChildren[childIdx]);
+ }
+ }
+ return result;
+}
+
+/**
+ * Translate text node children of this component.
+ * If a child component might produce a text node, it must be wrapped
+ * in a another non-text element.
+ *
+ * Example:
+ * ```
+ * <Translate>
+ * Hello. Your score is <span><PlayerScore player={player} /></span>
+ * </Translate>
+ * ```
+ */
+export class Translate extends React.Component<TranslateProps, {}> {
+ render(): JSX.Element {
+ const s = stringifyChildren(this.props.children);
+ const translation: string = i18nCore.jed.ngettext(s, s, 1);
+ const result = getTranslatedChildren(translation, this.props.children);
+ if (!this.props.wrap) {
+ return <div>{result}</div>;
+ }
+ return React.createElement(this.props.wrap, this.props.wrapProps, result);
+ }
+}
+
+/**
+ * Switch translation based on singular or plural based on the target prop.
+ * Should only contain TranslateSingular and TransplatePlural as children.
+ *
+ * Example:
+ * ```
+ * <TranslateSwitch target={n}>
+ * <TranslateSingular>I have {n} apple.</TranslateSingular>
+ * <TranslatePlural>I have {n} apples.</TranslatePlural>
+ * </TranslateSwitch>
+ * ```
+ */
+export class TranslateSwitch extends React.Component<
+ TranslateSwitchProps,
+ void
+> {
+ render(): JSX.Element {
+ let singular: React.ReactElement<TranslationPluralProps> | undefined;
+ let plural: React.ReactElement<TranslationPluralProps> | undefined;
+ const children = this.props.children;
+ if (children) {
+ React.Children.forEach(children, (child: any) => {
+ if (child.type === TranslatePlural) {
+ plural = child;
+ }
+ if (child.type === TranslateSingular) {
+ singular = child;
+ }
+ });
+ }
+ if (!singular || !plural) {
+ console.error("translation not found");
+ return React.createElement("span", {}, ["translation not found"]);
+ }
+ singular.props.target = this.props.target;
+ plural.props.target = this.props.target;
+ // We're looking up the translation based on the
+ // singular, even if we must use the plural form.
+ return singular;
+ }
+}
+
+interface TranslationPluralProps {
+ target: number;
+}
+
+/**
+ * See [[TranslateSwitch]].
+ */
+export class TranslatePlural extends React.Component<
+ TranslationPluralProps,
+ void
+> {
+ render(): JSX.Element {
+ const s = stringifyChildren(this.props.children);
+ const translation = i18nCore.jed.ngettext(s, s, 1);
+ const result = getTranslatedChildren(translation, this.props.children);
+ return <div>{result}</div>;
+ }
+}
+
+/**
+ * See [[TranslateSwitch]].
+ */
+export class TranslateSingular extends React.Component<
+ TranslationPluralProps,
+ void
+> {
+ render(): JSX.Element {
+ const s = stringifyChildren(this.props.children);
+ const translation = i18nCore.jed.ngettext(s, s, this.props.target);
+ const result = getTranslatedChildren(translation, this.props.children);
+ return <div>{result}</div>;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/pageEntryPoint.ts b/packages/taler-wallet-webextension/src/pageEntryPoint.ts
new file mode 100644
index 000000000..9fd1d36f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pageEntryPoint.ts
@@ -0,0 +1,72 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Main entry point for extension pages.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+import ReactDOM from "react-dom";
+import { createPopup } from "./pages/popup";
+import { createWithdrawPage } from "./pages/withdraw";
+import { createWelcomePage } from "./pages/welcome";
+import { createPayPage } from "./pages/pay";
+import { createRefundPage } from "./pages/refund";
+
+function main(): void {
+ try {
+ let mainElement;
+ const m = location.pathname.match(/([^/]+)$/);
+ if (!m) {
+ throw Error("can't parse page URL");
+ }
+ const page = m[1];
+ switch (page) {
+ case "popup.html":
+ mainElement = createPopup();
+ break;
+ case "withdraw.html":
+ mainElement = createWithdrawPage();
+ break;
+ case "welcome.html":
+ mainElement = createWelcomePage();
+ break;
+ case "pay.html":
+ mainElement = createPayPage();
+ break;
+ case "refund.html":
+ mainElement = createRefundPage();
+ break;
+ default:
+ throw Error(`page '${page}' not implemented`);
+ }
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("container not found, can't mount page contents");
+ }
+ ReactDOM.render(mainElement, container);
+ } catch (e) {
+ console.error("got error", e);
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/taler-wallet-webextension/src/pages/pay.tsx b/packages/taler-wallet-webextension/src/pages/pay.tsx
new file mode 100644
index 000000000..2abd423bd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/pay.tsx
@@ -0,0 +1,180 @@
+/*
+ This file is part of TALER
+ (C) 2015 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Page shown to the user to confirm entering
+ * a contract.
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+
+import { renderAmount, ProgressButton } from "../renderHtml";
+import * as wxApi from "../wxApi";
+
+import React, { useState, useEffect } from "react";
+
+import { Amounts, AmountJson, walletTypes, talerTypes } from "taler-wallet-core";
+
+function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
+ const [payStatus, setPayStatus] = useState<walletTypes.PreparePayResult | undefined>();
+ const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
+ const [numTries, setNumTries] = useState(0);
+ const [loading, setLoading] = useState(false);
+ let amountEffective: AmountJson | undefined = undefined;
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ const p = await wxApi.preparePay(talerPayUri);
+ setPayStatus(p);
+ };
+ doFetch();
+ }, [numTries, talerPayUri]);
+
+ if (!payStatus) {
+ return <span>Loading payment information ...</span>;
+ }
+
+ let insufficientBalance = false;
+ if (payStatus.status == "insufficient-balance") {
+ insufficientBalance = true;
+ }
+
+ if (payStatus.status === "payment-possible") {
+ amountEffective = Amounts.parseOrThrow(payStatus.amountEffective);
+ }
+
+ if (payStatus.status === walletTypes.PreparePayResultType.AlreadyConfirmed && numTries === 0) {
+ return (
+ <span>
+ You have already paid for this article. Click{" "}
+ <a href={payStatus.nextUrl}>here</a> to view it again.
+ </span>
+ );
+ }
+
+ let contractTerms: talerTypes.ContractTerms;
+
+ try {
+ contractTerms = talerTypes.codecForContractTerms().decode(payStatus.contractTerms);
+ } catch (e) {
+ // This should never happen, as the wallet is supposed to check the contract terms
+ // before storing them.
+ console.error(e);
+ console.log("raw contract terms were", payStatus.contractTerms);
+ return <span>Invalid contract terms.</span>;
+ }
+
+ if (!contractTerms) {
+ return (
+ <span>
+ Error: did not get contract terms from merchant or wallet backend.
+ </span>
+ );
+ }
+
+ let merchantName: React.ReactElement;
+ if (contractTerms.merchant && contractTerms.merchant.name) {
+ merchantName = <strong>{contractTerms.merchant.name}</strong>;
+ } else {
+ merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;
+ }
+
+ const amount = (
+ <strong>{renderAmount(Amounts.parseOrThrow(contractTerms.amount))}</strong>
+ );
+
+ const doPayment = async (): Promise<void> => {
+ if (payStatus.status !== "payment-possible") {
+ throw Error(`invalid state: ${payStatus.status}`);
+ }
+ const proposalId = payStatus.proposalId;
+ setNumTries(numTries + 1);
+ try {
+ setLoading(true);
+ const res = await wxApi.confirmPay(proposalId, undefined);
+ document.location.href = res.nextUrl;
+ } catch (e) {
+ console.error(e);
+ setPayErrMsg(e.message);
+ }
+ };
+
+ return (
+ <div>
+ <p>
+ <i18n.Translate wrap="p">
+ The merchant <span>{merchantName}</span> offers you to purchase:
+ </i18n.Translate>
+ <div style={{ textAlign: "center" }}>
+ <strong>{contractTerms.summary}</strong>
+ </div>
+ {amountEffective ? (
+ <i18n.Translate wrap="p">
+ The total price is <span>{amount} </span>
+ (plus <span>{renderAmount(amountEffective)}</span> fees).
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate wrap="p">
+ The total price is <span>{amount}</span>.
+ </i18n.Translate>
+ )}
+ </p>
+
+ {insufficientBalance ? (
+ <div>
+ <p style={{ color: "red", fontWeight: "bold" }}>
+ Unable to pay: Your balance is insufficient.
+ </p>
+ </div>
+ ) : null}
+
+ {payErrMsg ? (
+ <div>
+ <p>Payment failed: {payErrMsg}</p>
+ <button
+ className="pure-button button-success"
+ onClick={() => doPayment()}
+ >
+ {i18n.str`Retry`}
+ </button>
+ </div>
+ ) : (
+ <div>
+ <ProgressButton
+ loading={loading}
+ disabled={insufficientBalance}
+ onClick={() => doPayment()}
+ >
+ {i18n.str`Confirm payment`}
+ </ProgressButton>
+ </div>
+ )}
+ </div>
+ );
+}
+
+export function createPayPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerPayUri = url.searchParams.get("talerPayUri");
+ if (!talerPayUri) {
+ throw Error("invalid parameter");
+ }
+ return <TalerPayDialog talerPayUri={talerPayUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/payback.tsx b/packages/taler-wallet-webextension/src/pages/payback.tsx
new file mode 100644
index 000000000..5d42f5f47
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/payback.tsx
@@ -0,0 +1,30 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * View and edit auditors.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as React from "react";
+
+export function makePaybackPage(): JSX.Element {
+ return <div>not implemented</div>;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/popup.tsx b/packages/taler-wallet-webextension/src/pages/popup.tsx
new file mode 100644
index 000000000..72c9f4bcb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/popup.tsx
@@ -0,0 +1,502 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Popup shown to the user when they click
+ * the Taler browser action button.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+import {
+ AmountJson,
+ Amounts,
+ time,
+ taleruri,
+ walletTypes,
+} from "taler-wallet-core";
+
+
+import { abbrev, renderAmount, PageLink } from "../renderHtml";
+import * as wxApi from "../wxApi";
+
+import React, { Fragment, useState, useEffect } from "react";
+
+import moment from "moment";
+import { PermissionsCheckbox } from "./welcome";
+
+// FIXME: move to newer react functions
+/* eslint-disable react/no-deprecated */
+
+class Router extends React.Component<any, any> {
+ 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 () => {
+ const i = Router.routeHandlers.indexOf(f);
+ this.routeHandlers = this.routeHandlers.splice(i, 1);
+ };
+ }
+
+ private static routeHandlers: any[] = [];
+
+ componentWillMount(): void {
+ console.log("router mounted");
+ window.onhashchange = () => {
+ this.setState({});
+ for (const f of Router.routeHandlers) {
+ f();
+ }
+ };
+ }
+
+ render(): JSX.Element {
+ const route = window.location.hash.substring(1);
+ console.log("rendering route", route);
+ let defaultChild: React.ReactChild | null = null;
+ let foundChild: React.ReactChild | null = null;
+ React.Children.forEach(this.props.children, (child) => {
+ const childProps: any = (child as any).props;
+ if (!childProps) {
+ return;
+ }
+ if (childProps.default) {
+ defaultChild = child as React.ReactChild;
+ }
+ if (childProps.route === route) {
+ foundChild = child as React.ReactChild;
+ }
+ });
+ const c: React.ReactChild | null = foundChild || defaultChild;
+ if (!c) {
+ throw Error("unknown route");
+ }
+ Router.setRoute((c as any).props.route);
+ return <div>{c}</div>;
+ }
+}
+
+interface TabProps {
+ target: string;
+ children?: React.ReactNode;
+}
+
+function Tab(props: TabProps): JSX.Element {
+ let cssClass = "";
+ if (props.target === Router.getRoute()) {
+ cssClass = "active";
+ }
+ const onClick = (e: React.MouseEvent<HTMLAnchorElement>): void => {
+ Router.setRoute(props.target);
+ e.preventDefault();
+ };
+ return (
+ <a onClick={onClick} href={props.target} className={cssClass}>
+ {props.children}
+ </a>
+ );
+}
+
+class WalletNavBar extends React.Component<any, any> {
+ private cancelSubscription: any;
+
+ componentWillMount(): void {
+ this.cancelSubscription = Router.onRoute(() => {
+ this.setState({});
+ });
+ }
+
+ componentWillUnmount(): void {
+ if (this.cancelSubscription) {
+ this.cancelSubscription();
+ }
+ }
+
+ render(): JSX.Element {
+ console.log("rendering nav bar");
+ return (
+ <div className="nav" id="header">
+ <Tab target="/balance">{i18n.str`Balance`}</Tab>
+ <Tab target="/history">{i18n.str`History`}</Tab>
+ <Tab target="/settings">{i18n.str`Settings`}</Tab>
+ <Tab target="/debug">{i18n.str`Debug`}</Tab>
+ </div>
+ );
+ }
+}
+
+/**
+ * Render an amount as a large number with a small currency symbol.
+ */
+function bigAmount(amount: AmountJson): JSX.Element {
+ const v = amount.value + amount.fraction / Amounts.fractionalBase;
+ return (
+ <span>
+ <span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
+ <span>{amount.currency}</span>
+ </span>
+ );
+}
+
+function EmptyBalanceView(): JSX.Element {
+ return (
+ <i18n.Translate wrap="p">
+ You have no balance to show. Need some{" "}
+ <PageLink pageName="welcome.html">help</PageLink> getting started?
+ </i18n.Translate>
+ );
+}
+
+class WalletBalanceView extends React.Component<any, any> {
+ private balance?: walletTypes.BalancesResponse;
+ private gotError = false;
+ private canceler: (() => void) | undefined = undefined;
+ private unmount = false;
+ private updateBalanceRunning = false;
+
+ componentWillMount(): void {
+ this.canceler = wxApi.onUpdateNotification(() => this.updateBalance());
+ this.updateBalance();
+ }
+
+ componentWillUnmount(): void {
+ console.log("component WalletBalanceView will unmount");
+ if (this.canceler) {
+ this.canceler();
+ }
+ this.unmount = true;
+ }
+
+ async updateBalance(): Promise<void> {
+ if (this.updateBalanceRunning) {
+ return;
+ }
+ this.updateBalanceRunning = true;
+ let balance: walletTypes.BalancesResponse;
+ try {
+ balance = await wxApi.getBalance();
+ } catch (e) {
+ if (this.unmount) {
+ return;
+ }
+ this.gotError = true;
+ console.error("could not retrieve balances", e);
+ this.setState({});
+ return;
+ } finally {
+ this.updateBalanceRunning = false;
+ }
+ if (this.unmount) {
+ return;
+ }
+ this.gotError = false;
+ console.log("got balance", balance);
+ this.balance = balance;
+ this.setState({});
+ }
+
+ formatPending(entry: walletTypes.Balance): JSX.Element {
+ let incoming: JSX.Element | undefined;
+ let payment: JSX.Element | undefined;
+
+ const available = Amounts.parseOrThrow(entry.available);
+ const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
+ const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
+
+ console.log(
+ "available: ",
+ entry.pendingIncoming ? renderAmount(entry.available) : null,
+ );
+ console.log(
+ "incoming: ",
+ entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null,
+ );
+
+ if (!Amounts.isZero(pendingIncoming)) {
+ incoming = (
+ <i18n.Translate wrap="span">
+ <span style={{ color: "darkgreen" }}>
+ {"+"}
+ {renderAmount(entry.pendingIncoming)}
+ </span>{" "}
+ incoming
+ </i18n.Translate>
+ );
+ }
+
+ const l = [incoming, payment].filter((x) => x !== undefined);
+ if (l.length === 0) {
+ return <span />;
+ }
+
+ if (l.length === 1) {
+ return <span>({l})</span>;
+ }
+ return (
+ <span>
+ ({l[0]}, {l[1]})
+ </span>
+ );
+ }
+
+ render(): JSX.Element {
+ const wallet = this.balance;
+ if (this.gotError) {
+ return (
+ <div className="balance">
+ <p>{i18n.str`Error: could not retrieve balance information.`}</p>
+ <p>
+ Click <PageLink pageName="welcome.html">here</PageLink> for help and
+ diagnostics.
+ </p>
+ </div>
+ );
+ }
+ if (!wallet) {
+ return <span></span>;
+ }
+ console.log(wallet);
+ const listing = wallet.balances.map((entry) => {
+ const av = Amounts.parseOrThrow(entry.available);
+ return (
+ <p key={av.currency}>
+ {bigAmount(av)} {this.formatPending(entry)}
+ </p>
+ );
+ });
+ return listing.length > 0 ? (
+ <div className="balance">{listing}</div>
+ ) : (
+ <EmptyBalanceView />
+ );
+ }
+}
+
+function Icon({ l }: { l: string }): JSX.Element {
+ return <div className={"icon"}>{l}</div>;
+}
+
+function formatAndCapitalize(text: string): string {
+ text = text.replace("-", " ");
+ text = text.replace(/^./, text[0].toUpperCase());
+ return text;
+}
+
+const HistoryComponent = (props: any): JSX.Element => {
+ return <span>TBD</span>;
+};
+
+class WalletSettings extends React.Component<any, any> {
+ render(): JSX.Element {
+ return (
+ <div>
+ <h2>Permissions</h2>
+ <PermissionsCheckbox />
+ </div>
+ );
+ }
+}
+
+function reload(): void {
+ try {
+ chrome.runtime.reload();
+ window.close();
+ } catch (e) {
+ // Functionality missing in firefox, ignore!
+ }
+}
+
+function confirmReset(): void {
+ if (
+ confirm(
+ "Do you want to IRREVOCABLY DESTROY everything inside your" +
+ " wallet and LOSE ALL YOUR COINS?",
+ )
+ ) {
+ wxApi.resetDb();
+ window.close();
+ }
+}
+
+function WalletDebug(props: any): JSX.Element {
+ return (
+ <div>
+ <p>Debug tools:</p>
+ <button onClick={openExtensionPage("/popup.html")}>wallet tab</button>
+ <button onClick={openExtensionPage("/benchmark.html")}>benchmark</button>
+ <button onClick={openExtensionPage("/show-db.html")}>show db</button>
+ <button onClick={openExtensionPage("/tree.html")}>show tree</button>
+ <br />
+ <button onClick={confirmReset}>reset</button>
+ <button onClick={reload}>reload chrome extension</button>
+ </div>
+ );
+}
+
+function openExtensionPage(page: string) {
+ return () => {
+ chrome.tabs.create({
+ url: chrome.extension.getURL(page),
+ });
+ };
+}
+
+function openTab(page: string) {
+ return (evt: React.SyntheticEvent<any>) => {
+ evt.preventDefault();
+ chrome.tabs.create({
+ url: page,
+ });
+ };
+}
+
+function makeExtensionUrlWithParams(
+ url: string,
+ params?: { [name: string]: string | undefined },
+): string {
+ const innerUrl = new URL(chrome.extension.getURL("/" + url));
+ if (params) {
+ for (const key in params) {
+ const p = params[key];
+ if (p) {
+ innerUrl.searchParams.set(key, p);
+ }
+ }
+ }
+ return innerUrl.href;
+}
+
+function actionForTalerUri(talerUri: string): string | undefined {
+ const uriType = taleruri.classifyTalerUri(talerUri);
+ switch (uriType) {
+ case taleruri.TalerUriType.TalerWithdraw:
+ return makeExtensionUrlWithParams("withdraw.html", {
+ talerWithdrawUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerPay:
+ return makeExtensionUrlWithParams("pay.html", {
+ talerPayUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerTip:
+ return makeExtensionUrlWithParams("tip.html", {
+ talerTipUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerRefund:
+ return makeExtensionUrlWithParams("refund.html", {
+ talerRefundUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerNotifyReserve:
+ // FIXME: implement
+ break;
+ default:
+ console.warn(
+ "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
+ );
+ break;
+ }
+ return undefined;
+}
+
+async function findTalerUriInActiveTab(): Promise<string | undefined> {
+ return new Promise((resolve, reject) => {
+ chrome.tabs.executeScript(
+ {
+ code: `
+ (() => {
+ let x = document.querySelector("a[href^='taler://'");
+ return x ? x.href.toString() : null;
+ })();
+ `,
+ allFrames: false,
+ },
+ (result) => {
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ resolve(undefined);
+ return;
+ }
+ console.log("got result", result);
+ resolve(result[0]);
+ },
+ );
+ });
+}
+
+function WalletPopup(): JSX.Element {
+ const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
+ undefined,
+ );
+ const [dismissed, setDismissed] = useState(false);
+ useEffect(() => {
+ async function check(): Promise<void> {
+ const talerUri = await findTalerUriInActiveTab();
+ if (talerUri) {
+ const actionUrl = actionForTalerUri(talerUri);
+ setTalerActionUrl(actionUrl);
+ }
+ }
+ check();
+ });
+ if (talerActionUrl && !dismissed) {
+ return (
+ <div style={{ padding: "1em" }}>
+ <h1>Taler Action</h1>
+ <p>This page has a Taler action. </p>
+ <p>
+ <button
+ onClick={() => {
+ window.open(talerActionUrl, "_blank");
+ }}
+ >
+ Open
+ </button>
+ </p>
+ <p>
+ <button onClick={() => setDismissed(true)}>Dismiss</button>
+ </p>
+ </div>
+ );
+ }
+ return (
+ <div>
+ <WalletNavBar />
+ <div style={{ margin: "1em" }}>
+ <Router>
+ <WalletBalanceView route="/balance" default />
+ <WalletSettings route="/settings" />
+ <WalletDebug route="/debug" />
+ </Router>
+ </div>
+ </div>
+ );
+}
+
+export function createPopup(): JSX.Element {
+ return <WalletPopup />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/refund.tsx b/packages/taler-wallet-webextension/src/pages/refund.tsx
new file mode 100644
index 000000000..7326dfc88
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/refund.tsx
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2015-2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Page that shows refund status for purchases.
+ *
+ * @author Florian Dold
+ */
+
+import React, { useEffect, useState } from "react";
+
+import * as wxApi from "../wxApi";
+import { AmountView } from "../renderHtml";
+import { walletTypes } from "taler-wallet-core";
+
+function RefundStatusView(props: { talerRefundUri: string }): JSX.Element {
+ const [applied, setApplied] = useState(false);
+ const [purchaseDetails, setPurchaseDetails] = useState<
+ walletTypes.PurchaseDetails | undefined
+ >(undefined);
+ const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ try {
+ const result = await wxApi.applyRefund(props.talerRefundUri);
+ setApplied(true);
+ const r = await wxApi.getPurchaseDetails(result.proposalId);
+ setPurchaseDetails(r);
+ } catch (e) {
+ console.error(e);
+ setErrMsg(e.message);
+ console.log("err message", e.message);
+ }
+ };
+ doFetch();
+ }, [props.talerRefundUri]);
+
+ console.log("rendering");
+
+ if (errMsg) {
+ return <span>Error: {errMsg}</span>;
+ }
+
+ if (!applied || !purchaseDetails) {
+ return <span>Updating refund status</span>;
+ }
+
+ return (
+ <>
+ <h2>Refund Status</h2>
+ <p>
+ The product <em>{purchaseDetails.contractTerms.summary}</em> has
+ received a total refund of{" "}
+ <AmountView amount={purchaseDetails.totalRefundAmount} />.
+ </p>
+ <p>Note that additional fees from the exchange may apply.</p>
+ </>
+ );
+}
+
+export function createRefundPage(): JSX.Element {
+ const url = new URL(document.location.href);
+
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("fatal: can't mount component, container missing");
+ }
+
+ const talerRefundUri = url.searchParams.get("talerRefundUri");
+ if (!talerRefundUri) {
+ throw Error("taler refund URI requred");
+ }
+
+ return <RefundStatusView talerRefundUri={talerRefundUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/reset-required.tsx b/packages/taler-wallet-webextension/src/pages/reset-required.tsx
new file mode 100644
index 000000000..0ef5fe8b7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/reset-required.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of TALER
+ (C) 2017 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Page to inform the user when a database reset is required.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+
+import * as wxApi from "../wxApi";
+
+interface State {
+ /**
+ * Did the user check the confirmation check box?
+ */
+ checked: boolean;
+
+ /**
+ * Do we actually need to reset the db?
+ */
+ resetRequired: boolean;
+}
+
+class ResetNotification extends React.Component<any, State> {
+ constructor(props: any) {
+ super(props);
+ this.state = { checked: false, resetRequired: true };
+ setInterval(() => this.update(), 500);
+ }
+ async update(): Promise<void> {
+ const res = await wxApi.checkUpgrade();
+ this.setState({ resetRequired: res.dbResetRequired });
+ }
+ render(): JSX.Element {
+ if (this.state.resetRequired) {
+ return (
+ <div>
+ <h1>Manual Reset Reqired</h1>
+ <p>
+ The wallet&apos;s database in your browser is incompatible with the{" "}
+ currently installed wallet. Please reset manually.
+ </p>
+ <p>
+ Once the database format has stabilized, we will provide automatic
+ upgrades.
+ </p>
+ <input
+ id="check"
+ type="checkbox"
+ checked={this.state.checked}
+ onChange={(e) => this.setState({ checked: e.target.checked })}
+ />{" "}
+ <label htmlFor="check">
+ I understand that I will lose all my data
+ </label>
+ <br />
+ <button
+ className="pure-button"
+ disabled={!this.state.checked}
+ onClick={() => wxApi.resetDb()}
+ >
+ Reset
+ </button>
+ </div>
+ );
+ }
+ return (
+ <div>
+ <h1>Everything is fine!</h1>A reset is not required anymore, you can
+ close this page.
+ </div>
+ );
+ }
+}
+
+export function createResetRequiredPage(): JSX.Element {
+ return <ResetNotification />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/return-coins.tsx b/packages/taler-wallet-webextension/src/pages/return-coins.tsx
new file mode 100644
index 000000000..e8cf8c9dd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/return-coins.tsx
@@ -0,0 +1,30 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Return coins to own bank account.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as React from "react";
+
+export function createReturnCoinsPage(): JSX.Element {
+ return <span>Not implemented yet.</span>;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/tip.tsx b/packages/taler-wallet-webextension/src/pages/tip.tsx
new file mode 100644
index 000000000..6cf4e1875
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/tip.tsx
@@ -0,0 +1,103 @@
+/*
+ This file is part of TALER
+ (C) 2017 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+
+import { acceptTip, getTipStatus } from "../wxApi";
+
+import { renderAmount, ProgressButton } from "../renderHtml";
+
+import { useState, useEffect } from "react";
+import { walletTypes } from "taler-wallet-core";
+
+function TipDisplay(props: { talerTipUri: string }): JSX.Element {
+ const [tipStatus, setTipStatus] = useState<walletTypes.TipStatus | undefined>(undefined);
+ const [discarded, setDiscarded] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [finished, setFinished] = useState(false);
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ const ts = await getTipStatus(props.talerTipUri);
+ setTipStatus(ts);
+ };
+ doFetch();
+ }, [props.talerTipUri]);
+
+ if (discarded) {
+ return <span>You&apos;ve discarded the tip.</span>;
+ }
+
+ if (finished) {
+ return <span>Tip has been accepted!</span>;
+ }
+
+ if (!tipStatus) {
+ return <span>Loading ...</span>;
+ }
+
+ const discard = (): void => {
+ setDiscarded(true);
+ };
+
+ const accept = async (): Promise<void> => {
+ setLoading(true);
+ await acceptTip(tipStatus.tipId);
+ setFinished(true);
+ };
+
+ return (
+ <div>
+ <h2>Tip Received!</h2>
+ <p>
+ You received a tip of <strong>{renderAmount(tipStatus.amount)}</strong>{" "}
+ from <span> </span>
+ <strong>{tipStatus.merchantOrigin}</strong>.
+ </p>
+ <p>
+ The tip is handled by the exchange{" "}
+ <strong>{tipStatus.exchangeUrl}</strong>. This exchange will charge fees
+ of <strong>{renderAmount(tipStatus.totalFees)}</strong> for this
+ operation.
+ </p>
+ <form className="pure-form">
+ <ProgressButton loading={loading} onClick={() => accept()}>
+ Accept Tip
+ </ProgressButton>{" "}
+ <button className="pure-button" type="button" onClick={() => discard()}>
+ Discard tip
+ </button>
+ </form>
+ </div>
+ );
+}
+
+export function createTipPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerTipUri = url.searchParams.get("talerTipUri");
+ if (typeof talerTipUri !== "string") {
+ throw Error("talerTipUri must be a string");
+ }
+
+ return <TipDisplay talerTipUri={talerTipUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/welcome.tsx b/packages/taler-wallet-webextension/src/pages/welcome.tsx
new file mode 100644
index 000000000..ff5de572c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/welcome.tsx
@@ -0,0 +1,190 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Welcome page, shown on first installs.
+ *
+ * @author Florian Dold
+ */
+
+import React, { useState, useEffect } from "react";
+import { getDiagnostics } from "../wxApi";
+import { PageLink } from "../renderHtml";
+import * as wxApi from "../wxApi";
+import { getPermissionsApi } from "../compat";
+import { extendedPermissions } from "../permissions";
+import { walletTypes } from "taler-wallet-core";
+
+function Diagnostics(): JSX.Element | null {
+ const [timedOut, setTimedOut] = useState(false);
+ const [diagnostics, setDiagnostics] = useState<walletTypes.WalletDiagnostics | undefined>(
+ undefined,
+ );
+
+ useEffect(() => {
+ let gotDiagnostics = false;
+ setTimeout(() => {
+ if (!gotDiagnostics) {
+ console.error("timed out");
+ setTimedOut(true);
+ }
+ }, 1000);
+ const doFetch = async (): Promise<void> => {
+ const d = await getDiagnostics();
+ console.log("got diagnostics", d);
+ gotDiagnostics = true;
+ setDiagnostics(d);
+ };
+ console.log("fetching diagnostics");
+ doFetch();
+ }, []);
+
+ if (timedOut) {
+ return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
+ }
+
+ if (diagnostics) {
+ if (diagnostics.errors.length === 0) {
+ return null;
+ } else {
+ return (
+ <div
+ style={{
+ borderLeft: "0.5em solid red",
+ paddingLeft: "1em",
+ paddingTop: "0.2em",
+ paddingBottom: "0.2em",
+ }}
+ >
+ <p>Problems detected:</p>
+ <ol>
+ {diagnostics.errors.map((errMsg) => (
+ <li key={errMsg}>{errMsg}</li>
+ ))}
+ </ol>
+ {diagnostics.firefoxIdbProblem ? (
+ <p>
+ Please check in your <code>about:config</code> settings that you
+ have IndexedDB enabled (check the preference name{" "}
+ <code>dom.indexedDB.enabled</code>).
+ </p>
+ ) : null}
+ {diagnostics.dbOutdated ? (
+ <p>
+ Your wallet database is outdated. Currently automatic migration is
+ not supported. Please go{" "}
+ <PageLink pageName="reset-required.html">here</PageLink> to reset
+ the wallet database.
+ </p>
+ ) : null}
+ </div>
+ );
+ }
+ }
+
+ return <p>Running diagnostics ...</p>;
+}
+
+export function PermissionsCheckbox(): JSX.Element {
+ const [extendedPermissionsEnabled, setExtendedPermissionsEnabled] = useState(
+ false,
+ );
+ async function handleExtendedPerm(requestedVal: boolean): Promise<void> {
+ let nextVal: boolean | undefined;
+ if (requestedVal) {
+ const granted = await new Promise<boolean>((resolve, reject) => {
+ // We set permissions here, since apparently FF wants this to be done
+ // as the result of an input event ...
+ getPermissionsApi().request(extendedPermissions, (granted: boolean) => {
+ if (chrome.runtime.lastError) {
+ console.error("error requesting permissions");
+ console.error(chrome.runtime.lastError);
+ reject(chrome.runtime.lastError);
+ return;
+ }
+ console.log("permissions granted:", granted);
+ resolve(granted);
+ });
+ });
+ const res = await wxApi.setExtendedPermissions(granted);
+ console.log(res);
+ nextVal = res.newValue;
+ } else {
+ const res = await wxApi.setExtendedPermissions(false);
+ console.log(res);
+ nextVal = res.newValue;
+ }
+ console.log("new permissions applied:", nextVal);
+ setExtendedPermissionsEnabled(nextVal ?? false);
+ }
+ useEffect(() => {
+ async function getExtendedPermValue(): Promise<void> {
+ const res = await wxApi.getExtendedPermissions();
+ setExtendedPermissionsEnabled(res.newValue);
+ }
+ getExtendedPermValue();
+ });
+ return (
+ <div>
+ <input
+ checked={extendedPermissionsEnabled}
+ onChange={(x) => handleExtendedPerm(x.target.checked)}
+ type="checkbox"
+ id="checkbox-perm"
+ style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
+ />
+ <label
+ htmlFor="checkbox-perm"
+ style={{ marginLeft: "0.5em", fontWeight: "bold" }}
+ >
+ Automatically open wallet based on page content
+ </label>
+ <span
+ style={{
+ color: "#383838",
+ fontSize: "smaller",
+ display: "block",
+ marginLeft: "2em",
+ }}
+ >
+ (Enabling this option below will make using the wallet faster, but
+ requires more permissions from your browser.)
+ </span>
+ </div>
+ );
+}
+
+function Welcome(): JSX.Element {
+ return (
+ <>
+ <p>Thank you for installing the wallet.</p>
+ <Diagnostics />
+ <h2>Permissions</h2>
+ <PermissionsCheckbox />
+ <h2>Next Steps</h2>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ Try the demo »
+ </a>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ Learn how to top up your wallet balance »
+ </a>
+ </>
+ );
+}
+
+export function createWelcomePage(): JSX.Element {
+ return <Welcome />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/withdraw.tsx b/packages/taler-wallet-webextension/src/pages/withdraw.tsx
new file mode 100644
index 000000000..4a92704b3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/withdraw.tsx
@@ -0,0 +1,229 @@
+/*
+ This file is part of TALER
+ (C) 2015-2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as i18n from "../i18n";
+
+import { WithdrawDetailView, renderAmount } from "../renderHtml";
+
+import React, { useState, useEffect } from "react";
+import {
+ acceptWithdrawal,
+ onUpdateNotification,
+} from "../wxApi";
+
+function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
+ const [details, setDetails] = useState<
+ any | undefined
+ >();
+ const [selectedExchange, setSelectedExchange] = useState<
+ string | undefined
+ >();
+ const talerWithdrawUri = props.talerWithdrawUri;
+ const [cancelled, setCancelled] = useState(false);
+ const [selecting, setSelecting] = useState(false);
+ const [customUrl, setCustomUrl] = useState<string>("");
+ const [errMsg, setErrMsg] = useState<string | undefined>("");
+ const [updateCounter, setUpdateCounter] = useState(1);
+
+ useEffect(() => {
+ return onUpdateNotification(() => {
+ setUpdateCounter(updateCounter + 1);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async (): Promise<void> => {
+ // FIXME: re-implement with new API
+ // console.log("getting from", talerWithdrawUri);
+ // let d: WithdrawalDetailsResponse | undefined = undefined;
+ // try {
+ // d = await getWithdrawDetails(talerWithdrawUri, selectedExchange);
+ // } catch (e) {
+ // console.error(
+ // `error getting withdraw details for uri ${talerWithdrawUri}, exchange ${selectedExchange}`,
+ // e,
+ // );
+ // setErrMsg(e.message);
+ // return;
+ // }
+ // console.log("got withdrawDetails", d);
+ // if (!selectedExchange && d.bankWithdrawDetails.suggestedExchange) {
+ // console.log("setting selected exchange");
+ // setSelectedExchange(d.bankWithdrawDetails.suggestedExchange);
+ // }
+ // setDetails(d);
+ };
+ fetchData();
+ }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter]);
+
+ if (errMsg) {
+ return (
+ <div>
+ <i18n.Translate wrap="p">
+ Could not get details for withdraw operation:
+ </i18n.Translate>
+ <p style={{ color: "red" }}>{errMsg}</p>
+ <p>
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => {
+ setSelecting(true);
+ setErrMsg(undefined);
+ setSelectedExchange(undefined);
+ setDetails(undefined);
+ }}
+ >
+ {i18n.str`Chose different exchange provider`}
+ </span>
+ </p>
+ </div>
+ );
+ }
+
+ if (!details) {
+ return <span>Loading...</span>;
+ }
+
+ if (cancelled) {
+ return <span>Withdraw operation has been cancelled.</span>;
+ }
+
+ if (selecting) {
+ const bankSuggestion =
+ details && details.bankWithdrawDetails.suggestedExchange;
+ return (
+ <div>
+ {i18n.str`Please select an exchange. You can review the details before after your selection.`}
+ {bankSuggestion && (
+ <div>
+ <h2>Bank Suggestion</h2>
+ <button
+ className="pure-button button-success"
+ onClick={() => {
+ setDetails(undefined);
+ setSelectedExchange(bankSuggestion);
+ setSelecting(false);
+ }}
+ >
+ <i18n.Translate wrap="span">
+ Select <strong>{bankSuggestion}</strong>
+ </i18n.Translate>
+ </button>
+ </div>
+ )}
+ <h2>Custom Selection</h2>
+ <p>
+ <input
+ type="text"
+ onChange={(e) => setCustomUrl(e.target.value)}
+ value={customUrl}
+ />
+ </p>
+ <button
+ className="pure-button button-success"
+ onClick={() => {
+ setDetails(undefined);
+ setSelectedExchange(customUrl);
+ setSelecting(false);
+ }}
+ >
+ <i18n.Translate wrap="span">Select custom exchange</i18n.Translate>
+ </button>
+ </div>
+ );
+ }
+
+ const accept = async (): Promise<void> => {
+ if (!selectedExchange) {
+ throw Error("can't accept, no exchange selected");
+ }
+ console.log("accepting exchange", selectedExchange);
+ const res = await acceptWithdrawal(talerWithdrawUri, selectedExchange);
+ console.log("accept withdrawal response", res);
+ if (res.confirmTransferUrl) {
+ document.location.href = res.confirmTransferUrl;
+ }
+ };
+
+ return (
+ <div>
+ <h1>Digital Cash Withdrawal</h1>
+ <i18n.Translate wrap="p">
+ You are about to withdraw{" "}
+ <strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from
+ your bank account into your wallet.
+ </i18n.Translate>
+ {selectedExchange ? (
+ <p>
+ The exchange <strong>{selectedExchange}</strong> will be used as the
+ Taler payment service provider.
+ </p>
+ ) : null}
+
+ <div>
+ <button
+ className="pure-button button-success"
+ disabled={!selectedExchange}
+ onClick={() => accept()}
+ >
+ {i18n.str`Accept fees and withdraw`}
+ </button>
+ <p>
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => setSelecting(true)}
+ >
+ {i18n.str`Chose different exchange provider`}
+ </span>
+ <br />
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => setCancelled(true)}
+ >
+ {i18n.str`Cancel withdraw operation`}
+ </span>
+ </p>
+
+ {details.exchangeWithdrawDetails ? (
+ <WithdrawDetailView rci={details.exchangeWithdrawDetails} />
+ ) : null}
+ </div>
+ </div>
+ );
+}
+
+export function createWithdrawPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerWithdrawUri = url.searchParams.get("talerWithdrawUri");
+ if (!talerWithdrawUri) {
+ throw Error("withdraw URI required");
+ }
+ return <WithdrawalDialog talerWithdrawUri={talerWithdrawUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/permissions.ts b/packages/taler-wallet-webextension/src/permissions.ts
new file mode 100644
index 000000000..bcd357fd6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/permissions.ts
@@ -0,0 +1,20 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export const extendedPermissions = {
+ permissions: ["webRequest", "webRequestBlocking"],
+ origins: ["http://*/*", "https://*/*"],
+};
diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx
new file mode 100644
index 000000000..89f6c12e8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/renderHtml.tsx
@@ -0,0 +1,341 @@
+/*
+ This file is part of TALER
+ (C) 2016 INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers functions to render Taler-related data structures to HTML.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, Amounts, time, walletTypes } from "taler-wallet-core";
+import * as i18n from "./i18n";
+import React from "react";
+
+/**
+ * Render amount as HTML, which non-breaking space between
+ * decimal value and currency.
+ */
+export function renderAmount(amount: AmountJson | string): JSX.Element {
+ let a;
+ if (typeof amount === "string") {
+ a = Amounts.parse(amount);
+ } else {
+ a = amount;
+ }
+ if (!a) {
+ return <span>(invalid amount)</span>;
+ }
+ const x = a.value + a.fraction / Amounts.fractionalBase;
+ return (
+ <span>
+ {x}&nbsp;{a.currency}
+ </span>
+ );
+}
+
+export const AmountView = ({
+ amount,
+}: {
+ amount: AmountJson | string;
+}): JSX.Element => renderAmount(amount);
+
+/**
+ * Abbreviate a string to a given length, and show the full
+ * string on hover as a tooltip.
+ */
+export function abbrev(s: string, n = 5): JSX.Element {
+ let sAbbrev = s;
+ if (s.length > n) {
+ sAbbrev = s.slice(0, n) + "..";
+ }
+ return (
+ <span className="abbrev" title={s}>
+ {sAbbrev}
+ </span>
+ );
+}
+
+interface CollapsibleState {
+ collapsed: boolean;
+}
+
+interface CollapsibleProps {
+ initiallyCollapsed: boolean;
+ title: string;
+}
+
+/**
+ * Component that shows/hides its children when clicking
+ * a heading.
+ */
+export class Collapsible extends React.Component<
+ CollapsibleProps,
+ CollapsibleState
+> {
+ constructor(props: CollapsibleProps) {
+ super(props);
+ this.state = { collapsed: props.initiallyCollapsed };
+ }
+ render(): JSX.Element {
+ const doOpen = (e: any): void => {
+ this.setState({ collapsed: false });
+ e.preventDefault();
+ };
+ const doClose = (e: any): void => {
+ this.setState({ collapsed: true });
+ e.preventDefault();
+ };
+ if (this.state.collapsed) {
+ return (
+ <h2>
+ <a className="opener opener-collapsed" href="#" onClick={doOpen}>
+ {" "}
+ {this.props.title}
+ </a>
+ </h2>
+ );
+ }
+ return (
+ <div>
+ <h2>
+ <a className="opener opener-open" href="#" onClick={doClose}>
+ {" "}
+ {this.props.title}
+ </a>
+ </h2>
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+function WireFee(props: {
+ s: string;
+ rci: walletTypes.ExchangeWithdrawDetails;
+}): JSX.Element {
+ return (
+ <>
+ <thead>
+ <tr>
+ <th colSpan={3}>Wire Method {props.s}</th>
+ </tr>
+ <tr>
+ <th>Applies Until</th>
+ <th>Wire Fee</th>
+ <th>Closing Fee</th>
+ </tr>
+ </thead>
+ <tbody>
+ {props.rci.wireFees.feesForType[props.s].map((f) => (
+ <tr key={f.sig}>
+ <td>{time.stringifyTimestamp(f.endStamp)}</td>
+ <td>{renderAmount(f.wireFee)}</td>
+ <td>{renderAmount(f.closingFee)}</td>
+ </tr>
+ ))}
+ </tbody>
+ </>
+ );
+}
+
+function AuditorDetailsView(props: {
+ rci: walletTypes.ExchangeWithdrawDetails | null;
+}): JSX.Element {
+ const rci = props.rci;
+ console.log("rci", rci);
+ if (!rci) {
+ return (
+ <p>
+ Details will be displayed when a valid exchange provider URL is entered.
+ </p>
+ );
+ }
+ if ((rci.exchangeInfo.details?.auditors ?? []).length === 0) {
+ return <p>The exchange is not audited by any auditors.</p>;
+ }
+ return (
+ <div>
+ {(rci.exchangeInfo.details?.auditors ?? []).map((a) => (
+ <div key={a.auditor_pub}>
+ <h3>Auditor {a.auditor_url}</h3>
+ <p>
+ Public key: <ExpanderText text={a.auditor_pub} />
+ </p>
+ <p>
+ Trusted:{" "}
+ {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? "yes" : "no"}
+ </p>
+ <p>
+ Audits {a.denomination_keys.length} of {rci.numOfferedDenoms}{" "}
+ denominations
+ </p>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+function FeeDetailsView(props: {
+ rci: walletTypes.ExchangeWithdrawDetails | null;
+}): JSX.Element {
+ const rci = props.rci;
+ if (!rci) {
+ return (
+ <p>
+ Details will be displayed when a valid exchange provider URL is entered.
+ </p>
+ );
+ }
+
+ const denoms = rci.selectedDenoms;
+ const withdrawFee = renderAmount(rci.withdrawFee);
+ const overhead = renderAmount(rci.overhead);
+
+ return (
+ <div>
+ <h3>Overview</h3>
+ <p>
+ Public key:{" "}
+ <ExpanderText
+ text={rci.exchangeInfo.details?.masterPublicKey ?? "??"}
+ />
+ </p>
+ <p>
+ {i18n.str`Withdrawal fees:`} {withdrawFee}
+ </p>
+ <p>
+ {i18n.str`Rounding loss:`} {overhead}
+ </p>
+ <p>{i18n.str`Earliest expiration (for deposit): ${time.stringifyTimestamp(
+ rci.earliestDepositExpiration,
+ )}`}</p>
+ <h3>Coin Fees</h3>
+ <div style={{ overflow: "auto" }}>
+ <table className="pure-table">
+ <thead>
+ <tr>
+ <th>{i18n.str`# Coins`}</th>
+ <th>{i18n.str`Value`}</th>
+ <th>{i18n.str`Withdraw Fee`}</th>
+ <th>{i18n.str`Refresh Fee`}</th>
+ <th>{i18n.str`Deposit Fee`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {denoms.selectedDenoms.map((ds) => {
+ return (
+ <tr key={ds.denom.denomPub}>
+ <td>{ds.count + "x"}</td>
+ <td>{renderAmount(ds.denom.value)}</td>
+ <td>{renderAmount(ds.denom.feeWithdraw)}</td>
+ <td>{renderAmount(ds.denom.feeRefresh)}</td>
+ <td>{renderAmount(ds.denom.feeDeposit)}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ <h3>Wire Fees</h3>
+ <div style={{ overflow: "auto" }}>
+ <table className="pure-table">
+ {Object.keys(rci.wireFees.feesForType).map((s) => (
+ <WireFee key={s} s={s} rci={rci} />
+ ))}
+ </table>
+ </div>
+ </div>
+ );
+}
+
+/**
+ * Shows details about a withdraw request.
+ */
+export function WithdrawDetailView(props: {
+ rci: walletTypes.ExchangeWithdrawDetails | null;
+}): JSX.Element {
+ const rci = props.rci;
+ return (
+ <div>
+ <Collapsible initiallyCollapsed={true} title="Fee and Spending Details">
+ <FeeDetailsView rci={rci} />
+ </Collapsible>
+ <Collapsible initiallyCollapsed={true} title="Auditor Details">
+ <AuditorDetailsView rci={rci} />
+ </Collapsible>
+ </div>
+ );
+}
+
+interface ExpanderTextProps {
+ text: string;
+}
+
+/**
+ * Show a heading with a toggle to show/hide the expandable content.
+ */
+export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {
+ return <span>{text}</span>;
+}
+
+export interface LoadingButtonProps {
+ loading: boolean;
+}
+
+export function ProgressButton(
+ props: React.PropsWithChildren<LoadingButtonProps> &
+ React.DetailedHTMLProps<
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
+ HTMLButtonElement
+ >,
+): JSX.Element {
+ return (
+ <button
+ className="pure-button pure-button-primary"
+ type="button"
+ {...props}
+ >
+ {props.loading ? (
+ <span>
+ <object
+ className="svg-icon svg-baseline"
+ data="/img/spinner-bars.svg"
+ />
+ </span>
+ ) : null}{" "}
+ {props.children}
+ </button>
+ );
+}
+
+export function PageLink(
+ props: React.PropsWithChildren<{ pageName: string }>,
+): JSX.Element {
+ const url = chrome.extension.getURL(`/${props.pageName}`);
+ return (
+ <a
+ className="actionLink"
+ href={url}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {props.children}
+ </a>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
new file mode 100644
index 000000000..ee86d90e5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -0,0 +1,239 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Interface to the wallet through WebExtension messaging.
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, walletTypes } from "taler-wallet-core";
+
+
+export interface ExtendedPermissionsResponse {
+ newValue: boolean;
+}
+
+
+/**
+ * Response with information about available version upgrades.
+ */
+export interface UpgradeResponse {
+ /**
+ * Is a reset required because of a new DB version
+ * that can't be atomatically upgraded?
+ */
+ dbResetRequired: boolean;
+
+ /**
+ * Current database version.
+ */
+ currentDbVersion: string;
+
+ /**
+ * Old db version (if applicable).
+ */
+ oldDbVersion: string;
+}
+
+/**
+ * Error thrown when the function from the backend (via RPC) threw an error.
+ */
+export class WalletApiError extends Error {
+ constructor(message: string, public detail: any) {
+ super(message);
+ // restore prototype chain
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+async function callBackend(
+ type: string,
+ detail: any,
+): Promise<any> {
+ return new Promise<any>((resolve, reject) => {
+ chrome.runtime.sendMessage({ type, detail }, (resp) => {
+ if (chrome.runtime.lastError) {
+ console.log("Error calling backend");
+ reject(
+ new Error(
+ `Error contacting backend: chrome.runtime.lastError.message`,
+ ),
+ );
+ }
+ if (typeof resp === "object" && resp && resp.error) {
+ console.warn("response error:", resp);
+ const e = new WalletApiError(resp.error.message, resp.error);
+ reject(e);
+ } else {
+ resolve(resp);
+ }
+ });
+ });
+}
+
+
+
+/**
+ * Start refreshing a coin.
+ */
+export function refresh(coinPub: string): Promise<void> {
+ return callBackend("refresh-coin", { coinPub });
+}
+
+/**
+ * Pay for a proposal.
+ */
+export function confirmPay(
+ proposalId: string,
+ sessionId: string | undefined,
+): Promise<walletTypes.ConfirmPayResult> {
+ return callBackend("confirm-pay", { proposalId, sessionId });
+}
+
+/**
+ * Check upgrade information
+ */
+export function checkUpgrade(): Promise<UpgradeResponse> {
+ return callBackend("check-upgrade", {});
+}
+
+/**
+ * Reset database
+ */
+export function resetDb(): Promise<void> {
+ return callBackend("reset-db", {});
+}
+
+/**
+ * Get balances for all currencies/exchanges.
+ */
+export function getBalance(): Promise<walletTypes.BalancesResponse> {
+ return callBackend("balances", {});
+}
+
+/**
+ * Return coins to a bank account.
+ */
+export function returnCoins(args: {
+ amount: AmountJson;
+ exchange: string;
+ senderWire: string;
+}): Promise<void> {
+ return callBackend("return-coins", args);
+}
+
+/**
+ * Look up a purchase in the wallet database from
+ * the contract terms hash.
+ */
+export function getPurchaseDetails(
+ proposalId: string,
+): Promise<walletTypes.PurchaseDetails> {
+ return callBackend("get-purchase-details", { proposalId });
+}
+
+/**
+ * Get the status of processing a tip.
+ */
+export function getTipStatus(talerTipUri: string): Promise<walletTypes.TipStatus> {
+ return callBackend("get-tip-status", { talerTipUri });
+}
+
+/**
+ * Mark a tip as accepted by the user.
+ */
+export function acceptTip(talerTipUri: string): Promise<void> {
+ return callBackend("accept-tip", { talerTipUri });
+}
+
+/**
+ * Download a refund and accept it.
+ */
+export function applyRefund(
+ refundUrl: string,
+): Promise<{ contractTermsHash: string; proposalId: string }> {
+ return callBackend("accept-refund", { refundUrl });
+}
+
+/**
+ * Abort a failed payment and try to get a refund.
+ */
+export function abortFailedPayment(contractTermsHash: string): Promise<void> {
+ return callBackend("abort-failed-payment", { contractTermsHash });
+}
+
+/**
+ * Abort a failed payment and try to get a refund.
+ */
+export function benchmarkCrypto(repetitions: number): Promise<walletTypes.BenchmarkResult> {
+ return callBackend("benchmark-crypto", { repetitions });
+}
+
+/**
+ * Get details about a pay operation.
+ */
+export function preparePay(talerPayUri: string): Promise<walletTypes.PreparePayResult> {
+ return callBackend("prepare-pay", { talerPayUri });
+}
+
+/**
+ * Get details about a withdraw operation.
+ */
+export function acceptWithdrawal(
+ talerWithdrawUri: string,
+ selectedExchange: string,
+): Promise<walletTypes.AcceptWithdrawalResponse> {
+ return callBackend("accept-withdrawal", {
+ talerWithdrawUri,
+ selectedExchange,
+ });
+}
+
+/**
+ * Get diagnostics information
+ */
+export function getDiagnostics(): Promise<walletTypes.WalletDiagnostics> {
+ return callBackend("get-diagnostics", {});
+}
+
+/**
+ * Get diagnostics information
+ */
+export function setExtendedPermissions(
+ value: boolean,
+): Promise<ExtendedPermissionsResponse> {
+ return callBackend("set-extended-permissions", { value });
+}
+
+/**
+ * Get diagnostics information
+ */
+export function getExtendedPermissions(): Promise<ExtendedPermissionsResponse> {
+ return callBackend("get-extended-permissions", {});
+}
+
+export function onUpdateNotification(f: () => void): () => void {
+ const port = chrome.runtime.connect({ name: "notifications" });
+ const listener = (): void => {
+ f();
+ };
+ port.onMessage.addListener(listener);
+ return () => {
+ port.onMessage.removeListener(listener);
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
new file mode 100644
index 000000000..3adc9a82d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -0,0 +1,566 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Messaging for the WebExtensions wallet. Should contain
+ * parts that are specific for WebExtensions, but as little business
+ * logic as possible.
+ */
+
+/**
+ * Imports.
+ */
+import { isFirefox, getPermissionsApi } from "./compat";
+import * as wxApi from "./wxApi";
+import MessageSender = chrome.runtime.MessageSender;
+import { extendedPermissions } from "./permissions";
+
+import { Wallet, promiseUtil, db, walletTypes, taleruri, queryLib } from "taler-wallet-core";
+import { BrowserHttpLib } from "./browserHttpLib";
+import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
+
+const NeedsWallet = Symbol("NeedsWallet");
+
+/**
+ * Currently active wallet instance. Might be unloaded and
+ * re-instantiated when the database is reset.
+ */
+let currentWallet: Wallet | undefined;
+
+let currentDatabase: IDBDatabase | undefined;
+
+/**
+ * Last version if an outdated DB, if applicable.
+ */
+let outdatedDbVersion: number | undefined;
+
+const walletInit: promiseUtil.OpenedPromise<void> = promiseUtil.openPromise<void>();
+
+const notificationPorts: chrome.runtime.Port[] = [];
+
+async function handleMessage(
+ sender: MessageSender,
+ type: string,
+ detail: any,
+): Promise<any> {
+ function needsWallet(): Wallet {
+ if (!currentWallet) {
+ throw NeedsWallet;
+ }
+ return currentWallet;
+ }
+ switch (type) {
+ case "balances": {
+ return needsWallet().getBalances();
+ }
+ case "dump-db": {
+ const db = needsWallet().db;
+ return db.exportDatabase();
+ }
+ case "import-db": {
+ const db = needsWallet().db;
+ return db.importDatabase(detail.dump);
+ }
+ case "ping": {
+ return Promise.resolve();
+ }
+ case "reset-db": {
+ db.deleteTalerDatabase(indexedDB);
+ setBadgeText({ text: "" });
+ console.log("reset done");
+ if (!currentWallet) {
+ reinitWallet();
+ }
+ return Promise.resolve({});
+ }
+ case "confirm-pay": {
+ if (typeof detail.proposalId !== "string") {
+ throw Error("proposalId must be string");
+ }
+ return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
+ }
+ case "exchange-info": {
+ if (!detail.baseUrl) {
+ return Promise.resolve({ error: "bad url" });
+ }
+ return needsWallet().updateExchangeFromUrl(detail.baseUrl);
+ }
+ case "get-exchanges": {
+ return needsWallet().getExchangeRecords();
+ }
+ case "get-currencies": {
+ return needsWallet().getCurrencies();
+ }
+ case "update-currency": {
+ return needsWallet().updateCurrency(detail.currencyRecord);
+ }
+ case "get-reserves": {
+ if (typeof detail.exchangeBaseUrl !== "string") {
+ return Promise.reject(Error("exchangeBaseUrl missing"));
+ }
+ return needsWallet().getReserves(detail.exchangeBaseUrl);
+ }
+ case "get-coins": {
+ if (typeof detail.exchangeBaseUrl !== "string") {
+ return Promise.reject(Error("exchangBaseUrl missing"));
+ }
+ return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
+ }
+ case "get-denoms": {
+ if (typeof detail.exchangeBaseUrl !== "string") {
+ return Promise.reject(Error("exchangBaseUrl missing"));
+ }
+ return needsWallet().getDenoms(detail.exchangeBaseUrl);
+ }
+ case "refresh-coin": {
+ if (typeof detail.coinPub !== "string") {
+ return Promise.reject(Error("coinPub missing"));
+ }
+ return needsWallet().refresh(detail.coinPub);
+ }
+ case "get-sender-wire-infos": {
+ return needsWallet().getSenderWireInfos();
+ }
+ case "return-coins": {
+ const d = {
+ amount: detail.amount,
+ exchange: detail.exchange,
+ senderWire: detail.senderWire,
+ };
+ return needsWallet().returnCoins(d);
+ }
+ case "check-upgrade": {
+ let dbResetRequired = false;
+ if (!currentWallet) {
+ dbResetRequired = true;
+ }
+ const resp: wxApi.UpgradeResponse = {
+ currentDbVersion: db.WALLET_DB_MINOR_VERSION.toString(),
+ dbResetRequired,
+ oldDbVersion: (outdatedDbVersion || "unknown").toString(),
+ };
+ return resp;
+ }
+ case "get-purchase-details": {
+ const proposalId = detail.proposalId;
+ if (!proposalId) {
+ throw Error("proposalId missing");
+ }
+ if (typeof proposalId !== "string") {
+ throw Error("proposalId must be a string");
+ }
+ return needsWallet().getPurchaseDetails(proposalId);
+ }
+ case "accept-refund":
+ return needsWallet().applyRefund(detail.refundUrl);
+ case "get-tip-status": {
+ return needsWallet().getTipStatus(detail.talerTipUri);
+ }
+ case "accept-tip": {
+ return needsWallet().acceptTip(detail.talerTipUri);
+ }
+ case "abort-failed-payment": {
+ if (!detail.contractTermsHash) {
+ throw Error("contracTermsHash not given");
+ }
+ return needsWallet().abortFailedPayment(detail.contractTermsHash);
+ }
+ case "benchmark-crypto": {
+ if (!detail.repetitions) {
+ throw Error("repetitions not given");
+ }
+ return needsWallet().benchmarkCrypto(detail.repetitions);
+ }
+ case "accept-withdrawal": {
+ return needsWallet().acceptWithdrawal(
+ detail.talerWithdrawUri,
+ detail.selectedExchange,
+ );
+ }
+ case "get-diagnostics": {
+ const manifestData = chrome.runtime.getManifest();
+ const errors: string[] = [];
+ let firefoxIdbProblem = false;
+ let dbOutdated = false;
+ try {
+ await walletInit.promise;
+ } catch (e) {
+ errors.push("Error during wallet initialization: " + e);
+ if (
+ currentDatabase === undefined &&
+ outdatedDbVersion === undefined &&
+ isFirefox()
+ ) {
+ firefoxIdbProblem = true;
+ }
+ }
+ if (!currentWallet) {
+ errors.push("Could not create wallet backend.");
+ }
+ if (!currentDatabase) {
+ errors.push("Could not open database");
+ }
+ if (outdatedDbVersion !== undefined) {
+ errors.push(`Outdated DB version: ${outdatedDbVersion}`);
+ dbOutdated = true;
+ }
+ const diagnostics: walletTypes.WalletDiagnostics = {
+ walletManifestDisplayVersion:
+ manifestData.version_name || "(undefined)",
+ walletManifestVersion: manifestData.version,
+ errors,
+ firefoxIdbProblem,
+ dbOutdated,
+ };
+ return diagnostics;
+ }
+ case "prepare-pay":
+ return needsWallet().preparePayForUri(detail.talerPayUri);
+ case "set-extended-permissions": {
+ const newVal = detail.value;
+ console.log("new extended permissions value", newVal);
+ if (newVal) {
+ setupHeaderListener();
+ return { newValue: true };
+ } else {
+ await new Promise((resolve, reject) => {
+ getPermissionsApi().remove(extendedPermissions, (rem) => {
+ console.log("permissions removed:", rem);
+ resolve();
+ });
+ });
+ return { newVal: false };
+ }
+ }
+ case "get-extended-permissions": {
+ const res = await new Promise((resolve, reject) => {
+ getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
+ resolve(result);
+ });
+ });
+ return { newValue: res };
+ }
+ default:
+ console.error(`Request type ${type} unknown`);
+ console.error(`Request detail was ${detail}`);
+ return {
+ error: {
+ message: `request type ${type} unknown`,
+ requestType: type,
+ },
+ };
+ }
+}
+
+async function dispatch(
+ req: any,
+ sender: any,
+ sendResponse: any,
+): Promise<void> {
+ try {
+ const p = handleMessage(sender, req.type, req.detail);
+ 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: {
+ message: e.message,
+ stack,
+ },
+ });
+ } catch (e) {
+ console.log(e);
+ // might fail if tab disconnected
+ }
+ }
+}
+
+function getTab(tabId: number): Promise<chrome.tabs.Tab> {
+ return new Promise((resolve, reject) => {
+ chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
+ });
+}
+
+function setBadgeText(options: chrome.browserAction.BadgeTextDetails): void {
+ // not supported by all browsers ...
+ if (chrome && chrome.browserAction && chrome.browserAction.setBadgeText) {
+ chrome.browserAction.setBadgeText(options);
+ } else {
+ console.warn("can't set badge text, not supported", options);
+ }
+}
+
+function waitMs(timeoutMs: number): Promise<void> {
+ return new Promise((resolve, reject) => {
+ const bgPage = chrome.extension.getBackgroundPage();
+ if (!bgPage) {
+ reject("fatal: no background page");
+ return;
+ }
+ bgPage.setTimeout(() => resolve(), timeoutMs);
+ });
+}
+
+function makeSyncWalletRedirect(
+ url: string,
+ tabId: number,
+ oldUrl: string,
+ params?: { [name: string]: string | undefined },
+): Record<string, unknown> {
+ const innerUrl = new URL(chrome.extension.getURL("/" + url));
+ if (params) {
+ for (const key in params) {
+ const p = params[key];
+ if (p) {
+ innerUrl.searchParams.set(key, p);
+ }
+ }
+ }
+ if (isFirefox()) {
+ // Some platforms don't support the sync redirect (yet), so fall back to
+ // async redirect after a timeout.
+ const doit = async (): Promise<void> => {
+ await waitMs(150);
+ const tab = await getTab(tabId);
+ if (tab.url === oldUrl) {
+ chrome.tabs.update(tabId, { url: innerUrl.href });
+ }
+ };
+ doit();
+ }
+ console.log("redirecting to", innerUrl.href);
+ chrome.tabs.update(tabId, { url: innerUrl.href });
+ return { redirectUrl: innerUrl.href };
+}
+
+async function reinitWallet(): Promise<void> {
+ if (currentWallet) {
+ currentWallet.stop();
+ currentWallet = undefined;
+ }
+ currentDatabase = undefined;
+ setBadgeText({ text: "" });
+ try {
+ currentDatabase = await db.openTalerDatabase(indexedDB, reinitWallet);
+ } catch (e) {
+ console.error("could not open database", e);
+ walletInit.reject(e);
+ return;
+ }
+ const http = new BrowserHttpLib();
+ console.log("setting wallet");
+ const wallet = new Wallet(
+ new queryLib.Database(currentDatabase),
+ http,
+ new BrowserCryptoWorkerFactory(),
+ );
+ wallet.addNotificationListener((x) => {
+ for (const x of notificationPorts) {
+ try {
+ x.postMessage({ type: "notification" });
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ });
+ wallet.runRetryLoop().catch((e) => {
+ console.log("error during wallet retry loop", e);
+ });
+ // Useful for debugging in the background page.
+ (window as any).talerWallet = wallet;
+ currentWallet = wallet;
+ walletInit.resolve();
+}
+
+try {
+ // This needs to be outside of main, as Firefox won't fire the event if
+ // the listener isn't created synchronously on loading the backend.
+ chrome.runtime.onInstalled.addListener((details) => {
+ console.log("onInstalled with reason", details.reason);
+ if (details.reason === "install") {
+ const url = chrome.extension.getURL("/welcome.html");
+ chrome.tabs.create({ active: true, url: url });
+ }
+ });
+} catch (e) {
+ console.error(e);
+}
+
+function headerListener(
+ details: chrome.webRequest.WebResponseHeadersDetails,
+): chrome.webRequest.BlockingResponse | undefined {
+ console.log("header listener");
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ return;
+ }
+ const wallet = currentWallet;
+ if (!wallet) {
+ console.warn("wallet not available while handling header");
+ return;
+ }
+ console.log("in header listener");
+ if (details.statusCode === 402 || details.statusCode === 202) {
+ console.log(`got 402/202 from ${details.url}`);
+ for (const header of details.responseHeaders || []) {
+ if (header.name.toLowerCase() === "taler") {
+ const talerUri = header.value || "";
+ const uriType = taleruri.classifyTalerUri(talerUri);
+ switch (uriType) {
+ case taleruri.TalerUriType.TalerWithdraw:
+ return makeSyncWalletRedirect(
+ "withdraw.html",
+ details.tabId,
+ details.url,
+ {
+ talerWithdrawUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerPay:
+ return makeSyncWalletRedirect(
+ "pay.html",
+ details.tabId,
+ details.url,
+ {
+ talerPayUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerTip:
+ return makeSyncWalletRedirect(
+ "tip.html",
+ details.tabId,
+ details.url,
+ {
+ talerTipUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerRefund:
+ return makeSyncWalletRedirect(
+ "refund.html",
+ details.tabId,
+ details.url,
+ {
+ talerRefundUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerNotifyReserve:
+ Promise.resolve().then(() => {
+ const w = currentWallet;
+ if (!w) {
+ return;
+ }
+ w.handleNotifyReserve();
+ });
+ break;
+ default:
+ console.warn(
+ "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
+ );
+ break;
+ }
+ }
+ }
+ }
+ return;
+}
+
+function setupHeaderListener(): void {
+ console.log("setting up header listener");
+ // Handlers for catching HTTP requests
+ getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
+ if (
+ chrome.webRequest.onHeadersReceived &&
+ chrome.webRequest.onHeadersReceived.hasListener(headerListener)
+ ) {
+ chrome.webRequest.onHeadersReceived.removeListener(headerListener);
+ }
+ if (result) {
+ console.log("actually adding listener");
+ chrome.webRequest.onHeadersReceived.addListener(
+ headerListener,
+ { urls: ["<all_urls>"] },
+ ["responseHeaders", "blocking"],
+ );
+ }
+ chrome.webRequest.handlerBehaviorChanged(() => {
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ }
+ });
+ });
+}
+
+/**
+ * Main function to run for the WebExtension backend.
+ *
+ * Sets up all event handlers and other machinery.
+ */
+export async function wxMain(): Promise<void> {
+ // Explicitly unload the extension page as soon as an update is available,
+ // so the update gets installed as soon as possible.
+ chrome.runtime.onUpdateAvailable.addListener((details) => {
+ console.log("update available:", details);
+ chrome.runtime.reload();
+ });
+ reinitWallet();
+
+ // Handlers for messages coming directly from the content
+ // script on the page
+ chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
+ dispatch(req, sender, sendResponse);
+ return true;
+ });
+
+ chrome.runtime.onConnect.addListener((port) => {
+ notificationPorts.push(port);
+ port.onDisconnect.addListener((discoPort) => {
+ const idx = notificationPorts.indexOf(discoPort);
+ if (idx >= 0) {
+ notificationPorts.splice(idx, 1);
+ }
+ });
+ });
+
+ try {
+ setupHeaderListener();
+ } catch (e) {
+ console.log(e);
+ }
+
+ // On platforms that support it, also listen to external
+ // modification of permissions.
+ getPermissionsApi().addPermissionsListener((perm) => {
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ return;
+ }
+ setupHeaderListener();
+ });
+}
diff --git a/packages/taler-wallet-webextension/tsconfig.json b/packages/taler-wallet-webextension/tsconfig.json
new file mode 100644
index 000000000..c3c4144bf
--- /dev/null
+++ b/packages/taler-wallet-webextension/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "lib": ["es6", "DOM"],
+ "jsx": "react",
+ "reactNamespace": "React",
+ "module": "commonjs",
+ "target": "es5",
+ "noImplicitAny": true,
+ "outDir": "lib",
+ "declaration": true,
+ "noEmitOnError": true,
+ "strict": true,
+ "incremental": true,
+ "sourceMap": true,
+ "esModuleInterop": true
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/taler-wallet-webextension/webextension/manifest.json b/packages/taler-wallet-webextension/webextension/manifest.json
new file mode 100644
index 000000000..b09e3ecbd
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/manifest.json
@@ -0,0 +1,49 @@
+{
+ "manifest_version": 2,
+
+ "name": "GNU Taler Wallet (git)",
+ "description": "Privacy preserving and transparent payments",
+ "author": "GNU Taler Developers",
+ "version": "0.6.77.4",
+ "version_name": "0.7.1-dev.3",
+
+ "minimum_chrome_version": "51",
+ "minimum_opera_version": "36",
+
+ "applications": {
+ "gecko": {
+ "id": "wallet@taler.net",
+ "strict_min_version": "68.0"
+ }
+ },
+
+ "icons": {
+ "32": "img/icon.png",
+ "128": "img/logo.png"
+ },
+
+ "permissions": [
+ "storage",
+ "activeTab"
+ ],
+
+ "optional_permissions": [
+ "webRequest",
+ "webRequestBlocking",
+ "http://*/*",
+ "https://*/*"
+ ],
+
+ "browser_action": {
+ "default_icon": {
+ "32": "img/icon.png"
+ },
+ "default_title": "Taler",
+ "default_popup": "popup.html"
+ },
+
+ "background": {
+ "page": "background.html",
+ "persistent": true
+ }
+}
diff --git a/packages/taler-wallet-webextension/webextension/pack.sh b/packages/taler-wallet-webextension/webextension/pack.sh
new file mode 100755
index 000000000..ef005014f
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/pack.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+set -eu
+
+if [[ ! -e package.json ]]; then
+ echo "Please run this from the root of the repo.">&2
+ exit 1
+fi
+
+vers_manifest=$(jq -r '.version' webextension/manifest.json)
+
+rm -rf dist/wx
+mkdir -p dist/wx
+cp webextension/manifest.json dist/wx/
+cp -r webextension/static/* dist/wx/
+cp -r dist/webextension/* dist/wx/
+
+cd dist/wx
+
+zipfile="../taler-wallet-${vers_manifest}.zip"
+
+rm -f -- "$zipfile"
+zip -r "$zipfile" ./*
diff --git a/packages/taler-wallet-webextension/webextension/static/add-auditor.html b/packages/taler-wallet-webextension/webextension/static/add-auditor.html
new file mode 100644
index 000000000..47a97c075
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/add-auditor.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+
+ <title>Taler Wallet: Add Auditor</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+
+ <link rel="icon" href="/img/icon.png" />
+
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/auditors.html b/packages/taler-wallet-webextension/webextension/static/auditors.html
new file mode 100644
index 000000000..15261290d
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/auditors.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Auditors</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+
+ <link rel="icon" href="/img/icon.png" />
+
+ <script src="/dist/webextension/pageEntryPoint.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ }
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/background.html b/packages/taler-wallet-webextension/webextension/static/background.html
new file mode 100644
index 000000000..b89c05588
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/background.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <script src="/background.js"></script>
+ <title>(wallet bg page)</title>
+ </head>
+ <body>
+ <img id="taler-logo" src="/img/icon.png" />
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/benchmark.html b/packages/taler-wallet-webextension/webextension/static/benchmark.html
new file mode 100644
index 000000000..a29fe0725
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/benchmark.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Benchmarks</title>
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+ <body>
+ <section id="main">
+ <h1>Benchmarks</h1>
+ <div id="container"></div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/img/icon.png b/packages/taler-wallet-webextension/webextension/static/img/icon.png
new file mode 100644
index 000000000..b4733bebc
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/icon.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.png b/packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.png
new file mode 100644
index 000000000..acf84baaf
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/webextension/static/img/logo.png b/packages/taler-wallet-webextension/webextension/static/img/logo.png
new file mode 120000
index 000000000..1ddb87d2c
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/logo.png
@@ -0,0 +1 @@
+logo-2015-medium.png \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg b/packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg
new file mode 100644
index 000000000..f6f7dfcb3
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg
@@ -0,0 +1,53 @@
+<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
+<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#fff">
+ <rect y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.5s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.5s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="30" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.25s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.25s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="60" width="15" height="140" rx="6">
+ <animate attributeName="height"
+ begin="0s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="90" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.25s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.25s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="120" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.5s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.5s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+</svg>
diff --git a/packages/taler-wallet-webextension/webextension/static/pay.html b/packages/taler-wallet-webextension/webextension/static/pay.html
new file mode 100644
index 000000000..452c56df0
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/pay.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Confirm Contract</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ button.accept {
+ background-color: #5757d2;
+ border: 1px solid black;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: white;
+ }
+ button.linky {
+ background: none !important;
+ border: none;
+ padding: 0 !important;
+
+ font-family: arial, sans-serif;
+ color: #069;
+ text-decoration: underline;
+ cursor: pointer;
+ }
+
+ input.url {
+ width: 25em;
+ }
+
+ button.accept:disabled {
+ background-color: #dedbe8;
+ border: 1px solid white;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: #2c2c2c;
+ }
+
+ .errorbox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #ff8a8a;
+ }
+
+ .okaybox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #00fa9a;
+ }
+ </style>
+ </head>
+
+ <body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <article id="container" class="fade"></article>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/payback.html b/packages/taler-wallet-webextension/webextension/static/payback.html
new file mode 100644
index 000000000..7ca9dc974
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/payback.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Payback</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ }
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/popup.html b/packages/taler-wallet-webextension/webextension/static/popup.html
new file mode 100644
index 000000000..83f2f2861
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/popup.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="stylesheet" type="text/css" href="/style/popup.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <div id="container" style="margin: 0; padding: 0;"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/refund.html b/packages/taler-wallet-webextension/webextension/static/refund.html
new file mode 100644
index 000000000..3c1d78a24
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/refund.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Refund Status</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <article id="container" class="fade"></article>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/reset-required.html b/packages/taler-wallet-webextension/webextension/static/reset-required.html
new file mode 100644
index 000000000..84943fbf1
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/reset-required.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Select Taler Provider</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ overflow-y: scroll;
+ }
+ </style>
+ </head>
+
+ <body>
+ <section id="main">
+ <div id="container"></div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/return-coins.html b/packages/taler-wallet-webextension/webextension/static/return-coins.html
new file mode 100644
index 000000000..90703b447
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/return-coins.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Return Coins to Bank Account</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/style/popup.css b/packages/taler-wallet-webextension/webextension/static/style/popup.css
new file mode 100644
index 000000000..cca002399
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/style/popup.css
@@ -0,0 +1,185 @@
+/**
+ * @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;
+ background-color: #f8faf7;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+.nav {
+ background-color: #033;
+ padding: 0.5em 0;
+}
+
+.nav a {
+ color: #f8faf7;
+ padding: 0.7em 1.4em;
+ text-decoration: none;
+}
+
+.nav a.active {
+ background-color: #f8faf7;
+ color: #000;
+ 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 {
+ min-width: 380px;
+ display: flex;
+ flex-direction: row;
+ border-bottom: 1px solid #d9dbd8;
+ padding: 0.5em;
+ align-items: center;
+}
+
+.historyItem .amount {
+ font-size: 110%;
+ font-weight: bold;
+ text-align: right;
+}
+
+.historyDate,
+.historyTitle,
+.historyText,
+.historySmall {
+ margin: 0.3em;
+}
+
+.historyDate {
+ font-size: 90%;
+ color: slategray;
+ text-align: right;
+}
+
+.historyLeft {
+ display: flex;
+ flex-direction: column;
+ text-align: right;
+}
+
+.historyContent {
+ flex: 1;
+}
+
+.historyTitle {
+ font-weight: 400;
+}
+
+.historyText {
+ font-size: 90%;
+}
+
+.historySmall {
+ font-size: 70%;
+ text-transform: uppercase;
+}
+
+.historyAmount {
+ flex-grow: 1;
+}
+
+.historyAmount .primary {
+ font-size: 100%;
+}
+
+.historyAmount .secondary {
+ font-size: 80%;
+}
+
+.historyAmount .positive {
+ color: #088;
+}
+
+.historyAmount .positive:before {
+ content: "+";
+}
+
+.historyAmount .negative {
+ color: #800;
+}
+
+.historyAmount .negative:before {
+ color: #800;
+ content: "-";
+}
+.icon {
+ margin: 0 10px;
+ text-align: center;
+ width: 35px;
+ font-size: 20px;
+ border-radius: 50%;
+ background: #ccc;
+ color: #fff;
+ padding-top: 4px;
+ height: 30px;
+}
+
+.option {
+ text-transform: uppercase;
+ text-align: right;
+ padding: 0.4em;
+ font-size: 0.9em;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+ vertical-align: middle;
+ position: relative;
+ bottom: 1px;
+}
+
+input[type="radio"] {
+ bottom: 2px;
+}
+
+.balance {
+ text-align: center;
+ padding-top: 2em;
+}
diff --git a/packages/taler-wallet-webextension/webextension/static/style/pure.css b/packages/taler-wallet-webextension/webextension/static/style/pure.css
new file mode 100644
index 000000000..88a4bb7d7
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/style/pure.css
@@ -0,0 +1,1513 @@
+/*!
+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.5%;
+ *width: 12.469%;
+}
+
+.pure-u-1-6,
+.pure-u-4-24 {
+ width: 16.6667%;
+ *width: 16.6357%;
+}
+
+.pure-u-1-5 {
+ width: 20%;
+ *width: 19.969%;
+}
+
+.pure-u-5-24 {
+ width: 20.8333%;
+ *width: 20.8023%;
+}
+
+.pure-u-1-4,
+.pure-u-6-24 {
+ width: 25%;
+ *width: 24.969%;
+}
+
+.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.5%;
+ *width: 37.469%;
+}
+
+.pure-u-2-5 {
+ width: 40%;
+ *width: 39.969%;
+}
+
+.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.969%;
+}
+
+.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.969%;
+}
+
+.pure-u-5-8,
+.pure-u-15-24 {
+ width: 62.5%;
+ *width: 62.469%;
+}
+
+.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.969%;
+}
+
+.pure-u-19-24 {
+ width: 79.1667%;
+ *width: 79.1357%;
+}
+
+.pure-u-4-5 {
+ width: 80%;
+ *width: 79.969%;
+}
+
+.pure-u-5-6,
+.pure-u-20-24 {
+ width: 83.3333%;
+ *width: 83.3023%;
+}
+
+.pure-u-7-8,
+.pure-u-21-24 {
+ width: 87.5%;
+ *width: 87.469%;
+}
+
+.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.8); /* 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.1)
+ );
+ background-image: linear-gradient(
+ transparent,
+ rgba(0, 0, 0, 0.05) 40%,
+ rgba(0, 0, 0, 0.1)
+ );
+}
+.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.2) 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.4;
+ 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: 0.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: 0.3em 0;
+}
+
+.pure-menu-horizontal .pure-menu-separator {
+ width: 1px;
+ height: 1.3em;
+ margin: 0 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: 0.5em 1em;
+}
+
+.pure-menu-disabled {
+ opacity: 0.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/packages/taler-wallet-webextension/webextension/static/style/wallet.css b/packages/taler-wallet-webextension/webextension/static/style/wallet.css
new file mode 100644
index 000000000..7c06f2386
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/style/wallet.css
@@ -0,0 +1,290 @@
+body {
+ font-size: 100%;
+ overflow-y: scroll;
+ margin-top: 2em;
+}
+
+#main {
+ border: solid 5px black;
+ border-radius: 10px;
+ margin-left: auto;
+ margin-right: auto;
+ padding-top: 2em;
+ max-width: 50%;
+ padding: 2em;
+}
+
+header {
+ width: 100%;
+ height: 100px;
+ margin: 0;
+ padding: 0;
+}
+
+header #logo {
+ float: left;
+ width: 100px;
+ height: 100px;
+ padding: 0;
+ margin: 0;
+ text-align: center;
+ background-image: url(/img/logo.png);
+ background-size: 100px;
+}
+
+aside {
+ width: 100px;
+ float: left;
+}
+
+section#main {
+ margin: auto;
+ padding: 20px;
+ height: 100%;
+ max-width: 50%;
+}
+
+section#main h1:first-child {
+ margin-top: 0;
+}
+
+h1 {
+ font-size: 160%;
+ font-family: "monospace";
+}
+
+h2 {
+ font-size: 140%;
+ font-family: "monospace";
+}
+
+h3 {
+ font-size: 120%;
+ font-family: "monospace";
+}
+
+h4,
+h5,
+h6 {
+ font-family: "monospace";
+ font-size: 100%;
+}
+
+.form-row {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+label {
+ padding-right: 1em;
+}
+
+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;
+}
+
+.blacklink a:link,
+.blacklink a:visited,
+.blacklink a:hover,
+.blacklink a:active {
+ color: #000;
+}
+
+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;
+}
+
+.errorbox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #ff8a8a;
+}
+
+.okaybox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #00fa9a;
+}
+
+a.opener {
+ color: black;
+}
+.opener-open::before {
+ content: "\25bc";
+}
+.opener-collapsed::before {
+ content: "\25b6 ";
+}
+
+.svg-icon {
+ display: inline-flex;
+ align-self: center;
+ position: relative;
+ height: 1em;
+ width: 1em;
+}
+.svg-icon svg {
+ height: 1em;
+ width: 1em;
+}
+object.svg-icon.svg-baseline {
+ transform: translate(0, 0.125em);
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+}
+
+/* Hide default HTML checkbox */
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/webextension/static/tip.html b/packages/taler-wallet-webextension/webextension/static/tip.html
new file mode 100644
index 000000000..00ed4d248
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/tip.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Received Tip</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <div id="container"></div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/welcome.html b/packages/taler-wallet-webextension/webextension/static/welcome.html
new file mode 100644
index 000000000..07ecac707
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/welcome.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet Installed</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
+ <h1 style="font-family: monospace; font-size: 250%;">
+ <span style="color: #aa3939;">❰</span>Taler Wallet<span style="color: #aa3939;">❱</span>
+ </h1>
+ </div>
+ <h1>Browser Extension Installed!</h1>
+ <div id="container">Loading...</div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/withdraw.html b/packages/taler-wallet-webextension/webextension/static/withdraw.html
new file mode 100644
index 000000000..5137204bd
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/withdraw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Withdraw</title>
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
+ <h1 style="font-family: monospace; font-size: 250%;">
+ <span style="color: #aa3939;">❰</span>Taler Wallet<span style="color: #aa3939;">❱</span>
+ </h1>
+ </div>
+ <div class="fade" id="container"></div>
+ </section>
+ </body>
+</html>