import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createShareUrl, type CreateShareUrlOptions, type ShareViewState, } from '../utils/shareState'; export type CopyShareLinkStatus = 'idle' | 'copying' | 'copied' | 'error'; export type CopyShareLinkMethod = 'clipboard' | 'execCommand' | 'unsupported'; export interface ClipboardLike { writeText(text: string): Promise; } export interface CopyShareLinkOptions { url?: string; urlOptions?: CreateShareUrlOptions; clipboard?: ClipboardLike | null; document?: Document; preferClipboard?: boolean; preferLegacyFallback?: boolean; } export interface CopyShareLinkResult { ok: boolean; url: string; method: CopyShareLinkMethod; error?: unknown; } export interface UseShareLinkOptions extends CopyShareLinkOptions { resetAfterMs?: number; } export interface UseShareLinkResult { url: string; status: CopyShareLinkStatus; error: unknown; copy: () => Promise; } export async function copyShareLink( state: ShareViewState, options: CopyShareLinkOptions = {}, ): Promise { const url = options.url ?? createShareUrl(state, options.urlOptions); const clipboard = options.clipboard ?? (typeof navigator !== 'undefined' && navigator.clipboard ? navigator.clipboard : undefined); let clipboardError: unknown; if (options.preferClipboard !== false && clipboard?.writeText) { try { await clipboard.writeText(url); return { ok: true, url, method: 'clipboard', }; } catch (error) { clipboardError = error; } } if (options.preferLegacyFallback !== false) { const copiedWithFallback = copyTextWithLegacyTextarea(url, options.document); if (copiedWithFallback) { return { ok: true, url, method: 'execCommand', }; } } return { ok: false, url, method: 'unsupported', error: clipboardError ?? new Error('Clipboard API is unavailable and the legacy copy fallback failed.'), }; } export function useShareLink( state: ShareViewState, options: UseShareLinkOptions = {}, ): UseShareLinkResult { const [status, setStatus] = useState('idle'); const [error, setError] = useState(undefined); const timeoutRef = useRef(undefined); const mountedRef = useRef(true); const resetAfterMs = options.resetAfterMs ?? 1800; const urlOptionsKey = safeStringify(options.urlOptions); const url = useMemo( () => options.url ?? createShareUrl(state, options.urlOptions), [state, options.url, options.urlOptions, urlOptionsKey], ); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; if (timeoutRef.current !== undefined) { window.clearTimeout(timeoutRef.current); } }; }, []); const copy = useCallback(async () => { if (timeoutRef.current !== undefined) { window.clearTimeout(timeoutRef.current); timeoutRef.current = undefined; } setStatus('copying'); setError(undefined); const result = await copyShareLink(state, { url, clipboard: options.clipboard, document: options.document, preferClipboard: options.preferClipboard, preferLegacyFallback: options.preferLegacyFallback, }); if (!mountedRef.current) { return result; } if (result.ok) { setStatus('copied'); timeoutRef.current = window.setTimeout(() => { if (mountedRef.current) { setStatus('idle'); } }, resetAfterMs); } else { setStatus('error'); setError(result.error); } return result; }, [ options.clipboard, options.document, options.preferClipboard, options.preferLegacyFallback, resetAfterMs, state, url, ]); return { url, status, error, copy, }; } function copyTextWithLegacyTextarea(text: string, providedDocument?: Document): boolean { const activeDocument = providedDocument ?? (typeof document !== 'undefined' ? document : undefined); if (!activeDocument?.body || typeof activeDocument.execCommand !== 'function') { return false; } const textarea = activeDocument.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.setAttribute('aria-hidden', 'true'); textarea.style.position = 'fixed'; textarea.style.top = '0'; textarea.style.left = '-9999px'; textarea.style.opacity = '0'; const selection = activeDocument.getSelection(); const previousRange = selection && selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : undefined; activeDocument.body.appendChild(textarea); try { textarea.focus({ preventScroll: true }); textarea.select(); return activeDocument.execCommand('copy'); } catch { return false; } finally { textarea.remove(); if (selection) { selection.removeAllRanges(); if (previousRange) { selection.addRange(previousRange); } } } } function safeStringify(value: unknown): string { try { return JSON.stringify(value); } catch { return '[unserialisable]'; } }