Add permission check around clipboard access (#22781)

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2023-08-29 13:50:38 +02:00 committed by GitHub
parent 8e649b05c9
commit 82a94cb12b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 22 deletions

View file

@ -317,6 +317,7 @@
"copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.", "copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.",
"copySuccess": "Successfully copied to clipboard!", "copySuccess": "Successfully copied to clipboard!",
"clipboardCopyError": "Error copying to clipboard.", "clipboardCopyError": "Error copying to clipboard.",
"clipboardCopyDenied": "Your browser is blocking access to the clipboard.",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"clientRegistration": "Client registration", "clientRegistration": "Client registration",
"anonymousAccessPolicies": "Anonymous access polices", "anonymousAccessPolicies": "Anonymous access polices",

View file

@ -6,6 +6,7 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import useSetTimeout from "../../utils/useSetTimeout"; import useSetTimeout from "../../utils/useSetTimeout";
import useQueryPermission from "../../utils/useQueryPermission";
enum CopyState { enum CopyState {
Ready, Ready,
@ -27,32 +28,39 @@ export const CopyToClipboardButton = ({
}: CopyToClipboardButtonProps) => { }: CopyToClipboardButtonProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const setTimeout = useSetTimeout(); const setTimeout = useSetTimeout();
const permission = useQueryPermission("clipboard-write" as PermissionName);
const permissionDenied = permission?.state === "denied";
const [copyState, setCopyState] = useState(CopyState.Ready);
const [copy, setCopy] = useState(CopyState.Ready); // Determine the message to use for the copy button.
const copyMessageKey = useMemo(() => {
if (permissionDenied) {
return "clipboardCopyDenied";
}
const copyMessage = useMemo(() => { switch (copyState) {
switch (copy) {
case CopyState.Ready: case CopyState.Ready:
return t("copyToClipboard"); return "copyToClipboard";
case CopyState.Copied: case CopyState.Copied:
return t("copySuccess"); return "copySuccess";
case CopyState.Error: case CopyState.Error:
return t("clipboardCopyError"); return "clipboardCopyError";
} }
}, [copy]); }, [permissionDenied, copyState]);
// Reset the message of the copy button after copying to the clipboard.
useEffect(() => { useEffect(() => {
if (copy !== CopyState.Ready) { if (copyState !== CopyState.Ready) {
return setTimeout(() => setCopy(CopyState.Ready), 1000); return setTimeout(() => setCopyState(CopyState.Ready), 1000);
} }
}, [copy]); }, [copyState, setTimeout]);
const copyToClipboard = async (text: string) => { const copyToClipboard = async (text: string) => {
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
setCopy(CopyState.Copied); setCopyState(CopyState.Copied);
} catch (error) { } catch (error) {
setCopy(CopyState.Error); setCopyState(CopyState.Error);
} }
}; };
@ -65,7 +73,7 @@ export const CopyToClipboardButton = ({
exitDelay={600} exitDelay={600}
variant={variant} variant={variant}
> >
{copyMessage} {t(copyMessageKey)}
</ClipboardCopyButton> </ClipboardCopyButton>
); );
}; };

View file

@ -0,0 +1,54 @@
import { useState, useEffect } from "react";
/** A 'plain' object version of the permission status. */
export type PlainPermissionStatus = {
readonly name: string;
readonly state: PermissionState;
};
export default function useQueryPermission(
name: PermissionName,
): PlainPermissionStatus | null {
const [status, setStatus] = useState<PermissionStatus | null>(null);
const [plainStatus, setPlainStatus] = useState<PlainPermissionStatus | null>(
null,
);
function updatePlainStatus(newStatus: PermissionStatus) {
setPlainStatus({
name: newStatus.name,
state: newStatus.state,
});
}
// Query the permission status when the name changes.
useEffect(() => {
setStatus(null);
setPlainStatus(null);
navigator.permissions.query({ name }).then((newStatus) => {
setStatus(newStatus);
updatePlainStatus(newStatus);
});
}, [name]);
// Update the 'plain' status when the permission status changes.
useEffect(() => {
if (!status) {
return;
}
function onStatusChange() {
if (!status) {
return;
}
updatePlainStatus(status);
}
status.addEventListener("change", onStatusChange);
return () => status.removeEventListener("change", onStatusChange);
}, [status]);
return plainStatus;
}

View file

@ -1,8 +1,8 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useCallback } from "react";
export default function useSetTimeout() { export default function useSetTimeout() {
const didUnmountRef = useRef(false); const didUnmountRef = useRef(false);
const { current: scheduledTimers } = useRef(new Set<number>()); const scheduledTimersRef = useRef(new Set<number>());
useEffect(() => { useEffect(() => {
didUnmountRef.current = false; didUnmountRef.current = false;
@ -14,27 +14,27 @@ export default function useSetTimeout() {
}, []); }, []);
function clearAll() { function clearAll() {
scheduledTimers.forEach((timer) => clearTimeout(timer)); scheduledTimersRef.current.forEach((timer) => clearTimeout(timer));
scheduledTimers.clear(); scheduledTimersRef.current.clear();
} }
return function scheduleTimeout(callback: () => void, delay: number) { return useCallback((callback: () => void, delay: number) => {
if (didUnmountRef.current) { if (didUnmountRef.current) {
throw new Error("Can't schedule a timeout on an unmounted component."); throw new Error("Can't schedule a timeout on an unmounted component.");
} }
const timer = Number(setTimeout(handleCallback, delay)); const timer = Number(setTimeout(handleCallback, delay));
scheduledTimers.add(timer); scheduledTimersRef.current.add(timer);
function handleCallback() { function handleCallback() {
scheduledTimers.delete(timer); scheduledTimersRef.current.delete(timer);
callback(); callback();
} }
return function cancelTimeout() { return function cancelTimeout() {
clearTimeout(timer); clearTimeout(timer);
scheduledTimers.delete(timer); scheduledTimersRef.current.delete(timer);
};
}; };
}, []);
} }