From d1166703185b59b2a9cdbeb8aaf5319153d47c2f Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 30 Nov 2025 20:03:07 +0100 Subject: [PATCH] fix: safe copy to clipboard --- .../common/components/copy-tag/CopyTag.tsx | 20 +++++++++++-------- .../src/common/utils/copyToClipboard.ts | 20 ++++++++++++++++--- .../src/features/sharing/GenerateLinkForm.tsx | 6 +++--- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/apps/client/src/common/components/copy-tag/CopyTag.tsx b/apps/client/src/common/components/copy-tag/CopyTag.tsx index 15cd21c51..9fd21dddc 100644 --- a/apps/client/src/common/components/copy-tag/CopyTag.tsx +++ b/apps/client/src/common/components/copy-tag/CopyTag.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, useRef, useState } from 'react'; import { IoCheckmark, IoCopy } from 'react-icons/io5'; -import copyToClipboard from '../../utils/copyToClipboard'; +import { copyToClipboard } from '../../utils/copyToClipboard'; import { cx } from '../../utils/styleUtils'; import Button from '../buttons/Button'; import IconButton from '../buttons/IconButton'; @@ -25,15 +25,19 @@ export default function CopyTag({ const [copied, setCopied] = useState(false); const timeoutRef = useRef(null); - const handleClick = () => { - copyToClipboard(copyValue); - setCopied(true); + const handleClick = async () => { + try { + await copyToClipboard(copyValue); + setCopied(true); - // reset copied state - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); + // reset copied state + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => setCopied(false), 2000); + } catch { + // ignore errors } - timeoutRef.current = setTimeout(() => setCopied(false), 2000); }; return ( diff --git a/apps/client/src/common/utils/copyToClipboard.ts b/apps/client/src/common/utils/copyToClipboard.ts index a7430690a..f1e13dcde 100644 --- a/apps/client/src/common/utils/copyToClipboard.ts +++ b/apps/client/src/common/utils/copyToClipboard.ts @@ -1,4 +1,18 @@ -// we need to this as a promise because safari -export default async function copyToClipboard(text: string) { - setTimeout(async () => await navigator.clipboard?.writeText(text)); +/** + * copy text to clipboard + * @throws if not supported or permission denied + */ +export async function copyToClipboard(text: string) { + await navigator.clipboard?.writeText(text); +} + +/** + * Copy to clipboard but safely ignore errors + */ +export async function safeCopyToClipboard(text: string): Promise { + try { + await copyToClipboard(text); + } catch { + // Silently ignore errors + } } diff --git a/apps/client/src/features/sharing/GenerateLinkForm.tsx b/apps/client/src/features/sharing/GenerateLinkForm.tsx index 3d74db2ea..df7c470a4 100644 --- a/apps/client/src/features/sharing/GenerateLinkForm.tsx +++ b/apps/client/src/features/sharing/GenerateLinkForm.tsx @@ -13,7 +13,7 @@ import Input from '../../common/components/input/input/Input'; import Select from '../../common/components/select/Select'; import Switch from '../../common/components/switch/Switch'; import { useUpdateUrlPreset } from '../../common/hooks-query/useUrlPresets'; -import copyToClipboard from '../../common/utils/copyToClipboard'; +import { safeCopyToClipboard } from '../../common/utils/copyToClipboard'; import { preventEscape } from '../../common/utils/keyEvent'; import { isUrlSafe } from '../../common/utils/regex'; import { isOntimeCloud, serverURL } from '../../externals'; @@ -125,7 +125,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is lockNav: options.lockNav, preset: urlPreset.alias, }); - await copyToClipboard(url); + await safeCopyToClipboard(url); setUrl(url); } else { const presetPath = options.path.startsWith('preset-') ? options.path.replace('preset-', '') : undefined; @@ -143,7 +143,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is preset: presetPath, }); - await copyToClipboard(url); + await safeCopyToClipboard(url); setUrl(url); } reset(options, {