mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
fix: bring back TCF useractioncomplete and USP/GPP sectionChange event listeners, and only using dom observer as the fallback
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user