diff options
author | Sebastian <sebasjm@gmail.com> | 2022-03-23 10:50:12 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-03-23 10:58:57 -0300 |
commit | 32f6409ac312f31821f791c3a376168289f0e4f4 (patch) | |
tree | c77c660bb85cf359faf74b5cddbe95eb0a915c5e | |
parent | c539d1803c1376cba0831be64866b6d2c1652403 (diff) |
all the browser related code move into one place, making it easy for specific platform code or mocking for testing
21 files changed, 779 insertions, 790 deletions
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<string | undefined> { - 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 <http://www.gnu.org/licenses/> */ -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 <http://www.gnu.org/licenses/> - */ - -/** - * 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 { <p> <i18n.Translate> Your wallet database is outdated. Currently automatic migration is - not supported. Please go{" "} - <PageLink pageName="/reset-required"> - <i18n.Translate>here</i18n.Translate> - </PageLink>{" "} + not supported. Please go <i18n.Translate>here</i18n.Translate> to reset the wallet database. </i18n.Translate> </p> 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<string | undefined>; @@ -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 <View applyResult={applyResult} />; } + +export function renderAmount(amount: AmountJson | string): VNode { + 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 / amountFractionalBase; + return ( + <span> + {x} {a.currency} + </span> + ); +} + +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 <dold@taler.net> + * @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 <span>(invalid amount)</span>; + } + const x = a.value + a.fraction / amountFractionalBase; + return ( + <span> + {x} {a.currency} + </span> + ); +} + +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<void>] { @@ -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<void>((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<void>((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 <http://www.gnu.org/licenses/> + */ + +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<boolean>; + request(p: Permissions): Promise<boolean>; + remove(p: Permissions): Promise<boolean>; + + 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<CoreApiResponse>; + 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<string | undefined>; +} + +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 <http://www.gnu.org/licenses/> + */ + +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<boolean> { + 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<boolean> { + 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<boolean> { + 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<any> { + return new Promise<any>((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: ["<all_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<string | undefined> { + 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 <http://www.gnu.org/licenses/> + */ + +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<chrome.tabs.Tab> { - let queryOptions = { active: true, currentWindow: true }; - const tab = await new Promise<chrome.tabs.Tab>((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 ( <Fragment> <section> @@ -62,11 +47,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { <p> <i18n.Translate>This page has pay action.</i18n.Translate> </p> - <ButtonSuccess - onClick={() => { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + <ButtonSuccess onClick={redirectToWallet}> <i18n.Translate>Open pay page</i18n.Translate> </ButtonSuccess> </div> @@ -78,11 +59,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { This page has a withdrawal action. </i18n.Translate> </p> - <ButtonSuccess - onClick={() => { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + <ButtonSuccess onClick={redirectToWallet}> <i18n.Translate>Open withdraw page</i18n.Translate> </ButtonSuccess> </div> @@ -92,11 +69,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { <p> <i18n.Translate>This page has a tip action.</i18n.Translate> </p> - <ButtonSuccess - onClick={() => { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + <ButtonSuccess onClick={redirectToWallet}> <i18n.Translate>Open tip page</i18n.Translate> </ButtonSuccess> </div> @@ -108,11 +81,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { This page has a notify reserve action. </i18n.Translate> </p> - <ButtonSuccess - onClick={() => { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + <ButtonSuccess onClick={redirectToWallet}> <i18n.Translate>Notify</i18n.Translate> </ButtonSuccess> </div> @@ -122,11 +91,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { <p> <i18n.Translate>This page has a refund action.</i18n.Translate> </p> - <ButtonSuccess - onClick={() => { - navigateTo(actionForTalerUri(uriType, url)); - }} - > + <ButtonSuccess onClick={redirectToWallet}> <i18n.Translate>Open refund page</i18n.Translate> </ButtonSuccess> </div> 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 <dold@taler.net> + * @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 <http://www.gnu.org/licenses/> - */ - -/** - * 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 <span>(invalid amount)</span>; - } - const x = a.value + a.fraction / amountFractionalBase; - return ( - <span> - {x} {a.currency} - </span> - ); -} - -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 ( - <span class="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 Component<CollapsibleProps, CollapsibleState> { - 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 ( - <h2> - <a class="opener opener-collapsed" href="#" onClick={doOpen}> - {" "} - {this.props.title} - </a> - </h2> - ); - } - return ( - <div> - <h2> - <a class="opener opener-open" href="#" onClick={doClose}> - {" "} - {this.props.title} - </a> - </h2> - {this.props.children} - </div> - ); - } -} - -interface ExpanderTextProps { - text: string; -} - -/** - * Show a heading with a toggle to show/hide the expandable content. - */ -export function ExpanderText({ text }: ExpanderTextProps): VNode { - return <span>{text}</span>; -} - -export interface LoadingButtonProps - extends h.JSX.HTMLAttributes<HTMLButtonElement> { - isLoading: boolean; -} - -export function ProgressButton({ - isLoading, - ...rest -}: LoadingButtonProps): VNode { - return ( - <button class="pure-button pure-button-primary" type="button" {...rest}> - {isLoading ? ( - <span> - <object class="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> - </span> - ) : null}{" "} - {rest.children} - </button> - ); -} - -export function PageLink(props: { - pageName: string; - children?: ComponentChildren; -}): VNode { - // eslint-disable-next-line no-undef - - const url = - typeof chrome === "undefined" - ? undefined - : // eslint-disable-next-line no-undef - chrome.runtime?.getURL(`/static/wallet.html#/${props.pageName}`); - return ( - <a class="actionLink" href={url} target="_blank" rel="noopener noreferrer"> - {props.children} - </a> - ); -} diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts index cef0595d3..e5447f9cb 100644 --- a/packages/taler-wallet-webextension/src/utils/index.ts +++ b/packages/taler-wallet-webextension/src/utils/index.ts @@ -187,52 +187,3 @@ export function amountToString(text: AmountJson): string { return `${amount} ${aj.currency}`; } -export function actionForTalerUri( - uriType: TalerUriType, - talerUri: string, -): string | undefined { - switch (uriType) { - case TalerUriType.TalerWithdraw: - return makeExtensionUrlWithParams("static/wallet.html#/cta/withdraw", { - talerWithdrawUri: talerUri, - }); - case TalerUriType.TalerPay: - return makeExtensionUrlWithParams("static/wallet.html#/cta/pay", { - talerPayUri: talerUri, - }); - case TalerUriType.TalerTip: - return makeExtensionUrlWithParams("static/wallet.html#/cta/tip", { - talerTipUri: talerUri, - }); - case TalerUriType.TalerRefund: - return makeExtensionUrlWithParams("static/wallet.html#/cta/refund", { - 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; -} - -function makeExtensionUrlWithParams( - url: string, - params?: { [name: string]: string | undefined }, -): string { - // eslint-disable-next-line no-undef - const innerUrl = new URL(chrome.runtime.getURL("/" + url)); - if (params) { - const hParams = Object.keys(params) - .map((k) => `${k}=${params[k]}`) - .join("&"); - innerUrl.hash = innerUrl.hash + "?" + hParams; - } - return innerUrl.href; -} - - diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx index 3516bfbf1..bebf036c9 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx @@ -1,9 +1,9 @@ import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import { platform } from "../platform/api"; import { Button, ButtonSuccess, InputWithLabel } from "../components/styled"; import { useTranslationContext } from "../context/translation"; -import { actionForTalerUri } from "../utils/index"; export interface Props { onCancel: () => void; @@ -14,6 +14,10 @@ export function AddNewActionView({ onCancel }: Props): VNode { const uriType = classifyTalerUri(url); const { i18n } = useTranslationContext(); + function redirectToWallet() { + platform.openWalletURIFromPopup(uriType, url); + } + return ( <Fragment> <section> @@ -37,12 +41,7 @@ export function AddNewActionView({ onCancel }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </Button> {uriType !== TalerUriType.Unknown && ( - <ButtonSuccess - onClick={() => { - // eslint-disable-next-line no-undef - chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); - }} - > + <ButtonSuccess onClick={redirectToWallet}> {(() => { switch (uriType) { case TalerUriType.TalerNotifyReserve: diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index 2f53917e4..e7cb1f5e6 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -17,7 +17,7 @@ /** * Main entry point for extension pages. * - * @author sebasjm <dold@taler.net> + * @author sebasjm */ import { setupI18n } from "@gnu-taler/taler-util"; @@ -28,12 +28,7 @@ import Match from "preact-router/match"; import { useEffect, useState } from "preact/hooks"; import { LogoHeader } from "./components/LogoHeader"; import PendingTransactions from "./components/PendingTransactions"; -import { - NavigationHeader, - NavigationHeaderHolder, - SuccessBox, - WalletBox, -} from "./components/styled"; +import { SuccessBox, WalletBox } from "./components/styled"; import { DevContextProvider } from "./context/devContext"; import { IoCProviderForRuntime } from "./context/iocContext"; import { @@ -46,6 +41,9 @@ import { TipPage } from "./cta/Tip"; import { WithdrawPage } from "./cta/Withdraw"; import { strings } from "./i18n/strings"; import { Pages, WalletNavBar } from "./NavigationBar"; +import { setupPlatform } from "./platform/api"; +import chromeAPI from "./platform/chrome"; +import firefoxAPI from "./platform/firefox"; import { DeveloperPage } from "./popup/DeveloperPage"; import { BackupPage } from "./wallet/BackupPage"; import { DepositPage } from "./wallet/DepositPage"; @@ -75,6 +73,17 @@ function main(): void { setupI18n("en", strings); +const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined"; +//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 { + console.log("Wallet setup for Chrome API"); + setupPlatform(chromeAPI); +} + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", main); } else { diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 2071f85e5..ee2a81062 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -61,7 +61,7 @@ import { } from "@gnu-taler/taler-wallet-core"; import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw"; -import { MessageFromBackend } from "./wxBackend"; +import { platform, MessageFromBackend } from "./platform/api"; /** * @@ -95,27 +95,18 @@ export interface UpgradeResponse { } async function callBackend(operation: string, payload: any): Promise<any> { - return new Promise<any>((resolve, reject) => { - // eslint-disable-next-line no-undef - chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { - // eslint-disable-next-line no-undef - if (chrome.runtime.lastError) { - console.log("Error calling backend"); - reject( - new Error( - `Error contacting backend: ${chrome.runtime.lastError.message}`, - ), - ); - } - console.log("got response", resp); - const r = resp as CoreApiResponse; - if (r.type === "error") { - reject(TalerError.fromUncheckedDetail(r.error)); - return; - } - resolve(r.result); - }); - }); + let response: CoreApiResponse; + try { + response = await platform.setMessageToWalletBackground(operation, payload) + } catch (e) { + console.log("Error calling backend"); + throw new Error(`Error contacting backend: ${e}`) + } + console.log("got response", response); + if (response.type === "error") { + throw new TalerError.fromUncheckedDetail(response.error); + } + return response.result; } /** @@ -422,20 +413,12 @@ export function importDB(dump: any): Promise<void> { return callBackend("importDb", { dump }); } -export function onUpdateNotification( - messageTypes: Array<NotificationType>, - doCallback: () => void, -): () => void { - // eslint-disable-next-line no-undef - const port = chrome.runtime.connect({ name: "notifications" }); +export function onUpdateNotification(messageTypes: Array<NotificationType>, doCallback: () => void): () => void { const listener = (message: MessageFromBackend): void => { const shouldNotify = messageTypes.includes(message.type); if (shouldNotify) { doCallback(); } }; - port.onMessage.addListener(listener); - return () => { - port.onMessage.removeListener(listener); - }; + return platform.listenToWalletNotifications(listener) } diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index b7a0cdc54..048c8fc79 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -26,11 +26,9 @@ import { classifyTalerUri, CoreApiResponse, - CoreApiResponseSuccess, - NotificationType, - TalerErrorCode, + CoreApiResponseSuccess, TalerErrorCode, TalerUriType, - WalletDiagnostics, + WalletDiagnostics } from "@gnu-taler/taler-util"; import { DbAccess, @@ -40,13 +38,13 @@ import { openPromise, openTalerDatabase, Wallet, - WalletStoresV1, + WalletStoresV1 } from "@gnu-taler/taler-wallet-core"; import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory"; import { BrowserHttpLib } from "./browserHttpLib"; -import { getPermissionsApi, isFirefox } from "./compat"; import { getReadRequestPermissions } from "./permissions"; -import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js"; +import { MessageFromBackend, platform } from "./platform/api"; +import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory"; import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib"; /** @@ -66,10 +64,8 @@ let outdatedDbVersion: number | undefined; const walletInit: OpenedPromise<void> = openPromise<void>(); -const notificationPorts: chrome.runtime.Port[] = []; - async function getDiagnostics(): Promise<WalletDiagnostics> { - const manifestData = chrome.runtime.getManifest(); + const manifestData = platform.getWalletVersion(); const errors: string[] = []; let firefoxIdbProblem = false; let dbOutdated = false; @@ -80,7 +76,7 @@ async function getDiagnostics(): Promise<WalletDiagnostics> { if ( currentDatabase === undefined && outdatedDbVersion === undefined && - isFirefox() + platform.isFirefox() ) { firefoxIdbProblem = true; } @@ -132,14 +128,7 @@ async function dispatch( break; } case "wxGetExtendedPermissions": { - const res = await new Promise((resolve, reject) => { - getPermissionsApi().contains( - getReadRequestPermissions(), - (result: boolean) => { - resolve(result); - }, - ); - }); + const res = await platform.getPermissionsApi().contains(getReadRequestPermissions()); r = wrapResponse({ newValue: res }); break; } @@ -147,15 +136,11 @@ async function dispatch( const newVal = req.payload.value; console.log("new extended permissions value", newVal); if (newVal) { - setupHeaderListener(); + platform.registerTalerHeaderListener(parseTalerUriAndRedirect); r = wrapResponse({ newValue: true }); } else { - await new Promise<void>((resolve, reject) => { - getPermissionsApi().remove(getReadRequestPermissions(), (rem) => { - console.log("permissions removed:", rem); - resolve(); - }); - }); + const rem = await platform.getPermissionsApi().remove(getReadRequestPermissions()); + console.log("permissions removed:", rem); r = wrapResponse({ newVal: false }); } break; @@ -187,74 +172,13 @@ async function dispatch( } } -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.action.BadgeTextDetails): void { - // not supported by all browsers ... - if (chrome && chrome.action && chrome.action.setBadgeText) { - chrome.action.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.runtime.getURL(url)); - if (params) { - const hParams = Object.keys(params) - .map((k) => `${k}=${params[k]}`) - .join("&"); - innerUrl.hash = innerUrl.hash + "?" + hParams; - } - // 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) { - console.log("redirecting to", innerUrl.href); - chrome.tabs.update(tabId, { - url: innerUrl.href, - loadReplace: true, - } as any); - } - }; - doit(); - - return { redirectUrl: innerUrl.href }; -} - -export type MessageFromBackend = { - type: NotificationType; -}; - async function reinitWallet(): Promise<void> { if (currentWallet) { currentWallet.stop(); currentWallet = undefined; } currentDatabase = undefined; - setBadgeText({ text: "" }); + // setBadgeText({ text: "" }); try { currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet); } catch (e) { @@ -265,7 +189,7 @@ async function reinitWallet(): Promise<void> { let httpLib; let cryptoWorker; - if (chrome.runtime.getManifest().manifest_version === 3) { + if (platform.useServiceWorkerAsBackgroundProcess()) { httpLib = new ServiceWorkerHttpLib(); cryptoWorker = new SynchronousCryptoWorkerFactory(); } else { @@ -283,14 +207,8 @@ async function reinitWallet(): Promise<void> { return; } wallet.addNotificationListener((x) => { - for (const notif of notificationPorts) { - const message: MessageFromBackend = { type: x.type }; - try { - notif.postMessage(message); - } catch (e) { - console.error(e); - } - } + const message: MessageFromBackend = { type: x.type }; + platform.sendMessageToAllChannels(message) }); wallet.runTaskLoop().catch((e) => { console.log("error during wallet task loop", e); @@ -303,135 +221,41 @@ async function reinitWallet(): Promise<void> { 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.runtime.getURL("/static/wallet.html#/welcome"); - chrome.tabs.create({ active: true, url }); - } - }); -} catch (e) { - console.error(e); -} - -function headerListener( - details: chrome.webRequest.WebResponseHeadersDetails, -): chrome.webRequest.BlockingResponse | undefined { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - return; - } - const wallet = currentWallet; - if (!wallet) { - console.warn("wallet not available while handling header"); - return; - } - if ( - details.statusCode === 402 || - details.statusCode === 202 || - details.statusCode === 200 - ) { - 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( - "/static/wallet.html#/cta/withdraw", - details.tabId, - details.url, - { - talerWithdrawUri: talerUri, - }, - ); - case TalerUriType.TalerPay: - return makeSyncWalletRedirect( - "/static/wallet.html#/cta/pay", - details.tabId, - details.url, - { - talerPayUri: talerUri, - }, - ); - case TalerUriType.TalerTip: - return makeSyncWalletRedirect( - "/static/wallet.html#/cta/tip", - details.tabId, - details.url, - { - talerTipUri: talerUri, - }, - ); - case TalerUriType.TalerRefund: - return makeSyncWalletRedirect( - "/static/wallet.html#/cta/refund", - details.tabId, - details.url, - { - talerRefundUri: talerUri, - }, - ); - case TalerUriType.TalerNotifyReserve: - Promise.resolve().then(() => { - const w = currentWallet; - if (!w) { - return; - } - // FIXME: Is this still useful? - // handleNotifyReserve(w); - }); - break; - default: - console.warn( - "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", - ); - break; - } - } - } +function parseTalerUriAndRedirect(tabId: number, talerUri: string) { + const uriType = classifyTalerUri(talerUri); + switch (uriType) { + case TalerUriType.TalerWithdraw: + return platform.redirectTabToWalletPage( + tabId, + `/cta/withdraw?talerWithdrawUri=${talerUri}`, + ); + case TalerUriType.TalerPay: + return platform.redirectTabToWalletPage( + tabId, + `/cta/pay?talerPayUri=${talerUri}`, + ); + case TalerUriType.TalerTip: + return platform.redirectTabToWalletPage( + tabId, + `/cta/tip?talerTipUri=${talerUri}`, + ); + case TalerUriType.TalerRefund: + return platform.redirectTabToWalletPage( + tabId, + `/cta/refund?talerRefundUri=${talerUri}`, + ); + case TalerUriType.TalerNotifyReserve: + // FIXME: Is this still useful? + // handleNotifyReserve(w); + break; + default: + console.warn( + "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", + ); + break; } - return; } -function setupHeaderListener(): void { - // if (chrome.runtime.getManifest().manifest_version === 3) { - // console.error("cannot block request on manfest v3") - // return - // } - console.log("setting up header listener"); - // Handlers for catching HTTP requests - getPermissionsApi().contains( - getReadRequestPermissions(), - (result: boolean) => { - if ( - "webRequest" in chrome && - "onHeadersReceived" in chrome.webRequest && - 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"], - ); - } - if ("webRequest" in chrome) { - chrome.webRequest.handlerBehaviorChanged(() => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - } - }); - } - }, - ); -} /** * Main function to run for the WebExtension backend. @@ -439,48 +263,34 @@ function setupHeaderListener(): void { * 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(); - }); const afterWalletIsInitialized = reinitWallet(); + platform.registerReloadOnNewVersion(); + // Handlers for messages coming directly from the content // script on the page - chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { + platform.registerOnNewMessage((message, sender, callback) => { afterWalletIsInitialized.then(() => { - dispatch(req, sender, sendResponse); + dispatch(message, sender, callback); }); - 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); - } - }); - }); + platform.registerAllIncomingConnections() try { - if (chrome.runtime.getManifest().manifest_version === 2) { - setupHeaderListener(); - } + platform.registerTalerHeaderListener(parseTalerUriAndRedirect); } 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); + platform.getPermissionsApi().addPermissionsListener((perm) => { + const lastError = platform.getLastError() + if (lastError) { + console.error(lastError); return; } - setupHeaderListener(); + platform.registerTalerHeaderListener(parseTalerUriAndRedirect); }); } |