fix: bring back TCF useractioncomplete and USP/GPP sectionChange event listeners, and only using dom observer as the fallback

This commit is contained in:
tdgao
2026-07-23 21:17:24 -07:00
parent 8db44301fa
commit e650033573
6 changed files with 163 additions and 16 deletions
+4 -1
View File
@@ -190,9 +190,10 @@ async function setUspToggleStates(checked, controls, timeoutMs) {
/**
* @param {ConsentAction} action
* @param {AdsConsentVariant | null} variant
* @param {(() => void) | undefined} onSubmit
* @returns {Promise<ConsentActionResult>}
*/
async function performDocumentConsentAction(action, variant) {
async function performDocumentConsentAction(action, variant, onSubmit) {
if (variant === 'usp') {
if (action === 'manage') {
return 'handled'
@@ -204,6 +205,7 @@ async function performDocumentConsentAction(action, variant) {
const settledControls = await setUspToggleStates(action === 'reject', controls, 2_000)
if (!settledControls) return 'failed'
onSubmit?.()
settledControls.confirmButton.click()
return 'handled'
}
@@ -212,6 +214,7 @@ async function performDocumentConsentAction(action, variant) {
const button = findTcfConsentButton(action)
if (!button) return 'not-ready'
onSubmit?.()
button.click()
return 'handled'
}
+104 -9
View File
@@ -1,21 +1,33 @@
const ACTION_TIMEOUT = 10_000
const LAYOUT_DELAY = 100
const SUBMISSION_TIMEOUT = 10_000
class AdsConsentController {
constructor() {
/** @type {AdsConsentState} */
this.state = new AdsConsentState()
/** @type {AdsConsentPhase | null} */
this.preSubmissionPhase = null
/** @type {ReturnType<typeof setTimeout> | null} */
this.submissionTimeout = null
}
/** @returns {void} */
syncConsentPopup() {
// The CMP root persists while its internal views change, so only its removal finishes
// a normal active flow. Submissions wait for the CMP event or the timeout fallback so
// the consent has time to persist before the ads webview is refreshed.
const cmpMain = document.getElementById('qc-cmp2-main')
// no cmp container, remove our popup
if (!cmpMain) {
if (this.state.phase === 'idle') {
document.documentElement.classList.remove('modrinth-ads-consent-overlay')
} else if (this.state.phase !== 'complete') {
} else if (
this.state.phase !== 'submitting-consent' &&
this.state.phase !== 'finishing'
) {
this.finishConsentFlow()
}
return
@@ -23,12 +35,11 @@ class AdsConsentController {
if (this.state.phase !== 'idle') return
// has cmp container, add our popup
document.documentElement.classList.add('modrinth-ads-consent-overlay')
const variant = this.detectVariant()
if (!areConsentControlsPresent(variant)) return
this.state.setState('initial')
this.state.setState('showing-popup')
this.setPopupMode('custom')
}
@@ -56,13 +67,93 @@ class AdsConsentController {
}
try {
const result = await performDocumentConsentAction(action, variant)
if (result !== 'handled') this.showNativeCmpFallback()
const result = await performDocumentConsentAction(action, variant, () =>
this.beginConsentSubmission(),
)
if (result !== 'handled') {
this.cancelConsentSubmission()
this.showNativeCmpFallback()
}
} catch {
this.cancelConsentSubmission()
this.showNativeCmpFallback()
}
}
/** @returns {void} */
beginConsentSubmission() {
if (
!['showing-popup', 'showing-preferences', 'showing-reopened-preferences'].includes(
this.state.phase,
)
) {
return
}
this.preSubmissionPhase = this.state.phase
this.state.setState('submitting-consent')
clearTimeout(this.submissionTimeout ?? undefined)
this.submissionTimeout = setTimeout(() => {
const preSubmissionPhase = this.preSubmissionPhase
this.submissionTimeout = null
const dialogId = this.state.variant === 'usp' ? 'qc-cmp2-usp' : 'qc-cmp2-ui'
if (
this.state.phase === 'submitting-consent' &&
!document.getElementById(dialogId)
) {
this.finishConsentFlow()
} else if (this.state.phase === 'submitting-consent' && preSubmissionPhase) {
this.state.setState(preSubmissionPhase)
this.preSubmissionPhase = null
}
}, SUBMISSION_TIMEOUT)
}
/** @returns {void} */
cancelConsentSubmission() {
clearTimeout(this.submissionTimeout ?? undefined)
this.submissionTimeout = null
if (this.state.phase === 'submitting-consent' && this.preSubmissionPhase) {
this.state.setState(this.preSubmissionPhase)
}
this.preSubmissionPhase = null
}
/**
* @param {{ eventStatus?: string } | null | undefined} tcData
* @param {boolean} success
* @returns {void}
*/
handleTcfConsentEvent(tcData, success) {
if (
success &&
tcData?.eventStatus === 'useractioncomplete' &&
this.state.variant === 'tcf' &&
this.state.phase !== 'idle' &&
this.state.phase !== 'finishing'
) {
this.finishConsentFlow()
}
}
/**
* @param {{ eventName?: string } | null | undefined} gppData
* @param {boolean} success
* @returns {void}
*/
handleGppConsentEvent(gppData, success) {
if (
success &&
gppData?.eventName === 'sectionChange' &&
this.state.variant === 'usp' &&
this.state.phase === 'submitting-consent'
) {
this.finishConsentFlow()
}
}
/** @returns {Promise<void>} */
async reopenPreferences() {
if (document.documentElement.classList.contains('modrinth-ads-consent-overlay')) {
@@ -74,7 +165,7 @@ class AdsConsentController {
return
}
this.state.setState('reopened')
this.state.setState('showing-reopened-preferences')
this.preparePreferences()
try {
@@ -158,6 +249,7 @@ class AdsConsentController {
/** @returns {Promise<boolean>} */
async openPreferences() {
this.state.setState('showing-preferences')
await this.showExpandedUi()
if (!(await this.openConsentManagerWhenReady(ACTION_TIMEOUT))) {
@@ -207,11 +299,14 @@ class AdsConsentController {
/** @returns {void} */
finishConsentFlow() {
if (this.state.phase === 'idle' || this.state.phase === 'complete') {
if (this.state.phase === 'idle' || this.state.phase === 'finishing') {
return
}
this.state.setState('complete')
clearTimeout(this.submissionTimeout ?? undefined)
this.submissionTimeout = null
this.preSubmissionPhase = null
this.state.setState('finishing')
this.setPopupMode('hidden')
}
}
+47
View File
@@ -1,13 +1,56 @@
const controller = new AdsConsentController()
const CONSENT_LISTENER_RETRY_INTERVAL = 250
const CONSENT_LISTENER_MAX_ATTEMPTS = 60
let tcfListenerInstalled = false
let gppListenerInstalled = false
let consentListenerInstallAttempts = 0
/** @type {ReturnType<typeof setTimeout> | null} */
let consentListenerRetry = null
function isTopFrame() {
return window.top === window
}
function installConsentListeners() {
if (!tcfListenerInstalled && typeof window.__tcfapi === 'function') {
try {
window.__tcfapi('addEventListener', 2, (tcData, success) =>
controller.handleTcfConsentEvent(tcData, success),
)
tcfListenerInstalled = true
} catch {}
}
if (!gppListenerInstalled && typeof window.__gpp === 'function') {
try {
window.__gpp('addEventListener', (gppData, success) =>
controller.handleGppConsentEvent(gppData, success),
)
gppListenerInstalled = true
} catch {}
}
if (
(tcfListenerInstalled && gppListenerInstalled) ||
consentListenerInstallAttempts >= CONSENT_LISTENER_MAX_ATTEMPTS ||
consentListenerRetry
) {
return
}
consentListenerInstallAttempts += 1
consentListenerRetry = setTimeout(() => {
consentListenerRetry = null
installConsentListeners()
}, CONSENT_LISTENER_RETRY_INTERVAL)
}
function initializeTopFrame() {
if (!isTopFrame()) return
installConsentStyles()
installConsentListeners()
controller.syncConsentPopup()
}
@@ -17,6 +60,10 @@ document.addEventListener(
notifyAdClick()
const target = event.target instanceof Element ? event.target : null
if (target?.closest('#qc-cmp2-usp .qc-usp-ui-form-content button[mode="primary"]')) {
controller.beginConsentSubmission()
}
const link = target?.closest('a')
if (link?.href) {
event.preventDefault()
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @typedef {'idle' | 'initial' | 'reopened' | 'complete'} AdsConsentPhase
* @typedef {'idle' | 'showing-popup' | 'showing-preferences' | 'showing-reopened-preferences' | 'submitting-consent' | 'finishing'} AdsConsentPhase
* @typedef {'usp' | 'tcf'} AdsConsentVariant
*/
+1
View File
@@ -14,6 +14,7 @@ const THEME_STYLE = `
}
#qc-cmp2-usp {
outline: none !important;
background: var(--modrinth-usp-bg) !important;
border: 1px solid var(--modrinth-usp-divider) !important;
border-radius: 1rem !important;
+6 -5
View File
@@ -503,6 +503,7 @@ pub async fn init_ads_window<R: Runtime>(
// Aditude has separately informed SSPs and IVT vendors that this traffic
// originates from a desktop app.
.user_agent(ADS_USER_AGENT)
.incognito(true)
.zoom_hotkeys_enabled(false)
.transparent(true)
.on_new_window(|_, _| {
@@ -594,6 +595,8 @@ pub async fn init_ads_window<R: Runtime>(
}
})?;
webview.open_devtools();
Some(webview)
} else {
None
@@ -889,11 +892,9 @@ pub async fn get_ads_consent_required<R: Runtime>(
let state = app.state::<RwLock<AdsState>>();
let state = state.read().await;
Ok(
state.shown
&& state.consent_required
&& state.consent_notification_enabled,
)
Ok(state.shown
&& state.consent_required
&& state.consent_notification_enabled)
}
#[tauri::command]