Add permission check around clipboard access (#22781)
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
8e649b05c9
commit
82a94cb12b
4 changed files with 85 additions and 22 deletions
|
@ -317,6 +317,7 @@
|
|||
"copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.",
|
||||
"copySuccess": "Successfully copied to clipboard!",
|
||||
"clipboardCopyError": "Error copying to clipboard.",
|
||||
"clipboardCopyDenied": "Your browser is blocking access to the clipboard.",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"clientRegistration": "Client registration",
|
||||
"anonymousAccessPolicies": "Anonymous access polices",
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
|
||||
import useSetTimeout from "../../utils/useSetTimeout";
|
||||
import useQueryPermission from "../../utils/useQueryPermission";
|
||||
|
||||
enum CopyState {
|
||||
Ready,
|
||||
|
@ -27,32 +28,39 @@ export const CopyToClipboardButton = ({
|
|||
}: CopyToClipboardButtonProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
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 (copy) {
|
||||
switch (copyState) {
|
||||
case CopyState.Ready:
|
||||
return t("copyToClipboard");
|
||||
return "copyToClipboard";
|
||||
case CopyState.Copied:
|
||||
return t("copySuccess");
|
||||
return "copySuccess";
|
||||
case CopyState.Error:
|
||||
return t("clipboardCopyError");
|
||||
return "clipboardCopyError";
|
||||
}
|
||||
}, [copy]);
|
||||
}, [permissionDenied, copyState]);
|
||||
|
||||
// Reset the message of the copy button after copying to the clipboard.
|
||||
useEffect(() => {
|
||||
if (copy !== CopyState.Ready) {
|
||||
return setTimeout(() => setCopy(CopyState.Ready), 1000);
|
||||
if (copyState !== CopyState.Ready) {
|
||||
return setTimeout(() => setCopyState(CopyState.Ready), 1000);
|
||||
}
|
||||
}, [copy]);
|
||||
}, [copyState, setTimeout]);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopy(CopyState.Copied);
|
||||
setCopyState(CopyState.Copied);
|
||||
} catch (error) {
|
||||
setCopy(CopyState.Error);
|
||||
setCopyState(CopyState.Error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -65,7 +73,7 @@ export const CopyToClipboardButton = ({
|
|||
exitDelay={600}
|
||||
variant={variant}
|
||||
>
|
||||
{copyMessage}
|
||||
{t(copyMessageKey)}
|
||||
</ClipboardCopyButton>
|
||||
);
|
||||
};
|
||||
|
|
54
js/apps/admin-ui/src/utils/useQueryPermission.ts
Normal file
54
js/apps/admin-ui/src/utils/useQueryPermission.ts
Normal 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;
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
|
||||
export default function useSetTimeout() {
|
||||
const didUnmountRef = useRef(false);
|
||||
const { current: scheduledTimers } = useRef(new Set<number>());
|
||||
const scheduledTimersRef = useRef(new Set<number>());
|
||||
|
||||
useEffect(() => {
|
||||
didUnmountRef.current = false;
|
||||
|
@ -14,27 +14,27 @@ export default function useSetTimeout() {
|
|||
}, []);
|
||||
|
||||
function clearAll() {
|
||||
scheduledTimers.forEach((timer) => clearTimeout(timer));
|
||||
scheduledTimers.clear();
|
||||
scheduledTimersRef.current.forEach((timer) => clearTimeout(timer));
|
||||
scheduledTimersRef.current.clear();
|
||||
}
|
||||
|
||||
return function scheduleTimeout(callback: () => void, delay: number) {
|
||||
return useCallback((callback: () => void, delay: number) => {
|
||||
if (didUnmountRef.current) {
|
||||
throw new Error("Can't schedule a timeout on an unmounted component.");
|
||||
}
|
||||
|
||||
const timer = Number(setTimeout(handleCallback, delay));
|
||||
|
||||
scheduledTimers.add(timer);
|
||||
scheduledTimersRef.current.add(timer);
|
||||
|
||||
function handleCallback() {
|
||||
scheduledTimers.delete(timer);
|
||||
scheduledTimersRef.current.delete(timer);
|
||||
callback();
|
||||
}
|
||||
|
||||
return function cancelTimeout() {
|
||||
clearTimeout(timer);
|
||||
scheduledTimers.delete(timer);
|
||||
};
|
||||
scheduledTimersRef.current.delete(timer);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue