From 32f6409ac312f31821f791c3a376168289f0e4f4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 23 Mar 2022 10:50:12 -0300 Subject: all the browser related code move into one place, making it easy for specific platform code or mocking for testing --- .../taler-wallet-webextension/src/api/browser.ts | 54 --- .../taler-wallet-webextension/src/background.ts | 29 +- .../taler-wallet-webextension/src/chromeBadge.ts | 4 +- packages/taler-wallet-webextension/src/compat.ts | 101 ------ .../src/components/Diagnostics.tsx | 6 +- .../src/context/iocContext.ts | 4 +- .../taler-wallet-webextension/src/cta/Refund.tsx | 32 +- packages/taler-wallet-webextension/src/cta/Tip.tsx | 31 +- .../src/hooks/useExtendedPermissions.ts | 53 ++- .../taler-wallet-webextension/src/platform/api.ts | 90 +++++ .../src/platform/chrome.ts | 371 +++++++++++++++++++++ .../src/platform/firefox.ts | 74 ++++ .../src/popup/DeveloperPage.tsx | 20 -- .../src/popup/TalerActionFound.tsx | 53 +-- .../src/popupEntryPoint.tsx | 31 +- .../taler-wallet-webextension/src/renderHtml.tsx | 176 ---------- .../taler-wallet-webextension/src/utils/index.ts | 49 --- .../src/wallet/AddNewActionView.tsx | 13 +- .../src/walletEntryPoint.tsx | 23 +- packages/taler-wallet-webextension/src/wxApi.ts | 47 +-- .../taler-wallet-webextension/src/wxBackend.ts | 308 ++++------------- 21 files changed, 779 insertions(+), 790 deletions(-) delete mode 100644 packages/taler-wallet-webextension/src/api/browser.ts delete mode 100644 packages/taler-wallet-webextension/src/compat.ts create mode 100644 packages/taler-wallet-webextension/src/platform/api.ts create mode 100644 packages/taler-wallet-webextension/src/platform/chrome.ts create mode 100644 packages/taler-wallet-webextension/src/platform/firefox.ts delete mode 100644 packages/taler-wallet-webextension/src/renderHtml.tsx diff --git a/packages/taler-wallet-webextension/src/api/browser.ts b/packages/taler-wallet-webextension/src/api/browser.ts deleted file mode 100644 index b69a49680..000000000 --- a/packages/taler-wallet-webextension/src/api/browser.ts +++ /dev/null @@ -1,54 +0,0 @@ -function searchForTalerLinks(): string | undefined { - let found; - found = document.querySelector("a[href^='taler://'") - if (found) return found.toString() - found = document.querySelector("a[href^='taler+http://'") - if (found) return found.toString() - return undefined -} - -async function getCurrentTab() { - let queryOptions = { active: true, currentWindow: true }; - let [tab] = await chrome.tabs.query(queryOptions); - return tab; -} - - - -export async function findTalerUriInActiveTab(): Promise { - if (chrome.runtime.getManifest().manifest_version === 3) { - // manifest v3 - const tab = await getCurrentTab(); - const res = await chrome.scripting.executeScript({ - target: { - tabId: tab.id!, - allFrames: true, - } as any, - func: searchForTalerLinks, - args: [] - }) - return res[0].result - } - return new Promise((resolve, reject) => { - //manifest v2 - chrome.tabs.executeScript( - { - code: ` - (() => { - let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'"); - return x ? x.href.toString() : null; - })(); - `, - allFrames: false, - }, - (result) => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - resolve(undefined); - return; - } - resolve(result[0]); - }, - ); - }); -} diff --git a/packages/taler-wallet-webextension/src/background.ts b/packages/taler-wallet-webextension/src/background.ts index d6aeddc1d..9c572c176 100644 --- a/packages/taler-wallet-webextension/src/background.ts +++ b/packages/taler-wallet-webextension/src/background.ts @@ -23,14 +23,31 @@ /** * Imports. */ +import { platform, setupPlatform } from "./platform/api"; +import firefoxAPI from "./platform/firefox" +import chromeAPI from "./platform/chrome" import { wxMain } from "./wxBackend"; -const loadedFromWebpage = typeof window !== "undefined" +const isFirefox = typeof (window as any)['InstallTrigger'] !== 'undefined' -if (chrome.runtime.getManifest().manifest_version === 3) { - wxMain(); +//FIXME: create different entry point for any platform instead of +//switching in runtime +if (isFirefox) { + console.log("Wallet setup for Firefox API") + setupPlatform(firefoxAPI) } else { - window.addEventListener("load", () => { - wxMain(); - }); + console.log("Wallet setup for Chrome API") + setupPlatform(chromeAPI) +} + +try { + platform.registerOnInstalled(() => { + platform.openWalletPage("/welcome") + }) +} catch (e) { + console.error(e); } + +platform.notifyWhenAppIsReady(() => { + wxMain(); +}) diff --git a/packages/taler-wallet-webextension/src/chromeBadge.ts b/packages/taler-wallet-webextension/src/chromeBadge.ts index 60585793e..74c2fcd2f 100644 --- a/packages/taler-wallet-webextension/src/chromeBadge.ts +++ b/packages/taler-wallet-webextension/src/chromeBadge.ts @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see */ -import { isFirefox } from "./compat"; +import { platform } from "./platform/api"; /** * Polyfill for requestAnimationFrame, which @@ -210,7 +210,7 @@ export class ChromeBadge { if (this.animationRunning) { return; } - if (isFirefox()) { + if (platform.isFirefox()) { // Firefox does not support badge animations properly return; } diff --git a/packages/taler-wallet-webextension/src/compat.ts b/packages/taler-wallet-webextension/src/compat.ts deleted file mode 100644 index b17d0fb80..000000000 --- a/packages/taler-wallet-webextension/src/compat.ts +++ /dev/null @@ -1,101 +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 - */ - -/** - * Compatibility helpers needed for browsers that don't implement - * WebExtension APIs consistently. - */ - -// globalThis polyfill, see https://mathiasbynens.be/notes/globalthis -(function () { - if (typeof globalThis === "object") return; - Object.defineProperty(Object.prototype, "__magic__", { - get: function () { - return this; - }, - configurable: true, // This makes it possible to `delete` the getter later. - }); - // @ts-ignore: polyfill magic - __magic__.globalThis = __magic__; // lolwat - // @ts-ignore: polyfill magic - delete Object.prototype.__magic__; -})(); - -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: () => { - console.log("not supported for firefox") - // Not supported yet. - }, - contains: myBrowser.permissions.contains, - request: myBrowser.permissions.request, - remove: myBrowser.permissions.remove, - }; - } else { - return { - addPermissionsListener: chrome.permissions.onAdded.addListener.bind( - chrome.permissions.onAdded, - ), - contains: chrome.permissions.contains, - request: chrome.permissions.request, - remove: chrome.permissions.remove, - }; - } -} diff --git a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx index 0998cab7b..0cffff693 100644 --- a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx +++ b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx @@ -17,7 +17,6 @@ import { WalletDiagnostics } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { useTranslationContext } from "../context/translation"; -import { PageLink } from "../renderHtml"; interface Props { timedOut: boolean; @@ -70,10 +69,7 @@ export function Diagnostics({ timedOut, diagnostics }: Props): VNode {

Your wallet database is outdated. Currently automatic migration is - not supported. Please go{" "} - - here - {" "} + not supported. Please go here to reset the wallet database.

diff --git a/packages/taler-wallet-webextension/src/context/iocContext.ts b/packages/taler-wallet-webextension/src/context/iocContext.ts index 688e7b488..a24b0c1ca 100644 --- a/packages/taler-wallet-webextension/src/context/iocContext.ts +++ b/packages/taler-wallet-webextension/src/context/iocContext.ts @@ -21,7 +21,7 @@ import { createContext, h, VNode } from "preact"; import { useContext } from "preact/hooks"; -import { findTalerUriInActiveTab } from "../api/browser"; +import { platform } from "../platform/api"; interface Type { findTalerUriInActiveTab: () => Promise; @@ -45,5 +45,5 @@ export const IoCProviderForTesting = ({ value, children }: { value: Type, childr }; export const IoCProviderForRuntime = ({ children }: { children: any }): VNode => { - return h(Context.Provider, { value: { findTalerUriInActiveTab }, children }); + return h(Context.Provider, { value: { findTalerUriInActiveTab: platform.findTalerUriInActiveTab }, children }); }; diff --git a/packages/taler-wallet-webextension/src/cta/Refund.tsx b/packages/taler-wallet-webextension/src/cta/Refund.tsx index efc436bc8..790e8d9fa 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund.tsx @@ -20,11 +20,15 @@ * @author sebasjm */ -import { Amounts, ApplyRefundResponse } from "@gnu-taler/taler-util"; +import { + amountFractionalBase, + AmountJson, + Amounts, + ApplyRefundResponse, +} from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { useTranslationContext } from "../context/translation"; -import { AmountView } from "../renderHtml"; import * as wxApi from "../wxApi"; interface Props { @@ -120,3 +124,27 @@ export function RefundPage({ talerRefundUri }: Props): VNode { return ; } + +export function renderAmount(amount: AmountJson | string): VNode { + let a; + if (typeof amount === "string") { + a = Amounts.parse(amount); + } else { + a = amount; + } + if (!a) { + return (invalid amount); + } + const x = a.value + a.fraction / amountFractionalBase; + return ( + + {x} {a.currency} + + ); +} + +export const AmountView = ({ + amount, +}: { + amount: AmountJson | string; +}): VNode => renderAmount(amount); diff --git a/packages/taler-wallet-webextension/src/cta/Tip.tsx b/packages/taler-wallet-webextension/src/cta/Tip.tsx index 71aa04a2b..5767b5008 100644 --- a/packages/taler-wallet-webextension/src/cta/Tip.tsx +++ b/packages/taler-wallet-webextension/src/cta/Tip.tsx @@ -17,15 +17,19 @@ /** * Page shown to the user to accept or ignore a tip from a merchant. * - * @author sebasjm + * @author sebasjm */ -import { PrepareTipResult } from "@gnu-taler/taler-util"; +import { + amountFractionalBase, + AmountJson, + Amounts, + PrepareTipResult, +} from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { Loading } from "../components/Loading"; import { useTranslationContext } from "../context/translation"; -import { AmountView } from "../renderHtml"; import * as wxApi from "../wxApi"; interface Props { @@ -136,3 +140,24 @@ export function TipPage({ talerTipUri }: Props): VNode { /> ); } + +function renderAmount(amount: AmountJson | string): VNode { + let a; + if (typeof amount === "string") { + a = Amounts.parse(amount); + } else { + a = amount; + } + if (!a) { + return (invalid amount); + } + const x = a.value + a.fraction / amountFractionalBase; + return ( + + {x} {a.currency} + + ); +} + +const AmountView = ({ amount }: { amount: AmountJson | string }): VNode => + renderAmount(amount); diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts index 6bf6a7bdf..66d710705 100644 --- a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts +++ b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts @@ -16,7 +16,7 @@ import { useState, useEffect } from "preact/hooks"; import * as wxApi from "../wxApi"; -import { getPermissionsApi } from "../compat"; +import { platform } from "../platform/api"; import { getReadRequestPermissions } from "../permissions"; export function useExtendedPermissions(): [boolean, () => Promise] { @@ -40,24 +40,41 @@ async function handleExtendedPerm(isEnabled: boolean, onChange: (value: boolean) if (!isEnabled) { // We set permissions here, since apparently FF wants this to be done // as the result of an input event ... - return new Promise((res) => { - getPermissionsApi().request(getReadRequestPermissions(), async (granted: boolean) => { - console.log("permissions granted:", granted); - if (chrome.runtime.lastError) { - console.error("error requesting permissions"); - console.error(chrome.runtime.lastError); - onChange(false); - return; - } - try { - const res = await wxApi.setExtendedPermissions(granted); - onChange(res.newValue); - } finally { - res() - } + const granted = await platform.getPermissionsApi().request(getReadRequestPermissions()); + console.log("permissions granted:", granted); + const lastError = platform.getLastError(); + if (lastError) { + console.error("error requesting permissions"); + console.error(lastError); + onChange(false); + return; + } + // try { + const res = await wxApi.setExtendedPermissions(granted); + onChange(res.newValue); + // } finally { + // return + // } + + // return new Promise((res) => { + // platform.getPermissionsApi().request(getReadRequestPermissions(), async (granted: boolean) => { + // console.log("permissions granted:", granted); + // const lastError = getLastError() + // if (lastError) { + // console.error("error requesting permissions"); + // console.error(lastError); + // onChange(false); + // return; + // } + // try { + // const res = await wxApi.setExtendedPermissions(granted); + // onChange(res.newValue); + // } finally { + // res() + // } - }); - }) + // }); + // }) } await wxApi.setExtendedPermissions(false).then(r => onChange(r.newValue)); return diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts new file mode 100644 index 000000000..9b4e02ffb --- /dev/null +++ b/packages/taler-wallet-webextension/src/platform/api.ts @@ -0,0 +1,90 @@ +/* + 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 + */ + +import { CoreApiResponse, NotificationType, TalerUriType } from "@gnu-taler/taler-util"; + +export interface Permissions { + /** + * List of named permissions. + */ + permissions?: string[] | undefined; + /** + * List of origin permissions. Anything listed here must be a subset of a + * host that appears in the optional_permissions list in the manifest. + * + */ + origins?: string[] | undefined; + +} + +/** + * Compatibility API that works on multiple browsers. + */ +export interface CrossBrowserPermissionsApi { + contains(p: Permissions): Promise; + request(p: Permissions): Promise; + remove(p: Permissions): Promise; + + addPermissionsListener(callback: (p: Permissions) => void): void; + +} + +export type MessageFromBackend = { + type: NotificationType; +}; + +export interface WalletVersion { + version_name?: string | undefined; + version: string; +} + +/** + * Compatibility helpers needed for browsers that don't implement + * WebExtension APIs consistently. + */ +export interface PlatformAPI { + /** + * check if the platform is firefox + */ + isFirefox(): boolean; + /** + * + */ + getPermissionsApi(): CrossBrowserPermissionsApi; + notifyWhenAppIsReady(callback: () => void): void; + openWalletURIFromPopup(uriType: TalerUriType, talerUri: string): void; + openWalletPage(page: string): void; + openWalletPageFromPopup(page: string): void; + setMessageToWalletBackground(operation: string, payload: any): Promise; + listenToWalletNotifications(listener: (m: any) => void): () => void; + sendMessageToAllChannels(message: MessageFromBackend): void; + registerAllIncomingConnections(): void; + registerOnNewMessage(onNewMessage: (message: any, sender: any, callback: any) => void): void; + registerReloadOnNewVersion(): void; + redirectTabToWalletPage(tabId: number, page: string): void; + getWalletVersion(): WalletVersion; + registerTalerHeaderListener(onHeader: (tabId: number, url: string) => void): void; + registerOnInstalled(callback: () => void): void; + useServiceWorkerAsBackgroundProcess(): boolean; + getLastError(): string | undefined; + searchForTalerLinks(): string | undefined; + findTalerUriInActiveTab(): Promise; +} + +export let platform: PlatformAPI = undefined as any; +export function setupPlatform(impl: PlatformAPI) { + platform = impl; +} diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts new file mode 100644 index 000000000..dada23c57 --- /dev/null +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -0,0 +1,371 @@ +/* + 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 + */ + +import { TalerUriType } from "@gnu-taler/taler-util"; +import { getReadRequestPermissions } from "../permissions"; +import { CrossBrowserPermissionsApi, MessageFromBackend, Permissions, PlatformAPI } from "./api.js"; + +const api: PlatformAPI = { + isFirefox, + findTalerUriInActiveTab, + getLastError, + getPermissionsApi, + getWalletVersion, + listenToWalletNotifications, + notifyWhenAppIsReady, + openWalletPage, + openWalletPageFromPopup, + openWalletURIFromPopup, + redirectTabToWalletPage, + registerAllIncomingConnections, + registerOnInstalled, + registerOnNewMessage, + registerReloadOnNewVersion, + registerTalerHeaderListener, + searchForTalerLinks, + sendMessageToAllChannels, + setMessageToWalletBackground, + useServiceWorkerAsBackgroundProcess +} + +export default api; + +function isFirefox(): boolean { + return false; +} + +export function contains(p: Permissions): Promise { + return new Promise((res, rej) => { + chrome.permissions.contains(p, (resp) => { + const le = getLastError() + if (le) { + rej(le) + } + res(resp) + }) + }) +} + +export async function request(p: Permissions): Promise { + return new Promise((res, rej) => { + chrome.permissions.request(p, (resp) => { + const le = getLastError() + if (le) { + rej(le) + } + res(resp) + }) + }) +} + +export async function remove(p: Permissions): Promise { + return new Promise((res, rej) => { + chrome.permissions.remove(p, (resp) => { + const le = getLastError() + if (le) { + rej(le) + } + res(resp) + }) + }) +} + +function addPermissionsListener(callback: (p: Permissions) => void): void { + console.log("addPermissionListener is not supported for Firefox"); + chrome.permissions.onAdded.addListener(callback) +} + +function getPermissionsApi(): CrossBrowserPermissionsApi { + return { + addPermissionsListener, contains, request, remove + } +} + +/** + * + * @param callback function to be called + */ +function notifyWhenAppIsReady(callback: () => void) { + if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) { + callback() + } else { + window.addEventListener("load", callback); + } +} + + +function openWalletURIFromPopup(uriType: TalerUriType, talerUri: string) { + let url: string | undefined = undefined; + switch (uriType) { + case TalerUriType.TalerWithdraw: + url = chrome.runtime.getURL(`static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`); + break; + case TalerUriType.TalerPay: + url = chrome.runtime.getURL(`static/wallet.html#/cta/pay?talerPayUri=${talerUri}`); + break; + case TalerUriType.TalerTip: + url = chrome.runtime.getURL(`static/wallet.html#/cta/tip?talerTipUri=${talerUri}`); + break; + case TalerUriType.TalerRefund: + url = chrome.runtime.getURL(`static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`); + break; + default: + console.warn( + "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", + ); + return; + } + + chrome.tabs.create( + { active: true, url, }, + () => { window.close(); }, + ); +} + +function openWalletPage(page: string) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) + chrome.tabs.create( + { active: true, url, }, + ); +} + +function openWalletPageFromPopup(page: string) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) + chrome.tabs.create( + { active: true, url, }, + () => { window.close(); }, + ); +} + +async function setMessageToWalletBackground(operation: string, payload: any): Promise { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError.message) + } + resolve(resp) + // return true to keep the channel open + return true; + }) + }) +} + +let notificationPort: chrome.runtime.Port | undefined; +function listenToWalletNotifications(listener: (m: any) => void) { + if (notificationPort === undefined) { + notificationPort = chrome.runtime.connect({ name: "notifications" }) + } + notificationPort.onMessage.addListener(listener) + function removeListener() { + if (notificationPort !== undefined) { + notificationPort.onMessage.removeListener(listener) + } + } + return removeListener +} + + +const allPorts: chrome.runtime.Port[] = []; + +function sendMessageToAllChannels(message: MessageFromBackend) { + for (const notif of allPorts) { + // const message: MessageFromBackend = { type: msg.type }; + try { + notif.postMessage(message); + } catch (e) { + console.error(e); + } + } +} + +function registerAllIncomingConnections() { + chrome.runtime.onConnect.addListener((port) => { + allPorts.push(port); + port.onDisconnect.addListener((discoPort) => { + const idx = allPorts.indexOf(discoPort); + if (idx >= 0) { + allPorts.splice(idx, 1); + } + }); + }); +} + +function registerOnNewMessage(cb: (message: any, sender: any, callback: any) => void) { + chrome.runtime.onMessage.addListener((m, s, c) => { + cb(m, s, c) + + // keep the connection open + return true; + }); +} + +function registerReloadOnNewVersion() { + // 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(); + }); + +} + +function redirectTabToWalletPage( + tabId: number, + page: string, +) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); + console.log("redirecting tabId: ", tabId, " to: ", url); + chrome.tabs.update(tabId, { url }); +} + +interface WalletVersion { + version_name?: string | undefined; + version: string; +} + +function getWalletVersion(): WalletVersion { + const manifestData = chrome.runtime.getManifest(); + return manifestData; +} + + +function registerTalerHeaderListener(callback: (tabId: number, url: string) => void): void { + console.log("setting up header listener"); + + function headerListener( + details: chrome.webRequest.WebResponseHeadersDetails, + ) { + if (chrome.runtime.lastError) { + console.error(JSON.stringify(chrome.runtime.lastError)); + return; + } + if ( + details.statusCode === 402 || + details.statusCode === 202 || + details.statusCode === 200 + ) { + const values = (details.responseHeaders || []) + .filter(h => h.name.toLowerCase() === 'taler') + .map(h => h.value) + .filter((value): value is string => !!value) + if (values.length > 0) { + callback(details.tabId, values[0]) + } + } + return; + } + + getPermissionsApi().contains(getReadRequestPermissions()).then(result => { + //if there is a handler already, remove it + if ( + "webRequest" in chrome && + "onHeadersReceived" in chrome.webRequest && + chrome.webRequest.onHeadersReceived.hasListener(headerListener) + ) { + chrome.webRequest.onHeadersReceived.removeListener(headerListener); + } + //if the result was positive, add the headerListener + if (result) { + chrome.webRequest.onHeadersReceived.addListener( + headerListener, + { urls: [""] }, + ["responseHeaders"], + ); + } + //notify the browser about this change, this operation is expensive + if ("webRequest" in chrome) { + chrome.webRequest.handlerBehaviorChanged(() => { + if (chrome.runtime.lastError) { + console.error(JSON.stringify(chrome.runtime.lastError)); + } + }); + } + }); +} + +function registerOnInstalled(callback: () => void) { + // 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 === chrome.runtime.OnInstalledReason.INSTALL) { + callback() + } + }); +} + +function useServiceWorkerAsBackgroundProcess() { + return chrome.runtime.getManifest().manifest_version === 3 +} + +function getLastError() { + return chrome.runtime.lastError?.message; +} + + +function searchForTalerLinks(): string | undefined { + let found; + found = document.querySelector("a[href^='taler://'") + if (found) return found.toString() + found = document.querySelector("a[href^='taler+http://'") + if (found) return found.toString() + return undefined +} + +async function getCurrentTab() { + let queryOptions = { active: true, currentWindow: true }; + let [tab] = await chrome.tabs.query(queryOptions); + return tab; +} + + +async function findTalerUriInActiveTab(): Promise { + if (chrome.runtime.getManifest().manifest_version === 3) { + // manifest v3 + const tab = await getCurrentTab(); + const res = await chrome.scripting.executeScript({ + target: { + tabId: tab.id!, + allFrames: true, + } as any, + func: searchForTalerLinks, + args: [] + }) + return res[0].result + } + return new Promise((resolve, reject) => { + //manifest v2 + chrome.tabs.executeScript( + { + code: ` + (() => { + let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'"); + return x ? x.href.toString() : null; + })(); + `, + allFrames: false, + }, + (result) => { + if (chrome.runtime.lastError) { + console.error(JSON.stringify(chrome.runtime.lastError)); + resolve(undefined); + return; + } + resolve(result[0]); + }, + ); + }); +} diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts new file mode 100644 index 000000000..dad90626b --- /dev/null +++ b/packages/taler-wallet-webextension/src/platform/firefox.ts @@ -0,0 +1,74 @@ +/* + 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 + */ + +import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js"; +import chromePlatform, { contains as chromeContains, remove as chromeRemove, request as chromeRequest } from "./chrome"; + +const api: PlatformAPI = { + ...chromePlatform, + isFirefox, + getPermissionsApi, + notifyWhenAppIsReady, + redirectTabToWalletPage, + useServiceWorkerAsBackgroundProcess +}; + +export default api; + +function isFirefox(): boolean { + return true +} + + +function addPermissionsListener(callback: (p: Permissions) => void): void { + console.log("addPermissionListener is not supported for Firefox") +} + +function getPermissionsApi(): CrossBrowserPermissionsApi { + return { + addPermissionsListener, + contains: chromeContains, + request: chromeRequest, + remove: chromeRemove + } +} + +/** + * + * @param callback function to be called + */ +function notifyWhenAppIsReady(callback: () => void) { + if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) { + callback() + } else { + window.addEventListener("load", callback); + } +} + + +function redirectTabToWalletPage( + tabId: number, + page: string, +) { + const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); + console.log("redirecting tabId: ", tabId, " to: ", url); + chrome.tabs.update(tabId, { url, loadReplace: true } as any); +} + + +function useServiceWorkerAsBackgroundProcess() { + return false +} diff --git a/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx index 3deea032d..d47b8ce7b 100644 --- a/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/DeveloperPage.tsx @@ -375,16 +375,6 @@ function toBase64(str: string): string { ); } -export function reload(): void { - try { - // eslint-disable-next-line no-undef - chrome.runtime.reload(); - window.close(); - } catch (e) { - // Functionality missing in firefox, ignore! - } -} - function runIntegrationTest() {} export async function confirmReset( @@ -395,13 +385,3 @@ export async function confirmReset( window.close(); } } - -export function openExtensionPage(page: string) { - return () => { - // eslint-disable-next-line no-undef - chrome.tabs.create({ - // eslint-disable-next-line no-undef - url: chrome.runtime.getURL(page), - }); - }; -} diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx index 9ac83a578..a1082ad92 100644 --- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx +++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx @@ -21,36 +21,21 @@ import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util"; import { Fragment, h } from "preact"; +import { platform } from "../platform/api"; import { ButtonPrimary, ButtonSuccess } from "../components/styled"; import { useTranslationContext } from "../context/translation"; -import { actionForTalerUri } from "../utils/index"; export interface Props { url: string; onDismiss: () => void; } -async function getCurrentTab(): Promise { - let queryOptions = { active: true, currentWindow: true }; - const tab = await new Promise((res, rej) => { - chrome.tabs.query(queryOptions, (tabs) => { - res(tabs[0]); - }); - }); - return tab; -} - -async function navigateTo(url?: string) { - if (!url) return; - const tab = await getCurrentTab(); - if (!tab.id) return; - await chrome.tabs.update(tab.id, { url }); - window.close(); -} - export function TalerActionFound({ url, onDismiss }: Props) { const uriType = classifyTalerUri(url); const { i18n } = useTranslationContext(); + function redirectToWallet() { + platform.openWalletURIFromPopup(uriType, url); + } return (
@@ -62,11 +47,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {

This page has pay action.

- { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + Open pay page @@ -78,11 +59,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { This page has a withdrawal action.

- { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + Open withdraw page @@ -92,11 +69,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {

This page has a tip action.

- { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + Open tip page @@ -108,11 +81,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { This page has a notify reserve action.

- { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + Notify @@ -122,11 +91,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {

This page has a refund action.

- { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + Open refund page diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index 7ee6c8e4b..dfb12666b 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -17,7 +17,7 @@ /** * Main entry point for extension pages. * - * @author sebasjm + * @author sebasjm */ import { setupI18n } from "@gnu-taler/taler-util"; @@ -37,6 +37,9 @@ import { import { useTalerActionURL } from "./hooks/useTalerActionURL"; import { strings } from "./i18n/strings"; import { Pages, PopupNavBar } from "./NavigationBar"; +import { platform, setupPlatform } from "./platform/api"; +import chromeAPI from "./platform/chrome"; +import firefoxAPI from "./platform/firefox"; import { BalancePage } from "./popup/BalancePage"; import { TalerActionFound } from "./popup/TalerActionFound"; import { BackupPage } from "./wallet/BackupPage"; @@ -59,6 +62,17 @@ function main(): void { setupI18n("en", strings); +//FIXME: create different entry point for any platform instead of +//switching in runtime +const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined"; +if (isFirefox) { + console.log("Wallet setup for Firefox API"); + setupPlatform(firefoxAPI); +} else { + console.log("Wallet setup for Chrome API"); + setupPlatform(chromeAPI); +} + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", main); } else { @@ -106,7 +120,7 @@ function Application(): VNode { route(Pages.balance_deposit.replace(":currency", currency)) } goToWalletHistory={(currency: string) => - route(Pages.balance_history.replace(":currency", currency)) + route(Pages.balance_history.replace(":currency?", currency)) } /> @@ -180,19 +194,10 @@ function Application(): VNode { } function RedirectToWalletPage(): VNode { - const page = document.location.hash || "#/"; + const page = (document.location.hash || "#/").replace("#", ""); const [showText, setShowText] = useState(false); useEffect(() => { - chrome.tabs.create( - { - active: true, - // eslint-disable-next-line no-undef - url: chrome.runtime.getURL(`/static/wallet.html${page}`), - }, - () => { - window.close(); - }, - ); + platform.openWalletPageFromPopup(page); setTimeout(() => { setShowText(true); }, 250); diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx deleted file mode 100644 index 1e482ccea..000000000 --- a/packages/taler-wallet-webextension/src/renderHtml.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - This file is part of TALER - (C) 2016 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - -/** - * Helpers functions to render Taler-related data structures to HTML. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - AmountJson, - Amounts, - amountFractionalBase, -} from "@gnu-taler/taler-util"; -import { Component, ComponentChildren, h, VNode } from "preact"; - -/** - * Render amount as HTML, which non-breaking space between - * decimal value and currency. - */ -export function renderAmount(amount: AmountJson | string): VNode { - let a; - if (typeof amount === "string") { - a = Amounts.parse(amount); - } else { - a = amount; - } - if (!a) { - return (invalid amount); - } - const x = a.value + a.fraction / amountFractionalBase; - return ( - - {x} {a.currency} - - ); -} - -export const AmountView = ({ - amount, -}: { - amount: AmountJson | string; -}): VNode => 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): VNode { - let sAbbrev = s; - if (s.length > n) { - sAbbrev = s.slice(0, n) + ".."; - } - return ( - - {sAbbrev} - - ); -} - -interface CollapsibleState { - collapsed: boolean; -} - -interface CollapsibleProps { - initiallyCollapsed: boolean; - title: string; -} - -/** - * Component that shows/hides its children when clicking - * a heading. - */ -export class Collapsible extends Component { - constructor(props: CollapsibleProps) { - super(props); - this.state = { collapsed: props.initiallyCollapsed }; - } - render(): VNode { - 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 ( -

- - {" "} - {this.props.title} - -

- ); - } - return ( -
-

- - {" "} - {this.props.title} - -

- {this.props.children} -
- ); - } -} - -interface ExpanderTextProps { - text: string; -} - -/** - * Show a heading with a toggle to show/hide the expandable content. - */ -export function ExpanderText({ text }: ExpanderTextProps): VNode { - return {text}; -} - -export interface LoadingButtonProps - extends h.JSX.HTMLAttributes { - isLoading: boolean; -} - -export function ProgressButton({ - isLoading, - ...rest -}: LoadingButtonProps): VNode { - return ( -