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 /packages | |
parent | c539d1803c1376cba0831be64866b6d2c1652403 (diff) |
all the browser related code move into one place, making it easy for specific platform code or mocking for testing
Diffstat (limited to 'packages')
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); }); } |