Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/components/common/ConnectWalletButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
} from '@/hooks/useWalletConnectionStallDetection';
import { useCopySuccessAnnouncement } from '@/hooks/useCopySuccessAnnouncement';
import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement';
import showToast from '@/utils/toast.util';
import { copyTextToClipboard } from '@/utils/clipboard.utils';

function ConnectWalletButton() {
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
Expand All @@ -37,12 +39,15 @@ function ConnectWalletButton() {
const handleCopyAddress = async () => {
if (!address) return;
try {
await navigator.clipboard.writeText(address);
await copyTextToClipboard(address);
announceCopySuccess('Wallet address copied.');
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
showToast.error(
'Could not copy the wallet address. Please copy it manually.'
);
}
};

Expand Down
22 changes: 12 additions & 10 deletions src/components/common/CopyField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useAutoSelectOnFocus } from '@/hooks/useAutoSelectOnFocus';
import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement';
import { useCopySuccessAnnouncement } from '@/hooks/useCopySuccessAnnouncement';
import showToast from '@/utils/toast.util';
import { copyTextToClipboard } from '@/utils/clipboard.utils';

interface CopyFieldProps {
value: string;
Expand All @@ -26,16 +27,17 @@ const CopyField: React.FC<CopyFieldProps> = ({
const { announcement, announceCopySuccess } = useCopySuccessAnnouncement();

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(value);
announceCopySuccess(`${label} copied.`);
showToast.success('Address copied to clipboard', { duration: 2000 });
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
}
};
try {
await copyTextToClipboard(value);
announceCopySuccess(`${label} copied.`);
showToast.success('Address copied to clipboard', { duration: 2000 });
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
showToast.error(`Could not copy ${label}. Please copy it manually.`);
}
};

return (
<div className={cn('flex items-center gap-2', className)}>
Expand Down
27 changes: 17 additions & 10 deletions src/components/common/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import { AsyncButton } from '@/components/ui/async-button';
import { useNetworkMismatch } from '@/hooks/useNetworkMismatch';
import { useTransactionTelemetry } from '@/hooks/useTransactionTelemetry';
import { copyTextToClipboard } from '@/utils/clipboard.utils';
import TransactionRetryNotice from '@/components/common/TransactionRetryNotice';
import TransactionFailureDrawer from '@/components/common/TransactionFailureDrawer';
import type { TransactionFailureDetails } from '@/components/common/TransactionFailureDrawer';
Expand Down Expand Up @@ -116,7 +117,7 @@
cardElement.addEventListener('keydown', handleKeyDown);
return () => cardElement.removeEventListener('keydown', handleKeyDown);
}
}, [isConnected, isNetworkMismatch, displayCreatorName]);

Check warning on line 120 in src/components/common/CreatorCard.tsx

View workflow job for this annotation

GitHub Actions / verify

React Hook useEffect has a missing dependency: 'handleBuy'. Either include it or remove the dependency array

