diff options
14 files changed, 217 insertions, 48 deletions
diff --git a/packages/taler-wallet-webextension/manifest-v2.json b/packages/taler-wallet-webextension/manifest-v2.json index e2dd67623..74f103feb 100644 --- a/packages/taler-wallet-webextension/manifest-v2.json +++ b/packages/taler-wallet-webextension/manifest-v2.json @@ -24,7 +24,8 @@ "optional_permissions": [ "http://*/*", "https://*/*", - "webRequest" + "webRequest", + "clipboardRead" ], "browser_action": { "default_icon": { @@ -45,4 +46,4 @@ "page": "static/background.html", "persistent": true } -} +}
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/manifest-v3.json b/packages/taler-wallet-webextension/manifest-v3.json index 3ddaec513..78dcac623 100644 --- a/packages/taler-wallet-webextension/manifest-v3.json +++ b/packages/taler-wallet-webextension/manifest-v3.json @@ -27,7 +27,8 @@ } }, "optional_permissions": [ - "webRequest" + "webRequest", + "clipboardRead" ], "host_permissions": [ "http://*/*", @@ -51,4 +52,4 @@ "background": { "service_worker": "service_worker.js" } -} +}
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/background.ts b/packages/taler-wallet-webextension/src/background.ts index 0085ee4fd..0e2ea3f3a 100644 --- a/packages/taler-wallet-webextension/src/background.ts +++ b/packages/taler-wallet-webextension/src/background.ts @@ -42,14 +42,6 @@ if (isFirefox) { setupPlatform(chromeAPI); } -try { - platform.registerOnInstalled(() => { - platform.openWalletPage("/welcome"); - }); -} catch (e) { - console.error(e); -} - // setGlobalLogLevelFromString("trace") platform.notifyWhenAppIsReady(() => { wxMain(); diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts index 06ae84593..727d653af 100644 --- a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts +++ b/packages/taler-wallet-webextension/src/hooks/useAutoOpenPermissions.ts @@ -20,21 +20,21 @@ import { platform } from "../platform/api.js"; import { ToggleHandler } from "../mui/handlers.js"; import { TalerError } from "@gnu-taler/taler-wallet-core"; -export function useExtendedPermissions(): ToggleHandler { +export function useAutoOpenPermissions(): ToggleHandler { const [enabled, setEnabled] = useState(false); const [error, setError] = useState<TalerError | undefined>(); const toggle = async (): Promise<void> => { - return handleExtendedPerm(enabled, setEnabled).catch((e) => { + return handleAutoOpenPerm(enabled, setEnabled).catch((e) => { setError(TalerError.fromException(e)); }); }; useEffect(() => { - async function getExtendedPermValue(): Promise<void> { + async function getValue(): Promise<void> { const res = await wxApi.containsHeaderListener(); setEnabled(res.newValue); } - getExtendedPermValue(); + getValue(); }, []); return { value: enabled, @@ -45,7 +45,7 @@ export function useExtendedPermissions(): ToggleHandler { }; } -async function handleExtendedPerm( +async function handleAutoOpenPerm( isEnabled: boolean, onChange: (value: boolean) => void, ): Promise<void> { diff --git a/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts new file mode 100644 index 000000000..c69b116b7 --- /dev/null +++ b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts @@ -0,0 +1,73 @@ +/* + 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 { useState, useEffect } from "preact/hooks"; +import * as wxApi from "../wxApi.js"; +import { platform } from "../platform/api.js"; +import { ToggleHandler } from "../mui/handlers.js"; +import { TalerError } from "@gnu-taler/taler-wallet-core"; + +export function useClipboardPermissions(): ToggleHandler { + const [enabled, setEnabled] = useState(false); + const [error, setError] = useState<TalerError | undefined>(); + const toggle = async (): Promise<void> => { + return handleClipboardPerm(enabled, setEnabled).catch((e) => { + setError(TalerError.fromException(e)); + }); + }; + + useEffect(() => { + async function getValue(): Promise<void> { + const res = await wxApi.containsHeaderListener(); + setEnabled(res.newValue); + } + getValue(); + }, []); + + return { + value: enabled, + button: { + onClick: toggle, + error, + }, + }; +} + +async function handleClipboardPerm( + isEnabled: boolean, + onChange: (value: boolean) => void, +): Promise<void> { + if (!isEnabled) { + // We set permissions here, since apparently FF wants this to be done + // as the result of an input event ... + let granted: boolean; + try { + granted = await platform.getPermissionsApi().requestClipboardPermissions(); + } catch (lastError) { + onChange(false); + throw lastError; + } + // const res = await wxApi.toggleHeaderListener(granted); + onChange(granted); + } else { + try { + await wxApi.toggleHeaderListener(false).then((r) => onChange(r.newValue)); + } catch (e) { + console.log(e); + } + } + return; +} diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts index e1b08278b..75a92fd3c 100644 --- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts +++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts @@ -31,7 +31,6 @@ export function useTalerActionURL(): [ ); const [dismissed, setDismissed] = useState(false); const { findTalerUriInActiveTab, findTalerUriInClipboard } = useIocContext(); - useEffect(() => { async function check(): Promise<void> { const clipUri = await findTalerUriInClipboard(); @@ -52,7 +51,7 @@ export function useTalerActionURL(): [ } } check(); - }, [setTalerActionUrl]); + }, []); const url = dismissed ? undefined : talerActionUrl; return [url, setDismissed]; diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts index 23fd80ed7..da257c25f 100644 --- a/packages/taler-wallet-webextension/src/platform/api.ts +++ b/packages/taler-wallet-webextension/src/platform/api.ts @@ -37,6 +37,10 @@ export interface CrossBrowserPermissionsApi { requestHostPermissions(): Promise<boolean>; removeHostPermissions(): Promise<boolean>; + containsClipboardPermissions(): Promise<boolean>; + requestClipboardPermissions(): Promise<boolean>; + removeClipboardPermissions(): Promise<boolean>; + addPermissionsListener( callback: (p: Permissions, lastError?: string) => void, ): void; diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index 7311354c9..75900882f 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -77,6 +77,18 @@ const hostPermissions = { origins: ["http://*/*", "https://*/*"], }; +export function containsClipboardPermissions(): Promise<boolean> { + return new Promise((res, rej) => { + 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) => { @@ -89,6 +101,18 @@ export function containsHostPermissions(): Promise<boolean> { }); } +export async function requestClipboardPermissions(): Promise<boolean> { + return new Promise((res, rej) => { + 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) => { @@ -155,6 +179,18 @@ export async function removeHostPermissions(): Promise<boolean> { }); } +export function removeClipboardPermissions(): Promise<boolean> { + return new Promise((res, rej) => { + 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 { @@ -170,6 +206,9 @@ function getPermissionsApi(): CrossBrowserPermissionsApi { containsHostPermissions, requestHostPermissions, removeHostPermissions, + requestClipboardPermissions, + removeClipboardPermissions, + containsClipboardPermissions, }; } @@ -382,11 +421,9 @@ function registerTalerHeaderListener( } async function tabListener(tabId: number, info: chrome.tabs.TabChangeInfo): Promise<void> { - console.log("tab update", tabId, info) if (tabId < 0) return; if (info.status !== "complete") return; const uri = await findTalerUriInTab(tabId); - console.log("uri", uri) if (!uri) return; logger.info(`Found a Taler URI in the tab ${tabId}`) callback(tabId, uri) @@ -585,7 +622,6 @@ async function registerIconChangeOnTalerContent(): Promise<void> { chrome.tabs.onUpdated.addListener( async (tabId, info: chrome.tabs.TabChangeInfo) => { if (tabId < 0) return; - logger.info("tab updated", tabId, info); if (info.status !== "complete") return; const uri = await findTalerUriInTab(tabId); if (uri) { @@ -690,9 +726,22 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> { } } +async function timeout(ms: number): Promise<void> { + return new Promise(resolve => setTimeout(resolve, ms)); +} async function findTalerUriInClipboard(): Promise<string | undefined> { - const textInClipboard = await window.navigator.clipboard.readText(); - return textInClipboard.startsWith("taler://") || textInClipboard.startsWith("taler+http://") ? textInClipboard : undefined + 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 + } } async function findTalerUriInActiveTab(): Promise<string | undefined> { diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts index bb7e181c4..8a410b062 100644 --- a/packages/taler-wallet-webextension/src/platform/dev.ts +++ b/packages/taler-wallet-webextension/src/platform/dev.ts @@ -32,6 +32,9 @@ const api: PlatformAPI = { containsHostPermissions: async () => true, removeHostPermissions: async () => false, requestHostPermissions: async () => false, + containsClipboardPermissions: async () => true, + removeClipboardPermissions: async () => false, + requestClipboardPermissions: async () => false, }), getWalletWebExVersion: () => ({ version: "none", diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts index a56e21f24..943168956 100644 --- a/packages/taler-wallet-webextension/src/platform/firefox.ts +++ b/packages/taler-wallet-webextension/src/platform/firefox.ts @@ -16,9 +16,12 @@ import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js"; import chromePlatform, { - containsHostPermissions as chromeContains, - removeHostPermissions as chromeRemove, - requestHostPermissions as chromeRequest, + containsHostPermissions as chromeHostContains, + removeHostPermissions as chromeHostRemove, + requestHostPermissions as chromeHostRequest, + containsClipboardPermissions as chromeClipContains, + removeClipboardPermissions as chromeClipRemove, + requestClipboardPermissions as chromeClipRequest, } from "./chrome.js"; const api: PlatformAPI = { @@ -43,9 +46,12 @@ function addPermissionsListener(callback: (p: Permissions) => void): void { function getPermissionsApi(): CrossBrowserPermissionsApi { return { addPermissionsListener, - containsHostPermissions: chromeContains, - requestHostPermissions: chromeRequest, - removeHostPermissions: chromeRemove, + containsHostPermissions: chromeHostContains, + requestHostPermissions: chromeHostRequest, + removeHostPermissions: chromeHostRemove, + containsClipboardPermissions: chromeClipContains, + removeClipboardPermissions: chromeClipRemove, + requestClipboardPermissions: chromeClipRequest, }; } diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx index 9e85a9bed..d0707952f 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx @@ -46,21 +46,24 @@ const version = { export const AllOff = createExample(TestedComponent, { deviceName: "this-is-the-device-name", - permissionToggle: { value: false, button: {} }, + autoOpenToggle: { value: false, button: {} }, + clipboardToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), ...version, }); export const OneChecked = createExample(TestedComponent, { deviceName: "this-is-the-device-name", - permissionToggle: { value: false, button: {} }, + autoOpenToggle: { value: false, button: {} }, + clipboardToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), ...version, }); export const WithOneExchange = createExample(TestedComponent, { deviceName: "this-is-the-device-name", - permissionToggle: { value: false, button: {} }, + autoOpenToggle: { value: false, button: {} }, + clipboardToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), knownExchanges: [ { @@ -80,7 +83,8 @@ export const WithOneExchange = createExample(TestedComponent, { export const WithExchangeInDifferentState = createExample(TestedComponent, { deviceName: "this-is-the-device-name", - permissionToggle: { value: false, button: {} }, + autoOpenToggle: { value: false, button: {} }, + clipboardToggle: { value: false, button: {} }, setDeviceName: () => Promise.resolve(), knownExchanges: [ { diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 4a520c3bb..5219bbb38 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -33,17 +33,19 @@ import { useDevContext } from "../context/devContext.js"; import { useTranslationContext } from "../context/translation.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js"; -import { useExtendedPermissions } from "../hooks/useExtendedPermissions.js"; +import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js"; import { ToggleHandler } from "../mui/handlers.js"; import { Pages } from "../NavigationBar.js"; import { buildTermsOfServiceStatus } from "../utils/index.js"; import * as wxApi from "../wxApi.js"; import { platform } from "../platform/api.js"; +import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; export function SettingsPage(): VNode { - const permissionToggle = useExtendedPermissions(); + const autoOpenToggle = useAutoOpenPermissions(); + const clipboardToggle = useClipboardPermissions(); const { devMode, toggleDevMode } = useDevContext(); const { name, update } = useBackupDeviceName(); const webex = platform.getWalletWebExVersion(); @@ -63,7 +65,8 @@ export function SettingsPage(): VNode { knownExchanges={exchanges} deviceName={name} setDeviceName={update} - permissionToggle={permissionToggle} + autoOpenToggle={autoOpenToggle} + clipboardToggle={clipboardToggle} developerMode={devMode} toggleDeveloperMode={toggleDevMode} webexVersion={{ @@ -78,7 +81,8 @@ export function SettingsPage(): VNode { export interface ViewProps { deviceName: string; setDeviceName: (s: string) => Promise<void>; - permissionToggle: ToggleHandler; + autoOpenToggle: ToggleHandler; + clipboardToggle: ToggleHandler; developerMode: boolean; toggleDeveloperMode: () => Promise<void>; knownExchanges: Array<ExchangeListItem>; @@ -91,7 +95,8 @@ export interface ViewProps { export function SettingsView({ knownExchanges, - permissionToggle, + autoOpenToggle, + clipboardToggle, developerMode, coreVersion, webexVersion, @@ -102,10 +107,16 @@ export function SettingsView({ return ( <Fragment> <section> - {permissionToggle.button.error && ( + {autoOpenToggle.button.error && ( <ErrorTalerOperation title={<i18n.Translate>Could not toggle auto-open</i18n.Translate>} - error={permissionToggle.button.error.errorDetail} + error={autoOpenToggle.button.error.errorDetail} + /> + )} + {clipboardToggle.button.error && ( + <ErrorTalerOperation + title={<i18n.Translate>Could not toggle clipboard</i18n.Translate>} + error={clipboardToggle.button.error.errorDetail} /> )} <SubTitle> @@ -117,15 +128,31 @@ export function SettingsView({ Automatically open wallet based on page content </i18n.Translate> } - name="perm" + name="autoOpen" + description={ + <i18n.Translate> + Enabling this option below will make using the wallet faster, but + requires more permissions from your browser. + </i18n.Translate> + } + enabled={autoOpenToggle.value!} + onToggle={autoOpenToggle.button.onClick!} + /> + <Checkbox + label={ + <i18n.Translate> + Automatically check clipboard for Taler URI + </i18n.Translate> + } + name="clipboard" description={ <i18n.Translate> Enabling this option below will make using the wallet faster, but requires more permissions from your browser. </i18n.Translate> } - enabled={permissionToggle.value!} - onToggle={permissionToggle.button.onClick!} + enabled={clipboardToggle.value!} + onToggle={clipboardToggle.button.onClick!} /> <SubTitle> diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx index 0f327640e..659a6c2cf 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -26,12 +26,12 @@ import { Checkbox } from "../components/Checkbox.js"; import { SubTitle, Title } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; import { useDiagnostics } from "../hooks/useDiagnostics.js"; -import { useExtendedPermissions } from "../hooks/useExtendedPermissions.js"; +import { useAutoOpenPermissions } from "../hooks/useAutoOpenPermissions.js"; import { ToggleHandler } from "../mui/handlers.js"; import { platform } from "../platform/api.js"; export function WelcomePage(): VNode { - const permissionToggle = useExtendedPermissions(); + const permissionToggle = useAutoOpenPermissions(); const [diagnostics, timedOut] = useDiagnostics(); return ( <View diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 0835aae12..60b250453 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -330,11 +330,21 @@ export async function wxMain(): Promise<void> { platform.registerAllIncomingConnections(); try { - platform.registerTalerHeaderListener(parseTalerUriAndRedirect); + platform.registerOnInstalled(() => { + platform.openWalletPage("/welcome"); + + // + try { + platform.registerTalerHeaderListener(parseTalerUriAndRedirect); + } catch (e) { + logger.error("could not register header listener", e); + } + }); } catch (e) { - logger.error("could not register header listener", e); + console.error(e); } + // On platforms that support it, also listen to external // modification of permissions. platform.getPermissionsApi().addPermissionsListener((perm, lastError) => { |