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.

Copy link
Copy Markdown
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

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

i'm quite hesitant on this one, because i don't like the idea of entering that clipboard.write promise for every event on a webkit browser.

is it possible to refactor the places where we're returning set_clipboard from the backend to just not do that. we can say set_clipboard has to be directly attached to an event trigger and maybe even raise an error/warning if someone tries to use it from the backend?

the other option i was considering, but @adhami3310 didn't really like, is exposing this clipboard arming behavior as an event action (like debounce or throttle). then backend events that wanted clipboard permissions could set this as a kwarg on the rx.event decorator or pass it at the trigger site to opt-in to the clipboard arming. this kind of falls apart for chained events on the backend though because the clipboard opt-in event stuff would only work if that event handler is directly attached to a component's trigger.

i still think just warning when set_clipboard is used from the backend and maybe updating the docs to show examples of directly attaching it to a trigger is the simplest solution so we don't have to maintain hacks or try to break safari's security model for questionable gains.

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

2 participants