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:
parent
c539d1803c
commit
32f6409ac3
@ -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]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
@ -23,14 +23,31 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
|
import { platform, setupPlatform } from "./platform/api";
|
||||||
|
import firefoxAPI from "./platform/firefox"
|
||||||
|
import chromeAPI from "./platform/chrome"
|
||||||
import { wxMain } from "./wxBackend";
|
import { wxMain } from "./wxBackend";
|
||||||
|
|
||||||
const loadedFromWebpage = typeof window !== "undefined"
|
const isFirefox = typeof (window as any)['InstallTrigger'] !== 'undefined'
|
||||||
|
|
||||||
if (chrome.runtime.getManifest().manifest_version === 3) {
|
//FIXME: create different entry point for any platform instead of
|
||||||
wxMain();
|
//switching in runtime
|
||||||
|
if (isFirefox) {
|
||||||
|
console.log("Wallet setup for Firefox API")
|
||||||
|
setupPlatform(firefoxAPI)
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener("load", () => {
|
console.log("Wallet setup for Chrome API")
|
||||||
wxMain();
|
setupPlatform(chromeAPI)
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
platform.registerOnInstalled(() => {
|
||||||
|
platform.openWalletPage("/welcome")
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
platform.notifyWhenAppIsReady(() => {
|
||||||
|
wxMain();
|
||||||
|
})
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
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
|
* Polyfill for requestAnimationFrame, which
|
||||||
@ -210,7 +210,7 @@ export class ChromeBadge {
|
|||||||
if (this.animationRunning) {
|
if (this.animationRunning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isFirefox()) {
|
if (platform.isFirefox()) {
|
||||||
// Firefox does not support badge animations properly
|
// Firefox does not support badge animations properly
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,7 +17,6 @@
|
|||||||
import { WalletDiagnostics } from "@gnu-taler/taler-util";
|
import { WalletDiagnostics } from "@gnu-taler/taler-util";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useTranslationContext } from "../context/translation";
|
import { useTranslationContext } from "../context/translation";
|
||||||
import { PageLink } from "../renderHtml";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
timedOut: boolean;
|
timedOut: boolean;
|
||||||
@ -70,10 +69,7 @@ export function Diagnostics({ timedOut, diagnostics }: Props): VNode {
|
|||||||
<p>
|
<p>
|
||||||
<i18n.Translate>
|
<i18n.Translate>
|
||||||
Your wallet database is outdated. Currently automatic migration is
|
Your wallet database is outdated. Currently automatic migration is
|
||||||
not supported. Please go{" "}
|
not supported. Please go <i18n.Translate>here</i18n.Translate>
|
||||||
<PageLink pageName="/reset-required">
|
|
||||||
<i18n.Translate>here</i18n.Translate>
|
|
||||||
</PageLink>{" "}
|
|
||||||
to reset the wallet database.
|
to reset the wallet database.
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { createContext, h, VNode } from "preact";
|
import { createContext, h, VNode } from "preact";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
import { findTalerUriInActiveTab } from "../api/browser";
|
import { platform } from "../platform/api";
|
||||||
|
|
||||||
interface Type {
|
interface Type {
|
||||||
findTalerUriInActiveTab: () => Promise<string | undefined>;
|
findTalerUriInActiveTab: () => Promise<string | undefined>;
|
||||||
@ -45,5 +45,5 @@ export const IoCProviderForTesting = ({ value, children }: { value: Type, childr
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IoCProviderForRuntime = ({ children }: { children: any }): VNode => {
|
export const IoCProviderForRuntime = ({ children }: { children: any }): VNode => {
|
||||||
return h(Context.Provider, { value: { findTalerUriInActiveTab }, children });
|
return h(Context.Provider, { value: { findTalerUriInActiveTab: platform.findTalerUriInActiveTab }, children });
|
||||||
};
|
};
|
||||||
|
@ -20,11 +20,15 @@
|
|||||||
* @author sebasjm
|
* @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 { h, VNode } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { useTranslationContext } from "../context/translation";
|
import { useTranslationContext } from "../context/translation";
|
||||||
import { AmountView } from "../renderHtml";
|
|
||||||
import * as wxApi from "../wxApi";
|
import * as wxApi from "../wxApi";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -120,3 +124,27 @@ export function RefundPage({ talerRefundUri }: Props): VNode {
|
|||||||
|
|
||||||
return <View applyResult={applyResult} />;
|
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);
|
||||||
|
@ -17,15 +17,19 @@
|
|||||||
/**
|
/**
|
||||||
* Page shown to the user to accept or ignore a tip from a merchant.
|
* 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 { h, VNode } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Loading } from "../components/Loading";
|
import { Loading } from "../components/Loading";
|
||||||
import { useTranslationContext } from "../context/translation";
|
import { useTranslationContext } from "../context/translation";
|
||||||
import { AmountView } from "../renderHtml";
|
|
||||||
import * as wxApi from "../wxApi";
|
import * as wxApi from "../wxApi";
|
||||||
|
|
||||||
interface Props {
|
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);
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "preact/hooks";
|
import { useState, useEffect } from "preact/hooks";
|
||||||
import * as wxApi from "../wxApi";
|
import * as wxApi from "../wxApi";
|
||||||
import { getPermissionsApi } from "../compat";
|
import { platform } from "../platform/api";
|
||||||
import { getReadRequestPermissions } from "../permissions";
|
import { getReadRequestPermissions } from "../permissions";
|
||||||
|
|
||||||
export function useExtendedPermissions(): [boolean, () => Promise<void>] {
|
export function useExtendedPermissions(): [boolean, () => Promise<void>] {
|
||||||
@ -40,24 +40,41 @@ async function handleExtendedPerm(isEnabled: boolean, onChange: (value: boolean)
|
|||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
// We set permissions here, since apparently FF wants this to be done
|
// We set permissions here, since apparently FF wants this to be done
|
||||||
// as the result of an input event ...
|
// as the result of an input event ...
|
||||||
return new Promise<void>((res) => {
|
const granted = await platform.getPermissionsApi().request(getReadRequestPermissions());
|
||||||
getPermissionsApi().request(getReadRequestPermissions(), async (granted: boolean) => {
|
|
||||||
console.log("permissions granted:", granted);
|
console.log("permissions granted:", granted);
|
||||||
if (chrome.runtime.lastError) {
|
const lastError = platform.getLastError();
|
||||||
|
if (lastError) {
|
||||||
console.error("error requesting permissions");
|
console.error("error requesting permissions");
|
||||||
console.error(chrome.runtime.lastError);
|
console.error(lastError);
|
||||||
onChange(false);
|
onChange(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
// try {
|
||||||
const res = await wxApi.setExtendedPermissions(granted);
|
const res = await wxApi.setExtendedPermissions(granted);
|
||||||
onChange(res.newValue);
|
onChange(res.newValue);
|
||||||
} finally {
|
// } finally {
|
||||||
res()
|
// 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));
|
await wxApi.setExtendedPermissions(false).then(r => onChange(r.newValue));
|
||||||
return
|
return
|
||||||
|
90
packages/taler-wallet-webextension/src/platform/api.ts
Normal file
90
packages/taler-wallet-webextension/src/platform/api.ts
Normal 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;
|
||||||
|
}
|
371
packages/taler-wallet-webextension/src/platform/chrome.ts
Normal file
371
packages/taler-wallet-webextension/src/platform/chrome.ts
Normal 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]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
74
packages/taler-wallet-webextension/src/platform/firefox.ts
Normal file
74
packages/taler-wallet-webextension/src/platform/firefox.ts
Normal 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
|
||||||
|
}
|
@ -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() {}
|
function runIntegrationTest() {}
|
||||||
|
|
||||||
export async function confirmReset(
|
export async function confirmReset(
|
||||||
@ -395,13 +385,3 @@ export async function confirmReset(
|
|||||||
window.close();
|
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),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -21,36 +21,21 @@
|
|||||||
|
|
||||||
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
|
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
|
||||||
import { Fragment, h } from "preact";
|
import { Fragment, h } from "preact";
|
||||||
|
import { platform } from "../platform/api";
|
||||||
import { ButtonPrimary, ButtonSuccess } from "../components/styled";
|
import { ButtonPrimary, ButtonSuccess } from "../components/styled";
|
||||||
import { useTranslationContext } from "../context/translation";
|
import { useTranslationContext } from "../context/translation";
|
||||||
import { actionForTalerUri } from "../utils/index";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
url: string;
|
url: string;
|
||||||
onDismiss: () => void;
|
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) {
|
export function TalerActionFound({ url, onDismiss }: Props) {
|
||||||
const uriType = classifyTalerUri(url);
|
const uriType = classifyTalerUri(url);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
function redirectToWallet() {
|
||||||
|
platform.openWalletURIFromPopup(uriType, url);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<section>
|
<section>
|
||||||
@ -62,11 +47,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {
|
|||||||
<p>
|
<p>
|
||||||
<i18n.Translate>This page has pay action.</i18n.Translate>
|
<i18n.Translate>This page has pay action.</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
<ButtonSuccess
|
<ButtonSuccess onClick={redirectToWallet}>
|
||||||
onClick={() => {
|
|
||||||
navigateTo(actionForTalerUri(uriType, url));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Open pay page</i18n.Translate>
|
<i18n.Translate>Open pay page</i18n.Translate>
|
||||||
</ButtonSuccess>
|
</ButtonSuccess>
|
||||||
</div>
|
</div>
|
||||||
@ -78,11 +59,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {
|
|||||||
This page has a withdrawal action.
|
This page has a withdrawal action.
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
<ButtonSuccess
|
<ButtonSuccess onClick={redirectToWallet}>
|
||||||
onClick={() => {
|
|
||||||
navigateTo(actionForTalerUri(uriType, url));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Open withdraw page</i18n.Translate>
|
<i18n.Translate>Open withdraw page</i18n.Translate>
|
||||||
</ButtonSuccess>
|
</ButtonSuccess>
|
||||||
</div>
|
</div>
|
||||||
@ -92,11 +69,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {
|
|||||||
<p>
|
<p>
|
||||||
<i18n.Translate>This page has a tip action.</i18n.Translate>
|
<i18n.Translate>This page has a tip action.</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
<ButtonSuccess
|
<ButtonSuccess onClick={redirectToWallet}>
|
||||||
onClick={() => {
|
|
||||||
navigateTo(actionForTalerUri(uriType, url));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Open tip page</i18n.Translate>
|
<i18n.Translate>Open tip page</i18n.Translate>
|
||||||
</ButtonSuccess>
|
</ButtonSuccess>
|
||||||
</div>
|
</div>
|
||||||
@ -108,11 +81,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {
|
|||||||
This page has a notify reserve action.
|
This page has a notify reserve action.
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
<ButtonSuccess
|
<ButtonSuccess onClick={redirectToWallet}>
|
||||||
onClick={() => {
|
|
||||||
navigateTo(actionForTalerUri(uriType, url));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Notify</i18n.Translate>
|
<i18n.Translate>Notify</i18n.Translate>
|
||||||
</ButtonSuccess>
|
</ButtonSuccess>
|
||||||
</div>
|
</div>
|
||||||
@ -122,11 +91,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {
|
|||||||
<p>
|
<p>
|
||||||
<i18n.Translate>This page has a refund action.</i18n.Translate>
|
<i18n.Translate>This page has a refund action.</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
<ButtonSuccess
|
<ButtonSuccess onClick={redirectToWallet}>
|
||||||
onClick={() => {
|
|
||||||
navigateTo(actionForTalerUri(uriType, url));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Open refund page</i18n.Translate>
|
<i18n.Translate>Open refund page</i18n.Translate>
|
||||||
</ButtonSuccess>
|
</ButtonSuccess>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
/**
|
/**
|
||||||
* Main entry point for extension pages.
|
* Main entry point for extension pages.
|
||||||
*
|
*
|
||||||
* @author sebasjm <dold@taler.net>
|
* @author sebasjm
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setupI18n } from "@gnu-taler/taler-util";
|
import { setupI18n } from "@gnu-taler/taler-util";
|
||||||
@ -37,6 +37,9 @@ import {
|
|||||||
import { useTalerActionURL } from "./hooks/useTalerActionURL";
|
import { useTalerActionURL } from "./hooks/useTalerActionURL";
|
||||||
import { strings } from "./i18n/strings";
|
import { strings } from "./i18n/strings";
|
||||||
import { Pages, PopupNavBar } from "./NavigationBar";
|
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 { BalancePage } from "./popup/BalancePage";
|
||||||
import { TalerActionFound } from "./popup/TalerActionFound";
|
import { TalerActionFound } from "./popup/TalerActionFound";
|
||||||
import { BackupPage } from "./wallet/BackupPage";
|
import { BackupPage } from "./wallet/BackupPage";
|
||||||
@ -59,6 +62,17 @@ function main(): void {
|
|||||||
|
|
||||||
setupI18n("en", strings);
|
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") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", main);
|
document.addEventListener("DOMContentLoaded", main);
|
||||||
} else {
|
} else {
|
||||||
@ -106,7 +120,7 @@ function Application(): VNode {
|
|||||||
route(Pages.balance_deposit.replace(":currency", currency))
|
route(Pages.balance_deposit.replace(":currency", currency))
|
||||||
}
|
}
|
||||||
goToWalletHistory={(currency: string) =>
|
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 {
|
function RedirectToWalletPage(): VNode {
|
||||||
const page = document.location.hash || "#/";
|
const page = (document.location.hash || "#/").replace("#", "");
|
||||||
const [showText, setShowText] = useState(false);
|
const [showText, setShowText] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.tabs.create(
|
platform.openWalletPageFromPopup(page);
|
||||||
{
|
|
||||||
active: true,
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
url: chrome.runtime.getURL(`/static/wallet.html${page}`),
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
window.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowText(true);
|
setShowText(true);
|
||||||
}, 250);
|
}, 250);
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -187,52 +187,3 @@ export function amountToString(text: AmountJson): string {
|
|||||||
return `${amount} ${aj.currency}`;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
|
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
import { platform } from "../platform/api";
|
||||||
import { Button, ButtonSuccess, InputWithLabel } from "../components/styled";
|
import { Button, ButtonSuccess, InputWithLabel } from "../components/styled";
|
||||||
import { useTranslationContext } from "../context/translation";
|
import { useTranslationContext } from "../context/translation";
|
||||||
import { actionForTalerUri } from "../utils/index";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@ -14,6 +14,10 @@ export function AddNewActionView({ onCancel }: Props): VNode {
|
|||||||
const uriType = classifyTalerUri(url);
|
const uriType = classifyTalerUri(url);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
function redirectToWallet() {
|
||||||
|
platform.openWalletURIFromPopup(uriType, url);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<section>
|
<section>
|
||||||
@ -37,12 +41,7 @@ export function AddNewActionView({ onCancel }: Props): VNode {
|
|||||||
<i18n.Translate>Cancel</i18n.Translate>
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
</Button>
|
</Button>
|
||||||
{uriType !== TalerUriType.Unknown && (
|
{uriType !== TalerUriType.Unknown && (
|
||||||
<ButtonSuccess
|
<ButtonSuccess onClick={redirectToWallet}>
|
||||||
onClick={() => {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
chrome.tabs.create({ url: actionForTalerUri(uriType, url) });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(() => {
|
{(() => {
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case TalerUriType.TalerNotifyReserve:
|
case TalerUriType.TalerNotifyReserve:
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
/**
|
/**
|
||||||
* Main entry point for extension pages.
|
* Main entry point for extension pages.
|
||||||
*
|
*
|
||||||
* @author sebasjm <dold@taler.net>
|
* @author sebasjm
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setupI18n } from "@gnu-taler/taler-util";
|
import { setupI18n } from "@gnu-taler/taler-util";
|
||||||
@ -28,12 +28,7 @@ import Match from "preact-router/match";
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { LogoHeader } from "./components/LogoHeader";
|
import { LogoHeader } from "./components/LogoHeader";
|
||||||
import PendingTransactions from "./components/PendingTransactions";
|
import PendingTransactions from "./components/PendingTransactions";
|
||||||
import {
|
import { SuccessBox, WalletBox } from "./components/styled";
|
||||||
NavigationHeader,
|
|
||||||
NavigationHeaderHolder,
|
|
||||||
SuccessBox,
|
|
||||||
WalletBox,
|
|
||||||
} from "./components/styled";
|
|
||||||
import { DevContextProvider } from "./context/devContext";
|
import { DevContextProvider } from "./context/devContext";
|
||||||
import { IoCProviderForRuntime } from "./context/iocContext";
|
import { IoCProviderForRuntime } from "./context/iocContext";
|
||||||
import {
|
import {
|
||||||
@ -46,6 +41,9 @@ import { TipPage } from "./cta/Tip";
|
|||||||
import { WithdrawPage } from "./cta/Withdraw";
|
import { WithdrawPage } from "./cta/Withdraw";
|
||||||
import { strings } from "./i18n/strings";
|
import { strings } from "./i18n/strings";
|
||||||
import { Pages, WalletNavBar } from "./NavigationBar";
|
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 { DeveloperPage } from "./popup/DeveloperPage";
|
||||||
import { BackupPage } from "./wallet/BackupPage";
|
import { BackupPage } from "./wallet/BackupPage";
|
||||||
import { DepositPage } from "./wallet/DepositPage";
|
import { DepositPage } from "./wallet/DepositPage";
|
||||||
@ -75,6 +73,17 @@ function main(): void {
|
|||||||
|
|
||||||
setupI18n("en", strings);
|
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") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", main);
|
document.addEventListener("DOMContentLoaded", main);
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,7 +61,7 @@ import {
|
|||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
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> {
|
async function callBackend(operation: string, payload: any): Promise<any> {
|
||||||
return new Promise<any>((resolve, reject) => {
|
let response: CoreApiResponse;
|
||||||
// eslint-disable-next-line no-undef
|
try {
|
||||||
chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => {
|
response = await platform.setMessageToWalletBackground(operation, payload)
|
||||||
// eslint-disable-next-line no-undef
|
} catch (e) {
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.log("Error calling backend");
|
console.log("Error calling backend");
|
||||||
reject(
|
throw new Error(`Error contacting backend: ${e}`)
|
||||||
new Error(
|
|
||||||
`Error contacting backend: ${chrome.runtime.lastError.message}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
console.log("got response", resp);
|
console.log("got response", response);
|
||||||
const r = resp as CoreApiResponse;
|
if (response.type === "error") {
|
||||||
if (r.type === "error") {
|
throw new TalerError.fromUncheckedDetail(response.error);
|
||||||
reject(TalerError.fromUncheckedDetail(r.error));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
resolve(r.result);
|
return response.result;
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -422,20 +413,12 @@ export function importDB(dump: any): Promise<void> {
|
|||||||
return callBackend("importDb", { dump });
|
return callBackend("importDb", { dump });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onUpdateNotification(
|
export function onUpdateNotification(messageTypes: Array<NotificationType>, doCallback: () => void): () => void {
|
||||||
messageTypes: Array<NotificationType>,
|
|
||||||
doCallback: () => void,
|
|
||||||
): () => void {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const port = chrome.runtime.connect({ name: "notifications" });
|
|
||||||
const listener = (message: MessageFromBackend): void => {
|
const listener = (message: MessageFromBackend): void => {
|
||||||
const shouldNotify = messageTypes.includes(message.type);
|
const shouldNotify = messageTypes.includes(message.type);
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
doCallback();
|
doCallback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
port.onMessage.addListener(listener);
|
return platform.listenToWalletNotifications(listener)
|
||||||
return () => {
|
|
||||||
port.onMessage.removeListener(listener);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -26,11 +26,9 @@
|
|||||||
import {
|
import {
|
||||||
classifyTalerUri,
|
classifyTalerUri,
|
||||||
CoreApiResponse,
|
CoreApiResponse,
|
||||||
CoreApiResponseSuccess,
|
CoreApiResponseSuccess, TalerErrorCode,
|
||||||
NotificationType,
|
|
||||||
TalerErrorCode,
|
|
||||||
TalerUriType,
|
TalerUriType,
|
||||||
WalletDiagnostics,
|
WalletDiagnostics
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
DbAccess,
|
DbAccess,
|
||||||
@ -40,13 +38,13 @@ import {
|
|||||||
openPromise,
|
openPromise,
|
||||||
openTalerDatabase,
|
openTalerDatabase,
|
||||||
Wallet,
|
Wallet,
|
||||||
WalletStoresV1,
|
WalletStoresV1
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
|
import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
|
||||||
import { BrowserHttpLib } from "./browserHttpLib";
|
import { BrowserHttpLib } from "./browserHttpLib";
|
||||||
import { getPermissionsApi, isFirefox } from "./compat";
|
|
||||||
import { getReadRequestPermissions } from "./permissions";
|
import { getReadRequestPermissions } from "./permissions";
|
||||||
import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
|
import { MessageFromBackend, platform } from "./platform/api";
|
||||||
|
import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory";
|
||||||
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib";
|
import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,10 +64,8 @@ let outdatedDbVersion: number | undefined;
|
|||||||
|
|
||||||
const walletInit: OpenedPromise<void> = openPromise<void>();
|
const walletInit: OpenedPromise<void> = openPromise<void>();
|
||||||
|
|
||||||
const notificationPorts: chrome.runtime.Port[] = [];
|
|
||||||
|
|
||||||
async function getDiagnostics(): Promise<WalletDiagnostics> {
|
async function getDiagnostics(): Promise<WalletDiagnostics> {
|
||||||
const manifestData = chrome.runtime.getManifest();
|
const manifestData = platform.getWalletVersion();
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
let firefoxIdbProblem = false;
|
let firefoxIdbProblem = false;
|
||||||
let dbOutdated = false;
|
let dbOutdated = false;
|
||||||
@ -80,7 +76,7 @@ async function getDiagnostics(): Promise<WalletDiagnostics> {
|
|||||||
if (
|
if (
|
||||||
currentDatabase === undefined &&
|
currentDatabase === undefined &&
|
||||||
outdatedDbVersion === undefined &&
|
outdatedDbVersion === undefined &&
|
||||||
isFirefox()
|
platform.isFirefox()
|
||||||
) {
|
) {
|
||||||
firefoxIdbProblem = true;
|
firefoxIdbProblem = true;
|
||||||
}
|
}
|
||||||
@ -132,14 +128,7 @@ async function dispatch(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "wxGetExtendedPermissions": {
|
case "wxGetExtendedPermissions": {
|
||||||
const res = await new Promise((resolve, reject) => {
|
const res = await platform.getPermissionsApi().contains(getReadRequestPermissions());
|
||||||
getPermissionsApi().contains(
|
|
||||||
getReadRequestPermissions(),
|
|
||||||
(result: boolean) => {
|
|
||||||
resolve(result);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
r = wrapResponse({ newValue: res });
|
r = wrapResponse({ newValue: res });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -147,15 +136,11 @@ async function dispatch(
|
|||||||
const newVal = req.payload.value;
|
const newVal = req.payload.value;
|
||||||
console.log("new extended permissions value", newVal);
|
console.log("new extended permissions value", newVal);
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
setupHeaderListener();
|
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
|
||||||
r = wrapResponse({ newValue: true });
|
r = wrapResponse({ newValue: true });
|
||||||
} else {
|
} else {
|
||||||
await new Promise<void>((resolve, reject) => {
|
const rem = await platform.getPermissionsApi().remove(getReadRequestPermissions());
|
||||||
getPermissionsApi().remove(getReadRequestPermissions(), (rem) => {
|
|
||||||
console.log("permissions removed:", rem);
|
console.log("permissions removed:", rem);
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
r = wrapResponse({ newVal: false });
|
r = wrapResponse({ newVal: false });
|
||||||
}
|
}
|
||||||
break;
|
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> {
|
async function reinitWallet(): Promise<void> {
|
||||||
if (currentWallet) {
|
if (currentWallet) {
|
||||||
currentWallet.stop();
|
currentWallet.stop();
|
||||||
currentWallet = undefined;
|
currentWallet = undefined;
|
||||||
}
|
}
|
||||||
currentDatabase = undefined;
|
currentDatabase = undefined;
|
||||||
setBadgeText({ text: "" });
|
// setBadgeText({ text: "" });
|
||||||
try {
|
try {
|
||||||
currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
|
currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -265,7 +189,7 @@ async function reinitWallet(): Promise<void> {
|
|||||||
let httpLib;
|
let httpLib;
|
||||||
let cryptoWorker;
|
let cryptoWorker;
|
||||||
|
|
||||||
if (chrome.runtime.getManifest().manifest_version === 3) {
|
if (platform.useServiceWorkerAsBackgroundProcess()) {
|
||||||
httpLib = new ServiceWorkerHttpLib();
|
httpLib = new ServiceWorkerHttpLib();
|
||||||
cryptoWorker = new SynchronousCryptoWorkerFactory();
|
cryptoWorker = new SynchronousCryptoWorkerFactory();
|
||||||
} else {
|
} else {
|
||||||
@ -283,14 +207,8 @@ async function reinitWallet(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wallet.addNotificationListener((x) => {
|
wallet.addNotificationListener((x) => {
|
||||||
for (const notif of notificationPorts) {
|
|
||||||
const message: MessageFromBackend = { type: x.type };
|
const message: MessageFromBackend = { type: x.type };
|
||||||
try {
|
platform.sendMessageToAllChannels(message)
|
||||||
notif.postMessage(message);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
wallet.runTaskLoop().catch((e) => {
|
wallet.runTaskLoop().catch((e) => {
|
||||||
console.log("error during wallet task loop", e);
|
console.log("error during wallet task loop", e);
|
||||||
@ -303,87 +221,32 @@ async function reinitWallet(): Promise<void> {
|
|||||||
walletInit.resolve();
|
walletInit.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
function parseTalerUriAndRedirect(tabId: number, talerUri: string) {
|
||||||
// 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);
|
const uriType = classifyTalerUri(talerUri);
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
case TalerUriType.TalerWithdraw:
|
case TalerUriType.TalerWithdraw:
|
||||||
return makeSyncWalletRedirect(
|
return platform.redirectTabToWalletPage(
|
||||||
"/static/wallet.html#/cta/withdraw",
|
tabId,
|
||||||
details.tabId,
|
`/cta/withdraw?talerWithdrawUri=${talerUri}`,
|
||||||
details.url,
|
|
||||||
{
|
|
||||||
talerWithdrawUri: talerUri,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
case TalerUriType.TalerPay:
|
case TalerUriType.TalerPay:
|
||||||
return makeSyncWalletRedirect(
|
return platform.redirectTabToWalletPage(
|
||||||
"/static/wallet.html#/cta/pay",
|
tabId,
|
||||||
details.tabId,
|
`/cta/pay?talerPayUri=${talerUri}`,
|
||||||
details.url,
|
|
||||||
{
|
|
||||||
talerPayUri: talerUri,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
case TalerUriType.TalerTip:
|
case TalerUriType.TalerTip:
|
||||||
return makeSyncWalletRedirect(
|
return platform.redirectTabToWalletPage(
|
||||||
"/static/wallet.html#/cta/tip",
|
tabId,
|
||||||
details.tabId,
|
`/cta/tip?talerTipUri=${talerUri}`,
|
||||||
details.url,
|
|
||||||
{
|
|
||||||
talerTipUri: talerUri,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
case TalerUriType.TalerRefund:
|
case TalerUriType.TalerRefund:
|
||||||
return makeSyncWalletRedirect(
|
return platform.redirectTabToWalletPage(
|
||||||
"/static/wallet.html#/cta/refund",
|
tabId,
|
||||||
details.tabId,
|
`/cta/refund?talerRefundUri=${talerUri}`,
|
||||||
details.url,
|
|
||||||
{
|
|
||||||
talerRefundUri: talerUri,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
case TalerUriType.TalerNotifyReserve:
|
case TalerUriType.TalerNotifyReserve:
|
||||||
Promise.resolve().then(() => {
|
|
||||||
const w = currentWallet;
|
|
||||||
if (!w) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// FIXME: Is this still useful?
|
// FIXME: Is this still useful?
|
||||||
// handleNotifyReserve(w);
|
// handleNotifyReserve(w);
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -391,47 +254,8 @@ function headerListener(
|
|||||||
);
|
);
|
||||||
break;
|
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.
|
* Main function to run for the WebExtension backend.
|
||||||
@ -439,48 +263,34 @@ function setupHeaderListener(): void {
|
|||||||
* Sets up all event handlers and other machinery.
|
* Sets up all event handlers and other machinery.
|
||||||
*/
|
*/
|
||||||
export async function wxMain(): Promise<void> {
|
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();
|
const afterWalletIsInitialized = reinitWallet();
|
||||||
|
|
||||||
|
platform.registerReloadOnNewVersion();
|
||||||
|
|
||||||
// Handlers for messages coming directly from the content
|
// Handlers for messages coming directly from the content
|
||||||
// script on the page
|
// script on the page
|
||||||
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
platform.registerOnNewMessage((message, sender, callback) => {
|
||||||
afterWalletIsInitialized.then(() => {
|
afterWalletIsInitialized.then(() => {
|
||||||
dispatch(req, sender, sendResponse);
|
dispatch(message, sender, callback);
|
||||||
});
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener((port) => {
|
platform.registerAllIncomingConnections()
|
||||||
notificationPorts.push(port);
|
|
||||||
port.onDisconnect.addListener((discoPort) => {
|
|
||||||
const idx = notificationPorts.indexOf(discoPort);
|
|
||||||
if (idx >= 0) {
|
|
||||||
notificationPorts.splice(idx, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (chrome.runtime.getManifest().manifest_version === 2) {
|
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
|
||||||
setupHeaderListener();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On platforms that support it, also listen to external
|
// On platforms that support it, also listen to external
|
||||||
// modification of permissions.
|
// modification of permissions.
|
||||||
getPermissionsApi().addPermissionsListener((perm) => {
|
platform.getPermissionsApi().addPermissionsListener((perm) => {
|
||||||
if (chrome.runtime.lastError) {
|
const lastError = platform.getLastError()
|
||||||
console.error(chrome.runtime.lastError);
|
if (lastError) {
|
||||||
|
console.error(lastError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setupHeaderListener();
|
platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user