fix: US variant of consent popup

This commit is contained in:
tdgao
2026-07-20 20:53:10 -07:00
parent 620e7edd9c
commit 3df9f870a4
2 changed files with 428 additions and 62 deletions
+425 -62
View File
@@ -6,14 +6,19 @@ document.addEventListener(
window.top.postMessage({ modrinthAdClick: true }, MODRINTH_ORIGIN)
let target = e.target
if (target?.closest?.('.qc-cmp2-close-icon')) {
if (modrinthAdsConsentReprompt) {
const uspCloseIcon = target?.closest?.('.qc-usp-close-icon')
if (target?.closest?.('.qc-cmp2-close-icon') || uspCloseIcon) {
if (isAdsConsentReprompt()) {
setTimeout(finishAdsConsentReprompt)
} else if (document.documentElement.classList.contains('modrinth-ads-consent-preferences')) {
setTimeout(() => void restoreAdsConsentNotification())
}
}
if (target?.closest?.('#qc-cmp2-usp .qc-usp-ui-form-content button[mode="primary"]')) {
beginUspConsentCommit()
}
while (target != null) {
if (target.matches('a')) {
e.preventDefault()
@@ -32,14 +37,54 @@ window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, MODRINTH_ORIGIN)
}
let modrinthAdsConsentOverlayShown = false
const modrinthAdsConsentState = {
phase: 'idle',
variant: null,
commitTimeout: null,
}
let modrinthTcfListenerInstalled = false
let modrinthTcfListenerAttempts = 0
let modrinthAdsConsentReprompt = false
let modrinthAdsConsentRepromptManaging = false
let modrinthGppListenerInstalled = false
let modrinthGppListenerAttempts = 0
let modrinthAdsConsentActionRequestId = 0
const modrinthAdsConsentActionResolvers = new Map()
function transitionAdsConsent(event) {
const phase = modrinthAdsConsentState.phase
if (event === 'prompt-detected') {
if (phase === 'idle') modrinthAdsConsentState.phase = 'initial'
} else if (event === 'reprompt-started') {
modrinthAdsConsentState.phase = 'reprompt'
} else if (event === 'commit-started') {
modrinthAdsConsentState.phase = isAdsConsentReprompt()
? 'reprompt-committing'
: 'initial-committing'
} else if (event === 'commit-timed-out') {
if (phase === 'reprompt-committing') {
modrinthAdsConsentState.phase = 'reprompt'
} else if (phase === 'initial-committing') {
modrinthAdsConsentState.phase = 'initial'
}
} else if (event === 'completed') {
modrinthAdsConsentState.phase = 'complete'
}
}
function isAdsConsentReprompt() {
return (
modrinthAdsConsentState.phase === 'reprompt' ||
modrinthAdsConsentState.phase === 'reprompt-committing'
)
}
function isUspConsentCommitPending() {
return (
modrinthAdsConsentState.phase === 'initial-committing' ||
modrinthAdsConsentState.phase === 'reprompt-committing'
)
}
function installAdsRailStyle() {
if (document.getElementById('modrinth-ads-rail-style')) {
return
@@ -65,6 +110,10 @@ function installAdsConsentOverlayStyle() {
html.modrinth-ads-consent-preferences:not(.modrinth-ads-consent-preferences-visible) #qc-cmp2-container {
display: none !important;
}
#qc-cmp2-usp .qc-usp-close-icon {
display: none !important;
}
`
document.documentElement.appendChild(style)
}
@@ -124,9 +173,36 @@ function isDirectChildFrame(source) {
)
}
function findAdsConsentButton(action) {
const container = document.getElementById('qc-cmp2-container')
if (!container) return null
function displayUspConsentUi() {
if (typeof window.__uspapi !== 'function') return false
try {
window.__uspapi('displayUspUi', 1, () => {})
return true
} catch {
return false
}
}
function detectAdsConsentVariant() {
let variant = null
if (document.getElementById('qc-cmp2-usp')) {
variant = 'usp'
} else if (document.getElementById('qc-cmp2-ui')) {
variant = 'tcf'
}
if (variant) {
modrinthAdsConsentState.variant = variant
}
return variant
}
function findTcfConsentButton(action) {
const dialog = document.getElementById('qc-cmp2-ui')
if (!dialog) return null
const buttonIds = {
accept: 'accept-btn',
@@ -136,29 +212,142 @@ function findAdsConsentButton(action) {
const buttonId = buttonIds[action]
if (!buttonId) return null
const button = container.querySelector(`#${buttonId}`)
const button = dialog.querySelector(`#${buttonId}`)
return button && !button.disabled ? button : null
}
function clickAdsConsentButtonWhenReady(action, timeoutMs, onButtonFound) {
const deadline = Date.now() + timeoutMs
function getUspConsentControls() {
const dialog = document.getElementById('qc-cmp2-usp')
if (!dialog) return null
const toggles = Array.from(
dialog.querySelectorAll('.qc-usp-container button.qc-cmp2-toggle[role="switch"]'),
)
const confirmButton = dialog.querySelector('.qc-usp-ui-form-content button[mode="primary"]')
if (
toggles.length === 0 ||
!confirmButton ||
confirmButton.disabled ||
toggles.some(
(toggle) =>
toggle.disabled || !['true', 'false'].includes(toggle.getAttribute('aria-checked')),
)
) {
return null
}
return { toggles, confirmButton }
}
function waitForUspToggleState(index, checked, expectedCount, deadline) {
return new Promise((resolve) => {
function tryClick() {
const button = findAdsConsentButton(action)
if (button) {
// CMP navigation can replace this document during the click, so acknowledge it first.
onButtonFound?.()
resolve(true)
button.click()
function checkState() {
const controls = getUspConsentControls()
if (
controls &&
controls.toggles.length === expectedCount &&
controls.toggles[index]?.getAttribute('aria-checked') === String(checked)
) {
resolve(controls)
} else if (Date.now() >= deadline) {
resolve(false)
resolve(null)
} else {
setTimeout(tryClick, 50)
setTimeout(checkState, 50)
}
}
tryClick()
checkState()
})
}
async function setUspToggleStates(checked, controls, timeoutMs) {
const expectedCount = controls.toggles.length
const deadline = Date.now() + timeoutMs
for (let index = 0; index < expectedCount; index += 1) {
controls = getUspConsentControls()
if (!controls || controls.toggles.length !== expectedCount) return null
const toggle = controls.toggles[index]
if (toggle.getAttribute('aria-checked') !== String(checked)) {
toggle.click()
controls = await waitForUspToggleState(index, checked, expectedCount, deadline)
if (!controls) return null
}
}
return controls
}
async function performAdsConsentActionInDocument(action, onHandled) {
const variant = detectAdsConsentVariant()
const unknownDialog = document.querySelector('#qc-cmp2-container [role="dialog"]')
if (action === 'show') {
if (variant || unknownDialog) {
onHandled?.()
return 'handled'
}
return 'not-ready'
}
if (variant === 'usp') {
if (action === 'manage') {
onHandled?.()
return 'handled'
}
if (!['accept', 'reject'].includes(action)) return 'failed'
const controls = getUspConsentControls()
if (!controls) return 'not-ready'
const shouldOptOut = action === 'reject'
const settledControls = await setUspToggleStates(shouldOptOut, controls, 2000)
if (!settledControls) return 'failed'
// CMP navigation can replace this document during the click, so acknowledge it first.
onHandled?.()
beginUspConsentCommit()
settledControls.confirmButton.click()
return 'handled'
}
if (variant === 'tcf') {
const button = findTcfConsentButton(action)
if (!button) return 'not-ready'
// CMP navigation can replace this document during the click, so acknowledge it first.
onHandled?.()
button.click()
return 'handled'
}
if (action === 'manage' && unknownDialog) {
onHandled?.()
return 'handled'
}
return 'not-ready'
}
function performAdsConsentActionWhenReady(action, timeoutMs, onHandled) {
const deadline = Date.now() + timeoutMs
return new Promise((resolve) => {
async function tryAction() {
const result = await performAdsConsentActionInDocument(action, onHandled)
if (result === 'handled') {
resolve(true)
} else if (result === 'failed' || Date.now() >= deadline) {
resolve(false)
} else {
setTimeout(tryAction, 50)
}
}
void tryAction()
})
}
@@ -178,9 +367,7 @@ function performAdsConsentActionAcrossFrames(action, timeoutMs) {
const timeout = setTimeout(() => settle(false), timeoutMs)
modrinthAdsConsentActionResolvers.set(requestId, () => settle(true))
sendAdsConsentCommandToChildFrames({ type: 'perform', action, requestId, timeoutMs })
clickAdsConsentButtonWhenReady(action, timeoutMs, () => settle(true)).then((clicked) => {
if (!clicked) settle(false)
})
void performAdsConsentActionWhenReady(action, timeoutMs, () => settle(true))
})
}
@@ -198,18 +385,9 @@ async function restoreAdsConsentNotification() {
}
}
function finishAdsConsentReprompt() {
modrinthAdsConsentReprompt = false
modrinthAdsConsentRepromptManaging = false
modrinthAdsConsentOverlayShown = false
document.documentElement.classList.remove('modrinth-ads-consent-overlay')
concealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'conceal' })
invokeAdsConsentOverlayCommand(false)
}
async function openAdsConsentPreferences() {
async function showNativeAdsConsentUi() {
prepareAdsConsentPreferences()
await waitForAdsConsentLayout()
sendAdsConsentCommandToChildFrames({ type: 'prepare' })
await expandAdsConsentWebview()
await waitForAdsConsentLayout()
@@ -219,10 +397,28 @@ async function openAdsConsentPreferences() {
window.dispatchEvent(new Event('resize'))
sendAdsConsentCommandToChildFrames({ type: 'resize' })
const clicked = await performAdsConsentActionAcrossFrames('manage', 2500)
if (!clicked) {
const shown = await performAdsConsentActionAcrossFrames('show', 2500)
if (!shown) {
await restoreAdsConsentNotification()
}
return shown
}
function finishAdsConsentReprompt() {
transitionAdsConsent('completed')
clearTimeout(modrinthAdsConsentState.commitTimeout)
modrinthAdsConsentState.commitTimeout = null
document.documentElement.classList.remove('modrinth-ads-consent-overlay')
concealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'conceal' })
invokeAdsConsentOverlayCommand(false)
}
async function openAdsConsentPreferences() {
if (!(await showNativeAdsConsentUi())) return
await performAdsConsentActionAcrossFrames('manage', 2500)
}
async function performAdsConsentAction(action) {
@@ -237,16 +433,96 @@ async function performAdsConsentAction(action) {
return
}
await performAdsConsentActionAcrossFrames(action, 1000)
const handled = await performAdsConsentActionAcrossFrames(action, 2500)
if (!handled) {
try {
await showNativeAdsConsentUi()
} catch {
await restoreAdsConsentNotification()
}
}
}
window.modrinthAdsConsentAction = (action) => {
void performAdsConsentAction(action)
}
function isUspConsentApplicable() {
if (detectAdsConsentVariant() === 'usp') return Promise.resolve(true)
if (typeof window.__uspapi !== 'function') return Promise.resolve(false)
return new Promise((resolve) => {
let settled = false
const settle = (applicable) => {
if (settled) return
settled = true
clearTimeout(timeout)
resolve(applicable)
}
const timeout = setTimeout(() => settle(false), 500)
try {
window.__uspapi('uspPing', 1, (data, success) => {
if (!success || !data) {
settle(false)
return
}
const modes = Array.isArray(data.mode) ? data.mode : [data.mode]
const jurisdictions = Array.isArray(data.jurisdiction)
? data.jurisdiction
: [data.jurisdiction]
const location = String(data.location ?? '').toUpperCase()
const hasUspMode = modes.some((mode) =>
String(mode ?? '')
.toUpperCase()
.includes('USP'),
)
const locationApplies =
!location ||
jurisdictions.some((jurisdiction) =>
String(jurisdiction ?? '')
.toUpperCase()
.includes(location),
)
settle(hasUspMode && locationApplies)
})
} catch {
settle(false)
}
})
}
async function displayAdsConsentReprompt() {
if (
(modrinthAdsConsentState.variant === 'usp' || (await isUspConsentApplicable())) &&
typeof window.__uspapi === 'function'
) {
modrinthAdsConsentState.variant = 'usp'
return displayUspConsentUi()
}
if (typeof window.__tcfapi === 'function') {
modrinthAdsConsentState.variant = 'tcf'
window.__tcfapi('displayConsentUi', 2, () => {})
return true
}
return false
}
window.modrinthAdsReopenConsentPreferences = async () => {
modrinthAdsConsentReprompt = true
modrinthAdsConsentRepromptManaging = false
if (document.documentElement.classList.contains('modrinth-ads-consent-overlay')) {
try {
await openAdsConsentPreferences()
} catch {
await restoreAdsConsentNotification()
}
return
}
transitionAdsConsent('reprompt-started')
prepareAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'prepare' })
@@ -258,11 +534,17 @@ window.modrinthAdsReopenConsentPreferences = async () => {
window.dispatchEvent(new Event('resize'))
sendAdsConsentCommandToChildFrames({ type: 'resize' })
if (typeof window.__tcfapi === 'function') {
window.__tcfapi('displayConsentUi', 2, () => {})
} else {
if (!(await displayAdsConsentReprompt())) {
finishAdsConsentReprompt()
return
}
if (!(await performAdsConsentActionAcrossFrames('show', 2500))) {
finishAdsConsentReprompt()
return
}
await performAdsConsentActionAcrossFrames('manage', 2500)
} catch {
finishAdsConsentReprompt()
}
@@ -303,16 +585,15 @@ window.addEventListener('message', (event) => {
typeof command.timeoutMs === 'number'
) {
sendAdsConsentCommandToChildFrames(command)
clickAdsConsentButtonWhenReady(command.action, command.timeoutMs, () => {
performAdsConsentActionWhenReady(command.action, command.timeoutMs, () => {
window.parent.postMessage({ modrinthAdsConsentResult: command.requestId }, '*')
})
}
})
function setAdsConsentOverlay(shown) {
if (modrinthAdsConsentOverlayShown === shown) return
if (document.documentElement.classList.contains('modrinth-ads-consent-overlay') === shown) return
modrinthAdsConsentOverlayShown = shown
installAdsConsentOverlayStyle()
document.documentElement.classList.toggle('modrinth-ads-consent-overlay', shown)
if (!shown) {
@@ -329,36 +610,111 @@ function setAdsConsentOverlay(shown) {
if (window.top === window) {
window.addEventListener('message', (event) => {
if (
event.origin === MODRINTH_ORIGIN &&
typeof event.data?.modrinthAdsConsentOverlay === 'boolean'
) {
if (event.origin !== MODRINTH_ORIGIN) return
if (typeof event.data?.modrinthAdsConsentOverlay === 'boolean') {
setAdsConsentOverlay(event.data.modrinthAdsConsentOverlay)
}
})
}
function finishUspConsentCommit() {
if (!isUspConsentCommitPending()) return
const wasReprompt = isAdsConsentReprompt()
transitionAdsConsent('completed')
clearTimeout(modrinthAdsConsentState.commitTimeout)
modrinthAdsConsentState.commitTimeout = null
if (wasReprompt) {
finishAdsConsentReprompt()
} else {
setAdsConsentOverlay(false)
}
}
function beginUspConsentCommit() {
if (!document.getElementById('qc-cmp2-usp')) return
modrinthAdsConsentState.variant = 'usp'
transitionAdsConsent('commit-started')
clearTimeout(modrinthAdsConsentState.commitTimeout)
const deadline = Date.now() + 2500
function checkForDialogClosure() {
if (!isUspConsentCommitPending()) return
if (!document.getElementById('qc-cmp2-usp')) {
finishUspConsentCommit()
} else if (Date.now() >= deadline) {
transitionAdsConsent('commit-timed-out')
modrinthAdsConsentState.commitTimeout = null
} else {
modrinthAdsConsentState.commitTimeout = setTimeout(checkForDialogClosure, 50)
}
}
modrinthAdsConsentState.commitTimeout = setTimeout(checkForDialogClosure, 50)
}
function syncAdsConsentUi() {
const variant = detectAdsConsentVariant()
if (variant && !isAdsConsentReprompt() && modrinthAdsConsentState.phase !== 'complete') {
transitionAdsConsent('prompt-detected')
setAdsConsentOverlay(true)
}
if (isUspConsentCommitPending() && !document.getElementById('qc-cmp2-usp')) {
finishUspConsentCommit()
}
}
function handleGppConsentEvent(gppData, success) {
if (
success &&
gppData?.eventName === 'sectionChange' &&
modrinthAdsConsentState.variant === 'usp' &&
isUspConsentCommitPending()
) {
finishUspConsentCommit()
}
}
function installGppConsentListener() {
if (modrinthGppListenerInstalled) return
if (typeof window.__gpp === 'function') {
modrinthGppListenerInstalled = true
window.__gpp('addEventListener', handleGppConsentEvent)
return
}
if (modrinthGppListenerAttempts < 60) {
modrinthGppListenerAttempts += 1
setTimeout(installGppConsentListener, 500)
}
}
function handleTcfConsentEvent(tcData, success) {
if (!success || !tcData) return
detectAdsConsentVariant()
if (tcData.eventStatus === 'cmpuishown') {
if (modrinthAdsConsentReprompt) {
if (!modrinthAdsConsentRepromptManaging) {
modrinthAdsConsentRepromptManaging = true
window.dispatchEvent(new Event('resize'))
sendAdsConsentCommandToChildFrames({ type: 'resize' })
void performAdsConsentActionAcrossFrames('manage', 2500)
}
return
}
if (isAdsConsentReprompt()) return
transitionAdsConsent('prompt-detected')
setAdsConsentOverlay(true)
} else if (tcData.eventStatus === 'useractioncomplete') {
if (modrinthAdsConsentReprompt) {
} else if (
tcData.eventStatus === 'useractioncomplete' &&
modrinthAdsConsentState.variant === 'tcf'
) {
if (isAdsConsentReprompt()) {
finishAdsConsentReprompt()
return
}
transitionAdsConsent('completed')
setAdsConsentOverlay(false)
}
}
@@ -459,10 +815,17 @@ document.addEventListener('DOMContentLoaded', () => {
installAdsConsentOverlayStyle()
muteVideos()
muteAudioContext()
syncAdsConsentUi()
installTcfConsentListener()
installGppConsentListener()
const observer = new MutationObserver(muteVideos)
const observer = new MutationObserver(() => {
muteVideos()
syncAdsConsentUi()
})
observer.observe(document.body, { childList: true, subtree: true })
})
syncAdsConsentUi()
installTcfConsentListener()
installGppConsentListener()
+3
View File
@@ -483,6 +483,7 @@ pub async fn init_ads_window<R: Runtime>(
let webview = window.add_child(
tauri::webview::WebviewBuilder::new("ads-window", webview_url)
.incognito(true) // REMOVE WHEN DONE!!
.initialization_script_for_all_frames(include_str!(
"ads-init.js"
))
@@ -582,6 +583,8 @@ pub async fn init_ads_window<R: Runtime>(
}
})?;
webview.open_devtools();
Some(webview)
} else {
None