Handling programmatic "Copy to Clipboard" on DuckDuckGo Android browser

How to use the execCommand workaround to bypass the NotAllowedError on DuckDuckGo Android browser when using the async Clipboard API

ยท

4 min read

The background

This is going to be a short article. Recently I launched an in-browser daily puzzle game (GoldRoad). Later on, I added the ability to share your stats for the day on social media using the Web Share API. This is how it looks on my Android phone

But sharing menu may not be available everywhere, considering this is a web app which can be opened on laptops as well. So I also added a fallback to copy the stats to the device's clipboard, feeling proud & clever at the same time having thought of fallbacks and all.

And then one of my friends who plays the game religiously informs me that this share button doesn't do anything for him. Knowing him, I asked which browser are you using, and he says DuckDuckGo on Android. And believe me, all my smugness is gone now :-)

The original code

const shareStats = async () => {
  const text = 'The text to share';

  if (window.navigator.share) {
    try {
      await window.navigator.share({
        text,
      });
    } catch (error) {}
  } else {
    await window.navigator.clipboard.writeText(text);
  }
};

I had used the `writeText` method of the Clipboard API thinking it has wide availability (as can be seen here and in the below image) so it should work in almost any browser, and also because "The "clipboard-write" permission of the Permissions API is granted automatically to pages when they are in the active tab". There was one more caveat I found, "the Clipboard API is only supported for pages served over HTTPS" (which is true anyway in my case).

clipboard api support on major browsers

writeText support on major browsers

The issues with DuckDuckGo (DDG)

After doing some debugging my investigations revealed the following

  1. Navigator.share is not available on DDG

  2. Navigator.clipboard.writeText throws NotAllowedError with a message saying Write permission denied

  3. Since write permission was denied, so maybe somehow we could query and ask for the needed permission using the Permissions API? Alas! Navigator.permissions is not available on DDG

  4. Tried adding the clipboardWrite permission to the manifest.json file (though that is applicable for a web extension only), of course, it didn't work

Workaround with execCommand

Since all my trials with valid methods failed to yield results, I had to fall back to the document.execCommand. As shown in the linked article, you could use the below code to copy text to the device clipboard.

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

But there is a twist here as well, if you set the input element's display to none as shown above, then execCommand returns true but doesn't copy anything to the clipboard in the case of DDG (haven't tried it with other browsers, as for everyone else Clipboard API is working fine, at least for the latest versions where I tested)

Final Code

So without further ado, here is my final code which is working fine on DDG (sometimes I do see the phone's keyboard popping up for a split second, but that is fine I think).

const shareStats = async () => {
  const text = `The text to share\nWith multiple lines`;

  if (window.navigator.share) {
    try {
      await window.navigator.share({
        text,
      });
    } catch (error) {}

    return;
  }

  await copyToClipboard(text);
};

const copyToClipboard = async (text) => {
  if (window.navigator.clipboard) {
    try {
      await window.navigator.clipboard.writeText(text);
      return;
    } catch (error) {}
  }

  const textarea = document.createElement('textarea');
  textarea.style.position = 'fixed';
  textarea.style.width = '1px';
  textarea.style.height = '1px';
  textarea.style.padding = 0;
  textarea.style.border = 'none';
  textarea.style.outline = 'none';
  textarea.style.boxShadow = 'none';
  textarea.style.background = 'transparent';

  document.body.appendChild(textarea);

  textarea.textContent = text;
  textarea.focus();
  textarea.select();

  const result = document.execCommand('copy');
  textarea.remove();
  if (!result) {
    // Show some error message to the user
  } else {
    // Show a success message to the user mentioning the text is copied to their clipboard
  }
};

The only thing to note above is the use of a textarea instead of an input, because if our text is multiline then the input will eat away all our newlines. Also, since we're not hiding the textarea, we are adding some styles to make it inconsequential.

Conclusion

execCommand seems to be working at this point, but it has been made obsolete and may go away in future versions of all browsers. Just to be on the safer side, it is better to surround the execCommand call with a try-catch block. Hopefully in the future DDG will allow us to copy text using the Clipboard API itself.

Do let me know in the comments section if you spot an error, or if the explanation is wrong anywhere.

Thanks for reading :-)

ย