Skip to content

fix rx.set_clipboard from a backend event handler on Safari/iOS#6595

Open
carlosabadia wants to merge 2 commits into
mainfrom
carlos/safari-copy-fix
Open

fix rx.set_clipboard from a backend event handler on Safari/iOS#6595
carlosabadia wants to merge 2 commits into
mainfrom
carlos/safari-copy-fix

Conversation

@carlosabadia
Copy link
Copy Markdown
Contributor

closes #6583

@carlosabadia carlosabadia requested a review from a team as a code owner June 2, 2026 09:56
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jun 2, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing carlos/safari-copy-fix (1d60652) with main (dc9bfad)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR fixes rx.set_clipboard when called from a backend event handler on Safari/iOS. WebKit requires navigator.clipboard.writeText to be invoked within the browser's user-activation window; a backend round-trip over WebSocket expires that window, so the direct writeText call was silently failing.

  • set_clipboard is converted from run_script (inline writeText) to a server_side _set_clipboard event whose payload carries the content.
  • A new armClipboard helper is called inside addEvents on WebKit browsers; it synchronously starts a navigator.clipboard.write() backed by a deferred Promise during the active gesture, which is resolved when the backend's _set_clipboard event arrives, completing the write within the original activation window.
  • The error-boundary "Copy" button is updated from _call_function/writeText to the new _set_clipboard event, making it consistent and gaining the WebKit fix for free. Both unit and integration tests cover the non-WebKit fallback path and the WebKit arm-and-resolve path.

Confidence Score: 4/5

The change is well-scoped and correctly handles both WebKit and non-WebKit paths. The one area to watch is the unconditional clipboard-API call on every Safari user gesture.

The core logic — arm during gesture, resolve on event, fall back to writeText elsewhere — is correct and well-tested. The only non-trivial concern is that armClipboard fires for every addEvents call on WebKit, issuing navigator.clipboard.write() even when the event batch has nothing to do with the clipboard. Rejected writes leave the clipboard untouched, but some iOS versions may surface a transient clipboard indicator during the pending window, which would be visible on every user interaction on Safari.

The armClipboard call site in useEventLoop inside state.js deserves a second look to confirm the arm-every-gesture approach is acceptable across all iOS clipboard UI behaviour.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Core of the fix: adds isWebKit, armClipboard, and a _set_clipboard branch in applyEvent. armClipboard is called on every addEvents on Safari, arming a deferred ClipboardItem write inside the user gesture window that resolves when the backend event arrives.
packages/reflex-base/src/reflex_base/event/init.py Converts set_clipboard from run_script (direct writeText call) to a server_side _set_clipboard event so the frontend can handle the WebKit deferred-write workaround. The change is minimal and correct.
tests/integration/test_server_side_event.py Adds integration tests for both the non-WebKit writeText fallback path and the WebKit arm-and-resolve path by spoofing the user agent and stubbing ClipboardItem/clipboard.write.
tests/units/test_app.py Snapshot tests updated to reflect the error-boundary Copy button now emitting _set_clipboard instead of _call_function with an inline writeText call.
tests/units/test_event.py New unit test verifies set_clipboard emits a _set_clipboard EventSpec with the correct payload for both string literals and Var expressions.

Reviews (1): Last reviewed commit: "add new" | Re-trigger Greptile

Comment on lines +1025 to +1030
if (isWebKit()) {
// Arm a clipboard write inside the active user gesture so set_clipboard
// returned from a backend handler still works on WebKit. No-op unless a
// user activation is in effect, so programmatic dispatches are unaffected.
armClipboard();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 armClipboard fires on every user gesture on WebKit, not just clipboard ones

addEvents calls armClipboard() unconditionally for every event on Safari/iOS (guarded only by isWebKit()). Any user interaction — typing in a form field, navigating, clicking unrelated buttons — that has an active userActivation will trigger navigator.clipboard.write() with a pending promise. When no _set_clipboard event follows, that write is rejected and .catch(() => {}) silently swallows it, so the clipboard is never modified. However, some WebKit/iOS versions surface a transient clipboard-access indicator even for in-flight writes that ultimately fail, which could be disorienting to users clicking unrelated elements. Consider checking whether the outgoing event batch actually contains or is likely to produce a _set_clipboard response before arming — or document that the arm-for-every-gesture trade-off was explicitly evaluated against iOS clipboard UI behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rx.set_clipboard from a backend event handler fails on Safari/iOS with NotAllowedError

1 participant