all the browser related code move into one place, making it easy for specific platform code or mocking for testing

This commit is contained in:
Sebastian 2022-03-23 10:50:12 -03:00
parent c539d1803c
commit 32f6409ac3
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
21 changed files with 780 additions and 791 deletions

View File

@ -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]);
},
);
});
}

View File

@ -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();
})

View File

@ -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;
}

View File

@ -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,
};
}
}

View File

@ -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>

View File

@ -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 });
};

View File

@ -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}&nbsp;{a.currency}
</span>
);
}
export const AmountView = ({
amount,
}: {
amount: AmountJson | string;
}): VNode => renderAmount(amount);

View File

@ -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}&nbsp;{a.currency}
</span>
);
}
const AmountView = ({ amount }: { amount: AmountJson | string }): VNode =>
renderAmount(amount);

View File

@ -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

View File

@ -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;
}

View File

@ -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]);
},
);
});
}

View File

@ -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
}

View File

@ -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),
});
};
}

View File

@ -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>

View File

@ -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);

View File

@ -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}&nbsp;{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>
);
}

View File

@ -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;
}

View File

@ -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:

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 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;
}
}
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;
}
}
}
}
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);
});
}