const runPurchaseAttempt = () => {
setTransactionState('submitting');
Expand Down Expand Up @@ -170,23 +171,29 @@
const isRecentlyActive = (creator.volume24h ?? 0) > 0;
const keyPriceDisplay = formatCreatorKeyPriceDisplay(creator);

const handleCopyLink = () => {
const handleCopyLink = async () => {
const url = `${window.location.origin}/creator/${creator.id}`;
navigator.clipboard
.writeText(url)
.then(() => toast.success('Profile link copied'))
.catch(() => toast.error('Could not copy link'));
try {
await copyTextToClipboard(url);
toast.success('Profile link copied');
} catch {
toast.error('Could not copy the profile link. Please copy it manually.');
}
};

const handleShare = () => {
const handleShare = async () => {
const url = `${window.location.origin}/creator/${creator.id}`;
if (navigator.share) {
navigator.share({ title: displayCreatorName, url }).catch(() => {});
} else {
navigator.clipboard
.writeText(url)
.then(() => toast.success('Link copied to clipboard'))
.catch(() => toast.error('Could not share'));
try {
await copyTextToClipboard(url);
toast.success('Link copied to clipboard');
} catch {
toast.error(
'Could not copy the share link. Please copy it manually.'
);
}
}
};

Expand Down
7 changes: 5 additions & 2 deletions src/components/common/CreatorProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
import { Copy, Check, Share2 } from 'lucide-react';
import showToast from '@/utils/toast.util';
import appendUtmParams from '@/utils/utm.utils';
import { copyTextToClipboard } from '@/utils/clipboard.utils';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import VerifiedBadge from '@/components/common/VerifiedBadge';
Expand Down Expand Up @@ -73,12 +74,14 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({

// Fallback: copy to clipboard
try {
await navigator.clipboard.writeText(url);
await copyTextToClipboard(url);
setCopied(true);
showToast.success('Profile link copied to clipboard!');
setTimeout(() => setCopied(false), 2000);
} catch {
showToast.error('Failed to copy link');
showToast.error(
'Could not copy the profile link. Please copy it manually.'
);
}
};

Expand Down
7 changes: 6 additions & 1 deletion src/components/common/EmptyTransactionTimelineState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { formatRecentActivityCompactTimestamp } from '@/utils/recentActivityTime
import { groupEntriesByDate, formatDateHeader } from '@/utils/activityTimeline.utils';
import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement';
import { useCopySuccessAnnouncement } from '@/hooks/useCopySuccessAnnouncement';
import showToast from '@/utils/toast.util';
import { copyTextToClipboard } from '@/utils/clipboard.utils';

type CopyState = 'idle' | 'success' | 'error';

Expand Down Expand Up @@ -83,11 +85,14 @@ const EmptyTransactionTimelineState: React.FC<

const copyTxHash = async (entryId: string, txHash: string) => {
try {
await navigator.clipboard.writeText(txHash);
await copyTextToClipboard(txHash);
announceCopySuccess('Transaction hash copied.');
setCopyStateById(current => ({ ...current, [entryId]: 'success' }));
} catch {
setCopyStateById(current => ({ ...current, [entryId]: 'error' }));
showToast.error(
'Could not copy the transaction hash. Please copy it manually.'
);
}

window.setTimeout(() => {
Expand Down
9 changes: 7 additions & 2 deletions src/components/common/TransactionFailureDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button';
import { AlertCircle, Copy, Check } from 'lucide-react';
import showToast from '@/utils/toast.util';
import { formatTimestampTooltip } from '@/utils/time.utils';
import { copyTextToClipboard } from '@/utils/clipboard.utils';
import CopySuccessAnnouncement from '@/components/common/CopySuccessAnnouncement';
import {
COPY_SUCCESS_TOAST_ARIA_PROPS,
Expand Down Expand Up @@ -50,7 +51,7 @@ const TransactionFailureDrawer: React.FC<TransactionFailureDrawerProps> = ({
field: 'errorCode' | 'txHash'
) => {
try {
await navigator.clipboard.writeText(text);
await copyTextToClipboard(text);
showToast.success('Copied to clipboard', {
ariaProps: COPY_SUCCESS_TOAST_ARIA_PROPS,
});
Expand All @@ -62,7 +63,11 @@ const TransactionFailureDrawer: React.FC<TransactionFailureDrawerProps> = ({
setCopiedField(field);
window.setTimeout(() => setCopiedField(null), 2000);
} catch {
showToast.error('Failed to copy to clipboard');
showToast.error(
field === 'errorCode'
? 'Could not copy the error code. Please copy it manually.'
: 'Could not copy the transaction hash. Please copy it manually.'
);
}
};

Expand Down
68 changes: 68 additions & 0 deletions src/utils/clipboard.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const createClipboardUnavailableError = () =>
new Error('Clipboard API is unavailable in this environment.');

const copyWithExecCommandFallback = (text: string): boolean => {
if (
typeof document === 'undefined' ||
!document.body ||
typeof document.execCommand !== 'function'
) {
return false;
}

const activeElement =
document.activeElement instanceof HTMLElement
? document.activeElement
: null;
const textArea = document.createElement('textarea');

textArea.value = text;
textArea.setAttribute('readonly', '');
textArea.setAttribute('aria-hidden', 'true');
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.opacity = '0';
textArea.style.pointerEvents = 'none';

document.body.appendChild(textArea);
textArea.focus();
textArea.select();
textArea.setSelectionRange(0, text.length);

try {
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
activeElement?.focus();
}
};

export const copyTextToClipboard = async (text: string): Promise<void> => {
let originalError: unknown;

try {
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
throw createClipboardUnavailableError();
}

await navigator.clipboard.writeText(text);
return;
} catch (error) {
originalError = error;
console.error(
'Clipboard API copy failed. Attempting execCommand fallback.',
error
);
}

if (copyWithExecCommandFallback(text)) {
return;
}

if (originalError instanceof Error) {
throw originalError;
}

throw new Error('Copy to clipboard failed.');
};
Loading