fix(ui): report clipboard write failures instead of rejecting (#50292)

Closes DOCS-1390

## Problem

Sentry [DOCS-AA](https://supabase.sentry.io/issues/7727380816/) reports
`NotAllowedError: Failed to execute 'write' on 'Clipboard': Write
permission denied.` as an unhandled promise rejection.

The error names `write`, not `writeText`, which places it in the
`ClipboardItem` branch of `copyToClipboard`. That branch has two
problems:

- The write runs inside a `setTimeout`, so the surrounding `try/catch`
has already returned by the time it executes. A denied write routes to
the promise's `reject`.
- No caller attaches a `catch`. All call sites either fire-and-forget or
`await` inside an async handler with no `try/catch`, so the rejection
surfaces as an unhandled rejection.

The user-visible effect is worse than the Sentry noise. On that branch
the copy fails with no feedback at all, because the `toast.error` in the
outer `catch` is unreachable from inside the `setTimeout`. The
`writeText` branch does show the toast, so the two paths disagree.

The issue is filed against auth docs, where it surfaced, but the fix
belongs in `packages/ui`. The same branch runs in Studio and www.

## Solution

- Handle the failure inside the `setTimeout`, where it happens: report
it and resolve.
- `copyToClipboard` no longer rejects on either path, matching what the
`writeText` branch already did. No caller relied on rejection.
- Add regression tests for a denied write on both branches.

## Manual testing

1. Run the unit tests. Four `copyToClipboard` cases pass, including the
two new denial cases.

   ```
pnpm --filter studio exec vitest run lib/helpers.test.ts -t
copyToClipboard
   ```

2. Confirm the new test is a real guard. Revert `clipboard.ts` and
rerun. The write case fails with `promise rejected ... instead of
resolving`.
3. Confirm the ratchet is unchanged.

   ```
   pnpm --filter studio run lint:ratchet
   ```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Clipboard write failures now display an error notification instead of
causing an unhandled rejection.
* Copy operations resolve consistently when clipboard access is denied
or unavailable, including Safari clipboard support.
* Failed copy attempts no longer trigger completion callbacks,
preventing misleading success behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Miranda Limonczenko authored and GitHub committed 2026-09-16 19:01:21 +00:00
1 parent cf5bf65361
commit 91b7df64c2
2 files changed
+32 -14

No files matched your search

+3 -14
View File
@@ -30,20 +30,9 @@ export const copyToClipboard = async (str: ClipboardText, callback = noop) => {
'text/plain': Promise.resolve(str).then((text) => new Blob([text], { type: 'text/plain' })),
})
let resolve = () => {}
let reject = () => {}
const promise = new Promise<void>((res, rej) => {
resolve = res
reject = rej
})
// Safari also seems to require that the promise resolve soon after the
// clipboard write call, adding a setTimeout with 0 delay seems to work.
// Returning the promise to ensure the caller can await the clipboard
// copy operation intuitively.
setTimeout(() => {
navigator.clipboard.write([text]).then(callback).then(resolve).catch(reject)
}, 0)
return promise
await navigator.clipboard.write([text])
callback()
return
}
// NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,