diff options
Diffstat (limited to 'src/webex')
-rw-r--r-- | src/webex/background.ts | 30 | ||||
-rw-r--r-- | src/webex/chromeBadge.ts | 288 | ||||
-rw-r--r-- | src/webex/compat.ts | 85 | ||||
-rw-r--r-- | src/webex/i18n-test.tsx | 69 | ||||
-rw-r--r-- | src/webex/i18n.tsx | 250 | ||||
-rw-r--r-- | src/webex/pageEntryPoint.ts | 72 | ||||
-rw-r--r-- | src/webex/pages/add-auditor.tsx | 135 | ||||
-rw-r--r-- | src/webex/pages/auditors.tsx | 161 | ||||
-rw-r--r-- | src/webex/pages/benchmark.tsx | 104 | ||||
-rw-r--r-- | src/webex/pages/pay.tsx | 182 | ||||
-rw-r--r-- | src/webex/pages/payback.tsx | 30 | ||||
-rw-r--r-- | src/webex/pages/popup.tsx | 499 | ||||
-rw-r--r-- | src/webex/pages/refund.tsx | 89 | ||||
-rw-r--r-- | src/webex/pages/reset-required.tsx | 93 | ||||
-rw-r--r-- | src/webex/pages/return-coins.tsx | 30 | ||||
-rw-r--r-- | src/webex/pages/tip.tsx | 103 | ||||
-rw-r--r-- | src/webex/pages/welcome.tsx | 190 | ||||
-rw-r--r-- | src/webex/pages/withdraw.tsx | 229 | ||||
-rw-r--r-- | src/webex/permissions.ts | 20 | ||||
-rw-r--r-- | src/webex/renderHtml.tsx | 344 | ||||
-rw-r--r-- | src/webex/wxApi.ts | 310 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 575 |
22 files changed, 0 insertions, 3888 deletions
diff --git a/src/webex/background.ts b/src/webex/background.ts deleted file mode 100644 index dbc540df4..000000000 --- a/src/webex/background.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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/src/webex/chromeBadge.ts b/src/webex/chromeBadge.ts deleted file mode 100644 index 7bc5d368d..000000000 --- a/src/webex/chromeBadge.ts +++ /dev/null @@ -1,288 +0,0 @@ -/* - This file is part of TALER - (C) 2016 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -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/src/webex/compat.ts b/src/webex/compat.ts deleted file mode 100644 index 4635abd80..000000000 --- a/src/webex/compat.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - This file is part of TALER - (C) 2017 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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/src/webex/i18n-test.tsx b/src/webex/i18n-test.tsx deleted file mode 100644 index 4a1c40254..000000000 --- a/src/webex/i18n-test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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 } from "./i18n"; -import { strings } from "../i18n/strings"; -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/src/webex/i18n.tsx b/src/webex/i18n.tsx deleted file mode 100644 index 6b5c2318d..000000000 --- a/src/webex/i18n.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Translation helpers for React components and template literals. - */ - -/** - * Imports. - */ -import { strings } from "../i18n/strings"; - -// @ts-ignore: no type decl for this library -import * as jedLib from "jed"; - -import * as React from "react"; - -let jed = setupJed(); - -const enableTracing = false; - -/** - * Set up jed library for internationalization, - * based on browser language settings. - */ -function setupJed(): any { - let lang: string; - try { - lang = chrome.i18n.getUILanguage(); - // Chrome gives e.g. "en-US", but Firefox gives us "en_US" - lang = lang.replace("_", "-"); - } catch (e) { - lang = "en"; - console.warn("i18n default language not available"); - } - - if (!strings[lang]) { - lang = "en-US"; - console.log(`language ${lang} not found, defaulting to english`); - } - return new jedLib.Jed(strings[lang]); -} - -/** - * Use different translations for testing. Should not be used outside - * of test cases. - */ -export function internalSetStrings(langStrings: any): void { - jed = new jedLib.Jed(langStrings); -} - -/** - * 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; -} - -/** - * Internationalize a string template with arbitrary serialized values. - */ -export function str(stringSeq: TemplateStringsArray, ...values: any[]): string { - const s = toI18nString(stringSeq); - const tr = jed - .translate(s) - .ifPlural(1, s) - .fetch(...values); - return tr; -} - -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(); - enableTracing && 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 = 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 = 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 = jed.ngettext(s, s, this.props.target); - const result = getTranslatedChildren(translation, this.props.children); - return <div>{result}</div>; - } -} diff --git a/src/webex/pageEntryPoint.ts b/src/webex/pageEntryPoint.ts deleted file mode 100644 index 9fd1d36f1..000000000 --- a/src/webex/pageEntryPoint.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - 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/src/webex/pages/add-auditor.tsx b/src/webex/pages/add-auditor.tsx deleted file mode 100644 index c28d15cad..000000000 --- a/src/webex/pages/add-auditor.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Inria - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * View and edit auditors. - * - * @author Florian Dold - */ - -import { CurrencyRecord } from "../../types/dbTypes"; -import { getCurrencies, updateCurrency } from "../wxApi"; -import React, { useState } from "react"; - -interface ConfirmAuditorProps { - url: string; - currency: string; - auditorPub: string; - expirationStamp: number; -} - -function ConfirmAuditor(props: ConfirmAuditorProps): JSX.Element { - const [addDone, setAddDone] = useState(false); - - const add = async (): Promise<void> => { - const currencies = await getCurrencies(); - let currency: CurrencyRecord | undefined; - - for (const c of currencies) { - if (c.name === props.currency) { - currency = c; - } - } - - if (!currency) { - currency = { - name: props.currency, - auditors: [], - fractionalDigits: 2, - exchanges: [], - }; - } - - const newAuditor = { - auditorPub: props.auditorPub, - baseUrl: props.url, - expirationStamp: props.expirationStamp, - }; - - let auditorFound = false; - for (const idx in currency.auditors) { - const a = currency.auditors[idx]; - if (a.baseUrl === props.url) { - auditorFound = true; - // Update auditor if already found by URL. - currency.auditors[idx] = newAuditor; - } - } - - if (!auditorFound) { - currency.auditors.push(newAuditor); - } - - await updateCurrency(currency); - - setAddDone(true); - }; - - const back = (): void => { - window.history.back(); - }; - - return ( - <div id="main"> - <p> - Do you want to let <strong>{props.auditorPub}</strong> audit the - currency "{props.currency}"? - </p> - {addDone ? ( - <div> - Auditor was added! You can also{" "} - <a href={chrome.extension.getURL("/auditors.html")}>view and edit</a>{" "} - auditors. - </div> - ) : ( - <div> - <button - onClick={() => add()} - className="pure-button pure-button-primary" - > - Yes - </button> - <button onClick={() => back()} className="pure-button"> - No - </button> - </div> - )} - </div> - ); -} - -export function makeAddAuditorPage(): JSX.Element { - const walletPageUrl = new URL(document.location.href); - const url = walletPageUrl.searchParams.get("url"); - if (!url) { - throw Error("missign parameter (url)"); - } - const currency = walletPageUrl.searchParams.get("currency"); - if (!currency) { - throw Error("missing parameter (currency)"); - } - const auditorPub = walletPageUrl.searchParams.get("auditorPub"); - if (!auditorPub) { - throw Error("missing parameter (auditorPub)"); - } - const auditorStampStr = walletPageUrl.searchParams.get("expirationStamp"); - if (!auditorStampStr) { - throw Error("missing parameter (auditorStampStr)"); - } - const expirationStamp = Number.parseInt(auditorStampStr); - const args = { url, currency, auditorPub, expirationStamp }; - return <ConfirmAuditor {...args} />; -} diff --git a/src/webex/pages/auditors.tsx b/src/webex/pages/auditors.tsx deleted file mode 100644 index ac93afd31..000000000 --- a/src/webex/pages/auditors.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Inria - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * View and edit auditors. - * - * @author Florian Dold - */ - -import { - AuditorRecord, - CurrencyRecord, - ExchangeForCurrencyRecord, -} from "../../types/dbTypes"; - -import { getCurrencies, updateCurrency } from "../wxApi"; - -import * as React from "react"; - -interface CurrencyListState { - currencies?: CurrencyRecord[]; -} - -class CurrencyList extends React.Component<{}, CurrencyListState> { - constructor(props: {}) { - super(props); - const port = chrome.runtime.connect(); - port.onMessage.addListener((msg: any) => { - if (msg.notify) { - console.log("got notified"); - this.update(); - } - }); - this.update(); - this.state = {} as any; - } - - async update(): Promise<void> { - const currencies = await getCurrencies(); - console.log("currencies: ", currencies); - this.setState({ currencies }); - } - - async confirmRemoveAuditor( - c: CurrencyRecord, - a: AuditorRecord, - ): Promise<void> { - if ( - window.confirm( - `Do you really want to remove auditor ${a.baseUrl} for currency ${c.name}?`, - ) - ) { - c.auditors = c.auditors.filter((x) => x.auditorPub !== a.auditorPub); - await updateCurrency(c); - } - } - - async confirmRemoveExchange( - c: CurrencyRecord, - e: ExchangeForCurrencyRecord, - ): Promise<void> { - if ( - window.confirm( - `Do you really want to remove exchange ${e.baseUrl} for currency ${c.name}?`, - ) - ) { - c.exchanges = c.exchanges.filter((x) => x.baseUrl !== e.baseUrl); - await updateCurrency(c); - } - } - - renderAuditors(c: CurrencyRecord): any { - if (c.auditors.length === 0) { - return <p>No trusted auditors for this currency.</p>; - } - return ( - <div> - <p>Trusted Auditors:</p> - <ul> - {c.auditors.map((a) => ( - <li key={a.baseUrl}> - {a.baseUrl}{" "} - <button - className="pure-button button-destructive" - onClick={() => this.confirmRemoveAuditor(c, a)} - > - Remove - </button> - <ul> - <li>valid until {new Date(a.expirationStamp).toString()}</li> - <li>public key {a.auditorPub}</li> - </ul> - </li> - ))} - </ul> - </div> - ); - } - - renderExchanges(c: CurrencyRecord): any { - if (c.exchanges.length === 0) { - return <p>No trusted exchanges for this currency.</p>; - } - return ( - <div> - <p>Trusted Exchanges:</p> - <ul> - {c.exchanges.map((e) => ( - <li key={e.baseUrl}> - {e.baseUrl}{" "} - <button - className="pure-button button-destructive" - onClick={() => this.confirmRemoveExchange(c, e)} - > - Remove - </button> - </li> - ))} - </ul> - </div> - ); - } - - render(): JSX.Element { - const currencies = this.state.currencies; - if (!currencies) { - return <span>...</span>; - } - return ( - <div id="main"> - {currencies.map((c) => ( - <div key={c.name}> - <h1>Currency {c.name}</h1> - <p>Displayed with {c.fractionalDigits} fractional digits.</p> - <h2>Auditors</h2> - <div>{this.renderAuditors(c)}</div> - <h2>Exchanges</h2> - <div>{this.renderExchanges(c)}</div> - </div> - ))} - </div> - ); - } -} - -export function makeAuditorsPage(): JSX.Element { - return <CurrencyList />; -} diff --git a/src/webex/pages/benchmark.tsx b/src/webex/pages/benchmark.tsx deleted file mode 100644 index eb7193e0c..000000000 --- a/src/webex/pages/benchmark.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - This file is part of TALER - (C) 2015 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Benchmarks for the wallet. - * - * @author Florian Dold - */ - -import * as i18n from "../i18n"; - -import { BenchmarkResult } from "../../types/walletTypes"; - -import * as wxApi from "../wxApi"; - -import * as React from "react"; - -interface BenchmarkRunnerState { - repetitions: number; - result?: BenchmarkResult; - running: boolean; -} - -function BenchmarkDisplay(props: BenchmarkRunnerState): JSX.Element { - const result = props.result; - if (!result) { - if (props.running) { - return <div>Waiting for results ...</div>; - } else { - return <div></div>; - } - } - return ( - <> - <h2>Results for {result.repetitions} repetitions</h2> - <table className="pure-table"> - <thead> - <tr> - <th>{i18n.str`Operation`}</th> - <th>{i18n.str`time (ms/op)`}</th> - </tr> - {Object.keys(result.time) - .sort() - .map((k) => ( - <tr key={k}> - <td>{k}</td> - <td>{result.time[k] / result.repetitions}</td> - </tr> - ))} - </thead> - </table> - </> - ); -} - -class BenchmarkRunner extends React.Component<any, BenchmarkRunnerState> { - constructor(props: any) { - super(props); - this.state = { - repetitions: 10, - running: false, - }; - } - - async run(): Promise<void> { - this.setState({ result: undefined, running: true }); - const result = await wxApi.benchmarkCrypto(this.state.repetitions); - this.setState({ result, running: false }); - } - - render(): JSX.Element { - return ( - <div> - <label>Repetitions:</label> - <input - type="number" - value={this.state.repetitions} - onChange={(evt) => - this.setState({ repetitions: Number.parseInt(evt.target.value) }) - } - />{" "} - <button onClick={() => this.run()}>Run</button> - <BenchmarkDisplay {...this.state} /> - </div> - ); - } -} - -export function makeBenchmarkPage(): JSX.Element { - return <BenchmarkRunner />; -} diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx deleted file mode 100644 index ce44c0040..000000000 --- a/src/webex/pages/pay.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* - This file is part of TALER - (C) 2015 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Page shown to the user to confirm entering - * a contract. - */ - -/** - * Imports. - */ -import * as i18n from "../i18n"; - -import { PreparePayResult, PreparePayResultType } from "../../types/walletTypes"; - -import { renderAmount, ProgressButton } from "../renderHtml"; -import * as wxApi from "../wxApi"; - -import React, { useState, useEffect } from "react"; - -import * as Amounts from "../../util/amounts"; -import { codecForContractTerms, ContractTerms } from "../../types/talerTypes"; - -function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element { - const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(); - const [payErrMsg, setPayErrMsg] = useState<string | undefined>(""); - const [numTries, setNumTries] = useState(0); - const [loading, setLoading] = useState(false); - let amountEffective: Amounts.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 === 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: ContractTerms; - - try { - contractTerms = 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/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx deleted file mode 100644 index 5d42f5f47..000000000 --- a/src/webex/pages/payback.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Inria - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx deleted file mode 100644 index 8a99a6d90..000000000 --- a/src/webex/pages/popup.tsx +++ /dev/null @@ -1,499 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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 } from "../../util/amounts"; -import * as Amounts from "../../util/amounts"; - -import { abbrev, renderAmount, PageLink } from "../renderHtml"; -import * as wxApi from "../wxApi"; - -import React, { Fragment, useState, useEffect } from "react"; - -import moment from "moment"; -import { Timestamp } from "../../util/time"; -import { classifyTalerUri, TalerUriType } from "../../util/taleruri"; -import { PermissionsCheckbox } from "./welcome"; -import { BalancesResponse, Balance } from "../../types/walletTypes"; - -// 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: 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: 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: 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.isNonZero(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 = classifyTalerUri(talerUri); - switch (uriType) { - case TalerUriType.TalerWithdraw: - return makeExtensionUrlWithParams("withdraw.html", { - talerWithdrawUri: talerUri, - }); - case TalerUriType.TalerPay: - return makeExtensionUrlWithParams("pay.html", { - talerPayUri: talerUri, - }); - case TalerUriType.TalerTip: - return makeExtensionUrlWithParams("tip.html", { - talerTipUri: talerUri, - }); - case TalerUriType.TalerRefund: - return makeExtensionUrlWithParams("refund.html", { - talerRefundUri: talerUri, - }); - case 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/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx deleted file mode 100644 index c5d6a00df..000000000 --- a/src/webex/pages/refund.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - This file is part of TALER - (C) 2015-2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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 { PurchaseDetails } from "../../types/walletTypes"; -import { AmountView } from "../renderHtml"; - -function RefundStatusView(props: { talerRefundUri: string }): JSX.Element { - const [applied, setApplied] = useState(false); - const [purchaseDetails, setPurchaseDetails] = useState< - 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/src/webex/pages/reset-required.tsx b/src/webex/pages/reset-required.tsx deleted file mode 100644 index 9e40e7981..000000000 --- a/src/webex/pages/reset-required.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - 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"; - -class 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'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/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx deleted file mode 100644 index e8cf8c9dd..000000000 --- a/src/webex/pages/return-coins.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - This file is part of TALER - (C) 2017 Inria - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx deleted file mode 100644 index 4a1d3743a..000000000 --- a/src/webex/pages/tip.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - 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 { TipStatus } from "../../types/walletTypes"; - -function TipDisplay(props: { talerTipUri: string }): JSX.Element { - const [tipStatus, setTipStatus] = useState<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'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/src/webex/pages/welcome.tsx b/src/webex/pages/welcome.tsx deleted file mode 100644 index a7c24d659..000000000 --- a/src/webex/pages/welcome.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - 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 { WalletDiagnostics } from "../../types/walletTypes"; -import * as wxApi from "../wxApi"; -import { getPermissionsApi } from "../compat"; -import { extendedPermissions } from "../permissions"; - -function Diagnostics(): JSX.Element | null { - const [timedOut, setTimedOut] = useState(false); - const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>( - undefined, - ); - - useEffect(() => { - let gotDiagnostics = false; - setTimeout(() => { - if (!gotDiagnostics) { - console.error("timed out"); - setTimedOut(true); - } - }, 1000); - const doFetch = async (): 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/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx deleted file mode 100644 index 4a92704b3..000000000 --- a/src/webex/pages/withdraw.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* - This file is part of TALER - (C) 2015-2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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/src/webex/permissions.ts b/src/webex/permissions.ts deleted file mode 100644 index bcd357fd6..000000000 --- a/src/webex/permissions.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - 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/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx deleted file mode 100644 index 39ff470a2..000000000 --- a/src/webex/renderHtml.tsx +++ /dev/null @@ -1,344 +0,0 @@ -/* - This file is part of TALER - (C) 2016 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Helpers functions to render Taler-related data structures to HTML. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { AmountJson } from "../util/amounts"; -import * as Amounts from "../util/amounts"; -import { ExchangeWithdrawDetails } from "../types/walletTypes"; -import * as i18n from "./i18n"; -import React from "react"; -import { stringifyTimestamp } from "../util/time"; - -/** - * 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} {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: 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>{stringifyTimestamp(f.endStamp)}</td> - <td>{renderAmount(f.wireFee)}</td> - <td>{renderAmount(f.closingFee)}</td> - </tr> - ))} - </tbody> - </> - ); -} - -function AuditorDetailsView(props: { - rci: 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: 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): ${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: 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/src/webex/wxApi.ts b/src/webex/wxApi.ts deleted file mode 100644 index 4e11463d6..000000000 --- a/src/webex/wxApi.ts +++ /dev/null @@ -1,310 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Interface to the wallet through WebExtension messaging. - */ - -/** - * Imports. - */ -import { AmountJson } from "../util/amounts"; -import { - CoinRecord, - CurrencyRecord, - DenominationRecord, - ExchangeRecord, - ReserveRecord, -} from "../types/dbTypes"; -import { - BenchmarkResult, - ConfirmPayResult, - SenderWireInfos, - TipStatus, - PurchaseDetails, - WalletDiagnostics, - PreparePayResult, - AcceptWithdrawalResponse, - ExtendedPermissionsResponse, - BalancesResponse, -} from "../types/walletTypes"; - -/** - * 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); - } - }); - }); -} - -/** - * Get all exchanges the wallet knows about. - */ -export function getExchanges(): Promise<ExchangeRecord[]> { - return callBackend("get-exchanges", {}); -} - -/** - * Get all currencies the exchange knows about. - */ -export function getCurrencies(): Promise<CurrencyRecord[]> { - return callBackend("get-currencies", {}); -} - -/** - * Get information about a specific exchange. - */ -export function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> { - return callBackend("exchange-info", { baseUrl }); -} - -/** - * Replace an existing currency record with the one given. The currency to - * replace is specified inside the currency record. - */ -export function updateCurrency(currencyRecord: CurrencyRecord): Promise<void> { - return callBackend("update-currency", { currencyRecord }); -} - -/** - * Get all reserves the wallet has at an exchange. - */ -export function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> { - return callBackend("get-reserves", { exchangeBaseUrl }); -} - -/** - * Get all coins withdrawn from the given exchange. - */ -export function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> { - return callBackend("get-coins", { exchangeBaseUrl }); -} - -/** - * Get all denoms offered by the given exchange. - */ -export function getDenoms( - exchangeBaseUrl: string, -): Promise<DenominationRecord[]> { - return callBackend("get-denoms", { exchangeBaseUrl }); -} - -/** - * 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<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<BalancesResponse> { - return callBackend("balances", {}); -} - -/** - * Get possible sender wire infos for getting money - * wired from an exchange. - */ -export function getSenderWireInfos(): Promise<SenderWireInfos> { - return callBackend("get-sender-wire-infos", {}); -} - -/** - * 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<PurchaseDetails> { - return callBackend("get-purchase-details", { proposalId }); -} - -/** - * Get the status of processing a tip. - */ -export function getTipStatus(talerTipUri: string): Promise<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<BenchmarkResult> { - return callBackend("benchmark-crypto", { repetitions }); -} - -/** - * Get details about a pay operation. - */ -export function preparePay(talerPayUri: string): Promise<PreparePayResult> { - return callBackend("prepare-pay", { talerPayUri }); -} - -/** - * Get details about a withdraw operation. - */ -export function acceptWithdrawal( - talerWithdrawUri: string, - selectedExchange: string, -): Promise<AcceptWithdrawalResponse> { - return callBackend("accept-withdrawal", { - talerWithdrawUri, - selectedExchange, - }); -} - -/** - * Get diagnostics information - */ -export function getDiagnostics(): Promise<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/src/webex/wxBackend.ts b/src/webex/wxBackend.ts deleted file mode 100644 index 39fcf899e..000000000 --- a/src/webex/wxBackend.ts +++ /dev/null @@ -1,575 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <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 { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi"; -import { - deleteTalerDatabase, - openTalerDatabase, - WALLET_DB_MINOR_VERSION, -} from "../db"; -import { ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes"; -import { BrowserHttpLib } from "../util/http"; -import { OpenedPromise, openPromise } from "../util/promiseUtils"; -import { classifyTalerUri, TalerUriType } from "../util/taleruri"; -import { Wallet } from "../wallet"; -import { isFirefox, getPermissionsApi } from "./compat"; -import * as wxApi from "./wxApi"; -import MessageSender = chrome.runtime.MessageSender; -import { Database } from "../util/query"; -import { extendedPermissions } from "./permissions"; - -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: OpenedPromise<void> = 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": { - 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, - }; - const req = ReturnCoinsRequest.checked(d); - return needsWallet().returnCoins(req); - } - case "check-upgrade": { - let dbResetRequired = false; - if (!currentWallet) { - dbResetRequired = true; - } - const resp: wxApi.UpgradeResponse = { - currentDbVersion: 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: 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 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 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 = classifyTalerUri(talerUri); - switch (uriType) { - case TalerUriType.TalerWithdraw: - return makeSyncWalletRedirect( - "withdraw.html", - details.tabId, - details.url, - { - talerWithdrawUri: talerUri, - }, - ); - case TalerUriType.TalerPay: - return makeSyncWalletRedirect( - "pay.html", - details.tabId, - details.url, - { - talerPayUri: talerUri, - }, - ); - case TalerUriType.TalerTip: - return makeSyncWalletRedirect( - "tip.html", - details.tabId, - details.url, - { - talerTipUri: talerUri, - }, - ); - case TalerUriType.TalerRefund: - return makeSyncWalletRedirect( - "refund.html", - details.tabId, - details.url, - { - talerRefundUri: talerUri, - }, - ); - case 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(); - }); -} |