diff --git a/apps/app/src/api/ads.rs b/apps/app/src/api/ads.rs index 7dbcb4455c..463397c652 100644 --- a/apps/app/src/api/ads.rs +++ b/apps/app/src/api/ads.rs @@ -1,4 +1,6 @@ use std::collections::HashSet; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use tauri::plugin::TauriPlugin; use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime}; @@ -14,6 +16,148 @@ pub struct AdsState { } const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie"; +#[cfg(not(target_os = "linux"))] +const ADS_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; + +#[cfg(windows)] +fn ads_user_agent_override_params() -> String { + serde_json::json!({ + "userAgent": ADS_USER_AGENT, + "platform": "Win32", + "userAgentMetadata": { + "brands": [ + { "brand": "Chromium", "version": "128" }, + { "brand": "Google Chrome", "version": "128" }, + { "brand": "Not=A?Brand", "version": "99" }, + ], + "fullVersion": "128.0.0.0", + "fullVersionList": [ + { "brand": "Chromium", "version": "128.0.0.0" }, + { "brand": "Google Chrome", "version": "128.0.0.0" }, + { "brand": "Not=A?Brand", "version": "99.0.0.0" }, + ], + "platform": "Windows", + "platformVersion": "10.0.0", + "architecture": "x86", + "bitness": "64", + "model": "", + "mobile": false, + }, + }) + .to_string() +} + +#[cfg(windows)] +fn configure_ads_cookie_settings( + core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2, +) { + use webview2_com::Microsoft::Web::WebView2::Win32::{ + COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2, + ICoreWebView2_13, ICoreWebView2Profile3, + }; + use windows_core::Interface; + + match core_webview2 + .cast::() + .and_then(|core_webview2| unsafe { core_webview2.Profile() }) + .and_then(|profile| profile.cast::()) + { + Ok(profile) => { + if let Err(error) = unsafe { + profile.SetPreferredTrackingPreventionLevel( + COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, + ) + } { + tracing::warn!( + ?error, + "Failed to disable ads WebView2 tracking prevention" + ); + } + } + Err(error) => { + tracing::warn!( + ?error, + "Failed to access ads WebView2 profile tracking prevention settings" + ); + } + } +} + +fn set_webview_visible( + webview: &tauri::Webview, + _visible: bool, +) { + webview + .with_webview( + #[allow(unused_variables)] + move |wv| { + #[cfg(windows)] + { + let controller = wv.controller(); + unsafe { controller.SetIsVisible(_visible) }.ok(); + } + }, + ) + .ok(); +} + +fn set_webview_visible_for_window( + app: &tauri::AppHandle, + webview: &tauri::Webview, + visible: bool, +) { + let is_minimized = app + .get_window("main") + .and_then(|window| window.is_minimized().ok()) + .unwrap_or(false); + + set_webview_visible(webview, visible && !is_minimized); +} + +fn sync_webview_visibility_for_main_window( + app: &tauri::AppHandle, + main_window: &tauri::Window, + was_minimized: &AtomicBool, +) { + let is_minimized = main_window.is_minimized().unwrap_or(false); + let was = was_minimized.load(Ordering::SeqCst); + + if is_minimized == was { + return; + } + + was_minimized.store(is_minimized, Ordering::SeqCst); + + let ads_visible = if is_minimized { + false + } else { + match app.state::>().try_read() { + Ok(state) => state.shown && !state.modal_shown, + Err(_) => false, + } + }; + + let mut webviews = Vec::new(); + let mut seen_webviews = HashSet::new(); + + for webview in main_window.webviews() { + seen_webviews.insert(webview.label().to_string()); + webviews.push(webview); + } + + for webview in app.webviews().into_values() { + if seen_webviews.insert(webview.label().to_string()) { + webviews.push(webview); + } + } + + for webview in webviews { + let visible = + !is_minimized && (webview.label() != "ads-window" || ads_visible); + + set_webview_visible(&webview, visible); + } +} pub fn init() -> TauriPlugin { tauri::plugin::Builder::::new("ads") @@ -30,10 +174,11 @@ pub fn init() -> TauriPlugin { // visible when we refresh, the Aditude wrapper will not make any ad requests // unless Chromium reports the page as visible. The refresh does not reset the // visibility state. - let app = app.clone(); + let refresh_app = app.clone(); tauri::async_runtime::spawn(async move { loop { - if let Some(webview) = app.webviews().get_mut("ads-window") + if let Some(webview) = + refresh_app.webviews().get_mut("ads-window") { let _ = webview.navigate(AD_LINK.parse().unwrap()); } @@ -43,6 +188,34 @@ pub fn init() -> TauriPlugin { } }); + if let Some(main_window) = app.get_window("main") { + let app_handle = app.clone(); + let event_window = main_window.clone(); + let was_minimized = Arc::new(AtomicBool::new(false)); + + main_window.on_window_event(move |_| { + sync_webview_visibility_for_main_window( + &app_handle, + &event_window, + &was_minimized, + ); + + let delayed_app_handle = app_handle.clone(); + let delayed_event_window = event_window.clone(); + let delayed_was_minimized = was_minimized.clone(); + + tauri::async_runtime::spawn(async move { + tokio::time::sleep(Duration::from_millis(100)).await; + + sync_webview_visibility_for_main_window( + &delayed_app_handle, + &delayed_event_window, + &delayed_was_minimized, + ); + }); + }); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -103,31 +276,38 @@ pub async fn init_ads_window( webview.show().ok(); webview.set_position(position).ok(); webview.set_size(size).ok(); + set_webview_visible_for_window(&app, webview, true); } else { webview.hide().ok(); webview .set_position(PhysicalPosition::new(-1000, -1000)) .ok(); + set_webview_visible(webview, false); } Some(webview.clone()) } else if let Some(window) = app.get_window("main") { + #[cfg(windows)] + let webview_url = + WebviewUrl::External("about:blank".parse().unwrap()); + #[cfg(not(windows))] + let webview_url = WebviewUrl::External(AD_LINK.parse().unwrap()); + let webview = window.add_child( - tauri::webview::WebviewBuilder::new( - "ads-window", - WebviewUrl::External( - AD_LINK.parse().unwrap(), - ), - ) - .initialization_script_for_all_frames(include_str!("ads-init.js")) - // 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 - // originates from a desktop app. - .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") - .zoom_hotkeys_enabled(false) - .transparent(true) - .on_new_window(|_, _| tauri::webview::NewWindowResponse::Deny), + tauri::webview::WebviewBuilder::new("ads-window", webview_url) + .initialization_script_for_all_frames(include_str!( + "ads-init.js" + )) + // 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 + // originates from a desktop app. + .user_agent(ADS_USER_AGENT) + .zoom_hotkeys_enabled(false) + .transparent(true) + .on_new_window(|_, _| { + tauri::webview::NewWindowResponse::Deny + }), // set both the `hide`/`show` state and `position`, // to ensure that the webview is actually shown/hidden if state.shown { @@ -140,15 +320,68 @@ pub async fn init_ads_window( if state.shown { webview.show().ok(); + set_webview_visible_for_window(&app, &webview, true); } else { webview.hide().ok(); + set_webview_visible(&webview, false); } webview.with_webview(#[allow(unused_variables)] |webview2| { #[cfg(windows)] { + use webview2_com::CallDevToolsProtocolMethodCompletedHandler; use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2_8; use windows_core::Interface; + use windows_core::HSTRING; + + let core_webview2 = + unsafe { webview2.controller().CoreWebView2() }; + + if let Ok(core_webview2) = core_webview2 { + configure_ads_cookie_settings(&core_webview2); + + let navigate_webview = core_webview2.clone(); + let handler = + CallDevToolsProtocolMethodCompletedHandler::create( + Box::new(move |result: windows_core::Result<()>, _| { + if let Err(error) = result { + tracing::error!( + ?error, + "Failed to override ads user-agent client hints" + ); + } + + unsafe { + navigate_webview + .Navigate(&HSTRING::from(AD_LINK)) + .ok(); + } + + Ok(()) + }) as Box<_>, + ); + + unsafe { + if let Err(error) = core_webview2 + .CallDevToolsProtocolMethod( + &HSTRING::from( + "Emulation.setUserAgentOverride", + ), + &HSTRING::from( + ads_user_agent_override_params(), + ), + &handler, + ) + { + tracing::error!( + ?error, + "Failed to install ads user-agent client hints override" + ); + + core_webview2.Navigate(&HSTRING::from(AD_LINK)).ok(); + } + } + } let webview2_controller = webview2.controller(); let Ok(webview2_8) = unsafe { webview2_controller.CoreWebView2() } @@ -166,9 +399,9 @@ pub async fn init_ads_window( None }; - let Some(webview) = webview.clone() else { + if webview.is_none() { return Ok(()); - }; + } // tauri::async_runtime::spawn(async move { // loop { @@ -249,6 +482,7 @@ pub async fn show_ads_window( webview.set_size(size).ok(); webview.set_position(position).ok(); webview.show().ok(); + set_webview_visible_for_window(&app, webview, true); } }