783 lines
22 KiB
TypeScript
783 lines
22 KiB
TypeScript
/*
|
|
This file is part of GNU Taler
|
|
(C) 2022 Taler Systems S.A.
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
terms of the GNU General Public License as published by the Free Software
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
import {
|
|
Logger,
|
|
TalerErrorCode,
|
|
TalerUriAction,
|
|
TalerError,
|
|
parseTalerUri,
|
|
TalerUri,
|
|
stringifyTalerUri,
|
|
} from "@gnu-taler/taler-util";
|
|
import { WalletOperations } from "@gnu-taler/taler-wallet-core";
|
|
import { BackgroundOperations } from "../wxApi.js";
|
|
import {
|
|
BackgroundPlatformAPI,
|
|
CrossBrowserPermissionsApi,
|
|
ForegroundPlatformAPI,
|
|
MessageFromBackend,
|
|
MessageFromFrontend,
|
|
MessageResponse,
|
|
Permissions,
|
|
Settings,
|
|
defaultSettings,
|
|
} from "./api.js";
|
|
|
|
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
|
|
isFirefox,
|
|
getSettingsFromStorage,
|
|
findTalerUriInActiveTab,
|
|
findTalerUriInClipboard,
|
|
getPermissionsApi,
|
|
getWalletWebExVersion,
|
|
listenToWalletBackground,
|
|
notifyWhenAppIsReady,
|
|
openWalletPage,
|
|
openWalletPageFromPopup,
|
|
openWalletURIFromPopup,
|
|
redirectTabToWalletPage,
|
|
registerAllIncomingConnections,
|
|
registerOnInstalled,
|
|
listenToAllChannels: listenToAllChannels as any,
|
|
registerReloadOnNewVersion,
|
|
sendMessageToAllChannels,
|
|
sendMessageToBackground,
|
|
useServiceWorkerAsBackgroundProcess,
|
|
keepAlive,
|
|
listenNetworkConnectionState,
|
|
};
|
|
|
|
export default api;
|
|
|
|
const logger = new Logger("chrome.ts");
|
|
|
|
async function getSettingsFromStorage(): Promise<Settings> {
|
|
const data = await chrome.storage.local.get("wallet-settings");
|
|
if (!data) return defaultSettings;
|
|
const settings = data["wallet-settings"];
|
|
if (!settings) return defaultSettings;
|
|
try {
|
|
const parsed = JSON.parse(settings);
|
|
return parsed;
|
|
} catch (e) {
|
|
return defaultSettings;
|
|
}
|
|
}
|
|
|
|
function keepAlive(callback: any): void {
|
|
if (extensionIsManifestV3()) {
|
|
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
|
|
|
|
chrome.alarms.onAlarm.addListener((a) => {
|
|
logger.trace(`kee p alive alarm: ${a.name}`);
|
|
// callback()
|
|
});
|
|
// } else {
|
|
}
|
|
callback();
|
|
}
|
|
|
|
function isFirefox(): boolean {
|
|
return false;
|
|
}
|
|
|
|
// const hostPermissions = {
|
|
// permissions: ["webRequest"],
|
|
// origins: ["http://*/*", "https://*/*"],
|
|
// };
|
|
|
|
export function containsClipboardPermissions(): Promise<boolean> {
|
|
return new Promise((res, rej) => {
|
|
res(false);
|
|
// chrome.permissions.contains({ permissions: ["clipboardRead"] }, (resp) => {
|
|
// const le = chrome.runtime.lastError?.message;
|
|
// if (le) {
|
|
// rej(le);
|
|
// }
|
|
// res(resp);
|
|
// });
|
|
});
|
|
}
|
|
|
|
// export function containsHostPermissions(): Promise<boolean> {
|
|
// return new Promise((res, rej) => {
|
|
// chrome.permissions.contains(hostPermissions, (resp) => {
|
|
// const le = chrome.runtime.lastError?.message;
|
|
// if (le) {
|
|
// rej(le);
|
|
// }
|
|
// res(resp);
|
|
// });
|
|
// });
|
|
// }
|
|
|
|
export async function requestClipboardPermissions(): Promise<boolean> {
|
|
return new Promise((res, rej) => {
|
|
res(false);
|
|
// chrome.permissions.request({ permissions: ["clipboardRead"] }, (resp) => {
|
|
// const le = chrome.runtime.lastError?.message;
|
|
// if (le) {
|
|
// rej(le);
|
|
// }
|
|
// res(resp);
|
|
// });
|
|
});
|
|
}
|
|
|
|
// export async function requestHostPermissions(): Promise<boolean> {
|
|
// return new Promise((res, rej) => {
|
|
// chrome.permissions.request(hostPermissions, (resp) => {
|
|
// const le = chrome.runtime.lastError?.message;
|
|
// if (le) {
|
|
// rej(le);
|
|
// }
|
|
// res(resp);
|
|
// });
|
|
// });
|
|
// }
|
|
|
|
// type HeaderListenerFunc = (
|
|
// details: chrome.webRequest.WebResponseHeadersDetails,
|
|
// ) => void;
|
|
// let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
|
|
|
|
// type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
|
|
// let currentTabListener: TabListenerFunc | undefined = undefined;
|
|
|
|
// export async function removeHostPermissions(): Promise<boolean> {
|
|
// //if there is a handler already, remove it
|
|
// if (
|
|
// currentHeaderListener &&
|
|
// chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
|
|
// ) {
|
|
// chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
|
|
// }
|
|
// if (
|
|
// currentTabListener &&
|
|
// chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
|
|
// ) {
|
|
// chrome.tabs.onUpdated.removeListener(currentTabListener);
|
|
// }
|
|
|
|
// currentHeaderListener = undefined;
|
|
// currentTabListener = undefined;
|
|
|
|
// //notify the browser about this change, this operation is expensive
|
|
// if ("webRequest" in chrome) {
|
|
// chrome.webRequest.handlerBehaviorChanged(() => {
|
|
// if (chrome.runtime.lastError) {
|
|
// logger.error(JSON.stringify(chrome.runtime.lastError));
|
|
// }
|
|
// });
|
|
// }
|
|
|
|
// if (extensionIsManifestV3()) {
|
|
// // Trying to remove host permissions with manifest >= v3 throws an error
|
|
// return true;
|
|
// }
|
|
// return new Promise((res, rej) => {
|
|
// chrome.permissions.remove(hostPermissions, (resp) => {
|
|
// const le = chrome.runtime.lastError?.message;
|
|
// if (le) {
|
|
// rej(le);
|
|
// }
|
|
// res(resp);
|
|
// });
|
|
// });
|
|
// }
|
|
|
|
export function removeClipboardPermissions(): Promise<boolean> {
|
|
return new Promise((res, rej) => {
|
|
res(true);
|
|
// chrome.permissions.remove({ permissions: ["clipboardRead"] }, (resp) => {
|
|
// const le = chrome.runtime.lastError?.message;
|
|
// if (le) {
|
|
// rej(le);
|
|
// }
|
|
// res(resp);
|
|
// });
|
|
});
|
|
}
|
|
|
|
function addPermissionsListener(
|
|
callback: (p: Permissions, lastError?: string) => void,
|
|
): void {
|
|
chrome.permissions.onAdded.addListener((perm: Permissions) => {
|
|
const lastError = chrome.runtime.lastError?.message;
|
|
callback(perm, lastError);
|
|
});
|
|
}
|
|
|
|
function getPermissionsApi(): CrossBrowserPermissionsApi {
|
|
return {
|
|
addPermissionsListener,
|
|
// containsHostPermissions,
|
|
// requestHostPermissions,
|
|
// removeHostPermissions,
|
|
requestClipboardPermissions,
|
|
removeClipboardPermissions,
|
|
containsClipboardPermissions,
|
|
};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param callback function to be called
|
|
*/
|
|
function notifyWhenAppIsReady(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if (extensionIsManifestV3()) {
|
|
resolve();
|
|
} else {
|
|
window.addEventListener("load", () => {
|
|
resolve();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function openWalletURIFromPopup(uri: TalerUri): void {
|
|
const talerUri = stringifyTalerUri(uri);
|
|
//FIXME: this should redirect to just one place
|
|
// the target pathname should handle what happens if the endpoint is not there
|
|
// like "trying to open from popup but this uri is not handled"
|
|
|
|
encodeURIComponent;
|
|
let url: string | undefined = undefined;
|
|
switch (uri.type) {
|
|
case TalerUriAction.Withdraw:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/withdraw?talerUri=${encodeURIComponent(
|
|
talerUri,
|
|
)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.Restore:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/recovery?talerUri=${encodeURIComponent(
|
|
talerUri,
|
|
)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.Pay:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.Tip:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/tip?talerUri=${encodeURIComponent(talerUri)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.Refund:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/refund?talerUri=${encodeURIComponent(
|
|
talerUri,
|
|
)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.PayPull:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/invoice/pay?talerUri=${encodeURIComponent(
|
|
talerUri,
|
|
)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.PayPush:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/transfer/pickup?talerUri=${encodeURIComponent(
|
|
talerUri,
|
|
)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.PayTemplate:
|
|
url = chrome.runtime.getURL(
|
|
`static/wallet.html#/cta/pay/template?talerUri=${encodeURIComponent(
|
|
talerUri,
|
|
)}`,
|
|
);
|
|
break;
|
|
case TalerUriAction.DevExperiment:
|
|
logger.warn(`taler://dev-experiment URIs are not allowed in headers`);
|
|
return;
|
|
case TalerUriAction.Exchange:
|
|
logger.warn(`taler://exchange not yet supported`);
|
|
return;
|
|
case TalerUriAction.Auditor:
|
|
logger.warn(`taler://auditor not yet supported`);
|
|
return;
|
|
default: {
|
|
const error: never = uri;
|
|
logger.warn(
|
|
`Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
chrome.tabs.update({ active: true, url }, () => {
|
|
window.close();
|
|
});
|
|
}
|
|
|
|
function openWalletPage(page: string): void {
|
|
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
|
|
chrome.tabs.create({ active: true, url });
|
|
}
|
|
|
|
function openWalletPageFromPopup(page: string): void {
|
|
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
|
|
chrome.tabs.create({ active: true, url }, () => {
|
|
window.close();
|
|
});
|
|
}
|
|
|
|
let nextMessageIndex = 0;
|
|
|
|
/**
|
|
* To be used by the foreground
|
|
* @param message
|
|
* @returns
|
|
*/
|
|
async function sendMessageToBackground<
|
|
Op extends WalletOperations | BackgroundOperations,
|
|
>(message: MessageFromFrontend<Op>): Promise<MessageResponse> {
|
|
const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
|
|
|
|
return new Promise<any>((resolve, reject) => {
|
|
logger.trace("send operation to the wallet background", message);
|
|
let timedout = false;
|
|
const timerId = setTimeout(() => {
|
|
timedout = true;
|
|
throw TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {});
|
|
}, 5 * 1000); //five seconds
|
|
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
|
|
if (timedout) {
|
|
return false; //already rejected
|
|
}
|
|
clearTimeout(timerId);
|
|
if (chrome.runtime.lastError) {
|
|
reject(chrome.runtime.lastError.message);
|
|
} else {
|
|
resolve(backgroundResponse);
|
|
}
|
|
// return true to keep the channel open
|
|
return true;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* To be used by the foreground
|
|
*/
|
|
let notificationPort: chrome.runtime.Port | undefined;
|
|
function listenToWalletBackground(listener: (m: any) => void): () => void {
|
|
if (notificationPort === undefined) {
|
|
notificationPort = chrome.runtime.connect({ name: "notifications" });
|
|
}
|
|
notificationPort.onMessage.addListener(listener);
|
|
function removeListener(): void {
|
|
if (notificationPort !== undefined) {
|
|
notificationPort.onMessage.removeListener(listener);
|
|
}
|
|
}
|
|
return removeListener;
|
|
}
|
|
|
|
const allPorts: chrome.runtime.Port[] = [];
|
|
|
|
function sendMessageToAllChannels(message: MessageFromBackend): void {
|
|
for (const notif of allPorts) {
|
|
// const message: MessageFromBackend = { type: msg.type };
|
|
try {
|
|
notif.postMessage(message);
|
|
} catch (e) {
|
|
logger.error("error posting a message", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerAllIncomingConnections(): void {
|
|
chrome.runtime.onConnect.addListener((port) => {
|
|
try {
|
|
allPorts.push(port);
|
|
port.onDisconnect.addListener((discoPort) => {
|
|
try {
|
|
const idx = allPorts.indexOf(discoPort);
|
|
if (idx >= 0) {
|
|
allPorts.splice(idx, 1);
|
|
}
|
|
} catch (e) {
|
|
logger.error("error trying to remove connection", e);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
logger.error("error trying to save incoming connection", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
function listenToAllChannels(
|
|
notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
|
|
message: MessageFromFrontend<Op> & { id: string },
|
|
) => Promise<MessageResponse>,
|
|
): void {
|
|
chrome.runtime.onMessage.addListener((message, sender, reply) => {
|
|
notifyNewMessage(message)
|
|
.then((apiResponse) => {
|
|
try {
|
|
reply(apiResponse);
|
|
} catch (e) {
|
|
logger.error(
|
|
"sending response to frontend failed",
|
|
message,
|
|
apiResponse,
|
|
e,
|
|
);
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
logger.error("notify to background failed", e);
|
|
});
|
|
|
|
// keep the connection open
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function registerReloadOnNewVersion(): 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) => {
|
|
logger.info("update available:", details);
|
|
chrome.runtime.reload();
|
|
});
|
|
}
|
|
|
|
function redirectTabToWalletPage(tabId: number, page: string): void {
|
|
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
|
|
logger.trace("redirecting tabId: ", tabId, " to: ", url);
|
|
chrome.tabs.update(tabId, { url });
|
|
}
|
|
|
|
interface WalletVersion {
|
|
version_name?: string | undefined;
|
|
version: string;
|
|
}
|
|
|
|
function getWalletWebExVersion(): WalletVersion {
|
|
const manifestData = chrome.runtime.getManifest();
|
|
return manifestData;
|
|
}
|
|
|
|
const alertIcons = {
|
|
"16": "/static/img/taler-alert-16.png",
|
|
"19": "/static/img/taler-alert-19.png",
|
|
"32": "/static/img/taler-alert-32.png",
|
|
"38": "/static/img/taler-alert-38.png",
|
|
"48": "/static/img/taler-alert-48.png",
|
|
"64": "/static/img/taler-alert-64.png",
|
|
"128": "/static/img/taler-alert-128.png",
|
|
"256": "/static/img/taler-alert-256.png",
|
|
"512": "/static/img/taler-alert-512.png",
|
|
};
|
|
const normalIcons = {
|
|
"16": "/static/img/taler-logo-16.png",
|
|
"19": "/static/img/taler-logo-19.png",
|
|
"32": "/static/img/taler-logo-32.png",
|
|
"38": "/static/img/taler-logo-38.png",
|
|
"48": "/static/img/taler-logo-48.png",
|
|
"64": "/static/img/taler-logo-64.png",
|
|
"128": "/static/img/taler-logo-128.png",
|
|
"256": "/static/img/taler-logo-256.png",
|
|
"512": "/static/img/taler-logo-512.png",
|
|
};
|
|
function setNormalIcon(): void {
|
|
if (extensionIsManifestV3()) {
|
|
chrome.action.setIcon({ path: normalIcons });
|
|
} else {
|
|
chrome.browserAction.setIcon({ path: normalIcons });
|
|
}
|
|
}
|
|
|
|
function setAlertedIcon(): void {
|
|
if (extensionIsManifestV3()) {
|
|
chrome.action.setIcon({ path: alertIcons });
|
|
} else {
|
|
chrome.browserAction.setIcon({ path: alertIcons });
|
|
}
|
|
}
|
|
|
|
interface OffscreenCanvasRenderingContext2D
|
|
extends CanvasState,
|
|
CanvasTransform,
|
|
CanvasCompositing,
|
|
CanvasImageSmoothing,
|
|
CanvasFillStrokeStyles,
|
|
CanvasShadowStyles,
|
|
CanvasFilters,
|
|
CanvasRect,
|
|
CanvasDrawPath,
|
|
CanvasUserInterface,
|
|
CanvasText,
|
|
CanvasDrawImage,
|
|
CanvasImageData,
|
|
CanvasPathDrawingStyles,
|
|
CanvasTextDrawingStyles,
|
|
CanvasPath {
|
|
readonly canvas: OffscreenCanvas;
|
|
}
|
|
declare const OffscreenCanvasRenderingContext2D: {
|
|
prototype: OffscreenCanvasRenderingContext2D;
|
|
new (): OffscreenCanvasRenderingContext2D;
|
|
};
|
|
|
|
interface OffscreenCanvas extends EventTarget {
|
|
width: number;
|
|
height: number;
|
|
getContext(
|
|
contextId: "2d",
|
|
contextAttributes?: CanvasRenderingContext2DSettings,
|
|
): OffscreenCanvasRenderingContext2D | null;
|
|
}
|
|
declare const OffscreenCanvas: {
|
|
prototype: OffscreenCanvas;
|
|
new (width: number, height: number): OffscreenCanvas;
|
|
};
|
|
|
|
function createCanvas(size: number): OffscreenCanvas {
|
|
if (extensionIsManifestV3()) {
|
|
return new OffscreenCanvas(size, size);
|
|
} else {
|
|
const c = document.createElement("canvas");
|
|
c.height = size;
|
|
c.width = size;
|
|
return c;
|
|
}
|
|
}
|
|
|
|
async function createImage(size: number, file: string): Promise<ImageData> {
|
|
const r = await fetch(file);
|
|
const b = await r.blob();
|
|
const image = await createImageBitmap(b);
|
|
const canvas = createCanvas(size);
|
|
const canvasContext = canvas.getContext("2d")!;
|
|
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
|
canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
const imageData = canvasContext.getImageData(
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height,
|
|
);
|
|
return imageData;
|
|
}
|
|
|
|
async function registerIconChangeOnTalerContent(): Promise<void> {
|
|
const imgs = await Promise.all(
|
|
Object.entries(alertIcons).map(([key, value]) =>
|
|
createImage(parseInt(key, 10), value),
|
|
),
|
|
);
|
|
const imageData = imgs.reduce(
|
|
(prev, cur) => ({ ...prev, [cur.width]: cur }),
|
|
{} as { [size: string]: ImageData },
|
|
);
|
|
|
|
if (chrome.declarativeContent) {
|
|
// using declarative content does not need host permission
|
|
// and is faster
|
|
const secureTalerUrlLookup = {
|
|
conditions: [
|
|
new chrome.declarativeContent.PageStateMatcher({
|
|
css: ["a[href^='taler://'"],
|
|
}),
|
|
],
|
|
actions: [new chrome.declarativeContent.SetIcon({ imageData })],
|
|
};
|
|
const inSecureTalerUrlLookup = {
|
|
conditions: [
|
|
new chrome.declarativeContent.PageStateMatcher({
|
|
css: ["a[href^='taler+http://'"],
|
|
}),
|
|
],
|
|
actions: [new chrome.declarativeContent.SetIcon({ imageData })],
|
|
};
|
|
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
|
|
chrome.declarativeContent.onPageChanged.addRules([
|
|
secureTalerUrlLookup,
|
|
inSecureTalerUrlLookup,
|
|
]);
|
|
});
|
|
return;
|
|
}
|
|
|
|
//this browser doesn't have declarativeContent
|
|
//we need host_permission and we will check the content for changing the icon
|
|
chrome.tabs.onUpdated.addListener(
|
|
async (tabId, info: chrome.tabs.TabChangeInfo) => {
|
|
if (tabId < 0) return;
|
|
if (info.status !== "complete") return;
|
|
const uri = await findTalerUriInTab(tabId);
|
|
if (uri) {
|
|
setAlertedIcon();
|
|
} else {
|
|
setNormalIcon();
|
|
}
|
|
},
|
|
);
|
|
chrome.tabs.onActivated.addListener(
|
|
async ({ tabId }: chrome.tabs.TabActiveInfo) => {
|
|
if (tabId < 0) return;
|
|
const uri = await findTalerUriInTab(tabId);
|
|
if (uri) {
|
|
setAlertedIcon();
|
|
} else {
|
|
setNormalIcon();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function registerOnInstalled(callback: () => void): 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(async (details) => {
|
|
logger.info(`onInstalled with reason: "${details.reason}"`);
|
|
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
|
|
callback();
|
|
}
|
|
await registerIconChangeOnTalerContent();
|
|
});
|
|
}
|
|
|
|
function extensionIsManifestV3(): boolean {
|
|
return chrome.runtime.getManifest().manifest_version === 3;
|
|
}
|
|
|
|
function useServiceWorkerAsBackgroundProcess(): boolean {
|
|
return extensionIsManifestV3();
|
|
}
|
|
|
|
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(): Promise<chrome.tabs.Tab> {
|
|
const queryOptions = { active: true, currentWindow: true };
|
|
return new Promise<chrome.tabs.Tab>((resolve, reject) => {
|
|
chrome.tabs.query(queryOptions, (tabs) => {
|
|
if (chrome.runtime.lastError) {
|
|
reject(chrome.runtime.lastError);
|
|
return;
|
|
}
|
|
resolve(tabs[0]);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
|
|
if (extensionIsManifestV3()) {
|
|
// manifest v3
|
|
try {
|
|
const res = await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
func: searchForTalerLinks,
|
|
args: [],
|
|
});
|
|
return res[0].result;
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
} else {
|
|
return new Promise((resolve, reject) => {
|
|
//manifest v2
|
|
chrome.tabs.executeScript(
|
|
tabId,
|
|
{
|
|
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) {
|
|
logger.error(JSON.stringify(chrome.runtime.lastError));
|
|
resolve(undefined);
|
|
return;
|
|
}
|
|
resolve(result[0]);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function timeout(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
async function findTalerUriInClipboard(): Promise<string | undefined> {
|
|
//FIXME: add clipboard feature
|
|
// try {
|
|
// //It looks like clipboard promise does not return, so we need a timeout
|
|
// const textInClipboard = await Promise.any([
|
|
// timeout(100),
|
|
// window.navigator.clipboard.readText(),
|
|
// ]);
|
|
// if (!textInClipboard) return;
|
|
// return textInClipboard.startsWith("taler://") ||
|
|
// textInClipboard.startsWith("taler+http://")
|
|
// ? textInClipboard
|
|
// : undefined;
|
|
// } catch (e) {
|
|
// logger.error("could not read clipboard", e);
|
|
// return undefined;
|
|
// }
|
|
return undefined;
|
|
}
|
|
|
|
async function findTalerUriInActiveTab(): Promise<string | undefined> {
|
|
const tab = await getCurrentTab();
|
|
if (!tab || tab.id === undefined) return;
|
|
return findTalerUriInTab(tab.id);
|
|
}
|
|
|
|
function listenNetworkConnectionState(
|
|
notify: (state: "on" | "off") => void,
|
|
): () => void {
|
|
function notifyOffline() {
|
|
notify("off");
|
|
}
|
|
function notifyOnline() {
|
|
notify("on");
|
|
}
|
|
window.addEventListener("offline", notifyOffline);
|
|
window.addEventListener("online", notifyOnline);
|
|
return () => {
|
|
window.removeEventListener("offline", notifyOffline);
|
|
window.removeEventListener("online", notifyOnline);
|
|
};
|
|
}
|