fix: handling failures/invalid popup edge cases to show normal gdpr popup fallback (#6857)

* fix: remove drilling actions down iframes

* refactor: split up custom popup logic, trim it down a lot

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

* remove open devtools

* format

* harden CPM fallbacks for unknown layouts

* consolidate timeouts

* refactor: rename functions to make sense

* fix: sidebar closed on launch does not properly set state
This commit is contained in:
Truman Gao
2026-07-24 16:48:52 +00:00
committed by GitHub
parent fbe70c6938
commit dfc2ed3783
12 changed files with 1128 additions and 1041 deletions
+2 -2
View File
@@ -93,10 +93,10 @@ import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { config } from '@/config'
import {
ads_consent_listener,
get_ads_consent_required,
hide_ads_window,
init_ads_window,
perform_ads_consent_action,
should_show_ads_consent_popup,
show_ads_window,
} from '@/helpers/ads.js'
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
@@ -339,7 +339,7 @@ onMounted(async () => {
await useCheckDisableMouseover()
try {
unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired)
handleAdsConsentRequired(await get_ads_consent_required())
handleAdsConsentRequired(await should_show_ads_consent_popup())
} catch (error) {
handleError(error)
}
+2 -2
View File
@@ -16,8 +16,8 @@ export async function hide_ads_window(reset) {
return await invoke('plugin:ads|hide_ads_window', { reset })
}
export async function get_ads_consent_required() {
return await invoke('plugin:ads|get_ads_consent_required')
export async function should_show_ads_consent_popup() {
return await invoke('plugin:ads|should_show_ads_consent_popup')
}
export async function perform_ads_consent_action(action) {
+4 -5
View File
@@ -306,12 +306,11 @@ fn main() {
"init_ads_window",
"hide_ads_window",
"show_ads_window",
"show_ads_consent_overlay",
"show_ads_consent_preferences",
"show_ads_consent_ui",
"expand_ads_consent_webview",
"open_ads_consent_preferences",
"hide_ads_consent_preferences",
"hide_ads_consent_overlay",
"get_ads_consent_required",
"finish_ads_consent_flow",
"should_show_ads_consent_popup",
"perform_ads_consent_action",
"record_ads_click",
"open_link",
+54
View File
@@ -0,0 +1,54 @@
const MODRINTH_ORIGIN = 'https://modrinth.com'
/**
* @typedef {'custom' | 'fallback' | 'hidden'} AdsConsentPopupMode
*
* @typedef {(command: string, args?: Record<string, unknown>) => Promise<unknown>} TauriInvoke
*/
/** @returns {TauriInvoke | undefined} */
function getTauriInvoke() {
const invoke = window.__TAURI__?.core?.invoke ?? window.__TAURI_INTERNALS__?.invoke
return typeof invoke === 'function' ? invoke : undefined
}
/** @returns {void} */
function notifyAdClick() {
window.top?.postMessage({ modrinthAdClick: true }, MODRINTH_ORIGIN)
}
/**
* @param {string | URL} url
* @returns {void}
*/
function openExternalUrl(url) {
window.top?.postMessage({ modrinthOpenUrl: String(url) }, MODRINTH_ORIGIN)
}
/**
* @param {AdsConsentPopupMode} mode
* @returns {Promise<void>}
*/
async function invokeAdsConsentPopupMode(mode) {
const invoke = getTauriInvoke()
if (!invoke) return
try {
if (mode === 'hidden') {
await invoke('plugin:ads|finish_ads_consent_flow', { dpr: window.devicePixelRatio })
return
}
await invoke('plugin:ads|show_ads_consent_ui', {
notificationEnabled: mode === 'custom',
})
} catch {}
}
/** @returns {Promise<void>} */
async function expandAdsConsentWebview() {
const invoke = getTauriInvoke()
if (!invoke) throw new Error('Tauri invoke is unavailable in the ads webview')
await invoke('plugin:ads|expand_ads_consent_webview')
}
+296
View File
@@ -0,0 +1,296 @@
/**
* @typedef {'accept' | 'reject' | 'manage'} ConsentAction
* @typedef {'handled' | 'not-ready' | 'failed'} ConsentActionResult
*
* @typedef {object} UspConsentControls
* @property {HTMLButtonElement[]} toggles
* @property {HTMLButtonElement} confirmButton
*
* @typedef {object} UspPingData
* @property {string | string[]} [mode]
* @property {string | string[]} [jurisdiction]
* @property {string} [location]
*/
/** @type {Readonly<Record<ConsentAction, string>>} */
const ACTION_BUTTON_IDS = {
accept: 'accept-btn',
reject: 'disagree-btn',
manage: 'more-options-btn',
}
/** @type {Readonly<Record<AdsConsentVariant, string>>} */
const DIALOG_IDS = {
tcf: 'qc-cmp2-ui',
usp: 'qc-cmp2-usp',
}
/** @type {Readonly<{ toggles: string, confirmButton: string }>} */
const USP_CONTROL_SELECTORS = {
toggles: '.qc-usp-container button.qc-cmp2-toggle[role="switch"]',
confirmButton: '.qc-usp-ui-form-content button[mode="primary"]',
}
/**
* @param {HTMLButtonElement | null} button
* @returns {boolean | null}
*/
function isButtonEnabled(button) {
return button && !button.disabled && button.getAttribute('aria-disabled') !== 'true'
}
/** @returns {AdsConsentVariant | null} */
function detectConsentVariant() {
if (document.getElementById(DIALOG_IDS.usp)) return 'usp'
if (document.getElementById(DIALOG_IDS.tcf)) return 'tcf'
return null
}
/**
* @param {ConsentAction} action
* @returns {HTMLButtonElement | null}
*/
function findTcfConsentButton(action) {
const dialog = document.getElementById(DIALOG_IDS.tcf)
if (!dialog) return null
const button = /** @type {HTMLButtonElement | null} */ (
dialog.querySelector(`#${ACTION_BUTTON_IDS[action]}`)
)
return isButtonEnabled(button) ? button : null
}
/** @returns {UspConsentControls | null} */
function getUspConsentControls() {
const dialog = document.getElementById(DIALOG_IDS.usp)
if (!dialog) return null
const toggles = Array.from(
/** @type {NodeListOf<HTMLButtonElement>} */ (
dialog.querySelectorAll(USP_CONTROL_SELECTORS.toggles)
),
)
const confirmButton = /** @type {HTMLButtonElement | null} */ (
dialog.querySelector(USP_CONTROL_SELECTORS.confirmButton)
)
if (
toggles.length === 0 ||
!isButtonEnabled(confirmButton) ||
toggles.some(
(toggle) =>
!isButtonEnabled(toggle) ||
!['true', 'false'].includes(toggle.getAttribute('aria-checked') ?? ''),
)
) {
return null
}
return { toggles, confirmButton }
}
/**
* @param {AdsConsentVariant | null} variant
* @returns {boolean}
*/
function areConsentControlsPresent(variant) {
if (variant === 'tcf') {
const dialog = document.getElementById(DIALOG_IDS.tcf)
return (
dialog !== null &&
/** @type {ConsentAction[]} */ (['accept', 'reject', 'manage']).every((action) =>
dialog.querySelector(`#${ACTION_BUTTON_IDS[action]}`),
)
)
}
if (variant === 'usp') {
const dialog = document.getElementById(DIALOG_IDS.usp)
return (
dialog !== null &&
dialog.querySelectorAll(USP_CONTROL_SELECTORS.toggles).length > 0 &&
dialog.querySelector(USP_CONTROL_SELECTORS.confirmButton) !== null
)
}
return false
}
/**
* @param {ConsentAction} action
* @param {AdsConsentVariant | null} variant
* @returns {boolean}
*/
function isConsentActionAvailable(action, variant) {
if (variant === 'tcf') return findTcfConsentButton(action) !== null
if (variant === 'usp') {
if (action === 'manage') return document.getElementById(DIALOG_IDS.usp) !== null
return getUspConsentControls() !== null
}
return false
}
/**
* @param {number} index
* @param {boolean} checked
* @param {number} expectedCount
* @param {number} deadline
* @returns {Promise<UspConsentControls | null>}
*/
function waitForUspToggleState(index, checked, expectedCount, deadline) {
return new Promise((resolve) => {
/** @returns {void} */
const 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(null)
} else {
setTimeout(checkState, 50)
}
}
checkState()
})
}
/**
* @param {boolean} checked
* @param {UspConsentControls} controls
* @param {number} timeoutMs
* @returns {Promise<UspConsentControls | null>}
*/
async function setUspToggleStates(checked, controls, timeoutMs) {
const expectedCount = controls.toggles.length
const deadline = Date.now() + timeoutMs
for (let index = 0; index < expectedCount; index += 1) {
const currentControls = getUspConsentControls()
if (!currentControls || currentControls.toggles.length !== expectedCount) return null
controls = currentControls
const toggle = controls.toggles[index]
if (toggle.getAttribute('aria-checked') !== String(checked)) {
toggle.click()
const settledControls = await waitForUspToggleState(index, checked, expectedCount, deadline)
if (!settledControls) return null
controls = settledControls
}
}
return controls
}
/**
* @param {ConsentAction} action
* @param {AdsConsentVariant | null} variant
* @param {(() => void) | undefined} onSubmit
* @returns {Promise<ConsentActionResult>}
*/
async function performDocumentConsentAction(action, variant, onSubmit) {
if (variant === 'usp') {
if (action === 'manage') {
return 'handled'
}
const controls = getUspConsentControls()
if (!controls) return 'not-ready'
const settledControls = await setUspToggleStates(action === 'reject', controls, 2_000)
if (!settledControls) return 'failed'
onSubmit?.()
settledControls.confirmButton.click()
return 'handled'
}
if (variant === 'tcf') {
const button = findTcfConsentButton(action)
if (!button) return 'not-ready'
onSubmit?.()
button.click()
return 'handled'
}
return 'not-ready'
}
/** @returns {boolean} */
function displayUspConsentUi() {
if (!window.__uspapi) return false
try {
window.__uspapi('displayUspUi', 1, () => {})
return true
} catch {
return false
}
}
/** @returns {Promise<boolean>} */
function isUspConsentApplicable() {
if (detectConsentVariant() === 'usp') return Promise.resolve(true)
if (!window.__uspapi) return Promise.resolve(false)
return new Promise((resolve) => {
let settled = false
/**
* @param {boolean} applicable
* @returns {void}
*/
const settle = (applicable) => {
if (settled) return
settled = true
clearTimeout(timeout)
resolve(applicable)
}
const timeout = setTimeout(() => settle(false), 500)
try {
window.__uspapi?.(
'uspPing',
1,
/**
* @param {UspPingData | null | undefined} data
* @param {boolean} success
* @returns {void}
*/
(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)
}
})
}
+354
View File
@@ -0,0 +1,354 @@
const LAYOUT_DELAY = 100
const CONSENT_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
/** @type {ReturnType<typeof setTimeout> | null} */
this.popupReadinessTimeout = 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')
if (!cmpMain) {
this.clearPopupReadinessTimeout()
if (this.state.phase === 'idle') {
document.documentElement.classList.remove('modrinth-ads-consent-overlay')
} else if (this.state.phase !== 'submitting-consent' && this.state.phase !== 'finishing') {
this.finishConsentFlow()
}
return
}
if (this.state.phase !== 'idle') return
document.documentElement.classList.add('modrinth-ads-consent-overlay')
const variant = this.detectVariant()
if (!areConsentControlsPresent(variant)) {
this.waitForConsentControls()
return
}
this.clearPopupReadinessTimeout()
this.state.setState('showing-popup')
this.setPopupMode('custom')
}
/** @returns {void} */
waitForConsentControls() {
if (this.popupReadinessTimeout) return
this.popupReadinessTimeout = setTimeout(() => {
this.popupReadinessTimeout = null
if (this.state.phase !== 'idle' || !document.getElementById('qc-cmp2-main')) return
const variant = this.detectVariant()
if (areConsentControlsPresent(variant)) {
this.syncConsentPopup()
return
}
this.state.setState('showing-popup')
this.showNativeCmpFallback()
}, CONSENT_TIMEOUT)
}
/** @returns {void} */
clearPopupReadinessTimeout() {
clearTimeout(this.popupReadinessTimeout ?? undefined)
this.popupReadinessTimeout = null
}
/**
* Sends the clicks from custom privacy popup to native popup buttons
* @param {ConsentAction} action
* @returns {Promise<void>}
*/
async performAction(action) {
if (!['accept', 'reject', 'manage'].includes(action)) return
if (action === 'manage') {
try {
await this.openPreferences()
} catch {
this.showNativeCmpFallback()
}
return
}
const variant = this.detectVariant()
if (!isConsentActionAvailable(action, variant)) {
this.showNativeCmpFallback()
return
}
try {
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
this.showNativeCmpFallback()
}
}, CONSENT_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 {void} */
handleTcfClose() {
if (this.state.variant !== 'tcf') return
if (this.state.phase === 'showing-preferences') {
this.state.setState('showing-popup')
this.concealPreferences()
this.setPopupMode('custom')
} else if (this.state.phase === 'showing-reopened-preferences') {
this.finishReopenedPopup()
}
}
/** @returns {Promise<void>} */
async reopenPreferences() {
if (document.documentElement.classList.contains('modrinth-ads-consent-overlay')) {
try {
await this.openPreferences()
} catch {
this.showNativeCmpFallback()
}
return
}
this.state.setState('showing-reopened-preferences')
this.preparePreferences()
try {
await this.waitForLayout()
await expandAdsConsentWebview()
await this.waitForLayout()
this.revealPreferences()
window.dispatchEvent(new Event('resize'))
if (!(await this.displayReopenedPopup())) {
this.finishReopenedPopup()
return
}
if (!(await this.openConsentManagerWhenReady(CONSENT_TIMEOUT))) {
this.showNativeCmpFallback()
}
} catch {
this.finishReopenedPopup()
}
}
/** @returns {AdsConsentVariant | null} */
detectVariant() {
const variant = detectConsentVariant()
if (variant) this.state.setVariant(variant)
return variant
}
/**
* @param {number} timeoutMs
* @returns {Promise<boolean>}
*/
async openConsentManagerWhenReady(timeoutMs) {
const deadline = Date.now() + timeoutMs
while (true) {
try {
const result = await performDocumentConsentAction('manage', this.detectVariant())
if (result === 'handled') return true
if (result === 'failed' || Date.now() >= deadline) return false
} catch {
return false
}
await new Promise((resolve) => setTimeout(resolve, 200))
}
}
/** @returns {void} */
preparePreferences() {
document.documentElement.classList.add('modrinth-ads-consent-preferences')
document.documentElement.classList.remove('modrinth-ads-consent-preferences-visible')
}
/** @returns {void} */
revealPreferences() {
document.documentElement.classList.add('modrinth-ads-consent-preferences-visible')
}
/** @returns {void} */
concealPreferences() {
document.documentElement.classList.remove('modrinth-ads-consent-preferences')
document.documentElement.classList.remove('modrinth-ads-consent-preferences-visible')
}
/** @returns {Promise<void>} */
waitForLayout() {
return new Promise((resolve) => setTimeout(resolve, LAYOUT_DELAY))
}
/** @returns {Promise<void>} */
async showExpandedUi() {
this.preparePreferences()
await this.waitForLayout()
await expandAdsConsentWebview()
await this.waitForLayout()
this.revealPreferences()
window.dispatchEvent(new Event('resize'))
}
/** @returns {Promise<boolean>} */
async openPreferences() {
this.state.setState('showing-preferences')
await this.showExpandedUi()
if (!(await this.openConsentManagerWhenReady(CONSENT_TIMEOUT))) {
this.showNativeCmpFallback()
return false
}
return true
}
/** @returns {Promise<boolean>} */
async displayReopenedPopup() {
if ((this.state.variant === 'usp' || (await isUspConsentApplicable())) && window.__uspapi) {
this.state.setVariant('usp')
return displayUspConsentUi()
}
if (!window.__tcfapi) return false
this.state.setVariant('tcf')
window.__tcfapi('displayConsentUi', 2, () => {})
return true
}
/**
* @param {AdsConsentPopupMode} mode
* @returns {void}
*/
setPopupMode(mode) {
const shown = mode !== 'hidden'
const hidden = mode === 'hidden'
document.documentElement.classList.toggle('modrinth-ads-consent-overlay', shown)
document.documentElement.classList.toggle('modrinth-ads-consent-fallback', mode === 'fallback')
if (hidden) this.concealPreferences()
void invokeAdsConsentPopupMode(mode)
}
/** @returns {void} */
showNativeCmpFallback() {
this.concealPreferences()
this.setPopupMode('fallback')
}
/** @returns {void} */
finishReopenedPopup() {
this.finishConsentFlow()
}
/** @returns {void} */
finishConsentFlow() {
if (this.state.phase === 'idle' || this.state.phase === 'finishing') {
return
}
clearTimeout(this.submissionTimeout ?? undefined)
this.submissionTimeout = null
this.clearPopupReadinessTimeout()
this.preSubmissionPhase = null
this.state.setState('finishing')
this.setPopupMode('hidden')
}
}
+101
View File
@@ -0,0 +1,101 @@
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()
}
document.addEventListener(
'click',
(event) => {
notifyAdClick()
const target = event.target instanceof Element ? event.target : null
if (target?.closest('.qc-cmp2-close-icon')) {
setTimeout(() => controller.handleTcfClose())
}
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()
openExternalUrl(link.href)
}
},
true,
)
window.open = (url) => {
if (url) openExternalUrl(url)
return null
}
window.modrinthPrivacy ??= {}
window.modrinthPrivacy.adsConsentAction = (action) => void controller.performAction(action)
window.modrinthPrivacy.adsReopenConsentPreferences = () => controller.reopenPreferences()
document.addEventListener('DOMContentLoaded', () => {
muteMediaElements()
muteAudioContext()
initializeTopFrame()
const observer = new MutationObserver(() => {
muteMediaElements()
if (isTopFrame()) controller.syncConsentPopup()
})
observer.observe(document.body, { childList: true, subtree: true })
})
initializeTopFrame()
+73
View File
@@ -0,0 +1,73 @@
function muteAudioContext() {
const AudioContextClass = window.AudioContext ?? window.webkitAudioContext
if (!AudioContextClass) return
const prototype = AudioContextClass.prototype
const originalCreateGain = prototype.createGain
const originalCreateMediaElementSource = prototype.createMediaElementSource
const originalCreateMediaStreamSource = prototype.createMediaStreamSource
const originalCreateMediaStreamTrackSource = prototype.createMediaStreamTrackSource
const originalCreateBufferSource = prototype.createBufferSource
const originalCreateOscillator = prototype.createOscillator
prototype.createGain = function () {
const gain = originalCreateGain.call(this)
gain.gain.value = 0
return gain
}
prototype.createMediaElementSource = function (mediaElement) {
const source = originalCreateMediaElementSource.call(this, mediaElement)
source.connect(this.createGain())
return source
}
prototype.createMediaStreamSource = function (mediaStream) {
const source = originalCreateMediaStreamSource.call(this, mediaStream)
source.connect(this.createGain())
return source
}
if (originalCreateMediaStreamTrackSource) {
prototype.createMediaStreamTrackSource = function (mediaStreamTrack) {
const source = originalCreateMediaStreamTrackSource.call(this, mediaStreamTrack)
source.connect(this.createGain())
return source
}
}
prototype.createBufferSource = function () {
const source = originalCreateBufferSource.call(this)
source.connect(this.createGain())
return source
}
prototype.createOscillator = function () {
const oscillator = originalCreateOscillator.call(this)
oscillator.connect(this.createGain())
return oscillator
}
}
function muteMediaElement(mediaElement) {
const muteCount = Number(mediaElement.dataset.modrinthMutedCount ?? 0)
if (!mediaElement.muted || mediaElement.volume !== 0) {
mediaElement.muted = true
mediaElement.volume = 0
mediaElement.dataset.modrinthMutedCount = String(muteCount + 1)
}
if (muteCount > 5) mediaElement.remove()
}
function muteMediaElements() {
document.querySelectorAll('video, audio').forEach((mediaElement) => {
muteMediaElement(mediaElement)
if (!mediaElement.dataset.modrinthMuted) {
mediaElement.addEventListener('volumechange', () => muteMediaElement(mediaElement))
mediaElement.dataset.modrinthMuted = 'true'
}
})
}
+24
View File
@@ -0,0 +1,24 @@
/**
* @typedef {'idle' | 'showing-popup' | 'showing-preferences' | 'showing-reopened-preferences' | 'submitting-consent' | 'finishing'} AdsConsentPhase
* @typedef {'usp' | 'tcf'} AdsConsentVariant
*/
class AdsConsentState {
constructor() {
/** @type {AdsConsentPhase} */
this.phase = 'idle'
/** @type {AdsConsentVariant | null} */
this.variant = null
}
/** @param {AdsConsentPhase} phase */
setState(phase) {
this.phase = phase
}
/** @param {AdsConsentVariant} variant */
setVariant(variant) {
this.variant = variant
}
}
+160
View File
@@ -0,0 +1,160 @@
const THEME_STYLE = `
:root {
--modrinth-usp-bg: #27292e;
--modrinth-usp-surface: #34363c;
--modrinth-usp-divider: #34363c;
--modrinth-usp-text: #b0bac5;
--modrinth-usp-contrast: #ffffff;
--modrinth-usp-brand: #1bd96a;
--modrinth-usp-link: #4f9cff;
--modrinth-usp-accent-contrast: #000000;
--modrinth-usp-shadow: rgba(0, 0, 0, 0.1) 0 4px 6px -1px,
rgba(0, 0, 0, 0.06) 0 2px 4px -1px;
color-scheme: dark;
}
#qc-cmp2-usp {
outline: none !important;
background: var(--modrinth-usp-bg) !important;
border: 1px solid var(--modrinth-usp-divider) !important;
border-radius: 1rem !important;
box-shadow: var(--modrinth-usp-shadow) !important;
color: var(--modrinth-usp-text) !important;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif !important;
max-width: 660px;
}
#qc-cmp2-usp .qc-usp-ui-content,
#qc-cmp2-usp .qc-usp-ui-form-content,
#qc-cmp2-usp .qc-usp-container {
background: transparent !important;
}
#qc-cmp2-usp .qc-usp-container {
margin-bottom: 12px;
}
#qc-cmp2-usp p,
#qc-cmp2-usp label,
#qc-cmp2-usp .qc-usp-action-description {
color: var(--modrinth-usp-text) !important;
font-family: inherit !important;
}
#qc-cmp2-usp .qc-usp-title,
#qc-cmp2-usp .qc-cmp2-list-item-title {
color: var(--modrinth-usp-contrast) !important;
font-family: inherit !important;
font-weight: 700 !important;
}
#qc-cmp2-usp .qc-usp-title {
font-size: 1.25rem !important;
}
#qc-cmp2-usp a,
#qc-cmp2-usp .qc-usp-alt-action {
color: var(--modrinth-usp-link) !important;
}
#qc-cmp2-usp .qc-cmp2-list-item {
background: var(--modrinth-usp-surface) !important;
border: 1px solid var(--modrinth-usp-divider) !important;
border-radius: 0.75rem !important;
}
#qc-cmp2-usp .qc-cmp2-list-item-header {
background: transparent !important;
border: 0 !important;
color: var(--modrinth-usp-contrast) !important;
}
#qc-cmp2-usp .qc-cmp2-list-item-header svg {
color: var(--modrinth-usp-text) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle {
background: var(--modrinth-usp-bg) !important;
border-color: var(--modrinth-usp-divider) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle .toggle {
background: var(--modrinth-usp-contrast) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle .text {
color: var(--modrinth-usp-contrast) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle[aria-checked='true'] {
background: var(--modrinth-usp-brand) !important;
border-color: var(--modrinth-usp-brand) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle[aria-checked='true'] .text {
color: var(--modrinth-usp-accent-contrast) !important;
}
#qc-cmp2-usp button[mode='primary'] {
background: var(--modrinth-usp-brand) !important;
border: 0 !important;
border-radius: 0.75rem !important;
color: var(--modrinth-usp-accent-contrast) !important;
font-family: inherit !important;
font-weight: 700 !important;
}
#qc-cmp2-usp .qc-usp-close-icon {
border: 0 !important;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E")
center / 1.5rem 1.5rem no-repeat;
}
#qc-cmp2-usp a:focus-visible,
#qc-cmp2-usp button:focus-visible {
outline: 2px solid var(--modrinth-usp-brand) !important;
outline-offset: 2px !important;
}
#qc-cmp2-usp .qc-usp-ui-content {
max-width: 100% !important;
}
#qc-cmp2-usp .qc-usp-ui-content .qc-usp-ui-form-content {
border: 1px solid transparent !important;
padding: 0 !important;
}
`
const RAIL_STYLE = `
html.modrinth-ads-consent-preferences #modrinth-rail-1 {
display: none !important;
}
`
const OVERLAY_STYLE = `
html.modrinth-ads-consent-overlay:not(.modrinth-ads-consent-fallback):not(.modrinth-ads-consent-preferences) #qc-cmp2-main,
html.modrinth-ads-consent-preferences:not(.modrinth-ads-consent-preferences-visible) #qc-cmp2-main {
display: none !important;
}
#qc-cmp2-usp .qc-usp-close-icon {
display: none !important;
}
`
function installStyle(id, css) {
if (document.getElementById(id)) return
const style = document.createElement('style')
style.id = id
style.textContent = css
document.documentElement.appendChild(style)
}
function installConsentStyles() {
installStyle('modrinth-ads-consent-theme-style', THEME_STYLE)
installStyle('modrinth-ads-rail-style', RAIL_STYLE)
installStyle('modrinth-ads-consent-overlay-style', OVERLAY_STYLE)
}
-967
View File
@@ -1,967 +0,0 @@
const MODRINTH_ORIGIN = 'https://modrinth.com'
function installAdsConsentThemeStyle() {
if (document.getElementById('modrinth-ads-consent-theme-style')) return
const style = document.createElement('style')
style.id = 'modrinth-ads-consent-theme-style'
style.textContent = `
:root {
--modrinth-usp-bg: #27292e;
--modrinth-usp-surface: #34363c;
--modrinth-usp-divider: #34363c;
--modrinth-usp-text: #b0bac5;
--modrinth-usp-contrast: #ffffff;
--modrinth-usp-brand: #1bd96a;
--modrinth-usp-link: #4f9cff;
--modrinth-usp-accent-contrast: #000000;
--modrinth-usp-shadow: rgba(0, 0, 0, 0.1) 0 4px 6px -1px,
rgba(0, 0, 0, 0.06) 0 2px 4px -1px;
color-scheme: dark;
}
#qc-cmp2-usp {
background: var(--modrinth-usp-bg) !important;
border: 1px solid var(--modrinth-usp-divider) !important;
border-radius: 1rem !important;
box-shadow: var(--modrinth-usp-shadow) !important;
color: var(--modrinth-usp-text) !important;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif !important;
max-width: 660px;
}
#qc-cmp2-usp .qc-usp-ui-content,
#qc-cmp2-usp .qc-usp-ui-form-content,
#qc-cmp2-usp .qc-usp-container {
background: transparent !important;
}
#qc-cmp2-usp .qc-usp-container{
margin-bottom: 12px;
}
#qc-cmp2-usp p,
#qc-cmp2-usp label,
#qc-cmp2-usp .qc-usp-action-description {
color: var(--modrinth-usp-text) !important;
font-family: inherit !important;
}
#qc-cmp2-usp .qc-usp-title,
#qc-cmp2-usp .qc-cmp2-list-item-title {
color: var(--modrinth-usp-contrast) !important;
font-family: inherit !important;
font-weight: 700 !important;
}
#qc-cmp2-usp .qc-usp-title {
font-size: 1.25rem !important;
}
#qc-cmp2-usp a,
#qc-cmp2-usp .qc-usp-alt-action {
color: var(--modrinth-usp-link) !important;
}
#qc-cmp2-usp .qc-cmp2-list-item {
background: var(--modrinth-usp-surface) !important;
border: 1px solid var(--modrinth-usp-divider) !important;
border-radius: 0.75rem !important;
}
#qc-cmp2-usp .qc-cmp2-list-item-header {
background: transparent !important;
border: 0 !important;
color: var(--modrinth-usp-contrast) !important;
}
#qc-cmp2-usp .qc-cmp2-list-item-header svg {
color: var(--modrinth-usp-text) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle {
background: var(--modrinth-usp-bg) !important;
border-color: var(--modrinth-usp-divider) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle .toggle {
background: var(--modrinth-usp-contrast) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle .text {
color: var(--modrinth-usp-contrast) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle[aria-checked='true'] {
background: var(--modrinth-usp-brand) !important;
border-color: var(--modrinth-usp-brand) !important;
}
#qc-cmp2-usp .qc-cmp2-toggle[aria-checked='true'] .text {
color: var(--modrinth-usp-accent-contrast) !important;
}
#qc-cmp2-usp button[mode='primary'] {
background: var(--modrinth-usp-brand) !important;
border: 0 !important;
border-radius: 0.75rem !important;
color: var(--modrinth-usp-accent-contrast) !important;
font-family: inherit !important;
font-weight: 700 !important;
}
#qc-cmp2-usp .qc-usp-close-icon {
border: 0 !important;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E")
center / 1.5rem 1.5rem no-repeat;
}
#qc-cmp2-usp a:focus-visible,
#qc-cmp2-usp button:focus-visible {
outline: 2px solid var(--modrinth-usp-brand) !important;
outline-offset: 2px !important;
}
#qc-cmp2-usp .qc-usp-ui-content {
max-width: 100% !important;
}
#qc-cmp2-usp .qc-usp-ui-content .qc-usp-ui-form-content {
border: 1px solid transparent !important;
padding: 0 !important;
}
`
document.documentElement.appendChild(style)
}
document.addEventListener(
'click',
function (e) {
window.top.postMessage({ modrinthAdClick: true }, MODRINTH_ORIGIN)
let target = e.target
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()
if (target.href) {
window.top.postMessage({ modrinthOpenUrl: target.href }, MODRINTH_ORIGIN)
}
break
}
target = target.parentElement
}
},
true,
)
window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, MODRINTH_ORIGIN)
}
const modrinthAdsConsentState = {
phase: 'idle',
variant: null,
commitTimeout: null,
}
let modrinthTcfListenerInstalled = false
let modrinthTcfListenerAttempts = 0
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
}
const style = document.createElement('style')
style.id = 'modrinth-ads-rail-style'
style.textContent = `
html.modrinth-ads-consent-preferences #modrinth-rail-1 {
display: none !important;
}
`
document.documentElement.appendChild(style)
}
function installAdsConsentOverlayStyle() {
if (document.getElementById('modrinth-ads-consent-overlay-style')) {
return
}
const style = document.createElement('style')
style.id = 'modrinth-ads-consent-overlay-style'
style.textContent = `
html.modrinth-ads-consent-overlay:not(.modrinth-ads-consent-preferences) #qc-cmp2-container,
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)
}
function getTauriInvoke() {
return window.__TAURI__?.core?.invoke ?? window.__TAURI_INTERNALS__?.invoke
}
function invokeAdsConsentOverlayCommand(shown) {
const invoke = getTauriInvoke()
if (typeof invoke !== 'function') {
return
}
const command = shown ? 'show_ads_consent_overlay' : 'hide_ads_consent_overlay'
const args = shown ? {} : { dpr: window.devicePixelRatio }
invoke(`plugin:ads|${command}`, args).catch(() => {})
}
function prepareAdsConsentPreferences() {
installAdsRailStyle()
installAdsConsentOverlayStyle()
document.documentElement.classList.add('modrinth-ads-consent-preferences')
document.documentElement.classList.remove('modrinth-ads-consent-preferences-visible')
}
function revealAdsConsentPreferences() {
document.documentElement.classList.add('modrinth-ads-consent-preferences-visible')
}
function concealAdsConsentPreferences() {
document.documentElement.classList.remove('modrinth-ads-consent-preferences')
document.documentElement.classList.remove('modrinth-ads-consent-preferences-visible')
installAdsConsentOverlayStyle()
}
async function expandAdsConsentWebview() {
const invoke = getTauriInvoke()
if (typeof invoke !== 'function') {
throw new Error('Tauri invoke is unavailable in the ads webview')
}
await invoke('plugin:ads|show_ads_consent_preferences')
}
function sendAdsConsentCommandToChildFrames(command) {
document.querySelectorAll('iframe').forEach((frame) => {
frame.contentWindow?.postMessage({ modrinthAdsConsentCommand: command }, '*')
})
}
function isDirectChildFrame(source) {
return Array.from(document.querySelectorAll('iframe')).some(
(frame) => frame.contentWindow === source,
)
}
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',
reject: 'disagree-btn',
manage: 'more-options-btn',
}
const buttonId = buttonIds[action]
if (!buttonId) return null
const button = dialog.querySelector(`#${buttonId}`)
return button && !button.disabled ? button : null
}
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 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(null)
} else {
setTimeout(checkState, 50)
}
}
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()
})
}
function performAdsConsentActionAcrossFrames(action, timeoutMs) {
const requestId = `${Date.now()}-${++modrinthAdsConsentActionRequestId}`
return new Promise((resolve) => {
let settled = false
const settle = (clicked) => {
if (settled) return
settled = true
clearTimeout(timeout)
modrinthAdsConsentActionResolvers.delete(requestId)
resolve(clicked)
}
const timeout = setTimeout(() => settle(false), timeoutMs)
modrinthAdsConsentActionResolvers.set(requestId, () => settle(true))
sendAdsConsentCommandToChildFrames({ type: 'perform', action, requestId, timeoutMs })
void performAdsConsentActionWhenReady(action, timeoutMs, () => settle(true))
})
}
function waitForAdsConsentLayout() {
return new Promise((resolve) => setTimeout(resolve, 100))
}
async function restoreAdsConsentNotification() {
concealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'conceal' })
const invoke = getTauriInvoke()
if (typeof invoke === 'function') {
await invoke('plugin:ads|hide_ads_consent_preferences')
}
}
async function showNativeAdsConsentUi() {
prepareAdsConsentPreferences()
await waitForAdsConsentLayout()
sendAdsConsentCommandToChildFrames({ type: 'prepare' })
await expandAdsConsentWebview()
await waitForAdsConsentLayout()
revealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'reveal' })
window.dispatchEvent(new Event('resize'))
sendAdsConsentCommandToChildFrames({ type: 'resize' })
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) {
if (!['accept', 'reject', 'manage'].includes(action)) return
if (action === 'manage') {
try {
await openAdsConsentPreferences()
} catch {
await restoreAdsConsentNotification()
}
return
}
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 () => {
if (document.documentElement.classList.contains('modrinth-ads-consent-overlay')) {
try {
await openAdsConsentPreferences()
} catch {
await restoreAdsConsentNotification()
}
return
}
transitionAdsConsent('reprompt-started')
prepareAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'prepare' })
try {
await waitForAdsConsentLayout()
await expandAdsConsentWebview()
await waitForAdsConsentLayout()
revealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames({ type: 'reveal' })
window.dispatchEvent(new Event('resize'))
sendAdsConsentCommandToChildFrames({ type: 'resize' })
if (!(await displayAdsConsentReprompt())) {
finishAdsConsentReprompt()
return
}
if (!(await performAdsConsentActionAcrossFrames('show', 2500))) {
finishAdsConsentReprompt()
return
}
await performAdsConsentActionAcrossFrames('manage', 2500)
} catch {
finishAdsConsentReprompt()
}
}
window.addEventListener('message', (event) => {
const resultRequestId = event.data?.modrinthAdsConsentResult
if (typeof resultRequestId === 'string' && isDirectChildFrame(event.source)) {
if (window.top === window) {
modrinthAdsConsentActionResolvers.get(resultRequestId)?.()
} else {
window.parent.postMessage({ modrinthAdsConsentResult: resultRequestId }, '*')
}
return
}
if (window.top === window || event.source !== window.parent) return
const command = event.data?.modrinthAdsConsentCommand
if (!command || typeof command !== 'object') return
if (command.type === 'prepare') {
prepareAdsConsentPreferences()
sendAdsConsentCommandToChildFrames(command)
} else if (command.type === 'reveal') {
revealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames(command)
} else if (command.type === 'conceal') {
concealAdsConsentPreferences()
sendAdsConsentCommandToChildFrames(command)
} else if (command.type === 'resize') {
window.dispatchEvent(new Event('resize'))
sendAdsConsentCommandToChildFrames(command)
} else if (
command.type === 'perform' &&
typeof command.action === 'string' &&
typeof command.requestId === 'string' &&
typeof command.timeoutMs === 'number'
) {
sendAdsConsentCommandToChildFrames(command)
performAdsConsentActionWhenReady(command.action, command.timeoutMs, () => {
window.parent.postMessage({ modrinthAdsConsentResult: command.requestId }, '*')
})
}
})
function setAdsConsentOverlay(shown) {
if (document.documentElement.classList.contains('modrinth-ads-consent-overlay') === shown) return
installAdsConsentOverlayStyle()
document.documentElement.classList.toggle('modrinth-ads-consent-overlay', shown)
if (!shown) {
document.documentElement.classList.remove('modrinth-ads-consent-preferences')
document.documentElement.classList.remove('modrinth-ads-consent-preferences-visible')
}
if (window.top === window) {
invokeAdsConsentOverlayCommand(shown)
} else {
window.top.postMessage({ modrinthAdsConsentOverlay: shown }, MODRINTH_ORIGIN)
}
}
if (window.top === window) {
window.addEventListener('message', (event) => {
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 (isAdsConsentReprompt()) return
transitionAdsConsent('prompt-detected')
setAdsConsentOverlay(true)
} else if (
tcData.eventStatus === 'useractioncomplete' &&
modrinthAdsConsentState.variant === 'tcf'
) {
if (isAdsConsentReprompt()) {
finishAdsConsentReprompt()
return
}
transitionAdsConsent('completed')
setAdsConsentOverlay(false)
}
}
// polling to install listener on tcf api
function installTcfConsentListener() {
if (modrinthTcfListenerInstalled) return
if (typeof window.__tcfapi === 'function') {
modrinthTcfListenerInstalled = true
window.__tcfapi('addEventListener', 2, handleTcfConsentEvent)
return
}
if (modrinthTcfListenerAttempts < 60) {
modrinthTcfListenerAttempts += 1
setTimeout(installTcfConsentListener, 500)
}
}
function muteAudioContext() {
if (window.AudioContext || window.webkitAudioContext) {
const AudioContext = window.AudioContext || window.webkitAudioContext
const originalCreateMediaElementSource = AudioContext.prototype.createMediaElementSource
const originalCreateMediaStreamSource = AudioContext.prototype.createMediaStreamSource
const originalCreateMediaStreamTrackSource = AudioContext.prototype.createMediaStreamTrackSource
const originalCreateBufferSource = AudioContext.prototype.createBufferSource
const originalCreateOscillator = AudioContext.prototype.createOscillator
AudioContext.prototype.createGain = function () {
const gain = originalCreateGain.call(this)
gain.gain.value = 0
return gain
}
AudioContext.prototype.createMediaElementSource = function (mediaElement) {
const source = originalCreateMediaElementSource.call(this, mediaElement)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createMediaStreamSource = function (mediaStream) {
const source = originalCreateMediaStreamSource.call(this, mediaStream)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createMediaStreamTrackSource = function (mediaStreamTrack) {
const source = originalCreateMediaStreamTrackSource.call(this, mediaStreamTrack)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createBufferSource = function () {
const source = originalCreateBufferSource.call(this)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createOscillator = function () {
const oscillator = originalCreateOscillator.call(this)
oscillator.connect(this.createGain())
return oscillator
}
}
}
function muteVideo(mediaElement) {
let count = Number(mediaElement.getAttribute('data-modrinth-muted-count') ?? 0)
if (!mediaElement.muted || mediaElement.volume !== 0) {
mediaElement.muted = true
mediaElement.volume = 0
mediaElement.setAttribute('data-modrinth-muted-count', count + 1)
}
if (count > 5) {
// Video is detected as malicious, so it is removed from the page
mediaElement.remove()
}
}
function muteVideos() {
document.querySelectorAll('video, audio').forEach(function (mediaElement) {
muteVideo(mediaElement)
if (!mediaElement.hasAttribute('data-modrinth-muted')) {
mediaElement.addEventListener('volumechange', () => muteVideo(mediaElement))
mediaElement.setAttribute('data-modrinth-muted', 'true')
}
})
}
document.addEventListener('DOMContentLoaded', () => {
installAdsConsentThemeStyle()
installAdsRailStyle()
installAdsConsentOverlayStyle()
muteVideos()
muteAudioContext()
syncAdsConsentUi()
installTcfConsentListener()
installGppConsentListener()
const observer = new MutationObserver(() => {
muteVideos()
syncAdsConsentUi()
})
observer.observe(document.body, { childList: true, subtree: true })
})
syncAdsConsentUi()
installTcfConsentListener()
installGppConsentListener()
+58 -65
View File
@@ -12,6 +12,7 @@ pub struct AdsState {
pub shown: bool,
pub modal_shown: bool,
pub consent_required: bool,
pub consent_notification_enabled: bool,
pub consent_overlay_shown: bool,
pub occluded: bool,
pub last_click: Option<Instant>,
@@ -278,6 +279,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
shown: true,
modal_shown: false,
consent_required: false,
consent_notification_enabled: false,
consent_overlay_shown: false,
occluded: false,
last_click: None,
@@ -361,12 +363,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
init_ads_window,
hide_ads_window,
show_ads_window,
show_ads_consent_overlay,
show_ads_consent_preferences,
show_ads_consent_ui,
expand_ads_consent_webview,
open_ads_consent_preferences,
hide_ads_consent_preferences,
hide_ads_consent_overlay,
get_ads_consent_required,
finish_ads_consent_flow,
should_show_ads_consent_popup,
perform_ads_consent_action,
record_ads_click,
open_link,
@@ -475,6 +476,19 @@ pub async fn init_ads_window<R: Runtime>(
Some(webview.clone())
} else if let Some(window) = app.get_window("main") {
let ads_consent_script = [
"(() => {",
include_str!("ads-consent/state.js"),
include_str!("ads-consent/styles.js"),
include_str!("ads-consent/bridge.js"),
include_str!("ads-consent/cmp.js"),
include_str!("ads-consent/media.js"),
include_str!("ads-consent/controller.js"),
include_str!("ads-consent/index.js"),
"})()",
]
.join("\n");
#[cfg(windows)]
let webview_url =
WebviewUrl::External("about:blank".parse().unwrap());
@@ -483,9 +497,7 @@ pub async fn init_ads_window<R: Runtime>(
let webview = window.add_child(
tauri::webview::WebviewBuilder::new("ads-window", webview_url)
.initialization_script_for_all_frames(include_str!(
"ads-init.js"
))
.initialization_script_for_all_frames(ads_consent_script)
// We use a standard Chrome user agent for compatibility with our ad provider,
// since Tauri is not recognized by ad providers by default.
// Aditude has separately informed SSPs and IVT vendors that this traffic
@@ -644,7 +656,10 @@ pub async fn init_ads_window<R: Runtime>(
// });
}
if state.shown && state.consent_required {
if state.shown
&& state.consent_required
&& state.consent_notification_enabled
{
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
}
@@ -661,7 +676,7 @@ pub async fn show_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
dpr: f32,
) -> crate::api::Result<()> {
let mut consent_required = false;
let mut show_consent_notification = false;
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
@@ -683,10 +698,12 @@ pub async fn show_ads_window<R: Runtime>(
set_webview_visible_for_window(&app, webview, true);
}
consent_required = state.shown && state.consent_required;
show_consent_notification = state.shown
&& state.consent_required
&& state.consent_notification_enabled;
}
if consent_required {
if show_consent_notification {
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
}
@@ -738,22 +755,24 @@ pub async fn hide_ads_window<R: Runtime>(
}
#[tauri::command]
pub async fn show_ads_consent_overlay<R: Runtime>(
pub async fn show_ads_consent_ui<R: Runtime>(
app: tauri::AppHandle<R>,
notification_enabled: bool,
) -> crate::api::Result<()> {
let mut show_notification = false;
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
// dont show for hidden ads so consent events cannot re-enable the webview.
if !state.shown {
return Ok(());
}
// Preserve pending consent while the sidebar is hidden, but keep all visibility
// changes gated by `state.shown` so consent events cannot re-enable hidden ads.
state.consent_required = true;
state.consent_notification_enabled = notification_enabled;
state.consent_overlay_shown = false;
show_notification = state.shown && notification_enabled;
if !state.modal_shown {
if state.shown && !state.modal_shown {
let dpr = get_device_pixel_ratio(&app, None);
let (position, size) = get_webview_position(&app, dpr)?;
webview.set_size(size).ok();
@@ -763,13 +782,14 @@ pub async fn show_ads_consent_overlay<R: Runtime>(
}
}
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, show_notification)
.ok();
Ok(())
}
#[tauri::command]
pub async fn show_ads_consent_preferences<R: Runtime>(
pub async fn expand_ads_consent_webview<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
if let Some(webview) = app.webviews().get("ads-window") {
@@ -807,53 +827,21 @@ pub async fn open_ads_consent_preferences<R: Runtime>(
{
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if !state.consent_required {
state.consent_notification_enabled = false;
}
state.consent_required = true;
state.consent_overlay_shown = false;
}
webview.eval("window.modrinthAdsReopenConsentPreferences?.()")?;
Ok(())
}
/// Restores the ad inventory bounds without resolving the pending consent request.
#[tauri::command]
pub async fn hide_ads_consent_preferences<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.consent_overlay_shown = false;
if state.shown && !state.modal_shown {
let dpr = get_device_pixel_ratio(&app, None);
let (position, size) = get_webview_position(&app, dpr)?;
webview
.set_bounds(Rect {
position: position.into(),
size: size.into(),
})
.ok();
webview.show().ok();
set_webview_visible_for_window(&app, webview, true);
} else {
webview
.set_position(PhysicalPosition::new(-1000, -1000))
.ok();
webview.hide().ok();
}
}
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
webview.eval("window.modrinthPrivacy?.adsReopenConsentPreferences?.()")?;
Ok(())
}
#[tauri::command]
pub async fn hide_ads_consent_overlay<R: Runtime>(
pub async fn finish_ads_consent_flow<R: Runtime>(
app: tauri::AppHandle<R>,
dpr: Option<f32>,
) -> crate::api::Result<()> {
@@ -863,6 +851,7 @@ pub async fn hide_ads_consent_overlay<R: Runtime>(
let should_reload_ads = state.consent_required;
state.consent_required = false;
state.consent_notification_enabled = false;
state.consent_overlay_shown = false;
if state.shown && !state.modal_shown {
@@ -893,13 +882,15 @@ pub async fn hide_ads_consent_overlay<R: Runtime>(
}
#[tauri::command]
pub async fn get_ads_consent_required<R: Runtime>(
pub async fn should_show_ads_consent_popup<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<bool> {
let state = app.state::<RwLock<AdsState>>();
let state = state.read().await;
Ok(state.shown && state.consent_required)
Ok(state.shown
&& state.consent_required
&& state.consent_notification_enabled)
}
#[tauri::command]
@@ -908,16 +899,18 @@ pub async fn perform_ads_consent_action<R: Runtime>(
action: String,
) -> crate::api::Result<()> {
let script = match action.as_str() {
"accept" => "window.modrinthAdsConsentAction?.('accept')",
"reject" => "window.modrinthAdsConsentAction?.('reject')",
"manage" => "window.modrinthAdsConsentAction?.('manage')",
"accept" => "window.modrinthPrivacy?.adsConsentAction?.('accept')",
"reject" => "window.modrinthPrivacy?.adsConsentAction?.('reject')",
"manage" => "window.modrinthPrivacy?.adsConsentAction?.('manage')",
_ => return Ok(()),
};
let state = app.state::<RwLock<AdsState>>();
let should_perform = {
let state = state.read().await;
state.shown && state.consent_required
state.shown
&& state.consent_required
&& state.consent_notification_enabled
};
if !should_perform {