Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/types/src/opal-shell-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ export declare interface OpalShellHostProtocol {
* "Manage cookies" controls.
*/
isCookieSettingsAvailable(): Promise<boolean>;

/**
* 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<boolean>;
}

export declare interface OpalShellGuestProtocol {
Expand Down
3 changes: 3 additions & 0 deletions packages/visual-editor/eval/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ class EvalRun implements EvalHarnessRuntimeArgs {
isCookieSettingsAvailable: async function (): Promise<boolean> {
return false;
},
defaultMarketingOptinStatus: async function (): Promise<boolean> {
return true;
},
} satisfies OpalShellHostProtocol,
};
}
3 changes: 3 additions & 0 deletions packages/visual-editor/eval/graph-editing-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,9 @@ class GraphEditingEvalRun implements EvalLogger {
isCookieSettingsAvailable: async function (): Promise<boolean> {
return false;
},
defaultMarketingOptinStatus: async function (): Promise<boolean> {
return true;
},
} satisfies OpalShellHostProtocol,
};

Expand Down
4 changes: 4 additions & 0 deletions packages/visual-editor/fake/fake-mode-opal-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,8 @@ class FakeModeOpalShell implements OpalShellHostProtocol {
isCookieSettingsAvailable = async (): Promise<boolean> => {
return false;
};

defaultMarketingOptinStatus = async (): Promise<boolean> => {
return true;
};
}
1 change: 1 addition & 0 deletions packages/visual-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ class Main extends MainBase {
#renderWarmWelcomeModal() {
return html`<bb-warm-welcome-modal
.emailPrefsManager=${this.sca.services.emailPrefsManager}
.defaultOptIn=${this.defaultMarketingOptIn}
@bbmodaldismissed=${() => {
this.sca.controller.global.main.show.delete("WarmWelcome");
}}
Expand Down
15 changes: 14 additions & 1 deletion packages/visual-editor/src/main-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
65 changes: 56 additions & 9 deletions packages/visual-editor/src/shell/opal-shell-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof window.glue>["CookieNotificationBar"];

const { consentGranted: cookieConsentGranted, required: cookieBarRequired } =
setupCookieBar();
const {
consentGranted: cookieConsentGranted,
required: cookieBarRequired,
isConsentRegion: cookieConsentRegion,
} = setupCookieBar();

function setupCookieBar(): {
consentGranted: Promise<void>;
required: Promise<boolean>;
isConsentRegion: Promise<boolean>;
} {
const cookieBar = window.glue?.CookieNotificationBar;

Expand Down Expand Up @@ -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<boolean> => {
if (!cnb?.instance) return Promise.resolve(false);

return new Promise<boolean>((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),
};
}

Expand All @@ -142,32 +181,39 @@ 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<void>((r) => {
resolveConsent = r;
});
const required = new Promise<boolean>((r) => {
resolveRequired = r;
});
const isConsentRegion = new Promise<boolean>((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),
};
}

Expand Down Expand Up @@ -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: {
Expand Down
13 changes: 13 additions & 0 deletions packages/visual-editor/src/ui/elements/shell/warm-welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 15 additions & 3 deletions packages/visual-editor/src/ui/utils/oauth-based-opal-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> = Promise.resolve(false);
cookieSettingsRequired: Promise<boolean> = Promise.resolve(false);

isCookieSettingsAvailable = async (): Promise<boolean> => {
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<boolean> = Promise.resolve(false);

defaultMarketingOptinStatus = async (): Promise<boolean> => {
return !(await this.cookieConsentRequired);
};
}
Loading