diff --git a/packages/types/src/opal-shell-protocol.ts b/packages/types/src/opal-shell-protocol.ts index 9acdea2883b..6bd182edafe 100644 --- a/packages/types/src/opal-shell-protocol.ts +++ b/packages/types/src/opal-shell-protocol.ts @@ -87,6 +87,13 @@ export declare interface OpalShellHostProtocol { * "Manage cookies" controls. */ isCookieSettingsAvailable(): Promise; + + /** + * Returns the default opt-in status for marketing email preferences. In + * regions that require cookie consent (EEA, UK, etc.) this returns `false` + * so checkboxes default to unchecked. Elsewhere it returns `true`. + */ + defaultMarketingOptinStatus(): Promise; } export declare interface OpalShellGuestProtocol { diff --git a/packages/visual-editor/eval/eval.ts b/packages/visual-editor/eval/eval.ts index f8f0a0a1f16..d675ef6f77d 100644 --- a/packages/visual-editor/eval/eval.ts +++ b/packages/visual-editor/eval/eval.ts @@ -378,6 +378,9 @@ class EvalRun implements EvalHarnessRuntimeArgs { isCookieSettingsAvailable: async function (): Promise { return false; }, + defaultMarketingOptinStatus: async function (): Promise { + return true; + }, } satisfies OpalShellHostProtocol, }; } diff --git a/packages/visual-editor/eval/graph-editing-eval.ts b/packages/visual-editor/eval/graph-editing-eval.ts index 1fe90e5d737..2303684e104 100644 --- a/packages/visual-editor/eval/graph-editing-eval.ts +++ b/packages/visual-editor/eval/graph-editing-eval.ts @@ -999,6 +999,9 @@ class GraphEditingEvalRun implements EvalLogger { isCookieSettingsAvailable: async function (): Promise { return false; }, + defaultMarketingOptinStatus: async function (): Promise { + return true; + }, } satisfies OpalShellHostProtocol, }; diff --git a/packages/visual-editor/fake/fake-mode-opal-shell.ts b/packages/visual-editor/fake/fake-mode-opal-shell.ts index 79d2a314ea6..52918105906 100644 --- a/packages/visual-editor/fake/fake-mode-opal-shell.ts +++ b/packages/visual-editor/fake/fake-mode-opal-shell.ts @@ -175,4 +175,8 @@ class FakeModeOpalShell implements OpalShellHostProtocol { isCookieSettingsAvailable = async (): Promise => { return false; }; + + defaultMarketingOptinStatus = async (): Promise => { + return true; + }; } diff --git a/packages/visual-editor/src/index.ts b/packages/visual-editor/src/index.ts index 8ff833b8cfa..4dc907bbf42 100644 --- a/packages/visual-editor/src/index.ts +++ b/packages/visual-editor/src/index.ts @@ -554,6 +554,7 @@ class Main extends MainBase { #renderWarmWelcomeModal() { return html` { this.sca.controller.global.main.show.delete("WarmWelcome"); }} diff --git a/packages/visual-editor/src/main-base.ts b/packages/visual-editor/src/main-base.ts index c3fe5d0352f..1b83b4a5423 100644 --- a/packages/visual-editor/src/main-base.ts +++ b/packages/visual-editor/src/main-base.ts @@ -89,6 +89,13 @@ abstract class MainBase extends SignalWatcher(LitElement) { @state() protected accessor overflowMenuY = 0; + /** + * Default opt-in status for marketing email checkboxes. `true` means + * checked (non-consent regions), `false` means unchecked (EEA, UK, etc.). + */ + @state() + protected accessor defaultMarketingOptIn = true; + protected overflowMenuOnAction: | ((action: string, value: string | null) => void) | null = null; @@ -174,7 +181,13 @@ abstract class MainBase extends SignalWatcher(LitElement) { } }); - this.sca.services.emailPrefsManager.refreshPrefs().then(() => { + Promise.all([ + this.sca.services.emailPrefsManager.refreshPrefs(), + this.sca.env.shellHost + .defaultMarketingOptinStatus() + .catch(() => true), + ]).then(([, optInDefault]) => { + this.defaultMarketingOptIn = optInDefault; if ( this.sca.services.emailPrefsManager.prefsValid && !this.sca.services.emailPrefsManager.hasStoredPreferences diff --git a/packages/visual-editor/src/shell/opal-shell-host.ts b/packages/visual-editor/src/shell/opal-shell-host.ts index 164c4c12b56..dad31c66433 100644 --- a/packages/visual-editor/src/shell/opal-shell-host.ts +++ b/packages/visual-editor/src/shell/opal-shell-host.ts @@ -46,20 +46,27 @@ declare global { // miss the glueCookieNotificationBarLoaded callback fired by the cookie bar // script that loads before this module. // -// Returns two promises: -// consentGranted — resolves when the user accepts cookies (or no bar). -// required — resolves with whether the "Manage cookies" control -// should be shown for the user's region. +// Returns three promises: +// consentGranted — resolves when the user accepts cookies (or no bar). +// required — resolves with whether the "Manage cookies" control +// should be shown for the user's region. +// isConsentRegion — resolves with whether the cookie bar is required at +// all (EEA, UK, etc.), including regions where the +// "Manage cookies" control is hidden. // --------------------------------------------------------------------------- type CookieBar = NonNullable["CookieNotificationBar"]; -const { consentGranted: cookieConsentGranted, required: cookieBarRequired } = - setupCookieBar(); +const { + consentGranted: cookieConsentGranted, + required: cookieBarRequired, + isConsentRegion: cookieConsentRegion, +} = setupCookieBar(); function setupCookieBar(): { consentGranted: Promise; required: Promise; + isConsentRegion: Promise; } { const cookieBar = window.glue?.CookieNotificationBar; @@ -129,11 +136,43 @@ function setupCookieBar(): { }); }; + // Helper: determine whether cookie consent is required for this region + // (EEA, UK, etc.). Unlike shouldShowControl, this includes EEA countries + // where the cookie bar is shown but the "Manage cookies" button is hidden. + const checkConsentRegion = (cnb: CookieBar): Promise => { + if (!cnb?.instance) return Promise.resolve(false); + + return new Promise((resolve) => { + let settled = false; + const settle = (v: boolean) => { + if (!settled) { + settled = true; + resolve(v); + } + }; + + cnb.instance!.listen("loaded", (event: CustomEvent) => { + settle(event.detail.required === true); + }); + + // Fallback: if loaded already fired, check whether the bar element + // exists and is visible (the library renders it for consent regions). + requestAnimationFrame(() => { + if (settled) return; + const bar = document.querySelector(".glue-cookie-notification-bar"); + if (bar && !bar.hasAttribute("aria-hidden")) { + settle(true); + } + }); + }); + }; + // Case 1: Cookie bar already has an instance (script loaded before us). if (cookieBar?.instance) { return { consentGranted: watchForConsent(cookieBar), required: shouldShowControl(cookieBar), + isConsentRegion: checkConsentRegion(cookieBar), }; } @@ -142,6 +181,7 @@ function setupCookieBar(): { if (document.querySelector('script[src*="cookienotificationbar"]')) { let resolveConsent!: () => void; let resolveRequired!: (value: boolean) => void; + let resolveConsentRegion!: (value: boolean) => void; const consentGranted = new Promise((r) => { resolveConsent = r; @@ -149,25 +189,31 @@ function setupCookieBar(): { const required = new Promise((r) => { resolveRequired = r; }); + const isConsentRegion = new Promise((r) => { + resolveConsentRegion = r; + }); window.glueCookieNotificationBarLoaded = () => { const cnb = window.glue?.CookieNotificationBar; if (cnb) { watchForConsent(cnb).then(resolveConsent); shouldShowControl(cnb).then(resolveRequired); + checkConsentRegion(cnb).then(resolveConsentRegion); } else { resolveConsent(); resolveRequired(false); + resolveConsentRegion(false); } }; - return { consentGranted, required }; + return { consentGranted, required, isConsentRegion }; } // Case 3: No cookie bar present (e.g. local dev) — consent not required. return { consentGranted: Promise.resolve(), required: Promise.resolve(false), + isConsentRegion: Promise.resolve(false), }; } @@ -218,8 +264,9 @@ async function initializeOpalShellGuest() { // was set up at module scope so it's already listening. cookieConsentGranted.then(() => oauthShell.enableAnalytics()); - // Tell the shell whether cookie management is needed for this region. - oauthShell.cookieBarRequired = cookieBarRequired; + // Tell the shell whether cookie settings management is needed for this region. + oauthShell.cookieSettingsRequired = cookieBarRequired; + oauthShell.cookieConsentRequired = cookieConsentRegion; } const boxedState: { diff --git a/packages/visual-editor/src/ui/elements/shell/warm-welcome.ts b/packages/visual-editor/src/ui/elements/shell/warm-welcome.ts index 46fe12a3e22..0b39f9fdfe5 100644 --- a/packages/visual-editor/src/ui/elements/shell/warm-welcome.ts +++ b/packages/visual-editor/src/ui/elements/shell/warm-welcome.ts @@ -60,12 +60,25 @@ export class VEWarmWelcomeModal extends LitElement { @property() accessor emailPrefsManager: EmailPrefsManager | null = null; + /** + * Default opt-in state for the email checkboxes. Set to `false` in regions + * that require cookie consent (EEA, UK, etc.) so checkboxes start unchecked. + */ + @property({ type: Boolean }) + accessor defaultOptIn = true; + @state() accessor emailUpdates = true; @state() accessor userResearch = true; + override connectedCallback() { + super.connectedCallback(); + this.emailUpdates = this.defaultOptIn; + this.userResearch = this.defaultOptIn; + } + #handleModalDismissed({ withSave }: ModalDismissedEvent) { // After first dismissal, we always save the prefs, but we only respect the // checkbox values if the user explicitly clicked save. diff --git a/packages/visual-editor/src/ui/utils/oauth-based-opal-shell.ts b/packages/visual-editor/src/ui/utils/oauth-based-opal-shell.ts index 4740860e6d7..d424778feca 100644 --- a/packages/visual-editor/src/ui/utils/oauth-based-opal-shell.ts +++ b/packages/visual-editor/src/ui/utils/oauth-based-opal-shell.ts @@ -972,11 +972,23 @@ export class OAuthBasedOpalShell implements OpalShellHostProtocol { /** * Set by the shell host after the cookie bar's `loaded` event resolves. - * Indicates whether cookie management is needed for this user's region. + * Indicates whether the "Manage cookies" control is needed for this + * user's region. */ - cookieBarRequired: Promise = Promise.resolve(false); + cookieSettingsRequired: Promise = Promise.resolve(false); isCookieSettingsAvailable = async (): Promise => { - return this.cookieBarRequired; + return this.cookieSettingsRequired; + }; + + /** + * Set by the shell host after the cookie bar's `loaded` event resolves. + * True when the user is in any cookie consent region (EEA, UK, etc.), + * including those where the "Manage cookies" control is hidden. + */ + cookieConsentRequired: Promise = Promise.resolve(false); + + defaultMarketingOptinStatus = async (): Promise => { + return !(await this.cookieConsentRequired); }; }