Представим, что есть реализованная функция для копирования какого-то текста (например, промокода) в буфер обмена:

export async function copyToClipboard(textToCopy: string): Promise<string> {
	await navigator.clipboard.writeText(textToCopy);
 
	return textToCopy;
}

Казалось бы, всё хорошо, на локальном развёртывании функция работает, а, значит, фича готова к отгрузке. Версия фронтенда с этой функцией ставится на стенд, доступ к которому, например, осуществляется только по протоколу HTTP. Довольные, мы накатываем изменения, открываем наше веб-приложение, нажимаем на заветную кнопку, использующую функцию копирования, — и ничего не происходит.

Почему?

Первая причина (и самая частая) — браузер не даёт доступ к буферу обмена в небезопасном контексте. Разработчиков браузеров можно понять, так как такая операция на сайтах без SSL-сертификатов может быть потенциально опасной и позволить скопировать массу вредоносных вещей.

Почему это работало на локалке?

Моё предположение заключается в смягчении ограничений на доступ к буферу обмена для веб-страниц на ряде зарезервированных хостов на ПК разработчика / пользователя (например, localhost, 127.0.0.1 итд).

Как решить проблему

  1. Главное решение — стараться использовать HTTPS на вашем стенде или боевом сайте. Здесь всё очевидно, не даём браузеру повод думать, что мы скамеры или мошенники;
  2. Если нет нужды/возможности положить сайт под HTTPS: есть 2 способа обойти ограничение:
    1. Первый способ предполагает включение экспериментального флага, позволяющего давать больше доступов для сайтов с небезопасным контекстом. Этим можно воспользоваться если браузер хромообразный, на Firefox и форках такая функция может отсутствовать;
    2. Самый верный способ — обход использования navigator.clipboard в качестве fallback-действия. В таком случае нужно иметь скрытый элемент input или textarea, для которого можно будет применить выделение для последующего копирования и использовать более глубинный механизм (да, тот самый, что работает при выделении текста, нажатии ПКМ и в появившемся контекстном меню «Копировать»). Переписав функцию с учётом всех нюансов, мы бы получили что-то вроде:
export async function copyToClipboard(textToCopy: string): Promise<string> {
	if (navigator.clipboard && window.isSecureContext) {
		await navigator.clipboard.writeText(textToCopy);
 
		return textToCopy;
	}
 
	// Creating textarea
	const textarea = document.createElement('textarea');
	textarea.value = textToCopy;
 
	// Hide textarea
	textarea.style.position = 'absolute';
	textarea.style.left = '-99999999px';
 
	document.body.prepend(textarea);
 
	// Highlight contents of the textarea
	textarea.select();
 
	// Now copy
	try {
		document.execCommand('copy');
	} catch (err) {
		console.warn('Copy error', err);
	} finally {
		textarea.remove();
	}
 
	return textToCopy;
}