From 9e1a144f2bfa2c26efe5e6a2e5157dc0fc35bae6 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Fri, 10 Jul 2026 13:02:11 +0200 Subject: [PATCH 1/5] Verbum: surface login requirement when in-frame social login is unavailable --- .../cm-841-surface-login-requirement | 4 ++ .../verbum-comments/class-verbum-comments.php | 1 + .../src/components/logged-out.tsx | 51 +++++++++++++++++++ .../features/verbum-comments/src/style.scss | 7 +++ 4 files changed, 63 insertions(+) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/cm-841-surface-login-requirement diff --git a/projects/packages/jetpack-mu-wpcom/changelog/cm-841-surface-login-requirement b/projects/packages/jetpack-mu-wpcom/changelog/cm-841-surface-login-requirement new file mode 100644 index 000000000000..02fec7816e12 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/cm-841-surface-login-requirement @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Verbum Comments: surface the login requirement up front when registration is required but in-frame social login is unavailable, instead of showing a guest form that is rejected on submit. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php index 59a7025e7b0a..5451005a6c6c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php @@ -237,6 +237,7 @@ public function enqueue_assets() { /* translators: %s is the name of the provider (WordPress, Facebook, Twitter) */ 'Logged in via %s' => __( 'Logged in via %s', 'jetpack-mu-wpcom' ), 'Log out' => __( 'Log out', 'jetpack-mu-wpcom' ), + 'Log in' => __( 'Log in', 'jetpack-mu-wpcom' ), 'Email' => __( 'Email', 'jetpack-mu-wpcom' ), '(Address never made public)' => __( '(Address never made public)', 'jetpack-mu-wpcom'), // phpcs:ignore PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket 'Instantly' => __( 'Instantly', 'jetpack-mu-wpcom' ), diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx index 1b96b5824aad..7b4259dd02f2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx @@ -40,6 +40,29 @@ const getLoginCommentText = ( commentParent: Signal ) => { return { defaultText }; }; +/** + * Build a top-level login URL for the site the comment form belongs to. + * + * On Atomic/Jetpack the comment form runs inside a cross-origin iframe, so the parent post URL + * is passed in via the location hash (`#parent=…`). We log in against that site so the visitor + * returns authenticated (via Jetpack SSO) and can comment. + */ +const getSiteLoginUrl = () => { + const parentUrl = decodeURIComponent( + window.location.hash.match( /[#&]parent=([^&]*)/ )?.[ 1 ] ?? '' + ); + + const target = parentUrl || VerbumComments.homeURL || ''; + + try { + return `${ new URL( target ).origin }/wp-login.php?redirect_to=${ encodeURIComponent( + target + ) }`; + } catch { + return target; + } +}; + export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOutProps ) => { const [ activeService, setActiveService ] = useState( '' ); const closeLoginPopupService = requireNameEmail && ! mustLogIn ? 'mail' : ''; @@ -84,6 +107,34 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut const { commentParent } = useContext( VerbumSignals ); + // Login is required but we can't render the in-frame login options (e.g. cross-origin + // iframe on Atomic where cookies are blocked). Showing the guest form here is a dead end: + // the comment is rejected on submit with "you must be logged in". Surface the requirement + // up front with a link to log in instead. + const loginRequiredWithoutInFrameAuth = mustLogIn && ! canWeAccessCookies; + + if ( loginRequiredWithoutInFrameAuth ) { + return ( +
+
+
+
+ { getLoginCommentText( commentParent ) } +
+ + { translate( 'Log in' ) } + +
+
+
+ ); + } + return (
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss index 7f88cd7861db..b800e7687ba6 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss @@ -332,6 +332,13 @@ padding-bottom: 16px; } + .verbum-subscriptions__login-required { + + .components-button.is-primary { + text-decoration: none; + } + } + .verbum-logins { display: flex; align-items: center; From e75ac7e499ab6bb35cfa04744424d08dfcf6999c Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Fri, 10 Jul 2026 13:04:59 +0200 Subject: [PATCH 2/5] Verbum: enable social login inside the iframe via Storage Access API --- .../verbum-iframe-social-login-storage-access | 4 +++ .../src/components/logged-out.tsx | 23 +++++++++----- .../src/hooks/useSocialLogin.tsx | 30 +++++++++++++++++++ .../features/verbum-comments/src/state.tsx | 11 ++++--- 4 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/verbum-iframe-social-login-storage-access diff --git a/projects/packages/jetpack-mu-wpcom/changelog/verbum-iframe-social-login-storage-access b/projects/packages/jetpack-mu-wpcom/changelog/verbum-iframe-social-login-storage-access new file mode 100644 index 000000000000..11fbad5e0f61 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/verbum-iframe-social-login-storage-access @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Verbum Comments: allow social login (WordPress.com / Facebook) inside the cross-origin comment iframe on Atomic/Jetpack sites by requesting first-party cookie access via the Storage Access API. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx index 7b4259dd02f2..33f39cb7d629 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx @@ -107,11 +107,15 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut const { commentParent } = useContext( VerbumSignals ); - // Login is required but we can't render the in-frame login options (e.g. cross-origin - // iframe on Atomic where cookies are blocked). Showing the guest form here is a dead end: - // the comment is rejected on submit with "you must be logged in". Surface the requirement - // up front with a link to log in instead. - const loginRequiredWithoutInFrameAuth = mustLogIn && ! canWeAccessCookies; + // In the iframe (Atomic/Jetpack) we offer social login and request cookie access on click, + // so the buttons render even when the cookie test currently fails. + const showSocialButtons = canWeAccessCookies || !! VerbumComments.isJetpackComments; + + // Login is required but there's no way to log in here (cookies blocked and not in the iframe + // where social login is offered). Showing the guest form would be a dead end: the comment is + // rejected on submit with "you must be logged in". Surface the requirement with a login link. + const loginRequiredWithoutInFrameAuth = + mustLogIn && ! canWeAccessCookies && ! VerbumComments.isJetpackComments; if ( loginRequiredWithoutInFrameAuth ) { return ( @@ -139,7 +143,7 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut
- { canWeAccessCookies && ( + { showSocialButtons && ( <>
{ getLoginCommentText( commentParent ) } @@ -198,7 +202,12 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut
) } - +
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx index d5bc01afe0dc..57d37b39eb2d 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx @@ -25,6 +25,29 @@ const addWordPressDomain = window.location.hostname.endsWith( '.wordpress.com' ) ? ' Domain=.wordpress.com' : ''; +/** + * Ask the browser to grant this (cross-origin) frame access to its first-party cookies. + * + * On Atomic/Jetpack, Verbum runs inside a third-party iframe where the `.wordpress.com` auth + * cookies can't be read/written, so the social login state can't be established. The Storage + * Access API restores first-party cookie access, but only from a user gesture — so this must be + * called synchronously off the login click. If it's denied or unsupported we fall through: the + * popup + postMessage flow still authenticates for the current session, only persistence is lost. + */ +const ensureCookieAccess = async () => { + try { + if ( + typeof document.hasStorageAccess === 'function' && + typeof document.requestStorageAccess === 'function' && + ! ( await document.hasStorageAccess() ) + ) { + await document.requestStorageAccess(); + } + } catch { + // Access denied or unsupported — social login still works in-session via postMessage. + } +}; + /** * Hook to retrieve user info from server, handle social login, and logout functionality. * @@ -74,6 +97,13 @@ export default function useSocialLogin() { const login = async ( service: SocialServiceName ) => { const { connectURL } = VerbumComments; + + // Restore first-party cookie access before logging in, so the login state can persist + // inside the iframe. Runs off the login-button gesture, which the Storage Access API requires. + if ( VerbumComments.isJetpackComments ) { + await ensureCookieAccess(); + } + const broadcastChannel = new BroadcastChannel( 'verbum_post_message' ); const loginWindow = window.open( diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx index 88fa7047f019..858150d0b870 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx @@ -20,12 +20,15 @@ export function createSignals() { * Here we also check if cookies are accessible, userInfo is set and the service is different from 'guest' or 'jetpack'. */ const userLoggedIn = computed( () => { + const info = userInfo.value; + const isExternalService = + !! info && info.service !== 'guest' && info.service !== 'jetpack'; + + // A fresh social login (via postMessage) carries an access_token even when cookies are + // blocked in the iframe, so treat that as logged in too — not just readable-cookie logins. return ( VerbumComments.isJetpackCommentsLoggedIn || - ( canWeAccessCookies() && - userInfo.value && - userInfo.value?.service !== 'guest' && - userInfo.value?.service !== 'jetpack' ) + ( isExternalService && ( canWeAccessCookies() || !! info.access_token ) ) ); } ); From 7fd78001154988f93c0c8688e7e46f015613ec9f Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Tue, 28 Jul 2026 15:59:10 +0200 Subject: [PATCH 3/5] Fix storage-access ordering; keep a login fallback when access is denied --- .../src/components/logged-out.tsx | 25 ++++++++----- .../src/hooks/useSocialLogin.tsx | 36 ++++++++----------- .../features/verbum-comments/src/state.tsx | 11 +++--- .../features/verbum-comments/src/style.scss | 5 +++ 4 files changed, 41 insertions(+), 36 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx index 33f39cb7d629..8870ee482076 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/logged-out.tsx @@ -46,6 +46,8 @@ const getLoginCommentText = ( commentParent: Signal ) => { * On Atomic/Jetpack the comment form runs inside a cross-origin iframe, so the parent post URL * is passed in via the location hash (`#parent=…`). We log in against that site so the visitor * returns authenticated (via Jetpack SSO) and can comment. + * + * @return {string} - The site's login URL, or the bare target if it can't be parsed. */ const getSiteLoginUrl = () => { const parentUrl = decodeURIComponent( @@ -111,13 +113,11 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut // so the buttons render even when the cookie test currently fails. const showSocialButtons = canWeAccessCookies || !! VerbumComments.isJetpackComments; - // Login is required but there's no way to log in here (cookies blocked and not in the iframe - // where social login is offered). Showing the guest form would be a dead end: the comment is - // rejected on submit with "you must be logged in". Surface the requirement with a login link. - const loginRequiredWithoutInFrameAuth = - mustLogIn && ! canWeAccessCookies && ! VerbumComments.isJetpackComments; + // Login is required but cookies are blocked, so an in-frame login may not stick. Keep the + // top-level login link on offer — without it, a denied cookie prompt is a dead end again. + const showSiteLogin = mustLogIn && ! canWeAccessCookies; - if ( loginRequiredWithoutInFrameAuth ) { + if ( showSiteLogin && ! showSocialButtons ) { return (
@@ -202,10 +202,19 @@ export const LoggedOut = ( { login, canWeAccessCookies, loginWindow }: LoggedOut
) } + { showSiteLogin && showSocialButtons && ( + + { translate( 'Log in' ) } + + ) }
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx index 57d37b39eb2d..6bce478e8495 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx @@ -29,22 +29,18 @@ const addWordPressDomain = window.location.hostname.endsWith( '.wordpress.com' ) * Ask the browser to grant this (cross-origin) frame access to its first-party cookies. * * On Atomic/Jetpack, Verbum runs inside a third-party iframe where the `.wordpress.com` auth - * cookies can't be read/written, so the social login state can't be established. The Storage - * Access API restores first-party cookie access, but only from a user gesture — so this must be - * called synchronously off the login click. If it's denied or unsupported we fall through: the - * popup + postMessage flow still authenticates for the current session, only persistence is lost. + * cookies can't be read or written, so a social login never sticks — and the comment POST back to + * jetpack.wordpress.com arrives without a session, which WP.com downgrades to a guest comment. + * + * Best-effort: not awaited, so the login popup still opens on the same click. `window.open()` and + * this both want the user gesture, and the popup is the one that breaks visibly without it. If + * access is denied the visitor stays logged out and the top-level login link remains available. */ -const ensureCookieAccess = async () => { +const requestCookieAccess = () => { try { - if ( - typeof document.hasStorageAccess === 'function' && - typeof document.requestStorageAccess === 'function' && - ! ( await document.hasStorageAccess() ) - ) { - await document.requestStorageAccess(); - } + document.requestStorageAccess?.().catch( () => {} ); } catch { - // Access denied or unsupported — social login still works in-session via postMessage. + // Unsupported — the caller handles the still-logged-out case. } }; @@ -95,15 +91,8 @@ export default function useSocialLogin() { document.cookie = `${ cookieName }=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure=True;${ addWordPressDomain }`; }; - const login = async ( service: SocialServiceName ) => { + const login = ( service: SocialServiceName ) => { const { connectURL } = VerbumComments; - - // Restore first-party cookie access before logging in, so the login state can persist - // inside the iframe. Runs off the login-button gesture, which the Storage Access API requires. - if ( VerbumComments.isJetpackComments ) { - await ensureCookieAccess(); - } - const broadcastChannel = new BroadcastChannel( 'verbum_post_message' ); const loginWindow = window.open( @@ -112,6 +101,11 @@ export default function useSocialLogin() { `status=0,toolbar=0,location=1,menubar=0,directories=0,resizable=1,scrollbars=0${ serviceData[ service ].popup }` ); + // Requested once the popup is open, so it resolves while the visitor is still logging in. + if ( VerbumComments.isJetpackComments ) { + requestCookieAccess(); + } + const waitForLogin = ( event: MessageEvent ) => { if ( event.origin !== document.location.origin && diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx index 858150d0b870..88fa7047f019 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/state.tsx @@ -20,15 +20,12 @@ export function createSignals() { * Here we also check if cookies are accessible, userInfo is set and the service is different from 'guest' or 'jetpack'. */ const userLoggedIn = computed( () => { - const info = userInfo.value; - const isExternalService = - !! info && info.service !== 'guest' && info.service !== 'jetpack'; - - // A fresh social login (via postMessage) carries an access_token even when cookies are - // blocked in the iframe, so treat that as logged in too — not just readable-cookie logins. return ( VerbumComments.isJetpackCommentsLoggedIn || - ( isExternalService && ( canWeAccessCookies() || !! info.access_token ) ) + ( canWeAccessCookies() && + userInfo.value && + userInfo.value?.service !== 'guest' && + userInfo.value?.service !== 'jetpack' ) ); } ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss index b800e7687ba6..841fc41dd7d6 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/style.scss @@ -339,6 +339,11 @@ } } + .verbum-subscriptions__login-link { + display: inline-block; + margin-block-start: 8px; + } + .verbum-logins { display: flex; align-items: center; From b353c6a1c87cea088f3d30ca5f81b7f37b239192 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Tue, 28 Jul 2026 22:32:15 +0200 Subject: [PATCH 4/5] Add allow-storage-access-by-user-activation to the comment iframe sandbox --- .../changelog/verbum-iframe-social-login-storage-access | 4 ++++ projects/plugins/jetpack/modules/comments/comments.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 projects/plugins/jetpack/changelog/verbum-iframe-social-login-storage-access diff --git a/projects/plugins/jetpack/changelog/verbum-iframe-social-login-storage-access b/projects/plugins/jetpack/changelog/verbum-iframe-social-login-storage-access new file mode 100644 index 000000000000..1a3685ab23ee --- /dev/null +++ b/projects/plugins/jetpack/changelog/verbum-iframe-social-login-storage-access @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Comments: allow the comment form iframe to request first-party cookie access, so social login can work inside it. diff --git a/projects/plugins/jetpack/modules/comments/comments.php b/projects/plugins/jetpack/modules/comments/comments.php index 5082713660a9..e323419bbfc5 100644 --- a/projects/plugins/jetpack/modules/comments/comments.php +++ b/projects/plugins/jetpack/modules/comments/comments.php @@ -518,7 +518,8 @@ public function comment_form_after() { class="jetpack_remote_comment" id="jetpack_remote_comment" - sandbox="allow-same-origin allow-top-navigation allow-scripts allow-forms allow-popups" + + sandbox="allow-same-origin allow-top-navigation allow-scripts allow-forms allow-popups allow-storage-access-by-user-activation" > From ed864c797ae200680227bb6eb33466c54d9867d2 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Wed, 29 Jul 2026 15:04:09 +0200 Subject: [PATCH 5/5] Don't let BroadcastChannel take down login when storage is blocked --- .../src/hooks/useSocialLogin.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx index 6bce478e8495..dae6a5476de5 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/hooks/useSocialLogin.tsx @@ -93,7 +93,16 @@ export default function useSocialLogin() { const login = ( service: SocialServiceName ) => { const { connectURL } = VerbumComments; - const broadcastChannel = new BroadcastChannel( 'verbum_post_message' ); + + // BroadcastChannel needs storage access, which a cookie-blocked iframe doesn't have — Firefox + // throws SecurityError. It's a secondary channel: the popup also posts back via + // window.postMessage, picked up by the listener below, so carry on without it. + let broadcastChannel: BroadcastChannel | null = null; + try { + broadcastChannel = new BroadcastChannel( 'verbum_post_message' ); + } catch { + // No channel available. + } const loginWindow = window.open( `${ connectURL }&blog_id=${ VerbumComments.siteId }&post_id=${ VerbumComments.postId }&service=${ service }`, @@ -136,7 +145,7 @@ export default function useSocialLogin() { // Listen for login data window.addEventListener( 'message', waitForLogin ); - broadcastChannel.addEventListener( 'message', waitForLogin ); + broadcastChannel?.addEventListener( 'message', waitForLogin ); // Clean up loginWindow to reset activeService const loginClosed = setInterval( () => { @@ -144,8 +153,8 @@ export default function useSocialLogin() { clearInterval( loginClosed ); setLoginWindowRef( undefined ); window.removeEventListener( 'message', waitForLogin ); - broadcastChannel.removeEventListener( 'message', waitForLogin ); - broadcastChannel.close(); + broadcastChannel?.removeEventListener( 'message', waitForLogin ); + broadcastChannel?.close(); } }, 100 );