add clipboard perms

This commit is contained in:
Sebastian 2022-09-12 14:28:53 -03:00
parent 27201416c7
commit ad63d4c0e1
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
14 changed files with 217 additions and 48 deletions

View File

@ -24,7 +24,8 @@
"optional_permissions": [ "optional_permissions": [
"http://*/*", "http://*/*",
"https://*/*", "https://*/*",
"webRequest" "webRequest",
"clipboardRead"
], ],
"browser_action": { "browser_action": {
"default_icon": { "default_icon": {

View File

@ -27,7 +27,8 @@
} }
}, },
"optional_permissions": [ "optional_permissions": [
"webRequest" "webRequest",
"clipboardRead"
], ],
"host_permissions": [ "host_permissions": [
"http://*/*", "http://*/*",

View File

@ -42,14 +42,6 @@ if (isFirefox) {
setupPlatform(chromeAPI); setupPlatform(chromeAPI);
} }
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome");
});
} catch (e) {
console.error(e);
}
// setGlobalLogLevelFromString("trace") // setGlobalLogLevelFromString("trace")
platform.notifyWhenAppIsReady(() => { platform.notifyWhenAppIsReady(() => {
wxMain(); wxMain();

View File

@ -20,21 +20,21 @@ import { platform } from "../platform/api.js";
import { ToggleHandler } from "../mui/handlers.js"; import { ToggleHandler } from "../mui/handlers.js";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
export function useExtendedPermissions(): ToggleHandler { export function useAutoOpenPermissions(): ToggleHandler {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [error, setError] = useState<TalerError | undefined>(); const [error, setError] = useState<TalerError | undefined>();
const toggle = async (): Promise<void> => { const toggle = async (): Promise<void> => {
return handleExtendedPerm(enabled, setEnabled).catch((e) => { return handleAutoOpenPerm(enabled, setEnabled).catch((e) => {
setError(TalerError.fromException(e)); setError(TalerError.fromException(e));
}); });
}; };
useEffect(() => { useEffect(() => {
async function getExtendedPermValue(): Promise<void> { async function getValue(): Promise<void> {
const res = await wxApi.containsHeaderListener(); const res = await wxApi.containsHeaderListener();
setEnabled(res.newValue); setEnabled(res.newValue);
} }
getExtendedPermValue(); getValue();
}, []); }, []);
return { return {
value: enabled, value: enabled,
@ -45,7 +45,7 @@ export function useExtendedPermissions(): ToggleHandler {
}; };
} }
async function handleExtendedPerm( async function handleAutoOpenPerm(
isEnabled: boolean, isEnabled: boolean,
onChange: (value: boolean) => void, onChange: (value: boolean) => void,
): Promise<void> { ): Promise<void> {

View File

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

View File

@ -31,7 +31,6 @@ export function useTalerActionURL(): [
); );
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
const { findTalerUriInActiveTab, findTalerUriInClipboard } = useIocContext(); const { findTalerUriInActiveTab, findTalerUriInClipboard } = useIocContext();
useEffect(() => { useEffect(() => {
async function check(): Promise<void> { async function check(): Promise<void> {
const clipUri = await findTalerUriInClipboard(); const clipUri = await findTalerUriInClipboard();
@ -52,7 +51,7 @@ export function useTalerActionURL(): [
} }
} }
check(); check();
}, [setTalerActionUrl]); }, []);
const url = dismissed ? undefined : talerActionUrl; const url = dismissed ? undefined : talerActionUrl;
return [url, setDismissed]; return [url, setDismissed];

View File

@ -37,6 +37,10 @@ export interface CrossBrowserPermissionsApi {
requestHostPermissions(): Promise<boolean>; requestHostPermissions(): Promise<boolean>;
removeHostPermissions(): Promise<boolean>; removeHostPermissions(): Promise<boolean>;
containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>;
removeClipboardPermissions(): Promise<boolean>;
addPermissionsListener( addPermissionsListener(
callback: (p: Permissions, lastError?: string) => void, callback: (p: Permissions, lastError?: string) => void,
): void; ): void;

View File

@ -77,6 +77,18 @@ const hostPermissions = {
origins: ["http://*/*", "https://*/*"], 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> { export function containsHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
chrome.permissions.contains(hostPermissions, (resp) => { 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> { export async function requestHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
chrome.permissions.request(hostPermissions, (resp) => { 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( function addPermissionsListener(
callback: (p: Permissions, lastError?: string) => void, callback: (p: Permissions, lastError?: string) => void,
): void { ): void {
@ -170,6 +206,9 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
containsHostPermissions, containsHostPermissions,
requestHostPermissions, requestHostPermissions,
removeHostPermissions, removeHostPermissions,
requestClipboardPermissions,
removeClipboardPermissions,
containsClipboardPermissions,
}; };
} }
@ -382,11 +421,9 @@ function registerTalerHeaderListener(
} }
async function tabListener(tabId: number, info: chrome.tabs.TabChangeInfo): Promise<void> { async function tabListener(tabId: number, info: chrome.tabs.TabChangeInfo): Promise<void> {
console.log("tab update", tabId, info)
if (tabId < 0) return; if (tabId < 0) return;
if (info.status !== "complete") return; if (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId); const uri = await findTalerUriInTab(tabId);
console.log("uri", uri)
if (!uri) return; if (!uri) return;
logger.info(`Found a Taler URI in the tab ${tabId}`) logger.info(`Found a Taler URI in the tab ${tabId}`)
callback(tabId, uri) callback(tabId, uri)
@ -585,7 +622,6 @@ async function registerIconChangeOnTalerContent(): Promise<void> {
chrome.tabs.onUpdated.addListener( chrome.tabs.onUpdated.addListener(
async (tabId, info: chrome.tabs.TabChangeInfo) => { async (tabId, info: chrome.tabs.TabChangeInfo) => {
if (tabId < 0) return; if (tabId < 0) return;
logger.info("tab updated", tabId, info);
if (info.status !== "complete") return; if (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId); const uri = await findTalerUriInTab(tabId);
if (uri) { 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> { async function findTalerUriInClipboard(): Promise<string | undefined> {
const textInClipboard = await window.navigator.clipboard.readText(); 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 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> { async function findTalerUriInActiveTab(): Promise<string | undefined> {

View File

@ -32,6 +32,9 @@ const api: PlatformAPI = {
containsHostPermissions: async () => true, containsHostPermissions: async () => true,
removeHostPermissions: async () => false, removeHostPermissions: async () => false,
requestHostPermissions: async () => false, requestHostPermissions: async () => false,
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
requestClipboardPermissions: async () => false,
}), }),
getWalletWebExVersion: () => ({ getWalletWebExVersion: () => ({
version: "none", version: "none",

View File

@ -16,9 +16,12 @@
import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js"; import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js";
import chromePlatform, { import chromePlatform, {
containsHostPermissions as chromeContains, containsHostPermissions as chromeHostContains,
removeHostPermissions as chromeRemove, removeHostPermissions as chromeHostRemove,
requestHostPermissions as chromeRequest, requestHostPermissions as chromeHostRequest,
containsClipboardPermissions as chromeClipContains,
removeClipboardPermissions as chromeClipRemove,
requestClipboardPermissions as chromeClipRequest,
} from "./chrome.js"; } from "./chrome.js";
const api: PlatformAPI = { const api: PlatformAPI = {
@ -43,9 +46,12 @@ function addPermissionsListener(callback: (p: Permissions) => void): void {
function getPermissionsApi(): CrossBrowserPermissionsApi { function getPermissionsApi(): CrossBrowserPermissionsApi {
return { return {
addPermissionsListener, addPermissionsListener,
containsHostPermissions: chromeContains, containsHostPermissions: chromeHostContains,
requestHostPermissions: chromeRequest, requestHostPermissions: chromeHostRequest,
removeHostPermissions: chromeRemove, removeHostPermissions: chromeHostRemove,
containsClipboardPermissions: chromeClipContains,
removeClipboardPermissions: chromeClipRemove,
requestClipboardPermissions: chromeClipRequest,
}; };
} }

View File

@ -46,21 +46,24 @@ const version = {
export const AllOff = createExample(TestedComponent, { export const AllOff = createExample(TestedComponent, {
deviceName: "this-is-the-device-name", deviceName: "this-is-the-device-name",
permissionToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),
...version, ...version,
}); });
export const OneChecked = createExample(TestedComponent, { export const OneChecked = createExample(TestedComponent, {
deviceName: "this-is-the-device-name", deviceName: "this-is-the-device-name",
permissionToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),
...version, ...version,
}); });
export const WithOneExchange = createExample(TestedComponent, { export const WithOneExchange = createExample(TestedComponent, {
deviceName: "this-is-the-device-name", deviceName: "this-is-the-device-name",
permissionToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),
knownExchanges: [ knownExchanges: [
{ {
@ -80,7 +83,8 @@ export const WithOneExchange = createExample(TestedComponent, {
export const WithExchangeInDifferentState = createExample(TestedComponent, { export const WithExchangeInDifferentState = createExample(TestedComponent, {
deviceName: "this-is-the-device-name", deviceName: "this-is-the-device-name",
permissionToggle: { value: false, button: {} }, autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),
knownExchanges: [ knownExchanges: [
{ {

View File

@ -33,17 +33,19 @@ import { useDevContext } from "../context/devContext.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { useBackupDeviceName } from "../hooks/useBackupDeviceName.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 { ToggleHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { buildTermsOfServiceStatus } from "../utils/index.js"; import { buildTermsOfServiceStatus } from "../utils/index.js";
import * as wxApi from "../wxApi.js"; import * as wxApi from "../wxApi.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
export function SettingsPage(): VNode { export function SettingsPage(): VNode {
const permissionToggle = useExtendedPermissions(); const autoOpenToggle = useAutoOpenPermissions();
const clipboardToggle = useClipboardPermissions();
const { devMode, toggleDevMode } = useDevContext(); const { devMode, toggleDevMode } = useDevContext();
const { name, update } = useBackupDeviceName(); const { name, update } = useBackupDeviceName();
const webex = platform.getWalletWebExVersion(); const webex = platform.getWalletWebExVersion();
@ -63,7 +65,8 @@ export function SettingsPage(): VNode {
knownExchanges={exchanges} knownExchanges={exchanges}
deviceName={name} deviceName={name}
setDeviceName={update} setDeviceName={update}
permissionToggle={permissionToggle} autoOpenToggle={autoOpenToggle}
clipboardToggle={clipboardToggle}
developerMode={devMode} developerMode={devMode}
toggleDeveloperMode={toggleDevMode} toggleDeveloperMode={toggleDevMode}
webexVersion={{ webexVersion={{
@ -78,7 +81,8 @@ export function SettingsPage(): VNode {
export interface ViewProps { export interface ViewProps {
deviceName: string; deviceName: string;
setDeviceName: (s: string) => Promise<void>; setDeviceName: (s: string) => Promise<void>;
permissionToggle: ToggleHandler; autoOpenToggle: ToggleHandler;
clipboardToggle: ToggleHandler;
developerMode: boolean; developerMode: boolean;
toggleDeveloperMode: () => Promise<void>; toggleDeveloperMode: () => Promise<void>;
knownExchanges: Array<ExchangeListItem>; knownExchanges: Array<ExchangeListItem>;
@ -91,7 +95,8 @@ export interface ViewProps {
export function SettingsView({ export function SettingsView({
knownExchanges, knownExchanges,
permissionToggle, autoOpenToggle,
clipboardToggle,
developerMode, developerMode,
coreVersion, coreVersion,
webexVersion, webexVersion,
@ -102,10 +107,16 @@ export function SettingsView({
return ( return (
<Fragment> <Fragment>
<section> <section>
{permissionToggle.button.error && ( {autoOpenToggle.button.error && (
<ErrorTalerOperation <ErrorTalerOperation
title={<i18n.Translate>Could not toggle auto-open</i18n.Translate>} 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> <SubTitle>
@ -117,15 +128,31 @@ export function SettingsView({
Automatically open wallet based on page content Automatically open wallet based on page content
</i18n.Translate> </i18n.Translate>
} }
name="perm" name="autoOpen"
description={ description={
<i18n.Translate> <i18n.Translate>
Enabling this option below will make using the wallet faster, but Enabling this option below will make using the wallet faster, but
requires more permissions from your browser. requires more permissions from your browser.
</i18n.Translate> </i18n.Translate>
} }
enabled={permissionToggle.value!} enabled={autoOpenToggle.value!}
onToggle={permissionToggle.button.onClick!} 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={clipboardToggle.value!}
onToggle={clipboardToggle.button.onClick!}
/> />
<SubTitle> <SubTitle>

View File

@ -26,12 +26,12 @@ import { Checkbox } from "../components/Checkbox.js";
import { SubTitle, Title } from "../components/styled/index.js"; import { SubTitle, Title } from "../components/styled/index.js";
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { useDiagnostics } from "../hooks/useDiagnostics.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 { ToggleHandler } from "../mui/handlers.js";
import { platform } from "../platform/api.js"; import { platform } from "../platform/api.js";
export function WelcomePage(): VNode { export function WelcomePage(): VNode {
const permissionToggle = useExtendedPermissions(); const permissionToggle = useAutoOpenPermissions();
const [diagnostics, timedOut] = useDiagnostics(); const [diagnostics, timedOut] = useDiagnostics();
return ( return (
<View <View

View File

@ -329,11 +329,21 @@ export async function wxMain(): Promise<void> {
platform.registerAllIncomingConnections(); platform.registerAllIncomingConnections();
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome");
//
try { try {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect); platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
} catch (e) { } catch (e) {
logger.error("could not register header listener", e); logger.error("could not register header listener", e);
} }
});
} catch (e) {
console.error(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.