From c5ff613c5d844335e4d70f197c822d8216ea97fb Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Sun, 4 Oct 2026 05:23:16 -0700 Subject: [PATCH] feat: start the invite-link clipboard write inside the click so Safari keeps the gesture, say that earlier copied links stop working, and show the new link when the copy fails --- web/src/app/app/settings/members/page.tsx | 30 +++++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/web/src/app/app/settings/members/page.tsx b/web/src/app/app/settings/members/page.tsx index 3590d9eaa..7dd0a77cb 100644 --- a/web/src/app/app/settings/members/page.tsx +++ b/web/src/app/app/settings/members/page.tsx @@ -92,13 +92,33 @@ export default function MembersSettingsPage() { }); } async function copyInviteLink(invitationId: string) { + // Each copy mints a new link and retires the last, so the write starts inside the click: + // ClipboardItem takes the pending URL, which keeps Safari's user gesture. + const pending = getInvitationLink(invitationId).then( + ({ token }) => `${window.location.origin}/invite?token=${encodeURIComponent(token)}`, + ); + let url = ""; try { - const { token } = await getInvitationLink(invitationId); - const url = `${window.location.origin}/invite?token=${encodeURIComponent(token)}`; - await navigator.clipboard.writeText(url); - toast.success("Invite link copied"); + if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) { + await navigator.clipboard.write([ + new ClipboardItem({ + "text/plain": pending.then((u) => { + url = u; + return new Blob([u], { type: "text/plain" }); + }), + }), + ]); + } else { + url = await pending; + await navigator.clipboard.writeText(url); + } + toast.success("Invite link copied. Links you copied before no longer work."); } catch (e) { - toast.error(buildError(e as AppError)); + if (url) { + toast.error(`Could not copy. The new invite link is ${url}`, { duration: 60000 }); + } else { + toast.error(buildError(e as AppError)); + } } } async function changeRoles(memberId: string, roleIds: string[]) {